sushi 6.6.11 → 6.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/_cjs/evm/abi/selfPermitAbi/index.js +0 -2
  2. package/_cjs/evm/abi/selfPermitAbi/index.js.map +1 -1
  3. package/_cjs/evm/validate/zod.js +4 -3
  4. package/_cjs/evm/validate/zod.js.map +1 -1
  5. package/_cjs/generic/currency/price.js +23 -2
  6. package/_cjs/generic/currency/price.js.map +1 -1
  7. package/_cjs/generic/format/number.js +5 -1
  8. package/_cjs/generic/format/number.js.map +1 -1
  9. package/_cjs/generic/math/fraction.js +70 -3
  10. package/_cjs/generic/math/fraction.js.map +1 -1
  11. package/_cjs/generic/utils/id.js.map +1 -1
  12. package/_cjs/generic/utils/native-address.js.map +1 -1
  13. package/_cjs/generic/validate/zod.js +6 -0
  14. package/_cjs/generic/validate/zod.js.map +1 -1
  15. package/_cjs/mvm/index.js +1 -0
  16. package/_cjs/mvm/index.js.map +1 -1
  17. package/_cjs/mvm/validate/index.js +18 -0
  18. package/_cjs/mvm/validate/index.js.map +1 -0
  19. package/_cjs/mvm/validate/zod.js +48 -0
  20. package/_cjs/mvm/validate/zod.js.map +1 -0
  21. package/_cjs/stellar/config/simple-constants.js +1 -1
  22. package/_cjs/stellar/config/simple-constants.js.map +1 -1
  23. package/_cjs/stellar/index.js +1 -0
  24. package/_cjs/stellar/index.js.map +1 -1
  25. package/_cjs/stellar/validate/index.js +18 -0
  26. package/_cjs/stellar/validate/index.js.map +1 -0
  27. package/_cjs/stellar/validate/zod.js +60 -0
  28. package/_cjs/stellar/validate/zod.js.map +1 -0
  29. package/_cjs/svm/currency/token.js +1 -1
  30. package/_cjs/svm/currency/token.js.map +1 -1
  31. package/_cjs/svm/index.js +1 -0
  32. package/_cjs/svm/index.js.map +1 -1
  33. package/_cjs/svm/validate/index.js +18 -0
  34. package/_cjs/svm/validate/index.js.map +1 -0
  35. package/_cjs/svm/validate/zod.js +48 -0
  36. package/_cjs/svm/validate/zod.js.map +1 -0
  37. package/_cjs/version.js +1 -1
  38. package/_cjs/version.js.map +1 -1
  39. package/_esm/evm/abi/selfPermitAbi/index.js +0 -2
  40. package/_esm/evm/abi/selfPermitAbi/index.js.map +1 -1
  41. package/_esm/evm/validate/zod.js +4 -3
  42. package/_esm/evm/validate/zod.js.map +1 -1
  43. package/_esm/generic/currency/price.js +29 -2
  44. package/_esm/generic/currency/price.js.map +1 -1
  45. package/_esm/generic/format/number.js +5 -1
  46. package/_esm/generic/format/number.js.map +1 -1
  47. package/_esm/generic/math/fraction.js +70 -3
  48. package/_esm/generic/math/fraction.js.map +1 -1
  49. package/_esm/generic/utils/id.js.map +1 -1
  50. package/_esm/generic/utils/native-address.js.map +1 -1
  51. package/_esm/generic/validate/zod.js +6 -0
  52. package/_esm/generic/validate/zod.js.map +1 -1
  53. package/_esm/mvm/index.js +1 -0
  54. package/_esm/mvm/index.js.map +1 -1
  55. package/_esm/mvm/validate/index.js +2 -0
  56. package/_esm/mvm/validate/index.js.map +1 -0
  57. package/_esm/mvm/validate/zod.js +11 -0
  58. package/_esm/mvm/validate/zod.js.map +1 -0
  59. package/_esm/stellar/config/simple-constants.js +1 -1
  60. package/_esm/stellar/config/simple-constants.js.map +1 -1
  61. package/_esm/stellar/index.js +1 -0
  62. package/_esm/stellar/index.js.map +1 -1
  63. package/_esm/stellar/validate/index.js +2 -0
  64. package/_esm/stellar/validate/index.js.map +1 -0
  65. package/_esm/stellar/validate/zod.js +21 -0
  66. package/_esm/stellar/validate/zod.js.map +1 -0
  67. package/_esm/svm/currency/token.js +1 -1
  68. package/_esm/svm/currency/token.js.map +1 -1
  69. package/_esm/svm/index.js +1 -0
  70. package/_esm/svm/index.js.map +1 -1
  71. package/_esm/svm/validate/index.js +2 -0
  72. package/_esm/svm/validate/index.js.map +1 -0
  73. package/_esm/svm/validate/zod.js +11 -0
  74. package/_esm/svm/validate/zod.js.map +1 -0
  75. package/_esm/version.js +1 -1
  76. package/_esm/version.js.map +1 -1
  77. package/_types/evm/abi/selfPermitAbi/index.d.ts +0 -2
  78. package/_types/evm/abi/selfPermitAbi/index.d.ts.map +1 -1
  79. package/_types/evm/chain/chains.d.ts +10 -10
  80. package/_types/evm/validate/zod.d.ts.map +1 -1
  81. package/_types/generic/currency/price.d.ts +9 -2
  82. package/_types/generic/currency/price.d.ts.map +1 -1
  83. package/_types/generic/currency/serialized-currency.d.ts +1 -1
  84. package/_types/generic/math/fraction.d.ts +3 -1
  85. package/_types/generic/math/fraction.d.ts.map +1 -1
  86. package/_types/generic/types/for-chain.d.ts +2 -1
  87. package/_types/generic/types/for-chain.d.ts.map +1 -1
  88. package/_types/generic/utils/id.d.ts +4 -1
  89. package/_types/generic/utils/id.d.ts.map +1 -1
  90. package/_types/generic/utils/native-address.d.ts +2 -2
  91. package/_types/generic/utils/native-address.d.ts.map +1 -1
  92. package/_types/generic/validate/zod.d.ts +11 -0
  93. package/_types/generic/validate/zod.d.ts.map +1 -1
  94. package/_types/mvm/index.d.ts +1 -0
  95. package/_types/mvm/index.d.ts.map +1 -1
  96. package/_types/mvm/validate/index.d.ts +2 -0
  97. package/_types/mvm/validate/index.d.ts.map +1 -0
  98. package/_types/mvm/validate/zod.d.ts +6 -0
  99. package/_types/mvm/validate/zod.d.ts.map +1 -0
  100. package/_types/stellar/config/simple-constants.d.ts +1 -1
  101. package/_types/stellar/config/simple-constants.d.ts.map +1 -1
  102. package/_types/stellar/index.d.ts +1 -0
  103. package/_types/stellar/index.d.ts.map +1 -1
  104. package/_types/stellar/validate/index.d.ts +2 -0
  105. package/_types/stellar/validate/index.d.ts.map +1 -0
  106. package/_types/stellar/validate/zod.d.ts +10 -0
  107. package/_types/stellar/validate/zod.d.ts.map +1 -0
  108. package/_types/svm/config/tokens/tokens/USDC.d.ts +1 -1
  109. package/_types/svm/config/tokens/tokens/USDT.d.ts +1 -1
  110. package/_types/svm/config/tokens/tokens/WBTC.d.ts +1 -1
  111. package/_types/svm/config/tokens/tokens/WETH.d.ts +1 -1
  112. package/_types/svm/config/tokens/tokens/WSOL.d.ts +1 -1
  113. package/_types/svm/currency/currency.d.ts +2 -2
  114. package/_types/svm/currency/currency.d.ts.map +1 -1
  115. package/_types/svm/currency/token.d.ts +1 -1
  116. package/_types/svm/currency/token.d.ts.map +1 -1
  117. package/_types/svm/index.d.ts +1 -0
  118. package/_types/svm/index.d.ts.map +1 -1
  119. package/_types/svm/validate/index.d.ts +2 -0
  120. package/_types/svm/validate/index.d.ts.map +1 -0
  121. package/_types/svm/validate/zod.d.ts +7 -0
  122. package/_types/svm/validate/zod.d.ts.map +1 -0
  123. package/_types/version.d.ts +1 -1
  124. package/_types/version.d.ts.map +1 -1
  125. package/evm/abi/selfPermitAbi/index.ts +0 -2
  126. package/evm/pool/sushiswap-v3/entities/TickDataProvider.test.ts +4 -4
  127. package/evm/validate/zod.ts +4 -3
  128. package/generic/currency/price.test-d.ts +59 -0
  129. package/generic/currency/price.test.ts +132 -0
  130. package/generic/currency/price.ts +41 -3
  131. package/generic/format/number.test.ts +101 -1
  132. package/generic/format/number.ts +13 -6
  133. package/generic/math/fraction.test.ts +97 -0
  134. package/generic/math/fraction.ts +106 -3
  135. package/generic/types/for-chain.ts +16 -1
  136. package/generic/utils/id.test-d.ts +35 -0
  137. package/generic/utils/id.ts +8 -2
  138. package/generic/utils/native-address.test-d.ts +25 -0
  139. package/generic/utils/native-address.ts +6 -6
  140. package/generic/validate/zod.test.ts +44 -0
  141. package/generic/validate/zod.ts +6 -0
  142. package/mvm/index.ts +1 -0
  143. package/mvm/validate/index.ts +1 -0
  144. package/mvm/validate/zod.ts +13 -0
  145. package/package.json +3 -4
  146. package/stellar/config/simple-constants.ts +2 -5
  147. package/stellar/index.ts +1 -0
  148. package/stellar/validate/index.ts +1 -0
  149. package/stellar/validate/zod.ts +38 -0
  150. package/svm/currency/token.ts +1 -1
  151. package/svm/index.ts +1 -0
  152. package/svm/validate/index.ts +1 -0
  153. package/svm/validate/zod.ts +13 -0
  154. package/_cjs/evm/abi/selfPermitAbi/selfPermitAbi_selfPermitAllowedIfNecessary.js +0 -20
  155. package/_cjs/evm/abi/selfPermitAbi/selfPermitAbi_selfPermitAllowedIfNecessary.js.map +0 -1
  156. package/_cjs/evm/abi/selfPermitAbi/selfPermitAbi_selfPermitIfNecessary.js +0 -20
  157. package/_cjs/evm/abi/selfPermitAbi/selfPermitAbi_selfPermitIfNecessary.js.map +0 -1
  158. package/_cjs/evm/abi/splitter.js +0 -68
  159. package/_cjs/evm/abi/splitter.js.map +0 -1
  160. package/_cjs/evm/api/config.js +0 -20
  161. package/_cjs/evm/api/config.js.map +0 -1
  162. package/_cjs/evm/config/drpc.js +0 -83
  163. package/_cjs/evm/config/drpc.js.map +0 -1
  164. package/_cjs/evm/config/tokens/tokens/SGUSDC.js +0 -18
  165. package/_cjs/evm/config/tokens/tokens/SGUSDC.js.map +0 -1
  166. package/_esm/evm/abi/selfPermitAbi/selfPermitAbi_selfPermitAllowedIfNecessary.js +0 -17
  167. package/_esm/evm/abi/selfPermitAbi/selfPermitAbi_selfPermitAllowedIfNecessary.js.map +0 -1
  168. package/_esm/evm/abi/selfPermitAbi/selfPermitAbi_selfPermitIfNecessary.js +0 -17
  169. package/_esm/evm/abi/selfPermitAbi/selfPermitAbi_selfPermitIfNecessary.js.map +0 -1
  170. package/_esm/evm/abi/splitter.js +0 -34
  171. package/_esm/evm/abi/splitter.js.map +0 -1
  172. package/_esm/evm/api/config.js +0 -17
  173. package/_esm/evm/api/config.js.map +0 -1
  174. package/_esm/evm/config/drpc.js +0 -78
  175. package/_esm/evm/config/drpc.js.map +0 -1
  176. package/_esm/evm/config/tokens/tokens/SGUSDC.js +0 -15
  177. package/_esm/evm/config/tokens/tokens/SGUSDC.js.map +0 -1
  178. package/_types/evm/abi/selfPermitAbi/selfPermitAbi_selfPermitAllowedIfNecessary.d.ts +0 -32
  179. package/_types/evm/abi/selfPermitAbi/selfPermitAbi_selfPermitAllowedIfNecessary.d.ts.map +0 -1
  180. package/_types/evm/abi/selfPermitAbi/selfPermitAbi_selfPermitIfNecessary.d.ts +0 -32
  181. package/_types/evm/abi/selfPermitAbi/selfPermitAbi_selfPermitIfNecessary.d.ts.map +0 -1
  182. package/_types/evm/abi/splitter.d.ts +0 -2
  183. package/_types/evm/abi/splitter.d.ts.map +0 -1
  184. package/_types/evm/api/config.d.ts +0 -11
  185. package/_types/evm/api/config.d.ts.map +0 -1
  186. package/_types/evm/config/drpc.d.ts +0 -9
  187. package/_types/evm/config/drpc.d.ts.map +0 -1
  188. package/_types/evm/config/tokens/tokens/SGUSDC.d.ts +0 -9
  189. package/_types/evm/config/tokens/tokens/SGUSDC.d.ts.map +0 -1
  190. package/evm/abi/selfPermitAbi/selfPermitAbi_selfPermitAllowedIfNecessary.ts +0 -16
  191. package/evm/abi/selfPermitAbi/selfPermitAbi_selfPermitIfNecessary.ts +0 -16
  192. package/evm/abi/splitter.ts +0 -53
  193. package/evm/api/config.ts +0 -23
  194. package/evm/config/drpc.ts +0 -92
  195. package/evm/config/tokens/tokens/SGUSDC.ts +0 -19
