Skip to content
Merged
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
34 changes: 26 additions & 8 deletions src/assets/CurrencyRateController.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,11 +11,13 @@ const { Mutex } = require('await-semaphore');
* @property currentCurrency - Currently-active ISO 4217 currency code
* @property interval - Polling interval used to fetch new currency rate
* @property nativeCurrency - Symbol for the base asset used for conversion
* @property includeUSDRate - Whether to include the usd rate in addition to the currentCurrency
*/
export interface CurrencyRateConfig extends BaseConfig {
currentCurrency: string;
interval: number;
nativeCurrency: string;
includeUSDRate?: boolean;
}

/**
Expand All@@ -27,19 +29,24 @@ export interface CurrencyRateConfig extends BaseConfig {
* @property conversionRate - Conversion rate from current base asset to the current currency
* @property currentCurrency - Currently-active ISO 4217 currency code
* @property nativeCurrency - Symbol for the base asset used for conversion
* @property usdConversionRate - Conversion rate from usd to the current currency
*/
export interface CurrencyRateState extends BaseState {
conversionDate: number;
conversionRate: number;
currentCurrency: string;
nativeCurrency: string;
usdConversionRate?: number;
}

/**
* Controller that passively polls on a set interval for an exchange rate from the current base
* asset to the current currency
*/
export class CurrencyRateController extends BaseController<CurrencyRateConfig, CurrencyRateState> {
/* Optional config to include conversion to usd in all price url fetches and on state */
includeUSDRate?: boolean;

private activeCurrency = '';

private activeNativeCurrency = '';
Expand All@@ -52,10 +59,11 @@ export class CurrencyRateController extends BaseController<CurrencyRateConfig, C
return state && state.currentCurrency ? state.currentCurrency : 'usd';
}

private getPricingURL(currentCurrency: string, nativeCurrency: string) {
private getPricingURL(currentCurrency: string, nativeCurrency: string, includeUSDRate?: boolean) {
return (
`https://min-api.cryptocompare.com/data/price?fsym=` +
`${nativeCurrency.toUpperCase()}&tsyms=${currentCurrency.toUpperCase()}`
`${nativeCurrency.toUpperCase()}&tsyms=${currentCurrency.toUpperCase()}` +
`${includeUSDRate && currentCurrency.toUpperCase() !== 'USD' ? ',USD' : ''}`
);
}

Expand All@@ -77,12 +85,14 @@ export class CurrencyRateController extends BaseController<CurrencyRateConfig, C
disabled: true,
interval: 180000,
nativeCurrency: 'ETH',
includeUSDRate: false,
};
this.defaultState = {
conversionDate: 0,
conversionRate: 0,
currentCurrency: this.defaultConfig.currentCurrency,
nativeCurrency: this.defaultConfig.nativeCurrency,
usdConversionRate: 0,
};
this.initialize();
this.configure({ disabled: false }, false, false);
Expand DownExpand Up@@ -128,21 +138,26 @@ export class CurrencyRateController extends BaseController<CurrencyRateConfig, C
*
* @param currency - ISO 4217 currency code
* @param nativeCurrency - Symbol for base asset
* @param includeUSDRate - Whether to add the USD rate to the fetch
* @returns - Promise resolving to exchange rate for given currency
*/
async fetchExchangeRate(currency: string, nativeCurrency = this.activeNativeCurrency): Promise<CurrencyRateState> {
const json = await handleFetch(this.getPricingURL(currency, nativeCurrency));
async fetchExchangeRate(currency: string, nativeCurrency = this.activeNativeCurrency, includeUSDRate?: boolean): Promise<CurrencyRateState> {
const json = await handleFetch(this.getPricingURL(currency, nativeCurrency, includeUSDRate));
const conversionRate = Number(json[currency.toUpperCase()]);

const usdConversionRate = Number(json.USD);
if (!Number.isFinite(conversionRate)) {
throw new Error(`Invalid response for ${currency.toUpperCase()}: ${json[currency.toUpperCase()]}`);
}
if (includeUSDRate && !Number.isFinite(usdConversionRate)) {
throw new Error(`Invalid response for usdConversionRate: ${json.USD}`);
}

return {
conversionDate: Date.now() / 1000,
conversionRate,
currentCurrency: currency,
nativeCurrency,
usdConversionRate,
};
}

Expand All@@ -157,16 +172,19 @@ export class CurrencyRateController extends BaseController<CurrencyRateConfig, C
}
const releaseLock = await this.mutex.acquire();
try {
const { conversionDate, conversionRate } = await this.fetchExchangeRate(
const { conversionDate, conversionRate, usdConversionRate } = await this.fetchExchangeRate(
this.activeCurrency,
this.activeNativeCurrency,
this.includeUSDRate,
);
this.update({
const newState: CurrencyRateState = {
conversionDate,
conversionRate,
currentCurrency: this.activeCurrency,
nativeCurrency: this.activeNativeCurrency,
});
usdConversionRate: this.includeUSDRate ? usdConversionRate : this.defaultState.usdConversionRate,
};
this.update(newState);

return this.state;
} finally {
Expand Down
3 changes: 3 additions & 0 deletions tests/ComposableController.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,7 @@ describe('ComposableController', () => {
conversionRate: 0,
currentCurrency: 'usd',
nativeCurrency: 'ETH',
usdConversionRate: 0,
},
EnsController: {
ensEntries: {},
Expand DownExpand Up@@ -96,6 +97,7 @@ describe('ComposableController', () => {
selectedAddress: '',
suggestedAssets: [],
tokens: [],
usdConversionRate: 0,
});
});

Expand DownExpand Up@@ -149,6 +151,7 @@ describe('ComposableController', () => {
selectedAddress: '',
suggestedAssets: [],
tokens: [],
usdConversionRate: 0,
});
});

