Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ linkStyle default opacity:0.5
assets_controllers --> accounts_controller;
assets_controllers --> approval_controller;
assets_controllers --> base_controller;
assets_controllers --> config_registry_controller;
assets_controllers --> controller_utils;
assets_controllers --> core_backend;
assets_controllers --> keyring_controller;
Expand Down
9 changes: 9 additions & 0 deletions packages/assets-controllers/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `@metamask/config-registry-controller` as a dependency ([#9789](https://github.com/MetaMask/core/pull/9789))

### Changed

- **BREAKING:** `TokenRatesControllerMessenger` now requires the `ConfigRegistryController:getNetworkConfigByCaip2ChainId` action to be delegated ([#9789](https://github.com/MetaMask/core/pull/9789))
- `getAssetId`/`CodefiTokenPricesServiceV2` now resolve native asset CAIP-19 IDs from the config registry's `assets.native.assetId` before falling back to the hardcoded `SPOT_PRICES_SUPPORT_INFO` map, then to `NetworkEnablementController`'s `nativeAssetIdentifiers`. This lets new chains get correct native-asset pricing without a `SPOT_PRICES_SUPPORT_INFO` release. `TokenRatesController` seeds this per chain, right before pricing that chain's assets, via `ConfigRegistryController`'s per-chain lookup action, rather than mirroring its entire network map.

## [111.0.0]

### Changed
Expand Down
1 change: 1 addition & 0 deletions packages/assets-controllers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"@metamask/accounts-controller": "^39.0.6",
"@metamask/approval-controller": "^9.0.2",
"@metamask/base-controller": "^9.1.0",
"@metamask/config-registry-controller": "^2.0.1",
"@metamask/contract-metadata": "^2.4.0",
"@metamask/controller-utils": "^12.3.0",
"@metamask/core-backend": "^8.1.1",
Expand Down
81 changes: 79 additions & 2 deletions packages/assets-controllers/src/TokenRatesController.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { deriveStateFromMetadata } from '@metamask/base-controller';
import type { RegistryNetworkConfig } from '@metamask/config-registry-controller';
import { ChainId, toChecksumHexAddress } from '@metamask/controller-utils';
import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger';
import type {
Expand All @@ -23,7 +24,11 @@ import type {
AbstractTokenPricesService,
EvmAssetWithMarketData,
} from './token-prices-service/abstract-token-prices-service.js';
import { ZERO_ADDRESS } from './token-prices-service/codefi-v2.js';
import {
getAssetId,
resetNetworkConfigsCache,
ZERO_ADDRESS,
} from './token-prices-service/codefi-v2.js';
import {
controllerName,
TokenRatesController,
Expand Down Expand Up @@ -75,13 +80,21 @@ function buildTokenRatesControllerMessenger(
'TokensController:getState',
'NetworkController:getState',
'NetworkEnablementController:getState',
'ConfigRegistryController:getNetworkConfigByCaip2ChainId',
],
events: ['TokensController:stateChange', 'NetworkController:stateChange'],
});
return tokenRatesControllerMessenger;
}

describe('TokenRatesController', () => {
afterEach(() => {
// getAssetId's config registry cache is a module-level singleton (shared
// across the whole process, like getSupportedNetworks); reset it so
// tests don't leak state into each other.
resetNetworkConfigsCache();
});

describe('constructor', () => {
it('should set default state', async () => {
await withController(async ({ controller }) => {
Expand Down Expand Up @@ -232,6 +245,50 @@ describe('TokenRatesController', () => {
);
});

it('seeds the config registry cache used by getAssetId for the chains being priced', async () => {
const mockGetNetworkConfigByCaip2ChainId = jest
.fn()
.mockImplementation((caipChainId: string) =>
caipChainId === 'eip155:1'
? { assets: { native: { assetId: 'eip155:1/slip44:61' } } }
: undefined,
);

await withController(
{ mockGetNetworkConfigByCaip2ChainId },
async ({ controller }) => {
await controller.updateExchangeRates([
{ chainId: '0x1', nativeCurrency: 'ETH' },
]);

expect(mockGetNetworkConfigByCaip2ChainId).toHaveBeenCalledWith(
'eip155:1',
);
// getAssetId is the function extracted for client parity with
// fetchTokenPrices; it must reflect the same registry data
// updateExchangeRates just seeded, with no params beyond chain/token.
expect(
getAssetId({ chainId: '0x1', tokenAddress: ZERO_ADDRESS }),
).toBe('eip155:1/slip44:61');
},
);
});

it('does not seed the cache for chains outside the current batch', async () => {
await withController(async ({ controller }) => {
await controller.updateExchangeRates([
{ chainId: '0x89', nativeCurrency: 'MATIC' },
]);

// 0x1 was never part of a priced batch, so it falls back to the
// hardcoded SPOT_PRICES_SUPPORT_INFO entry rather than picking up
// stale or unrelated registry data.
expect(getAssetId({ chainId: '0x1', tokenAddress: ZERO_ADDRESS })).toBe(
'eip155:1/slip44:60',
);
});
});

it('clears stale marketData when isDeprecated toggles to true at runtime', async () => {
const tokenPricesService = buildMockTokenPricesService();
jest.spyOn(tokenPricesService, 'fetchTokenPrices');
Expand Down Expand Up @@ -1458,6 +1515,9 @@ type WithControllerOptions = {
>;
mockTokensControllerState?: Partial<TokensControllerState>;
mockNetworkState?: Partial<NetworkState>;
mockGetNetworkConfigByCaip2ChainId?: (
caipChainId: string,
) => RegistryNetworkConfig | undefined;
};

type WithControllerArgs<ReturnValue> =
Expand All @@ -1477,7 +1537,12 @@ async function withController<ReturnValue>(
...args: WithControllerArgs<ReturnValue>
): Promise<ReturnValue> {
const [{ ...rest }, fn] = args.length === 2 ? args : [{}, args[0]];
const { options, mockTokensControllerState, mockNetworkState } = rest;
const {
options,
mockTokensControllerState,
mockNetworkState,
mockGetNetworkConfigByCaip2ChainId,
} = rest;
const messenger: RootMessenger = new Messenger({
namespace: MOCK_ANY_NAMESPACE,
});
Expand Down Expand Up @@ -1512,6 +1577,18 @@ async function withController<ReturnValue>(
}),
);

// Register ConfigRegistryController:getNetworkConfigByCaip2ChainId handler
const defaultGetNetworkConfigByCaip2ChainId = (): undefined => undefined;
messenger.registerActionHandler(
'ConfigRegistryController:getNetworkConfigByCaip2ChainId',
jest
.fn()
.mockImplementation(
mockGetNetworkConfigByCaip2ChainId ??
defaultGetNetworkConfigByCaip2ChainId,
),
);

const controller = new TokenRatesController({
tokenPricesService: buildMockTokenPricesService(),
messenger: buildTokenRatesControllerMessenger(messenger),
Expand Down
44 changes: 42 additions & 2 deletions packages/assets-controllers/src/TokenRatesController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
ControllerStateChangeEvent,
StateMetadata,
} from '@metamask/base-controller';
import type { ConfigRegistryControllerGetNetworkConfigByCaip2ChainIdAction } from '@metamask/config-registry-controller';
import { toChecksumHexAddress } from '@metamask/controller-utils';
import type { Messenger } from '@metamask/messenger';
import type {
Expand All @@ -12,14 +13,22 @@ import type {
import type { NetworkEnablementControllerGetStateAction } from '@metamask/network-enablement-controller';
import { StaticIntervalPollingController } from '@metamask/polling-controller';
import type { Hex } from '@metamask/utils';
import {
hexToNumber,
KnownCaipNamespace,
toCaipChainId,
} from '@metamask/utils';
import { isEqual } from 'lodash';

import {
reduceInBatchesSerially,
TOKEN_PRICES_BATCH_SIZE,
} from './assetsUtil.js';
import type { AbstractTokenPricesService } from './token-prices-service/abstract-token-prices-service.js';
import { getNativeTokenAddress } from './token-prices-service/codefi-v2.js';
import {
getNativeTokenAddress,
setNetworkConfig,
} from './token-prices-service/codefi-v2.js';
import { TokenRwaData } from './token-service.js';
import type {
TokensControllerGetStateAction,
Expand Down Expand Up @@ -98,7 +107,8 @@ type ChainIdAndNativeCurrency = {
export type AllowedActions =
| TokensControllerGetStateAction
| NetworkControllerGetStateAction
| NetworkEnablementControllerGetStateAction;
| NetworkEnablementControllerGetStateAction
| ConfigRegistryControllerGetNetworkConfigByCaip2ChainIdAction;

/**
* The external events available to the {@link TokenRatesController}.
Expand Down Expand Up @@ -380,6 +390,32 @@ export class TokenRatesController extends StaticIntervalPollingController<TokenR
}
}

/**
* Seeds the shared config registry cache used by getAssetId (the primary
* source of native asset CAIP-19 IDs) for the given chains, via
* ConfigRegistryController's per-chain `getNetworkConfigByCaip2ChainId`
* action. Called right before pricing a chain's assets, so lookups always
* reflect the registry's current state without needing to mirror its
* entire network map or subscribe to unrelated state changes.
*
* @param chainIds - The hexadecimal chain IDs about to be priced.
*/
#seedNetworkConfigs(chainIds: Iterable<Hex>): void {
for (const chainId of new Set(chainIds)) {
const caipChainId = toCaipChainId(
KnownCaipNamespace.Eip155,
hexToNumber(chainId).toString(),
);
setNetworkConfig(
caipChainId,
this.messenger.call(
'ConfigRegistryController:getNetworkConfigByCaip2ChainId',
caipChainId,
),
);
}
}

/**
* Get the tokens for the given chain.
*
Expand Down Expand Up @@ -448,6 +484,10 @@ export class TokenRatesController extends StaticIntervalPollingController<TokenR
return;
}

this.#seedNetworkConfigs(
chainIdAndNativeCurrency.map(({ chainId }) => chainId),
);

const marketData: Record<Hex, Record<Hex, MarketDataDetails>> = {};
const assetsByNativeCurrency: Record<
string,
Expand Down
Loading