@metamask-previews/perps-controller 3.0.0-preview-e61cfa5 → 3.1.0-preview-548bdd1d9

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 (90) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/dist/PerpsController-method-action-types.cjs.map +1 -1
  3. package/dist/PerpsController-method-action-types.d.cts +8 -0
  4. package/dist/PerpsController-method-action-types.d.cts.map +1 -1
  5. package/dist/PerpsController-method-action-types.d.mts +8 -0
  6. package/dist/PerpsController-method-action-types.d.mts.map +1 -1
  7. package/dist/PerpsController-method-action-types.mjs.map +1 -1
  8. package/dist/PerpsController.cjs +117 -29
  9. package/dist/PerpsController.cjs.map +1 -1
  10. package/dist/PerpsController.d.cts +14 -2
  11. package/dist/PerpsController.d.cts.map +1 -1
  12. package/dist/PerpsController.d.mts +14 -2
  13. package/dist/PerpsController.d.mts.map +1 -1
  14. package/dist/PerpsController.mjs +118 -30
  15. package/dist/PerpsController.mjs.map +1 -1
  16. package/dist/constants/eventNames.cjs +1 -0
  17. package/dist/constants/eventNames.cjs.map +1 -1
  18. package/dist/constants/eventNames.d.cts +1 -0
  19. package/dist/constants/eventNames.d.cts.map +1 -1
  20. package/dist/constants/eventNames.d.mts +1 -0
  21. package/dist/constants/eventNames.d.mts.map +1 -1
  22. package/dist/constants/eventNames.mjs +1 -0
  23. package/dist/constants/eventNames.mjs.map +1 -1
  24. package/dist/constants/perpsConfig.cjs +46 -1
  25. package/dist/constants/perpsConfig.cjs.map +1 -1
  26. package/dist/constants/perpsConfig.d.cts +35 -0
  27. package/dist/constants/perpsConfig.d.cts.map +1 -1
  28. package/dist/constants/perpsConfig.d.mts +35 -0
  29. package/dist/constants/perpsConfig.d.mts.map +1 -1
  30. package/dist/constants/perpsConfig.mjs +43 -0
  31. package/dist/constants/perpsConfig.mjs.map +1 -1
  32. package/dist/constants/transactionsHistoryConfig.cjs +23 -4
  33. package/dist/constants/transactionsHistoryConfig.cjs.map +1 -1
  34. package/dist/constants/transactionsHistoryConfig.d.cts +23 -4
  35. package/dist/constants/transactionsHistoryConfig.d.cts.map +1 -1
  36. package/dist/constants/transactionsHistoryConfig.d.mts +23 -4
  37. package/dist/constants/transactionsHistoryConfig.d.mts.map +1 -1
  38. package/dist/constants/transactionsHistoryConfig.mjs +23 -4
  39. package/dist/constants/transactionsHistoryConfig.mjs.map +1 -1
  40. package/dist/index.cjs +14 -3
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/index.d.cts +3 -1
  43. package/dist/index.d.cts.map +1 -1
  44. package/dist/index.d.mts +3 -1
  45. package/dist/index.d.mts.map +1 -1
  46. package/dist/index.mjs +2 -1
  47. package/dist/index.mjs.map +1 -1
  48. package/dist/providers/HyperLiquidProvider.cjs +83 -27
  49. package/dist/providers/HyperLiquidProvider.cjs.map +1 -1
  50. package/dist/providers/HyperLiquidProvider.d.cts.map +1 -1
  51. package/dist/providers/HyperLiquidProvider.d.mts.map +1 -1
  52. package/dist/providers/HyperLiquidProvider.mjs +83 -27
  53. package/dist/providers/HyperLiquidProvider.mjs.map +1 -1
  54. package/dist/services/HyperLiquidSubscriptionService.cjs +6 -0
  55. package/dist/services/HyperLiquidSubscriptionService.cjs.map +1 -1
  56. package/dist/services/HyperLiquidSubscriptionService.d.cts.map +1 -1
  57. package/dist/services/HyperLiquidSubscriptionService.d.mts.map +1 -1
  58. package/dist/services/HyperLiquidSubscriptionService.mjs +6 -0
  59. package/dist/services/HyperLiquidSubscriptionService.mjs.map +1 -1
  60. package/dist/types/index.cjs.map +1 -1
  61. package/dist/types/index.d.cts +6 -0
  62. package/dist/types/index.d.cts.map +1 -1
  63. package/dist/types/index.d.mts +6 -0
  64. package/dist/types/index.d.mts.map +1 -1
  65. package/dist/types/index.mjs.map +1 -1
  66. package/dist/utils/index.cjs +2 -0
  67. package/dist/utils/index.cjs.map +1 -1
  68. package/dist/utils/index.d.cts +2 -0
  69. package/dist/utils/index.d.cts.map +1 -1
  70. package/dist/utils/index.d.mts +2 -0
  71. package/dist/utils/index.d.mts.map +1 -1
  72. package/dist/utils/index.mjs +2 -0
  73. package/dist/utils/index.mjs.map +1 -1
  74. package/dist/utils/perpsDiskPersistence.cjs +252 -0
  75. package/dist/utils/perpsDiskPersistence.cjs.map +1 -0
  76. package/dist/utils/perpsDiskPersistence.d.cts +108 -0
  77. package/dist/utils/perpsDiskPersistence.d.cts.map +1 -0
  78. package/dist/utils/perpsDiskPersistence.d.mts +108 -0
  79. package/dist/utils/perpsDiskPersistence.d.mts.map +1 -0
  80. package/dist/utils/perpsDiskPersistence.mjs +244 -0
  81. package/dist/utils/perpsDiskPersistence.mjs.map +1 -0
  82. package/dist/utils/perpsFormatters.cjs +498 -0
  83. package/dist/utils/perpsFormatters.cjs.map +1 -0
  84. package/dist/utils/perpsFormatters.d.cts +202 -0
  85. package/dist/utils/perpsFormatters.d.cts.map +1 -0
  86. package/dist/utils/perpsFormatters.d.mts +202 -0
  87. package/dist/utils/perpsFormatters.d.mts.map +1 -0
  88. package/dist/utils/perpsFormatters.mjs +489 -0
  89. package/dist/utils/perpsFormatters.mjs.map +1 -0
  90. package/package.json +3 -3
