Skip to content

Commit

Permalink
feat: integrate multichain assets rates controller to extension UI (#…
Browse files Browse the repository at this point in the history
…30291)

<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**
This PR aims to integrate the new multichainAssetsRatesControllers into
the extension. To achieve this, an upgrade of the assets controller to
version 49 is required. As a result, the target branch is set
accordingly:
[MetaMask PR
#30250](#30250).

<!--
Write a short description of the changes included in this pull request,
also include relevant motivation and context. Have in mind the following
questions:
1. What is the reason for the change?
2. What is the improvement/solution?
-->

[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/30291?quickstart=1)

## **Related issues**

Fixes:

## **Manual testing steps**

1. open a flask build and add a solana account
2. check if the polling for the rates is running

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->

### **After**

<!-- [screenshots/recordings] -->

## **Pre-merge author checklist**

- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

---------

Co-authored-by: sahar-fehri <[email protected]>
Co-authored-by: MetaMask Bot <[email protected]>
Co-authored-by: António Regadas <[email protected]>
Co-authored-by: Guillaume Roux <[email protected]>
  • Loading branch information
5 people authored Feb 17, 2025
1 parent 422201c commit 1f4afda
Show file tree
Hide file tree
Showing 14 changed files with 266 additions and 1 deletion.
3 changes: 3 additions & 0 deletions app/scripts/constants/sentry-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ export const SENTRY_BACKGROUND_STATE = {
accountsAssets: false,
assetsMetadata: false,
},
MultiChainAssetsRatesController: {
assetsRates: false,
},
BridgeController: {
bridgeState: {
bridgeFeatureFlags: {
Expand Down
3 changes: 3 additions & 0 deletions app/scripts/controller-init/controller-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { TransactionUpdateController } from '@metamask-institutional/transaction
import { AccountsController } from '@metamask/accounts-controller';
import {
MultichainAssetsController,
MultiChainAssetsRatesController,
MultichainBalancesController,
} from '@metamask/assets-controllers';
import { MultichainTransactionsController } from '@metamask/multichain-transactions-controller';
Expand Down Expand Up @@ -42,6 +43,7 @@ export type Controller =
| JsonSnapsRegistry
| KeyringController
| MultichainAssetsController
| MultiChainAssetsRatesController
| MultichainBalancesController
| MultichainTransactionsController
| NetworkController
Expand Down Expand Up @@ -73,6 +75,7 @@ export type ControllerFlatState = AccountsController['state'] &
JsonSnapsRegistry['state'] &
KeyringController['state'] &
MultichainAssetsController['state'] &
MultiChainAssetsRatesController['state'] &
MultichainBalancesController['state'] &
MultichainTransactionsController['state'] &
NetworkController['state'] &
Expand Down
5 changes: 5 additions & 0 deletions app/scripts/controller-init/messengers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
getMultichainBalancesControllerMessenger,
getMultichainTransactionsControllerMessenger,
getMultichainAssetsControllerMessenger,
getMultiChainAssetsRatesControllerMessenger,
} from './multichain';

export const CONTROLLER_MESSENGERS = {
Expand All @@ -37,6 +38,10 @@ export const CONTROLLER_MESSENGERS = {
getMessenger: getMultichainAssetsControllerMessenger,
getInitMessenger: noop,
},
MultiChainAssetsRatesController: {
getMessenger: getMultiChainAssetsRatesControllerMessenger,
getInitMessenger: noop,
},
MultichainBalancesController: {
getMessenger: getMultichainBalancesControllerMessenger,
getInitMessenger: noop,
Expand Down
2 changes: 2 additions & 0 deletions app/scripts/controller-init/messengers/multichain/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
export { getMultichainAssetsControllerMessenger } from './multichain-assets-controller-messenger';
export { getMultiChainAssetsRatesControllerMessenger } from './multichain-assets-rates-controller-messenger';
export { getMultichainBalancesControllerMessenger } from './multichain-balances-controller-messenger';
export { getMultichainTransactionsControllerMessenger } from './multichain-transactions-controller-messenger';

export type { MultichainAssetsControllerMessenger } from './multichain-assets-controller-messenger';
export type { MultiChainAssetsRatesControllerMessenger } from './multichain-assets-rates-controller-messenger';
export type { MultichainBalancesControllerMessenger } from './multichain-balances-controller-messenger';
export type { MultichainTransactionsControllerMessenger } from './multichain-transactions-controller-messenger';
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Messenger, RestrictedMessenger } from '@metamask/base-controller';
import { getMultiChainAssetsRatesControllerMessenger } from './multichain-assets-rates-controller-messenger';

describe('getMultiChainAssetsRatesControllerMessenger', () => {
it('returns a restricted messenger', () => {
const messenger = new Messenger<never, never>();
const multichainAssetsRatesControllerMessenger =
getMultiChainAssetsRatesControllerMessenger(messenger);

expect(multichainAssetsRatesControllerMessenger).toBeInstanceOf(
RestrictedMessenger,
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Messenger } from '@metamask/base-controller';
import {
AccountsControllerAccountAddedEvent,
AccountsControllerListMultichainAccountsAction,
} from '@metamask/accounts-controller';
import {
CurrencyRateStateChange,
GetCurrencyRateState,
MultichainAssetsControllerStateChangeEvent,
MultichainAssetsControllerGetStateAction,
} from '@metamask/assets-controllers';
import {
KeyringControllerLockEvent,
KeyringControllerUnlockEvent,
} from '@metamask/keyring-controller';
import { HandleSnapRequest } from '@metamask/snaps-controllers';

type Actions =
| HandleSnapRequest
| AccountsControllerListMultichainAccountsAction
| GetCurrencyRateState
| MultichainAssetsControllerGetStateAction;

type Events =
| KeyringControllerLockEvent
| KeyringControllerUnlockEvent
| AccountsControllerAccountAddedEvent
| CurrencyRateStateChange
| MultichainAssetsControllerStateChangeEvent;

export type MultiChainAssetsRatesControllerMessenger = ReturnType<
typeof getMultiChainAssetsRatesControllerMessenger
>;

/**
* Get a restricted messenger for the Multichain Assets Rate controller. This is scoped to the
* actions and events that the multichain Assets Rate controller is allowed to handle.
*
* @param messenger - The controller messenger to restrict.
* @returns The restricted controller messenger.
*/
export function getMultiChainAssetsRatesControllerMessenger(
messenger: Messenger<Actions, Events>,
) {
return messenger.getRestricted({
name: 'MultiChainAssetsRatesController',
allowedEvents: [
'AccountsController:accountAdded',
'KeyringController:lock',
'KeyringController:unlock',
'CurrencyRateController:stateChange',
'MultichainAssetsController:stateChange',
],
allowedActions: [
'AccountsController:listMultichainAccounts',
'SnapController:handleRequest',
'CurrencyRateController:getState',
'MultichainAssetsController:getState',
],
});
}
1 change: 1 addition & 0 deletions app/scripts/controller-init/multichain/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { MultichainAssetsControllerInit } from './multichain-assets-controller-init';
export { MultichainBalancesControllerInit } from './multichain-balances-controller-init';
export { MultichainTransactionsControllerInit } from './multichain-transactions-controller-init';
export { MultiChainAssetsRatesControllerInit } from './multichain-rates-assets-controller-init';
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { MultiChainAssetsRatesController } from '@metamask/assets-controllers';
import { Messenger } from '@metamask/base-controller';
import { buildControllerInitRequestMock } from '../test/utils';
import { ControllerInitRequest } from '../types';
import {
getMultiChainAssetsRatesControllerMessenger,
MultiChainAssetsRatesControllerMessenger,
} from '../messengers/multichain';
import { MultiChainAssetsRatesControllerInit } from './multichain-rates-assets-controller-init';

jest.mock('@metamask/assets-controllers');

function buildInitRequestMock(): jest.Mocked<
ControllerInitRequest<MultiChainAssetsRatesControllerMessenger>
> {
const baseControllerMessenger = new Messenger();

return {
...buildControllerInitRequestMock(),
controllerMessenger: getMultiChainAssetsRatesControllerMessenger(
baseControllerMessenger,
),
initMessenger: undefined,
};
}

describe('MultiChainAssetsRatesControllerInit', () => {
const multiChainAssetsRatesControllerClassMock = jest.mocked(
MultiChainAssetsRatesController,
);

beforeEach(() => {
jest.resetAllMocks();
});

it('returns controller instance', () => {
const requestMock = buildInitRequestMock();
expect(
MultiChainAssetsRatesControllerInit(requestMock).controller,
).toBeInstanceOf(MultiChainAssetsRatesController);
});

it('initializes with correct messenger and state', () => {
const requestMock = buildInitRequestMock();
MultiChainAssetsRatesControllerInit(requestMock);

expect(multiChainAssetsRatesControllerClassMock).toHaveBeenCalledWith({
messenger: requestMock.controllerMessenger,
state: requestMock.persistedState.MultiChainAssetsRatesController,
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { MultiChainAssetsRatesController } from '@metamask/assets-controllers';
import { ControllerInitFunction } from '../types';
import { MultiChainAssetsRatesControllerMessenger } from '../messengers/multichain';

/**
* Initialize the Multichain Assets Rate controller.
*
* @param request - The request object.
* @param request.controllerMessenger - The messenger to use for the controller.
* @param request.persistedState - The persisted state of the extension.
* @returns The initialized controller.
*/
export const MultiChainAssetsRatesControllerInit: ControllerInitFunction<
MultiChainAssetsRatesController,
MultiChainAssetsRatesControllerMessenger
> = ({ controllerMessenger, persistedState }) => {
const controller = new MultiChainAssetsRatesController({
messenger: controllerMessenger,
state: persistedState.MultiChainAssetsRatesController,
});

return {
controller,
};
};
1 change: 1 addition & 0 deletions app/scripts/controller-init/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export type ControllersToInitialize =
| 'CronjobController'
| 'ExecutionService'
| 'MultichainAssetsController'
| 'MultiChainAssetsRatesController'
| 'MultichainBalancesController'
| 'MultichainTransactionsController'
| 'RateLimitController'
Expand Down
5 changes: 5 additions & 0 deletions app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ import {
MultichainAssetsControllerInit,
MultichainTransactionsControllerInit,
MultichainBalancesControllerInit,
MultiChainAssetsRatesControllerInit,
} from './controller-init/multichain';
///: END:ONLY_INCLUDE_IF
import { TransactionControllerInit } from './controller-init/confirmations/transaction-controller-init';
Expand Down Expand Up @@ -2054,6 +2055,7 @@ export default class MetamaskController extends EventEmitter {
TransactionController: TransactionControllerInit,
///: BEGIN:ONLY_INCLUDE_IF(build-flask)
MultichainAssetsController: MultichainAssetsControllerInit,
MultiChainAssetsRatesController: MultiChainAssetsRatesControllerInit,
MultichainBalancesController: MultichainBalancesControllerInit,
MultichainTransactionsController: MultichainTransactionsControllerInit,
///: END:ONLY_INCLUDE_IF
Expand Down Expand Up @@ -2091,6 +2093,8 @@ export default class MetamaskController extends EventEmitter {
controllersByName.MultichainBalancesController;
this.multichainTransactionsController =
controllersByName.MultichainTransactionsController;
this.multiChainAssetsRatesController =
controllersByName.MultiChainAssetsRatesController;
///: END:ONLY_INCLUDE_IF

this.controllerMessenger.subscribe(
Expand Down Expand Up @@ -2266,6 +2270,7 @@ export default class MetamaskController extends EventEmitter {
MultichainAssetsController: this.multichainAssetsController,
MultichainBalancesController: this.multichainBalancesController,
MultichainTransactionsController: this.multichainTransactionsController,
MultiChainAssetsRatesController: this.multiChainAssetsRatesController,
///: END:ONLY_INCLUDE_IF
NetworkController: this.networkController,
KeyringController: this.keyringController,
Expand Down
1 change: 1 addition & 0 deletions test/e2e/tests/metrics/errors.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,7 @@ describe('Sentry errors', function () {
balances: false,
accountsAssets: false,
assetsMetadata: false,
assetsRates: false,
smartTransactionsState: {
fees: {
approvalTxFees: true, // Initialized as undefined
Expand Down
75 changes: 75 additions & 0 deletions ui/selectors/assets.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {
getAssetsRates,
AssetsRatesState,
AssetsState,
getAccountAssets,
getAssetsMetadata,
} from './assets';

const mockRatesState = {
metamask: {
conversionRates: {
'token-1': { rate: 1.5, currency: 'USD' },
'token-2': { rate: 0.8, currency: 'EUR' },
},
},
};

// Mock state for testing
const mockAssetsState: AssetsState = {
metamask: {
accountsAssets: {
'5132883f-598e-482c-a02b-84eeaa352f5b': [
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501',
],
},
assetsMetadata: {
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501': {
name: 'Token 1',
symbol: 'TKN1',
iconUrl: 'https://example.com/token-1.png',
fungible: true,
units: [{ symbol: 'TKN1', name: 'Token 1', decimals: 9 }],
},
},
},
};

describe('getAccountAssets', () => {
it('should return the assets from the state', () => {
const result = getAccountAssets(mockAssetsState);
expect(result).toEqual(mockAssetsState.metamask.accountsAssets);
});
});

describe('getAssetsMetadata', () => {
it('should return the assets metadata from the state', () => {
const result = getAssetsMetadata(mockAssetsState);
expect(result).toEqual(mockAssetsState.metamask.assetsMetadata);
});

it('should return undefined if state does not have metamask property', () => {
const invalidState = {} as AssetsState;
expect(() => getAssetsMetadata(invalidState)).toThrow();
});
});

describe('getAssetsRates', () => {
it('should return the assetsRates from the state', () => {
const result = getAssetsRates(mockRatesState);
expect(result).toEqual(mockRatesState.metamask.conversionRates);
});

it('should return an empty object if assetsRates is empty', () => {
const emptyState: AssetsRatesState = {
metamask: { conversionRates: {} },
};
const result = getAssetsRates(emptyState);
expect(result).toEqual({});
});

it('should return undefined if state does not have metamask property', () => {
const invalidState = {} as AssetsRatesState;
expect(() => getAssetsRates(invalidState)).toThrow();
});
});
19 changes: 18 additions & 1 deletion ui/selectors/assets.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { MultichainAssetsControllerState } from '@metamask/assets-controllers';
import {
MultichainAssetsControllerState,
MultichainAssetsRatesControllerState,
} from '@metamask/assets-controllers';

export type AssetsState = {
metamask: MultichainAssetsControllerState;
};

export type AssetsRatesState = {
metamask: MultichainAssetsRatesControllerState;
};

/**
* Gets non-EVM accounts assets.
*
Expand All @@ -23,3 +30,13 @@ export function getAccountAssets(state: AssetsState) {
export function getAssetsMetadata(state: AssetsState) {
return state.metamask.assetsMetadata;
}

/**
* Gets non-EVM accounts assets rates.
*
* @param state - Redux state object.
* @returns An object containing non-EVM assets per accounts.
*/
export function getAssetsRates(state: AssetsRatesState) {
return state.metamask.conversionRates;
}

0 comments on commit 1f4afda

Please sign in to comment.