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
5 changes: 5 additions & 0 deletions packages/assets-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `currentCurrency` state (ISO 4217 code, default `'usd'`) and `setCurrentCurrency(currentCurrency)` to `AssetsController`. Changing the currency updates state and triggers a one-off price refetch so displayed prices use the new currency ([#7991](https://github.com/MetaMask/core/pull/7991))

### Changed

- `PriceDataSourceOptions` now requires `getCurrentCurrency: () => SupportedCurrency` instead of optional `currency?: SupportedCurrency` ([#7991](https://github.com/MetaMask/core/pull/7991))
- Refactor data source tests to use shared `MockAssetControllerMessenger` fixture ([#7958](https://github.com/MetaMask/core/pull/7958))
- Export `STAKING_INTERFACE` from the staked balance fetcher for use with the staking contract ABI.
- `StakedBalanceDataSource` teardown now uses the messenger's `clearEventSubscriptions`; custom messenger implementations must support it for correct cleanup.
Expand Down
54 changes: 54 additions & 0 deletions packages/assets-controller/src/AssetsController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ describe('AssetsController', () => {
assetsPrice: {},
customAssets: {},
assetPreferences: {},
selectedCurrency: 'usd',
});
});
});
Expand All @@ -182,6 +183,7 @@ describe('AssetsController', () => {
assetsPrice: {},
customAssets: {},
assetPreferences: {},
selectedCurrency: 'usd',
});
});
});
Expand All @@ -198,6 +200,7 @@ describe('AssetsController', () => {
},
assetsBalance: {},
customAssets: {},
selectedCurrency: 'eur',
};

await withController({ state: initialState }, ({ controller }) => {
Expand All @@ -207,6 +210,7 @@ describe('AssetsController', () => {
name: 'USD Coin',
decimals: 6,
});
expect(controller.state.selectedCurrency).toBe('eur');
});
});

Expand Down Expand Up @@ -254,6 +258,7 @@ describe('AssetsController', () => {
assetsBalance: {},
assetsPrice: {},
customAssets: {},
selectedCurrency: 'usd',
});

// Action handlers should NOT be registered when disabled
Expand All @@ -275,6 +280,7 @@ describe('AssetsController', () => {
assetsBalance: {},
assetsPrice: {},
customAssets: {},
selectedCurrency: 'usd',
});

// Action handlers should be registered
Expand Down Expand Up @@ -724,6 +730,54 @@ describe('AssetsController', () => {
});
});

describe('setSelectedCurrency', () => {
it('updates selectedCurrency in state', async () => {
await withController(({ controller }) => {
expect(controller.state.selectedCurrency).toBe('usd');

controller.setSelectedCurrency('eur');
expect(controller.state.selectedCurrency).toBe('eur');

controller.setSelectedCurrency('gbp');
expect(controller.state.selectedCurrency).toBe('gbp');
});
});

it('returns early when new currency is same as current', async () => {
await withController(({ controller }) => {
expect(controller.state.selectedCurrency).toBe('usd');

const getAssetsSpy = jest.spyOn(controller, 'getAssets');

controller.setSelectedCurrency('usd');

expect(controller.state.selectedCurrency).toBe('usd');
expect(getAssetsSpy).not.toHaveBeenCalled();

getAssetsSpy.mockRestore();
});
});

it('calls getAssets with forceUpdate and price dataType to refresh prices', async () => {
await withController(({ controller }) => {
const getAssetsSpy = jest.spyOn(controller, 'getAssets');

controller.setSelectedCurrency('eur');

expect(getAssetsSpy).toHaveBeenCalledTimes(1);
expect(getAssetsSpy).toHaveBeenCalledWith(
expect.any(Array),
expect.objectContaining({
forceUpdate: true,
dataTypes: ['price'],
}),
);

getAssetsSpy.mockRestore();
});
});
});