Expand Down
38 changes: 37 additions & 1 deletion tests/CurrencyRateController.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,11 @@ import CurrencyRateController from '../src/assets/CurrencyRateController';

describe('CurrencyRateController', () => {
beforeEach(() => {
fetchMock.mock('*', () => new Response(JSON.stringify({ USD: 1337 }))).spy();
fetchMock
.mock(/XYZ,USD/u, () => new Response(JSON.stringify({ XYZ: 123, USD: 456 })))
.mock(/DEF,USD/u, () => new Response(JSON.stringify({ DEF: 123 })))
.mock('*', () => new Response(JSON.stringify({ USD: 1337 })))
.spy();
});

afterEach(() => {
Expand All@@ -19,6 +23,7 @@ describe('CurrencyRateController', () => {
conversionRate: 0,
currentCurrency: 'usd',
nativeCurrency: 'ETH',
usdConversionRate: 0,
});
});

Expand All@@ -29,6 +34,7 @@ describe('CurrencyRateController', () => {
disabled: false,
interval: 180000,
nativeCurrency: 'ETH',
includeUSDRate: false,
});
});

Expand All@@ -40,6 +46,7 @@ describe('CurrencyRateController', () => {
disabled: false,
interval: 180000,
nativeCurrency: 'ETH',
includeUSDRate: false,
});
});

Expand DownExpand Up@@ -89,13 +96,42 @@ describe('CurrencyRateController', () => {
expect(controller.state.conversionRate).toBeGreaterThan(0);
});

it('should add usd rate to state when includeUSDRate is configured true', async () => {
const controller = new CurrencyRateController({ includeUSDRate: true, currentCurrency: 'xyz' });
expect(controller.state.usdConversionRate).toEqual(0);
await controller.updateExchangeRate();
expect(controller.state.usdConversionRate).toEqual(456);
});

it('should use default base asset', async () => {
const nativeCurrency = 'FOO';
const controller = new CurrencyRateController({ nativeCurrency });
await controller.fetchExchangeRate('usd');
expect(fetchMock.calls()[0][0]).toContain(nativeCurrency);
});

it('should add usd rate to state fetches when configured', async () => {
const controller = new CurrencyRateController({ includeUSDRate: true });
const result = await controller.fetchExchangeRate('xyz', 'FOO', true);
expect(fetchMock.calls()[0][0]).toContain('XYZ,USD');
expect(result.usdConversionRate).toEqual(456);
expect(result.conversionRate).toEqual(123);
});

it('should throw correctly when configured to return usd but receives an invalid response for currentCurrency rate', async () => {
const controller = new CurrencyRateController({ includeUSDRate: true });
await expect(controller.fetchExchangeRate('abc', 'FOO', true)).rejects.toThrow(
'Invalid response for ABC: undefined',
);
});

it('should throw correctly when configured to return usd but receives an invalid response for usdConversionRate', async () => {
const controller = new CurrencyRateController({ includeUSDRate: true });
await expect(controller.fetchExchangeRate('def', 'FOO', true)).rejects.toThrow(
'Invalid response for usdConversionRate: undefined',
);
});

describe('#fetchExchangeRate', () => {
it('should handle a valid symbol in the API response', async () => {
const controller = new CurrencyRateController({ nativeCurrency: 'usd' });
Expand Down