Skip to content

Commit acbb6c0

Browse files
authored
feat: price API v3 upgrade (#7119)
## Explanation <!-- Thanks for your contribution! Take a moment to answer these questions so that reviewers have the information they need to properly understand your changes: * What is the current state of things and why does it need to change? * What is the solution your changes offer and how does it work? * Are there any changes whose purpose might not obvious to those unfamiliar with the domain? * If your primary goal was to update one package but you found you had to update another one along the way, why did you do so? * If you had to upgrade a dependency, why did you do so? --> Upgrade to Price API v3 Preview PR for extension: MetaMask/metamask-extension#37741 Preview PR for mobile: MetaMask/metamask-mobile#22876 ## References <!-- Are there any issues that this pull request is tied to? Are there other links that reviewers should consult to understand these changes better? Are there client or consumer pull requests to adopt any breaking changes? For example: * Fixes #12345 * Related to #67890 --> Fixes https://consensyssoftware.atlassian.net/browse/ASSETS-1746 ## 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/contributing.md#updating-changelogs), highlighting breaking changes as necessary - [X] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Upgrades token price fetching to Price API v3, updates service/types and controllers to new assets-based API, and removes legacy polling paths. > > - **Price API Integration (v3)** > - `CodefiTokenPricesServiceV2`: add v3 `/v3/spot-prices` with CAIP-19 `assetId`, fallback to v2 per-chain endpoint; expand `SUPPORTED_CURRENCIES`; introduce `SPOT_PRICES_SUPPORT_INFO`; handle native token addresses; circuit-breaker/degraded handling retained. > - **Types/Interfaces**: > - BREAKING: `AbstractTokenPricesService.fetchTokenPrices` now accepts `assets: { chainId, tokenAddress }[]` and returns `EvmAssetWithMarketData[]` (replaces address->object map and adds `assetId`). > - Add `EvmAssetAddressWithChain`, `EvmAssetWithId`, `EvmAssetWithMarketData`. > - **Controllers**: > - `TokenRatesController`: fetch prices grouped by native currency; support unsupported native currencies via USD conversion; include native token in queries; remove legacy polling/state/event deps; handle network deletions; enable/disable gating maintained. > - `CurrencyRateController`: on API failure, fallback to spot prices via `fetchTokenPrices` using native token asset; map multiple networks by native currency. > - `TokenSearchDiscoveryDataController`: adapt to new `fetchTokenPrices` response (array) and asset inputs. > - **Utils**: > - `assetsUtil.fetchTokenContractExchangeRates`: switch to assets-based batching (includes native token) and map array response. > - **Tests/Changelog**: > - Update tests to new arguments/returns and behaviors; changelog notes BREAKING changes and polling cleanup. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit e306214. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent fcb43e6 commit acbb6c0

File tree

13 files changed

+1822
-3772
lines changed

13 files changed

+1822
-3772
lines changed

packages/assets-controllers/CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Changed
11+
12+
- **BREAKING:** Update `spot-prices` endpoint to use Price API v3 ([#7119](https://github.com/MetaMask/core/pull/7119))
13+
- Update `AbstractTokenPricesService.fetchTokenPrices` arguments and return type
14+
- Update `CodefiTokenPricesServiceV2` list of supported currencies
15+
- Update `TokenRatesController` to fetch prices by native currency instead of by chain
16+
- Remove legacy polling code and unused events from `TokenRatesController`
17+
1018
## [90.0.0]
1119

1220
### Added

packages/assets-controllers/src/CurrencyRateController.test.ts

Lines changed: 72 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ function buildMockTokenPricesService(
4444
): AbstractTokenPricesService {
4545
return {
4646
async fetchTokenPrices() {
47-
return {};
47+
return [];
4848
},
4949
async fetchExchangeRates() {
5050
return {};
@@ -1138,12 +1138,15 @@ describe('CurrencyRateController', () => {
11381138
// Mock fetchTokenPrices to return token prices
11391139
jest
11401140
.spyOn(tokenPricesService, 'fetchTokenPrices')
1141-
.mockImplementation(async ({ chainId }) => {
1142-
if (chainId === '0x1') {
1143-
return {
1144-
'0x0000000000000000000000000000000000000000': {
1141+
.mockImplementation(async ({ assets }) => {
1142+
// eslint-disable-next-line jest/no-conditional-in-test
1143+
if (assets.some((asset) => asset.chainId === '0x1')) {
1144+
return [
1145+
{
11451146
currency: 'usd',
11461147
tokenAddress: '0x0000000000000000000000000000000000000000',
1148+
chainId: assets[0].chainId,
1149+
assetId: 'xx:yy/aa:bb',
11471150
price: 2500.5,
11481151
pricePercentChange1d: 0,
11491152
priceChange1d: 0,
@@ -1163,13 +1166,16 @@ describe('CurrencyRateController', () => {
11631166
pricePercentChange7d: 100,
11641167
totalVolume: 100,
11651168
},
1166-
};
1169+
];
11671170
}
1168-
if (chainId === '0x89') {
1169-
return {
1170-
'0x0000000000000000000000000000000000001010': {
1171+
// eslint-disable-next-line jest/no-conditional-in-test
1172+
if (assets.some((asset) => asset.chainId === '0x89')) {
1173+
return [
1174+
{
11711175
currency: 'usd',
11721176
tokenAddress: '0x0000000000000000000000000000000000001010',
1177+
chainId: assets[0].chainId,
1178+
assetId: 'xx:yy/aa:bb',
11731179
price: 0.85,
11741180
pricePercentChange1d: 0,
11751181
priceChange1d: 0,
@@ -1189,9 +1195,9 @@ describe('CurrencyRateController', () => {
11891195
pricePercentChange7d: 100,
11901196
totalVolume: 100,
11911197
},
1192-
};
1198+
];
11931199
}
1194-
return {};
1200+
return [];
11951201
});
11961202

11971203
// Make crypto compare also fail by not mocking it (no nock setup)
@@ -1255,12 +1261,21 @@ describe('CurrencyRateController', () => {
12551261

12561262
const fetchTokenPricesSpy = jest
12571263
.spyOn(tokenPricesService, 'fetchTokenPrices')
1258-
.mockImplementation(async ({ chainId }) => {
1259-
if (chainId === '0x1' || chainId === '0xaa36a7') {
1260-
return {
1261-
'0x0000000000000000000000000000000000000000': {
1264+
.mockImplementation(async ({ assets }) => {
1265+
// eslint-disable-next-line jest/no-conditional-in-test
1266+
if (
1267+
assets.some(
1268+
(asset) =>
1269+
// eslint-disable-next-line jest/no-conditional-in-test
1270+
asset.chainId === '0x1' || asset.chainId === '0xaa36a7',
1271+
)
1272+
) {
1273+
return [
1274+
{
12621275
currency: 'usd',
12631276
tokenAddress: '0x0000000000000000000000000000000000000000',
1277+
chainId: assets[0].chainId,
1278+
assetId: 'xx:yy/aa:bb',
12641279
price: 2500.5,
12651280
pricePercentChange1d: 0,
12661281
priceChange1d: 0,
@@ -1280,9 +1295,9 @@ describe('CurrencyRateController', () => {
12801295
pricePercentChange7d: 100,
12811296
totalVolume: 100,
12821297
},
1283-
};
1298+
];
12841299
}
1285-
return {};
1300+
return [];
12861301
});
12871302

12881303
const controller = new CurrencyRateController({
@@ -1296,8 +1311,12 @@ describe('CurrencyRateController', () => {
12961311
// Should only call fetchTokenPrices once, using first matching chainId (line 255)
12971312
expect(fetchTokenPricesSpy).toHaveBeenCalledTimes(1);
12981313
expect(fetchTokenPricesSpy).toHaveBeenCalledWith({
1299-
chainId: '0x1', // First chainId with ETH as native currency
1300-
tokenAddresses: [],
1314+
assets: [
1315+
{
1316+
chainId: '0x1',
1317+
tokenAddress: '0x0000000000000000000000000000000000000000',
1318+
},
1319+
],
13011320
currency: 'usd',
13021321
});
13031322

@@ -1338,13 +1357,16 @@ describe('CurrencyRateController', () => {
13381357

13391358
jest
13401359
.spyOn(tokenPricesService, 'fetchTokenPrices')
1341-
.mockImplementation(async ({ chainId }) => {
1342-
if (chainId === '0x1') {
1360+
.mockImplementation(async ({ assets }) => {
1361+
// eslint-disable-next-line jest/no-conditional-in-test
1362+
if (assets.some((asset) => asset.chainId === '0x1')) {
13431363
// ETH succeeds
1344-
return {
1345-
'0x0000000000000000000000000000000000000000': {
1364+
return [
1365+
{
13461366
currency: 'usd',
13471367
tokenAddress: '0x0000000000000000000000000000000000000000',
1368+
chainId: assets[0].chainId,
1369+
assetId: 'xx:yy/aa:bb',
13481370
price: 2500.5,
13491371
pricePercentChange1d: 0,
13501372
priceChange1d: 0,
@@ -1364,7 +1386,7 @@ describe('CurrencyRateController', () => {
13641386
pricePercentChange7d: 100,
13651387
totalVolume: 100,
13661388
},
1367-
};
1389+
];
13681390
}
13691391
// POL fails
13701392
throw new Error('Failed to fetch POL price');
@@ -1427,7 +1449,7 @@ describe('CurrencyRateController', () => {
14271449
.mockRejectedValue(new Error('Price API failed'));
14281450

14291451
// Return empty object (no token price)
1430-
jest.spyOn(tokenPricesService, 'fetchTokenPrices').mockResolvedValue({});
1452+
jest.spyOn(tokenPricesService, 'fetchTokenPrices').mockResolvedValue([]);
14311453

14321454
const controller = new CurrencyRateController({
14331455
messenger,
@@ -1475,12 +1497,15 @@ describe('CurrencyRateController', () => {
14751497

14761498
const fetchTokenPricesSpy = jest
14771499
.spyOn(tokenPricesService, 'fetchTokenPrices')
1478-
.mockImplementation(async ({ chainId }) => {
1479-
if (chainId === '0x1') {
1480-
return {
1481-
'0x0000000000000000000000000000000000000000': {
1500+
.mockImplementation(async ({ assets }) => {
1501+
// eslint-disable-next-line jest/no-conditional-in-test
1502+
if (assets.some((asset) => asset.chainId === '0x1')) {
1503+
return [
1504+
{
14821505
currency: 'usd',
14831506
tokenAddress: '0x0000000000000000000000000000000000000000',
1507+
chainId: assets[0].chainId,
1508+
assetId: 'xx:yy/aa:bb',
14841509
price: 2500.5,
14851510
pricePercentChange1d: 0,
14861511
priceChange1d: 0,
@@ -1500,9 +1525,9 @@ describe('CurrencyRateController', () => {
15001525
pricePercentChange7d: 100,
15011526
totalVolume: 100,
15021527
},
1503-
};
1528+
];
15041529
}
1505-
return {};
1530+
return [];
15061531
});
15071532

15081533
const controller = new CurrencyRateController({
@@ -1517,8 +1542,12 @@ describe('CurrencyRateController', () => {
15171542
// Should only call fetchTokenPrices for ETH, not BNB (line 252: if chainIds.length > 0)
15181543
expect(fetchTokenPricesSpy).toHaveBeenCalledTimes(1);
15191544
expect(fetchTokenPricesSpy).toHaveBeenCalledWith({
1520-
chainId: '0x1',
1521-
tokenAddresses: [],
1545+
assets: [
1546+
{
1547+
chainId: '0x1',
1548+
tokenAddress: '0x0000000000000000000000000000000000000000',
1549+
},
1550+
],
15221551
currency: 'usd',
15231552
});
15241553

@@ -1562,10 +1591,11 @@ describe('CurrencyRateController', () => {
15621591

15631592
const fetchTokenPricesSpy = jest
15641593
.spyOn(tokenPricesService, 'fetchTokenPrices')
1565-
.mockResolvedValue({
1566-
'0x0000000000000000000000000000000000001010': {
1594+
.mockResolvedValue([
1595+
{
15671596
currency: 'usd',
15681597
tokenAddress: '0x0000000000000000000000000000000000001010',
1598+
chainId: '0x89',
15691599
price: 0.85,
15701600
pricePercentChange1d: 0,
15711601
priceChange1d: 0,
@@ -1585,7 +1615,7 @@ describe('CurrencyRateController', () => {
15851615
pricePercentChange7d: 100,
15861616
totalVolume: 100,
15871617
},
1588-
});
1618+
]);
15891619

15901620
const controller = new CurrencyRateController({
15911621
messenger,
@@ -1597,8 +1627,12 @@ describe('CurrencyRateController', () => {
15971627

15981628
// Should use Polygon's native token address (line 269)
15991629
expect(fetchTokenPricesSpy).toHaveBeenCalledWith({
1600-
chainId: '0x89',
1601-
tokenAddresses: [],
1630+
assets: [
1631+
{
1632+
chainId: '0x89',
1633+
tokenAddress: '0x0000000000000000000000000000000000001010',
1634+
},
1635+
],
16021636
currency: 'usd',
16031637
});
16041638

packages/assets-controllers/src/CurrencyRateController.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,12 +259,15 @@ export class CurrencyRateController extends StaticIntervalPollingController<Curr
259259
const nativeTokenAddress = getNativeTokenAddress(chainId);
260260
// Pass empty array as fetchTokenPrices automatically includes the native token address
261261
const tokenPrices = await this.#tokenPricesService.fetchTokenPrices({
262-
chainId,
263-
tokenAddresses: [],
262+
assets: [{ chainId, tokenAddress: nativeTokenAddress }],
264263
currency: currentCurrency,
265264
});
266265

267-
const tokenPrice = tokenPrices[nativeTokenAddress];
266+
const tokenPrice = tokenPrices.find(
267+
(item) =>
268+
item.tokenAddress.toLowerCase() ===
269+
nativeTokenAddress.toLowerCase(),
270+
);
268271

269272
return {
270273
nativeCurrency,

0 commit comments

Comments
 (0)