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