Skip to content

Commit a7ce511

Browse files
committed
chore: use price-api to fetch exchange rates and fallback to crypto-compare
1 parent 82101f9 commit a7ce511

File tree

8 files changed

+1152
-68
lines changed

8 files changed

+1152
-68
lines changed

packages/assets-controllers/src/CurrencyRateController.test.ts

Lines changed: 393 additions & 49 deletions
Large diffs are not rendered by default.

packages/assets-controllers/src/CurrencyRateController.ts

Lines changed: 76 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { StaticIntervalPollingController } from '@metamask/polling-controller';
1212
import { Mutex } from 'async-mutex';
1313

1414
import { fetchMultiExchangeRate as defaultFetchMultiExchangeRate } from './crypto-compare-service';
15+
import type { AbstractTokenPricesService } from './token-prices-service/abstract-token-prices-service';
1516

1617
/**
1718
* @type CurrencyRateState
@@ -107,6 +108,8 @@ export class CurrencyRateController extends StaticIntervalPollingController<Curr
107108

108109
private readonly useExternalServices: () => boolean;
109110

111+
readonly #tokenPricesService: AbstractTokenPricesService;
112+
110113
/**
111114
* Creates a CurrencyRateController instance.
112115
*
@@ -117,6 +120,7 @@ export class CurrencyRateController extends StaticIntervalPollingController<Curr
117120
* @param options.state - Initial state to set on this controller.
118121
* @param options.useExternalServices - Feature Switch for using external services (default: true)
119122
* @param options.fetchMultiExchangeRate - Fetches the exchange rate from an external API. This option is primarily meant for use in unit tests.
123+
* @param options.tokenPricesService - An object in charge of retrieving token prices
120124
*/
121125
constructor({
122126
includeUsdRate = false,
@@ -125,13 +129,15 @@ export class CurrencyRateController extends StaticIntervalPollingController<Curr
125129
messenger,
126130
state,
127131
fetchMultiExchangeRate = defaultFetchMultiExchangeRate,
132+
tokenPricesService,
128133
}: {
129134
includeUsdRate?: boolean;
130135
interval?: number;
131136
messenger: CurrencyRateMessenger;
132137
state?: Partial<CurrencyRateState>;
133138
useExternalServices?: () => boolean;
134139
fetchMultiExchangeRate?: typeof defaultFetchMultiExchangeRate;
140+
tokenPricesService: AbstractTokenPricesService;
135141
}) {
136142
super({
137143
name,
@@ -143,6 +149,7 @@ export class CurrencyRateController extends StaticIntervalPollingController<Curr
143149
this.useExternalServices = useExternalServices;
144150
this.setIntervalLength(interval);
145151
this.fetchMultiExchangeRate = fetchMultiExchangeRate;
152+
this.#tokenPricesService = tokenPricesService;
146153
}
147154

148155
/**
@@ -168,6 +175,73 @@ export class CurrencyRateController extends StaticIntervalPollingController<Curr
168175
this.updateExchangeRate(nativeCurrencies);
169176
}
170177

178+
async #fetchExchangeRatesWithFallback(
179+
nativeCurrenciesToFetch: Record<string, string>,
180+
): Promise<CurrencyRateState['currencyRates']> {
181+
const { currentCurrency } = this.state;
182+
183+
try {
184+
const priceApiExchangeRatesResponse =
185+
await this.#tokenPricesService.fetchExchangeRates({
186+
baseCurrency: currentCurrency,
187+
includeUsdRate: this.includeUsdRate,
188+
cryptocurrencies: [
189+
...new Set(Object.values(nativeCurrenciesToFetch)),
190+
],
191+
});
192+
193+
const ratesPriceApi = Object.entries(nativeCurrenciesToFetch).reduce(
194+
(acc, [nativeCurrency, fetchedCurrency]) => {
195+
const rate =
196+
priceApiExchangeRatesResponse[fetchedCurrency.toLowerCase()];
197+
198+
acc[nativeCurrency] = {
199+
conversionDate: rate !== undefined ? Date.now() / 1000 : null,
200+
conversionRate: rate?.value
201+
? Number((1 / rate?.value).toFixed(2))
202+
: null,
203+
usdConversionRate: rate?.usd
204+
? Number((1 / rate?.usd).toFixed(2))
205+
: null,
206+
};
207+
return acc;
208+
},
209+
{} as CurrencyRateState['currencyRates'],
210+
);
211+
return ratesPriceApi;
212+
} catch (error) {
213+
console.error('Failed to fetch exchange rates.', error);
214+
}
215+
216+
// fallback to crypto compare
217+
218+
try {
219+
const fetchExchangeRateResponse = await this.fetchMultiExchangeRate(
220+
currentCurrency,
221+
[...new Set(Object.values(nativeCurrenciesToFetch))],
222+
this.includeUsdRate,
223+
);
224+
225+
const rates = Object.entries(nativeCurrenciesToFetch).reduce(
226+
(acc, [nativeCurrency, fetchedCurrency]) => {
227+
const rate = fetchExchangeRateResponse[fetchedCurrency.toLowerCase()];
228+
acc[nativeCurrency] = {
229+
conversionDate: rate !== undefined ? Date.now() / 1000 : null,
230+
conversionRate: rate?.[currentCurrency.toLowerCase()] ?? null,
231+
usdConversionRate: rate?.usd ?? null,
232+
};
233+
return acc;
234+
},
235+
{} as CurrencyRateState['currencyRates'],
236+
);
237+
238+
return rates;
239+
} catch (error) {
240+
console.error('Failed to fetch exchange rates.', error);
241+
throw error;
242+
}
243+
}
244+
171245
/**
172246
* Updates the exchange rate for the current currency and native currency pairs.
173247
*
@@ -182,8 +256,6 @@ export class CurrencyRateController extends StaticIntervalPollingController<Curr
182256

183257
const releaseLock = await this.mutex.acquire();
184258
try {
185-
const { currentCurrency } = this.state;
186-
187259
// For preloaded testnets (Goerli, Sepolia) we want to fetch exchange rate for real ETH.
188260
// Map each native currency to the symbol we want to fetch for it.
189261
const testnetSymbols = Object.values(TESTNET_TICKER_SYMBOLS);
@@ -201,23 +273,8 @@ export class CurrencyRateController extends StaticIntervalPollingController<Curr
201273
{} as Record<string, string>,
202274
);
203275

204-
const fetchExchangeRateResponse = await this.fetchMultiExchangeRate(
205-
currentCurrency,
206-
[...new Set(Object.values(nativeCurrenciesToFetch))],
207-
this.includeUsdRate,
208-
);
209-
210-
const rates = Object.entries(nativeCurrenciesToFetch).reduce(
211-
(acc, [nativeCurrency, fetchedCurrency]) => {
212-
const rate = fetchExchangeRateResponse[fetchedCurrency.toLowerCase()];
213-
acc[nativeCurrency] = {
214-
conversionDate: rate !== undefined ? Date.now() / 1000 : null,
215-
conversionRate: rate?.[currentCurrency.toLowerCase()] ?? null,
216-
usdConversionRate: rate?.usd ?? null,
217-
};
218-
return acc;
219-
},
220-
{} as CurrencyRateState['currencyRates'],
276+
const rates = await this.#fetchExchangeRatesWithFallback(
277+
nativeCurrenciesToFetch,
221278
);
222279

223280
this.update((state) => {

packages/assets-controllers/src/TokenRatesController.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2973,6 +2973,9 @@ function buildMockTokenPricesService(
29732973
async fetchTokenPrices() {
29742974
return {};
29752975
},
2976+
async fetchExchangeRates() {
2977+
return {};
2978+
},
29762979
validateChainIdSupported(_chainId: unknown): _chainId is Hex {
29772980
return true;
29782981
},

packages/assets-controllers/src/TokenSearchDiscoveryDataController/TokenSearchDiscoveryDataController.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ function buildMockTokenPricesService(
130130
overrides: Partial<AbstractTokenPricesService> = {},
131131
): AbstractTokenPricesService {
132132
return {
133+
async fetchExchangeRates() {
134+
return {};
135+
},
133136
async fetchTokenPrices() {
134137
return {};
135138
},

packages/assets-controllers/src/assetsUtil.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,5 +783,8 @@ function createMockPriceService(): AbstractTokenPricesService {
783783
async fetchTokenPrices() {
784784
return {};
785785
},
786+
async fetchExchangeRates() {
787+
return {};
788+
},
786789
};
787790
}

packages/assets-controllers/src/token-prices-service/abstract-token-prices-service.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,17 @@ export type TokenPrice<TokenAddress extends Hex, Currency extends string> = {
2727
totalVolume: number;
2828
};
2929

30+
/**
31+
* Represents an exchange rate.
32+
*/
33+
export type ExchangeRate = {
34+
name: string;
35+
ticker: string;
36+
value: number;
37+
currencyType: string;
38+
usd?: number;
39+
};
40+
3041
/**
3142
* A map of token address to its price.
3243
*/
@@ -37,6 +48,13 @@ export type TokenPricesByTokenAddress<
3748
[A in TokenAddress]: TokenPrice<A, Currency>;
3849
};
3950

51+
/**
52+
* A map of currency to its exchange rate.
53+
*/
54+
export type ExchangeRatesByCurrency<Currency extends string> = {
55+
[C in Currency]: ExchangeRate;
56+
};
57+
4058
/**
4159
* An ideal token prices service. All implementations must confirm to this
4260
* interface.
@@ -75,6 +93,25 @@ export type AbstractTokenPricesService<
7593
currency: Currency;
7694
}): Promise<Partial<TokenPricesByTokenAddress<TokenAddress, Currency>>>;
7795

96+
/**
97+
* Retrieves exchange rates in the given currency.
98+
*
99+
* @param args - The arguments to this function.
100+
* @param args.baseCurrency - The desired currency of the token prices.
101+
* @param args.includeUsdRate - Whether to include the USD rate in the response.
102+
* @param args.cryptocurrencies - The cryptocurrencies to get exchange rates for.
103+
* @returns The exchange rates in the requested base currency.
104+
*/
105+
fetchExchangeRates({
106+
baseCurrency,
107+
includeUsdRate,
108+
cryptocurrencies,
109+
}: {
110+
baseCurrency: Currency;
111+
includeUsdRate: boolean;
112+
cryptocurrencies: string[];
113+
}): Promise<ExchangeRatesByCurrency<Currency>>;
114+
78115
/**
79116
* Type guard for whether the API can return token prices for the given chain
80117
* ID.

0 commit comments

Comments
 (0)