@@ -0,0 +1,132 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { Price } from './price.js'
3
+ import { Token } from './token.js'
4
+
5
+ class TestToken extends Token {
6
+ public override toJSON() {
7
+ return {} as any
8
+ }
9
+
10
+ public override wrap() {
11
+ return this
12
+ }
13
+ }
14
+
15
+ const wbtc = new TestToken({
16
+ decimals: 8,
17
+ symbol: 'WBTC',
18
+ name: 'Wrapped Bitcoin',
19
+ chainId: 1,
20
+ address: '0x1234567890abcdef1234567890abcdef12345678',
21
+ })
22
+
23
+ const usdc = new TestToken({
24
+ decimals: 6,
25
+ symbol: 'USDC',
26
+ name: 'USD Coin',
27
+ chainId: 1,
28
+ address: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
29
+ })
30
+
31
+ describe('Price', () => {
32
+ describe('fromHuman', () => {
33
+ it('creates a price for currencies with different decimals', () => {
34
+ const price = Price.fromHuman(wbtc, usdc, '1.25')
35
+
36
+ expect(price.base).toBe(wbtc)
37
+ expect(price.quote).toBe(usdc)
38
+ expect(price.toNumber()).toBe(1.25)
39
+ expect(price.numerator).toBe(125000000n)
40
+ expect(price.denominator).toBe(10000000000n)
41
+ expect(price.invert().toNumber()).toBe(0.8)
42
+ expect(price.invert().toString({ maxFixed: 18 })).toBe('0.8')
43
+ })
44
+
45
+ it('accepts a decimal without a leading zero', () => {
46
+ expect(Price.fromHuman(wbtc, usdc, '.5').toNumber()).toBe(0.5)
47
+ })
48
+
49
+ it('accepts zero', () => {
50
+ const price = Price.fromHuman(wbtc, usdc, '0')
51
+
52
+ expect(price.toNumber()).toBe(0)
53
+ expect(price.numerator).toBe(0n)
54
+ })
55
+
56
+ it('preserves more than 18 fractional digits', () => {
57
+ const price = Price.fromHuman(wbtc, usdc, '0.0000000000000000001')
58
+
59
+ expect(price.numerator).toBe(1000000n)
60
+ expect(price.denominator).toBe(1000000000000000000000000000n)
61
+ })
62
+
63
+ it.each([
64
+ '',
65
+ '-1',
66
+ '1e6',
67
+ 'Infinity',
68
+ ])('rejects invalid input %j', (value) => {
69
+ expect(() => Price.fromHuman(wbtc, usdc, value)).toThrow(
70
+ `Invalid price: ${value}`,
71
+ )
72
+ })
73
+ })
74
+
75
+ describe('tryFromHuman', () => {
76
+ it('returns a price for valid input', () => {
77
+ expect(Price.tryFromHuman(wbtc, usdc, '1.25')?.toNumber()).toBe(1.25)
78
+ })
79
+
80
+ it.each([
81
+ '',
82
+ '-1',
83
+ '1e6',
84
+ 'Infinity',
85
+ ])('returns undefined for invalid input %j', (value) => {
86
+ expect(Price.tryFromHuman(wbtc, usdc, value)).toBeUndefined()
87
+ })
88
+ })
89
+
90
+ describe('string formatting', () => {
91
+ it('formats very small prices without scientific notation', () => {
92
+ const price = Price.fromHuman(wbtc, usdc, '0.000000000001')
93
+
94
+ expect(price.toString({ maxFixed: 18 })).toBe('0.000000000001')
95
+ expect(price.toString({ significant: 6 })).toBe('0.000000000001')
96
+ })
97
+
98
+ it('truncates repeating prices to maxFixed decimal places', () => {
99
+ const price = new Price({
100
+ base: wbtc,
101
+ quote: wbtc,
102
+ numerator: 1n,
103
+ denominator: 3n,
104
+ })
105
+
106
+ expect(price.toString({ maxFixed: 18 })).toBe('0.333333333333333333')
107
+ })
108
+
109
+ it('preserves prices beyond Number.MAX_SAFE_INTEGER', () => {
110
+ const price = new Price({
111
+ base: wbtc,
112
+ quote: wbtc,
113
+ numerator: 9007199254740993n,
114
+ })
115
+
116
+ expect(price.toString({ maxFixed: 18 })).toBe('9007199254740993')
117
+ })
118
+
119
+ it('supports fixed, maxFixed, and significant formatting', () => {
120
+ const price = new Price({
121
+ base: wbtc,
122
+ quote: wbtc,
123
+ numerator: 1n,
124
+ denominator: 8n,
125
+ })
126
+
127
+ expect(price.toString({ fixed: 3 })).toBe('0.125')
128
+ expect(price.toString({ maxFixed: 2 })).toBe('0.12')
129
+ expect(price.toString({ significant: 2 })).toBe('0.13')
130
+ })
131
+ })
132
+ })
@@ -1,4 +1,3 @@
1
- import { numberToFixed } from '../format/number.js'
2
1
  import { Fraction } from '../math/fraction.js'
3
2
  import type { BigintIsh } from '../types/bigintish.js'
4
3
  import { Amount } from './amount.js'
@@ -11,6 +10,45 @@ export class Price<
11
10
  public readonly base: TBase
12
11
  public readonly quote: TQuote
13
12
 
13
+ /**
14
+ * Creates a Price from a human-readable quote-per-base value, e.g. "1.5".
15
+ */
16
+ public static fromHuman<TBase extends Currency, TQuote extends Currency>(
17
+ base: TBase,
18
+ quote: TQuote,
19
+ value: string,
20
+ ): Price<TBase, TQuote> {
21
+ if (!value.match(/^\d*\.?\d+$/)) {
22
+ throw new Error(`Invalid price: ${value}`)
23
+ }
24
+
25
+ const [whole = '', fraction = ''] = value.split('.')
26
+ const decimalScale = 10n ** BigInt(fraction.length)
27
+ const valueWithoutDecimals = BigInt(`${whole}${fraction}`)
28
+
29
+ return new Price({
30
+ base,
31
+ quote,
32
+ numerator: valueWithoutDecimals * 10n ** BigInt(quote.decimals),
33
+ denominator: decimalScale * 10n ** BigInt(base.decimals),
34
+ })
35
+ }
36
+
37
+ /**
38
+ * Tries to create a Price from a human-readable quote-per-base value.
39
+ */
40
+ public static tryFromHuman<TBase extends Currency, TQuote extends Currency>(
41
+ base: TBase,
42
+ quote: TQuote,
43
+ value: string,
44
+ ): Price<TBase, TQuote> | undefined {
45
+ try {
46
+ return Price.fromHuman(base, quote, value)
47
+ } catch {
48
+ return undefined
49
+ }
50
+ }
51
+
14
52
  constructor(
15
53
  args:
16
54
  | {
@@ -65,8 +103,8 @@ export class Price<
65
103
  return super.mul(this.scalar).toNumber()
66
104
  }
67
105
 
68
- public override toString(args: Parameters<typeof numberToFixed>[1]): string {
69
- return numberToFixed(this.toNumber(), args)
106
+ public override toString(args: Parameters<Fraction['toString']>[0]): string {
107
+ return super.mul(this.scalar).toString(args)
70
108
  }
71
109
 
72
110
  public override toSignificant(significantDigits = 5): string {
@@ -127,7 +127,7 @@ describe('numberToFixed', () => {
127
127
  })
128
128
 
129
129
  describe('edge cases', () => {
130
- it('should handle empty and z8ero values', () => {
130
+ it('should handle empty and zero values', () => {
131
131
  expect(numberToFixed(0, { maxFixed: 2 })).toBe('0')
132
132
  expect(numberToFixed('0', { maxFixed: 2 })).toBe('0')
133
133
  expect(numberToFixed('0.00', { maxFixed: 2 })).toBe('0')
@@ -143,4 +143,104 @@ describe('numberToFixed', () => {
143
143
  expect(numberToFixed('1.23e+2', { maxFixed: 2 })).toBe('123')
144
144
  })
145
145
  })
146
+
147
+ describe('string rounding edge cases', () => {
148
+ it('should round fixed-decimal string inputs that overflow into a new integer digit', () => {
149
+ expect(numberToFixed('9.995', { fixed: 2 })).toBe('10')
150
+ expect(numberToFixed('-9.995', { fixed: 2 })).toBe('-10')
151
+ })
152
+
153
+ it('should round long leading-zero decimals when using significant digits', () => {
154
+ expect(numberToFixed('0.00009999', { significant: 2 })).toBe('0.0001')
155
+ expect(numberToFixed('-0.00009999', { significant: 2 })).toBe('-0.0001')
156
+ })
157
+
158
+ it('should round large negative strings when all significant digits are 9s', () => {
159
+ expect(numberToFixed('-9999.9', { significant: 1 })).toBe('-10000')
160
+ })
161
+ })
162
+
163
+ describe('fixed rounding cascades', () => {
164
+ it('should cascade rounding across multiple integer digits for strings', () => {
165
+ expect(numberToFixed('199.995', { fixed: 2 })).toBe('200')
166
+ expect(numberToFixed('-199.995', { fixed: 2 })).toBe('-200')
167
+ })
168
+
169
+ it('should keep trailing zeros for numeric inputs when the same rounding occurs', () => {
170
+ expect(numberToFixed(199.995, { fixed: 2 })).toBe('200.00')
171
+ expect(numberToFixed(-199.995, { fixed: 2 })).toBe('-200.00')
172
+ })
173
+
174
+ it('should handle +/-0.005 thresholds symmetrically', () => {
175
+ expect(numberToFixed('0.005', { fixed: 2 })).toBe('0.01')
176
+ expect(numberToFixed('-0.005', { fixed: 2 })).toBe('-0.01')
177
+ expect(numberToFixed(0.005, { fixed: 2 })).toBe('0.01')
178
+ expect(numberToFixed(-0.005, { fixed: 2 })).toBe('-0.01')
179
+ })
180
+
181
+ it('should preserve decimal accuracy for strings at IEEE rounding pain points', () => {
182
+ expect(numberToFixed('2.675', { fixed: 2 })).toBe('2.68')
183
+ expect(numberToFixed(2.675, { fixed: 2 })).toBe('2.67')
184
+ })
185
+ })
186
+
187
+ describe('maxFixed nuances', () => {
188
+ it('should truncate (not round) string inputs for maxFixed', () => {
189
+ expect(numberToFixed('2.999', { maxFixed: 2 })).toBe('2.99')
190
+ expect(numberToFixed('-2.999', { maxFixed: 2 })).toBe('-2.99')
191
+ })
192
+
193
+ it('should round numeric inputs for maxFixed after trimming trailing zeros', () => {
194
+ expect(numberToFixed(2.999, { maxFixed: 2 })).toBe('3')
195
+ expect(numberToFixed(-2.999, { maxFixed: 2 })).toBe('-3')
196
+ })
197
+
198
+ it('should drop redundant zeros for very small fractions supplied as strings', () => {
199
+ expect(numberToFixed('0.0001000', { maxFixed: 6 })).toBe('0.0001')
200
+ expect(numberToFixed('-0.0001000', { maxFixed: 6 })).toBe('-0.0001')
201
+ })
202
+
203
+ it('should distinguish between truncation and rounding when maxFixed is zero', () => {
204
+ expect(numberToFixed('12.9', { maxFixed: 0 })).toBe('12')
205
+ expect(numberToFixed('-12.9', { maxFixed: 0 })).toBe('-12')
206
+ expect(numberToFixed(12.9, { maxFixed: 0 })).toBe('13')
207
+ expect(numberToFixed(-12.9, { maxFixed: 0 })).toBe('-13')
208
+ })
209
+
210
+ it('should trim or round repeating decimals based on input type', () => {
211
+ expect(numberToFixed('0.10999', { maxFixed: 3 })).toBe('0.109')
212
+ expect(numberToFixed('-0.10999', { maxFixed: 3 })).toBe('-0.109')
213
+ expect(numberToFixed(0.10999, { maxFixed: 3 })).toBe('0.11')
214
+ expect(numberToFixed(-0.10999, { maxFixed: 3 })).toBe('-0.11')
215
+ })
216
+ })
217
+
218
+ describe('significant digits with decimals', () => {
219
+ it('should keep track of leading zeros for negative decimals', () => {
220
+ expect(numberToFixed('-0.00056789', { significant: 2 })).toBe('-0.0006')
221
+ })
222
+
223
+ it('should round decimals just below one down to -1 with a single significant digit', () => {
224
+ expect(numberToFixed('-0.9994', { significant: 1 })).toBe('-1')
225
+ })
226
+ })
227
+
228
+ describe('input validation', () => {
229
+ it('should accept trailing decimal strings', () => {
230
+ expect(numberToFixed('1.', { fixed: 2 })).toBe('1.00')
231
+ expect(numberToFixed('1.', { significant: 2 })).toBe('1')
232
+ })
233
+
234
+ it('should reject invalid numeric strings', () => {
235
+ expect(() => numberToFixed('invalid', { fixed: 2 })).toThrow(
236
+ 'Invalid number string',
237
+ )
238
+ })
239
+
240
+ it('should reject empty option objects', () => {
241
+ expect(() => numberToFixed(1, {} as any)).toThrow(
242
+ 'Invalid arguments for numberToFixed',
243
+ )
244
+ })
245
+ })
146
246
  })
@@ -175,7 +175,9 @@ function stringToFixed(
175
175
  significantDecimal = significantDecimal.slice(0, -1)
176
176
  }
177
177
 
178
- const res = `${integerPart}${significantDecimal ? `.${significantDecimal}` : ''}`
178
+ const res = `${integerPart}${
179
+ significantDecimal ? `.${significantDecimal}` : ''
180
+ }`
179
181
  return roundString(str, res)
180
182
  }
181
183
 
@@ -192,6 +194,10 @@ function stringToFixed(
192
194
  }
193
195
 
194
196
  function roundString(original: string, formatted: string) {
197
+ if (!original.match(/^-?(?:\d+\.?|\d*\.\d+)$/)) {
198
+ throw new Error('Invalid number string')
199
+ }
200
+
195
201
  const decimalIndex = original.indexOf('.')
196
202
 
197
203
  const nextDigitIndex = formatted.length
@@ -216,15 +222,16 @@ function roundString(original: string, formatted: string) {
216
222
  // handle full carry (e.g., "9.9" -> "10")
217
223
  if (formatted.length === 0 || formatted === '-') {
218
224
  const negative = original.startsWith('-')
219
- const zeros = Math.max(decimalIndex, 0)
225
+ const digitsLeftOfDecimal = decimalIndex - (negative ? 1 : 0)
226
+ const zeros = Math.max(digitsLeftOfDecimal, 0)
220
227
  return `${negative ? '-' : ''}1${'0'.repeat(zeros)}`
221
228
  }
222
229
  }
223
230
 
224
- return `${formatted.substring(0, formatted.length - 1)}${String.fromCharCode(lastDigit() + 1)}`.padEnd(
225
- decimalIndex,
226
- '0',
227
- )
231
+ return `${formatted.substring(
232
+ 0,
233
+ formatted.length - 1,
234
+ )}${String.fromCharCode(lastDigit() + 1)}`.padEnd(decimalIndex, '0')
228
235
  }
229
236
 
230
237
  return formatted
@@ -188,6 +188,103 @@ describe('Fraction', () => {
188
188
  })
189
189
  })
190
190
 
191
+ describe('string formatting', () => {
192
+ it('formats terminating fractions without floating-point artifacts', () => {
193
+ const fraction = new Fraction({ numerator: 4n, denominator: 5n })
194
+
195
+ expect(fraction.toString({ maxFixed: 18 })).toBe('0.8')
196
+ })
197
+
198
+ it('truncates repeating fractions to maxFixed decimal places', () => {
199
+ const fraction = new Fraction({ numerator: 1n, denominator: 3n })
200
+
201
+ expect(fraction.toString({ maxFixed: 18 })).toBe('0.333333333333333333')
202
+ })
203
+
204
+ it('preserves very small fractions in ordinary decimal notation', () => {
205
+ const fraction = new Fraction({
206
+ numerator: 1n,
207
+ denominator: 10n ** 12n,
208
+ })
209
+
210
+ expect(fraction.toString({ maxFixed: 18 })).toBe('0.000000000001')
211
+ expect(fraction.toSignificant(6)).toBe('0.000000000001')
212
+ })
213
+
214
+ it('preserves integers beyond Number.MAX_SAFE_INTEGER', () => {
215
+ const fraction = new Fraction({
216
+ numerator: 9007199254740993n,
217
+ denominator: 1n,
218
+ })
219
+
220
+ expect(fraction.toString({ maxFixed: 18 })).toBe('9007199254740993')
221
+ })
222
+
223
+ it('rounds and pads fixed decimal places', () => {
224
+ expect(
225
+ new Fraction({ numerator: 1n, denominator: 8n }).toString({
226
+ fixed: 2,
227
+ }),
228
+ ).toBe('0.13')
229
+ expect(
230
+ new Fraction({ numerator: 1n, denominator: 2n }).toString({
231
+ fixed: 3,
232
+ }),
233
+ ).toBe('0.500')
234
+ expect(
235
+ new Fraction({ numerator: 199995n, denominator: 1000n }).toString({
236
+ fixed: 2,
237
+ }),
238
+ ).toBe('200.00')
239
+ })
240
+
241
+ it('rounds to significant digits without scientific notation', () => {
242
+ expect(
243
+ new Fraction({ numerator: 1n, denominator: 3n }).toString({
244
+ significant: 6,
245
+ }),
246
+ ).toBe('0.333333')
247
+ expect(
248
+ new Fraction({
249
+ numerator: 123456789123n,
250
+ denominator: 100n,
251
+ }).toString({ significant: 4 }),
252
+ ).toBe('1235000000')
253
+ expect(
254
+ new Fraction({ numerator: 9999n, denominator: 10n }).toString({
255
+ significant: 3,
256
+ }),
257
+ ).toBe('1000')
258
+ })
259
+
260
+ it('formats negative fractions', () => {
261
+ const fraction = new Fraction({ numerator: -1n, denominator: 8n })
262
+
263
+ expect(fraction.toString({ fixed: 2 })).toBe('-0.13')
264
+ expect(fraction.toString({ maxFixed: 2 })).toBe('-0.12')
265
+ })
266
+
267
+ it('formats zero-denominator fractions as non-finite numbers', () => {
268
+ const infinity = new Fraction({ numerator: 1n, denominator: 2n }).div(
269
+ new Fraction({ numerator: 0n }),
270
+ )
271
+
272
+ expect(infinity.toString({ fixed: 2 })).toBe('Infinity')
273
+ expect(infinity.toString({ maxFixed: 2 })).toBe('Infinity')
274
+ expect(infinity.toString({ significant: 2 })).toBe('Infinity')
275
+ expect(
276
+ new Fraction({ numerator: -1n, denominator: 0n }).toString({
277
+ maxFixed: 2,
278
+ }),
279
+ ).toBe('-Infinity')
280
+ expect(
281
+ new Fraction({ numerator: 0n, denominator: 0n }).toString({
282
+ maxFixed: 2,
283
+ }),
284
+ ).toBe('NaN')
285
+ })
286
+ })
287
+
191
288
  describe('serialization', () => {
192
289
  it('should serialize to JSON correctly', () => {
193
290
  const fraction = new Fraction({ numerator: 3, denominator: 4 })
@@ -1,5 +1,5 @@
1
1
  import invariant from 'tiny-invariant'
2
- import { numberToFixed } from '../format/number.js'
2
+ import type { numberToFixed } from '../format/number.js'
3
3
  import type { BigintIsh } from '../types/bigintish.js'
4
4
 
5
5
  export class Fraction {
@@ -164,12 +164,115 @@ export class Fraction {
164
164
  return Number(this.numerator) / Number(this.denominator)
165
165
  }
166
166
 
167
+ private formatDecimal(
168
+ decimalPlaces: number,
169
+ { round, trim }: { round: boolean; trim: boolean },
170
+ ): string {
171
+ invariant(
172
+ Number.isSafeInteger(decimalPlaces),
173
+ 'Decimal places must be a safe integer',
174
+ )
175
+ invariant(this.denominator !== 0n, 'Cannot format a fraction by zero')
176
+
177
+ const negative = this.numerator < 0n !== this.denominator < 0n
178
+ const numerator = this.numerator < 0n ? -this.numerator : this.numerator
179
+ const denominator =
180
+ this.denominator < 0n ? -this.denominator : this.denominator
181
+
182
+ const scale = 10n ** BigInt(Math.abs(decimalPlaces))
183
+ const scaledDenominator =
184
+ decimalPlaces >= 0 ? denominator : denominator * scale
185
+ const scaledNumerator = decimalPlaces >= 0 ? numerator * scale : numerator
186
+
187
+ let scaledValue = scaledNumerator / scaledDenominator
188
+ const remainder = scaledNumerator % scaledDenominator
189
+ if (round && remainder * 2n >= scaledDenominator) {
190
+ scaledValue += 1n
191
+ }
192
+
193
+ let integerPart: string
194
+ let decimalPart = ''
195
+ if (decimalPlaces > 0) {
196
+ const value = scaledValue.toString().padStart(decimalPlaces + 1, '0')
197
+ integerPart = value.slice(0, -decimalPlaces)
198
+ decimalPart = value.slice(-decimalPlaces)
199
+ } else {
200
+ integerPart = (scaledValue * scale).toString()
201
+ }
202
+
203
+ if (trim) {
204
+ decimalPart = decimalPart.replace(/0+$/, '')
205
+ }
206
+
207
+ return `${negative ? '-' : ''}${integerPart}${
208
+ decimalPart ? `.${decimalPart}` : ''
209
+ }`
210
+ }
211
+
212
+ private getDecimalExponent(): number {
213
+ const numerator = this.numerator < 0n ? -this.numerator : this.numerator
214
+ const denominator =
215
+ this.denominator < 0n ? -this.denominator : this.denominator
216
+
217
+ invariant(numerator !== 0n, 'Cannot get the decimal exponent of zero')
218
+ invariant(denominator !== 0n, 'Cannot format a fraction by zero')
219
+
220
+ let exponent = numerator.toString().length - denominator.toString().length
221
+
222
+ if (
223
+ (exponent >= 0 && numerator < denominator * 10n ** BigInt(exponent)) ||
224
+ (exponent < 0 && numerator * 10n ** BigInt(-exponent) < denominator)
225
+ ) {
226
+ exponent -= 1
227
+ }
228
+
229
+ return exponent
230
+ }
231
+
167
232
  public toString(args: Parameters<typeof numberToFixed>[1]): string {
168
- return numberToFixed(this.toNumber(), args)
233
+ if ('fixed' in args) {
234
+ invariant(
235
+ Number.isSafeInteger(args.fixed) && args.fixed >= 0,
236
+ 'Fixed digits must be a non-negative safe integer',
237
+ )
238
+ } else if ('maxFixed' in args) {
239
+ invariant(
240
+ Number.isSafeInteger(args.maxFixed) && args.maxFixed >= 0,
241
+ 'Maximum fixed digits must be a non-negative safe integer',
242
+ )
243
+ } else if ('significant' in args) {
244
+ invariant(
245
+ Number.isSafeInteger(args.significant) && args.significant > 0,
246
+ 'Significant digits must be a positive safe integer',
247
+ )
248
+ } else {
249
+ throw new Error('Invalid arguments for Fraction.toString')
250
+ }
251
+
252
+ if (this.denominator === 0n) {
253
+ return this.toNumber().toString()
254
+ }
255
+
256
+ if ('fixed' in args) {
257
+ return this.formatDecimal(args.fixed, { round: true, trim: false })
258
+ }
259
+
260
+ if ('maxFixed' in args) {
261
+ return this.formatDecimal(args.maxFixed, { round: false, trim: true })
262
+ }
263
+
264
+ if ('significant' in args) {
265
+ if (this.numerator === 0n) return '0'
266
+
267
+ const decimalPlaces = args.significant - this.getDecimalExponent() - 1
268
+ return this.formatDecimal(decimalPlaces, { round: true, trim: true })
269
+ }
270
+
271
+ throw new Error('Invalid arguments for Fraction.toString')
169
272
  }
170
273
 
171
274
  public toSignificant(significantDigits: number): string {
172
- return this.toNumber().toPrecision(significantDigits)
275
+ return this.toString({ significant: significantDigits })
173
276
  }
174
277
 
175
278
  public toJSON() {
@@ -13,7 +13,11 @@ import type {
13
13
  MvmToken,
14
14
  MvmTxHash,
15
15
  } from '../../mvm/currency/token.js'
16
- import type { StellarAddress, StellarTxHash } from '../../stellar/address.js'
16
+ import type {
17
+ StellarAddress,
18
+ StellarContractAddress,
19
+ StellarTxHash,
20
+ } from '../../stellar/address.js'
17
21
  import type { StellarChainId } from '../../stellar/chain/chains.js'
18
22
  import type { StellarCurrency } from '../../stellar/currency/currency.js'
19
23
  import type { StellarToken } from '../../stellar/currency/token.js'
@@ -82,6 +86,17 @@ export type AddressFor<TChainId extends ChainId> = TChainId extends EvmChainId
82
86
  ? StellarAddress
83
87
  : never
84
88
 
89
+ export type ContractAddressFor<TChainId extends ChainId> =
90
+ TChainId extends EvmChainId
91
+ ? EvmAddress
92
+ : TChainId extends MvmChainId
93
+ ? MvmAddress
94
+ : TChainId extends SvmChainId
95
+ ? SvmAddress
96
+ : TChainId extends StellarChainId
97
+ ? StellarContractAddress
98
+ : never
99
+
85
100
  export type TxHashFor<TChainId extends ChainId> = TChainId extends EvmChainId
86
101
  ? EvmTxHash
87
102
  : TChainId extends MvmChainId
@@ -0,0 +1,35 @@
1
+ import { describe, expectTypeOf, it } from 'vitest'
2
+ import { SvmChainId } from '~/svm/chain/chains.js'
3
+ import type { SvmAddress } from '~/svm/currency/token.js'
4
+ import type { EvmChainId } from '../../evm/chain/chains.js'
5
+ import type { EvmAddress } from '../../evm/currency/token.js'
6
+ import type { ID } from '../types/id.js'
7
+ import { getChainIdAddressFromId, getIdFromChainIdAddress } from './id.js'
8
+
9
+ describe('getChainIdAddressFromId', () => {
10
+ it('preserves EVM chainId and address types', () => {
11
+ const id = '1:0xaa' as ID<EvmChainId, EvmAddress>
12
+
13
+ expectTypeOf(getChainIdAddressFromId(id)).toEqualTypeOf<{
14
+ chainId: EvmChainId
15
+ address: EvmAddress
16
+ }>()
17
+ })
18
+
19
+ it('preserves Solana chainId and address types', () => {
20
+ const id = getIdFromChainIdAddress(
21
+ SvmChainId.SOLANA,
22
+ 'So11111111111111111111111111111111111111112' as SvmAddress,
23
+ )
24
+
25
+ expectTypeOf(getChainIdAddressFromId(id)).not.toEqualTypeOf<{
26
+ chainId: SvmChainId
27
+ address: `${SvmAddress}`
28
+ }>()
29
+
30
+ expectTypeOf(getChainIdAddressFromId(id)).toEqualTypeOf<{
31
+ chainId: SvmChainId
32
+ address: SvmAddress
33
+ }>()
34
+ })
35
+ })
@@ -1,9 +1,15 @@
1
+ import type { SvmAddress } from '../../svm/currency/token.js'
1
2
  import type { ChainId } from '../chain/chains.js'
2
3
  import type { AddressFor } from '../types/for-chain.js'
3
4
  import type { ID } from '../types/id.js'
4
5
  import { getNativeAddress } from './native-address.js'
5
6
  import { normalizeAddress } from './normalize-address.js'
6
7
 
8
+ // Hacky fix, SvmAddress is a branded type, but when used in a template literal type, it loses its branding.
9
+ type ReturnedAddress<TAddress extends string> = TAddress extends `${SvmAddress}`
10
+ ? SvmAddress
11
+ : TAddress
12
+
7
13
  export function getChainIdAddressFromId<
8
14
  TChainId extends ChainId,
9
15
  TAddress extends string,
@@ -11,7 +17,7 @@ export function getChainIdAddressFromId<
11
17
  id: ID<TChainId, TAddress>,
12
18
  ): {
13
19
  chainId: TChainId
14
- address: TAddress
20
+ address: ReturnedAddress<TAddress>
15
21
  } {
16
22
  const separatorIndex = id.indexOf(':')
17
23
 
@@ -22,7 +28,7 @@ export function getChainIdAddressFromId<
22
28
  }
23
29
 
24
30
  const chainId = +id.slice(0, separatorIndex) as TChainId
25
- const address = id.slice(separatorIndex + 1) as TAddress
31
+ const address = id.slice(separatorIndex + 1) as ReturnedAddress<TAddress>
26
32
 
27
33
  if (!chainId || !address) {
28
34
  throw new Error(