Skip to content

feat: integrate multichain assets rates controller to extension UI #30291

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 36 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
50802a2
fix: bump assets-controllers to v49
sahar-fehri Feb 11, 2025
b1463f9
Update LavaMoat policies
metamaskbot Feb 11, 2025
fb857ae
fix: dedupe
sahar-fehri Feb 11, 2025
4a453d1
Update LavaMoat policies
metamaskbot Feb 11, 2025
54c75c9
fix: merge conflicts
sahar-fehri Feb 12, 2025
db67847
fix: bump assets
sahar-fehri Feb 12, 2025
607ca16
fix: patch
sahar-fehri Feb 12, 2025
66c944f
fix: fix yarn
sahar-fehri Feb 12, 2025
98eebaa
fix: fix test
sahar-fehri Feb 12, 2025
6fd1caa
Update LavaMoat policies
metamaskbot Feb 12, 2025
70fabea
fix: fix patch
sahar-fehri Feb 12, 2025
7d4627d
Update LavaMoat policies
metamaskbot Feb 12, 2025
9d28e99
Merge branch 'main' into feat/bump-assets-controller-49.0.0
sahar-fehri Feb 12, 2025
91f130a
Merge branch 'main' into feat/bump-assets-controller-49.0.0
zone-live Feb 13, 2025
414eef1
Merge branch 'main' into feat/bump-assets-controller-49.0.0
sahar-fehri Feb 13, 2025
550a15d
fix: lint
sahar-fehri Feb 13, 2025
d27585b
Merge branch 'main' into feat/bump-assets-controller-49.0.0
sahar-fehri Feb 13, 2025
855ff7e
Merge branch 'main' into feat/bump-assets-controller-49.0.0
sahar-fehri Feb 13, 2025
209900a
[WIP] `MultichainAssetsController` init
GuillaumeRx Feb 13, 2025
51c7df5
Update multichain controllers init
GuillaumeRx Feb 13, 2025
d8aa775
fix: integrate multichainAssetsRatesController
salimtb Feb 13, 2025
48aa482
fix: fix after rebase
salimtb Feb 13, 2025
ef32bf4
fix: fix linter
salimtb Feb 13, 2025
f174ac4
fix: fix PR comments
salimtb Feb 14, 2025
3276a75
Merge branch 'main' into salim/integrate-mutichain-assets-rates-contr…
salimtb Feb 14, 2025
aaabc9b
fix: fix e2e tests
salimtb Feb 14, 2025
f05509a
fix: fix build
salimtb Feb 14, 2025
da24a10
fix: fix build
salimtb Feb 14, 2025
04ae981
fix: fix linter
salimtb Feb 14, 2025
60a2284
fix: fix PR comments
salimtb Feb 17, 2025
c629eb7
Merge branch 'main' into salim/integrate-mutichain-assets-rates-contr…
salimtb Feb 17, 2025
942e8e9
fix: fix linter
salimtb Feb 17, 2025
415aa4c
fix: add controller name
salimtb Feb 17, 2025
ec00454
fix: sort alphabetically the added controller
salimtb Feb 17, 2025
ae46e89
fix: sort initialisation
salimtb Feb 17, 2025
72519bf
Merge branch 'main' into salim/integrate-mutichain-assets-rates-contr…
salimtb Feb 17, 2025
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
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;
Copy link
Contributor

@zone-live zone-live Feb 17, 2025

Choose a reason for hiding this comment

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

Quick question, will this selector replace the existing getMultichainCoinRates
Asking because it's being used in the getMultichainConversionRate
cc @salimtb @ccharly

Copy link
Contributor Author

@salimtb salimtb Feb 17, 2025

Choose a reason for hiding this comment

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

@zone-live The one you mentioned is limited to Native tokens, whereas the one in this PR supports both SPL tokens and Native tokens.
So, the answer is yes—if we need comprehensive data, including SPL tokens, we should use the one in this PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you @salimtb 💪🏼

}
Loading