@@ -0,0 +1,489 @@
1
+ /**
2
+ * Portable perps decimal formatters.
3
+ *
4
+ * These are the canonical implementations, exported from the controller so
5
+ * extension and any future consumer can import them directly.
6
+ * No mobile-specific imports — safe to sync to Core.
7
+ *
8
+ * Intl.NumberFormat instances are cached in a module-level Map keyed by
9
+ * serialized options, avoiding repeated construction costs.
10
+ */
11
+ import { DECIMAL_PRECISION_CONFIG, FUNDING_RATE_CONFIG, PERPS_CONSTANTS } from "../constants/perpsConfig.mjs";
12
+ // Module-level Intl.NumberFormat cache (keyed by serialized options).
13
+ const _fmtCache = new Map();
14
+ function _formatCurrency(value, currency, opts) {
15
+ const key = `${currency}:${opts.minimumFractionDigits}:${opts.maximumFractionDigits}`;
16
+ let formatter = _fmtCache.get(key);
17
+ if (!formatter) {
18
+ formatter = new Intl.NumberFormat('en-US', {
19
+ style: 'currency',
20
+ currency,
21
+ currencyDisplay: 'narrowSymbol',
22
+ minimumFractionDigits: opts.minimumFractionDigits,
23
+ maximumFractionDigits: opts.maximumFractionDigits,
24
+ });
25
+ _fmtCache.set(key, formatter);
26
+ }
27
+ return formatter.format(value);
28
+ }
29
+ /**
30
+ * Internal equivalent of the mobile formatWithThreshold utility.
31
+ * Formats a currency value, returning "<$X.XX" for values below threshold.
32
+ *
33
+ * @param amount - The numeric amount to format.
34
+ * @param threshold - The threshold below which the "<" prefix is shown.
35
+ * @param options - Intl formatting options.
36
+ * @param options.currency - ISO 4217 currency code.
37
+ * @param options.minimumFractionDigits - Minimum decimal digits.
38
+ * @param options.maximumFractionDigits - Maximum decimal digits.
39
+ * @returns Formatted currency string.
40
+ */
41
+ function _formatWithThreshold(amount, threshold, options) {
42
+ const formatOpts = {
43
+ minimumFractionDigits: options.minimumFractionDigits,
44
+ maximumFractionDigits: options.maximumFractionDigits,
45
+ currencyDisplay: 'narrowSymbol',
46
+ };
47
+ if (amount === 0) {
48
+ return _formatCurrency(0, options.currency, formatOpts);
49
+ }
50
+ return Math.abs(amount) < threshold
51
+ ? `<${_formatCurrency(threshold, options.currency, formatOpts)}`
52
+ : _formatCurrency(amount, options.currency, formatOpts);
53
+ }
54
+ /**
55
+ * Price threshold constants for PRICE_RANGES_UNIVERSAL
56
+ * These define the boundaries between different formatting ranges
57
+ */
58
+ export const PRICE_THRESHOLD = {
59
+ /** Very high values boundary (> $100k) */
60
+ VERY_HIGH: 100000,
61
+ /** High values boundary (> $10k) */
62
+ HIGH: 10000,
63
+ /** Large values boundary (> $1k) */
64
+ LARGE: 1000,
65
+ /** Medium values boundary (> $100) */
66
+ MEDIUM: 100,
67
+ /** Medium-low values boundary (> $10) */
68
+ MEDIUM_LOW: 10,
69
+ /** Low values boundary (>= $0.01) */
70
+ LOW: 0.01,
71
+ /**
72
+ * Very small values threshold (< $0.01)
73
+ * This is the minimum value for formatWithThreshold and should align with
74
+ * the 6 decimal maximum (0.000001 is the smallest representable value)
75
+ */
76
+ VERY_SMALL: 0.000001,
77
+ };
78
+ /**
79
+ * Formats a number to a specific number of significant digits
80
+ * Strips trailing zeros unless minDecimals requires them
81
+ *
82
+ * @param value - The numeric value to format
83
+ * @param significantDigits - Number of significant digits to maintain
84
+ * @param minDecimals - Minimum decimal places to show (may add zeros)
85
+ * @param maxDecimals - Maximum decimal places allowed
86
+ * @returns Formatted number with appropriate precision, trailing zeros removed
87
+ */
88
+ export function formatWithSignificantDigits(value, significantDigits, minDecimals, maxDecimals) {
89
+ // Handle special cases
90
+ if (value === 0) {
91
+ // Return zero with no trailing decimals by default (matches stripTrailingZeros behavior)
92
+ // Can be overridden by explicit minDecimals if needed
93
+ return { value: 0, decimals: minDecimals ?? 0 };
94
+ }
95
+ const absValue = Math.abs(value);
96
+ // For numbers >= 1, calculate decimals based on magnitude to achieve target significant figures
97
+ // This ensures consistent precision across different price ranges:
98
+ // Examples with 4 significant figures:
99
+ // $123,456.78 → $123,456.78 (≥$1000: 2 decimals minimum, 8 sig figs)
100
+ // $456.12 → $456.12 (≥$10: 2 decimals minimum, 5 sig figs)
101
+ // $56.123 → $56.123 (≥$10: 2 decimals minimum, 5 sig figs)
102
+ // $5.123 → $5.123 ($1-$10: 3 decimals = 4 sig figs)
103
+ // $2.801 → $2.801 ($1-$10: 3 decimals = 4 sig figs)
104
+ // $1.234 → $1.234 ($1-$10: 3 decimals = 4 sig figs)
105
+ if (absValue >= 1) {
106
+ let targetDecimals;
107
+ // Calculate decimals needed based on integer digits to achieve target significant figures
108
+ // For $38.388 with 5 sig figs: 2 integer digits, need 3 decimals (3,8,3,8,8)
109
+ // For $123.45 with 5 sig figs: 3 integer digits, need 2 decimals (1,2,3,4,5)
110
+ const integerDigits = Math.floor(Math.log10(absValue)) + 1;
111
+ const decimalsNeeded = significantDigits - integerDigits;
112
+ targetDecimals = Math.max(decimalsNeeded, 0); // Can't have negative decimals
113
+ // Apply explicit minimum decimals constraint if provided (for special cases)
114
+ if (minDecimals !== undefined && targetDecimals < minDecimals) {
115
+ targetDecimals = minDecimals;
116
+ }
117
+ // Apply maximum decimals constraint if specified
118
+ const finalDecimals = maxDecimals === undefined
119
+ ? targetDecimals
120
+ : Math.min(targetDecimals, maxDecimals);
121
+ // Round to prevent floating-point artifacts (e.g., 2.820000000000003 → 2.82)
122
+ const roundedValue = Number(value.toFixed(finalDecimals));
123
+ return {
124
+ value: roundedValue,
125
+ decimals: finalDecimals,
126
+ };
127
+ }
128
+ // For numbers < 1, use toPrecision to limit to significantDigits
129
+ // Examples: 0.1234, 0.01234 should show exactly 4 sig figs
130
+ const precisionStr = absValue.toPrecision(significantDigits);
131
+ const precisionNum = parseFloat(precisionStr);
132
+ // Convert to string to count actual decimals after trailing zeros are removed
133
+ const valueStr = precisionNum.toString();
134
+ const [, decPart = ''] = valueStr.split('.');
135
+ let actualDecimals = decPart.length;
136
+ // Apply min/max decimal constraints
137
+ if (minDecimals !== undefined && actualDecimals < minDecimals) {
138
+ actualDecimals = minDecimals; // Will add zeros if needed
139
+ }
140
+ if (maxDecimals !== undefined && actualDecimals > maxDecimals) {
141
+ actualDecimals = maxDecimals;
142
+ }
143
+ // Return the value with sign restored and decimal count
144
+ return {
145
+ value: value < 0 ? -precisionNum : precisionNum,
146
+ decimals: actualDecimals,
147
+ };
148
+ }
149
+ /**
150
+ * Minimal view fiat range configuration
151
+ * Uses fiat-style stripping for clean currency display
152
+ * Strips only .00 to avoid partial decimals like $1,250.1
153
+ */
154
+ export const PRICE_RANGES_MINIMAL_VIEW = [
155
+ {
156
+ // Large values (>= $1000): Strip .00 only ($5,000 not $5,000.00, but $5,000.10 stays)
157
+ condition: (val) => Math.abs(val) >= PRICE_THRESHOLD.LARGE,
158
+ minimumDecimals: 2,
159
+ maximumDecimals: 2,
160
+ threshold: PRICE_THRESHOLD.LARGE,
161
+ stripTrailingZeros: true,
162
+ fiatStyleStripping: true,
163
+ },
164
+ {
165
+ // Small values (< $1000): Also use fiat-style stripping ($100 not $100.00, but $13.40 stays)
166
+ condition: () => true,
167
+ minimumDecimals: 2,
168
+ maximumDecimals: 2,
169
+ threshold: PRICE_THRESHOLD.LOW,
170
+ stripTrailingZeros: true,
171
+ fiatStyleStripping: true,
172
+ },
173
+ ];
174
+ /**
175
+ * Universal price range configuration following comprehensive rules from rules-decimals.md
176
+ *
177
+ * Rules:
178
+ * - Max 6 decimals across all ranges (Hyperliquid limit)
179
+ * - Strip trailing zeros by default
180
+ * - Use |v| (absolute value) for conditions
181
+ *
182
+ * Significant digits by range:
183
+ * - > $100,000: 6 sig digs
184
+ * - $100,000 > x > $0.01: 5 sig digs
185
+ * - < $0.01: 4 sig digs
186
+ *
187
+ * Decimal limits by price range:
188
+ * - |v| > 10,000: min 0, max 0 decimals; 5 sig digs (6 if >100k)
189
+ * - |v| > 1,000: min 0, max 1 decimal; 5 sig digs
190
+ * - |v| > 100: min 0, max 2 decimals; 5 sig digs
191
+ * - |v| > 10: min 0, max 4 decimals; 5 sig digs
192
+ * - |v| ≥ 0.01: 5 sig digs, min 2, max 6 decimals
193
+ * - |v| < 0.01: 4 sig digs, min 2, max 6 decimals
194
+ *
195
+ * Examples:
196
+ * - $123,456.78 → $123,457 (>$10k: 0 decimals, 6 sig figs)
197
+ * - $12,345.67 → $12,346 (>$10k: 0 decimals, 5 sig figs)
198
+ * - $1,234.56 → $1,234.6 ($1k-$10k: 1 decimal, 5 sig figs)
199
+ * - $123.456 → $123.46 ($100-$1k: 2 decimals, 5 sig figs)
200
+ * - $12.34567 → $12.346 ($10-$100: 4 decimals, 5 sig figs)
201
+ * - $1.3445555 → $1.3446 (≥$0.01: 5 sig figs)
202
+ * - $0.333333 → $0.33333 (≥$0.01: 5 sig figs)
203
+ * - $0.004236 → $0.004236 (<$0.01: 4 sig figs, max 6 decimals)
204
+ * - $0.0000006 → $0.000001 (<$0.01: 4 sig figs, rounds with max 6 decimals)
205
+ */
206
+ export const PRICE_RANGES_UNIVERSAL = [
207
+ {
208
+ // Very high values (> $100,000): No decimals, 6 significant figures
209
+ // Ex: $123,456.78 → $123,457
210
+ condition: (val) => Math.abs(val) > PRICE_THRESHOLD.VERY_HIGH,
211
+ minimumDecimals: 0,
212
+ maximumDecimals: 0,
213
+ significantDigits: 6,
214
+ threshold: PRICE_THRESHOLD.VERY_HIGH,
215
+ },
216
+ {
217
+ // High values ($10,000-$100,000]: No decimals, 5 significant figures
218
+ // Ex: $12,345.67 → $12,346
219
+ condition: (val) => Math.abs(val) > PRICE_THRESHOLD.HIGH,
220
+ minimumDecimals: 0,
221
+ maximumDecimals: 0,
222
+ significantDigits: 5,
223
+ threshold: PRICE_THRESHOLD.HIGH,
224
+ },
225
+ {
226
+ // Large values ($1,000-$10,000]: Max 1 decimal, 5 significant figures
227
+ // Ex: $1,234.56 → $1,234.6
228
+ condition: (val) => Math.abs(val) > PRICE_THRESHOLD.LARGE,
229
+ minimumDecimals: 0,
230
+ maximumDecimals: 1,
231
+ significantDigits: 5,
232
+ threshold: PRICE_THRESHOLD.LARGE,
233
+ },
234
+ {
235
+ // Medium values ($100-$1,000]: Max 2 decimals, 5 significant figures
236
+ // Ex: $123.456 → $123.46
237
+ condition: (val) => Math.abs(val) > PRICE_THRESHOLD.MEDIUM,
238
+ minimumDecimals: 0,
239
+ maximumDecimals: 2,
240
+ significantDigits: 5,
241
+ threshold: PRICE_THRESHOLD.MEDIUM,
242
+ },
243
+ {
244
+ // Medium-low values ($10-$100]: Max 4 decimals, 5 significant figures
245
+ // Ex: $12.34567 → $12.346
246
+ condition: (val) => Math.abs(val) > PRICE_THRESHOLD.MEDIUM_LOW,
247
+ minimumDecimals: 0,
248
+ maximumDecimals: 4,
249
+ significantDigits: 5,
250
+ threshold: PRICE_THRESHOLD.MEDIUM_LOW,
251
+ },
252
+ {
253
+ // Low values ($0.01-$10]: 5 significant figures, min 2 max MAX_PRICE_DECIMALS decimals
254
+ // Ex: $1.3445555 → $1.3446 | $0.333333 → $0.33333
255
+ condition: (val) => Math.abs(val) >= PRICE_THRESHOLD.LOW,
256
+ significantDigits: 5,
257
+ minimumDecimals: 2,
258
+ maximumDecimals: DECIMAL_PRECISION_CONFIG.MaxPriceDecimals,
259
+ threshold: PRICE_THRESHOLD.LOW,
260
+ },
261
+ {
262
+ // Very small values (< $0.01): 4 significant figures, min 2 max MAX_PRICE_DECIMALS decimals
263
+ // Ex: $0.004236 → $0.004236 | $0.0000006 → $0.000001
264
+ condition: () => true,
265
+ significantDigits: 4,
266
+ minimumDecimals: 2,
267
+ maximumDecimals: DECIMAL_PRECISION_CONFIG.MaxPriceDecimals,
268
+ threshold: PRICE_THRESHOLD.VERY_SMALL,
269
+ },
270
+ ];
271
+ /**
272
+ * Formats a balance value as USD currency with appropriate decimal places
273
+ *
274
+ * @param balance - Raw numeric balance value (e.g., 1234.56, not token minimal denomination)
275
+ * @param options - Optional formatting options
276
+ * @param options.minimumDecimals - Global minimum decimal places (overrides range configs)
277
+ * @param options.maximumDecimals - Global maximum decimal places (overrides range configs)
278
+ * @param options.significantDigits - Global significant digits (overrides decimal settings when set)
279
+ * @param options.ranges - Custom range configurations (defaults to PRICE_RANGES_MINIMAL_VIEW)
280
+ * @param options.currency - Currency code (default: 'USD')
281
+ * @param options.locale - Locale for formatting (default: 'en-US')
282
+ * @param options.stripTrailingZeros - Strip trailing zeros from output (default: false via PRICE_RANGES_MINIMAL_VIEW). When true, overrides minimumDecimals constraint.
283
+ * @returns Formatted currency string with variable decimals based on configured ranges
284
+ * @example
285
+ * // Using defaults (preserves trailing zeros for fiat)
286
+ * formatPerpsFiat(1234.56) => "$1,234.56"
287
+ * formatPerpsFiat(1250.00) => "$1,250.00" // Trailing zeros preserved
288
+ * formatPerpsFiat(50000) => "$50,000.00" // Trailing zeros preserved
289
+ *
290
+ * // Stripping trailing zeros when needed (e.g., for crypto)
291
+ * formatPerpsFiat(1250, { stripTrailingZeros: true }) => "$1,250"
292
+ *
293
+ * // With custom ranges
294
+ * formatPerpsFiat(0.00001, {
295
+ * ranges: [
296
+ * { condition: (v) => v < 0.001, minimumDecimals: 6, maximumDecimals: 8 },
297
+ * { condition: () => true, minimumDecimals: 2, maximumDecimals: 2 }
298
+ * ]
299
+ * }) => "$0.00001" // Trailing zero stripped
300
+ *
301
+ * // With significant digits
302
+ * formatPerpsFiat(1234.56789, { significantDigits: 5 }) => "$1,234.6"
303
+ * formatPerpsFiat(0.0001234, { significantDigits: 3 }) => "$0.000123"
304
+ */
305
+ export const formatPerpsFiat = (balance, options) => {
306
+ const value = typeof balance === 'string' ? parseFloat(balance) : balance;
307
+ const currency = options?.currency ?? 'USD';
308
+ let formatted;
309
+ if (isNaN(value)) {
310
+ // Return placeholder for invalid values to avoid confusion with actual $0 values
311
+ return PERPS_CONSTANTS.FallbackPriceDisplay;
312
+ }
313
+ // Use custom ranges or defaults
314
+ const ranges = options?.ranges ?? PRICE_RANGES_MINIMAL_VIEW;
315
+ // Find the first matching range configuration
316
+ const rangeConfig = ranges.find((range) => range.condition(value));
317
+ if (rangeConfig) {
318
+ // Check for significant digits (global or range-specific)
319
+ const sigDigits = options?.significantDigits ?? rangeConfig.significantDigits;
320
+ // If significant digits are specified, use them
321
+ if (sigDigits) {
322
+ // Get min/max decimals (global overrides range, range overrides default)
323
+ const minDecimals = options?.minimumDecimals ?? rangeConfig.minimumDecimals;
324
+ const maxDecimals = options?.maximumDecimals ?? rangeConfig.maximumDecimals;
325
+ // Calculate appropriate formatting based on significant digits
326
+ const { value: formattedValue, decimals } = formatWithSignificantDigits(value, sigDigits, minDecimals, maxDecimals);
327
+ // Format with the calculated decimal places
328
+ formatted = _formatWithThreshold(formattedValue, rangeConfig.threshold ?? 0.01, {
329
+ currency,
330
+ minimumFractionDigits: decimals,
331
+ maximumFractionDigits: decimals,
332
+ });
333
+ }
334
+ else {
335
+ // Standard decimal-based formatting (existing logic)
336
+ const minDecimals = options?.minimumDecimals ?? rangeConfig.minimumDecimals;
337
+ const maxDecimals = options?.maximumDecimals ?? rangeConfig.maximumDecimals;
338
+ // Use custom formatting if provided
339
+ if (rangeConfig.customFormat) {
340
+ formatted = rangeConfig.customFormat(value, options?.locale ?? 'en-US', currency);
341
+ }
342
+ else {
343
+ // Use standard formatting with threshold
344
+ formatted = _formatWithThreshold(value, rangeConfig.threshold ?? 0.01, {
345
+ currency,
346
+ minimumFractionDigits: minDecimals,
347
+ maximumFractionDigits: maxDecimals,
348
+ });
349
+ }
350
+ }
351
+ }
352
+ else {
353
+ // Fallback if no range matches (shouldn't happen with proper default config)
354
+ const fallbackMin = options?.minimumDecimals ?? 2;
355
+ const fallbackMax = options?.maximumDecimals ?? 2;
356
+ formatted = _formatWithThreshold(value, 0.01, {
357
+ currency,
358
+ minimumFractionDigits: fallbackMin,
359
+ maximumFractionDigits: fallbackMax,
360
+ });
361
+ }
362
+ // Post-process: strip trailing zeros unless explicitly disabled
363
+ // Priority: explicit options.stripTrailingZeros false > rangeConfig > options default > true
364
+ // If options.stripTrailingZeros is explicitly false, skip stripping entirely
365
+ if (options?.stripTrailingZeros === false) {
366
+ return formatted;
367
+ }
368
+ // Otherwise check range config or default to true
369
+ const shouldStrip = rangeConfig?.stripTrailingZeros ?? options?.stripTrailingZeros ?? true;
370
+ if (shouldStrip) {
371
+ // Check if fiat-style stripping is enabled (only strips .00)
372
+ const useFiatStyle = rangeConfig?.fiatStyleStripping ?? false;
373
+ if (useFiatStyle) {
374
+ // Fiat-style: Only strip .00 (no meaningful decimals), preserve 2-decimal format
375
+ // Examples: $1,250.00 → $1,250 | $1,000.10 → $1,000.10 | $13.40 → $13.40
376
+ return formatted.replace(/\.00$/u, '');
377
+ }
378
+ // Standard: Strip all trailing zeros after decimal point
379
+ // Examples: $1,250.00 → $1,250 | $100.0 → $100 | $10.5 → $10.5 | $1.234 → $1.234
380
+ return formatted.replace(/(\.\d*?)0+$/u, '$1').replace(/\.$/u, '');
381
+ }
382
+ return formatted;
383
+ };
384
+ /**
385
+ * Formats position size with variable decimal precision based on magnitude or asset-specific decimals
386
+ * Removes trailing zeros to match task requirements
387
+ *
388
+ * @param size - Raw position size value
389
+ * @param szDecimals - Optional asset-specific decimal precision from Hyperliquid metadata (e.g., BTC=5, ETH=4, DOGE=1)
390
+ * @returns Format varies by size or uses asset-specific decimals, with trailing zeros removed:
391
+ * If szDecimals provided: Uses exact decimals (e.g., 0.00009 BTC with szDecimals=5 => "0.00009")
392
+ * Otherwise falls back to magnitude-based logic:
393
+ * - Size < 0.01: Up to 6 decimals (e.g., "0.00009" not "0.000090")
394
+ * - Size < 1: Up to 4 decimals (e.g., "0.0024" not "0.002400")
395
+ * - Size >= 1: Up to 2 decimals (e.g., "44" not "44.00")
396
+ * @example formatPositionSize(0.00009, 5) => "0.00009" (uses szDecimals)
397
+ * @example formatPositionSize(44.00, 1) => "44" (uses szDecimals, trailing zeros removed)
398
+ * @example formatPositionSize(0.0024) => "0.0024" (no szDecimals, uses magnitude logic)
399
+ * @example formatPositionSize(44.00) => "44" (no szDecimals, uses magnitude logic)
400
+ */
401
+ export const formatPositionSize = (size, szDecimals) => {
402
+ const value = typeof size === 'string' ? parseFloat(size) : size;
403
+ if (isNaN(value) || value === 0) {
404
+ return '0';
405
+ }
406
+ // Use asset-specific decimals if provided (Hyperliquid metadata)
407
+ if (szDecimals !== undefined) {
408
+ return value.toFixed(szDecimals).replace(/\.?0+$/u, '');
409
+ }
410
+ // Fallback: magnitude-based decimal logic for backwards compatibility
411
+ const abs = Math.abs(value);
412
+ let formatted;
413
+ if (abs < 0.01) {
414
+ // For very small numbers, use more decimal places
415
+ formatted = value.toFixed(6);
416
+ }
417
+ else if (abs < 1) {
418
+ // For small numbers, use 4 decimal places
419
+ formatted = value.toFixed(4);
420
+ }
421
+ else {
422
+ // For normal numbers, use 2 decimal places
423
+ formatted = value.toFixed(2);
424
+ }
425
+ // Remove trailing zeros and unnecessary decimal point
426
+ return formatted.replace(/\.?0+$/u, '');
427
+ };
428
+ /**
429
+ * Formats a PnL (Profit and Loss) value with sign prefix
430
+ *
431
+ * @param pnl - Raw numeric PnL value (positive for profit, negative for loss)
432
+ * @returns Format: "+$X,XXX.XX" or "-$X,XXX.XX" (always shows sign, 2 decimals)
433
+ * @example formatPnl(1234.56) => "+$1,234.56"
434
+ * @example formatPnl(-500) => "-$500.00"
435
+ * @example formatPnl(0) => "+$0.00"
436
+ */
437
+ export const formatPnl = (pnl) => {
438
+ const value = typeof pnl === 'string' ? parseFloat(pnl) : pnl;
439
+ if (isNaN(value)) {
440
+ return PERPS_CONSTANTS.ZeroAmountDetailedDisplay;
441
+ }
442
+ const formatted = _formatCurrency(Math.abs(value), 'USD', {
443
+ minimumFractionDigits: 2,
444
+ maximumFractionDigits: 2,
445
+ });
446
+ return value >= 0 ? `+${formatted}` : `-${formatted}`;
447
+ };
448
+ /**
449
+ * Formats a percentage value with sign prefix
450
+ *
451
+ * @param value - Raw percentage value (e.g., 5.25 for 5.25%, not 0.0525)
452
+ * @param decimals - Number of decimal places to show (default: 2)
453
+ * @returns Format: "+X.XX%" or "-X.XX%" (always shows sign, 2 decimals)
454
+ * @example formatPercentage(5.25) => "+5.25%"
455
+ * @example formatPercentage(-2.75) => "-2.75%"
456
+ * @example formatPercentage(0) => "+0.00%"
457
+ */
458
+ export const formatPercentage = (value, decimals = 2) => {
459
+ const parsed = typeof value === 'string' ? parseFloat(value) : value;
460
+ if (isNaN(parsed)) {
461
+ return '0.00%';
462
+ }
463
+ return `${parsed >= 0 ? '+' : ''}${parsed.toFixed(decimals)}%`;
464
+ };
465
+ /**
466
+ * Formats funding rate for display
467
+ *
468
+ * @param value - Raw funding rate value (decimal, not percentage)
469
+ * @param options - Optional formatting options
470
+ * @param options.showZero - Whether to return zero display value for zero/undefined (default: true)
471
+ * @returns Formatted funding rate as percentage string
472
+ * @example formatFundingRate(0.0005) => "0.0500%"
473
+ * @example formatFundingRate(-0.0001) => "-0.0100%"
474
+ * @example formatFundingRate(undefined) => "0.0000%"
475
+ */
476
+ export const formatFundingRate = (value, options) => {
477
+ const showZero = options?.showZero ?? true;
478
+ if (value === undefined || value === null) {
479
+ return showZero ? FUNDING_RATE_CONFIG.ZeroDisplay : '';
480
+ }
481
+ const percentage = value * FUNDING_RATE_CONFIG.PercentageMultiplier;
482
+ const formatted = percentage.toFixed(FUNDING_RATE_CONFIG.Decimals);
483
+ // Check if the result is effectively zero
484
+ if (showZero && parseFloat(formatted) === 0) {
485
+ return FUNDING_RATE_CONFIG.ZeroDisplay;
486
+ }
487
+ return `${formatted}%`;
488
+ };
489
+ //# sourceMappingURL=perpsFormatters.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"perpsFormatters.mjs","sourceRoot":"","sources":["../../src/utils/perpsFormatters.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Portable perps decimal formatters.\n *\n * These are the canonical implementations, exported from the controller so\n * extension and any future consumer can import them directly.\n * No mobile-specific imports — safe to sync to Core.\n *\n * Intl.NumberFormat instances are cached in a module-level Map keyed by\n * serialized options, avoiding repeated construction costs.\n */\nimport {\n DECIMAL_PRECISION_CONFIG,\n FUNDING_RATE_CONFIG,\n PERPS_CONSTANTS,\n} from '../constants/perpsConfig';\n\n// Module-level Intl.NumberFormat cache (keyed by serialized options).\nconst _fmtCache = new Map<string, Intl.NumberFormat>();\n\nfunction _formatCurrency(\n value: number,\n currency: string,\n opts: { minimumFractionDigits: number; maximumFractionDigits: number },\n): string {\n const key = `${currency}:${opts.minimumFractionDigits}:${opts.maximumFractionDigits}`;\n let formatter = _fmtCache.get(key);\n if (!formatter) {\n formatter = new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency,\n currencyDisplay: 'narrowSymbol',\n minimumFractionDigits: opts.minimumFractionDigits,\n maximumFractionDigits: opts.maximumFractionDigits,\n });\n _fmtCache.set(key, formatter);\n }\n return formatter.format(value);\n}\n\n/**\n * Internal equivalent of the mobile formatWithThreshold utility.\n * Formats a currency value, returning \"<$X.XX\" for values below threshold.\n *\n * @param amount - The numeric amount to format.\n * @param threshold - The threshold below which the \"<\" prefix is shown.\n * @param options - Intl formatting options.\n * @param options.currency - ISO 4217 currency code.\n * @param options.minimumFractionDigits - Minimum decimal digits.\n * @param options.maximumFractionDigits - Maximum decimal digits.\n * @returns Formatted currency string.\n */\nfunction _formatWithThreshold(\n amount: number,\n threshold: number,\n options: {\n currency: string;\n minimumFractionDigits: number;\n maximumFractionDigits: number;\n },\n): string {\n const formatOpts = {\n minimumFractionDigits: options.minimumFractionDigits,\n maximumFractionDigits: options.maximumFractionDigits,\n currencyDisplay: 'narrowSymbol' as const,\n };\n if (amount === 0) {\n return _formatCurrency(0, options.currency, formatOpts);\n }\n return Math.abs(amount) < threshold\n ? `<${_formatCurrency(threshold, options.currency, formatOpts)}`\n : _formatCurrency(amount, options.currency, formatOpts);\n}\n\n/**\n * Price threshold constants for PRICE_RANGES_UNIVERSAL\n * These define the boundaries between different formatting ranges\n */\nexport const PRICE_THRESHOLD = {\n /** Very high values boundary (> $100k) */\n VERY_HIGH: 100_000,\n /** High values boundary (> $10k) */\n HIGH: 10_000,\n /** Large values boundary (> $1k) */\n LARGE: 1_000,\n /** Medium values boundary (> $100) */\n MEDIUM: 100,\n /** Medium-low values boundary (> $10) */\n MEDIUM_LOW: 10,\n /** Low values boundary (>= $0.01) */\n LOW: 0.01,\n /**\n * Very small values threshold (< $0.01)\n * This is the minimum value for formatWithThreshold and should align with\n * the 6 decimal maximum (0.000001 is the smallest representable value)\n */\n VERY_SMALL: 0.000001,\n} as const;\n\n/**\n * Configuration for a specific number range formatting\n */\nexport type FiatRangeConfig = {\n /**\n * The condition to match for this range (e.g., < 0.0001, < 1, >= 1000)\n * Function should return true if this config should be applied\n */\n condition: (value: number) => boolean;\n /** Minimum decimal places for this range */\n minimumDecimals: number;\n /** Maximum decimal places for this range */\n maximumDecimals: number;\n /** Optional threshold for formatWithThreshold (defaults to the range boundary) */\n threshold?: number;\n /** Optional significant digits for this range (overrides decimal places when set) */\n significantDigits?: number;\n /** Optional custom formatting logic for this range */\n customFormat?: (value: number, locale: string, currency: string) => string;\n /** Optional flag to strip trailing zeros for this range (overrides global stripTrailingZeros option) */\n stripTrailingZeros?: boolean;\n /**\n * Optional flag for fiat-style stripping (only strips .00, preserves meaningful decimals like .10, .40)\n * When true, \"$1,250.00\" → \"$1,250\" but \"$1,250.10\" stays \"$1,250.10\"\n * When false (default), strips all trailing zeros: \"$1,250.10\" → \"$1,250.1\"\n */\n fiatStyleStripping?: boolean;\n};\n\n/**\n * Formats a number to a specific number of significant digits\n * Strips trailing zeros unless minDecimals requires them\n *\n * @param value - The numeric value to format\n * @param significantDigits - Number of significant digits to maintain\n * @param minDecimals - Minimum decimal places to show (may add zeros)\n * @param maxDecimals - Maximum decimal places allowed\n * @returns Formatted number with appropriate precision, trailing zeros removed\n */\nexport function formatWithSignificantDigits(\n value: number,\n significantDigits: number,\n minDecimals?: number,\n maxDecimals?: number,\n): { value: number; decimals: number } {\n // Handle special cases\n if (value === 0) {\n // Return zero with no trailing decimals by default (matches stripTrailingZeros behavior)\n // Can be overridden by explicit minDecimals if needed\n return { value: 0, decimals: minDecimals ?? 0 };\n }\n\n const absValue = Math.abs(value);\n\n // For numbers >= 1, calculate decimals based on magnitude to achieve target significant figures\n // This ensures consistent precision across different price ranges:\n // Examples with 4 significant figures:\n // $123,456.78 → $123,456.78 (≥$1000: 2 decimals minimum, 8 sig figs)\n // $456.12 → $456.12 (≥$10: 2 decimals minimum, 5 sig figs)\n // $56.123 → $56.123 (≥$10: 2 decimals minimum, 5 sig figs)\n // $5.123 → $5.123 ($1-$10: 3 decimals = 4 sig figs)\n // $2.801 → $2.801 ($1-$10: 3 decimals = 4 sig figs)\n // $1.234 → $1.234 ($1-$10: 3 decimals = 4 sig figs)\n if (absValue >= 1) {\n let targetDecimals: number;\n\n // Calculate decimals needed based on integer digits to achieve target significant figures\n // For $38.388 with 5 sig figs: 2 integer digits, need 3 decimals (3,8,3,8,8)\n // For $123.45 with 5 sig figs: 3 integer digits, need 2 decimals (1,2,3,4,5)\n const integerDigits = Math.floor(Math.log10(absValue)) + 1;\n const decimalsNeeded = significantDigits - integerDigits;\n targetDecimals = Math.max(decimalsNeeded, 0); // Can't have negative decimals\n\n // Apply explicit minimum decimals constraint if provided (for special cases)\n if (minDecimals !== undefined && targetDecimals < minDecimals) {\n targetDecimals = minDecimals;\n }\n\n // Apply maximum decimals constraint if specified\n const finalDecimals =\n maxDecimals === undefined\n ? targetDecimals\n : Math.min(targetDecimals, maxDecimals);\n\n // Round to prevent floating-point artifacts (e.g., 2.820000000000003 → 2.82)\n const roundedValue = Number(value.toFixed(finalDecimals));\n\n return {\n value: roundedValue,\n decimals: finalDecimals,\n };\n }\n\n // For numbers < 1, use toPrecision to limit to significantDigits\n // Examples: 0.1234, 0.01234 should show exactly 4 sig figs\n const precisionStr = absValue.toPrecision(significantDigits);\n const precisionNum = parseFloat(precisionStr);\n\n // Convert to string to count actual decimals after trailing zeros are removed\n const valueStr = precisionNum.toString();\n const [, decPart = ''] = valueStr.split('.');\n let actualDecimals = decPart.length;\n\n // Apply min/max decimal constraints\n if (minDecimals !== undefined && actualDecimals < minDecimals) {\n actualDecimals = minDecimals; // Will add zeros if needed\n }\n if (maxDecimals !== undefined && actualDecimals > maxDecimals) {\n actualDecimals = maxDecimals;\n }\n\n // Return the value with sign restored and decimal count\n return {\n value: value < 0 ? -precisionNum : precisionNum,\n decimals: actualDecimals,\n };\n}\n\n/**\n * Minimal view fiat range configuration\n * Uses fiat-style stripping for clean currency display\n * Strips only .00 to avoid partial decimals like $1,250.1\n */\nexport const PRICE_RANGES_MINIMAL_VIEW: FiatRangeConfig[] = [\n {\n // Large values (>= $1000): Strip .00 only ($5,000 not $5,000.00, but $5,000.10 stays)\n condition: (val: number) => Math.abs(val) >= PRICE_THRESHOLD.LARGE,\n minimumDecimals: 2,\n maximumDecimals: 2,\n threshold: PRICE_THRESHOLD.LARGE,\n stripTrailingZeros: true,\n fiatStyleStripping: true,\n },\n {\n // Small values (< $1000): Also use fiat-style stripping ($100 not $100.00, but $13.40 stays)\n condition: () => true,\n minimumDecimals: 2,\n maximumDecimals: 2,\n threshold: PRICE_THRESHOLD.LOW,\n stripTrailingZeros: true,\n fiatStyleStripping: true,\n },\n];\n\n/**\n * Universal price range configuration following comprehensive rules from rules-decimals.md\n *\n * Rules:\n * - Max 6 decimals across all ranges (Hyperliquid limit)\n * - Strip trailing zeros by default\n * - Use |v| (absolute value) for conditions\n *\n * Significant digits by range:\n * - > $100,000: 6 sig digs\n * - $100,000 > x > $0.01: 5 sig digs\n * - < $0.01: 4 sig digs\n *\n * Decimal limits by price range:\n * - |v| > 10,000: min 0, max 0 decimals; 5 sig digs (6 if >100k)\n * - |v| > 1,000: min 0, max 1 decimal; 5 sig digs\n * - |v| > 100: min 0, max 2 decimals; 5 sig digs\n * - |v| > 10: min 0, max 4 decimals; 5 sig digs\n * - |v| ≥ 0.01: 5 sig digs, min 2, max 6 decimals\n * - |v| < 0.01: 4 sig digs, min 2, max 6 decimals\n *\n * Examples:\n * - $123,456.78 → $123,457 (>$10k: 0 decimals, 6 sig figs)\n * - $12,345.67 → $12,346 (>$10k: 0 decimals, 5 sig figs)\n * - $1,234.56 → $1,234.6 ($1k-$10k: 1 decimal, 5 sig figs)\n * - $123.456 → $123.46 ($100-$1k: 2 decimals, 5 sig figs)\n * - $12.34567 → $12.346 ($10-$100: 4 decimals, 5 sig figs)\n * - $1.3445555 → $1.3446 (≥$0.01: 5 sig figs)\n * - $0.333333 → $0.33333 (≥$0.01: 5 sig figs)\n * - $0.004236 → $0.004236 (<$0.01: 4 sig figs, max 6 decimals)\n * - $0.0000006 → $0.000001 (<$0.01: 4 sig figs, rounds with max 6 decimals)\n */\nexport const PRICE_RANGES_UNIVERSAL: FiatRangeConfig[] = [\n {\n // Very high values (> $100,000): No decimals, 6 significant figures\n // Ex: $123,456.78 → $123,457\n condition: (val) => Math.abs(val) > PRICE_THRESHOLD.VERY_HIGH,\n minimumDecimals: 0,\n maximumDecimals: 0,\n significantDigits: 6,\n threshold: PRICE_THRESHOLD.VERY_HIGH,\n },\n {\n // High values ($10,000-$100,000]: No decimals, 5 significant figures\n // Ex: $12,345.67 → $12,346\n condition: (val) => Math.abs(val) > PRICE_THRESHOLD.HIGH,\n minimumDecimals: 0,\n maximumDecimals: 0,\n significantDigits: 5,\n threshold: PRICE_THRESHOLD.HIGH,\n },\n {\n // Large values ($1,000-$10,000]: Max 1 decimal, 5 significant figures\n // Ex: $1,234.56 → $1,234.6\n condition: (val) => Math.abs(val) > PRICE_THRESHOLD.LARGE,\n minimumDecimals: 0,\n maximumDecimals: 1,\n significantDigits: 5,\n threshold: PRICE_THRESHOLD.LARGE,\n },\n {\n // Medium values ($100-$1,000]: Max 2 decimals, 5 significant figures\n // Ex: $123.456 → $123.46\n condition: (val) => Math.abs(val) > PRICE_THRESHOLD.MEDIUM,\n minimumDecimals: 0,\n maximumDecimals: 2,\n significantDigits: 5,\n threshold: PRICE_THRESHOLD.MEDIUM,\n },\n {\n // Medium-low values ($10-$100]: Max 4 decimals, 5 significant figures\n // Ex: $12.34567 → $12.346\n condition: (val) => Math.abs(val) > PRICE_THRESHOLD.MEDIUM_LOW,\n minimumDecimals: 0,\n maximumDecimals: 4,\n significantDigits: 5,\n threshold: PRICE_THRESHOLD.MEDIUM_LOW,\n },\n {\n // Low values ($0.01-$10]: 5 significant figures, min 2 max MAX_PRICE_DECIMALS decimals\n // Ex: $1.3445555 → $1.3446 | $0.333333 → $0.33333\n condition: (val) => Math.abs(val) >= PRICE_THRESHOLD.LOW,\n significantDigits: 5,\n minimumDecimals: 2,\n maximumDecimals: DECIMAL_PRECISION_CONFIG.MaxPriceDecimals,\n threshold: PRICE_THRESHOLD.LOW,\n },\n {\n // Very small values (< $0.01): 4 significant figures, min 2 max MAX_PRICE_DECIMALS decimals\n // Ex: $0.004236 → $0.004236 | $0.0000006 → $0.000001\n condition: () => true,\n significantDigits: 4,\n minimumDecimals: 2,\n maximumDecimals: DECIMAL_PRECISION_CONFIG.MaxPriceDecimals,\n threshold: PRICE_THRESHOLD.VERY_SMALL,\n },\n];\n\n/**\n * Formats a balance value as USD currency with appropriate decimal places\n *\n * @param balance - Raw numeric balance value (e.g., 1234.56, not token minimal denomination)\n * @param options - Optional formatting options\n * @param options.minimumDecimals - Global minimum decimal places (overrides range configs)\n * @param options.maximumDecimals - Global maximum decimal places (overrides range configs)\n * @param options.significantDigits - Global significant digits (overrides decimal settings when set)\n * @param options.ranges - Custom range configurations (defaults to PRICE_RANGES_MINIMAL_VIEW)\n * @param options.currency - Currency code (default: 'USD')\n * @param options.locale - Locale for formatting (default: 'en-US')\n * @param options.stripTrailingZeros - Strip trailing zeros from output (default: false via PRICE_RANGES_MINIMAL_VIEW). When true, overrides minimumDecimals constraint.\n * @returns Formatted currency string with variable decimals based on configured ranges\n * @example\n * // Using defaults (preserves trailing zeros for fiat)\n * formatPerpsFiat(1234.56) => \"$1,234.56\"\n * formatPerpsFiat(1250.00) => \"$1,250.00\" // Trailing zeros preserved\n * formatPerpsFiat(50000) => \"$50,000.00\" // Trailing zeros preserved\n *\n * // Stripping trailing zeros when needed (e.g., for crypto)\n * formatPerpsFiat(1250, { stripTrailingZeros: true }) => \"$1,250\"\n *\n * // With custom ranges\n * formatPerpsFiat(0.00001, {\n * ranges: [\n * { condition: (v) => v < 0.001, minimumDecimals: 6, maximumDecimals: 8 },\n * { condition: () => true, minimumDecimals: 2, maximumDecimals: 2 }\n * ]\n * }) => \"$0.00001\" // Trailing zero stripped\n *\n * // With significant digits\n * formatPerpsFiat(1234.56789, { significantDigits: 5 }) => \"$1,234.6\"\n * formatPerpsFiat(0.0001234, { significantDigits: 3 }) => \"$0.000123\"\n */\nexport const formatPerpsFiat = (\n balance: string | number,\n options?: {\n minimumDecimals?: number;\n maximumDecimals?: number;\n significantDigits?: number;\n ranges?: FiatRangeConfig[];\n currency?: string;\n locale?: string;\n stripTrailingZeros?: boolean;\n },\n): string => {\n const value = typeof balance === 'string' ? parseFloat(balance) : balance;\n const currency = options?.currency ?? 'USD';\n\n let formatted: string;\n\n if (isNaN(value)) {\n // Return placeholder for invalid values to avoid confusion with actual $0 values\n return PERPS_CONSTANTS.FallbackPriceDisplay;\n }\n\n // Use custom ranges or defaults\n const ranges = options?.ranges ?? PRICE_RANGES_MINIMAL_VIEW;\n\n // Find the first matching range configuration\n const rangeConfig = ranges.find((range) => range.condition(value));\n\n if (rangeConfig) {\n // Check for significant digits (global or range-specific)\n const sigDigits =\n options?.significantDigits ?? rangeConfig.significantDigits;\n\n // If significant digits are specified, use them\n if (sigDigits) {\n // Get min/max decimals (global overrides range, range overrides default)\n const minDecimals =\n options?.minimumDecimals ?? rangeConfig.minimumDecimals;\n const maxDecimals =\n options?.maximumDecimals ?? rangeConfig.maximumDecimals;\n\n // Calculate appropriate formatting based on significant digits\n const { value: formattedValue, decimals } = formatWithSignificantDigits(\n value,\n sigDigits,\n minDecimals,\n maxDecimals,\n );\n\n // Format with the calculated decimal places\n formatted = _formatWithThreshold(\n formattedValue,\n rangeConfig.threshold ?? 0.01,\n {\n currency,\n minimumFractionDigits: decimals,\n maximumFractionDigits: decimals,\n },\n );\n } else {\n // Standard decimal-based formatting (existing logic)\n const minDecimals =\n options?.minimumDecimals ?? rangeConfig.minimumDecimals;\n const maxDecimals =\n options?.maximumDecimals ?? rangeConfig.maximumDecimals;\n\n // Use custom formatting if provided\n if (rangeConfig.customFormat) {\n formatted = rangeConfig.customFormat(\n value,\n options?.locale ?? 'en-US',\n currency,\n );\n } else {\n // Use standard formatting with threshold\n formatted = _formatWithThreshold(value, rangeConfig.threshold ?? 0.01, {\n currency,\n minimumFractionDigits: minDecimals,\n maximumFractionDigits: maxDecimals,\n });\n }\n }\n } else {\n // Fallback if no range matches (shouldn't happen with proper default config)\n const fallbackMin = options?.minimumDecimals ?? 2;\n const fallbackMax = options?.maximumDecimals ?? 2;\n formatted = _formatWithThreshold(value, 0.01, {\n currency,\n minimumFractionDigits: fallbackMin,\n maximumFractionDigits: fallbackMax,\n });\n }\n\n // Post-process: strip trailing zeros unless explicitly disabled\n // Priority: explicit options.stripTrailingZeros false > rangeConfig > options default > true\n // If options.stripTrailingZeros is explicitly false, skip stripping entirely\n if (options?.stripTrailingZeros === false) {\n return formatted;\n }\n\n // Otherwise check range config or default to true\n const shouldStrip =\n rangeConfig?.stripTrailingZeros ?? options?.stripTrailingZeros ?? true;\n\n if (shouldStrip) {\n // Check if fiat-style stripping is enabled (only strips .00)\n const useFiatStyle = rangeConfig?.fiatStyleStripping ?? false;\n\n if (useFiatStyle) {\n // Fiat-style: Only strip .00 (no meaningful decimals), preserve 2-decimal format\n // Examples: $1,250.00 → $1,250 | $1,000.10 → $1,000.10 | $13.40 → $13.40\n return formatted.replace(/\\.00$/u, '');\n }\n // Standard: Strip all trailing zeros after decimal point\n // Examples: $1,250.00 → $1,250 | $100.0 → $100 | $10.5 → $10.5 | $1.234 → $1.234\n return formatted.replace(/(\\.\\d*?)0+$/u, '$1').replace(/\\.$/u, '');\n }\n\n return formatted;\n};\n\n/**\n * Formats position size with variable decimal precision based on magnitude or asset-specific decimals\n * Removes trailing zeros to match task requirements\n *\n * @param size - Raw position size value\n * @param szDecimals - Optional asset-specific decimal precision from Hyperliquid metadata (e.g., BTC=5, ETH=4, DOGE=1)\n * @returns Format varies by size or uses asset-specific decimals, with trailing zeros removed:\n * If szDecimals provided: Uses exact decimals (e.g., 0.00009 BTC with szDecimals=5 => \"0.00009\")\n * Otherwise falls back to magnitude-based logic:\n * - Size < 0.01: Up to 6 decimals (e.g., \"0.00009\" not \"0.000090\")\n * - Size < 1: Up to 4 decimals (e.g., \"0.0024\" not \"0.002400\")\n * - Size >= 1: Up to 2 decimals (e.g., \"44\" not \"44.00\")\n * @example formatPositionSize(0.00009, 5) => \"0.00009\" (uses szDecimals)\n * @example formatPositionSize(44.00, 1) => \"44\" (uses szDecimals, trailing zeros removed)\n * @example formatPositionSize(0.0024) => \"0.0024\" (no szDecimals, uses magnitude logic)\n * @example formatPositionSize(44.00) => \"44\" (no szDecimals, uses magnitude logic)\n */\nexport const formatPositionSize = (\n size: string | number,\n szDecimals?: number,\n): string => {\n const value = typeof size === 'string' ? parseFloat(size) : size;\n\n if (isNaN(value) || value === 0) {\n return '0';\n }\n\n // Use asset-specific decimals if provided (Hyperliquid metadata)\n if (szDecimals !== undefined) {\n return value.toFixed(szDecimals).replace(/\\.?0+$/u, '');\n }\n\n // Fallback: magnitude-based decimal logic for backwards compatibility\n const abs = Math.abs(value);\n let formatted: string;\n\n if (abs < 0.01) {\n // For very small numbers, use more decimal places\n formatted = value.toFixed(6);\n } else if (abs < 1) {\n // For small numbers, use 4 decimal places\n formatted = value.toFixed(4);\n } else {\n // For normal numbers, use 2 decimal places\n formatted = value.toFixed(2);\n }\n\n // Remove trailing zeros and unnecessary decimal point\n return formatted.replace(/\\.?0+$/u, '');\n};\n\n/**\n * Formats a PnL (Profit and Loss) value with sign prefix\n *\n * @param pnl - Raw numeric PnL value (positive for profit, negative for loss)\n * @returns Format: \"+$X,XXX.XX\" or \"-$X,XXX.XX\" (always shows sign, 2 decimals)\n * @example formatPnl(1234.56) => \"+$1,234.56\"\n * @example formatPnl(-500) => \"-$500.00\"\n * @example formatPnl(0) => \"+$0.00\"\n */\nexport const formatPnl = (pnl: string | number): string => {\n const value = typeof pnl === 'string' ? parseFloat(pnl) : pnl;\n\n if (isNaN(value)) {\n return PERPS_CONSTANTS.ZeroAmountDetailedDisplay;\n }\n\n const formatted = _formatCurrency(Math.abs(value), 'USD', {\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n });\n\n return value >= 0 ? `+${formatted}` : `-${formatted}`;\n};\n\n/**\n * Formats a percentage value with sign prefix\n *\n * @param value - Raw percentage value (e.g., 5.25 for 5.25%, not 0.0525)\n * @param decimals - Number of decimal places to show (default: 2)\n * @returns Format: \"+X.XX%\" or \"-X.XX%\" (always shows sign, 2 decimals)\n * @example formatPercentage(5.25) => \"+5.25%\"\n * @example formatPercentage(-2.75) => \"-2.75%\"\n * @example formatPercentage(0) => \"+0.00%\"\n */\nexport const formatPercentage = (\n value: string | number,\n decimals: number = 2,\n): string => {\n const parsed = typeof value === 'string' ? parseFloat(value) : value;\n\n if (isNaN(parsed)) {\n return '0.00%';\n }\n\n return `${parsed >= 0 ? '+' : ''}${parsed.toFixed(decimals)}%`;\n};\n\n/**\n * Formats funding rate for display\n *\n * @param value - Raw funding rate value (decimal, not percentage)\n * @param options - Optional formatting options\n * @param options.showZero - Whether to return zero display value for zero/undefined (default: true)\n * @returns Formatted funding rate as percentage string\n * @example formatFundingRate(0.0005) => \"0.0500%\"\n * @example formatFundingRate(-0.0001) => \"-0.0100%\"\n * @example formatFundingRate(undefined) => \"0.0000%\"\n */\nexport const formatFundingRate = (\n value?: number | null,\n options?: { showZero?: boolean },\n): string => {\n const showZero = options?.showZero ?? true;\n\n if (value === undefined || value === null) {\n return showZero ? FUNDING_RATE_CONFIG.ZeroDisplay : '';\n }\n\n const percentage = value * FUNDING_RATE_CONFIG.PercentageMultiplier;\n const formatted = percentage.toFixed(FUNDING_RATE_CONFIG.Decimals);\n\n // Check if the result is effectively zero\n if (showZero && parseFloat(formatted) === 0) {\n return FUNDING_RATE_CONFIG.ZeroDisplay;\n }\n\n return `${formatted}%`;\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metamask-previews/perps-controller",
3
- "version": "3.0.0-preview-e61cfa5",
3
+ "version": "3.1.0-preview-548bdd1d9",
4
4
  "description": "Controller for perpetual trading functionality in MetaMask",
5
5
  "keywords": [
6
6
  "Ethereum",
@@ -57,7 +57,7 @@
57
57
  },
58
58
  "dependencies": {
59
59
  "@metamask/abi-utils": "^2.0.3",
60
- "@metamask/base-controller": "^9.0.1",
60
+ "@metamask/base-controller": "^9.1.0",
61
61
  "@metamask/controller-utils": "^11.20.0",
62
62
  "@metamask/messenger": "^1.1.1",
63
63
  "@metamask/utils": "^11.9.0",
@@ -71,7 +71,7 @@
71
71
  "@metamask/auto-changelog": "^6.0.0",
72
72
  "@metamask/geolocation-controller": "^0.1.2",
73
73
  "@metamask/keyring-controller": "^25.2.0",
74
- "@metamask/keyring-internal-api": "^10.0.0",
74
+ "@metamask/keyring-internal-api": "^10.1.0",
75
75
  "@metamask/network-controller": "^30.0.1",
76
76
  "@metamask/profile-sync-controller": "^28.0.2",
77
77
  "@metamask/remote-feature-flag-controller": "^4.2.0",