@lifi/perps-sdk 23.1.0 → 24.0.0

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 (289) hide show
  1. package/README.md +85 -45
  2. package/dist/cjs/client/PerpsClient.d.ts +3 -2
  3. package/dist/cjs/client/PerpsClient.d.ts.map +1 -1
  4. package/dist/cjs/client/PerpsClient.js +16 -9
  5. package/dist/cjs/client/PerpsClient.js.map +1 -1
  6. package/dist/cjs/decimal/arithmetic.d.ts +13 -0
  7. package/dist/cjs/decimal/arithmetic.d.ts.map +1 -0
  8. package/dist/cjs/decimal/arithmetic.js +48 -0
  9. package/dist/cjs/decimal/arithmetic.js.map +1 -0
  10. package/dist/cjs/decimal/big.d.ts +0 -1
  11. package/dist/cjs/decimal/big.d.ts.map +1 -1
  12. package/dist/cjs/decimal/big.js +0 -4
  13. package/dist/cjs/decimal/big.js.map +1 -1
  14. package/dist/cjs/decimal/compare.d.ts +6 -2
  15. package/dist/cjs/decimal/compare.d.ts.map +1 -1
  16. package/dist/cjs/decimal/compare.js +15 -2
  17. package/dist/cjs/decimal/compare.js.map +1 -1
  18. package/dist/cjs/decimal/convert.d.ts +12 -6
  19. package/dist/cjs/decimal/convert.d.ts.map +1 -1
  20. package/dist/cjs/decimal/convert.js +36 -14
  21. package/dist/cjs/decimal/convert.js.map +1 -1
  22. package/dist/cjs/decimal/decimalStringToBig.d.ts +5 -0
  23. package/dist/cjs/decimal/decimalStringToBig.d.ts.map +1 -0
  24. package/dist/cjs/decimal/decimalStringToBig.js +27 -0
  25. package/dist/cjs/decimal/decimalStringToBig.js.map +1 -0
  26. package/dist/cjs/decimal/format.d.ts.map +1 -1
  27. package/dist/cjs/decimal/format.js +70 -52
  28. package/dist/cjs/decimal/format.js.map +1 -1
  29. package/dist/cjs/decimal/parse.d.ts +4 -4
  30. package/dist/cjs/decimal/parse.d.ts.map +1 -1
  31. package/dist/cjs/decimal/parse.js +42 -25
  32. package/dist/cjs/decimal/parse.js.map +1 -1
  33. package/dist/cjs/index.d.ts +13 -11
  34. package/dist/cjs/index.d.ts.map +1 -1
  35. package/dist/cjs/index.js +64 -8
  36. package/dist/cjs/index.js.map +1 -1
  37. package/dist/cjs/math/fills.d.ts +1 -1
  38. package/dist/cjs/math/fills.d.ts.map +1 -1
  39. package/dist/cjs/math/fills.js +28 -13
  40. package/dist/cjs/math/fills.js.map +1 -1
  41. package/dist/cjs/math/order.d.ts +27 -16
  42. package/dist/cjs/math/order.d.ts.map +1 -1
  43. package/dist/cjs/math/order.js +106 -122
  44. package/dist/cjs/math/order.js.map +1 -1
  45. package/dist/cjs/math/position.d.ts +85 -35
  46. package/dist/cjs/math/position.d.ts.map +1 -1
  47. package/dist/cjs/math/position.js +88 -91
  48. package/dist/cjs/math/position.js.map +1 -1
  49. package/dist/cjs/services/getGasRecommendation.js +1 -1
  50. package/dist/cjs/services/getGasRecommendation.js.map +1 -1
  51. package/dist/cjs/services/getQuote.d.ts +1 -1
  52. package/dist/cjs/services/resolveSubscribeQuote.d.ts.map +1 -1
  53. package/dist/cjs/services/resolveSubscribeQuote.js +3 -2
  54. package/dist/cjs/services/resolveSubscribeQuote.js.map +1 -1
  55. package/dist/cjs/transport/request.d.ts +1 -0
  56. package/dist/cjs/transport/request.d.ts.map +1 -1
  57. package/dist/cjs/transport/request.js +2 -2
  58. package/dist/cjs/transport/request.js.map +1 -1
  59. package/dist/cjs/types/api.d.ts +4 -0
  60. package/dist/cjs/types/api.d.ts.map +1 -1
  61. package/dist/cjs/types/provider.d.ts +9 -5
  62. package/dist/cjs/types/provider.d.ts.map +1 -1
  63. package/dist/cjs/types/withdrawal.d.ts +12 -4
  64. package/dist/cjs/types/withdrawal.d.ts.map +1 -1
  65. package/dist/cjs/utils/createSafeFunction.d.ts +2 -0
  66. package/dist/cjs/utils/createSafeFunction.d.ts.map +1 -0
  67. package/dist/cjs/utils/createSafeFunction.js +15 -0
  68. package/dist/cjs/utils/createSafeFunction.js.map +1 -0
  69. package/dist/cjs/utils/timestampToIsoString.d.ts +3 -0
  70. package/dist/cjs/utils/timestampToIsoString.d.ts.map +1 -0
  71. package/dist/cjs/utils/timestampToIsoString.js +16 -0
  72. package/dist/cjs/utils/timestampToIsoString.js.map +1 -0
  73. package/dist/cjs/utils/warnOnce.d.ts +2 -0
  74. package/dist/cjs/utils/warnOnce.d.ts.map +1 -1
  75. package/dist/cjs/utils/warnOnce.js +13 -1
  76. package/dist/cjs/utils/warnOnce.js.map +1 -1
  77. package/dist/cjs/version.d.ts +1 -1
  78. package/dist/cjs/version.js +1 -1
  79. package/dist/cjs/websocket/PerpsWsClient.d.ts +2 -0
  80. package/dist/cjs/websocket/PerpsWsClient.d.ts.map +1 -1
  81. package/dist/cjs/websocket/PerpsWsClient.js +14 -1
  82. package/dist/cjs/websocket/PerpsWsClient.js.map +1 -1
  83. package/dist/cjs/websocket/wsLog.d.ts +2 -0
  84. package/dist/cjs/websocket/wsLog.d.ts.map +1 -1
  85. package/dist/cjs/websocket/wsLog.js +8 -0
  86. package/dist/cjs/websocket/wsLog.js.map +1 -1
  87. package/dist/cjs/wire/orderAmounts.d.ts.map +1 -1
  88. package/dist/cjs/wire/orderAmounts.js +6 -9
  89. package/dist/cjs/wire/orderAmounts.js.map +1 -1
  90. package/dist/cjs/wire/refuel.d.ts +4 -1
  91. package/dist/cjs/wire/refuel.d.ts.map +1 -1
  92. package/dist/cjs/wire/refuel.js +23 -6
  93. package/dist/cjs/wire/refuel.js.map +1 -1
  94. package/dist/cjs/wire/transferable.js +3 -3
  95. package/dist/cjs/wire/transferable.js.map +1 -1
  96. package/dist/cjs/wire/withdraw.d.ts.map +1 -1
  97. package/dist/cjs/wire/withdraw.js +3 -3
  98. package/dist/cjs/wire/withdraw.js.map +1 -1
  99. package/dist/esm/client/PerpsClient.d.ts +17 -4
  100. package/dist/esm/client/PerpsClient.d.ts.map +1 -1
  101. package/dist/esm/client/PerpsClient.js +30 -8
  102. package/dist/esm/client/PerpsClient.js.map +1 -1
  103. package/dist/esm/decimal/arithmetic.d.ts +84 -0
  104. package/dist/esm/decimal/arithmetic.d.ts.map +1 -0
  105. package/dist/esm/decimal/arithmetic.js +110 -0
  106. package/dist/esm/decimal/arithmetic.js.map +1 -0
  107. package/dist/esm/decimal/big.d.ts +0 -2
  108. package/dist/esm/decimal/big.d.ts.map +1 -1
  109. package/dist/esm/decimal/big.js +0 -4
  110. package/dist/esm/decimal/big.js.map +1 -1
  111. package/dist/esm/decimal/compare.d.ts +39 -5
  112. package/dist/esm/decimal/compare.d.ts.map +1 -1
  113. package/dist/esm/decimal/compare.js +45 -5
  114. package/dist/esm/decimal/compare.js.map +1 -1
  115. package/dist/esm/decimal/convert.d.ts +62 -26
  116. package/dist/esm/decimal/convert.d.ts.map +1 -1
  117. package/dist/esm/decimal/convert.js +76 -25
  118. package/dist/esm/decimal/convert.js.map +1 -1
  119. package/dist/esm/decimal/decimalStringToBig.d.ts +26 -0
  120. package/dist/esm/decimal/decimalStringToBig.d.ts.map +1 -0
  121. package/dist/esm/decimal/decimalStringToBig.js +40 -0
  122. package/dist/esm/decimal/decimalStringToBig.js.map +1 -0
  123. package/dist/esm/decimal/format.d.ts +4 -1
  124. package/dist/esm/decimal/format.d.ts.map +1 -1
  125. package/dist/esm/decimal/format.js +71 -70
  126. package/dist/esm/decimal/format.js.map +1 -1
  127. package/dist/esm/decimal/parse.d.ts +22 -13
  128. package/dist/esm/decimal/parse.d.ts.map +1 -1
  129. package/dist/esm/decimal/parse.js +56 -46
  130. package/dist/esm/decimal/parse.js.map +1 -1
  131. package/dist/esm/index.d.ts +13 -11
  132. package/dist/esm/index.d.ts.map +1 -1
  133. package/dist/esm/index.js +9 -7
  134. package/dist/esm/index.js.map +1 -1
  135. package/dist/esm/math/fills.d.ts +6 -3
  136. package/dist/esm/math/fills.d.ts.map +1 -1
  137. package/dist/esm/math/fills.js +31 -16
  138. package/dist/esm/math/fills.js.map +1 -1
  139. package/dist/esm/math/order.d.ts +85 -65
  140. package/dist/esm/math/order.d.ts.map +1 -1
  141. package/dist/esm/math/order.js +158 -166
  142. package/dist/esm/math/order.js.map +1 -1
  143. package/dist/esm/math/position.d.ts +155 -98
  144. package/dist/esm/math/position.d.ts.map +1 -1
  145. package/dist/esm/math/position.js +157 -155
  146. package/dist/esm/math/position.js.map +1 -1
  147. package/dist/esm/services/getGasRecommendation.js +1 -1
  148. package/dist/esm/services/getGasRecommendation.js.map +1 -1
  149. package/dist/esm/services/getQuote.d.ts +2 -2
  150. package/dist/esm/services/getQuote.js +1 -1
  151. package/dist/esm/services/resolveSubscribeQuote.d.ts +1 -1
  152. package/dist/esm/services/resolveSubscribeQuote.d.ts.map +1 -1
  153. package/dist/esm/services/resolveSubscribeQuote.js +4 -3
  154. package/dist/esm/services/resolveSubscribeQuote.js.map +1 -1
  155. package/dist/esm/transport/request.d.ts +5 -0
  156. package/dist/esm/transport/request.d.ts.map +1 -1
  157. package/dist/esm/transport/request.js +2 -2
  158. package/dist/esm/transport/request.js.map +1 -1
  159. package/dist/esm/types/api.d.ts +9 -0
  160. package/dist/esm/types/api.d.ts.map +1 -1
  161. package/dist/esm/types/provider.d.ts +26 -13
  162. package/dist/esm/types/provider.d.ts.map +1 -1
  163. package/dist/esm/types/withdrawal.d.ts +32 -7
  164. package/dist/esm/types/withdrawal.d.ts.map +1 -1
  165. package/dist/esm/utils/createSafeFunction.d.ts +9 -0
  166. package/dist/esm/utils/createSafeFunction.d.ts.map +1 -0
  167. package/dist/esm/utils/createSafeFunction.js +19 -0
  168. package/dist/esm/utils/createSafeFunction.js.map +1 -0
  169. package/dist/esm/utils/timestampToIsoString.d.ts +15 -0
  170. package/dist/esm/utils/timestampToIsoString.d.ts.map +1 -0
  171. package/dist/esm/utils/timestampToIsoString.js +24 -0
  172. package/dist/esm/utils/timestampToIsoString.js.map +1 -0
  173. package/dist/esm/utils/warnOnce.d.ts +16 -0
  174. package/dist/esm/utils/warnOnce.d.ts.map +1 -1
  175. package/dist/esm/utils/warnOnce.js +24 -0
  176. package/dist/esm/utils/warnOnce.js.map +1 -1
  177. package/dist/esm/version.d.ts +1 -1
  178. package/dist/esm/version.js +1 -1
  179. package/dist/esm/websocket/PerpsWsClient.d.ts +7 -0
  180. package/dist/esm/websocket/PerpsWsClient.d.ts.map +1 -1
  181. package/dist/esm/websocket/PerpsWsClient.js +20 -2
  182. package/dist/esm/websocket/PerpsWsClient.js.map +1 -1
  183. package/dist/esm/websocket/wsLog.d.ts +12 -0
  184. package/dist/esm/websocket/wsLog.d.ts.map +1 -1
  185. package/dist/esm/websocket/wsLog.js +18 -0
  186. package/dist/esm/websocket/wsLog.js.map +1 -1
  187. package/dist/esm/wire/orderAmounts.d.ts.map +1 -1
  188. package/dist/esm/wire/orderAmounts.js +2 -2
  189. package/dist/esm/wire/orderAmounts.js.map +1 -1
  190. package/dist/esm/wire/refuel.d.ts +24 -14
  191. package/dist/esm/wire/refuel.d.ts.map +1 -1
  192. package/dist/esm/wire/refuel.js +38 -17
  193. package/dist/esm/wire/refuel.js.map +1 -1
  194. package/dist/esm/wire/transferable.js +3 -3
  195. package/dist/esm/wire/transferable.js.map +1 -1
  196. package/dist/esm/wire/withdraw.d.ts.map +1 -1
  197. package/dist/esm/wire/withdraw.js +3 -3
  198. package/dist/esm/wire/withdraw.js.map +1 -1
  199. package/dist/types/client/PerpsClient.d.ts +17 -4
  200. package/dist/types/client/PerpsClient.d.ts.map +1 -1
  201. package/dist/types/decimal/arithmetic.d.ts +84 -0
  202. package/dist/types/decimal/arithmetic.d.ts.map +1 -0
  203. package/dist/types/decimal/big.d.ts +0 -2
  204. package/dist/types/decimal/big.d.ts.map +1 -1
  205. package/dist/types/decimal/compare.d.ts +39 -5
  206. package/dist/types/decimal/compare.d.ts.map +1 -1
  207. package/dist/types/decimal/convert.d.ts +62 -26
  208. package/dist/types/decimal/convert.d.ts.map +1 -1
  209. package/dist/types/decimal/decimalStringToBig.d.ts +26 -0
  210. package/dist/types/decimal/decimalStringToBig.d.ts.map +1 -0
  211. package/dist/types/decimal/format.d.ts +4 -1
  212. package/dist/types/decimal/format.d.ts.map +1 -1
  213. package/dist/types/decimal/parse.d.ts +22 -13
  214. package/dist/types/decimal/parse.d.ts.map +1 -1
  215. package/dist/types/index.d.ts +13 -11
  216. package/dist/types/index.d.ts.map +1 -1
  217. package/dist/types/math/fills.d.ts +6 -3
  218. package/dist/types/math/fills.d.ts.map +1 -1
  219. package/dist/types/math/order.d.ts +85 -65
  220. package/dist/types/math/order.d.ts.map +1 -1
  221. package/dist/types/math/position.d.ts +155 -98
  222. package/dist/types/math/position.d.ts.map +1 -1
  223. package/dist/types/services/getQuote.d.ts +2 -2
  224. package/dist/types/services/resolveSubscribeQuote.d.ts +1 -1
  225. package/dist/types/services/resolveSubscribeQuote.d.ts.map +1 -1
  226. package/dist/types/transport/request.d.ts +5 -0
  227. package/dist/types/transport/request.d.ts.map +1 -1
  228. package/dist/types/types/api.d.ts +9 -0
  229. package/dist/types/types/api.d.ts.map +1 -1
  230. package/dist/types/types/provider.d.ts +26 -13
  231. package/dist/types/types/provider.d.ts.map +1 -1
  232. package/dist/types/types/withdrawal.d.ts +32 -7
  233. package/dist/types/types/withdrawal.d.ts.map +1 -1
  234. package/dist/types/utils/createSafeFunction.d.ts +9 -0
  235. package/dist/types/utils/createSafeFunction.d.ts.map +1 -0
  236. package/dist/types/utils/timestampToIsoString.d.ts +15 -0
  237. package/dist/types/utils/timestampToIsoString.d.ts.map +1 -0
  238. package/dist/types/utils/warnOnce.d.ts +16 -0
  239. package/dist/types/utils/warnOnce.d.ts.map +1 -1
  240. package/dist/types/version.d.ts +1 -1
  241. package/dist/types/websocket/PerpsWsClient.d.ts +7 -0
  242. package/dist/types/websocket/PerpsWsClient.d.ts.map +1 -1
  243. package/dist/types/websocket/wsLog.d.ts +12 -0
  244. package/dist/types/websocket/wsLog.d.ts.map +1 -1
  245. package/dist/types/wire/orderAmounts.d.ts.map +1 -1
  246. package/dist/types/wire/refuel.d.ts +24 -14
  247. package/dist/types/wire/refuel.d.ts.map +1 -1
  248. package/dist/types/wire/withdraw.d.ts.map +1 -1
  249. package/package.json +2 -2
  250. package/src/client/PerpsClient.ts +48 -10
  251. package/src/decimal/arithmetic.ts +144 -0
  252. package/src/decimal/big.ts +0 -5
  253. package/src/decimal/compare.ts +60 -10
  254. package/src/decimal/convert.ts +123 -44
  255. package/src/decimal/decimalStringToBig.ts +47 -0
  256. package/src/decimal/format.ts +81 -72
  257. package/src/decimal/parse.ts +71 -61
  258. package/src/index.ts +73 -10
  259. package/src/math/fills.ts +32 -17
  260. package/src/math/order.ts +261 -216
  261. package/src/math/position.ts +277 -194
  262. package/src/services/getGasRecommendation.ts +1 -1
  263. package/src/services/getQuote.ts +2 -2
  264. package/src/services/resolveSubscribeQuote.ts +4 -3
  265. package/src/transport/request.ts +7 -2
  266. package/src/types/api.ts +10 -0
  267. package/src/types/provider.ts +35 -13
  268. package/src/types/withdrawal.ts +35 -8
  269. package/src/utils/createSafeFunction.ts +20 -0
  270. package/src/utils/timestampToIsoString.ts +31 -0
  271. package/src/utils/warnOnce.ts +44 -0
  272. package/src/version.ts +1 -1
  273. package/src/websocket/PerpsWsClient.ts +25 -2
  274. package/src/websocket/wsLog.ts +31 -0
  275. package/src/wire/orderAmounts.ts +3 -2
  276. package/src/wire/refuel.ts +56 -20
  277. package/src/wire/transferable.ts +3 -3
  278. package/src/wire/withdraw.ts +3 -5
  279. package/dist/cjs/decimal/requireDecimal.d.ts +0 -4
  280. package/dist/cjs/decimal/requireDecimal.d.ts.map +0 -1
  281. package/dist/cjs/decimal/requireDecimal.js +0 -12
  282. package/dist/cjs/decimal/requireDecimal.js.map +0 -1
  283. package/dist/esm/decimal/requireDecimal.d.ts +0 -5
  284. package/dist/esm/decimal/requireDecimal.d.ts.map +0 -1
  285. package/dist/esm/decimal/requireDecimal.js +0 -7
  286. package/dist/esm/decimal/requireDecimal.js.map +0 -1
  287. package/dist/types/decimal/requireDecimal.d.ts +0 -5
  288. package/dist/types/decimal/requireDecimal.d.ts.map +0 -1
  289. package/src/decimal/requireDecimal.ts +0 -8