describe('events', () => {
it('publishes balanceChanged event when balance updates', async () => {
await withController(async ({ controller, messenger }) => {
Expand Down
44 changes: 44 additions & 0 deletions packages/assets-controller/src/AssetsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
ApiPlatformClient,
BackendWebSocketServiceActions,
BackendWebSocketServiceEvents,
SupportedCurrency,
} from '@metamask/core-backend';
import type {
KeyringControllerLockEvent,
Expand Down Expand Up @@ -143,6 +144,8 @@ export type AssetsControllerState = {
customAssets: { [accountId: string]: Caip19AssetId[] };
/** UI preferences per asset (e.g. hidden) */
assetPreferences: { [assetId: string]: AssetPreferences };
/** Currently-active ISO 4217 currency code */
selectedCurrency: SupportedCurrency;
};

/**
Expand All @@ -157,6 +160,7 @@ export function getDefaultAssetsControllerState(): AssetsControllerState {
assetsPrice: {},
customAssets: {},
assetPreferences: {},
selectedCurrency: 'usd',
};
}

Expand Down Expand Up @@ -350,6 +354,12 @@ const stateMetadata: StateMetadata<AssetsControllerState> = {
includeInDebugSnapshot: false,
usedInUi: true,
},
selectedCurrency: {
persist: true,
includeInStateLogs: false,
includeInDebugSnapshot: false,
usedInUi: true,
},
};

// ============================================================================
Expand Down Expand Up @@ -605,6 +615,7 @@ export class AssetsController extends BaseController<
});
this.#priceDataSource = new PriceDataSource({
queryApiClient,
getSelectedCurrency: (): SupportedCurrency => this.state.selectedCurrency,
...priceDataSourceConfig,
});
this.#detectionMiddleware = new DetectionMiddleware();
Expand Down Expand Up @@ -1133,6 +1144,39 @@ export class AssetsController extends BaseController<
});
}

// ============================================================================
// CURRENT CURRENCY MANAGEMENT
// ============================================================================

/**
* Set the current currency.
*
* @param selectedCurrency - The ISO 4217 currency code to set.
*/
setSelectedCurrency(selectedCurrency: SupportedCurrency): void {
const previousCurrency = this.state.selectedCurrency;

if (previousCurrency === selectedCurrency) {
return;
}

this.update((state) => {
state.selectedCurrency = selectedCurrency;
});

log('Current currency changed', {
previousCurrency,
selectedCurrency,
});

this.getAssets(this.#selectedAccounts, {
forceUpdate: true,
dataTypes: ['price'],
}).catch((error) => {
log('Failed to fetch asset prices after current currency change', error);
});
}

// ============================================================================
// SUBSCRIPTIONS
// ============================================================================
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { SupportedCurrency } from '@metamask/core-backend';
import type { InternalAccount } from '@metamask/keyring-internal-api';

import type { PriceDataSourceOptions } from './PriceDataSource';
Expand Down Expand Up @@ -108,14 +109,14 @@ function setupController(
options: {
priceResponse?: Record<string, unknown>;
balanceState?: Record<string, Record<string, unknown>>;
currency?: 'usd' | 'eur';
getSelectedCurrency?: () => SupportedCurrency;
pollInterval?: number;
} = {},
): SetupResult {
const {
priceResponse = {},
balanceState = {},
currency,
getSelectedCurrency = (): SupportedCurrency => 'usd',
pollInterval,
} = options;

Expand All @@ -124,11 +125,9 @@ function setupController(
const controllerOptions: PriceDataSourceOptions = {
queryApiClient:
apiClient as unknown as PriceDataSourceOptions['queryApiClient'],
getSelectedCurrency,
};

if (currency) {
controllerOptions.currency = currency;
}
if (pollInterval) {
controllerOptions.pollInterval = pollInterval;
}
Expand Down Expand Up @@ -254,7 +253,7 @@ describe('PriceDataSource', () => {

it('fetch uses custom currency', async () => {
const { controller, apiClient, getAssetsState } = setupController({
currency: 'eur',
getSelectedCurrency: () => 'eur',
balanceState: {
'mock-account-id': {
[MOCK_NATIVE_ASSET]: { amount: '1000000000000000000' },
Expand Down
10 changes: 5 additions & 5 deletions packages/assets-controller/src/data-sources/PriceDataSource.ts
Copy link
Contributor Author

@bergarces bergarces Feb 19, 2026

Choose a reason for hiding this comment

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

Currency is already included as part of the query key for cache purposes, so no changes needed in the client.

Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ export type PriceDataSourceConfig = {
export type PriceDataSourceOptions = PriceDataSourceConfig & {
/** ApiPlatformClient for API calls with caching */
queryApiClient: ApiPlatformClient;
/** Currency to fetch prices in (default: 'usd') */
currency?: SupportedCurrency;
/** Function returning the currently-active ISO 4217 currency code */
getSelectedCurrency: () => SupportedCurrency;
};

// ============================================================================
Expand Down Expand Up @@ -110,7 +110,7 @@ export class PriceDataSource {
return PriceDataSource.controllerName;
}

readonly #currency: SupportedCurrency;
readonly #getSelectedCurrency: () => SupportedCurrency;

readonly #pollInterval: number;

Expand All @@ -129,7 +129,7 @@ export class PriceDataSource {
> = new Map();

constructor(options: PriceDataSourceOptions) {
this.#currency = options.currency ?? 'usd';
this.#getSelectedCurrency = options.getSelectedCurrency;
this.#pollInterval = options.pollInterval ?? DEFAULT_POLL_INTERVAL;
this.#apiClient = options.queryApiClient;
}
Expand Down Expand Up @@ -219,7 +219,7 @@ export class PriceDataSource {
*/
async #fetchSpotPrices(assetIds: string[]): Promise<V3SpotPricesResponse> {
return this.#apiClient.prices.fetchV3SpotPrices(assetIds, {
currency: this.#currency,
currency: this.#getSelectedCurrency(),
includeMarketData: true,
});
}
Expand Down