@waterx/sdk 4.0.1 → 4.3.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 (130) hide show
  1. package/README.md +37 -17
  2. package/dist/cjs/src/account/account.js +2 -1
  3. package/dist/cjs/src/account/config.d.ts +3 -2
  4. package/dist/cjs/src/account/funding/balance.d.ts +25 -2
  5. package/dist/cjs/src/account/funding/balance.js +36 -24
  6. package/dist/cjs/src/account/funding/credit.js +6 -10
  7. package/dist/cjs/src/constants.d.ts +15 -1
  8. package/dist/cjs/src/constants.js +18 -4
  9. package/dist/cjs/src/generated/waterx_rule/deps/bucket_v2_framework/float.d.ts +8 -0
  10. package/dist/cjs/src/generated/waterx_rule/deps/bucket_v2_framework/float.js +13 -0
  11. package/dist/cjs/src/generated/waterx_rule/deps/sui/vec_map.d.ts +36 -0
  12. package/dist/cjs/src/generated/waterx_rule/deps/sui/vec_map.js +31 -0
  13. package/dist/cjs/src/generated/waterx_rule/waterx_rule.d.ts +813 -0
  14. package/dist/cjs/src/generated/waterx_rule/waterx_rule.js +946 -0
  15. package/dist/cjs/src/oracle/aggregate.d.ts +23 -21
  16. package/dist/cjs/src/oracle/aggregate.js +133 -59
  17. package/dist/cjs/src/oracle/config.d.ts +82 -41
  18. package/dist/cjs/src/oracle/config.js +0 -42
  19. package/dist/cjs/src/oracle/host.d.ts +26 -11
  20. package/dist/cjs/src/oracle/index.d.ts +6 -2
  21. package/dist/cjs/src/oracle/index.js +36 -8
  22. package/dist/cjs/src/oracle/price-update-rule.d.ts +16 -10
  23. package/dist/cjs/src/oracle/price-update-rule.js +3 -3
  24. package/dist/cjs/src/oracle/pyth.d.ts +44 -0
  25. package/dist/cjs/src/oracle/pyth.js +48 -6
  26. package/dist/cjs/src/oracle/read-plane.d.ts +70 -0
  27. package/dist/cjs/src/oracle/read-plane.js +78 -0
  28. package/dist/cjs/src/oracle/rule-registry.d.ts +14 -11
  29. package/dist/cjs/src/oracle/rule-registry.js +16 -11
  30. package/dist/cjs/src/oracle/rules/pyth-core-rule.js +4 -4
  31. package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +21 -0
  32. package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +29 -4
  33. package/dist/cjs/src/oracle/rules/pyth-rule.js +5 -4
  34. package/dist/cjs/src/oracle/rules/waterx-rule.d.ts +126 -0
  35. package/dist/cjs/src/oracle/rules/waterx-rule.js +312 -0
  36. package/dist/cjs/src/perp/client.d.ts +71 -20
  37. package/dist/cjs/src/perp/client.js +35 -13
  38. package/dist/cjs/src/perp/config.d.ts +6 -7
  39. package/dist/cjs/src/perp/config.js +12 -11
  40. package/dist/cjs/src/perp/constants.d.ts +0 -6
  41. package/dist/cjs/src/perp/constants.js +11 -9
  42. package/dist/cjs/src/perp/fetch/account.js +3 -0
  43. package/dist/cjs/src/perp/fetch/bridge.js +2 -1
  44. package/dist/cjs/src/perp/fetch/market.js +2 -1
  45. package/dist/cjs/src/perp/fetch/positions.d.ts +16 -10
  46. package/dist/cjs/src/perp/fetch/positions.js +23 -20
  47. package/dist/cjs/src/perp/index.d.ts +9 -5
  48. package/dist/cjs/src/perp/index.js +16 -7
  49. package/dist/cjs/src/perp/liq-view.d.ts +64 -0
  50. package/dist/cjs/src/perp/liq-view.js +74 -0
  51. package/dist/cjs/src/perp/user/order.d.ts +13 -0
  52. package/dist/cjs/src/perp/user/order.js +30 -16
  53. package/dist/cjs/src/perp/user/staking.js +3 -2
  54. package/dist/cjs/src/perp/user/trading.js +25 -24
  55. package/dist/cjs/src/perp/user/wlp.js +6 -5
  56. package/dist/cjs/src/prediction/utils.d.ts +11 -2
  57. package/dist/cjs/src/prediction/utils.js +22 -22
  58. package/dist/cjs/src/unified-client.d.ts +41 -15
  59. package/dist/cjs/src/unified-client.js +4 -2
  60. package/dist/cjs/src/utils/format.d.ts +14 -0
  61. package/dist/cjs/src/utils/format.js +24 -0
  62. package/dist/cjs/src/utils/math.d.ts +304 -12
  63. package/dist/cjs/src/utils/math.js +397 -17
  64. package/dist/cjs/src/utils/validate.d.ts +69 -0
  65. package/dist/cjs/src/utils/validate.js +183 -0
  66. package/dist/src/account/account.js +2 -1
  67. package/dist/src/account/config.d.ts +3 -2
  68. package/dist/src/account/funding/balance.d.ts +25 -2
  69. package/dist/src/account/funding/balance.js +36 -24
  70. package/dist/src/account/funding/credit.js +6 -10
  71. package/dist/src/constants.d.ts +15 -1
  72. package/dist/src/constants.js +17 -3
  73. package/dist/src/generated/waterx_rule/deps/bucket_v2_framework/float.d.ts +8 -0
  74. package/dist/src/generated/waterx_rule/deps/bucket_v2_framework/float.js +10 -0
  75. package/dist/src/generated/waterx_rule/deps/sui/vec_map.d.ts +36 -0
  76. package/dist/src/generated/waterx_rule/deps/sui/vec_map.js +27 -0
  77. package/dist/src/generated/waterx_rule/waterx_rule.d.ts +813 -0
  78. package/dist/src/generated/waterx_rule/waterx_rule.js +855 -0
  79. package/dist/src/oracle/aggregate.d.ts +23 -21
  80. package/dist/src/oracle/aggregate.js +133 -59
  81. package/dist/src/oracle/config.d.ts +82 -41
  82. package/dist/src/oracle/config.js +1 -41
  83. package/dist/src/oracle/host.d.ts +26 -11
  84. package/dist/src/oracle/index.d.ts +6 -2
  85. package/dist/src/oracle/index.js +27 -9
  86. package/dist/src/oracle/price-update-rule.d.ts +16 -10
  87. package/dist/src/oracle/price-update-rule.js +3 -3
  88. package/dist/src/oracle/pyth.d.ts +44 -0
  89. package/dist/src/oracle/pyth.js +45 -5
  90. package/dist/src/oracle/read-plane.d.ts +70 -0
  91. package/dist/src/oracle/read-plane.js +74 -0
  92. package/dist/src/oracle/rule-registry.d.ts +14 -11
  93. package/dist/src/oracle/rule-registry.js +16 -11
  94. package/dist/src/oracle/rules/pyth-core-rule.js +5 -5
  95. package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +21 -0
  96. package/dist/src/oracle/rules/pyth-lazer-rule.js +28 -3
  97. package/dist/src/oracle/rules/pyth-rule.js +5 -4
  98. package/dist/src/oracle/rules/waterx-rule.d.ts +126 -0
  99. package/dist/src/oracle/rules/waterx-rule.js +305 -0
  100. package/dist/src/perp/client.d.ts +71 -20
  101. package/dist/src/perp/client.js +36 -14
  102. package/dist/src/perp/config.d.ts +6 -7
  103. package/dist/src/perp/config.js +11 -9
  104. package/dist/src/perp/constants.d.ts +0 -6
  105. package/dist/src/perp/constants.js +10 -8
  106. package/dist/src/perp/fetch/account.js +3 -0
  107. package/dist/src/perp/fetch/bridge.js +2 -1
  108. package/dist/src/perp/fetch/market.js +2 -1
  109. package/dist/src/perp/fetch/positions.d.ts +16 -10
  110. package/dist/src/perp/fetch/positions.js +28 -20
  111. package/dist/src/perp/index.d.ts +9 -5
  112. package/dist/src/perp/index.js +6 -4
  113. package/dist/src/perp/liq-view.d.ts +64 -0
  114. package/dist/src/perp/liq-view.js +71 -0
  115. package/dist/src/perp/user/order.d.ts +13 -0
  116. package/dist/src/perp/user/order.js +30 -16
  117. package/dist/src/perp/user/staking.js +3 -2
  118. package/dist/src/perp/user/trading.js +25 -24
  119. package/dist/src/perp/user/wlp.js +6 -5
  120. package/dist/src/prediction/utils.d.ts +11 -2
  121. package/dist/src/prediction/utils.js +22 -22
  122. package/dist/src/unified-client.d.ts +41 -15
  123. package/dist/src/unified-client.js +4 -2
  124. package/dist/src/utils/format.d.ts +14 -0
  125. package/dist/src/utils/format.js +21 -0
  126. package/dist/src/utils/math.d.ts +304 -12
  127. package/dist/src/utils/math.js +394 -17
  128. package/dist/src/utils/validate.d.ts +69 -0
  129. package/dist/src/utils/validate.js +167 -0
  130. package/package.json +4 -1