@@ -1,44 +1,92 @@
1
- import { type DecimalString, PerpsErrorCode } from '@lifi/perps-types'
1
+ import { PerpsErrorCode } from '@lifi/perps-types'
2
2
  import Big from 'big.js'
3
3
  import { formatUnits } from 'viem'
4
4
  import { PerpsError } from '../errors/PerpsError.js'
5
- import { requireDecimal } from './requireDecimal.js'
5
+ import { createSafeFunction } from '../utils/createSafeFunction.js'
6
+ import { bigToDecimalString, decimalStringToBig } from './decimalStringToBig.js'
7
+ import { isDecimalString } from './parse.js'
6
8
 
7
9
  /**
8
- * Off-grid resolution for {@link decimalToBaseUnits}:
9
- * - `truncate` — toward zero, for sizes and collateral amounts. The wire
10
- * value never exceeds the caller's intent.
11
- * - `round` — half away from zero, for prices, which snap to the nearest
12
- * tick.
10
+ * Off-grid resolution for {@link roundDecimalString}:
11
+ * - `truncate`: toward zero, for sizes and collateral amounts. The result
12
+ * never exceeds the caller's intent.
13
+ * - `round`: half away from zero, for prices, which snap to the nearest tick.
14
+ * - `up`: away from zero, for an amount that must cover a target.
13
15
  * @public
14
16
  */
