Skip to content
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

feat: add rates controller #25314

Merged
merged 13 commits into from
Jun 20, 2024
5 changes: 5 additions & 0 deletions app/scripts/lib/setupSentry.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,11 @@ export const SENTRY_BACKGROUND_STATE = {
PushPlatformNotificationsController: {
fcmToken: false,
},
MultichainRatesController: {
fiatCurrency: true,
rates: true,
cryptocurrencies: true,
},
SelectedNetworkController: { domains: false },
SignatureController: {
unapprovedMsgCount: true,
Expand Down
44 changes: 44 additions & 0 deletions app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
TokenRatesController,
TokensController,
CodefiTokenPricesServiceV2,
RatesController,
fetchMultiExchangeRate,
} from '@metamask/assets-controllers';
import { ObservableStore } from '@metamask/obs-store';
import { storeAsStream } from '@metamask/obs-store/dist/asStream';
Expand Down Expand Up @@ -149,6 +151,7 @@ import {
} from '@metamask/snaps-utils';
///: END:ONLY_INCLUDE_IF

import { isEvmAccountType } from '@metamask/keyring-api';
import {
methodsRequiringNetworkSwitch,
methodsWithConfirmation,
Expand Down Expand Up @@ -922,6 +925,17 @@ export default class MetamaskController extends EventEmitter {
state: initState.AccountOrderController,
});

const multichainRatesControllerMessenger =
this.controllerMessenger.getRestricted({
name: 'RatesController',
});
this.multichainRatesController = new RatesController({
state: initState.MultichainRatesController,
messenger: multichainRatesControllerMessenger,
includeUsdRate: true,
fetchMultiExchangeRate,
});

// token exchange rate tracker
this.tokenRatesController = new TokenRatesController(
{
Expand Down Expand Up @@ -2195,6 +2209,7 @@ export default class MetamaskController extends EventEmitter {
PhishingController: this.phishingController,
SelectedNetworkController: this.selectedNetworkController,
LoggingController: this.loggingController,
MultichainRatesController: this.multichainRatesController,
///: BEGIN:ONLY_INCLUDE_IF(snaps)
SnapController: this.snapController,
CronjobController: this.cronjobController,
Expand Down Expand Up @@ -2250,6 +2265,7 @@ export default class MetamaskController extends EventEmitter {
SelectedNetworkController: this.selectedNetworkController,
LoggingController: this.loggingController,
TxController: this.txController,
MultichainRatesController: this.multichainRatesController,
///: BEGIN:ONLY_INCLUDE_IF(snaps)
SnapController: this.snapController,
CronjobController: this.cronjobController,
Expand Down Expand Up @@ -2329,6 +2345,7 @@ export default class MetamaskController extends EventEmitter {
});

this.setupControllerEventSubscriptions();
this.setupMultichainDataAndSubscriptions();

// For more information about these legacy streams, see here:
// https://github.com/MetaMask/metamask-extension/issues/15491
Expand Down Expand Up @@ -2818,6 +2835,33 @@ export default class MetamaskController extends EventEmitter {
///: END:ONLY_INCLUDE_IF
}

/**
* Sets up multichain data and subscriptions.
* This method is called during the MetaMaskController constructor.
* It starts the MultichainRatesController if selected account is non-EVM
* and subscribes to account changes.
*/
setupMultichainDataAndSubscriptions() {
if (
!isEvmAccountType(
this.accountsController.getSelectedMultichainAccount().type,
)
) {
this.multichainRatesController.start();
}

this.controllerMessenger.subscribe(
'AccountsController:selectedAccountChange',
(selectedAccount) => {
if (isEvmAccountType(selectedAccount.type)) {
this.multichainRatesController.stop();
return;
}
this.multichainRatesController.start();
},
);
}

/**
* TODO:LegacyProvider: Delete
* Constructor helper: initialize a public config store.
Expand Down
120 changes: 118 additions & 2 deletions app/scripts/metamask-controller.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,19 @@ import {
METAMASK_STALELIST_FILE,
METAMASK_HOTLIST_DIFF_FILE,
} from '@metamask/phishing-controller';
import { EthAccountType } from '@metamask/keyring-api';
import {
BtcAccountType,
BtcMethod,
EthAccountType,
} from '@metamask/keyring-api';
import { NetworkType } from '@metamask/controller-utils';
import { ControllerMessenger } from '@metamask/base-controller';
import { LoggingController, LogType } from '@metamask/logging-controller';
import { TransactionController } from '@metamask/transaction-controller';
import { TokenListController } from '@metamask/assets-controllers';
import {
RatesController,
TokenListController,
} from '@metamask/assets-controllers';
import { NETWORK_TYPES } from '../../shared/constants/network';
import { createTestProviderTools } from '../../test/stub/provider';
import { HardwareDeviceNames } from '../../shared/constants/hardware-wallets';
Expand All @@ -29,6 +36,7 @@ import mockEncryptor from '../../test/lib/mock-encryptor';
import * as tokenUtils from '../../shared/lib/token-util';
import { flushPromises } from '../../test/lib/timer-helpers';
import { ETH_EOA_METHODS } from '../../shared/constants/eth-methods';
import { createMockInternalAccount } from '../../test/jest/mocks';
import { deferredPromise } from './lib/util';
import MetaMaskController from './metamask-controller';

Expand Down Expand Up @@ -2025,6 +2033,114 @@ describe('MetaMaskController', () => {
expect(TokenListController.prototype.start).toHaveBeenCalledTimes(1);
});
});

describe('MultichainRatesController start/stop', () => {
const mockEvmAccount = createMockInternalAccount();
const mockNonEvmAccount = {
...mockEvmAccount,
id: '21690786-6abd-45d8-a9f0-9ff1d8ca76a1',
type: BtcAccountType.P2wpkh,
methods: [BtcMethod.SendMany],
address: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq',
};

beforeEach(() => {
jest.spyOn(metamaskController.multichainRatesController, 'start');
jest.spyOn(metamaskController.multichainRatesController, 'stop');
});

afterEach(() => {
jest.clearAllMocks();
});

it('starts MultichainRatesController if selected account is changed to non-EVM', async () => {
expect(
metamaskController.multichainRatesController.start,
).not.toHaveBeenCalled();

metamaskController.controllerMessenger.publish(
'AccountsController:selectedAccountChange',
mockNonEvmAccount,
);

expect(
metamaskController.multichainRatesController.start,
).toHaveBeenCalledTimes(1);
});

it('stops MultichainRatesController if selected account is changed to EVM', async () => {
expect(
metamaskController.multichainRatesController.start,
).not.toHaveBeenCalled();

metamaskController.controllerMessenger.publish(
'AccountsController:selectedAccountChange',
mockNonEvmAccount,
);

expect(
metamaskController.multichainRatesController.start,
).toHaveBeenCalledTimes(1);

metamaskController.controllerMessenger.publish(
'AccountsController:selectedAccountChange',
mockEvmAccount,
);
expect(
metamaskController.multichainRatesController.start,
).toHaveBeenCalledTimes(1);
expect(
metamaskController.multichainRatesController.stop,
).toHaveBeenCalledTimes(1);
});

it('does not start MultichainRatesController if selected account is changed to EVM', async () => {
expect(
metamaskController.multichainRatesController.start,
).not.toHaveBeenCalled();

metamaskController.controllerMessenger.publish(
'AccountsController:selectedAccountChange',
mockEvmAccount,
);

expect(
metamaskController.multichainRatesController.start,
).not.toHaveBeenCalled();
});

it('starts MultichainRatesController if selected account is non-EVM account during initialization', async () => {
jest.spyOn(RatesController.prototype, 'start');
const localMetamaskController = new MetaMaskController({
showUserConfirmation: noop,
encryptor: mockEncryptor,
initState: {
...cloneDeep(firstTimeState),
AccountsController: {
internalAccounts: {
accounts: {
[mockNonEvmAccount.id]: mockNonEvmAccount,
[mockEvmAccount.id]: mockEvmAccount,
},
selectedAccount: mockNonEvmAccount.id,
},
},
},
initLangCode: 'en_US',
platform: {
showTransactionNotification: () => undefined,
getVersion: () => 'foo',
},
browser: browserPolyfillMock,
infuraProjectId: 'foo',
isFirstMetaMaskControllerSetup: true,
});

expect(
localMetamaskController.multichainRatesController.start,
).toHaveBeenCalled();
});
});
});

describe('MV3 Specific behaviour', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,11 @@
"MetaMetricsController": {
"participateInMetaMetrics": true,
"metaMetricsId": "fake-metrics-id",
"dataCollectionForMarketing": "boolean",
"eventsBeforeMetricsOptIn": "object",
"traits": "object",
"previousUserTraits": "object",
"fragments": "object",
"dataCollectionForMarketing": "boolean",
"segmentApiCalls": "object"
},
"MetamaskNotificationsController": {
Expand All @@ -119,6 +119,11 @@
"isUpdatingMetamaskNotificationsAccount": "object",
"isCheckingAccountsPresence": "boolean"
},
"MultichainRatesController": {
"fiatCurrency": "usd",
"rates": { "btc": { "conversionDate": 0, "conversionRate": "0" } },
"cryptocurrencies": ["btc"]
},
"NameController": { "names": "object", "nameSources": "object" },
"NetworkController": {
"selectedNetworkClientId": "string",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,9 @@
"logs": "object",
"methodData": "object",
"lastFetchedBlockNumbers": "object",
"fiatCurrency": "usd",
"rates": { "btc": { "conversionDate": 0, "conversionRate": "0" } },
"cryptocurrencies": ["btc"],
"snaps": "object",
"snapStates": "object",
"unencryptedSnapStates": "object",
Expand Down