@provex/utils 1.0.16 → 1.2.3

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.
Files changed (122) hide show
  1. package/dist/src/chain.d.ts +436 -4
  2. package/dist/src/chain.js +80 -0
  3. package/dist/src/chain.test.d.ts +1 -0
  4. package/dist/src/chain.test.js +112 -0
  5. package/dist/src/contracts.d.ts +196 -20
  6. package/dist/src/contracts.js +245 -17
  7. package/dist/src/contracts.test.d.ts +1 -0
  8. package/dist/src/contracts.test.js +243 -0
  9. package/dist/src/conversionRates.d.ts +99 -8
  10. package/dist/src/conversionRates.js +74 -4
  11. package/dist/src/conversionRates.test.d.ts +1 -0
  12. package/dist/src/conversionRates.test.js +473 -0
  13. package/dist/src/currencies.d.ts +113 -9
  14. package/dist/src/currencies.js +119 -38
  15. package/dist/src/currencies.test.d.ts +1 -0
  16. package/dist/src/currencies.test.js +343 -0
  17. package/dist/src/data/countries.json +166 -166
  18. package/dist/src/deposit.d.ts +69 -1
  19. package/dist/src/deposit.js +52 -1
  20. package/dist/src/depositKeys.d.ts +154 -0
  21. package/dist/src/depositKeys.js +104 -0
  22. package/dist/src/depositKeys.test.d.ts +1 -0
  23. package/dist/src/depositKeys.test.js +231 -0
  24. package/dist/src/endpoints.d.ts +51 -0
  25. package/dist/src/endpoints.js +70 -0
  26. package/dist/src/endpoints.test.d.ts +1 -0
  27. package/dist/src/endpoints.test.js +54 -0
  28. package/dist/src/events.d.ts +40 -2
  29. package/dist/src/events.js +86 -10
  30. package/dist/src/events.test.d.ts +1 -0
  31. package/dist/src/events.test.js +111 -0
  32. package/dist/src/fees.d.ts +136 -0
  33. package/dist/src/fees.js +178 -0
  34. package/dist/src/fees.test.d.ts +1 -0
  35. package/dist/src/fees.test.js +165 -0
  36. package/dist/src/ids.d.ts +125 -22
  37. package/dist/src/ids.js +211 -61
  38. package/dist/src/ids.test.d.ts +1 -0
  39. package/dist/src/ids.test.js +607 -0
  40. package/dist/src/index.d.ts +30 -1
  41. package/dist/src/index.js +30 -1
  42. package/dist/src/input.d.ts +23 -4
  43. package/dist/src/input.js +23 -4
  44. package/dist/src/intent.d.ts +38 -2
  45. package/dist/src/intent.js +28 -1
  46. package/dist/src/intent.test.d.ts +1 -0
  47. package/dist/src/intent.test.js +119 -0
  48. package/dist/src/json.d.ts +38 -1
  49. package/dist/src/json.js +38 -2
  50. package/dist/src/normalization.d.ts +224 -0
  51. package/dist/src/normalization.js +119 -0
  52. package/dist/src/normalization.test.d.ts +1 -0
  53. package/dist/src/normalization.test.js +284 -0
  54. package/dist/src/participants.d.ts +19 -4
  55. package/dist/src/participants.js +19 -4
  56. package/dist/src/payment.d.ts +333 -16
  57. package/dist/src/payment.js +768 -58
  58. package/dist/src/payment.test.d.ts +1 -0
  59. package/dist/src/payment.test.js +272 -0
  60. package/dist/src/peer-types.d.ts +327 -0
  61. package/dist/src/peer-types.js +169 -0
  62. package/dist/src/profit.d.ts +26 -5
  63. package/dist/src/profit.js +31 -8
  64. package/dist/src/profit.test.d.ts +1 -0
  65. package/dist/src/profit.test.js +315 -0
  66. package/dist/src/reputation.d.ts +125 -0
  67. package/dist/src/reputation.js +188 -0
  68. package/dist/src/reputation.test.d.ts +1 -0
  69. package/dist/src/reputation.test.js +156 -0
  70. package/dist/src/sort.d.ts +49 -10
  71. package/dist/src/sort.js +40 -9
  72. package/dist/src/sort.test.d.ts +1 -0
  73. package/dist/src/sort.test.js +216 -0
  74. package/dist/src/stateKey.d.ts +35 -0
  75. package/dist/src/stateKey.js +36 -0
  76. package/dist/src/stats.d.ts +55 -1
  77. package/dist/src/stats.js +46 -1
  78. package/dist/src/stats.test.d.ts +1 -0
  79. package/dist/src/stats.test.js +104 -0
  80. package/dist/src/time.d.ts +59 -9
  81. package/dist/src/time.js +71 -15
  82. package/dist/src/time.test.d.ts +1 -0
  83. package/dist/src/time.test.js +231 -0
  84. package/dist/src/tokens.d.ts +11972 -145
  85. package/dist/src/tokens.js +98 -17
  86. package/dist/src/tokens.test.d.ts +1 -0
  87. package/dist/src/tokens.test.js +193 -0
  88. package/dist/src/units.d.ts +183 -0
  89. package/dist/src/units.js +206 -6
  90. package/dist/src/units.test.d.ts +1 -0
  91. package/dist/src/units.test.js +327 -0
  92. package/dist/src/zkp2p-types.d.ts +273 -4
  93. package/dist/src/zkp2p-types.js +169 -1
  94. package/dist/tsconfig.tsbuildinfo +1 -1
  95. package/package.json +116 -26
  96. package/scripts/convert-svg.ts +0 -22
  97. package/src/chain.ts +0 -27
  98. package/src/contracts.ts +0 -89
  99. package/src/conversionRates.ts +0 -356
  100. package/src/currencies.ts +0 -286
  101. package/src/data/countries.json +0 -1099
  102. package/src/deposit.ts +0 -2
  103. package/src/events.ts +0 -21
  104. package/src/flags/eur.svg +0 -4
  105. package/src/flags/usd.svg +0 -25
  106. package/src/flags.ts +0 -4
  107. package/src/global.d.ts +0 -9
  108. package/src/ids.ts +0 -137
  109. package/src/index.ts +0 -16
  110. package/src/input.ts +0 -35
  111. package/src/intent.ts +0 -15
  112. package/src/json.ts +0 -42
  113. package/src/participants.ts +0 -14
  114. package/src/payment.ts +0 -573
  115. package/src/profit.ts +0 -55
  116. package/src/sort.ts +0 -49
  117. package/src/stats.ts +0 -10
  118. package/src/time.ts +0 -123
  119. package/src/tokens.ts +0 -159
  120. package/src/units.ts +0 -120
  121. package/src/zkp2p-types.ts +0 -55
  122. package/tsconfig.json +0 -18
