Skip to content

Commit

Permalink
CurrencyController can be configured to always fetch usd rate (MetaMa…
Browse files Browse the repository at this point in the history
…sk#292)

* CurrencyController can be configured to always fetch usd rate in addition to currentCurrency rate

* Update unit tests

Co-authored-by: Mark Stacey <markjstacey@gmail.com>
  • Loading branch information
danjm and Gudahtt committed Nov 6, 2020
1 parent 1450178 commit 282102c
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 9 deletions.
34 changes: 26 additions & 8 deletions src/assets/CurrencyRateController.ts
Original file line number Diff line number Diff 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 Down Expand 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 number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ describe('ComposableController', () => {
conversionRate: 0,
currentCurrency: 'usd',
nativeCurrency: 'ETH',
usdConversionRate: 0,
},
EnsController: {
ensEntries: {},
Expand Down Expand Up @@ -96,6 +97,7 @@ describe('ComposableController', () => {
selectedAddress: '',
suggestedAssets: [],
tokens: [],
usdConversionRate: 0,
});
});

Expand Down Expand 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 number Diff line number Diff 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 Down Expand 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

0 comments on commit 282102c

Please sign in to comment.