Skip to content

fetch both selectedcurrency and usd prices#8123

Merged
bergarces merged 11 commits intomainfrom
assets-controller-usd-prices
Mar 9, 2026
Merged

fetch both selectedcurrency and usd prices#8123
bergarces merged 11 commits intomainfrom
assets-controller-usd-prices

Conversation

@bergarces
Copy link
Copy Markdown
Contributor

@bergarces bergarces commented Mar 5, 2026

Explanation

Starts including usdPrice for fungible asset prices. This might mean an extra call to price api if the selected currency is different.

It also updates functions that make use of the prices for state migration.

References

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

Note

Medium Risk
Changes price fetching and exchange-rate formatting used by bridge and transaction-pay consumers; incorrect assumptions about usdPrice, lastUpdated units, or missing network/native-currency config could cause missing or wrong rates in non-USD scenarios.

Overview
Updates asset price data to always carry both the selected-currency price (price) and the corresponding USD price (usdPrice), and formalizes price shapes via assetPriceType (e.g. FungibleAssetPrice). PriceDataSource now fetches spot prices in the selected currency and USD (in parallel when needed) and merges results into responses/state.

Refactors formatExchangeRatesForBridge (and the transaction-pay legacy formatter via formatStateForTransactionPay) to stop using usdToSelectedCurrencyRate and instead read price/usdPrice directly; it also tightens behavior by returning empty rates for unknown fiat currencies and skipping EVM entries when required native-currency/network data is missing. Controller methods getExchangeRatesForBridge and getStateForTransactionPay drop the optional conversion-rate parameter, and tests are updated accordingly.

Written by Cursor Bugbot for commit 84240e4. This will update automatically on new commits. Configure here.

@bergarces bergarces requested a review from a team as a code owner March 5, 2026 16:03
}

const prices: Record<Caip19AssetId, AssetPrice> = {};

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the logic that validates the result from the API here and added an extra check to verify that the USD price is also there.

This also allows us to remove this logic from the two places that were calling this private method.

Comment thread packages/assets-controller/src/data-sources/PriceDataSource.ts
Comment thread packages/assets-controller/src/data-sources/PriceDataSource.ts
export type AssetPrice =
| FungibleAssetPrice
| NFTAssetPrice
| (BaseAssetPrice & { [key: string]: Json });
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have made some minor adjustment to this type, which also addresses this TODO I added to the client (https://github.com/MetaMask/metamask-extension/blob/main/shared/modules/selectors/assets-migration.ts#L715).

It's very convenient to have a type discriminator field with unions of very different types, as that allows typescript to to perform type guards easily.

For that reason, I added an assetPriceType with a specific value for each type and removed the generic (BaseAssetPrice & { [key: string]: Json }), as it is not used anywhere and it's unlikely it'll ever be needed. If it ever is needed, we can add it then and create a new type for it.

Comment thread packages/assets-controller/src/data-sources/PriceDataSource.ts
@@ -190,7 +190,7 @@ export type AssetMetadata =
* Base price attributes.
*/
export type BaseAssetPrice = {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make all the price types extend this one, which is the absolutely bare minimum needed.

@bergarces bergarces requested a review from a team as a code owner March 9, 2026 09:58
@bergarces bergarces force-pushed the assets-controller-usd-prices branch from 860f2a6 to 635bdf9 Compare March 9, 2026 10:00
data !== null &&
'price' in data &&
typeof (data as SpotPriceMarketData).price === 'number'
typeof data.price === 'number'
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed unnecessary casting.

In any case, this function does not validate well enough the result from fetch. If we have any package to validate schemas we should consider using it instead.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

++ on validation 🤞🏾

Otherwise can be a nice improvement

(personal thoughts - we have openAPI specs on our services. This is a source of truth that can drive backend and front-end changes. It is totally doable to perform type-gen and validators from openAPI specs)

response.assetsPrice = {
...(response.assetsPrice ?? {}),
...spotPrices,
};
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validation is moved to #fetchSpotPrices

response.assetsPrice = {
...(response.assetsPrice ?? {}),
...spotPrices,
};
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validation is moved to ``#fetchSpotPrices`

});

it('skips entries with invalid or negative price', () => {
it('skips entries with negative price', () => {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We cannot not trust and verify for a second type every piece of data we have in our controller state. If price data is not valid, we are not storing it..

acc[assetId as Caip19AssetId] = priceData;
}
return acc;
}, {});
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filter in fungible asset prices, as those are the only that we need and have the type signature we require.

marketData: {},
currentCurrency: selectedCurrency,
};
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We cannot just return usd prices if the selected currency is not in that list, as it might display wrong currency symbols with a price that does not correspond.

