@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.
- package/README.md +37 -17
- package/dist/cjs/src/account/account.js +2 -1
- package/dist/cjs/src/account/config.d.ts +3 -2
- package/dist/cjs/src/account/funding/balance.d.ts +25 -2
- package/dist/cjs/src/account/funding/balance.js +36 -24
- package/dist/cjs/src/account/funding/credit.js +6 -10
- package/dist/cjs/src/constants.d.ts +15 -1
- package/dist/cjs/src/constants.js +18 -4
- package/dist/cjs/src/generated/waterx_rule/deps/bucket_v2_framework/float.d.ts +8 -0
- package/dist/cjs/src/generated/waterx_rule/deps/bucket_v2_framework/float.js +13 -0
- package/dist/cjs/src/generated/waterx_rule/deps/sui/vec_map.d.ts +36 -0
- package/dist/cjs/src/generated/waterx_rule/deps/sui/vec_map.js +31 -0
- package/dist/cjs/src/generated/waterx_rule/waterx_rule.d.ts +813 -0
- package/dist/cjs/src/generated/waterx_rule/waterx_rule.js +946 -0
- package/dist/cjs/src/oracle/aggregate.d.ts +23 -21
- package/dist/cjs/src/oracle/aggregate.js +133 -59
- package/dist/cjs/src/oracle/config.d.ts +82 -41
- package/dist/cjs/src/oracle/config.js +0 -42
- package/dist/cjs/src/oracle/host.d.ts +26 -11
- package/dist/cjs/src/oracle/index.d.ts +6 -2
- package/dist/cjs/src/oracle/index.js +36 -8
- package/dist/cjs/src/oracle/price-update-rule.d.ts +16 -10
- package/dist/cjs/src/oracle/price-update-rule.js +3 -3
- package/dist/cjs/src/oracle/pyth.d.ts +44 -0
- package/dist/cjs/src/oracle/pyth.js +48 -6
- package/dist/cjs/src/oracle/read-plane.d.ts +70 -0
- package/dist/cjs/src/oracle/read-plane.js +78 -0
- package/dist/cjs/src/oracle/rule-registry.d.ts +14 -11
- package/dist/cjs/src/oracle/rule-registry.js +16 -11
- package/dist/cjs/src/oracle/rules/pyth-core-rule.js +4 -4
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.d.ts +21 -0
- package/dist/cjs/src/oracle/rules/pyth-lazer-rule.js +29 -4
- package/dist/cjs/src/oracle/rules/pyth-rule.js +5 -4
- package/dist/cjs/src/oracle/rules/waterx-rule.d.ts +126 -0
- package/dist/cjs/src/oracle/rules/waterx-rule.js +312 -0
- package/dist/cjs/src/perp/client.d.ts +71 -20
- package/dist/cjs/src/perp/client.js +35 -13
- package/dist/cjs/src/perp/config.d.ts +6 -7
- package/dist/cjs/src/perp/config.js +12 -11
- package/dist/cjs/src/perp/constants.d.ts +0 -6
- package/dist/cjs/src/perp/constants.js +11 -9
- package/dist/cjs/src/perp/fetch/account.js +3 -0
- package/dist/cjs/src/perp/fetch/bridge.js +2 -1
- package/dist/cjs/src/perp/fetch/market.js +2 -1
- package/dist/cjs/src/perp/fetch/positions.d.ts +16 -10
- package/dist/cjs/src/perp/fetch/positions.js +23 -20
- package/dist/cjs/src/perp/index.d.ts +9 -5
- package/dist/cjs/src/perp/index.js +16 -7
- package/dist/cjs/src/perp/liq-view.d.ts +64 -0
- package/dist/cjs/src/perp/liq-view.js +74 -0
- package/dist/cjs/src/perp/user/order.d.ts +13 -0
- package/dist/cjs/src/perp/user/order.js +30 -16
- package/dist/cjs/src/perp/user/staking.js +3 -2
- package/dist/cjs/src/perp/user/trading.js +25 -24
- package/dist/cjs/src/perp/user/wlp.js +6 -5
- package/dist/cjs/src/prediction/utils.d.ts +11 -2
- package/dist/cjs/src/prediction/utils.js +22 -22
- package/dist/cjs/src/unified-client.d.ts +41 -15
- package/dist/cjs/src/unified-client.js +4 -2
- package/dist/cjs/src/utils/format.d.ts +14 -0
- package/dist/cjs/src/utils/format.js +24 -0
- package/dist/cjs/src/utils/math.d.ts +304 -12
- package/dist/cjs/src/utils/math.js +397 -17
- package/dist/cjs/src/utils/validate.d.ts +69 -0
- package/dist/cjs/src/utils/validate.js +183 -0
- package/dist/src/account/account.js +2 -1
- package/dist/src/account/config.d.ts +3 -2
- package/dist/src/account/funding/balance.d.ts +25 -2
- package/dist/src/account/funding/balance.js +36 -24
- package/dist/src/account/funding/credit.js +6 -10
- package/dist/src/constants.d.ts +15 -1
- package/dist/src/constants.js +17 -3
- package/dist/src/generated/waterx_rule/deps/bucket_v2_framework/float.d.ts +8 -0
- package/dist/src/generated/waterx_rule/deps/bucket_v2_framework/float.js +10 -0
- package/dist/src/generated/waterx_rule/deps/sui/vec_map.d.ts +36 -0
- package/dist/src/generated/waterx_rule/deps/sui/vec_map.js +27 -0
- package/dist/src/generated/waterx_rule/waterx_rule.d.ts +813 -0
- package/dist/src/generated/waterx_rule/waterx_rule.js +855 -0
- package/dist/src/oracle/aggregate.d.ts +23 -21
- package/dist/src/oracle/aggregate.js +133 -59
- package/dist/src/oracle/config.d.ts +82 -41
- package/dist/src/oracle/config.js +1 -41
- package/dist/src/oracle/host.d.ts +26 -11
- package/dist/src/oracle/index.d.ts +6 -2
- package/dist/src/oracle/index.js +27 -9
- package/dist/src/oracle/price-update-rule.d.ts +16 -10
- package/dist/src/oracle/price-update-rule.js +3 -3
- package/dist/src/oracle/pyth.d.ts +44 -0
- package/dist/src/oracle/pyth.js +45 -5
- package/dist/src/oracle/read-plane.d.ts +70 -0
- package/dist/src/oracle/read-plane.js +74 -0
- package/dist/src/oracle/rule-registry.d.ts +14 -11
- package/dist/src/oracle/rule-registry.js +16 -11
- package/dist/src/oracle/rules/pyth-core-rule.js +5 -5
- package/dist/src/oracle/rules/pyth-lazer-rule.d.ts +21 -0
- package/dist/src/oracle/rules/pyth-lazer-rule.js +28 -3
- package/dist/src/oracle/rules/pyth-rule.js +5 -4
- package/dist/src/oracle/rules/waterx-rule.d.ts +126 -0
- package/dist/src/oracle/rules/waterx-rule.js +305 -0
- package/dist/src/perp/client.d.ts +71 -20
- package/dist/src/perp/client.js +36 -14
- package/dist/src/perp/config.d.ts +6 -7
- package/dist/src/perp/config.js +11 -9
- package/dist/src/perp/constants.d.ts +0 -6
- package/dist/src/perp/constants.js +10 -8
- package/dist/src/perp/fetch/account.js +3 -0
- package/dist/src/perp/fetch/bridge.js +2 -1
- package/dist/src/perp/fetch/market.js +2 -1
- package/dist/src/perp/fetch/positions.d.ts +16 -10
- package/dist/src/perp/fetch/positions.js +28 -20
- package/dist/src/perp/index.d.ts +9 -5
- package/dist/src/perp/index.js +6 -4
- package/dist/src/perp/liq-view.d.ts +64 -0
- package/dist/src/perp/liq-view.js +71 -0
- package/dist/src/perp/user/order.d.ts +13 -0
- package/dist/src/perp/user/order.js +30 -16
- package/dist/src/perp/user/staking.js +3 -2
- package/dist/src/perp/user/trading.js +25 -24
- package/dist/src/perp/user/wlp.js +6 -5
- package/dist/src/prediction/utils.d.ts +11 -2
- package/dist/src/prediction/utils.js +22 -22
- package/dist/src/unified-client.d.ts +41 -15
- package/dist/src/unified-client.js +4 -2
- package/dist/src/utils/format.d.ts +14 -0
- package/dist/src/utils/format.js +21 -0
- package/dist/src/utils/math.d.ts +304 -12
- package/dist/src/utils/math.js +394 -17
- package/dist/src/utils/validate.d.ts +69 -0
- package/dist/src/utils/validate.js +167 -0
- package/package.json +4 -1
package/dist/src/utils/math.js
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
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(
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
290
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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;
|