@@ -1,9 +1,24 @@
1
+ /**
2
+ * @fileoverview Fiat currency definitions and utilities for the Provex protocol.
3
+ *
4
+ * This module defines supported fiat currencies with their symbols, decimals,
5
+ * and contract IDs (keccak256 hashes used for on-chain identification).
6
+ *
7
+ * @remarks
8
+ * - Currency contract IDs are keccak256(currencyCode) used in smart contracts
9
+ * - Most currencies use 2 decimal places, but JPY/KRW/VND/UGX use 0
10
+ * - The Currency class provides parsing/formatting utilities for amounts
11
+ *
12
+ * @see {@link payment} for payment provider currency support
13
+ * @see {@link conversionRates} for exchange rate calculations
14
+ */
1
15
  import { isHash, keccak256, toBytes } from 'viem';
2
16
  import { addThousandsSeparator, formatUnits, parseUnits } from './units.js';
3
17
  import countriesData from './data/countries.json' with { type: 'json' };
4
18
  import { USD_FLAG_SVG, EUR_FLAG_SVG } from './flags.js';
5
- // Convert SVG strings to data URLs
19
+ /** Pre-computed data URL for USD flag (custom square design) */
6
20
  const USD_FLAG_DATA_URL = `data:image/svg+xml;base64,${btoa(USD_FLAG_SVG)}`;
21
+ /** Pre-computed data URL for EUR flag (custom square design) */
7
22
  const EUR_FLAG_DATA_URL = `data:image/svg+xml;base64,${btoa(EUR_FLAG_SVG)}`;
8
23
  const countries = countriesData;
9
24
  /**
@@ -15,12 +30,20 @@ export const tickerToContractId = (ticker) => {
15
30
  return keccak256(toBytes(ticker.toUpperCase()));
16
31
  };
17
32
  /**
18
- * Currency class that automatically generates contract ID
19
- * @param code - The currency code
20
- * @param symbol - The currency symbol
21
- * @param name - The currency name
22
- * @param decimals - The number of decimals
23
- * @param flag - The flag image path
33
+ * Currency class representing a fiat currency with parsing and formatting utilities.
34
+ *
35
+ * Each currency has:
36
+ * - A unique ticker (ISO 4217 code)
37
+ * - A display symbol
38
+ * - A contract ID (keccak256 hash for on-chain use)
39
+ * - Decimal precision for amount formatting
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * const usd = SUPPORTED_CURRENCIES.USD
44
+ * const amount = usd.parseAmount('100.50') // 10050n (2 decimals)
45
+ * const display = usd.formatAmount(10050n) // '100.50'
46
+ * ```
24
47
  */