15
- export type BaseUnitsRounding = 'truncate' | 'round'
17
+ export type DecimalRounding = 'truncate' | 'round' | 'up'
18
+
19
+ const BIG_ROUNDING = {
20
+ truncate: Big.roundDown,
21
+ round: Big.roundHalfUp,
22
+ up: Big.roundUp,
23
+ } as const
16
24
 
17
25
  /**
18
- * Scale a decimal string to a scaled integer in exact decimal arithmetic — an
19
- * on-grid input maps to its exact scaled integer with no binary float
20
- * artifacts (`'0.29'` at 2 decimals is 29, never 28). Off-grid input resolves
21
- * per `rounding`; there is no silent default.
26
+ * Round a decimal string to `decimals` places with exact decimal arithmetic.
27
+ * The result has no trailing zeros: `roundDecimalString('1.50', 1, 'round')`
28
+ * is `'1.5'`.
22
29
  *
23
- * @throws {PerpsError} `ValidationError` when `value` is not a
24
- * {@link DecimalString}, `decimals` is not a non-negative integer, or the scaled
25
- * result's magnitude exceeds `Number.MAX_SAFE_INTEGER`.
30
+ * @throws {PerpsError} `ValidationError` when `value` is not a decimal string,
31
+ * or `decimals` is not a non-negative integer.
26
32
  * @public
27
33
  */
