@xoxno/sdk-js 1.0.224 → 1.0.225
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/dist/esm/index.js +3 -0
- package/dist/esm/sdk/index.js +198 -0
- package/dist/esm/sdk/stellar/admin.js +385 -0
- package/dist/esm/sdk/stellar/auth-policy.js +310 -0
- package/dist/esm/sdk/stellar/cctp.js +23 -0
- package/dist/esm/sdk/stellar/contracts.js +49 -0
- package/dist/esm/sdk/stellar/events/decode.js +415 -0
- package/dist/esm/sdk/stellar/events/id.js +62 -0
- package/dist/esm/sdk/stellar/events/index.js +2 -0
- package/dist/esm/sdk/stellar/governance.js +403 -0
- package/dist/esm/sdk/stellar/index.js +25 -0
- package/dist/esm/sdk/stellar/lending-api-types.js +3 -0
- package/dist/esm/sdk/stellar/lending-read-types.js +1 -0
- package/dist/esm/sdk/stellar/lending-read.js +341 -0
- package/dist/esm/sdk/stellar/lending.js +358 -0
- package/dist/esm/sdk/stellar/math/account.js +256 -0
- package/dist/esm/sdk/stellar/math/index.js +11 -0
- package/dist/esm/sdk/stellar/math/limits.js +273 -0
- package/dist/esm/sdk/stellar/math/projection.js +130 -0
- package/dist/esm/sdk/stellar/math/rates.js +62 -0
- package/dist/esm/sdk/stellar/math/scaled.js +156 -0
- package/dist/esm/sdk/stellar/position-mode.js +11 -0
- package/dist/esm/sdk/stellar/prepare.js +47 -0
- package/dist/esm/sdk/stellar/quote.js +87 -0
- package/dist/esm/sdk/stellar/repay-swap.js +12 -0
- package/dist/esm/sdk/stellar/route-verify.js +285 -0
- package/dist/esm/sdk/stellar/scval-encode.js +464 -0
- package/dist/esm/sdk/stellar/swap-history.js +46 -0
- package/dist/esm/sdk/stellar/swap-index.js +1 -0
- package/dist/esm/sdk/stellar/swap.js +117 -0
- package/dist/esm/sdk/stellar/xdr-compat.js +17 -0
- package/dist/esm/sdk/stellar-schema.js +129 -0
- package/dist/esm/sdk/swagger.js +1957 -0
- package/dist/esm/sdk/types.js +1 -0
- package/dist/esm/sdk/utils.js +21 -0
- package/dist/esm/utils/api.js +169 -0
- package/dist/esm/utils/const.js +11 -0
- package/dist/esm/utils/errors.js +20 -0
- package/dist/esm/utils/guards.js +29 -0
- package/dist/esm/utils/helpers.js +20 -0
- package/dist/esm/utils/regex.js +14 -0
- package/dist/esm/utils/soroban-errors.js +149 -0
- package/dist/index.esm.js +1 -2
- package/dist/sdk/stellar/index.esm.js +1 -1
- package/package.json +2 -2
- package/dist/index.esm.js.LICENSE.txt +0 -8
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Largest valid amount per action, in token base units, with the reason that
|
|
3
|
+
* binds. Mirrors the gates the contract enforces rather than capping:
|
|
4
|
+
*
|
|
5
|
+
* - risk gate: `contracts/controller/src/risk/validation.rs`
|
|
6
|
+
* `require_post_pool_risk_gates` (debt ≤ `min(LTV, threshold)`-weighted
|
|
7
|
+
* collateral, HF ≥ 1, `ltv_collateral ≥ min_borrow_collateral_usd`);
|
|
8
|
+
* - listing flags: `positions/mod.rs::enforce_spoke_asset_flags`
|
|
9
|
+
* (`paused` blocks every verb, `frozen` blocks entry only);
|
|
10
|
+
* - spoke caps: `formulas.md` §Caps (native units, no unlimited sentinel);
|
|
11
|
+
* - pool cash and the 200 BPS liquidation buffer:
|
|
12
|
+
* `contracts/pool/src/guards.rs::require_liquidation_buffer`;
|
|
13
|
+
* - hub utilization ceiling: `guards.rs::require_utilization_below_max`
|
|
14
|
+
* (skipped when `max_utilization >= RAY`; repay and liquidation are exempt).
|
|
15
|
+
*
|
|
16
|
+
* Hub totals and pool cash come from the `*Short` display numbers on
|
|
17
|
+
* `ReserveDto`, so cap / liquidity / utilization candidates are exact only to
|
|
18
|
+
* the double precision of those fields; the risk-gate candidate is exact.
|
|
19
|
+
* xoxno-ui parity: `stellar-limits.ts::computeStellarLimits`,
|
|
20
|
+
* `utils.ts::calculateMaxBorrow / calculateMaxSupply / calculateMaxRemove`.
|
|
21
|
+
*/ import { buildLiveSnapshot, burnShares, liveIndexKey, mintShares } from "./account.js";
|
|
22
|
+
import { utilizationRay } from "./rates.js";
|
|
23
|
+
import { BPS, RAY, baseToRay, baseToShort, mulDivCeil, mulDivFloor, mulDivHalfUp, numberToScaled, numberToWad, positionValueWad, toBigInt, unscale } from "./scaled.js";
|
|
24
|
+
/** `common/src/constants/pool.rs::LIQUIDATION_BUFFER_BPS`. */ export const LIQUIDATION_BUFFER_BPS = 200n;
|
|
25
|
+
const pickMin = (candidates)=>candidates.reduce((best, c)=>c.amount < best.amount ? c : best);
|
|
26
|
+
const nonNegative = (v)=>v < 0n ? 0n : v;
|
|
27
|
+
const finish = (chosen, decimals, isFullClose, projectedUtilization)=>({
|
|
28
|
+
amountBase: chosen.amount,
|
|
29
|
+
amountShort: baseToShort(chosen.amount, decimals),
|
|
30
|
+
isFullClose,
|
|
31
|
+
reason: chosen.reason,
|
|
32
|
+
projectedUtilization
|
|
33
|
+
});
|
|
34
|
+
const zero = (reason)=>({
|
|
35
|
+
amountBase: 0n,
|
|
36
|
+
amountShort: 0,
|
|
37
|
+
isFullClose: false,
|
|
38
|
+
reason,
|
|
39
|
+
projectedUtilization: null
|
|
40
|
+
});
|
|
41
|
+
/**
|
|
42
|
+
* Reserve totals in base units from the API's display numbers.
|
|
43
|
+
*
|
|
44
|
+
* Assumption (Unverified against the API source): `availableLiquidityShort`
|
|
45
|
+
* is the hub pool's cash (`pool/cache/mod.rs::cash()`), the base both
|
|
46
|
+
* `require_reserves` and the liquidation buffer are checked against. The DTO
|
|
47
|
+
* describes it only as "Available liquidity / cash" without a scope marker
|
|
48
|
+
* while `suppliedShort` / `borrowedShort` are marked spoke-scoped and
|
|
49
|
+
* `HubPoolDto` carries no cash field; if the API serves it spoke-scoped the
|
|
50
|
+
* `liquidity` candidates are computed against the wrong base.
|
|
51
|
+
*/ function hubTotals(reserve) {
|
|
52
|
+
const d = reserve.assetDecimals;
|
|
53
|
+
const maxUtil = toBigInt(reserve.irm.maxUtilizationRay, 'irm.maxUtilizationRay');
|
|
54
|
+
return {
|
|
55
|
+
suppliedBase: numberToScaled(reserve.hubPool.suppliedShort, d),
|
|
56
|
+
borrowedBase: numberToScaled(reserve.hubPool.borrowedShort, d),
|
|
57
|
+
cashBase: numberToScaled(reserve.availableLiquidityShort, d),
|
|
58
|
+
spokeSuppliedBase: numberToScaled(reserve.suppliedShort, d),
|
|
59
|
+
spokeBorrowedBase: numberToScaled(reserve.borrowedShort, d),
|
|
60
|
+
maxUtilizationRay: maxUtil >= RAY ? null : maxUtil
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const projectedUtil = (suppliedBase, borrowedBase)=>suppliedBase <= 0n ? null : Number(borrowedBase) / Number(suppliedBase);
|
|
64
|
+
/** Price the reserve the way `computeAccountRisk` would: accepted live row, else `reserve.usdPrice`. */ function reservePriceWad(reserve, liveState) {
|
|
65
|
+
return buildLiveSnapshot(liveState).prices.get(reserve.asset) ?? numberToWad(reserve.usdPrice);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Largest `x` in `[0, hi]` with `ok(x)` true, for a predicate that is true on
|
|
69
|
+
* a prefix (every gate here is monotone in the amount). Binary search, so the
|
|
70
|
+
* estimate feeding `hi` only needs to be an upper bound: rounding slack in the
|
|
71
|
+
* gates is not bounded in base units (an 18-decimal token at $0.01 needs
|
|
72
|
+
* hundreds of single-unit steps), so no fixed decrement count is safe.
|
|
73
|
+
*/ function fitMax(hi, ok) {
|
|
74
|
+
if (hi <= 0n || !ok(0n)) return 0n;
|
|
75
|
+
let lo = 0n // ok
|
|
76
|
+
;
|
|
77
|
+
let bad = hi + 1n;
|
|
78
|
+
while(bad - lo > 1n){
|
|
79
|
+
const mid = (lo + bad) / 2n;
|
|
80
|
+
if (ok(mid)) lo = mid;
|
|
81
|
+
else bad = mid;
|
|
82
|
+
}
|
|
83
|
+
return lo;
|
|
84
|
+
}
|
|
85
|
+
/** Slack added to a rounding-free estimate before searching, so the exact answer is never above `hi`. */ const ESTIMATE_SLACK = 16n;
|
|
86
|
+
/** Largest borrow keeping `half_up(borrowed / supplied) <= maxUtilization`; null when the guard does not apply. */ function utilizationBorrowCap(totals, decimals) {
|
|
87
|
+
const max = totals.maxUtilizationRay;
|
|
88
|
+
if (max === null) return null;
|
|
89
|
+
const suppliedRay = baseToRay(totals.suppliedBase, decimals);
|
|
90
|
+
// `require_utilization_below_max` returns early with no supply; cash then binds.
|
|
91
|
+
if (suppliedRay === 0n) return null;
|
|
92
|
+
const estimate = nonNegative(mulDivFloor(totals.suppliedBase, max, RAY) - totals.borrowedBase);
|
|
93
|
+
return fitMax(estimate + ESTIMATE_SLACK, (x)=>utilizationRay(baseToRay(totals.borrowedBase + x, decimals), suppliedRay) <= max);
|
|
94
|
+
}
|
|
95
|
+
/** Largest withdraw keeping `half_up(borrowed / (supplied - x)) <= maxUtilization`; null when unbounded. */ function utilizationWithdrawCap(totals, decimals) {
|
|
96
|
+
const max = totals.maxUtilizationRay;
|
|
97
|
+
if (max === null || totals.borrowedBase === 0n) return null;
|
|
98
|
+
if (max === 0n) return 0n;
|
|
99
|
+
const minSupplied = mulDivCeil(totals.borrowedBase, RAY, max);
|
|
100
|
+
const hi = totals.suppliedBase < minSupplied ? 0n : totals.suppliedBase - minSupplied + ESTIMATE_SLACK;
|
|
101
|
+
const borrowedRay = baseToRay(totals.borrowedBase, decimals);
|
|
102
|
+
return fitMax(hi > totals.suppliedBase ? totals.suppliedBase : hi, (x)=>{
|
|
103
|
+
const remaining = baseToRay(totals.suppliedBase - x, decimals);
|
|
104
|
+
return remaining > 0n && utilizationRay(borrowedRay, remaining) <= max;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
const findPosition = (list, reserve)=>list.find((p)=>p.hubId === reserve.hubId && p.asset === reserve.asset);
|
|
108
|
+
/**
|
|
109
|
+
* Largest borrow of `reserve` the account can open now. Candidates, all in
|
|
110
|
+
* base units:
|
|
111
|
+
*
|
|
112
|
+
* - LTV gate: the contract mints `calculate_scaled_borrow` (ceil) shares,
|
|
113
|
+
* merges them into the existing debt leg and requires
|
|
114
|
+
* `ltv_collateral >= total_debt` with the merged leg valued by
|
|
115
|
+
* `position_value_ceil` (`ops/borrow.rs::mint_debt`, `risk/totals.rs`,
|
|
116
|
+
* `risk/validation.rs`). The search runs on exactly that predicate, with
|
|
117
|
+
* the index and price `computeAccountRisk` used for the leg (else the
|
|
118
|
+
* accepted live row, else `reserve.liveBorrowIndexRay` / `reserve.usdPrice`).
|
|
119
|
+
* - min-collateral floor, spoke borrow-cap headroom, pool cash minus the
|
|
120
|
+
* liquidation buffer, and the hub utilization ceiling.
|
|
121
|
+
*
|
|
122
|
+
* A reserve that is not borrowable reports 0 with reason `cap` (same effect
|
|
123
|
+
* as a zero `borrowCap`).
|
|
124
|
+
*/ export function maxBorrow(risk, reserve, liveState) {
|
|
125
|
+
if (reserve.paused) return zero('paused');
|
|
126
|
+
if (reserve.frozen) return zero('frozen');
|
|
127
|
+
if (!reserve.isBorrowable) return zero('cap');
|
|
128
|
+
const d = reserve.assetDecimals;
|
|
129
|
+
const totals = hubTotals(reserve);
|
|
130
|
+
const existing = findPosition(risk.borrows, reserve);
|
|
131
|
+
const liveRow = buildLiveSnapshot(liveState).indexes.get(liveIndexKey(reserve.hubId, reserve.asset));
|
|
132
|
+
const indexRay = existing ? BigInt(existing.indexRay) : toBigInt(liveRow?.borrowIndex ?? reserve.liveBorrowIndexRay, 'borrowIndex');
|
|
133
|
+
const priceWad = existing ? BigInt(existing.usdPriceWad) : reservePriceWad(reserve, liveState);
|
|
134
|
+
const existingScaled = existing ? BigInt(existing.scaledRay) : 0n;
|
|
135
|
+
const otherDebt = BigInt(risk.totalDebtUsdWad) - (existing ? BigInt(existing.usdWad) : 0n);
|
|
136
|
+
const ltvCollateral = BigInt(risk.ltvWeightedCollateralUsdWad);
|
|
137
|
+
const floorWad = BigInt(risk.minBorrowCollateralUsdWad);
|
|
138
|
+
const candidates = [];
|
|
139
|
+
if (ltvCollateral < floorWad) {
|
|
140
|
+
candidates.push({
|
|
141
|
+
amount: 0n,
|
|
142
|
+
reason: 'minCollateral'
|
|
143
|
+
});
|
|
144
|
+
} else {
|
|
145
|
+
const headroom = ltvCollateral - BigInt(risk.totalDebtUsdWad);
|
|
146
|
+
const estimate = headroom <= 0n || priceWad <= 0n ? 0n : mulDivFloor(headroom, 10n ** BigInt(d), priceWad);
|
|
147
|
+
const debtAfter = (x)=>otherDebt + positionValueWad(existingScaled + mintShares('borrow', x, d, indexRay), indexRay, priceWad, 'ceil');
|
|
148
|
+
candidates.push({
|
|
149
|
+
amount: estimate === 0n ? 0n : fitMax(estimate + ESTIMATE_SLACK, (x)=>debtAfter(x) <= ltvCollateral),
|
|
150
|
+
reason: 'ltv'
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
candidates.push({
|
|
154
|
+
amount: nonNegative(toBigInt(reserve.borrowCap, 'borrowCap') - totals.spokeBorrowedBase),
|
|
155
|
+
reason: 'cap'
|
|
156
|
+
});
|
|
157
|
+
const buffer = mulDivHalfUp(totals.suppliedBase, LIQUIDATION_BUFFER_BPS, BPS);
|
|
158
|
+
candidates.push({
|
|
159
|
+
amount: nonNegative(totals.cashBase - buffer),
|
|
160
|
+
reason: 'liquidity'
|
|
161
|
+
});
|
|
162
|
+
const utilCap = utilizationBorrowCap(totals, d);
|
|
163
|
+
if (utilCap !== null) candidates.push({
|
|
164
|
+
amount: utilCap,
|
|
165
|
+
reason: 'utilizationCap'
|
|
166
|
+
});
|
|
167
|
+
const chosen = pickMin(candidates);
|
|
168
|
+
return finish(chosen, d, false, projectedUtil(totals.suppliedBase, totals.borrowedBase + chosen.amount));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Largest withdraw of the account's `reserve` supply. Candidates: the risk
|
|
172
|
+
* gate (LTV-weighted collateral must stay ≥ `max(debt, floor)` — reported as
|
|
173
|
+
* `healthFactor`, or `minCollateral` when the floor is the stricter bound),
|
|
174
|
+
* pool cash, and the hub utilization ceiling. `paused` reports 0. When the
|
|
175
|
+
* whole position can go, `amountBase` is the floor payout and `isFullClose`
|
|
176
|
+
* is true — sign `"0"` so the contract takes the withdraw-all path
|
|
177
|
+
* (`resolve_withdrawal` treats any request ≥ the half-up balance as full).
|
|
178
|
+
* The position is priced as `risk` valued it, so `_liveState` is accepted for
|
|
179
|
+
* signature symmetry with {@link maxBorrow} but not consulted.
|
|
180
|
+
*/ export function maxWithdraw(risk, reserve, _liveState) {
|
|
181
|
+
if (reserve.paused) return zero('paused');
|
|
182
|
+
const pos = findPosition(risk.supplies, reserve);
|
|
183
|
+
if (!pos) return zero(null);
|
|
184
|
+
const d = pos.decimals;
|
|
185
|
+
const totals = hubTotals(reserve);
|
|
186
|
+
const scaled = BigInt(pos.scaledRay);
|
|
187
|
+
const index = BigInt(pos.indexRay);
|
|
188
|
+
const price = BigInt(pos.usdPriceWad);
|
|
189
|
+
const payout = BigInt(pos.amountBase);
|
|
190
|
+
const halfUp = unscale(scaled, index, d, 'halfUp');
|
|
191
|
+
const debt = BigInt(risk.totalDebtUsdWad);
|
|
192
|
+
const floorWad = BigInt(risk.minBorrowCollateralUsdWad);
|
|
193
|
+
const bps = BigInt(Math.min(pos.entryLtvBps, pos.entryLiquidationThresholdBps));
|
|
194
|
+
const candidates = [];
|
|
195
|
+
let fullAllowed = true;
|
|
196
|
+
if (debt > 0n && bps > 0n) {
|
|
197
|
+
const required = debt > floorWad ? debt : floorWad;
|
|
198
|
+
const reason = floorWad > debt ? 'minCollateral' : 'healthFactor';
|
|
199
|
+
const others = BigInt(risk.ltvWeightedCollateralUsdWad) - BigInt(pos.ltvWeightedUsdWad);
|
|
200
|
+
if (others < required) {
|
|
201
|
+
fullAllowed = false;
|
|
202
|
+
const need = required - others;
|
|
203
|
+
const keepGateWad = mulDivCeil(need, BPS, bps);
|
|
204
|
+
const removableWad = BigInt(pos.gateUsdWad) - keepGateWad;
|
|
205
|
+
const estimate = removableWad <= 0n ? 0n : mulDivFloor(removableWad, 10n ** BigInt(d), price);
|
|
206
|
+
// A partial request at or above the half-up balance becomes a full close.
|
|
207
|
+
const partialMax = halfUp > 0n ? halfUp - 1n : 0n;
|
|
208
|
+
const hi = estimate + ESTIMATE_SLACK > partialMax ? partialMax : estimate + ESTIMATE_SLACK;
|
|
209
|
+
const ltvAfter = (amount)=>{
|
|
210
|
+
const burned = burnShares('supply', amount, d, index);
|
|
211
|
+
const remaining = burned >= scaled ? 0n : scaled - burned;
|
|
212
|
+
return mulDivFloor(positionValueWad(remaining, index, price, 'floor'), bps, BPS);
|
|
213
|
+
};
|
|
214
|
+
candidates.push({
|
|
215
|
+
amount: fitMax(hi, (x)=>ltvAfter(x) >= need),
|
|
216
|
+
reason
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
candidates.push({
|
|
221
|
+
amount: payout,
|
|
222
|
+
reason: null
|
|
223
|
+
});
|
|
224
|
+
candidates.push({
|
|
225
|
+
amount: totals.cashBase,
|
|
226
|
+
reason: 'liquidity'
|
|
227
|
+
});
|
|
228
|
+
const utilCap = utilizationWithdrawCap(totals, d);
|
|
229
|
+
if (utilCap !== null) candidates.push({
|
|
230
|
+
amount: utilCap,
|
|
231
|
+
reason: 'utilizationCap'
|
|
232
|
+
});
|
|
233
|
+
const chosen = pickMin(candidates);
|
|
234
|
+
const isFullClose = fullAllowed && chosen.amount >= payout;
|
|
235
|
+
return finish(chosen, d, isFullClose, projectedUtil(totals.suppliedBase - chosen.amount, totals.borrowedBase));
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Largest supply into `reserve`: the spoke supply-cap headroom
|
|
239
|
+
* (`supplyCap - spoke supplied`, always enforced, `0` closes the side).
|
|
240
|
+
* `paused` and `frozen` both block entry.
|
|
241
|
+
*/ export function maxSupply(reserve) {
|
|
242
|
+
if (reserve.paused) return zero('paused');
|
|
243
|
+
if (reserve.frozen) return zero('frozen');
|
|
244
|
+
const d = reserve.assetDecimals;
|
|
245
|
+
const totals = hubTotals(reserve);
|
|
246
|
+
const chosen = {
|
|
247
|
+
amount: nonNegative(toBigInt(reserve.supplyCap, 'supplyCap') - totals.spokeSuppliedBase),
|
|
248
|
+
reason: 'cap'
|
|
249
|
+
};
|
|
250
|
+
return finish(chosen, d, false, projectedUtil(totals.suppliedBase + chosen.amount, totals.borrowedBase));
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Largest repay of the account's `reserve` debt: the ceil-rounded debt
|
|
254
|
+
* (`unscale_borrow_ceil`, which `resolve_repay` treats as a full close and
|
|
255
|
+
* refunds anything above), bounded by `walletBalance` (base units) when
|
|
256
|
+
* given — reported as `liquidity`. `paused` reports 0.
|
|
257
|
+
*/ export function maxRepay(risk, reserve, walletBalance) {
|
|
258
|
+
if (reserve.paused) return zero('paused');
|
|
259
|
+
const pos = findPosition(risk.borrows, reserve);
|
|
260
|
+
if (!pos) return zero(null);
|
|
261
|
+
const d = pos.decimals;
|
|
262
|
+
const totals = hubTotals(reserve);
|
|
263
|
+
const debt = BigInt(pos.amountBase);
|
|
264
|
+
const wallet = walletBalance === undefined ? null : toBigInt(walletBalance, 'walletBalance');
|
|
265
|
+
const chosen = wallet !== null && wallet < debt ? {
|
|
266
|
+
amount: wallet,
|
|
267
|
+
reason: 'liquidity'
|
|
268
|
+
} : {
|
|
269
|
+
amount: debt,
|
|
270
|
+
reason: null
|
|
271
|
+
};
|
|
272
|
+
return finish(chosen, d, chosen.amount >= debt, projectedUtil(totals.suppliedBase, nonNegative(totals.borrowedBase - chosen.amount)));
|
|
273
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What-if projections: account risk after hypothetical position changes and
|
|
3
|
+
* reserve rates after hypothetical pool flows. Reuses the same aggregation
|
|
4
|
+
* (`aggregateAccountRisk`) and curve (`rates.ts`) as the live computations.
|
|
5
|
+
* xoxno-ui parity: `utils.ts::getProjectedHealthPercentage`,
|
|
6
|
+
* `getProjectedUtilization`, `getApyProjection`.
|
|
7
|
+
*/ import { aggregateAccountRisk, burnShares, mintShares, valueBasePosition, valueScaledPosition } from "./account.js";
|
|
8
|
+
import { annualBorrowRateRay, depositRateRay, rayAprToApy, rayAprToNumber, utilizationRay } from "./rates.js";
|
|
9
|
+
import { baseToRay, numberToScaled, numberToWad, rayToNumber, toBigInt, unscale } from "./scaled.js";
|
|
10
|
+
const sameLeg = (p, d)=>p.hubId === d.hubId && p.asset === d.asset;
|
|
11
|
+
const signedBigInt = (value)=>{
|
|
12
|
+
if (typeof value === 'bigint') return value;
|
|
13
|
+
const trimmed = value.trim();
|
|
14
|
+
if (!/^-?\d+$/.test(trimmed)) throw new Error(`Stellar math: delta amountBase must be an integer string, got "${value}"`);
|
|
15
|
+
return BigInt(trimmed);
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Re-value the account after applying `deltas`, moving shares the way the
|
|
19
|
+
* pool does. Existing legs change in shares (`mintShares` on entry,
|
|
20
|
+
* `burnShares` on a partial exit; a withdraw ≥ the half-up balance or a
|
|
21
|
+
* repay ≥ the ceil debt burns every share) and are revalued with
|
|
22
|
+
* `valueScaledPosition`; a new leg is minted from `amountBase`
|
|
23
|
+
* (`valueBasePosition`) and takes its index, price and entry weights from
|
|
24
|
+
* `delta.reserve` (no live price: pass the reserve you would supply into).
|
|
25
|
+
* A leg driven to zero is dropped. The min-collateral floor and
|
|
26
|
+
* `liveDataValid` carry over from `risk`.
|
|
27
|
+
*/ export function projectAccountRisk(risk, deltas) {
|
|
28
|
+
const supplies = [
|
|
29
|
+
...risk.supplies
|
|
30
|
+
];
|
|
31
|
+
const borrows = [
|
|
32
|
+
...risk.borrows
|
|
33
|
+
];
|
|
34
|
+
for (const delta of deltas){
|
|
35
|
+
const list = delta.side === 'supply' ? supplies : borrows;
|
|
36
|
+
const idx = list.findIndex((p)=>sameLeg(p, delta));
|
|
37
|
+
const existing = idx === -1 ? undefined : list[idx];
|
|
38
|
+
const change = signedBigInt(delta.amountBase);
|
|
39
|
+
let projected;
|
|
40
|
+
if (!existing) {
|
|
41
|
+
projected = change <= 0n ? null : valueBasePosition(seedFromReserve(delta), change);
|
|
42
|
+
} else {
|
|
43
|
+
const seed = {
|
|
44
|
+
side: existing.side,
|
|
45
|
+
hubId: existing.hubId,
|
|
46
|
+
asset: existing.asset,
|
|
47
|
+
decimals: existing.decimals,
|
|
48
|
+
indexRay: BigInt(existing.indexRay),
|
|
49
|
+
priceWad: BigInt(existing.usdPriceWad),
|
|
50
|
+
entryLtvBps: existing.entryLtvBps,
|
|
51
|
+
entryLiquidationThresholdBps: existing.entryLiquidationThresholdBps,
|
|
52
|
+
entryLiquidationBonusBps: existing.entryLiquidationBonusBps,
|
|
53
|
+
entryLiquidationFeesBps: existing.entryLiquidationFeesBps,
|
|
54
|
+
liveDataUsed: existing.liveDataUsed
|
|
55
|
+
};
|
|
56
|
+
const scaled = BigInt(existing.scaledRay);
|
|
57
|
+
let next;
|
|
58
|
+
if (change >= 0n) {
|
|
59
|
+
next = scaled + mintShares(seed.side, change, seed.decimals, seed.indexRay);
|
|
60
|
+
} else {
|
|
61
|
+
const exit = -change;
|
|
62
|
+
const closeAt = seed.side === 'supply' ? unscale(scaled, seed.indexRay, seed.decimals, 'halfUp') : BigInt(existing.amountBase);
|
|
63
|
+
const burned = exit >= closeAt ? scaled : burnShares(seed.side, exit, seed.decimals, seed.indexRay);
|
|
64
|
+
next = burned >= scaled ? 0n : scaled - burned;
|
|
65
|
+
}
|
|
66
|
+
projected = next === 0n ? null : valueScaledPosition({
|
|
67
|
+
...seed,
|
|
68
|
+
scaledRay: next
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (idx === -1) {
|
|
72
|
+
if (projected) list.push(projected);
|
|
73
|
+
} else if (projected) {
|
|
74
|
+
list[idx] = projected;
|
|
75
|
+
} else {
|
|
76
|
+
list.splice(idx, 1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return aggregateAccountRisk({
|
|
80
|
+
accountId: risk.accountId,
|
|
81
|
+
owner: risk.owner,
|
|
82
|
+
spokeId: risk.spokeId,
|
|
83
|
+
mode: risk.mode
|
|
84
|
+
}, supplies, borrows, BigInt(risk.minBorrowCollateralUsdWad), risk.liveDataValid);
|
|
85
|
+
}
|
|
86
|
+
function seedFromReserve(delta) {
|
|
87
|
+
const reserve = delta.reserve;
|
|
88
|
+
if (!reserve) {
|
|
89
|
+
throw new Error(`Stellar math: projectAccountRisk needs \`reserve\` for a new ${delta.side} position on ${delta.hubId}:${delta.asset}`);
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
side: delta.side,
|
|
93
|
+
hubId: delta.hubId,
|
|
94
|
+
asset: delta.asset,
|
|
95
|
+
decimals: reserve.assetDecimals,
|
|
96
|
+
indexRay: toBigInt(delta.side === 'supply' ? reserve.liveSupplyIndexRay : reserve.liveBorrowIndexRay, 'reserve live index'),
|
|
97
|
+
priceWad: numberToWad(reserve.usdPrice),
|
|
98
|
+
entryLtvBps: reserve.collateralFactorBps,
|
|
99
|
+
entryLiquidationThresholdBps: reserve.liquidationThresholdBps,
|
|
100
|
+
entryLiquidationBonusBps: reserve.liquidationPenaltyBps,
|
|
101
|
+
entryLiquidationFeesBps: reserve.liquidationFeesBps,
|
|
102
|
+
liveDataUsed: false
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Hub rates after adding `supplyDeltaBase` / `borrowDeltaBase` (signed base
|
|
107
|
+
* units) to the reserve's hub-pool totals. Utilization is
|
|
108
|
+
* `curve.rs::utilization` over the projected totals; rates follow
|
|
109
|
+
* `annualBorrowRateRay` / `depositRateRay`. Whether `ReserveDto.supplyApy`
|
|
110
|
+
* / `borrowApy` are simple or compounded is not pinned by the API contract,
|
|
111
|
+
* so both `*Apr` and `*Apy` are returned.
|
|
112
|
+
*/ export function projectReserveApy(reserve, supplyDeltaBase = 0n, borrowDeltaBase = 0n) {
|
|
113
|
+
const d = reserve.assetDecimals;
|
|
114
|
+
const clamp = (v)=>v < 0n ? 0n : v;
|
|
115
|
+
const supplied = clamp(numberToScaled(reserve.hubPool.suppliedShort, d) + signedBigInt(supplyDeltaBase));
|
|
116
|
+
const borrowed = clamp(numberToScaled(reserve.hubPool.borrowedShort, d) + signedBigInt(borrowDeltaBase));
|
|
117
|
+
const util = utilizationRay(baseToRay(borrowed, d), baseToRay(supplied, d));
|
|
118
|
+
const borrowRate = annualBorrowRateRay(util, reserve.irm);
|
|
119
|
+
const supplyRate = depositRateRay(util, borrowRate, reserve.irm.reserveFactorBps);
|
|
120
|
+
return {
|
|
121
|
+
utilization: rayToNumber(util),
|
|
122
|
+
utilizationRay: util.toString(),
|
|
123
|
+
borrowAprRay: borrowRate.toString(),
|
|
124
|
+
supplyAprRay: supplyRate.toString(),
|
|
125
|
+
borrowApr: rayAprToNumber(borrowRate),
|
|
126
|
+
supplyApr: rayAprToNumber(supplyRate),
|
|
127
|
+
borrowApy: rayAprToApy(borrowRate),
|
|
128
|
+
supplyApy: rayAprToApy(supplyRate)
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interest-rate curve mirror of `rs-lending-xlm/common/src/rates/curve.rs`.
|
|
3
|
+
* Annual RAY in, annual RAY out; `Ray::mul` / `Ray::div` are half-up
|
|
4
|
+
* (`fp.rs`), matching the pool's view getters.
|
|
5
|
+
*/ import { BPS, RAY, mulDivHalfUp, rayToNumber, toBigInt } from "./scaled.js";
|
|
6
|
+
/** `Ray::mul` — half_up(a * b / RAY). */ const rayMul = (a, b)=>mulDivHalfUp(a, b, RAY);
|
|
7
|
+
/** `Ray::div` — half_up(a * RAY / b). */ const rayDiv = (a, b)=>mulDivHalfUp(a, RAY, b);
|
|
8
|
+
/**
|
|
9
|
+
* `curve.rs::utilization` — `borrowed / supplied` half-up in RAY; zero when
|
|
10
|
+
* nothing is supplied. The contract feeds it half-up-valued debt and supply
|
|
11
|
+
* (`formulas.md` §Utilization and annual rates).
|
|
12
|
+
*/ export function utilizationRay(borrowedRay, suppliedRay) {
|
|
13
|
+
if (suppliedRay === 0n) return 0n;
|
|
14
|
+
return rayDiv(borrowedRay, suppliedRay);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* `curve.rs::calculate_annual_borrow_rate` — three-segment piecewise-linear
|
|
18
|
+
* curve. Utilization is clamped to one RAY; below `mid` the rate ramps from
|
|
19
|
+
* `base` by `slope1`, between `mid` and `optimal` by `slope2`, above
|
|
20
|
+
* `optimal` by `slope3`; the result is capped at `maxBorrowRate`.
|
|
21
|
+
*/ export function annualBorrowRateRay(utilization, irm) {
|
|
22
|
+
const u = utilization > RAY ? RAY : utilization;
|
|
23
|
+
const base = toBigInt(irm.baseRateRay, 'irm.baseRateRay');
|
|
24
|
+
const slope1 = toBigInt(irm.slope1Ray, 'irm.slope1Ray');
|
|
25
|
+
const slope2 = toBigInt(irm.slope2Ray, 'irm.slope2Ray');
|
|
26
|
+
const slope3 = toBigInt(irm.slope3Ray, 'irm.slope3Ray');
|
|
27
|
+
const mid = toBigInt(irm.midUtilizationRay, 'irm.midUtilizationRay');
|
|
28
|
+
const optimal = toBigInt(irm.optimalUtilizationRay, 'irm.optimalUtilizationRay');
|
|
29
|
+
const max = toBigInt(irm.maxBorrowRateRay, 'irm.maxBorrowRateRay');
|
|
30
|
+
let rate;
|
|
31
|
+
if (u < mid) {
|
|
32
|
+
rate = base + rayDiv(rayMul(u, slope1), mid);
|
|
33
|
+
} else if (u < optimal) {
|
|
34
|
+
const range = optimal - mid;
|
|
35
|
+
rate = base + slope1 + rayDiv(rayMul(u - mid, slope2), range);
|
|
36
|
+
} else {
|
|
37
|
+
const excess = u - optimal;
|
|
38
|
+
const range = RAY - optimal;
|
|
39
|
+
// `optimal == RAY` forces `u == RAY` (clamped) so `excess == 0`; the contract
|
|
40
|
+
// would trap on the zero divisor, the SDK reports the kink rate instead.
|
|
41
|
+
const contribution = range === 0n ? 0n : rayDiv(rayMul(excess, slope3), range);
|
|
42
|
+
rate = base + slope1 + slope2 + contribution;
|
|
43
|
+
}
|
|
44
|
+
return rate > max ? max : rate;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* `curve.rs::calculate_deposit_rate` — supplier rate after the reserve factor:
|
|
48
|
+
* `half_up(half_up(u * borrowRate / RAY) * (BPS - rf) / BPS)`. Zero at zero
|
|
49
|
+
* utilization or a reserve factor outside `0..BPS`.
|
|
50
|
+
*/ export function depositRateRay(utilization, borrowRateRay, reserveFactorBps) {
|
|
51
|
+
if (utilization === 0n) return 0n;
|
|
52
|
+
if (!Number.isInteger(reserveFactorBps) || reserveFactorBps < 0 || reserveFactorBps >= 10000) return 0n;
|
|
53
|
+
const rateXUtil = rayMul(utilization, borrowRateRay);
|
|
54
|
+
return mulDivHalfUp(rateXUtil, BPS - BigInt(reserveFactorBps), BPS);
|
|
55
|
+
}
|
|
56
|
+
/** Annual RAY rate → simple APR as a fraction (`0.05` = 5%). */ export const rayAprToNumber = (rateRay)=>rayToNumber(rateRay);
|
|
57
|
+
/**
|
|
58
|
+
* Annual RAY rate → continuously compounded APY fraction, `e^apr − 1`. The
|
|
59
|
+
* pool compounds per millisecond with an eighth-order Taylor series
|
|
60
|
+
* (`compound.rs`), which the continuous exponential approximates to well
|
|
61
|
+
* within display precision.
|
|
62
|
+
*/ export const rayAprToApy = (rateRay)=>Math.expm1(rayToNumber(rateRay));
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract-exact fixed-point primitives for the Stellar lending math module.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `rs-lending-xlm/common/src/math/fp_core.rs` (`mul_div_*`,
|
|
5
|
+
* `rescale_*`, `div_by_int_half_up`), `common/src/math/fp.rs` (`Ray`, `Wad`)
|
|
6
|
+
* and `common/src/rates/scaling.rs` (`unscale_supply_floor`,
|
|
7
|
+
* `unscale_borrow_ceil`). All arithmetic is native `BigInt` on the raw decimal
|
|
8
|
+
* strings the API serves; nothing here touches floating point except the
|
|
9
|
+
* `*Short` display converters at the bottom.
|
|
10
|
+
*
|
|
11
|
+
* Every helper assumes the contract's non-negative domain (`Ray`, `Wad` and
|
|
12
|
+
* position balances are never negative on-chain); {@link toBigInt} enforces
|
|
13
|
+
* that at the string boundary.
|
|
14
|
+
*/ /** 10^27 — shares, indexes, rates and utilization. */ export const RAY = 10n ** 27n;
|
|
15
|
+
/** 10^18 — USD values, prices per whole token, health factor. */ export const WAD = 10n ** 18n;
|
|
16
|
+
/** 10,000 — risk ratios and fees. */ export const BPS = 10000n;
|
|
17
|
+
export const RAY_DECIMALS = 27;
|
|
18
|
+
export const WAD_DECIMALS = 18;
|
|
19
|
+
/** Parse a raw integer string (or bigint/number) into a non-negative bigint. */ export function toBigInt(value, label = 'value') {
|
|
20
|
+
let out;
|
|
21
|
+
if (typeof value === 'bigint') out = value;
|
|
22
|
+
else if (typeof value === 'number') {
|
|
23
|
+
if (!Number.isSafeInteger(value)) {
|
|
24
|
+
throw new Error(`Stellar math: ${label} must be a safe integer, got ${value}`);
|
|
25
|
+
}
|
|
26
|
+
out = BigInt(value);
|
|
27
|
+
} else {
|
|
28
|
+
const trimmed = value.trim();
|
|
29
|
+
if (!/^-?\d+$/.test(trimmed)) {
|
|
30
|
+
throw new Error(`Stellar math: ${label} must be a decimal integer string, got "${value}"`);
|
|
31
|
+
}
|
|
32
|
+
out = BigInt(trimmed);
|
|
33
|
+
}
|
|
34
|
+
if (out < 0n) throw new Error(`Stellar math: ${label} must be non-negative, got ${out}`);
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
/** `fp_core::mul_div_floor` — floor(x * y / d) for non-negative operands. */ export function mulDivFloor(x, y, d) {
|
|
38
|
+
if (d === 0n) throw new Error('Stellar math: division by zero');
|
|
39
|
+
return x * y / d;
|
|
40
|
+
}
|
|
41
|
+
/** `fp_core::mul_div_ceil` — ceil(x * y / d) for non-negative operands. */ export function mulDivCeil(x, y, d) {
|
|
42
|
+
if (d === 0n) throw new Error('Stellar math: division by zero');
|
|
43
|
+
const p = x * y;
|
|
44
|
+
const q = p / d;
|
|
45
|
+
return p % d === 0n ? q : q + 1n;
|
|
46
|
+
}
|
|
47
|
+
/** `fp_core::mul_div_half_up` — floor((x * y + d/2) / d) for non-negative operands. */ export function mulDivHalfUp(x, y, d) {
|
|
48
|
+
if (d === 0n) throw new Error('Stellar math: division by zero');
|
|
49
|
+
return (x * y + d / 2n) / d;
|
|
50
|
+
}
|
|
51
|
+
/** `fp_core::div_by_int_half_up` — a / b rounded half up (non-negative a, b > 0). */ export function divByIntHalfUp(a, b) {
|
|
52
|
+
if (b === 0n) throw new Error('Stellar math: division by zero');
|
|
53
|
+
const q = a / b;
|
|
54
|
+
const r = a % b;
|
|
55
|
+
const half = b / 2n + b % 2n;
|
|
56
|
+
return r >= half ? q + 1n : q;
|
|
57
|
+
}
|
|
58
|
+
const pow10 = (n)=>10n ** BigInt(n);
|
|
59
|
+
/** `fp_core::rescale_floor` — move `a` between decimal scales, truncating when dropping digits. */ export function rescaleFloor(a, fromDecimals, toDecimals) {
|
|
60
|
+
if (fromDecimals === toDecimals) return a;
|
|
61
|
+
if (toDecimals > fromDecimals) return a * pow10(toDecimals - fromDecimals);
|
|
62
|
+
return a / pow10(fromDecimals - toDecimals);
|
|
63
|
+
}
|
|
64
|
+
/** `fp_core::rescale_ceil` — like {@link rescaleFloor} but rounds a dropped remainder up. */ export function rescaleCeil(a, fromDecimals, toDecimals) {
|
|
65
|
+
if (fromDecimals === toDecimals) return a;
|
|
66
|
+
if (toDecimals > fromDecimals) return a * pow10(toDecimals - fromDecimals);
|
|
67
|
+
const factor = pow10(fromDecimals - toDecimals);
|
|
68
|
+
const q = a / factor;
|
|
69
|
+
return a % factor === 0n ? q : q + 1n;
|
|
70
|
+
}
|
|
71
|
+
/** `fp_core::rescale_half_up` — like {@link rescaleFloor} but rounds a dropped remainder half up. */ export function rescaleHalfUp(a, fromDecimals, toDecimals) {
|
|
72
|
+
if (fromDecimals === toDecimals) return a;
|
|
73
|
+
if (toDecimals > fromDecimals) return a * pow10(toDecimals - fromDecimals);
|
|
74
|
+
return divByIntHalfUp(a, pow10(fromDecimals - toDecimals));
|
|
75
|
+
}
|
|
76
|
+
const MUL_DIV = {
|
|
77
|
+
floor: mulDivFloor,
|
|
78
|
+
ceil: mulDivCeil,
|
|
79
|
+
halfUp: mulDivHalfUp
|
|
80
|
+
};
|
|
81
|
+
const RESCALE = {
|
|
82
|
+
floor: rescaleFloor,
|
|
83
|
+
ceil: rescaleCeil,
|
|
84
|
+
halfUp: rescaleHalfUp
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Scaled shares → token base units with one rounding direction at both steps.
|
|
88
|
+
* `scaling.rs`: `unscale_supply_floor` = `mul_floor` + `to_asset_floor`,
|
|
89
|
+
* `unscale_borrow_ceil` = `mul_ceil` + `to_asset_ceil`, `unscale_supply` /
|
|
90
|
+
* `unscale_borrow` (displayed balance) = `mul` (half-up) + `to_asset` (half-up).
|
|
91
|
+
*/ export function unscale(scaledRay, indexRay, decimals, rounding) {
|
|
92
|
+
return RESCALE[rounding](MUL_DIV[rounding](scaledRay, indexRay, RAY), RAY_DECIMALS, decimals);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Current supplied amount in token base units — `scaling.rs::unscale_supply_floor`
|
|
96
|
+
* (`mul_floor` then `to_asset_floor`). This is what the pool pays on a full
|
|
97
|
+
* withdrawal, so the SDK never reports more collateral than the contract releases.
|
|
98
|
+
*/ export function unscaleSupplyFloor(scaledRay, indexRay, decimals) {
|
|
99
|
+
return unscale(toBigInt(scaledRay, 'scaledRay'), toBigInt(indexRay, 'indexRay'), decimals, 'floor');
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Current borrowed amount in token base units — `scaling.rs::unscale_borrow_ceil`
|
|
103
|
+
* (`mul_ceil` then `to_asset_ceil`). A repayment of at least this amount burns
|
|
104
|
+
* every debt share (`resolve_repay`), so a max repay always covers the debt.
|
|
105
|
+
*
|
|
106
|
+
* Note: the xoxno-ui mirror (`stellar-scaled-math.ts`) rounds the share product
|
|
107
|
+
* half-up before the ceil rescale; the contract ceils both steps. This module
|
|
108
|
+
* follows the contract.
|
|
109
|
+
*/ export function unscaleBorrowCeil(scaledRay, indexRay, decimals) {
|
|
110
|
+
return unscale(toBigInt(scaledRay, 'scaledRay'), toBigInt(indexRay, 'indexRay'), decimals, 'ceil');
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Displayed balance in token base units — `scaling.rs::unscale_supply` /
|
|
114
|
+
* `unscale_borrow` (half-up at both steps). `resolve_withdrawal` treats a
|
|
115
|
+
* request at or above this value as a full withdrawal.
|
|
116
|
+
*/ export function unscaleHalfUp(scaledRay, indexRay, decimals) {
|
|
117
|
+
return unscale(toBigInt(scaledRay, 'scaledRay'), toBigInt(indexRay, 'indexRay'), decimals, 'halfUp');
|
|
118
|
+
}
|
|
119
|
+
/** `Ray::from_asset` — token base units → RAY (exact for decimals ≤ 27). */ export function baseToRay(amountBase, decimals) {
|
|
120
|
+
if (decimals > RAY_DECIMALS) throw new Error(`Stellar math: decimals ${decimals} exceed RAY scale`);
|
|
121
|
+
return rescaleFloor(amountBase, decimals, RAY_DECIMALS);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* USD value (WAD) of a scaled position — `common/src/rates/value.rs`
|
|
125
|
+
* `position_value` (halfUp) / `position_value_floor` / `position_value_ceil`:
|
|
126
|
+
* `asset_ray = round(scaled * index / RAY)`, `asset_wad = round(asset_ray / 1e9)`,
|
|
127
|
+
* `usd_wad = round(asset_wad * price / WAD)` with one direction at all three steps.
|
|
128
|
+
*/ export function positionValueWad(scaledRay, indexRay, priceWad, rounding) {
|
|
129
|
+
const assetRay = MUL_DIV[rounding](scaledRay, indexRay, RAY);
|
|
130
|
+
const assetWad = RESCALE[rounding](assetRay, RAY_DECIMALS, WAD_DECIMALS);
|
|
131
|
+
return MUL_DIV[rounding](assetWad, priceWad, WAD);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* USD value (WAD) of a token amount in base units, using the same last two
|
|
135
|
+
* steps as {@link positionValueWad}. Used by projections, where the hypothetical
|
|
136
|
+
* amount is a token quantity rather than shares.
|
|
137
|
+
*/ export function baseValueWad(amountBase, decimals, priceWad, rounding) {
|
|
138
|
+
const assetWad = RESCALE[rounding](baseToRay(amountBase, decimals), RAY_DECIMALS, WAD_DECIMALS);
|
|
139
|
+
return MUL_DIV[rounding](assetWad, priceWad, WAD);
|
|
140
|
+
}
|
|
141
|
+
/** WAD → JS number (display only; ~1e-16 relative precision). */ export const wadToNumber = (wad)=>Number(wad) / 1e18;
|
|
142
|
+
/** RAY → JS number (display only). */ export const rayToNumber = (ray)=>Number(ray) / 1e27;
|
|
143
|
+
/** Token base units → human-readable number (display only). */ export const baseToShort = (amountBase, decimals)=>Number(amountBase) / 10 ** decimals;
|
|
144
|
+
/**
|
|
145
|
+
* Decimal JS number (a `usdPrice` / `*Short` API field) → integer at `decimals`.
|
|
146
|
+
* Uses `toFixed` so no float arithmetic runs after the parse; precision is that
|
|
147
|
+
* of the input double (~15–16 significant digits). Domain: `0 < value < 1e21`
|
|
148
|
+
* (`toFixed` switches to exponential notation at 1e21); anything outside,
|
|
149
|
+
* non-finite included, maps to 0 rather than throwing.
|
|
150
|
+
*/ export function numberToScaled(value, decimals) {
|
|
151
|
+
if (!Number.isFinite(value) || value <= 0 || value >= 1e21) return 0n;
|
|
152
|
+
const fixed = value.toFixed(decimals);
|
|
153
|
+
const [whole = '0', frac = ''] = fixed.split('.');
|
|
154
|
+
return BigInt(whole + frac.padEnd(decimals, '0'));
|
|
155
|
+
}
|
|
156
|
+
/** JS USD price → WAD, see {@link numberToScaled}. */ export const numberToWad = (usd)=>numberToScaled(usd, WAD_DECIMALS);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map MVX `PositionMode` numbering to the Stellar lending controller enum.
|
|
3
|
+
*
|
|
4
|
+
* MVX: None=0, Normal=1, Multiply=2, Long=3, Short=4
|
|
5
|
+
* Stellar: Normal=0, Multiply=1, Long=2, Short=3
|
|
6
|
+
*/ export function toStellarPositionMode(mvxMode) {
|
|
7
|
+
if (mvxMode <= 0 || mvxMode > 4) {
|
|
8
|
+
throw new Error(`Invalid PositionMode ${mvxMode} for Stellar — ` + `expected 1 (Normal) through 4 (Short). ` + `"None" (0) has no Stellar equivalent.`);
|
|
9
|
+
}
|
|
10
|
+
return mvxMode - 1;
|
|
11
|
+
}
|