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 new rates controller for non-EVM chains #4242

Merged
merged 78 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
2554c92
chore: move crypto-compare service into new directory
gantunesr May 1, 2024
4a029d8
feat: add non-evm rates controller
gantunesr May 1, 2024
272168d
refactor: rename controller
gantunesr May 1, 2024
9d2da6f
Merge branch 'main' into feat/nonevm-rates
gantunesr May 6, 2024
77613e1
feat: add support for multiprice
gantunesr May 8, 2024
4c825fc
refactor: change endpoint to multiprice
gantunesr May 8, 2024
b04949f
test: add unit tests to RatesController
gantunesr May 9, 2024
3a1254d
chore: throw an error if the currency is an empty string
gantunesr May 10, 2024
adecee9
test: add jsdoc to test helper methods
gantunesr May 10, 2024
9f5c830
docs: improve JSDocs
gantunesr May 10, 2024
24234cb
Merge branch 'main' into feat/nonevm-rates
gantunesr May 10, 2024
9bec616
test: update crypto-compare tests
gantunesr May 10, 2024
72a94fe
Merge branch 'feat/nonevm-rates' of https://github.com/MetaMask/core …
gantunesr May 10, 2024
f95b93a
fix: update exports
gantunesr May 10, 2024
9639a50
chore: update RatesControllerOptions JSDocs
gantunesr May 10, 2024
e0a75ab
chore: update JSDocs
gantunesr May 10, 2024
e95d3b3
chore: update JSDocs
gantunesr May 10, 2024
8a6232e
chore: update JSDocs
gantunesr May 10, 2024
7fbad35
refactor: replace callbacks with events
gantunesr May 13, 2024
aa22c5d
chore: add mutex to setCryptocurrencyList
gantunesr May 13, 2024
1d3849c
refactor: remove setIntervalLength method
gantunesr May 14, 2024
e452e2f
refactor: set DEFAULT_INTERVAL value
gantunesr May 14, 2024
9f0401e
chore: update JSDocs
gantunesr May 14, 2024
778a389
refactor: update event variable names
gantunesr May 14, 2024
72ab713
refactor: rename action variable
gantunesr May 14, 2024
7de9924
Merge branch 'feat/nonevm-rates' of https://github.com/MetaMask/core …
gantunesr May 14, 2024
df2304e
fix: data types
gantunesr May 14, 2024
2b9d38a
refactor: change name cryptocurrencyList to fromCurrencies
gantunesr May 14, 2024
ae0af1b
fix: unit tests
gantunesr May 14, 2024
e6ea938
fix: unit tests
gantunesr May 14, 2024
b23817d
chore: update exports
gantunesr May 14, 2024
c44002d
test: increase code coverage
gantunesr May 14, 2024
aa4b2da
Merge branch 'main' into feat/nonevm-rates
gantunesr May 14, 2024
f51a478
refactor: change rate type from number to string
gantunesr May 20, 2024
bd727dc
test: update unit tests data type
gantunesr May 20, 2024
c60c4aa
refactor: make includeUsdRate not optional
gantunesr May 20, 2024
bc8e9bc
refactor: change DefaultCurrencies to enum
gantunesr May 20, 2024
ff06cc4
refactor: use DefaultCurrencies value in defaultState
gantunesr May 20, 2024
898d455
chore: add comment to explain ms convertion
gantunesr May 20, 2024
58b2093
refactor: lock logic
gantunesr May 20, 2024
265f6d1
chore: refactor event names
gantunesr May 20, 2024
c64cf5a
test: update RatesController unit test
gantunesr May 20, 2024
7290084
refactor: getMultiPricingURL method
gantunesr May 20, 2024
ccca895
refactor: remove comments
gantunesr May 20, 2024
5255f58
fix: lint
gantunesr May 20, 2024
1d547e5
refactor: make includeUSDRate not optional
gantunesr May 20, 2024
c4118e0
fix: intervalLength data type
gantunesr May 20, 2024
f54c598
docs: add RatesController README docs
gantunesr May 20, 2024
26ae373
docs: update RatesController README docs
gantunesr May 20, 2024
66b8a93
docs: udpate Rate
gantunesr May 20, 2024
13f2b2a
refactor: use Cryptocurrency instead of string
gantunesr May 20, 2024
8d40f6e
refactor: change iterable variable name
gantunesr May 20, 2024
cda3a20
docs: update definition
gantunesr May 20, 2024
a79984e
fix: Cryptocurrency enum
gantunesr May 20, 2024
0a19ee2
refactor: update iterable variable names
gantunesr May 20, 2024
8fb09ed
test: fix
gantunesr May 20, 2024
9b7125a
fix: type casting
gantunesr May 20, 2024
a6086ea
refactor: fetchMultiExchangeRate
gantunesr May 20, 2024
af653f5
refactor: updateRates
gantunesr May 20, 2024
7a8fd4e
Merge branch 'main' into feat/nonevm-rates
gantunesr May 20, 2024
bf52ebe
chore: set conversionDate to ms
gantunesr May 21, 2024
9ff7d52
Merge branch 'feat/nonevm-rates' of https://github.com/MetaMask/core …
gantunesr May 21, 2024
7531017
chore: update variable name
gantunesr May 21, 2024
3106034
chore: update messenger type name to RatesControllerMessenger
gantunesr May 21, 2024
81b5b4f
test: fix
gantunesr May 21, 2024
3c22dd6
chore: update variable names
gantunesr May 21, 2024
e2b092a
refactor: withLock
gantunesr May 21, 2024
b8c5bf2
docs: update types
gantunesr May 21, 2024
c3db5cb
test: increase coverage
gantunesr May 21, 2024
a4953ae
docs: update README
gantunesr May 21, 2024
aab6e05
chore: update commentary
gantunesr May 22, 2024
0ea038f
chore: update commentary and method names
gantunesr May 22, 2024
04d4172
Merge branch 'feat/nonevm-rates' of https://github.com/MetaMask/core …
gantunesr May 22, 2024
2c552d3
chore: update method name
gantunesr May 22, 2024
3dc06e7
chore: update commentary
gantunesr May 22, 2024
598acd5
fix: lint
gantunesr May 22, 2024
e11ba0f
chore: add return type
gantunesr May 22, 2024
4c70e44
Merge branch 'main' into feat/nonevm-rates
gantunesr May 22, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type {
import { StaticIntervalPollingController } from '@metamask/polling-controller';
import { Mutex } from 'async-mutex';

import { fetchExchangeRate as defaultFetchExchangeRate } from './crypto-compare';
ccharly marked this conversation as resolved.
Show resolved Hide resolved
import { fetchExchangeRate as defaultFetchExchangeRate } from './crypto-compare-service';

/**
* @type CurrencyRateState
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,328 @@
import { ControllerMessenger } from '@metamask/base-controller';
import { useFakeTimers } from 'sinon';

import { advanceTime } from '../../../../tests/helpers';
import type { fetchMultiExchangeRate as defaultFetchExchangeRate } from '../crypto-compare-service';
import {
RatesController,
name as ratesControllerName,
} from './RatesController';
import type {
RatesControllerActions,
RatesControllerEvents,
RatesMessenger,
RatesState,
} from './types';

const MOCK_TIMESTAMP = 1709983353;

/**
* Returns a stubbed date based on a predefined timestamp.
* @returns The stubbed date in milliseconds.
*/
const getStubbedDate = () => {
gantunesr marked this conversation as resolved.
Show resolved Hide resolved
return new Date(MOCK_TIMESTAMP * 1000).getTime();
};

/**
* Builds a new ControllerMessenger instance for RatesController.
* @returns A new ControllerMessenger instance.
*/
function buildMessenger(): ControllerMessenger<
RatesControllerActions,
RatesControllerEvents
> {
return new ControllerMessenger<
RatesControllerActions,
RatesControllerEvents
>();
}

/**
* Builds a restricted messenger for the RatesController.
* @param messenger - The base messenger instance.
* @returns A restricted messenger for the RatesController.
*/
function buildRatesControllerMessenger(
messenger: ControllerMessenger<RatesControllerActions, RatesControllerEvents>,
): RatesMessenger {
return messenger.getRestricted({
name: ratesControllerName,
allowedEvents: [],
allowedActions: [],
});
}

/**
* Sets up and returns a new instance of RatesController with the provided configuration.
* @param config - The configuration object for the RatesController.
* @param config.initialState - Initial state of the controller.
* @param config.messenger - ControllerMessenger instance.
* @param config.includeUsdRate - Indicates if the USD rate should be included.
* @param config.fetchMultiExchangeRate - Callback to fetch rates data.
* @param config.onStart - Optional method to be executed when the polling starts.
* @param config.onStop - Optional method to be executed when the polling stops.
* @returns A new instance of RatesController.
*/
function setupRatesController({
initialState,
messenger,
includeUsdRate,
fetchMultiExchangeRate,
onStart,
onStop,
}: {
initialState: Partial<RatesState>;
messenger: ControllerMessenger<RatesControllerActions, RatesControllerEvents>;
includeUsdRate?: boolean;
fetchMultiExchangeRate: typeof defaultFetchExchangeRate;
onStart?: () => Promise<unknown>;
onStop?: () => Promise<unknown>;
}) {
const ratesControllerMessenger = buildRatesControllerMessenger(messenger);
return new RatesController({
interval: 150,
messenger: ratesControllerMessenger,
state: initialState,
includeUsdRate,
fetchMultiExchangeRate,
onStart,
onStop,
});
}

describe('RatesController', () => {
let clock: sinon.SinonFakeTimers;

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

describe('contruct', () => {
it('constructs the RatesController with the correct values', () => {
const fetchExchangeRateStub = jest.fn().mockResolvedValue({});
const ratesController = setupRatesController({
initialState: {},
messenger: buildMessenger(),
includeUsdRate: false,
fetchMultiExchangeRate: fetchExchangeRateStub,
});
const { currency, rates, cryptocurrencyList } = ratesController.state;
expect(ratesController).toBeDefined();
expect(currency).toBe('usd');
expect(Object.keys(rates)).toStrictEqual(['btc']);
expect(cryptocurrencyList).toStrictEqual(['btc']);
});
});

describe('start', () => {
beforeEach(() => {
clock = useFakeTimers();
});

afterEach(() => {
clock.restore();
jest.restoreAllMocks();
});

it('starts the polling process with default values', async () => {
jest.spyOn(global.Date, 'now').mockImplementation(() => getStubbedDate());
const mockRateValue = 62235.48;
const fetchExchangeRateStub = jest.fn(() => {
return Promise.resolve({
btc: {
usd: mockRateValue,
},
});
});
const ratesController = setupRatesController({
initialState: {},
messenger: buildMessenger(),
fetchMultiExchangeRate: fetchExchangeRateStub,
});

const ratesPreUpdate = ratesController.state.rates;

expect(ratesPreUpdate).toStrictEqual({
btc: {
conversionDate: 0,
conversionRate: 0,
usdConversionRate: null,
},
});

await ratesController.start();

await advanceTime({ clock, duration: 200 });

const ratesPosUpdate = ratesController.state.rates;

expect(fetchExchangeRateStub).toHaveBeenCalled();
expect(ratesPosUpdate).toStrictEqual({
btc: {
conversionDate: MOCK_TIMESTAMP,
conversionRate: mockRateValue,
usdConversionRate: mockRateValue,
},
});
});

it('starts the polling process with custom values', async () => {
jest.spyOn(global.Date, 'now').mockImplementation(() => getStubbedDate());
const mockBtcUsdRateValue = 62235.48;
const mockSolUsdRateValue = 148.41;
const mockStrkUsdRateValue = 1.248;
const mockBtcEurRateValue = 57715.42;
const mockSolEurRateValue = 137.68;
const mockStrkEurRateValue = 1.157;
const fetchExchangeRateStub = jest.fn(() => {
return Promise.resolve({
btc: {
usd: mockBtcUsdRateValue,
eur: mockBtcEurRateValue,
},
sol: {
usd: mockSolUsdRateValue,
eur: mockSolEurRateValue,
},
strk: {
usd: mockStrkUsdRateValue,
eur: mockStrkEurRateValue,
},
});
});
const onStartStub = jest.fn().mockResolvedValue({});

const ratesController = setupRatesController({
initialState: {
cryptocurrencyList: ['btc', 'sol', 'strk'],
currency: 'eur',
},
messenger: buildMessenger(),
includeUsdRate: true,
fetchMultiExchangeRate: fetchExchangeRateStub,
onStart: onStartStub,
});

await ratesController.start();

await advanceTime({ clock, duration: 200 });

const { rates } = ratesController.state;
expect(fetchExchangeRateStub).toHaveBeenCalled();
expect(onStartStub).toHaveBeenCalled();
expect(rates).toStrictEqual({
btc: {
conversionDate: MOCK_TIMESTAMP,
conversionRate: mockBtcEurRateValue,
usdConversionRate: mockBtcUsdRateValue,
},
sol: {
conversionDate: MOCK_TIMESTAMP,
conversionRate: mockSolEurRateValue,
usdConversionRate: mockSolUsdRateValue,
},
strk: {
conversionDate: MOCK_TIMESTAMP,
conversionRate: mockStrkEurRateValue,
usdConversionRate: mockStrkUsdRateValue,
},
});
});
});

describe('stop', () => {
beforeEach(() => {
clock = useFakeTimers();
});

afterEach(() => {
clock.restore();
jest.restoreAllMocks();
});

it('stops the polling process', async () => {
const fetchExchangeRateStub = jest.fn().mockResolvedValue({});
const onStopStub = jest.fn().mockResolvedValue({});
const ratesController = setupRatesController({
initialState: {},
messenger: buildMessenger(),
fetchMultiExchangeRate: fetchExchangeRateStub,
onStop: onStopStub,
});

await ratesController.start();

await advanceTime({ clock, duration: 200 });

expect(fetchExchangeRateStub).toHaveBeenCalledTimes(1);

await ratesController.stop();

expect(onStopStub).toHaveBeenCalled();

await advanceTime({ clock, duration: 200 });

expect(fetchExchangeRateStub).toHaveBeenCalledTimes(1);
});
});

describe('getCryptocurrencyList', () => {
it('returns the current cryptocurrency list', () => {
const fetchExchangeRateStub = jest.fn().mockResolvedValue({});
const mockCryptocurrencyList = ['btc', 'sol', 'strk'];
const ratesController = setupRatesController({
initialState: {
cryptocurrencyList: mockCryptocurrencyList,
},
messenger: buildMessenger(),
fetchMultiExchangeRate: fetchExchangeRateStub,
});

const cryptocurrencyList = ratesController.getCryptocurrencyList();
expect(cryptocurrencyList).toStrictEqual(mockCryptocurrencyList);
});
});

describe('setCryptocurrencyList', () => {
it('updates the cryptocurrency list', () => {
const fetchExchangeRateStub = jest.fn().mockResolvedValue({});
const mockCryptocurrencyList = ['btc', 'sol', 'strk'];
const ratesController = setupRatesController({
initialState: {},
messenger: buildMessenger(),
fetchMultiExchangeRate: fetchExchangeRateStub,
});

const cryptocurrencyListPreUpdate =
ratesController.getCryptocurrencyList();
expect(cryptocurrencyListPreUpdate).toStrictEqual(['btc']);

ratesController.setCryptocurrencyList(mockCryptocurrencyList);
const cryptocurrencyListPostUpdate =
ratesController.getCryptocurrencyList();
expect(cryptocurrencyListPostUpdate).toStrictEqual(
mockCryptocurrencyList,
);
});
});

describe('setCurrentCurrency', () => {
it('sets the currency to a new value', async () => {
const fetchExchangeRateStub = jest.fn().mockResolvedValue({});
const ratesController = setupRatesController({
initialState: {},
messenger: buildMessenger(),
fetchMultiExchangeRate: fetchExchangeRateStub,
});

const currencyPreUpdate = ratesController.state.currency;
expect(currencyPreUpdate).toBe('usd');

await ratesController.setCurrency('eur');

const currencyPostUpdate = ratesController.state.currency;
expect(currencyPostUpdate).toBe('eur');
});
});
});
Loading
Loading