@@ -1,14 +1,85 @@
1
+ /**
2
+ * Perp / WLP number math: Move-mirroring formulas, the `Float` fixed-point
3
+ * mirrors the canonical raw estimates need, and the on-chain price encoding.
4
+ *
5
+ * ## Validation policy — two tiers, deliberate
6
+ *
7
+ * The per-function `@throws` blocks below are INSTANCES of this rule, not a
8
+ * dozen independent decisions — every tier-1 function carries one. Guards come
9
+ * from `utils/validate.ts` (`assertFinite` / `assertFiniteNonNegative` /
10
+ * `assertUnitFraction` / `assertTokenDecimal` / `assertUnsignedBigInt`) and
11
+ * every message names the offending parameter.
12
+ *
13
+ * 1. **MONEY PATH — throws `RangeError`.** The fee bundles
14
+ * (`calcRealLiqNetCostUsd`, `calcViewEstLiqFeesUsd`), the liquidation
15
+ * estimates (`calcEstLiqPrice`, `calcEstLiqPriceRaw`), collateral and
16
+ * withdrawable (`calcEffectiveCollateralUsd`,
17
+ * `calcMaxReducibleCollateralUsd`), and the WLP quote / APY surface
18
+ * (`calcWlpMintOut`, `calcWlpRedeemOut`, `calcDynamicFeeBps`,
19
+ * `annualizedApyFromRatio`, `calcWlpIncentiveApy`). These map garbage onto a
20
+ * PLAUSIBLE number — an `Infinity` fee returns 0, indistinguishable from
21
+ * "already liquidatable"; a negative fee ADDS to equity — so the input dies
22
+ * at the boundary instead of becoming a wrong price on a screen.
23
+ * 2. **BARE ARITHMETIC — no guards.** Mostly one-line helpers (`calcNotional`,
24
+ * `calcFee`, `calcUnrealizedPnl`, `calcLeverage`, `calcTotalTradingFeeRate`,
25
+ * `calcFundingRate`, `calcFundingFeeUsd`, `decodeFundingIndexDelta`,
26
+ * `calcBorrowRate`, `calcBorrowRateAccrual`, `calcPositionBorrowFee`,
27
+ * `calcTokenUtilizationBps`, `annualizeFundingRate`, `calcWlpPrice`), plus
28
+ * `calcImpactFeeRate` — NOT a one-liner (a branchy curve with eight early
29
+ * returns) but the same category: each of its early returns tests an exact
30
+ * zero or an ordering that a NaN never satisfies, so garbage falls straight
31
+ * through the branches instead of being absorbed by one. A NaN / Infinity
32
+ * input propagates VISIBLY to the output in all of them — there is no
33
+ * plausible-looking value for it to hide behind — so a guard would add noise
34
+ * and cost without buying safety.
35
+ *
36
+ * `rawPrice` sits in neither tier: it is a PARSE, and throws plain `Error` on
37
+ * malformed input.
38
+ *
39
+ * ### Documented domain zeros
40
+ * Distinct from garbage: an input that is legitimately zero and has exactly one
41
+ * honest answer returns it rather than throwing. `sizeInAsset === 0` /
42
+ * `entryNotional === 0` → `0` (nothing to liquidate); `totalSupply === 0` → `0`
43
+ * (no shares to price — `calcWlpMintOut` is the deliberate exception, where it
44
+ * means the bootstrap mint); `intervalMs === 0` / `days <= 0` → `0` (no window
45
+ * to annualize over); `liquidityAmount === 0` → `0` utilization;
46
+ * `collateralUsd === 0` → `Infinity` leverage. Each is noted at its function.
47
+ */
1
48
  import { BPS_SCALE, DOUBLE_SCALE, FLOAT_SCALE, MS_PER_YEAR } from "../constants.js";
49
+ import { assertFinite, assertFiniteNonNegative, assertTokenDecimal, assertUnitFraction, assertUnsignedBigInt, } from "./validate.js";
2
50
  // ======== On-chain encoding ========
51
+ const DECIMAL_USD_RE = /^(\d+)(?:\.(\d+))?$/;
3
52
  /**
4
53
  * Convert a human-readable USD price to the raw 1e9-scaled `u128` value
5
54
  * that on-chain `Float`-typed parameters expect.
55
+ *
56
+ * Use for **tx-build** price args only (`acceptablePrice` / `triggerPrice` /
57
+ * size args). Do NOT use for the view-read params `basePriceUsd` /
58
+ * `collateralPriceUsd` on `perp/fetch` (`getPosition`, `getMarketPositions`,
59
+ * `getOrder`, …) — those take WHOLE-DOLLAR integer USD (the Move view applies
60
+ * `float::from` internally; a 1e9-scaled value inflates pnl/notional-derived
61
+ * fields by 1e9).
62
+ *
63
+ * Precision: see {@link RawPriceInput}. The `number` path rounds through f64
64
+ * and is exact only below ≈ $9,007,199; an {@link ExactDecimalUsd} string
65
+ * parses digits directly onto the 1e9 grid without touching f64. This bites
66
+ * hardest on `triggerPrice`, which is an EXACT order-book key: a raw value off
67
+ * by one unit silently fails the order lookup.
6
68
  */
7
69
  export function rawPrice(usd) {
8
- const n = typeof usd === "string" ? Number(usd) : usd;
9
- if (!Number.isFinite(n))
70
+ if (typeof usd === "string") {
71
+ const match = DECIMAL_USD_RE.exec(usd.trim());
72
+ if (!match)
73
+ throw new Error(`Invalid USD price: ${usd}`);
74
+ const [, whole, frac = ""] = match;
75
+ if (frac.length > 9) {
76
+ throw new Error(`Invalid USD price: ${usd} — more than 9 decimal places cannot be represented on the 1e9 grid`);
77
+ }
78
+ return BigInt(whole) * FLOAT_SCALE + BigInt(frac.padEnd(9, "0") || "0");
79
+ }
80
+ if (!Number.isFinite(usd))
10
81
  throw new Error(`Invalid USD price: ${usd}`);
11
- return BigInt(Math.round(n * Number(FLOAT_SCALE)));
82
+ return BigInt(Math.round(usd * Number(FLOAT_SCALE)));
12
83
  }
13
84
  // ======== Basic position math ========
14
85
  /** Notional value in USD: sizeInAsset × price. */
@@ -30,9 +101,81 @@ export function calcLeverage(sizeUsd, collateralUsd) {
30
101
  return sizeUsd / collateralUsd;
31
102
  }
32
103
  /**
33
- * Estimated liquidation price.
104
+ * Net fee cost (USD) of the REAL liquidation check — a plain SIGNED sum:
105
+ *
106
+ * netCostUsd = borrowFeeUsd + openFeeUsd + closingFeeUsd + fundingFeeUsd
107
+ *
108
+ * `fundingFeeUsd` is SIGNED cost-positive, and the result MAY BE NEGATIVE
109
+ * when funding income exceeds the other fees. That is deliberate:
110
+ * `position.move::is_liquidatable` credits funding income IN FULL — income
111
+ * first pays down any accumulated deficit, and the remainder is added back to
112
+ * remaining equity — so the linearized equity model is a plain signed sum
113
+ * with no floor. Flooring at 0 here would understate an income-rich
114
+ * position's equity and show a liq price closer to spot than the real check.
115
+ *
116
+ * Three-way Move-semantics relationship (verified against the Move sources
117
+ * 2026-07-30):
118
+ * - the REAL liquidation check, `position.move::is_liquidatable`
119
+ * (waterx_perp): deducts borrow + open + CLOSING fee and credits funding
120
+ * income in full (deficit first, remainder back to equity) — exactly this
121
+ * function's signed sum;
122
+ * - the view estimate, `view.move::calculate_est_liq_price`
123
+ * (waterx_perp_view): OMITS the closing fee AND floors its fee bundle at 0
124
+ * (`Float.saturating_sub` — Float is unsigned) — see `calcViewEstLiqFeesUsd`
125
+ * and the op-exact `calcEstLiqPriceRaw`;
126
+ * - SDK `calcEffectiveCollateralUsd` does NOT credit funding income at all:
127
+ * it mirrors the withdrawable-collateral checks
128
+ * (`calculate_effective_collateral_amount` in `trading.move`), not the
129
+ * liquidation inequality.
130
+ * Do not "fix" any of the three to match another.
131
+ *
132
+ * @throws RangeError when borrow/open/closing fees are not finite `>= 0`
133
+ * numbers, or `fundingFeeUsd` is not finite.
134
+ */
135
+ export function calcRealLiqNetCostUsd(fees) {
136
+ assertFiniteNonNegative("fees.borrowFeeUsd", fees.borrowFeeUsd);
137
+ assertFiniteNonNegative("fees.openFeeUsd", fees.openFeeUsd);
138
+ assertFiniteNonNegative("fees.closingFeeUsd", fees.closingFeeUsd);
139
+ assertFinite("fees.fundingFeeUsd", fees.fundingFeeUsd);
140
+ return fees.borrowFeeUsd + fees.openFeeUsd + fees.closingFeeUsd + fees.fundingFeeUsd;
141
+ }
142
+ /**
143
+ * Fee bundle (USD) of the VIEW estimate `view.move::calculate_est_liq_price`:
144
+ *
145
+ * viewFeesUsd = max(0, borrowFeeUsd + openFeeUsd + fundingFeeUsd)
146
+ *
147
+ * Mirrors the view's unsigned Float arithmetic: funding income is credited
148
+ * via `Float.saturating_sub`, so the bundle FLOORS AT 0 — income beyond the
149
+ * other fees is discarded by the view. There is deliberately NO
150
+ * `closingFeeUsd` field: the view omits the close-fee term, and this shape
151
+ * makes it structurally impossible to include one. For the REAL liquidation
152
+ * check's semantics use `calcRealLiqNetCostUsd`; for chain-bit-identical
153
+ * output use `calcEstLiqPriceRaw`.
154
+ *
155
+ * Exported on purpose even though `calcEstLiqPriceRaw` covers the chain-exact
156
+ * path: this is the VIEW-model counterpart of `calcRealLiqNetCostUsd`, for
157
+ * consumers that need the view's fee bundle as a Number (mirroring what the
158
+ * view displays) rather than the full raw price. The two-API split is what makes
159
+ * the REAL and VIEW models impossible to confuse — do not fold it away.
160
+ *
161
+ * @throws RangeError when borrow/open fees are not finite `>= 0` numbers, or
162
+ * `fundingFeeUsd` is not finite.
163
+ */
164
+ export function calcViewEstLiqFeesUsd(fees) {
165
+ assertFiniteNonNegative("fees.borrowFeeUsd", fees.borrowFeeUsd);
166
+ assertFiniteNonNegative("fees.openFeeUsd", fees.openFeeUsd);
167
+ assertFinite("fees.fundingFeeUsd", fees.fundingFeeUsd);
168
+ return Math.max(0, fees.borrowFeeUsd + fees.openFeeUsd + fees.fundingFeeUsd);
169
+ }
170
+ /**
171
+ * Estimated liquidation price — Number (f64) UI convenience.
34
172
  *
35
- * Matches `calculate_est_liq_price` in `waterx_perp_view/sources/view.move`:
173
+ * An approximation for display. The CANONICAL implementation is
174
+ * `calcEstLiqPriceRaw` (BigInt fixed-point, op-for-op mirror of
175
+ * `calculate_est_liq_price` in `waterx_perp_view/sources/view.move`); use that
176
+ * wherever exact parity with the on-chain `est_liq_price` matters.
177
+ *
178
+ * Same linear model as the view:
36
179
  * maintenance = maintenanceMarginRate × (size × spotPrice) ← uses current notional
37
180
  * ratio = (collateralUsd − totalFeesUsd − maintenance) / (size × avgPrice)
38
181
  * long: liq = avgPrice × (1 − ratio)
@@ -40,10 +183,40 @@ export function calcLeverage(sizeUsd, collateralUsd) {
40
183
  *
41
184
  * Returns 0 when the position is already liquidatable or has no size.
42
185
  *
43
- * @param totalFeesUsd Sum of all accrued fees in USD: borrow + trading + net funding.
186
+ * Fees pass EITHER:
187
+ * - `fees`: the structured bundle, matching the REAL liquidation check
188
+ * (`position.move::is_liquidatable`). `totalFeesUsd` is derived via
189
+ * `calcRealLiqNetCostUsd` — a SIGNED sum including the closing fee, with
190
+ * funding income credited IN FULL, so a caller structurally cannot omit a
191
+ * term or mis-handle income. A negative net cost ADDS to the remaining
192
+ * margin, pushing the estimate FARTHER from spot. Takes precedence when
193
+ * both are given; or
194
+ * - `totalFeesUsd`: a pre-computed number (back-compat path) — the caller
195
+ * owns the fee model.
196
+ * NOTE the deliberate model difference vs the on-chain VIEW: the view both
197
+ * floors its fee bundle at 0 and omits the close fee (`calcViewEstLiqFeesUsd`
198
+ * / `calcEstLiqPriceRaw`); the `fees` path here matches the REAL check.
199
+ *
200
+ * @throws RangeError when sizeInAsset / avgPrice / spotPrice / collateralUsd
201
+ * are not finite `>= 0` numbers, when `maintenanceMarginRate` is not finite
202
+ * inside `[0, 1]`, or when `totalFeesUsd` is not finite.
203
+ * (`sizeInAsset === 0` stays a documented domain case returning 0.)
44
204
  */
45
205
  export function calcEstLiqPrice(params) {
46
- const { isLong, avgPrice, sizeInAsset, collateralUsd, maintenanceMarginRate, spotPrice, totalFeesUsd, } = params;
206
+ const { isLong, avgPrice, sizeInAsset, collateralUsd, maintenanceMarginRate, spotPrice } = params;
207
+ assertFiniteNonNegative("sizeInAsset", sizeInAsset);
208
+ assertFiniteNonNegative("avgPrice", avgPrice);
209
+ assertFiniteNonNegative("spotPrice", spotPrice);
210
+ assertFiniteNonNegative("collateralUsd", collateralUsd);
211
+ assertUnitFraction("maintenanceMarginRate", maintenanceMarginRate);
212
+ let totalFeesUsd;
213
+ if (params.fees !== undefined) {
214
+ totalFeesUsd = calcRealLiqNetCostUsd(params.fees);
215
+ }
216
+ else {
217
+ totalFeesUsd = params.totalFeesUsd ?? 0;
218
+ assertFinite("totalFeesUsd", totalFeesUsd);
219
+ }
47
220
  if (sizeInAsset === 0)
48
221
  return 0;
49
222
  const entryNotional = sizeInAsset * avgPrice;
@@ -59,6 +232,118 @@ export function calcEstLiqPrice(params) {
59
232
  }
60
233
  return avgPrice * (1 + ratio);
61
234
  }
235
+ // ======== Move Float mirrors (internal) ========
236
+ // `bucket_v2_framework::float` — UNSIGNED u128 fixed-point at 1e9 where every
237
+ // operation truncates via integer division. These mirror exactly the ops
238
+ // `calcEstLiqPriceRaw` needs. Move's overflow aborts are not mirrored: JS
239
+ // BigInt cannot overflow, and an input that would abort on chain has no
240
+ // chain-produced value to be compared against.
241
+ /** `float::from_fraction(n, m)` — `(n × 1e9) / m`, truncating. */
242
+ const floatFromFraction = (n, m) => (n * FLOAT_SCALE) / m;
243
+ /** `float::mul(a, b)` — `(a × b) / 1e9`, truncating. */
244
+ const floatMul = (a, b) => (a * b) / FLOAT_SCALE;
245
+ /** `float::div(a, b)` — `(a × 1e9) / b`, truncating. */
246
+ const floatDiv = (a, b) => (a * FLOAT_SCALE) / b;
247
+ /** `float::saturating_sub(a, b)` — `a − b` floored at 0 (Float is unsigned). */
248
+ const floatSaturatingSub = (a, b) => (a < b ? 0n : a - b);
249
+ /**
250
+ * `waterx_perp::math::amount_to_usd(amount, decimal, price)` — the exact
251
+ * two-step composition `from_fraction(amount, 10^decimal).mul(price)`, each
252
+ * step truncating independently (NOT algebraically merged).
253
+ */
254
+ const amountToUsdRaw = (amountRaw, pow10, priceRaw) => floatMul(floatFromFraction(amountRaw, pow10), priceRaw);
255
+ /**
256
+ * `10u64.pow(decimal)` for the whole u64 decimal domain — exhaustive, since the
257
+ * `collateralDecimal` guard pins it to `[0, 19]` (10^19 < 2^64 ≤ 10^20). Built
258
+ * once at module load instead of exponentiating per call.
259
+ */
260
+ const POW10 = Object.freeze(Array.from({ length: 20 }, (_, decimal) => 10n ** BigInt(decimal)));
261
+ /**
262
+ * Estimated liquidation price — CANONICAL raw fixed-point implementation.
263
+ *
264
+ * Op-for-op mirror of `calculate_est_liq_price` in
265
+ * `waterx_perp_view/sources/view.move` under `bucket_v2_framework::float`
266
+ * semantics: unsigned 1e9 fixed-point, every `mul` is `(a×b)/1e9` and every
267
+ * `div` is `(a×1e9)/b` with truncating BigInt division at EACH step,
268
+ * including `math::amount_to_usd`'s exact composition, the u64 addition of
269
+ * borrow + open fee BEFORE the USD conversion, and the `saturating_sub`
270
+ * funding credit (the VIEW model: fee bundle floors at 0, close fee omitted).
271
+ * Given the same raw inputs the view receives, the result is bit-identical to
272
+ * the chain's `PositionData.est_liq_price`.
273
+ *
274
+ * The Number `calcEstLiqPrice` is a UI convenience approximation of this
275
+ * canonical form (and its `fees` path models the REAL liquidation check
276
+ * instead of the view — see `calcRealLiqNetCostUsd`).
277
+ *
278
+ * Inputs are the raw on-chain values exactly as the view takes them.
279
+ * Returns the raw 1e9-scaled u128 price; `0n` = already liquidatable /
280
+ * zero size (the view's N/A signal).
281
+ *
282
+ * Holding a fetched `PositionDataView` row? Use `calcEstLiqPriceRawFromView`
283
+ * (`perp/liq-view.ts`) instead of hand-mapping its nine raw fields — the adapter
284
+ * owns that mapping and carries the invariant below on its own signature.
285
+ *
286
+ * INVARIANT the signature cannot enforce: `basePriceUsd` / `collateralPriceUsd`
287
+ * MUST be the same whole-dollar values passed to the `perp/fetch` read that
288
+ * produced the row whose fields you are feeding in. `PositionDataView` does not
289
+ * carry the probe prices, so nothing here can check it — feed different prices
290
+ * and the fee/notional-derived fields were computed against one price while the
291
+ * estimate is computed against another, and parity with
292
+ * `PositionData.est_liq_price` silently breaks.
293
+ *
294
+ * @throws RangeError when any bigint input is negative or
295
+ * `collateralDecimal` is not an integer in `[0, 19]` (u64 `10^decimal`).
296
+ */
297
+ export function calcEstLiqPriceRaw(params) {
298
+ const { isLong, sizeRaw, avgPriceRaw, collateralAmountRaw, collateralDecimal, basePriceUsd, collateralPriceUsd, maintenanceMarginRaw, borrowFeeRaw, fundingSign, fundingFeeRaw, tradingFeeRaw, } = params;
299
+ // One loop so the guard labels cannot drift from the field names.
300
+ for (const [label, value] of Object.entries({
301
+ sizeRaw,
302
+ avgPriceRaw,
303
+ collateralAmountRaw,
304
+ basePriceUsd,
305
+ collateralPriceUsd,
306
+ maintenanceMarginRaw,
307
+ borrowFeeRaw,
308
+ fundingFeeRaw,
309
+ tradingFeeRaw,
310
+ })) {
311
+ assertUnsignedBigInt(label, value);
312
+ }
313
+ assertTokenDecimal("collateralDecimal", collateralDecimal);
314
+ // float::from(u64) — whole dollars onto the 1e9 grid (exact, no truncation).
315
+ const basePrice = basePriceUsd * FLOAT_SCALE;
316
+ const collPrice = collateralPriceUsd * FLOAT_SCALE;
317
+ const pow10 = POW10[collateralDecimal];
318
+ const entryNotional = floatMul(sizeRaw, avgPriceRaw);
319
+ // math::amount_to_usd — the exact two-step from_fraction(amount, 10^dec).mul(price),
320
+ // each step truncating independently (see amountToUsdRaw).
321
+ const collateralUsd = amountToUsdRaw(collateralAmountRaw, pow10, collPrice);
322
+ const currentNotional = floatMul(sizeRaw, basePrice);
323
+ const maintenance = floatMul(maintenanceMarginRaw, currentNotional);
324
+ // u64 addition BEFORE the conversion — ONE from_fraction over the summed fees,
325
+ // not two conversions added (the truncation points differ).
326
+ const accruedFeesUsd = amountToUsdRaw(borrowFeeRaw + tradingFeeRaw, pow10, collPrice);
327
+ const fundingFeeUsd = amountToUsdRaw(fundingFeeRaw, pow10, collPrice);
328
+ // owed → add; income → Float.saturating_sub (unsigned: the bundle floors at 0,
329
+ // so the view DISCARDS income beyond the other fees).
330
+ const totalFeesUsd = fundingSign
331
+ ? accruedFeesUsd + fundingFeeUsd
332
+ : floatSaturatingSub(accruedFeesUsd, fundingFeeUsd);
333
+ const deductions = totalFeesUsd + maintenance;
334
+ // The view's early return is `lte` / `eq` — INCLUSIVE: collateral exactly at
335
+ // deductions is already liquidatable, hence `<=` and `=== 0n` here.
336
+ if (collateralUsd <= deductions || entryNotional === 0n)
337
+ return 0n;
338
+ const marginRemaining = collateralUsd - deductions;
339
+ const ratio = floatDiv(marginRemaining, entryNotional);
340
+ if (isLong) {
341
+ if (ratio >= FLOAT_SCALE)
342
+ return 0n;
343
+ return floatMul(avgPriceRaw, FLOAT_SCALE - ratio);
344
+ }
345
+ return floatMul(avgPriceRaw, FLOAT_SCALE + ratio);
346
+ }
62
347
  /**
63
348
  * Effective (fee-adjusted) collateral in USD.
64
349
  *
@@ -79,12 +364,19 @@ export function calcEstLiqPrice(params) {
79
364
  * @param grossCollateralUsd Position collateral in USD (`collateral_amount` → USD).
80
365
  * @param borrowFeeUsd `unrealized_borrow_fee` in USD.
81
366
  * @param fundingSign `unrealized_funding_sign` — true when the position owes funding.
82
- * @param fundingFeeUsd `unrealized_funding_fee` magnitude in USD.
367
+ * @param fundingFeeUsd `unrealized_funding_fee` magnitude in USD (unsigned; sign travels separately).
83
368
  * @param tradingFeeUsd `unrealized_trading_fee` in USD.
84
369
  * @param projectedTradingFeeUsd Closing fee to reserve (0 for a bare collateral withdrawal).
370
+ * @throws RangeError when any USD input is not a finite `>= 0` number — a
371
+ * negative fee would silently ADD to effective collateral.
85
372
  */
86
373
  export function calcEffectiveCollateralUsd(params) {
87
374
  const { grossCollateralUsd, borrowFeeUsd, fundingSign, fundingFeeUsd, tradingFeeUsd, projectedTradingFeeUsd = 0, } = params;
375
+ assertFiniteNonNegative("grossCollateralUsd", grossCollateralUsd);
376
+ assertFiniteNonNegative("borrowFeeUsd", borrowFeeUsd);
377
+ assertFiniteNonNegative("fundingFeeUsd", fundingFeeUsd);
378
+ assertFiniteNonNegative("tradingFeeUsd", tradingFeeUsd);
379
+ assertFiniteNonNegative("projectedTradingFeeUsd", projectedTradingFeeUsd);
88
380
  const eff = grossCollateralUsd -
89
381
  borrowFeeUsd -
90
382
  tradingFeeUsd -
@@ -128,9 +420,42 @@ export function calcEffectiveCollateralUsd(params) {
128
420
  * @param closingFeeUsd Full closing fee in USD (`close_fee` → USD).
129
421
  * @param collateralPriceUsd Oracle price of the collateral token (USD per token).
130
422
  * @param collateralDecimal Collateral token decimals — sets the smallest withdraw step.
423
+ * @throws RangeError when any USD / size / price input is not a finite `>= 0`
424
+ * number, when `maintenanceMarginRate` is not finite inside `[0, 1]`, or when
425
+ * `collateralDecimal` is not an integer in `[0, 19]` — the same domains
426
+ * `calcEstLiqPrice` / `calcEstLiqPriceRaw` enforce. Two of these are the
427
+ * reason the whole set is here: a negative fee silently INFLATES the
428
+ * withdrawable amount, and a negative `collateralPriceUsd` drops the
429
+ * one-raw-unit liquidation back-off to 0 — removing the abort-safety margin
430
+ * while still returning a plausible-looking dollar figure.
431
+ * (`maxLeverage === 0` → no leverage cap and `collateralPriceUsd === 0` → no
432
+ * back-off stay documented domain zeros, not garbage.)
131
433
  */
132
434
  export function calcMaxReducibleCollateralUsd(params) {
133
435
  const { grossCollateralUsd, sizeInAsset, spotPrice, isLong, entryPrice, maxLeverage, maintenanceMarginRate, minCollValueUsd, borrowFeeUsd, tradingFeeUsd, closingFeeUsd, fundingSign, fundingFeeUsd, collateralPriceUsd, collateralDecimal, } = params;
436
+ // MONEY PATH (see the validation policy at the top of this file): every
437
+ // numeric input is pinned to the same domains calcEstLiqPrice uses. Fees are
438
+ // UNSIGNED here — funding's sign travels separately in `fundingSign` — so a
439
+ // negative one would silently INFLATE the withdrawable amount. One loop so
440
+ // the guard labels cannot drift from the field names; the grossCollateral /
441
+ // borrow / trading legs are re-checked inside calcEffectiveCollateralUsd.
442
+ for (const [label, value] of Object.entries({
443
+ grossCollateralUsd,
444
+ sizeInAsset,
445
+ spotPrice,
446
+ entryPrice,
447
+ maxLeverage,
448
+ minCollValueUsd,
449
+ borrowFeeUsd,
450
+ tradingFeeUsd,
451
+ closingFeeUsd,
452
+ fundingFeeUsd,
453
+ collateralPriceUsd,
454
+ })) {
455
+ assertFiniteNonNegative(label, value);
456
+ }
457
+ assertUnitFraction("maintenanceMarginRate", maintenanceMarginRate);
458
+ assertTokenDecimal("collateralDecimal", collateralDecimal);
134
459
  const notional = sizeInAsset * spotPrice;
135
460
  // effLeverage: matches calculate_effective_collateral_amount(..., projectedTradingFee = 0).
136
461
  const effLeverage = calcEffectiveCollateralUsd({
@@ -286,8 +611,11 @@ export function calcBorrowRate(utilizationBps, rate0, rate1, rate2, threshold0Bp
286
611
  /**
287
612
  * Time-weighted borrow rate accrual for a given elapsed period.
288
613
  *
289
- * Matches `calculate_borrow_rate_accrual` in `lp_pool.move`.
290
- * `elapsedMs / intervalMs` gives the number of completed intervals.
614
+ * Continuous proration: `rate × elapsedMs / intervalMs`, matching the formula
615
+ * of `lp_pool.move::calculate_borrow_rate_accrual`
616
+ * (`borrow_rate.mul_u64(elapsed_ms).div_u64(interval_ms)`) — the contract does
617
+ * NOT floor to completed intervals; a partial interval accrues pro rata
618
+ * (verified against the Move source 2026-07-29).
291
619
  */
292
620
  export function calcBorrowRateAccrual(borrowRate, elapsedMs, intervalMs) {
293
621
  if (borrowRate === 0 || elapsedMs === 0 || intervalMs === 0)
@@ -332,13 +660,17 @@ export function annualizeFundingRate(rate, intervalMs) {
332
660
  * Annualized APY from a NAV ratio over a given number of days.
333
661
  *
334
662
  * Compounds `ratio` (WLP price now / WLP price past) to a 365-day return.
335
- * Returns 0 when the result is not finite (e.g. ratio 0 or days = 0).
663
+ * Documented domain cases returning 0: `ratio <= 0` or `days <= 0` (no valid
664
+ * sample window), and an overflowing compound result.
336
665
  *
337
666
  * @param ratio Current NAV divided by past NAV (e.g. 1.05 for 5% growth).
338
667
  * @param days Number of days elapsed between the two NAV samples.
668
+ * @throws RangeError when `ratio` or `days` is NaN / ±Infinity.
339
669
  */
340
670
  export function annualizedApyFromRatio(ratio, days) {
341
- if (days === 0 || ratio <= 0)
671
+ assertFinite("ratio", ratio);
672
+ assertFinite("days", days);
673
+ if (days <= 0 || ratio <= 0)
342
674
  return 0;
343
675
  const apy = Math.pow(ratio, 365 / days) - 1;
344
676
  return Number.isFinite(apy) ? apy : 0;
@@ -347,11 +679,13 @@ export function annualizedApyFromRatio(ratio, days) {
347
679
  * Convert a continuously-compounded incentive APR to APY.
348
680
  *
349
681
  * Rewards stream via `flow_rate` (continuous compounding), so APY = e^APR − 1.
350
- * Returns 0 when the result is not finite.
682
+ * Returns 0 when the compound result overflows to Infinity.
351
683
  *
352
684
  * @param apr Time-weighted incentive APR as a decimal fraction (e.g. 0.12 for 12%).
685
+ * @throws RangeError when `apr` is NaN / ±Infinity.
353
686
  */
354
687
  export function calcWlpIncentiveApy(apr) {
688
+ assertFinite("apr", apr);
355
689
  const apy = Math.expm1(apr);
356
690
  return Number.isFinite(apy) ? apy : 0;
357
691
  }
@@ -373,17 +707,32 @@ export function calcWlpPrice(tvlUsd, totalSupply, lpDecimals) {
373
707
  *
374
708
  * Matches the LP-amount formula in `mint_wlp_with_pricing_tvl` in `lp_pool.move`.
375
709
  * Pass `netDepositUsd` (after the dynamic mint fee is deducted).
376
- * Bootstrap path (totalSupply === 0): lpAmount = netDepositUsd × 10^lpDecimals.
710
+ *
711
+ * Bootstrap ($1/share par, `lpAmount = netDepositUsd × 10^lpDecimals`) applies
712
+ * ONLY to the genuine first mint (`totalSupply === 0`) — exactly like the
713
+ * chain. When supply is outstanding but the priced TVL has floored to 0 (e.g.
714
+ * trader unrealized profit ≥ TVL drove the equity `saturating_sub` to 0) the
715
+ * chain ABORTS `EInvalidBootstrap` rather than par-minting cheap shares that
716
+ * dilute existing LPs (re-audit F-023); this helper throws `RangeError` in
717
+ * that state instead of silently par-quoting.
377
718
  *
378
719
  * @param netDepositUsd Deposit value in USD after dynamic mint fee.
379
720
  * @param tvlUsd Pool TVL in USD at pricing time.
380
721
  * @param totalSupply Current total WLP supply in raw units.
381
722
  * @param lpDecimals WLP token decimals (6).
723
+ * @throws RangeError when inputs are not finite `>= 0` numbers, or on the
724
+ * `totalSupply > 0 && tvlUsd === 0` state (chain aborts `EInvalidBootstrap`).
382
725
  */
383
726
  export function calcWlpMintOut(netDepositUsd, tvlUsd, totalSupply, lpDecimals) {
727
+ assertFiniteNonNegative("netDepositUsd", netDepositUsd);
728
+ assertFiniteNonNegative("tvlUsd", tvlUsd);
729
+ assertFiniteNonNegative("totalSupply", totalSupply);
384
730
  const scale = Math.pow(10, lpDecimals);
385
- if (totalSupply === 0 || tvlUsd === 0)
731
+ if (totalSupply === 0)
386
732
  return Math.floor(netDepositUsd * scale);
733
+ if (tvlUsd === 0) {
734
+ throw new RangeError("calcWlpMintOut: totalSupply > 0 with tvlUsd === 0 — the chain aborts EInvalidBootstrap here (par-minting against zero priced TVL dilutes existing LPs; F-023). Retry once the equity snapshot recovers.");
735
+ }
387
736
  return Math.floor((netDepositUsd * totalSupply) / tvlUsd);
388
737
  }
389
738
  /**
@@ -392,13 +741,27 @@ export function calcWlpMintOut(netDepositUsd, tvlUsd, totalSupply, lpDecimals) {
392
741
  * Matches the settlement formula in `settle_redeem_with_pricing_tvl` in `lp_pool.move`.
393
742
  * Apply `calcDynamicFeeBps` separately to get the net output.
394
743
  *
744
+ * Chain-divergence notes (display-convenience helper, not settlement-exact):
745
+ * - `tokenPriceUsd === 0` returns 0 here, but the chain ABORTS (`EZeroPrice`,
746
+ * lp_pool.move) — a zero from this helper on that input is a display
747
+ * placeholder, not a real quote.
748
+ * - The burn fee is applied on-chain as
749
+ * `float::from_bps(fee_bps).mul_u64(raw_amount).ceil()` — composing this
750
+ * helper with `calcDynamicFeeBps` in f64 (which cannot `.ceil()` on the raw
751
+ * grid) can drift ±1 raw unit from the settled output.
752
+ *
395
753
  * @param lpAmount LP tokens being redeemed (raw units).
396
754
  * @param tvlUsd Pool TVL in USD at pricing time.
397
755
  * @param totalSupply Current total WLP supply in raw units.
398
756
  * @param tokenPriceUsd Oracle price of the output token.
399
757
  * @param tokenDecimals Output token decimals.
758
+ * @throws RangeError when any numeric input is not a finite `>= 0` number.
400
759
  */
401
760
  export function calcWlpRedeemOut(lpAmount, tvlUsd, totalSupply, tokenPriceUsd, tokenDecimals) {
761
+ assertFiniteNonNegative("lpAmount", lpAmount);
762
+ assertFiniteNonNegative("tvlUsd", tvlUsd);
763
+ assertFiniteNonNegative("totalSupply", totalSupply);
764
+ assertFiniteNonNegative("tokenPriceUsd", tokenPriceUsd);
402
765
  if (totalSupply === 0 || tokenPriceUsd === 0)
403
766
  return 0;
404
767
  const burnValueUsd = (tvlUsd * lpAmount) / totalSupply;
@@ -410,7 +773,8 @@ export function calcWlpRedeemOut(lpAmount, tvlUsd, totalSupply, tokenPriceUsd, t
410
773
  * Matches `calculate_dynamic_fee` in `lp_pool.move`. Returns `baseFeeBps` when
411
774
  * the operation moves the token closer to (or does not worsen) its target weight.
412
775
  * Adds an additional fee proportional to the average deviation when it moves
413
- * further away.
776
+ * further away. Clamped to 100% (`bp_scale`) on both the additional term and
777
+ * the total, mirroring the on-chain F-039 clamp.
414
778
  *
415
779
  * @param tokenValueUsd Current USD value of this token in the pool.
416
780
  * @param tvlUsd Total pool TVL in USD.
@@ -418,8 +782,16 @@ export function calcWlpRedeemOut(lpAmount, tvlUsd, totalSupply, tokenPriceUsd, t
418
782
  * @param targetWeightBps Target allocation weight for this token (bps).
419
783
  * @param baseFeeBps Base mint/burn fee (bps).
420
784
  * @param isDeposit True for mint, false for redeem.
785
+ * @throws RangeError when any USD / bps input is not a finite `>= 0` number.
786
+ * (`tvlUsd` / `operationValueUsd` / `targetWeightBps` of exactly 0 stay
787
+ * documented domain cases returning `baseFeeBps` — nothing to deviate from.)
421
788
  */
422
789
  export function calcDynamicFeeBps(tokenValueUsd, tvlUsd, operationValueUsd, targetWeightBps, baseFeeBps, isDeposit) {
790
+ assertFiniteNonNegative("tokenValueUsd", tokenValueUsd);
791
+ assertFiniteNonNegative("tvlUsd", tvlUsd);
792
+ assertFiniteNonNegative("operationValueUsd", operationValueUsd);
793
+ assertFiniteNonNegative("targetWeightBps", targetWeightBps);
794
+ assertFiniteNonNegative("baseFeeBps", baseFeeBps);
423
795
  if (tvlUsd === 0 || operationValueUsd === 0 || targetWeightBps === 0)
424
796
  return baseFeeBps;
425
797
  const targetValue = (tvlUsd * targetWeightBps) / Number(BPS_SCALE);
@@ -439,5 +811,10 @@ export function calcDynamicFeeBps(tokenValueUsd, tvlUsd, operationValueUsd, targ
439
811
  if (avgTargetValue === 0)
440
812
  return baseFeeBps;
441
813
  const additional = Math.floor((avgDiff / avgTargetValue) * baseFeeBps);
442
- return baseFeeBps + additional;
814
+ // F-039 (lp_pool.move::calculate_dynamic_fee): clamp the dynamic fee to 100%.
815
+ // Both the additional term and the total are capped at bp_scale, mirroring
816
+ // `(base_fee_bps + additional.min(bp_scale)).min(bp_scale)` — without it the
817
+ // fee could exceed the operation amount and the on-chain `amount - fee`
818
+ // subtraction would abort.
819
+ return Math.min(baseFeeBps + Math.min(additional, Number(BPS_SCALE)), Number(BPS_SCALE));
443
820
  }
@@ -0,0 +1,69 @@
1
+ import type { TransactionArgument } from "@mysten/sui/transactions";
2
+ export declare const U64_MAX = 18446744073709551615n;
3
+ /** Validate a u64 param (bigint passthrough with range check; number must be a safe integer >= 0). */
4
+ export declare function toU64(value: bigint | number, label: string): bigint;
5
+ /** Validate a u128 param (bigint passthrough with range check; number must be a safe integer >= 0). */
6
+ export declare function toU128(value: bigint | number, label: string): bigint;
7
+ /**
8
+ * Validate a u8 param. Returns a `number` (not a bigint) because that is the
9
+ * shape the generated BCS layer takes at this width.
10
+ *
11
+ * Separate from `toU64` on purpose: at u8/u16 the BCS writer already throws on
12
+ * an out-of-range value, but SILENTLY TRUNCATES a fractional one — so the
13
+ * fractional case is what this guard exists to catch (see the header note).
14
+ */
15
+ export declare function toU8(value: number, label: string): number;
16
+ /** Validate a u16 param. Same width caveat as {@link toU8}. */
17
+ export declare function toU16(value: number, label: string): number;
18
+ /** `Option<u64>` param: `null` / `undefined` pass through as `null`, anything else is validated. */
19
+ export declare function toU64OrNull(value: bigint | number | null | undefined, label: string): bigint | null;
20
+ /** `Option<u128>` param: `null` / `undefined` pass through as `null`, anything else is validated. */
21
+ export declare function toU128OrNull(value: bigint | number | null | undefined, label: string): bigint | null;
22
+ /**
23
+ * u64 param that may instead be a PTB result chained from an earlier command
24
+ * (e.g. the `lp_amount` returned by `mintWlp`). A `TransactionArgument` passes
25
+ * through untouched — its value only exists on chain, where Move types it.
26
+ */
27
+ export declare function toU64Arg(value: bigint | number | TransactionArgument, label: string): bigint | TransactionArgument;
28
+ /** Reject NaN / ±Infinity for a `number` money-path input. */
29
+ export declare function assertFinite(label: string, value: number): void;
30
+ /** Reject NaN / ±Infinity / negative for a `number` money-path input. */
31
+ export declare function assertFiniteNonNegative(label: string, value: number): void;
32
+ /**
33
+ * Reject anything outside `[0, 1]` for a `number` input that is a FRACTION of a
34
+ * whole — a maintenance-margin rate above 1 means "maintenance exceeds the
35
+ * entire notional", which is not a rate the caller can have meant.
36
+ */
37
+ export declare function assertUnitFraction(label: string, value: number): void;
38
+ /**
39
+ * Reject a token-decimal outside `[0, 19]` — the domain of the contract's
40
+ * `10u64.pow(decimal)` (`10^19 < 2^64 <= 10^20`), and the same bound the raw
41
+ * `POW10` table in `utils/math.ts` is built over.
42
+ */
43
+ export declare function assertTokenDecimal(label: string, value: number): void;
44
+ /** Reject a negative `bigint` where the on-chain type is unsigned. */
45
+ export declare function assertUnsignedBigInt(label: string, value: bigint): void;
46
+ /**
47
+ * WHOLE-DOLLAR integer USD price for the `waterx_perp_view` read params
48
+ * (`80000n` for BTC at $80k). The Move view applies `float::from(...)`
49
+ * internally — do NOT pass a 1e9-scaled `rawPrice()` value or every
50
+ * pnl/notional-derived field inflates by 1e9 (`rawPrice()` is for tx-build
51
+ * args only; the u128 order-book `triggerPrice` key on `getOrder` is the one
52
+ * view param that still takes that scale). Sub-$1 prices cannot be
53
+ * represented (u64 whole dollars). The price only bases the
54
+ * pnl / close-fee / notional-derived fields — `0n` zero-bases them (still
55
+ * computed with price 0, not skipped).
56
+ */
57
+ export type WholeDollarUsdPrice = bigint | number;
58
+ /**
59
+ * Parse a user/env-supplied value into a whole-dollar u64 price
60
+ * (`WholeDollarUsdPrice`) with NO silent rounding: throws `RangeError` on
61
+ * fractional, negative, non-finite, non-numeric, or `> u64::MAX` input. If
62
+ * rounding is ever wanted it must be explicit at the call site
63
+ * (e.g. `parseWholeDollarU64(Math.round(x))`) — never baked in here.
64
+ *
65
+ * The numeric domain (finite → safe integer → non-negative → `<= u64::MAX`) is
66
+ * `toU64`'s and is NOT restated here; this function only adds the string form,
67
+ * whose digits parse exactly past the 2^53 f64 cliff.
68
+ */
69
+ export declare function parseWholeDollarU64(value: string | number): bigint;