I have checked, and the list on that constant coincides with the currencies we offer in the client.

const conversionTime =
lastUpdated > 1e12 ? lastUpdated / 1000 : lastUpdated;
const expirationTime = conversionTime + expirationOffset;
const lastUpdatedInSeconds = lastUpdated / 1000;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Price data is always in ms as it is always built with Date.now(), so we should always divide it by 1000 to get the value in seconds.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

: null;
if (!baseMeta) {
continue;
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not need to double-check the type here. If it comes from our controller and it matches the type, it is correct.

@bergarces bergarces force-pushed the assets-controller-usd-prices branch from 4919168 to 0c1e527 Compare March 9, 2026 11:05
@bergarces
Copy link
Copy Markdown
Contributor Author

@metamaskbot publish-previews

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 9, 2026

Preview builds have been published. See these instructions for more information about preview builds.

Expand for full list of packages and versions.
{
  "@metamask-previews/account-tree-controller": "4.1.1-preview-84240e41c",
  "@metamask-previews/accounts-controller": "36.0.1-preview-84240e41c",
  "@metamask-previews/address-book-controller": "7.0.1-preview-84240e41c",
  "@metamask-previews/ai-controllers": "0.2.0-preview-84240e41c",
  "@metamask-previews/analytics-controller": "1.0.0-preview-84240e41c",
  "@metamask-previews/analytics-data-regulation-controller": "0.0.0-preview-84240e41c",
  "@metamask-previews/announcement-controller": "8.0.0-preview-84240e41c",
  "@metamask-previews/app-metadata-controller": "2.0.0-preview-84240e41c",
  "@metamask-previews/approval-controller": "8.0.0-preview-84240e41c",
  "@metamask-previews/assets-controller": "2.2.0-preview-84240e41c",
  "@metamask-previews/assets-controllers": "100.1.0-preview-84240e41c",
  "@metamask-previews/base-controller": "9.0.0-preview-84240e41c",
  "@metamask-previews/base-data-service": "0.0.0-preview-84240e41c",
  "@metamask-previews/bridge-controller": "68.0.0-preview-84240e41c",
  "@metamask-previews/bridge-status-controller": "68.0.0-preview-84240e41c",
  "@metamask-previews/build-utils": "3.0.4-preview-84240e41c",
  "@metamask-previews/chain-agnostic-permission": "1.4.0-preview-84240e41c",
  "@metamask-previews/claims-controller": "0.4.2-preview-84240e41c",
  "@metamask-previews/client-controller": "1.0.0-preview-84240e41c",
  "@metamask-previews/compliance-controller": "1.0.1-preview-84240e41c",
  "@metamask-previews/composable-controller": "12.0.0-preview-84240e41c",
  "@metamask-previews/config-registry-controller": "0.1.0-preview-84240e41c",
  "@metamask-previews/connectivity-controller": "0.1.0-preview-84240e41c",
  "@metamask-previews/controller-utils": "11.19.0-preview-84240e41c",
  "@metamask-previews/core-backend": "6.0.0-preview-84240e41c",
  "@metamask-previews/delegation-controller": "2.0.1-preview-84240e41c",
  "@metamask-previews/earn-controller": "11.1.1-preview-84240e41c",
  "@metamask-previews/eip-5792-middleware": "3.0.0-preview-84240e41c",
  "@metamask-previews/eip-7702-internal-rpc-middleware": "0.1.0-preview-84240e41c",
  "@metamask-previews/eip1193-permission-middleware": "1.0.3-preview-84240e41c",
  "@metamask-previews/ens-controller": "19.0.3-preview-84240e41c",
  "@metamask-previews/error-reporting-service": "3.0.1-preview-84240e41c",
  "@metamask-previews/eth-block-tracker": "15.0.1-preview-84240e41c",
  "@metamask-previews/eth-json-rpc-middleware": "23.1.0-preview-84240e41c",
  "@metamask-previews/eth-json-rpc-provider": "6.0.0-preview-84240e41c",
  "@metamask-previews/foundryup": "1.0.1-preview-84240e41c",
  "@metamask-previews/gas-fee-controller": "26.0.3-preview-84240e41c",
  "@metamask-previews/gator-permissions-controller": "2.0.0-preview-84240e41c",
  "@metamask-previews/geolocation-controller": "0.1.1-preview-84240e41c",
  "@metamask-previews/json-rpc-engine": "10.2.3-preview-84240e41c",
  "@metamask-previews/json-rpc-middleware-stream": "8.0.8-preview-84240e41c",
  "@metamask-previews/keyring-controller": "25.1.0-preview-84240e41c",
  "@metamask-previews/logging-controller": "7.0.1-preview-84240e41c",
  "@metamask-previews/message-manager": "14.1.0-preview-84240e41c",
  "@metamask-previews/messenger": "0.3.0-preview-84240e41c",
  "@metamask-previews/multichain-account-service": "7.0.0-preview-84240e41c",
  "@metamask-previews/multichain-api-middleware": "1.2.7-preview-84240e41c",
  "@metamask-previews/multichain-network-controller": "3.0.4-preview-84240e41c",
  "@metamask-previews/multichain-transactions-controller": "7.0.1-preview-84240e41c",
  "@metamask-previews/name-controller": "9.0.0-preview-84240e41c",
  "@metamask-previews/network-controller": "30.0.0-preview-84240e41c",
  "@metamask-previews/network-enablement-controller": "4.2.0-preview-84240e41c",
  "@metamask-previews/notification-services-controller": "22.0.0-preview-84240e41c",
  "@metamask-previews/permission-controller": "12.2.0-preview-84240e41c",
  "@metamask-previews/permission-log-controller": "5.0.0-preview-84240e41c",
  "@metamask-previews/perps-controller": "1.0.0-preview-84240e41c",
  "@metamask-previews/phishing-controller": "16.3.0-preview-84240e41c",
  "@metamask-previews/polling-controller": "16.0.3-preview-84240e41c",
  "@metamask-previews/preferences-controller": "22.1.0-preview-84240e41c",
  "@metamask-previews/profile-metrics-controller": "3.0.1-preview-84240e41c",
  "@metamask-previews/profile-sync-controller": "27.1.0-preview-84240e41c",
  "@metamask-previews/ramps-controller": "10.2.0-preview-84240e41c",
  "@metamask-previews/rate-limit-controller": "7.0.0-preview-84240e41c",
  "@metamask-previews/remote-feature-flag-controller": "4.1.0-preview-84240e41c",
  "@metamask-previews/sample-controllers": "4.0.3-preview-84240e41c",
  "@metamask-previews/seedless-onboarding-controller": "8.1.0-preview-84240e41c",
  "@metamask-previews/selected-network-controller": "26.0.3-preview-84240e41c",
  "@metamask-previews/shield-controller": "5.0.1-preview-84240e41c",
  "@metamask-previews/signature-controller": "39.0.4-preview-84240e41c",
  "@metamask-previews/storage-service": "1.0.0-preview-84240e41c",
  "@metamask-previews/subscription-controller": "6.0.0-preview-84240e41c",
  "@metamask-previews/transaction-controller": "62.20.0-preview-84240e41c",
  "@metamask-previews/transaction-pay-controller": "16.4.0-preview-84240e41c",
  "@metamask-previews/user-operation-controller": "41.0.3-preview-84240e41c"
}

### Fixed

- `getStateForTransactionPay()` and `formatStateForTransactionPay()` now accept optional `usdToSelectedCurrencyRate` (rate: 1 USD = N units of selected currency). When `selectedCurrency` is not USD, passing this rate ensures `currencyRates` and `marketData` use correct user-currency vs USD values; previously they incorrectly used USD price for both
- `formatExchangeRatesForBridge` and `formatStateForTransactionPay` now read both `price` (selected currency) and `usdPrice` (USD) directly from asset price data ([#8123](https://github.com/MetaMask/core/pull/8123))
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we haven't released this yet, it is not breaking. I'm just amending the changelog.

@@ -621,99 +634,33 @@ describe('AssetsController', () => {
});

describe('getExchangeRatesForBridge', () => {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not need to re-test formatExchangeRatesForBridge logic here. That function is imported from the utils module and is already being tested, we just need to check that it is being called with the correct parameters.

getExchangeRatesForBridge(options?: {
usdToSelectedCurrencyRate?: number;
}): BridgeExchangeRatesFormat {
getExchangeRatesForBridge(): BridgeExchangeRatesFormat {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is good and exactly what we need here 👍

@bergarces bergarces added this pull request to the merge queue Mar 9, 2026
Merged via the queue into main with commit e3e7bab Mar 9, 2026
322 checks passed
@bergarces bergarces deleted the assets-controller-usd-prices branch March 9, 2026 13:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants