Skip to content

Commit 8a647c1

Browse files
authored
feat: Incorrect token currency shown (#152)
- Add support for multiple currencies in token price conversion and update mock exchange rates - Correct locale and expected value for unsupported currency case
1 parent 2d35b52 commit 8a647c1

File tree

7 files changed

+359
-18
lines changed

7 files changed

+359
-18
lines changed

packages/gator-permissions-snap/snap.manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"url": "https://github.com/MetaMask/snap-7715-permissions.git"
88
},
99
"source": {
10-
"shasum": "2vOYwTM30DDpYzGG2NRfWFDCJ+jxKa36N5T9C1BnyWk=",
10+
"shasum": "LC/2zCP+93w0bFP4+jOLk4KyND/wrxlcPJZpl7oUcMw=",
1111
"location": {
1212
"npm": {
1313
"filePath": "dist/bundle.js",

packages/gator-permissions-snap/src/constants.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,69 @@ export const TRUNCATED_ADDRESS_START_CHARS = 7;
1111
// The number of characters to slice from the end of an address for truncated format:
1212
// `${TRUNCATED_ADDRESS_START_CHARS}...${TRUNCATED_ADDRESS_END_CHARS}`
1313
export const TRUNCATED_ADDRESS_END_CHARS = 5;
14+
15+
// List of all supported currencies by the Price API
16+
export const SUPPORTED_CURRENCIES: readonly string[] = [
17+
'btc',
18+
'eth',
19+
'ltc',
20+
'bch',
21+
'bnb',
22+
'eos',
23+
'xrp',
24+
'xlm',
25+
'link',
26+
'dot',
27+
'yfi',
28+
'usd',
29+
'aed',
30+
'ars',
31+
'aud',
32+
'bdt',
33+
'bhd',
34+
'bmd',
35+
'brl',
36+
'cad',
37+
'chf',
38+
'clp',
39+
'cny',
40+
'czk',
41+
'dkk',
42+
'eur',
43+
'gbp',
44+
'gel',
45+
'hkd',
46+
'huf',
47+
'idr',
48+
'ils',
49+
'inr',
50+
'jpy',
51+
'krw',
52+
'kwd',
53+
'lkr',
54+
'mmk',
55+
'mxn',
56+
'myr',
57+
'ngn',
58+
'nok',
59+
'nzd',
60+
'php',
61+
'pkr',
62+
'pln',
63+
'rub',
64+
'sar',
65+
'sek',
66+
'sgd',
67+
'thb',
68+
'try',
69+
'twd',
70+
'uah',
71+
'vef',
72+
'vnd',
73+
'zar',
74+
'xdr',
75+
'xag',
76+
'xau',
77+
'bits',
78+
'sats',
79+
] as const;

packages/gator-permissions-snap/src/services/tokenPricesService.ts

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { CaipAssetType } from '@metamask/utils';
55

66
import { type PriceApiClient } from '../clients/priceApiClient';
77
import type { VsCurrencyParam } from '../clients/types';
8+
import { SUPPORTED_CURRENCIES } from '../constants';
89
import {
910
FALLBACK_PREFERENCE,
1011
formatAsCurrency,
@@ -27,16 +28,33 @@ export class TokenPricesService {
2728
this.#snapsProvider = snapsProvider;
2829
}
2930

31+
/**
32+
* Check if a currency is supported by the Price API.
33+
* @param currency - The currency to check.
34+
* @returns True if the currency is supported, false otherwise.
35+
*/
36+
#isSupportedCurrency(currency: string): boolean {
37+
return SUPPORTED_CURRENCIES.includes(currency);
38+
}
39+
3040
/**
3141
* Safely parse the user's preferences to determine the currency to use for the token prices.
3242
* @param preferences - The user's preferences.
3343
* @returns The currency to use for the token prices.
3444
*/
3545
#safeParsePreferences(preferences: Preferences): VsCurrencyParam {
36-
const { currency, locale } = preferences;
37-
return locale === 'en'
38-
? (currency.toLowerCase() as VsCurrencyParam)
39-
: (FALLBACK_PREFERENCE.currency.toLowerCase() as VsCurrencyParam);
46+
const { currency } = preferences;
47+
const normalizedCurrency = currency.toLowerCase();
48+
49+
// Check if the currency is supported by the Price API
50+
if (this.#isSupportedCurrency(normalizedCurrency)) {
51+
return normalizedCurrency as VsCurrencyParam;
52+
}
53+
54+
logger.debug(
55+
`TokenPricesService:#safeParsePreferences() - Currency "${currency}" not supported, falling back to USD`,
56+
);
57+
return FALLBACK_PREFERENCE.currency.toLowerCase() as VsCurrencyParam;
4058
}
4159

4260
/**
@@ -78,17 +96,29 @@ export class TokenPricesService {
7896
logger.debug('TokenPricesService:getCryptoToFiatConversion()');
7997
const preferences = await this.#getPreferences();
8098

99+
// Get the currency to use for fetching prices
100+
const vsCurrency = this.#safeParsePreferences(preferences);
101+
102+
// If we're falling back to USD, we need to update the preferences for formatting
103+
const formattingPreferences =
104+
vsCurrency === 'usd' && preferences.currency.toLowerCase() !== 'usd'
105+
? { ...preferences, currency: 'USD' }
106+
: preferences;
107+
81108
// Value in fiat=(Amount in crypto)×(Spot price)
82109
const tokenSpotPrice = await this.#priceApiClient.getSpotPrice(
83110
tokenCaip19Type,
84-
this.#safeParsePreferences(preferences),
111+
vsCurrency,
85112
);
86113
const formattedBalance = Number(
87114
formatUnits({ value: BigInt(balance), decimals }),
88115
);
89116
const valueInFiat = formattedBalance * tokenSpotPrice;
90117

91-
const humanReadableValue = formatAsCurrency(preferences, valueInFiat);
118+
const humanReadableValue = formatAsCurrency(
119+
formattingPreferences,
120+
valueInFiat,
121+
);
92122
logger.debug(
93123
'TokenPricesService:formatAsCurrency() - formatted balance to currency',
94124
humanReadableValue,
@@ -100,7 +130,6 @@ export class TokenPricesService {
100130
'TokenPricesService:getCryptoToFiatConversion() - failed to fetch token spot price',
101131
error,
102132
);
103-
104133
// If we can't fetch the price then show nothing.
105134
return ' ';
106135
}

packages/gator-permissions-snap/src/utils/locale.ts

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,62 @@
1-
// TODO: Add more currencies and locales that we support
2-
export type Locale = 'en';
1+
// data coming from https://github.com/MetaMask/metamask-extension/blob/main/app/_locales/index.json
2+
export type Locale =
3+
| 'am'
4+
| 'ar'
5+
| 'bg'
6+
| 'bn'
7+
| 'ca'
8+
| 'cs'
9+
| 'da'
10+
| 'de'
11+
| 'el'
12+
| 'en'
13+
| 'es'
14+
| 'es_419'
15+
| 'et'
16+
| 'fa'
17+
| 'fi'
18+
| 'fil'
19+
| 'fr'
20+
| 'gu'
21+
| 'he'
22+
| 'hi'
23+
| 'hn'
24+
| 'hr'
25+
| 'ht'
26+
| 'hu'
27+
| 'id'
28+
| 'it'
29+
| 'ja'
30+
| 'kn'
31+
| 'ko'
32+
| 'lt'
33+
| 'lv'
34+
| 'ml'
35+
| 'mr'
36+
| 'ms'
37+
| 'nl'
38+
| 'no'
39+
| 'ph'
40+
| 'pl'
41+
| 'pt'
42+
| 'pt_BR'
43+
| 'pt_PT'
44+
| 'ro'
45+
| 'ru'
46+
| 'sk'
47+
| 'sl'
48+
| 'sr'
49+
| 'sv'
50+
| 'sw'
51+
| 'ta'
52+
| 'te'
53+
| 'th'
54+
| 'tl'
55+
| 'tr'
56+
| 'uk'
57+
| 'vi'
58+
| 'zh_CN'
59+
| 'zh_TW';
360

461
export type Preferences = {
562
locale: Locale;
@@ -25,7 +82,8 @@ export const formatAsCurrency = (
2582
value: number,
2683
decimalPlaces = 2,
2784
): string => {
28-
return new Intl.NumberFormat(preferences.locale, {
85+
// The replace('_', '-') ensures compatibility when metamask uses POSIX-style locale codes but needs to pass them to web APIs that expect BCP 47 format.
86+
return new Intl.NumberFormat(preferences.locale.replace('_', '-'), {
2987
style: 'currency',
3088
currency: preferences.currency,
3189
minimumFractionDigits: decimalPlaces,

packages/gator-permissions-snap/test/service/tokenPricesService.test.ts

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ describe('TokenPricesService', () => {
2121
});
2222

2323
describe('getCryptoToFiatConversion', () => {
24-
describe('ETH -> usd', () => {
24+
describe('ETH -> Multiple currencies', () => {
2525
it('should use usd as fallback currency when no user preferences are found', async () => {
2626
mockPriceApiClient.getSpotPrice.mockResolvedValueOnce(1000);
2727

@@ -168,6 +168,95 @@ describe('TokenPricesService', () => {
168168
method: 'snap_getPreferences',
169169
});
170170
});
171+
172+
it('should respect EUR currency for French users', async () => {
173+
mockPriceApiClient.getSpotPrice.mockResolvedValueOnce(900); // 900 EUR per ETH
174+
175+
mockSnapsProvider.request.mockResolvedValueOnce({
176+
locale: 'fr',
177+
currency: 'EUR',
178+
});
179+
180+
const humanReadableValue =
181+
await tokenPricesService.getCryptoToFiatConversion(
182+
'eip155:1/slip44:60',
183+
bigIntToHex(parseUnits({ formatted: '1', decimals: 18 })), // 1 ETH
184+
18,
185+
);
186+
187+
// French locale uses non-breaking space between number and currency symbol
188+
expect(humanReadableValue).toBe('900,00\u00A0€'); // French locale format
189+
expect(mockPriceApiClient.getSpotPrice).toHaveBeenCalledWith(
190+
'eip155:1/slip44:60',
191+
'eur',
192+
);
193+
});
194+
195+
it('should respect GBP currency for British users', async () => {
196+
mockPriceApiClient.getSpotPrice.mockResolvedValueOnce(800); // 800 GBP per ETH
197+
198+
mockSnapsProvider.request.mockResolvedValueOnce({
199+
locale: 'en-GB',
200+
currency: 'GBP',
201+
});
202+
203+
const humanReadableValue =
204+
await tokenPricesService.getCryptoToFiatConversion(
205+
'eip155:1/slip44:60',
206+
bigIntToHex(parseUnits({ formatted: '0.5', decimals: 18 })), // 0.5 ETH
207+
18,
208+
);
209+
210+
expect(humanReadableValue).toBe('£400.00');
211+
expect(mockPriceApiClient.getSpotPrice).toHaveBeenCalledWith(
212+
'eip155:1/slip44:60',
213+
'gbp',
214+
);
215+
});
216+
217+
it('should fall back to USD for unsupported currencies', async () => {
218+
mockPriceApiClient.getSpotPrice.mockResolvedValueOnce(1000); // 1000 USD per ETH
219+
220+
mockSnapsProvider.request.mockResolvedValueOnce({
221+
locale: 'en',
222+
currency: 'XXX', // Not a supported currency
223+
});
224+
225+
const humanReadableValue =
226+
await tokenPricesService.getCryptoToFiatConversion(
227+
'eip155:1/slip44:60',
228+
bigIntToHex(parseUnits({ formatted: '0.1', decimals: 18 })), // 0.1 ETH
229+
18,
230+
);
231+
232+
expect(humanReadableValue).toBe('$100.00');
233+
expect(mockPriceApiClient.getSpotPrice).toHaveBeenCalledWith(
234+
'eip155:1/slip44:60',
235+
'usd', // Falls back to USD
236+
);
237+
});
238+
239+
it('should handle case-insensitive currency codes', async () => {
240+
mockPriceApiClient.getSpotPrice.mockResolvedValueOnce(3500); // 3500 CAD per ETH
241+
242+
mockSnapsProvider.request.mockResolvedValueOnce({
243+
locale: 'en-CA',
244+
currency: 'CAD', // Uppercase
245+
});
246+
247+
const humanReadableValue =
248+
await tokenPricesService.getCryptoToFiatConversion(
249+
'eip155:1/slip44:60',
250+
bigIntToHex(parseUnits({ formatted: '0.2', decimals: 18 })), // 0.2 ETH
251+
18,
252+
);
253+
254+
expect(humanReadableValue).toBe('$700.00'); // CAD uses $ symbol
255+
expect(mockPriceApiClient.getSpotPrice).toHaveBeenCalledWith(
256+
'eip155:1/slip44:60',
257+
'cad', // Lowercase
258+
);
259+
});
171260
});
172261
});
173262
});

0 commit comments

Comments
 (0)