28
- export const decimalToBaseUnits = (
29
- value: DecimalString,
34
+ export function roundDecimalString(
35
+ value: string,
30
36
  decimals: number,
31
- rounding: BaseUnitsRounding
32
- ): number => {
37
+ rounding: DecimalRounding
38
+ ): string {
39
+ if (!Number.isInteger(decimals) || decimals < 0) {
40
+ throw new PerpsError(
41
+ PerpsErrorCode.ValidationError,
42
+ `Invalid decimals for rounding: ${decimals}`
43
+ )
44
+ }
45
+ return bigToDecimalString(
46
+ decimalStringToBig(value).round(decimals, BIG_ROUNDING[rounding])
47
+ )
48
+ }
49
+
50
+ /**
51
+ * {@link roundDecimalString}, or `undefined` with a warning when it throws.
52
+ *
53
+ * @public
54
+ */
55
+ export const safeRoundDecimalString = createSafeFunction(
56
+ 'roundDecimalString',
57
+ roundDecimalString
58
+ )
59
+
60
+ /**
61
+ * Scale a decimal string to an integer at `decimals` places in exact decimal
62
+ * arithmetic: `'0.29'` at 2 decimals is 29, never 28. Off-grid input resolves
63
+ * per `rounding`. No `$`, `%` or `,` is removed first.
64
+ *
65
+ * @throws {PerpsError} `ValidationError` when `value` is not a decimal string,
66
+ * `decimals` is not a non-negative integer, or the result's magnitude
67
+ * exceeds `Number.MAX_SAFE_INTEGER`.
68
+ * @public
69
+ */
70
+ export function decimalStringToScaledInteger(
71
+ value: string,
72
+ decimals: number,
73
+ rounding: DecimalRounding
74
+ ): number {
75
+ if (!isDecimalString(value)) {
76
+ throw new PerpsError(
77
+ PerpsErrorCode.ValidationError,
78
+ `'${value}' is not a decimal string.`
79
+ )
80
+ }
33
81
  if (!Number.isInteger(decimals) || decimals < 0) {
34
82
  throw new PerpsError(
35
83
  PerpsErrorCode.ValidationError,
36
84
  `Invalid decimals for integer scaling: ${decimals}`
37
85
  )
38
86
  }
39
- const scaled = requireDecimal(value, 'decimalToBaseUnits(value)')
87
+ const scaled = new Big(value)
40
88
  .times(new Big(10).pow(decimals))
41
- .round(0, rounding === 'truncate' ? Big.roundDown : Big.roundHalfUp)
89
+ .round(0, BIG_ROUNDING[rounding])
42
90
  if (scaled.abs().gt(Number.MAX_SAFE_INTEGER)) {
43
91
  throw new PerpsError(
44
92
  PerpsErrorCode.ValidationError,
@@ -48,40 +96,57 @@ export const decimalToBaseUnits = (
48
96
  return scaled.toNumber()
49
97
  }
50
98
 
51
- const BASE_UNITS_PATTERN = /^-?\d+$/
99
+ /**
100
+ * {@link decimalStringToScaledInteger}, or `undefined` with a warning when it
101
+ * throws.
102
+ *
103
+ * @public
104
+ */
105
+ export const safeDecimalStringToScaledInteger = createSafeFunction(
106
+ 'decimalStringToScaledInteger',
107
+ decimalStringToScaledInteger
108
+ )
109
+
110
+ export const BASE_UNITS_PATTERN = /^-?\d+$/
52
111
 
53
112
  /**
54
- * Convert a base-unit amount (integer string) to a decimal string.
113
+ * Convert a scaled integer string (a token amount in base units) to a decimal
114
+ * string: `scaledIntegerToDecimalString('1000000', 6)` is `'1'`.
55
115
  *
56
- * @param amount - Amount in base units (e.g. "1000000" for 1 USDC)
57
- * @param decimals - Token decimals (e.g. 6 for USDC)
58
116
  * @throws {PerpsError} `ValidationError` when `amount` is not an integer
59
117
  * string, or `decimals` is not a non-negative integer.
60
- * @example
61
- * ```ts
62
- * baseUnitsToDecimal('1000000', 6) // '1'
63
- * ```
64
118
  * @public
65
119
  */
66
- export function baseUnitsToDecimal(
67
- amount: DecimalString,
120
+ export function scaledIntegerToDecimalString(
121
+ amount: string,
68
122
  decimals: number
69
- ): DecimalString {
123
+ ): string {
70
124
  if (!Number.isInteger(decimals) || decimals < 0) {
71
125
  throw new PerpsError(
72
126
  PerpsErrorCode.ValidationError,
73
- `Invalid \`baseUnitsToDecimal(decimals)\`: ${decimals} is not a non-negative integer.`
127
+ `Invalid \`scaledIntegerToDecimalString(decimals)\`: ${decimals} is not a non-negative integer.`
74
128
  )
75
129
  }
76
130
  if (!BASE_UNITS_PATTERN.test(amount)) {
77
131
  throw new PerpsError(
78
132
  PerpsErrorCode.ValidationError,
79
- `Invalid \`baseUnitsToDecimal(amount)\`: '${amount}' is not an integer base-unit string.`
133
+ `Invalid \`scaledIntegerToDecimalString(amount)\`: '${amount}' is not an integer base-unit string.`
80
134
  )
81
135
  }
82
136
  return formatUnits(BigInt(amount), decimals)
83
137
  }
84
138
 
139
+ /**
140
+ * {@link scaledIntegerToDecimalString}, or `undefined` with a warning when it
141
+ * throws.
142
+ *
143
+ * @public
144
+ */
145
+ export const safeScaledIntegerToDecimalString = createSafeFunction(
146
+ 'scaledIntegerToDecimalString',
147
+ scaledIntegerToDecimalString
148
+ )
149
+
85
150
  /**
86
151
  * Round `value` down to `decimals` and pad the result to exactly that many
87
152
  * decimal places. The padding is the point: this seeds a fixed-decimal input,
@@ -94,27 +159,31 @@ export function baseUnitsToDecimal(
94
159
  * `'0.010000000000000000'`, which `Number#toFixed` cannot.
95
160
  *
96
161
  * @throws {PerpsError} `ValidationError` when `value` is not a
97
- * {@link DecimalString}, or `decimals` is not a non-negative integer.
162
+ * decimal string, or `decimals` is not a non-negative integer.
98
163
  * @public
99
164
  */
100
- export function truncateDecimal(
101
- value: DecimalString,
102
- decimals: number
103
- ): DecimalString {
165
+ export function truncateDecimal(value: string, decimals: number): string {
104
166
  if (!Number.isInteger(decimals) || decimals < 0) {
105
167
  throw new PerpsError(
106
168
  PerpsErrorCode.ValidationError,
107
169
  `Invalid decimals for truncation: ${decimals}`
108
170
  )
109
171
  }
110
- const truncated = requireDecimal(value, 'truncateDecimal(value)').round(
111
- decimals,
112
- Big.roundDown
113
- )
172
+ const truncated = decimalStringToBig(value).round(decimals, Big.roundDown)
114
173
  // big.js carries the sign through a round to zero; '-0.00' is not a spelling.
115
174
  return (truncated.eq(0) ? new Big(0) : truncated).toFixed(decimals)
116
175
  }
117
176
 
177
+ /**
178
+ * {@link truncateDecimal}, or `undefined` with a warning when it throws.
179
+ *
180
+ * @public
181
+ */
182
+ export const safeTruncateDecimal = createSafeFunction(
183
+ 'truncateDecimal',
184
+ truncateDecimal
185
+ )
186
+
118
187
  /**
119
188
  * Spell a `number` as a {@link DecimalString} in plain notation: `1e-7`
120
189
  * becomes `'0.0000001'`, never `'1e-7'`. The boundary helper for a venue or
@@ -125,7 +194,7 @@ export function truncateDecimal(
125
194
  * @throws {PerpsError} `ValidationError` when `value` is `NaN` or infinite.
126
195
  * @public
127
196
  */
128
- export function numberToDecimalString(value: number): DecimalString {
197
+ export function numberToDecimalString(value: number): string {
129
198
  if (!Number.isFinite(value)) {
130
199
  throw new PerpsError(
131
200
  PerpsErrorCode.ValidationError,
@@ -135,3 +204,13 @@ export function numberToDecimalString(value: number): DecimalString {
135
204
  const parsed = new Big(value)
136
205
  return (parsed.eq(0) ? new Big(0) : parsed).toFixed()
137
206
  }
207
+
208
+ /**
209
+ * {@link numberToDecimalString}, or `undefined` with a warning when it throws.
210
+ *
211
+ * @public
212
+ */
213
+ export const safeNumberToDecimalString = createSafeFunction(
214
+ 'numberToDecimalString',
215
+ numberToDecimalString
216
+ )
@@ -0,0 +1,47 @@
1
+ import { DECIMAL_PATTERN, PerpsErrorCode } from '@lifi/perps-types'
2
+ import Big from 'big.js'
3
+ import { PerpsError } from '../errors/PerpsError.js'
4
+ import { DivBig } from './big.js'
5
+
6
+ const DISPLAY_CHARACTERS = /[\s$%,]/g
7
+
8
+ /**
9
+ * Read a decimal string as a `Big`. Whitespace, `$`, `%` and `,` are removed
10
+ * first, so `'$1,234.5'` reads as `1234.5`; the rest must match
11
+ * `DECIMAL_PATTERN`, so an exponent form such as `'1e5'` is rejected.
12
+ *
13
+ * @throws {PerpsError} `ValidationError` when `value` is not a string or the
14
+ * cleaned string is not a decimal string.
15
+ * @internal
16
+ */
17
+ export function decimalStringToBig(value: string): Big {
18
+ const cleaned =
19
+ typeof value === 'string' ? value.replace(DISPLAY_CHARACTERS, '') : ''
20
+ if (!DECIMAL_PATTERN.test(cleaned)) {
21
+ throw new PerpsError(
22
+ PerpsErrorCode.ValidationError,
23
+ `'${String(value)}' is not a decimal string.`
24
+ )
25
+ }
26
+ return new Big(cleaned)
27
+ }
28
+
29
+ /**
30
+ * Spell a `Big` as a canonical decimal string, with no exponent and no `-0`.
31
+ *
32
+ * @internal
33
+ */
34
+ export function bigToDecimalString(value: Big): string {
35
+ return value.eq(0) ? '0' : value.toFixed()
36
+ }
37
+
38
+ /**
39
+ * {@link decimalStringToBig} as a `DivBig`, so a later `div` keeps 40 decimal
40
+ * places.
41
+ *
42
+ * @throws {PerpsError} `ValidationError` as {@link decimalStringToBig}.
43
+ * @internal
44
+ */
45
+ export function decimalStringToDivBig(value: string): Big {
46
+ return new DivBig(decimalStringToBig(value))
47
+ }
@@ -1,4 +1,6 @@
1
1
  import type { DecimalString } from '@lifi/perps-types'
2
+ import Big from 'big.js'
3
+ import { decimalStringToBig } from './decimalStringToBig.js'
2
4
 
3
5
  /**
4
6
  * Options shared by the human-facing display formatters.
@@ -8,7 +10,10 @@ import type { DecimalString } from '@lifi/perps-types'
8
10
  export interface FormatOptions {
9
11
  /** Fixed number of decimal places. Defaults are per-formatter. */
10
12
  decimals?: number
11
- /** Rendered when the value is null/undefined/NaN/non-finite. Defaults to `'—'`. */
13
+ /**
14
+ * Rendered when the value is null, undefined, blank or a non-finite number.
15
+ * A string that is not a decimal is shown unchanged. Defaults to `'—'`.
16
+ */
12
17
  placeholder?: string
13
18
  /** BCP 47 locale controlling digit grouping and separators (e.g. `'en-US'`). */
14
19
  locale?: string
@@ -33,76 +38,62 @@ const DEFAULT_PLACEHOLDER = '—'
33
38
  type FormatInput = number | DecimalString | string | null | undefined
34
39
 
35
40
  /**
36
- * Extra decimal places used to normalise IEEE-754 representation noise before
37
- * truncating. `toFixed` at this higher precision cleans values like
38
- * `0.28999999…` back to `0.29`; truncation then chops the guard digits.
39
- */
40
- const FLOAT_GUARD_DIGITS = 9
41
-
42
- /**
43
- * Tolerantly coerce a display input to a finite number.
44
- *
45
- * Strips `$`, `,`, and surrounding whitespace from strings. Returns `null`
46
- * when the value is null/undefined/blank or does not resolve to a finite
47
- * number, so callers can render a placeholder instead of `$0.00` or `$NaN`.
41
+ * Read a display input as a `Big`: `$`, `%`, `,` and whitespace are removed
42
+ * from a string first. `null` means the formatter renders the placeholder;
43
+ * a string it cannot read is returned unchanged for display.
48
44
  */
49
- function toFiniteNumber(value: FormatInput): number | null {
50
- let n: number
51
- if (typeof value === 'string') {
52
- const cleaned = value.replace(/[$,\s]/g, '')
53
- if (!cleaned) {
54
- return null
55
- }
56
- n = Number(cleaned)
57
- } else if (typeof value === 'number') {
58
- n = value
59
- } else {
45
+ function toBig(value: FormatInput): Big | null | string {
46
+ if (typeof value === 'number') {
47
+ return Number.isFinite(value) ? new Big(value) : null
48
+ }
49
+ if (typeof value !== 'string' || value.trim() === '') {
60
50
  return null
61
51
  }
62
- return Number.isFinite(n) ? n : null
52
+ try {
53
+ return decimalStringToBig(value)
54
+ } catch {
55
+ return value
56
+ }
63
57
  }
64
58
 
65
- /**
66
- * Truncate a non-negative value toward zero to `decimals` places using its
67
- * decimal string, never float scaling. Representation noise is first
68
- * normalised by `toFixed` at {@link FLOAT_GUARD_DIGITS} extra precision, then
69
- * the guard digits are chopped: `99.999` at 2dp → `99.99`, `0.29` → `0.29`.
70
- */
71
- function truncateAbs(abs: number, decimals: number): number {
72
- // `toFixed` emits exponent notation at >= 1e21; every double that large is an integer.
73
- if (Number.isInteger(abs)) {
74
- return abs
75
- }
76
- const s = abs.toFixed(Math.min(decimals + FLOAT_GUARD_DIGITS, 100))
77
- const dot = s.indexOf('.')
78
- const cut = decimals === 0 ? dot : dot + 1 + decimals
79
- return Number(s.slice(0, cut))
59
+ function decimalSeparator(locale: string | undefined): string {
60
+ return (
61
+ new Intl.NumberFormat(locale)
62
+ .formatToParts(1.5)
63
+ .find((part) => part.type === 'decimal')?.value ?? '.'
64
+ )
80
65
  }
81
66
 
82
67
  /**
83
- * Reduce `n` to `decimals` places then split into a sign and the
68
+ * Reduce `value` to `decimals` places then split into a sign and the
84
69
  * locale-formatted absolute body. The sign is derived from the reduced value
85
70
  * so magnitudes that collapse to zero render without a spurious `-`/`+`.
86
71
  * `signed` emits `+` for positives; `rounding` selects half-up or truncation.
87
72
  */
88
73
  function signAndBody(
89
- n: number,
74
+ value: Big,
90
75
  decimals: number,
91
76
  locale: string | undefined,
92
77
  grouping: boolean,
93
78
  rounding: RoundingMode,
94
79
  signed: boolean
95
80
  ): { sign: '+' | '-' | ''; body: string } {
96
- const reduced =
97
- rounding === 'floor'
98
- ? (n < 0 ? -1 : 1) * truncateAbs(Math.abs(n), decimals)
99
- : Number(n.toFixed(decimals))
100
- const sign = reduced < 0 ? '-' : signed && reduced > 0 ? '+' : ''
101
- const body = Math.abs(reduced).toLocaleString(locale, {
102
- minimumFractionDigits: decimals,
103
- maximumFractionDigits: decimals,
104
- useGrouping: grouping,
105
- })
81
+ const reduced = value.round(
82
+ decimals,
83
+ rounding === 'floor' ? Big.roundDown : Big.roundHalfUp
84
+ )
85
+ const sign = reduced.lt(0) ? '-' : signed && reduced.gt(0) ? '+' : ''
86
+ const [integerPart = '0', fractionPart] = reduced
87
+ .abs()
88
+ .toFixed(decimals)
89
+ .split('.')
90
+ const integerBody = grouping
91
+ ? BigInt(integerPart).toLocaleString(locale)
92
+ : integerPart
93
+ const body =
94
+ fractionPart === undefined
95
+ ? integerBody
96
+ : `${integerBody}${decimalSeparator(locale)}${fractionPart}`
106
97
  return { sign, body }
107
98
  }
108
99
 
@@ -110,14 +101,14 @@ function signAndBody(
110
101
  * Auto-detect a sensible number of decimal places from a value's magnitude:
111
102
  * `>=1` → 2, `>=0.1` → 4, `>=0.01` → 5, otherwise 6.
112
103
  */
113
- function autoDecimals(abs: number): number {
114
- if (abs >= 1) {
104
+ function autoDecimals(abs: Big): number {
105
+ if (abs.gte(1)) {
115
106
  return 2
116
107
  }
117
- if (abs >= 0.1) {
108
+ if (abs.gte('0.1')) {
118
109
  return 4
119
110
  }
120
- if (abs >= 0.01) {
111
+ if (abs.gte('0.01')) {
121
112
  return 5
122
113
  }
123
114
  return 6
@@ -145,10 +136,13 @@ export function formatNumber(
145
136
  locale,
146
137
  rounding = 'halfUp',
147
138
  } = options
148
- const n = toFiniteNumber(value)
139
+ const n = toBig(value)
149
140
  if (n === null) {
150
141
  return placeholder
151
142
  }
143
+ if (typeof n === 'string') {
144
+ return n
145
+ }
152
146
  const { sign, body } = signAndBody(n, decimals, locale, true, rounding, false)
153
147
  return `${sign}${body}`
154
148
  }
@@ -171,10 +165,13 @@ export function formatUsd(
171
165
  locale,
172
166
  rounding = 'halfUp',
173
167
  } = options
174
- const n = toFiniteNumber(value)
168
+ const n = toBig(value)
175
169
  if (n === null) {
176
170
  return placeholder
177
171
  }
172
+ if (typeof n === 'string') {
173
+ return n
174
+ }
178
175
  const { sign, body } = signAndBody(n, decimals, locale, true, rounding, false)
179
176
  return `${sign}$${body}`
180
177
  }
@@ -198,10 +195,13 @@ export function formatSignedUsd(
198
195
  locale,
199
196
  rounding = 'halfUp',
200
197
  } = options
201
- const n = toFiniteNumber(value)
198
+ const n = toBig(value)
202
199
  if (n === null) {
203
200
  return placeholder
204
201
  }
202
+ if (typeof n === 'string') {
203
+ return n
204
+ }
205
205
  const { sign, body } = signAndBody(n, decimals, locale, true, rounding, true)
206
206
  return `${sign}$${body}`
207
207
  }
@@ -225,10 +225,13 @@ export function formatSignedPercent(
225
225
  locale,
226
226
  rounding = 'halfUp',
227
227
  } = options
228
- const n = toFiniteNumber(value)
228
+ const n = toBig(value)
229
229
  if (n === null) {
230
230
  return placeholder
231
231
  }
232
+ if (typeof n === 'string') {
233
+ return n
234
+ }
232
235
  const { sign, body } = signAndBody(n, decimals, locale, false, rounding, true)
233
236
  return `${sign}${body}%`
234
237
  }
@@ -252,17 +255,20 @@ export function formatPrice(
252
255
  locale,
253
256
  rounding = 'halfUp',
254
257
  } = options
255
- const n = toFiniteNumber(value)
258
+ const n = toBig(value)
256
259
  if (n === null) {
257
260
  return placeholder
258
261
  }
259
- const abs = Math.abs(n)
262
+ if (typeof n === 'string') {
263
+ return n
264
+ }
265
+ const abs = n.abs()
260
266
  const decimals = options.decimals ?? autoDecimals(abs)
261
267
  const { sign, body } = signAndBody(
262
268
  n,
263
269
  decimals,
264
270
  locale,
265
- abs >= 1000,
271
+ abs.gte(1000),
266
272
  rounding,
267
273
  false
268
274
  )
@@ -283,20 +289,23 @@ export function formatCompactUsd(
283
289
  options: FormatOptions = {}
284
290
  ): string {
285
291
  const { decimals = 2, placeholder = DEFAULT_PLACEHOLDER } = options
286
- const n = toFiniteNumber(value)
292
+ const n = toBig(value)
287
293
  if (n === null) {
288
294
  return placeholder
289
295
  }
290
- const sign = n < 0 ? '-' : ''
291
- const abs = Math.abs(n)
292
- if (abs >= 1_000_000_000) {
293
- return `${sign}$${(abs / 1_000_000_000).toFixed(decimals)}B`
296
+ if (typeof n === 'string') {
297
+ return n
298
+ }
299
+ const sign = n.lt(0) ? '-' : ''
300
+ const abs = n.abs()
301
+ if (abs.gte(1_000_000_000)) {
302
+ return `${sign}$${abs.div(1_000_000_000).toFixed(decimals)}B`
294
303
  }
295
- if (abs >= 1_000_000) {
296
- return `${sign}$${(abs / 1_000_000).toFixed(decimals)}M`
304
+ if (abs.gte(1_000_000)) {
305
+ return `${sign}$${abs.div(1_000_000).toFixed(decimals)}M`
297
306
  }
298
- if (abs >= 1_000) {
299
- return `${sign}$${(abs / 1_000).toFixed(decimals)}K`
307
+ if (abs.gte(1_000)) {
308
+ return `${sign}$${abs.div(1_000).toFixed(decimals)}K`
300
309
  }
301
310
  return `${sign}$${abs.toFixed(decimals)}`
302
311
  }