25
48
  export class Currency {
26
49
  ticker;
@@ -29,12 +52,24 @@ export class Currency {
29
52
  decimals;
30
53
  contractId;
31
54
  flag;
32
- constructor(code, symbol, name, decimals, flag) {
55
+ zoom;
56
+ /**
57
+ * Create a new Currency instance.
58
+ *
59
+ * @param code - ISO 4217 currency code (converted to uppercase)
60
+ * @param symbol - Display symbol (e.g., '$')
61
+ * @param name - Full currency name
62
+ * @param decimals - Number of decimal places
63
+ * @param flag - Optional flag image URL
64
+ * @param zoom - Optional zoom level for flag display (default: 1.5)
65
+ */
66
+ constructor(code, symbol, name, decimals, flag, zoom) {
33
67
  this.ticker = code.toUpperCase();
34
68
  this.symbol = symbol;
35
69
  this.name = name;
36
70
  this.decimals = decimals;
37
- this.flag = flag || ''; // figure out a fallback flag
71
+ this.flag = flag || '';
72
+ this.zoom = zoom ?? 1.5;
38
73
  this.contractId = this.generateContractId();
39
74
  }
40
75
  /**
@@ -64,21 +99,9 @@ export class Currency {
64
99
  * @returns The amount as a string
65
100
  */
66
101
  formatAmount(amount, options = {}) {
67
- try {
68
- if (amount === null) {
69
- return null;
70
- }
71
- return formatUnits(amount, {
72
- decimals: this.decimals,
73
- truncateDecimals: 2,
74
- padDecimals: 2,
75
- ...options,
76
- });
77
- }
78
- catch (err) {
79
- console.error('Error formatting amount: %o', amount, err);
102
+ if (amount === null)
80
103
  return null;
81
- }
104
+ return formatUnits(amount, { decimals: this.decimals, truncateDecimals: 2, padDecimals: 2, ...options });
82
105
  }
83
106
  /**
84
107
  * Convert to plain object for compatibility
@@ -92,6 +115,7 @@ export class Currency {
92
115
  decimals: this.decimals,
93
116
  contractId: this.contractId,
94
117
  flag: this.flag,
118
+ zoom: this.zoom,
95
119
  };
96
120
  }
97
121
  }
@@ -109,13 +133,35 @@ export function generateCurrencyId(currencyCode) {
109
133
  * @returns The flag image path
110
134
  */
111
135
  const flagFromCountries = (countryCode) => {
112
- const country = countries.find((country) => country.code === countryCode);
113
- return country?.flag || '';
136
+ return countries.find((country) => country.code === countryCode)?.flag ?? '';
114
137
  };
115
- // Supported fiat currencies with local flag files for major ones
138
+ /**
139
+ * Registry of all supported fiat currencies.
140
+ *
141
+ * Each currency is a Currency instance with:
142
+ * - Correct decimal precision (0 for JPY/KRW/VND/UGX, 2 for others)
143
+ * - Unique contract ID (keccak256 hash)
144
+ * - Flag image for UI display
145
+ *
146
+ * @remarks
147
+ * Payment providers support different subsets of currencies.
148
+ * Use `getCurrenciesForProvider()` in payment.ts to get supported currencies
149
+ * for a specific payment method.
150
+ *
151
+ * @example
152
+ * ```ts
153
+ * // Get USD currency info
154
+ * const usd = SUPPORTED_CURRENCIES.USD
155
+ * console.log(usd.symbol) // '$'
156
+ * console.log(usd.contractId) // '0x...' (keccak256('USD'))
157
+ *
158
+ * // Parse user input
159
+ * const amount = usd.parseAmount('100.50')
160
+ * ```
161
+ */
116
162
  export const SUPPORTED_CURRENCIES = {
117
- USD: new Currency('USD', '$', 'US Dollar', 2, USD_FLAG_DATA_URL),
118
- EUR: new Currency('EUR', '€', 'Euro', 2, EUR_FLAG_DATA_URL),
163
+ USD: new Currency('USD', '$', 'US Dollar', 2, USD_FLAG_DATA_URL, 1),
164
+ EUR: new Currency('EUR', '€', 'Euro', 2, EUR_FLAG_DATA_URL, 1),
119
165
  GBP: new Currency('GBP', '£', 'British Pound', 2, flagFromCountries('GBP')),
120
166
  AUD: new Currency('AUD', 'A$', 'Australian Dollar', 2, flagFromCountries('AUD')),
121
167
  CAD: new Currency('CAD', 'C$', 'Canadian Dollar', 2, flagFromCountries('CAD')),
@@ -151,17 +197,33 @@ export const SUPPORTED_CURRENCIES = {
151
197
  RON: new Currency('RON', 'RON', 'Romanian Leu', 2, flagFromCountries('RON')),
152
198
  HUF: new Currency('HUF', 'Ft', 'Hungarian Forint', 2, flagFromCountries('HUF')),
153
199
  };
154
- /** Entries for the supported currencies */
200
+ /** Sorted entries for consistent iteration order */
155
201
  const entries = Object.entries(SUPPORTED_CURRENCIES).sort(([a], [b]) => a.localeCompare(b));
156
- entries.forEach(([, currency]) => {
157
- // console.log(currency.ticker, currency.contractId)
158
- if (!currency.flag) {
159
- console.error(`missing flag for ${currency.ticker}`);
160
- }
161
- });
202
+ /**
203
+ * Map from contract ID (keccak256 hash) to Currency instance.
204
+ * Used for O(1) lookup of currency info from on-chain data.
205
+ *
206
+ * @example
207
+ * ```ts
208
+ * const contractId = '0x...' // from blockchain event
209
+ * const currency = currencyById.get(contractId)
210
+ * if (currency) {
211
+ * console.log(currency.ticker) // 'USD'
212
+ * }
213
+ * ```
214
+ */
162
215
  export const currencyById = new Map(entries.map(([, currency]) => {
163
216
  return [currency.contractId, currency];
164
217
  }));
218
+ /**
219
+ * Get the currency name/ticker by contract ID
220
+ * @param contractId - The contract ID (keccak256 hash)
221
+ * @returns The currency ticker or the original contractId if not found
222
+ */
223
+ export function getCurrencyNameByContractId(contractId) {
224
+ const currency = currencyById.get(contractId);
225
+ return currency?.ticker ?? contractId;
226
+ }
165
227
  /**
166
228
  * Get the currency info for a currency code
167
229
  * @param currencyCode - The currency code
@@ -169,7 +231,7 @@ export const currencyById = new Map(entries.map(([, currency]) => {
169
231
  */
170
232
  export function getCurrencyInfo(currencyCode) {
171
233
  const currency = SUPPORTED_CURRENCIES[currencyCode.toUpperCase()];
172
- return currency ? currency.toJSON() : null;
234
+ return currency?.toJSON() ?? null;
173
235
  }
174
236
  /**
175
237
  * Get the currency info for a contract ID
@@ -180,8 +242,7 @@ export function getCurrencyByContractId(contractId) {
180
242
  if (!isHash(contractId)) {
181
243
  throw new Error('Invalid contract ID');
182
244
  }
183
- const currency = currencyById.get(contractId);
184
- return currency ? currency : null;
245
+ return currencyById.get(contractId) ?? null;
185
246
  }
186
247
  /**
187
248
  * Format currency amount safely without Number() constructor, preserving decimal places
@@ -232,3 +293,23 @@ export function getAllSupportedCurrencies() {
232
293
  export function isSupportedCurrency(currencyCode) {
233
294
  return currencyCode.toUpperCase() in SUPPORTED_CURRENCIES;
234
295
  }
296
+ /**
297
+ * Format the default conversion rate based on currency decimals.
298
+ * Returns "1" for zero-decimal currencies (JPY, KRW, VND, UGX),
299
+ * or "1.00" for standard 2-decimal currencies (USD, EUR, etc.).
300
+ *
301
+ * @param currency - The currency to format the default rate for
302
+ * @returns Formatted default rate string matching the currency's decimal precision
303
+ *
304
+ * @example
305
+ * ```ts
306
+ * formatDefaultRate(SUPPORTED_CURRENCIES.JPY) // "1" (0 decimals)
307
+ * formatDefaultRate(SUPPORTED_CURRENCIES.USD) // "1.00" (2 decimals)
308
+ * formatDefaultRate(SUPPORTED_CURRENCIES.EUR) // "1.00" (2 decimals)
309
+ * ```
310
+ */
311
+ export function formatDefaultRate(currency) {
312
+ if (currency.decimals === 0)
313
+ return '1';
314
+ return `1.${'0'.repeat(currency.decimals)}`;
315
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,343 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { SUPPORTED_CURRENCIES, getCurrencyInfo, getCurrencyByContractId, getCurrencyNameByContractId, getAllSupportedCurrencies, isSupportedCurrency, formatCurrencyAmount, tickerToContractId, generateCurrencyId, formatDefaultRate, Currency, } from './currencies.js';
4
+ describe('currencies', () => {
5
+ describe('SUPPORTED_CURRENCIES', () => {
6
+ it('should have exactly 36 supported currencies', () => {
7
+ const count = Object.keys(SUPPORTED_CURRENCIES).length;
8
+ assert.equal(count, 36, `Expected 36 currencies, but found ${count}`);
9
+ });
10
+ it('should have USD as the first major currency', () => {
11
+ assert.ok(SUPPORTED_CURRENCIES.USD);
12
+ assert.equal(SUPPORTED_CURRENCIES.USD.ticker, 'USD');
13
+ assert.equal(SUPPORTED_CURRENCIES.USD.symbol, '$');
14
+ assert.equal(SUPPORTED_CURRENCIES.USD.decimals, 2);
15
+ });
16
+ it('should have EUR as a supported currency', () => {
17
+ assert.ok(SUPPORTED_CURRENCIES.EUR);
18
+ assert.equal(SUPPORTED_CURRENCIES.EUR.ticker, 'EUR');
19
+ assert.equal(SUPPORTED_CURRENCIES.EUR.symbol, '€');
20
+ assert.equal(SUPPORTED_CURRENCIES.EUR.decimals, 2);
21
+ });
22
+ it('should include zero-decimal currencies', () => {
23
+ assert.equal(SUPPORTED_CURRENCIES.JPY.decimals, 0);
24
+ assert.equal(SUPPORTED_CURRENCIES.KRW.decimals, 0);
25
+ assert.equal(SUPPORTED_CURRENCIES.VND.decimals, 0);
26
+ assert.equal(SUPPORTED_CURRENCIES.UGX.decimals, 0);
27
+ });
28
+ it('should have unique contract IDs for all currencies', () => {
29
+ const contractIds = new Set();
30
+ const currencies = Object.values(SUPPORTED_CURRENCIES);
31
+ for (const currency of currencies) {
32
+ assert.ok(!contractIds.has(currency.contractId), `Duplicate contract ID found for ${currency.ticker}`);
33
+ contractIds.add(currency.contractId);
34
+ }
35
+ assert.equal(contractIds.size, currencies.length);
36
+ });
37
+ it('should have flags for all currencies', () => {
38
+ const currencies = Object.values(SUPPORTED_CURRENCIES);
39
+ for (const currency of currencies) {
40
+ assert.ok(currency.flag, `Missing flag for ${currency.ticker}`);
41
+ assert.ok(currency.flag.length > 0, `Empty flag for ${currency.ticker}`);
42
+ }
43
+ });
44
+ it('should have valid ticker codes', () => {
45
+ const currencies = Object.values(SUPPORTED_CURRENCIES);
46
+ for (const currency of currencies) {
47
+ assert.ok(currency.ticker.length === 3, `Invalid ticker length for ${currency.ticker}`);
48
+ assert.equal(currency.ticker, currency.ticker.toUpperCase(), `Ticker should be uppercase: ${currency.ticker}`);
49
+ }
50
+ });
51
+ });
52
+ describe('getCurrencyInfo', () => {
53
+ it('should return currency info for valid currency code', () => {
54
+ const usd = getCurrencyInfo('USD');
55
+ assert.ok(usd);
56
+ assert.equal(usd.ticker, 'USD');
57
+ assert.equal(usd.symbol, '$');
58
+ assert.equal(usd.decimals, 2);
59
+ });
60
+ it('should be case-insensitive', () => {
61
+ const usdLower = getCurrencyInfo('usd');
62
+ const usdUpper = getCurrencyInfo('USD');
63
+ const usdMixed = getCurrencyInfo('UsD');
64
+ assert.deepEqual(usdLower, usdUpper);
65
+ assert.deepEqual(usdLower, usdMixed);
66
+ });
67
+ it('should return null for invalid currency code', () => {
68
+ const invalid = getCurrencyInfo('INVALID');
69
+ assert.equal(invalid, null);
70
+ });
71
+ it('should return null for empty string', () => {
72
+ const empty = getCurrencyInfo('');
73
+ assert.equal(empty, null);
74
+ });
75
+ });
76
+ describe('getCurrencyByContractId', () => {
77
+ it('should return currency for valid contract ID', () => {
78
+ const usdContractId = SUPPORTED_CURRENCIES.USD.contractId;
79
+ const currency = getCurrencyByContractId(usdContractId);
80
+ assert.ok(currency);
81
+ assert.equal(currency.ticker, 'USD');
82
+ });
83
+ it('should return null for invalid contract ID', () => {
84
+ const fakeHash = '0x' + '0'.repeat(64);
85
+ const currency = getCurrencyByContractId(fakeHash);
86
+ assert.equal(currency, null);
87
+ });
88
+ it('should throw for non-hash input', () => {
89
+ assert.throws(() => {
90
+ getCurrencyByContractId('not-a-hash');
91
+ }, /Invalid contract ID/);
92
+ });
93
+ });
94
+ describe('getCurrencyNameByContractId', () => {
95
+ it('should return ticker for valid contract ID', () => {
96
+ const usdContractId = SUPPORTED_CURRENCIES.USD.contractId;
97
+ const name = getCurrencyNameByContractId(usdContractId);
98
+ assert.equal(name, 'USD');
99
+ });
100
+ it('should return original contract ID for unknown ID', () => {
101
+ const fakeHash = '0x' + '1'.repeat(64);
102
+ const name = getCurrencyNameByContractId(fakeHash);
103
+ assert.equal(name, fakeHash);
104
+ });
105
+ });
106
+ describe('getAllSupportedCurrencies', () => {
107
+ it('should return all currencies', () => {
108
+ const all = getAllSupportedCurrencies();
109
+ assert.equal(all.length, 36);
110
+ });
111
+ it('should return CurrencyInfo objects', () => {
112
+ const all = getAllSupportedCurrencies();
113
+ const first = all[0];
114
+ assert.ok(first.ticker);
115
+ assert.ok(first.symbol);
116
+ assert.ok(first.name);
117
+ assert.ok(typeof first.decimals === 'number');
118
+ assert.ok(first.contractId);
119
+ assert.ok(first.flag);
120
+ });
121
+ it('should return sorted currencies', () => {
122
+ const all = getAllSupportedCurrencies();
123
+ const tickers = all.map(c => c.ticker);
124
+ const sorted = [...tickers].sort();
125
+ assert.deepEqual(tickers, sorted);
126
+ });
127
+ });
128
+ describe('isSupportedCurrency', () => {
129
+ it('should return true for supported currencies', () => {
130
+ assert.equal(isSupportedCurrency('USD'), true);
131
+ assert.equal(isSupportedCurrency('EUR'), true);
132
+ assert.equal(isSupportedCurrency('GBP'), true);
133
+ });
134
+ it('should be case-insensitive', () => {
135
+ assert.equal(isSupportedCurrency('usd'), true);
136
+ assert.equal(isSupportedCurrency('USD'), true);
137
+ assert.equal(isSupportedCurrency('UsD'), true);
138
+ });
139
+ it('should return false for unsupported currencies', () => {
140
+ assert.equal(isSupportedCurrency('INVALID'), false);
141
+ assert.equal(isSupportedCurrency(''), false);
142
+ assert.equal(isSupportedCurrency('XXX'), false);
143
+ });
144
+ });
145
+ describe('formatCurrencyAmount', () => {
146
+ it('should format USD amounts with 2 decimals', () => {
147
+ assert.equal(formatCurrencyAmount(100, 'USD'), '$100.00');
148
+ assert.equal(formatCurrencyAmount(1000, 'USD'), '$1,000.00');
149
+ assert.equal(formatCurrencyAmount(1234.56, 'USD'), '$1,234.56');
150
+ });
151
+ it('should format JPY amounts with 0 decimals', () => {
152
+ assert.equal(formatCurrencyAmount(100, 'JPY'), '¥100');
153
+ assert.equal(formatCurrencyAmount(1000, 'JPY'), '¥1,000');
154
+ assert.equal(formatCurrencyAmount(1234, 'JPY'), '¥1,234');
155
+ });
156
+ it('should format EUR amounts correctly', () => {
157
+ assert.equal(formatCurrencyAmount(100, 'EUR'), '€100.00');
158
+ assert.equal(formatCurrencyAmount(1234.56, 'EUR'), '€1,234.56');
159
+ });
160
+ it('should handle string inputs', () => {
161
+ assert.equal(formatCurrencyAmount('100', 'USD'), '$100.00');
162
+ assert.equal(formatCurrencyAmount('1234.56', 'USD'), '$1,234.56');
163
+ });
164
+ it('should handle unknown currencies', () => {
165
+ const result = formatCurrencyAmount(100, 'UNKNOWN');
166
+ assert.equal(result, '100 UNKNOWN');
167
+ });
168
+ it('should add thousands separators', () => {
169
+ assert.equal(formatCurrencyAmount(1000000, 'USD'), '$1,000,000.00');
170
+ assert.equal(formatCurrencyAmount(123456789, 'JPY'), '¥123,456,789');
171
+ });
172
+ });
173
+ describe('tickerToContractId', () => {
174
+ it('should generate consistent contract IDs', () => {
175
+ const id1 = tickerToContractId('USD');
176
+ const id2 = tickerToContractId('USD');
177
+ assert.equal(id1, id2);
178
+ });
179
+ it('should be case-insensitive', () => {
180
+ const idUpper = tickerToContractId('USD');
181
+ const idLower = tickerToContractId('usd');
182
+ const idMixed = tickerToContractId('UsD');
183
+ assert.equal(idUpper, idLower);
184
+ assert.equal(idUpper, idMixed);
185
+ });
186
+ it('should generate different IDs for different tickers', () => {
187
+ const usdId = tickerToContractId('USD');
188
+ const eurId = tickerToContractId('EUR');
189
+ const gbpId = tickerToContractId('GBP');
190
+ assert.notEqual(usdId, eurId);
191
+ assert.notEqual(usdId, gbpId);
192
+ assert.notEqual(eurId, gbpId);
193
+ });
194
+ it('should generate 32-byte hashes', () => {
195
+ const id = tickerToContractId('USD');
196
+ assert.equal(id.length, 66); // '0x' + 64 hex chars
197
+ assert.ok(id.startsWith('0x'));
198
+ });
199
+ });
200
+ describe('Currency class', () => {
201
+ it('should parse amounts correctly', () => {
202
+ const usd = SUPPORTED_CURRENCIES.USD;
203
+ const amount = usd.parseAmount('100.50');
204
+ assert.equal(amount, 10050n); // 100.50 with 2 decimals
205
+ });
206
+ it('should return null for invalid amounts', () => {
207
+ const usd = SUPPORTED_CURRENCIES.USD;
208
+ const amount = usd.parseAmount('invalid');
209
+ assert.equal(amount, null);
210
+ });
211
+ it('should format amounts correctly', () => {
212
+ const usd = SUPPORTED_CURRENCIES.USD;
213
+ const formatted = usd.formatAmount(10050n);
214
+ assert.equal(formatted, '100.50');
215
+ });
216
+ it('should handle null amounts', () => {
217
+ const usd = SUPPORTED_CURRENCIES.USD;
218
+ const formatted = usd.formatAmount(null);
219
+ assert.equal(formatted, null);
220
+ });
221
+ it('should format with custom options', () => {
222
+ const usd = SUPPORTED_CURRENCIES.USD;
223
+ const formatted = usd.formatAmount(10050n, { truncateDecimals: 0, padDecimals: 0 });
224
+ assert.ok(formatted, 'formatted should be truthy');
225
+ assert.ok(!formatted.includes('.'), `formatted should not include '.', but got: ${formatted}`);
226
+ });
227
+ it('should have default zoom of 1.5 when not specified', () => {
228
+ // GBP uses flagFromCountries and no explicit zoom
229
+ const gbp = SUPPORTED_CURRENCIES.GBP;
230
+ assert.equal(gbp.zoom, 1.5);
231
+ });
232
+ it('should use explicit zoom when specified', () => {
233
+ // USD has explicit zoom of 1
234
+ const usd = SUPPORTED_CURRENCIES.USD;
235
+ assert.equal(usd.zoom, 1);
236
+ });
237
+ it('should have flag from countries data', () => {
238
+ // GBP gets flag from countries.json
239
+ const gbp = SUPPORTED_CURRENCIES.GBP;
240
+ assert.ok(gbp.flag.length > 0);
241
+ });
242
+ it('should have explicit flag for USD/EUR', () => {
243
+ const usd = SUPPORTED_CURRENCIES.USD;
244
+ const eur = SUPPORTED_CURRENCIES.EUR;
245
+ assert.ok(usd.flag.startsWith('data:'));
246
+ assert.ok(eur.flag.startsWith('data:'));
247
+ });
248
+ it('should default flag to empty string when not provided', () => {
249
+ const currency = new Currency('TST', '$', 'Test', 2);
250
+ assert.equal(currency.flag, '');
251
+ });
252
+ it('should default zoom to 1.5 when not provided', () => {
253
+ const currency = new Currency('TST', '$', 'Test', 2);
254
+ assert.equal(currency.zoom, 1.5);
255
+ });
256
+ it('should use provided flag when specified', () => {
257
+ const currency = new Currency('TST', '$', 'Test', 2, 'custom-flag.png');
258
+ assert.equal(currency.flag, 'custom-flag.png');
259
+ });
260
+ it('should use provided zoom when specified', () => {
261
+ const currency = new Currency('TST', '$', 'Test', 2, '', 2.0);
262
+ assert.equal(currency.zoom, 2.0);
263
+ });
264
+ it('should use provided flag with default zoom', () => {
265
+ const currency = new Currency('TST', '$', 'Test', 2, 'flag.png');
266
+ assert.equal(currency.flag, 'flag.png');
267
+ assert.equal(currency.zoom, 1.5); // default
268
+ });
269
+ it('should handle empty string flag', () => {
270
+ const currency = new Currency('TST', '$', 'Test', 2, '');
271
+ assert.equal(currency.flag, '');
272
+ assert.equal(currency.zoom, 1.5);
273
+ });
274
+ });
275
+ describe('generateCurrencyId', () => {
276
+ it('should generate consistent IDs', () => {
277
+ const id1 = generateCurrencyId('USD');
278
+ const id2 = generateCurrencyId('USD');
279
+ assert.equal(id1, id2);
280
+ });
281
+ it('should be case insensitive', () => {
282
+ const upper = generateCurrencyId('USD');
283
+ const lower = generateCurrencyId('usd');
284
+ const mixed = generateCurrencyId('UsD');
285
+ assert.equal(upper, lower);
286
+ assert.equal(upper, mixed);
287
+ });
288
+ it('should generate different IDs for different currencies', () => {
289
+ const usd = generateCurrencyId('USD');
290
+ const eur = generateCurrencyId('EUR');
291
+ assert.notEqual(usd, eur);
292
+ });
293
+ it('should match tickerToContractId output', () => {
294
+ const fromGenerate = generateCurrencyId('USD');
295
+ const fromTicker = tickerToContractId('USD');
296
+ assert.equal(fromGenerate, fromTicker);
297
+ });
298
+ });
299
+ describe('formatDefaultRate', () => {
300
+ it('returns "1" for zero-decimal currencies (JPY)', () => {
301
+ const result = formatDefaultRate(SUPPORTED_CURRENCIES.JPY);
302
+ assert.equal(result, '1');
303
+ });
304
+ it('returns "1" for zero-decimal currencies (KRW)', () => {
305
+ const result = formatDefaultRate(SUPPORTED_CURRENCIES.KRW);
306
+ assert.equal(result, '1');
307
+ });
308
+ it('returns "1" for zero-decimal currencies (VND)', () => {
309
+ const result = formatDefaultRate(SUPPORTED_CURRENCIES.VND);
310
+ assert.equal(result, '1');
311
+ });
312
+ it('returns "1" for zero-decimal currencies (UGX)', () => {
313
+ const result = formatDefaultRate(SUPPORTED_CURRENCIES.UGX);
314
+ assert.equal(result, '1');
315
+ });
316
+ it('returns "1.00" for 2-decimal currencies (USD)', () => {
317
+ const result = formatDefaultRate(SUPPORTED_CURRENCIES.USD);
318
+ assert.equal(result, '1.00');
319
+ });
320
+ it('returns "1.00" for 2-decimal currencies (EUR)', () => {
321
+ const result = formatDefaultRate(SUPPORTED_CURRENCIES.EUR);
322
+ assert.equal(result, '1.00');
323
+ });
324
+ it('returns "1.00" for 2-decimal currencies (GBP)', () => {
325
+ const result = formatDefaultRate(SUPPORTED_CURRENCIES.GBP);
326
+ assert.equal(result, '1.00');
327
+ });
328
+ it('returns "1.00" for 2-decimal currencies (ARS)', () => {
329
+ const result = formatDefaultRate(SUPPORTED_CURRENCIES.ARS);
330
+ assert.equal(result, '1.00');
331
+ });
332
+ it('works with CurrencyInfo (toJSON output)', () => {
333
+ const currencyInfo = SUPPORTED_CURRENCIES.USD.toJSON();
334
+ const result = formatDefaultRate(currencyInfo);
335
+ assert.equal(result, '1.00');
336
+ });
337
+ it('handles custom Currency with different decimals', () => {
338
+ const customCurrency = new Currency('TST', '$', 'Test', 3);
339
+ const result = formatDefaultRate(customCurrency);
340
+ assert.equal(result, '1.000');
341
+ });
342
+ });
343
+ });