@cemalidev/trate 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/convert.ts ADDED
@@ -0,0 +1,562 @@
1
+ import fetch from 'node-fetch';
2
+ import { configService } from './config/config';
3
+ import { t } from './i18n';
4
+ import { logger } from './utils/logger';
5
+
6
+ const TRATE_API = 'https://trate-api.cemali.dev/v1';
7
+
8
+ const cache = new Map<string, { data: any; timestamp: number; base?: string }>();
9
+ const CACHE_TTL = 5 * 60 * 1000;
10
+
11
+ const rateLimitMap = new Map<string, { count: number; resetTime: number }>();
12
+ const RATE_LIMIT_WINDOW = 60 * 1000;
13
+ const RATE_LIMIT_MAX = 30;
14
+
15
+ function checkRateLimit(endpoint: string): boolean {
16
+ const now = Date.now();
17
+ const entry = rateLimitMap.get(endpoint);
18
+
19
+ if (!entry || now > entry.resetTime) {
20
+ rateLimitMap.set(endpoint, { count: 1, resetTime: now + RATE_LIMIT_WINDOW });
21
+ return true;
22
+ }
23
+
24
+ if (entry.count >= RATE_LIMIT_MAX) {
25
+ logger.warn(`Rate limit exceeded for ${endpoint}`, { resetIn: entry.resetTime - now });
26
+ return false;
27
+ }
28
+
29
+ entry.count++;
30
+ return true;
31
+ }
32
+
33
+ export const cacheApi = {
34
+ clear: () => cache.clear(),
35
+ };
36
+
37
+ export const CRYPTO_SYMBOLS = [
38
+ 'BTC',
39
+ 'ETH',
40
+ 'SOL',
41
+ 'BNB',
42
+ 'XRP',
43
+ 'ADA',
44
+ 'DOGE',
45
+ 'DOT',
46
+ 'AVAX',
47
+ 'MATIC',
48
+ 'LINK',
49
+ 'UNI',
50
+ 'LTC',
51
+ 'ATOM',
52
+ ];
53
+
54
+ export const METAL_SYMBOLS = ['ONS', 'GUMUS_OZ', 'GRAM_ALTIN'];
55
+
56
+ export const JEWELRY_SYMBOLS = ['CEYREK_ALTIN', 'YARIM_ALTIN', 'TAM_ALTIN', 'ATA_ALTIN'];
57
+
58
+ export const SUPPORTED_FIAT = [
59
+ 'EUR',
60
+ 'USD',
61
+ 'GBP',
62
+ 'JPY',
63
+ 'CHF',
64
+ 'CAD',
65
+ 'AUD',
66
+ 'NZD',
67
+ 'CNY',
68
+ 'HKD',
69
+ 'SGD',
70
+ 'INR',
71
+ 'RUB',
72
+ 'TRY',
73
+ 'BRL',
74
+ 'ZAR',
75
+ 'MXN',
76
+ ];
77
+
78
+ export const JEWELRY_API_MAP: Record<string, string> = {
79
+ CEYREK_ALTIN: 'ceyrek_altin',
80
+ YARIM_ALTIN: 'yarim_altin',
81
+ TAM_ALTIN: 'tam_altin',
82
+ ATA_ALTIN: 'ata_altin',
83
+ };
84
+
85
+ export const METAL_API_MAP: Record<string, string> = {
86
+ ONS: 'ons_altin',
87
+ GUMUS_OZ: 'gumus_ons',
88
+ GRAM_ALTIN: 'gram_altin',
89
+ };
90
+
91
+ export function isCrypto(symbol: string): boolean {
92
+ return CRYPTO_SYMBOLS.includes(symbol.toUpperCase());
93
+ }
94
+
95
+ export function isMetal(symbol: string): boolean {
96
+ return METAL_SYMBOLS.includes(symbol.toUpperCase());
97
+ }
98
+
99
+ export function isJewelry(symbol: string): boolean {
100
+ return JEWELRY_SYMBOLS.includes(symbol.toUpperCase());
101
+ }
102
+
103
+ export function isFiat(symbol: string): boolean {
104
+ return SUPPORTED_FIAT.includes(symbol.toUpperCase());
105
+ }
106
+
107
+ function getCached<T>(key: string): T | null {
108
+ const cached = cache.get(key);
109
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
110
+ return cached.data as T;
111
+ }
112
+ return null;
113
+ }
114
+
115
+ function setCache(key: string, data: any, base?: string): void {
116
+ cache.set(key, { data, timestamp: Date.now(), base });
117
+ }
118
+
119
+ async function fetchWithTimeout(
120
+ url: string,
121
+ timeout = 8000
122
+ ): Promise<import('node-fetch').Response> {
123
+ const controller = new AbortController();
124
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
125
+
126
+ try {
127
+ const response = (await fetch(url, {
128
+ signal: controller.signal,
129
+ headers: {
130
+ 'X-Client-ID': configService.clientId,
131
+ 'User-Agent': 'trate-cli/1.0.0',
132
+ },
133
+ })) as import('node-fetch').Response;
134
+ clearTimeout(timeoutId);
135
+ return response;
136
+ } catch (error) {
137
+ clearTimeout(timeoutId);
138
+ throw error;
139
+ }
140
+ }
141
+
142
+ async function fetchFiatRates(): Promise<Record<string, number>> {
143
+ const cacheKey = 'fiat-rates';
144
+ const cached = getCached<Record<string, number>>(cacheKey);
145
+ if (cached) return cached;
146
+
147
+ if (!checkRateLimit('fiat')) {
148
+ throw new Error(t('error.noConnection'));
149
+ }
150
+
151
+ try {
152
+ const response = await fetchWithTimeout(`${TRATE_API}/latest`);
153
+ if (!response.ok) throw new Error();
154
+ const data = (await response.json()) as { rates: Record<string, number> };
155
+ if (!data.rates || Object.keys(data.rates).length === 0) throw new Error('Empty rates');
156
+ setCache(cacheKey, data.rates, 'USD');
157
+ return data.rates;
158
+ } catch {
159
+ throw new Error(t('error.noConnection'));
160
+ }
161
+ }
162
+
163
+ function getFiatRateBase(): string | undefined {
164
+ const cached = cache.get('fiat-rates');
165
+ return cached?.base;
166
+ }
167
+
168
+ async function fetchCryptoRates(): Promise<Record<string, number>> {
169
+ const cacheKey = 'crypto-rates';
170
+ const cached = getCached<Record<string, number>>(cacheKey);
171
+ if (cached) return cached;
172
+
173
+ if (!checkRateLimit('crypto')) {
174
+ throw new Error(t('error.noConnection'));
175
+ }
176
+
177
+ try {
178
+ const response = await fetchWithTimeout(`${TRATE_API}/crypto`);
179
+ if (!response.ok) throw new Error();
180
+ const data = (await response.json()) as { prices: Record<string, number> };
181
+ setCache(cacheKey, data.prices);
182
+ return data.prices;
183
+ } catch {
184
+ throw new Error(t('error.noConnection'));
185
+ }
186
+ }
187
+
188
+ async function fetchMetalsPrices(): Promise<Record<string, { alis: number; satis: number }>> {
189
+ const cacheKey = 'metals-prices';
190
+ const cached = getCached<Record<string, { alis: number; satis: number }>>(cacheKey);
191
+ if (cached) return cached;
192
+
193
+ if (!checkRateLimit('metals')) {
194
+ throw new Error(t('error.noConnection'));
195
+ }
196
+
197
+ try {
198
+ const response = await fetchWithTimeout(`${TRATE_API}/metals`);
199
+ if (!response.ok) throw new Error();
200
+ const data = (await response.json()) as {
201
+ prices: Record<string, { alis: number; satis: number }>;
202
+ };
203
+ if (!data.prices || Object.keys(data.prices).length === 0) {
204
+ throw new Error(t('error.goldPricesUnavailable'));
205
+ }
206
+ setCache(cacheKey, data.prices);
207
+ return data.prices;
208
+ } catch (error) {
209
+ if (
210
+ error instanceof Error &&
211
+ error.message.includes(t('error.goldPricesUnavailable').replace('ş', 's').replace('ı', 'i'))
212
+ ) {
213
+ throw error;
214
+ }
215
+ throw new Error(t('error.goldPricesLoadFailed'), { cause: error });
216
+ }
217
+ }
218
+
219
+ export interface ConversionResult {
220
+ success: boolean;
221
+ result?: number;
222
+ rate?: number;
223
+ base?: string;
224
+ date?: string;
225
+ error?: string;
226
+ }
227
+
228
+ export async function convert(amount: number, from: string, to: string): Promise<ConversionResult> {
229
+ try {
230
+ from = from.toUpperCase();
231
+ to = to.toUpperCase();
232
+
233
+ if (from === to) {
234
+ return {
235
+ success: true,
236
+ result: amount,
237
+ rate: 1,
238
+ base: from,
239
+ date: new Date().toISOString().split('T')[0],
240
+ };
241
+ }
242
+
243
+ const fromIsCrypto = isCrypto(from);
244
+ const toIsCrypto = isCrypto(to);
245
+ const fromIsMetal = isMetal(from);
246
+ const toIsMetal = isMetal(to);
247
+ const fromIsJewelry = isJewelry(from);
248
+ const toIsJewelry = isJewelry(to);
249
+ const fromIsFiat = isFiat(from);
250
+ const toIsFiat = isFiat(to);
251
+
252
+ let result: number;
253
+ let rate: number;
254
+
255
+ if (fromIsJewelry && toIsJewelry) {
256
+ const metalsPrices = await fetchMetalsPrices();
257
+ const fromApiKey = JEWELRY_API_MAP[from];
258
+ const toApiKey = JEWELRY_API_MAP[to];
259
+ const fromPriceTRY = metalsPrices[fromApiKey].satis;
260
+ const toPriceTRY = metalsPrices[toApiKey].satis;
261
+ rate = fromPriceTRY / toPriceTRY;
262
+ result = amount * rate;
263
+ } else if (fromIsJewelry && toIsFiat) {
264
+ const metalsPrices = await fetchMetalsPrices();
265
+ const fiatRates = await fetchFiatRates();
266
+ const fromApiKey = JEWELRY_API_MAP[from];
267
+ const jewelryPriceTRY = metalsPrices[fromApiKey].satis;
268
+ const jewelryPriceUSD = jewelryPriceTRY / fiatRates['TRY'];
269
+ if (to === 'USD') {
270
+ rate = jewelryPriceUSD;
271
+ } else {
272
+ const targetToUSD = fiatRates[to];
273
+ if (!targetToUSD) {
274
+ return { success: false, error: `${to} ${t('error.rateNotFound')}` };
275
+ }
276
+ rate = jewelryPriceUSD * targetToUSD;
277
+ }
278
+ result = amount * rate;
279
+ } else if (fromIsFiat && toIsJewelry) {
280
+ const metalsPrices = await fetchMetalsPrices();
281
+ const fiatRates = await fetchFiatRates();
282
+ const toApiKey = JEWELRY_API_MAP[to];
283
+ const jewelryPriceTRY = metalsPrices[toApiKey].satis;
284
+ const jewelryPriceUSD = jewelryPriceTRY / fiatRates['TRY'];
285
+ if (from === 'USD') {
286
+ rate = 1 / jewelryPriceUSD;
287
+ } else {
288
+ const fiatToUSD = fiatRates[from];
289
+ if (!fiatToUSD) {
290
+ return { success: false, error: `${from} ${t('error.rateNotFound')}` };
291
+ }
292
+ rate = fiatToUSD / jewelryPriceUSD;
293
+ }
294
+ result = amount * rate;
295
+ } else if (fromIsJewelry && toIsCrypto) {
296
+ const metalsPrices = await fetchMetalsPrices();
297
+ const fiatRates = await fetchFiatRates();
298
+ const cryptoRates = await fetchCryptoRates();
299
+ const fromApiKey = JEWELRY_API_MAP[from];
300
+ const jewelryPriceTRY = metalsPrices[fromApiKey].satis;
301
+ const cryptoPriceUSD = cryptoRates[to];
302
+ const usdToTry = fiatRates['TRY'];
303
+ const jewelryPriceUSD = jewelryPriceTRY / usdToTry;
304
+ rate = jewelryPriceUSD / cryptoPriceUSD;
305
+ result = amount * rate;
306
+ } else if (fromIsCrypto && toIsJewelry) {
307
+ const metalsPrices = await fetchMetalsPrices();
308
+ const fiatRates = await fetchFiatRates();
309
+ const cryptoRates = await fetchCryptoRates();
310
+ const toApiKey = JEWELRY_API_MAP[to];
311
+ const jewelryPriceTRY = metalsPrices[toApiKey].satis;
312
+ const cryptoPriceUSD = cryptoRates[from];
313
+ const usdToTry = fiatRates['TRY'];
314
+ const cryptoInTRY = cryptoPriceUSD * usdToTry;
315
+ rate = cryptoInTRY / jewelryPriceTRY;
316
+ result = amount * rate;
317
+ } else if (fromIsMetal && toIsFiat) {
318
+ const metalsPrices = await fetchMetalsPrices();
319
+ const fiatRates = await fetchFiatRates();
320
+ const apiKey = METAL_API_MAP[from];
321
+ let metalPriceTRY: number;
322
+ if (from === 'ONS') {
323
+ metalPriceTRY = metalsPrices[apiKey].satis;
324
+ } else if (from === 'ALTIN_KG') {
325
+ metalPriceTRY = metalsPrices[apiKey].satis * 10.33;
326
+ } else {
327
+ metalPriceTRY = metalsPrices[apiKey].satis;
328
+ }
329
+ const metalPriceUSD = metalPriceTRY / fiatRates['TRY'];
330
+ if (to === 'USD') {
331
+ rate = metalPriceUSD;
332
+ } else {
333
+ const targetToUSD = fiatRates[to];
334
+ if (!targetToUSD) {
335
+ return { success: false, error: `${to} ${t('error.rateNotFound')}` };
336
+ }
337
+ rate = metalPriceUSD * targetToUSD;
338
+ }
339
+ result = amount * rate;
340
+ } else if (fromIsFiat && toIsMetal) {
341
+ const metalsPrices = await fetchMetalsPrices();
342
+ const fiatRates = await fetchFiatRates();
343
+ const apiKey = METAL_API_MAP[to];
344
+ let metalPriceTRY: number;
345
+ if (to === 'ONS') {
346
+ metalPriceTRY = metalsPrices[apiKey].satis;
347
+ } else if (to === 'ALTIN_KG') {
348
+ metalPriceTRY = metalsPrices[apiKey].satis * 10.33;
349
+ } else {
350
+ metalPriceTRY = metalsPrices[apiKey].satis;
351
+ }
352
+ const metalPriceUSD = metalPriceTRY / fiatRates['TRY'];
353
+ if (from === 'USD') {
354
+ rate = 1 / metalPriceUSD;
355
+ } else {
356
+ const fiatToUSD = fiatRates[from];
357
+ if (!fiatToUSD) {
358
+ return { success: false, error: `${from} ${t('error.rateNotFound')}` };
359
+ }
360
+ rate = fiatToUSD / metalPriceUSD;
361
+ }
362
+ result = amount * rate;
363
+ } else if (fromIsMetal && toIsCrypto) {
364
+ const metalsPrices = await fetchMetalsPrices();
365
+ const fiatRates = await fetchFiatRates();
366
+ const cryptoRates = await fetchCryptoRates();
367
+ const apiKey = METAL_API_MAP[from];
368
+ let metalPriceTRY: number;
369
+ if (from === 'ONS') {
370
+ metalPriceTRY = metalsPrices[apiKey].satis;
371
+ } else if (from === 'ALTIN_KG') {
372
+ metalPriceTRY = metalsPrices[apiKey].satis * 10.33;
373
+ } else {
374
+ metalPriceTRY = metalsPrices[apiKey].satis;
375
+ }
376
+ const metalPriceUSD = metalPriceTRY / fiatRates['TRY'];
377
+ const cryptoPriceUSD = cryptoRates[to];
378
+ rate = metalPriceUSD / cryptoPriceUSD;
379
+ result = amount * rate;
380
+ } else if (fromIsCrypto && toIsMetal) {
381
+ const metalsPrices = await fetchMetalsPrices();
382
+ const fiatRates = await fetchFiatRates();
383
+ const cryptoRates = await fetchCryptoRates();
384
+ const apiKey = METAL_API_MAP[to];
385
+ let metalPriceTRY: number;
386
+ if (to === 'ONS') {
387
+ metalPriceTRY = metalsPrices[apiKey].satis;
388
+ } else if (to === 'ALTIN_KG') {
389
+ metalPriceTRY = metalsPrices[apiKey].satis * 10.33;
390
+ } else {
391
+ metalPriceTRY = metalsPrices[apiKey].satis;
392
+ }
393
+ const cryptoPriceUSD = cryptoRates[from];
394
+ const cryptoInTRY = cryptoPriceUSD * fiatRates['TRY'];
395
+ rate = cryptoInTRY / metalPriceTRY;
396
+ result = amount * rate;
397
+ } else if (fromIsCrypto && toIsCrypto) {
398
+ const cryptoRates = await fetchCryptoRates();
399
+ const fromPrice = cryptoRates[from];
400
+ const toPrice = cryptoRates[to];
401
+ if (!fromPrice || !toPrice) {
402
+ return { success: false, error: `${!fromPrice ? from : to} ${t('error.notFound')}` };
403
+ }
404
+ rate = fromPrice / toPrice;
405
+ result = amount * rate;
406
+ } else if (fromIsCrypto && toIsFiat) {
407
+ const cryptoRates = await fetchCryptoRates();
408
+ const fiatRates = await fetchFiatRates();
409
+ const cryptoPriceUSD = cryptoRates[from];
410
+ if (!cryptoPriceUSD) {
411
+ return { success: false, error: `${from} ${t('error.notFound')}` };
412
+ }
413
+ if (to === 'USD') {
414
+ rate = cryptoPriceUSD;
415
+ } else {
416
+ const targetToUSD = fiatRates[to];
417
+ if (!targetToUSD) {
418
+ return { success: false, error: `${to} ${t('error.rateNotFound')}` };
419
+ }
420
+ rate = cryptoPriceUSD * targetToUSD;
421
+ }
422
+ result = amount * rate;
423
+ } else if (fromIsFiat && toIsCrypto) {
424
+ const cryptoRates = await fetchCryptoRates();
425
+ const fiatRates = await fetchFiatRates();
426
+ const cryptoPriceUSD = cryptoRates[to];
427
+ if (!cryptoPriceUSD) {
428
+ return { success: false, error: `${to} ${t('error.notFound')}` };
429
+ }
430
+ if (from === 'USD') {
431
+ rate = 1 / cryptoPriceUSD;
432
+ } else {
433
+ const fiatToUSD = fiatRates[from];
434
+ if (!fiatToUSD) {
435
+ return { success: false, error: `${from} ${t('error.rateNotFound')}` };
436
+ }
437
+ rate = fiatToUSD / cryptoPriceUSD;
438
+ }
439
+ result = amount * rate;
440
+ } else {
441
+ const fiatRates = await fetchFiatRates();
442
+ const rateBase = getFiatRateBase();
443
+ const fromRate = fiatRates[from];
444
+ const toRate = fiatRates[to];
445
+ if (!fromRate) {
446
+ return { success: false, error: `${from} ${t('error.rateNotFound')}` };
447
+ }
448
+ if (!toRate) {
449
+ return { success: false, error: `${to} ${t('error.rateNotFound')}` };
450
+ }
451
+ if (rateBase === 'USD') {
452
+ rate = toRate / fromRate;
453
+ } else {
454
+ rate = fromRate / toRate;
455
+ }
456
+ result = amount * rate;
457
+ }
458
+
459
+ return {
460
+ success: true,
461
+ result: result < 0.01 ? parseFloat(result.toFixed(8)) : Math.round(result * 100) / 100,
462
+ rate: result < 0.01 ? parseFloat(rate.toPrecision(6)) : Math.round(rate * 1000000) / 1000000,
463
+ base: configService.baseCurrency,
464
+ date: new Date().toISOString().split('T')[0],
465
+ };
466
+ } catch (error) {
467
+ return {
468
+ success: false,
469
+ error: error instanceof Error ? error.message : t('error.noConnection'),
470
+ };
471
+ }
472
+ }
473
+
474
+ export async function getLatestRates(
475
+ _base: string
476
+ ): Promise<{ base: string; date: string; rates: Record<string, number> }> {
477
+ const data = await fetchFiatRates();
478
+ const rateBase = getFiatRateBase() || configService.baseCurrency;
479
+ return {
480
+ base: rateBase,
481
+ date: new Date().toISOString().split('T')[0],
482
+ rates: data,
483
+ };
484
+ }
485
+
486
+ export async function getRate(symbol: string): Promise<number | null> {
487
+ try {
488
+ symbol = symbol.toUpperCase();
489
+
490
+ if (isCrypto(symbol)) {
491
+ const rates = await fetchCryptoRates();
492
+ return rates[symbol] || null;
493
+ }
494
+
495
+ if (isJewelry(symbol)) {
496
+ const prices = await fetchMetalsPrices();
497
+ const apiKey = JEWELRY_API_MAP[symbol];
498
+ return prices[apiKey]?.satis || null;
499
+ }
500
+
501
+ if (isMetal(symbol)) {
502
+ const prices = await fetchMetalsPrices();
503
+ const apiKey = METAL_API_MAP[symbol];
504
+ return prices[apiKey]?.satis || null;
505
+ }
506
+
507
+ const rates = await fetchFiatRates();
508
+ return rates[symbol] || null;
509
+ } catch {
510
+ return null;
511
+ }
512
+ }
513
+
514
+ export async function getFiatRate(from: string, to: string): Promise<number | null> {
515
+ try {
516
+ const rates = await fetchFiatRates();
517
+ const fromRate = rates[from.toUpperCase()];
518
+ const toRate = rates[to.toUpperCase()];
519
+ if (!fromRate || !toRate) return null;
520
+ return fromRate / toRate;
521
+ } catch {
522
+ return null;
523
+ }
524
+ }
525
+
526
+ export async function getCryptoRate(symbol: string): Promise<number | null> {
527
+ try {
528
+ const rates = await fetchCryptoRates();
529
+ return rates[symbol.toUpperCase()] || null;
530
+ } catch {
531
+ return null;
532
+ }
533
+ }
534
+
535
+ export function getFiatList(): string[] {
536
+ return SUPPORTED_FIAT;
537
+ }
538
+
539
+ export function getCryptoList(): string[] {
540
+ return CRYPTO_SYMBOLS;
541
+ }
542
+
543
+ export function getMetalList(): string[] {
544
+ return METAL_SYMBOLS;
545
+ }
546
+
547
+ export function getJewelryList(): string[] {
548
+ return JEWELRY_SYMBOLS;
549
+ }
550
+
551
+ export function getDisplayName(symbol: string): string {
552
+ const displayNames: Record<string, string> = {
553
+ ONS: t('display.goldOunce'),
554
+ GUMUS_OZ: t('display.silverOunce'),
555
+ GRAM_ALTIN: t('display.goldKg'),
556
+ CEYREK_ALTIN: t('display.quarterGold'),
557
+ YARIM_ALTIN: t('display.halfGold'),
558
+ TAM_ALTIN: t('display.fullGold'),
559
+ ATA_ALTIN: t('display.ataGold'),
560
+ };
561
+ return displayNames[symbol.toUpperCase()] || symbol;
562
+ }
@@ -0,0 +1,76 @@
1
+ import { configService } from './config/config';
2
+ import { convert, getDisplayName, isCrypto, isMetal, isJewelry } from './convert';
3
+ import { colors } from './ui';
4
+ import { t } from './i18n';
5
+
6
+ export async function displayFavoritesDashboard(): Promise<string> {
7
+ const config = configService.getAll();
8
+ const favorites = config.favorites;
9
+ const base = config.baseCurrency;
10
+
11
+ const lines: string[] = [];
12
+ lines.push(`\n ${colors.bold(t('dashboard.title'))} ${t('ui.base')}: ${colors.cyan(base)}`);
13
+ lines.push(colors.dim(' ' + '─'.repeat(70)));
14
+
15
+ const header = ` ${colors.bold(t('ui.symbol'))} ${colors.bold(t('ui.price'))} ${colors.bold(t('ui.type'))}`;
16
+ lines.push(header);
17
+ lines.push(colors.dim(' ' + '─'.repeat(70)));
18
+
19
+ const promises = favorites.map(async symbol => {
20
+ const isCryptoSymbol = isCrypto(symbol);
21
+ const isMetalSymbol = isMetal(symbol);
22
+ const isJewelrySymbol = isJewelry(symbol);
23
+
24
+ const rate = await convert(1, symbol, base);
25
+
26
+ if (rate.success) {
27
+ const displaySymbol = isMetalSymbol || isJewelrySymbol ? getDisplayName(symbol) : symbol;
28
+ const resultStr =
29
+ rate.result! < 0.01 ? rate.result!.toPrecision(6) : rate.result!.toLocaleString();
30
+
31
+ let typeLabel = t('dashboard.type.fiat');
32
+ let symbolColor = colors.cyan(symbol);
33
+
34
+ if (isCryptoSymbol) {
35
+ typeLabel = t('dashboard.type.crypto');
36
+ symbolColor = colors.yellow(symbol);
37
+ } else if (isMetalSymbol) {
38
+ typeLabel = t('dashboard.type.metal');
39
+ symbolColor = colors.magenta(symbol);
40
+ } else if (isJewelrySymbol) {
41
+ typeLabel = t('dashboard.type.gold');
42
+ symbolColor = colors.magenta(symbol + ' 🏆');
43
+ }
44
+
45
+ return {
46
+ symbol: displaySymbol,
47
+ type: typeLabel,
48
+ symbolColor,
49
+ price: resultStr,
50
+ rate: `1 ${symbol}`,
51
+ };
52
+ }
53
+
54
+ return null;
55
+ });
56
+
57
+ const results = await Promise.all(promises);
58
+
59
+ for (const result of results) {
60
+ if (result) {
61
+ lines.push(
62
+ ` ${result.symbolColor.padEnd(14)} ${result.rate.padEnd(8)} ${base.padEnd(12)} ${colors.dim(result.type)}`
63
+ );
64
+ }
65
+ }
66
+
67
+ lines.push(colors.dim(' ' + '─'.repeat(70)));
68
+ lines.push(
69
+ ` ${colors.dim(t('ui.tip') + ':')} Use ${colors.cyan('trate <symbol>')} ${t('dashboard.usage').toLowerCase()}`
70
+ );
71
+ lines.push(
72
+ ` ${colors.dim(t('ui.tip') + ':')} Use ${colors.cyan('trate add <currency>')} ${t('dashboard.usageAdd').toLowerCase()}`
73
+ );
74
+
75
+ return lines.join('\n');
76
+ }