@xoxno/sdk-js 1.0.224 → 1.0.226

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +39 -0
  2. package/dist/cjs/sdk/stellar/index.d.ts +1 -0
  3. package/dist/cjs/sdk/stellar/read/__tests__/client.test.d.ts +1 -0
  4. package/dist/cjs/sdk/stellar/read/client.d.ts +19 -0
  5. package/dist/cjs/sdk/stellar/read/index.d.ts +4 -0
  6. package/dist/cjs/sdk/stellar/read/schemas.d.ts +370 -0
  7. package/dist/cjs/sdk/stellar/read/types.d.ts +83 -0
  8. package/dist/esm/index.js +3 -0
  9. package/dist/esm/sdk/index.js +198 -0
  10. package/dist/esm/sdk/stellar/admin.js +385 -0
  11. package/dist/esm/sdk/stellar/auth-policy.js +310 -0
  12. package/dist/esm/sdk/stellar/cctp.js +23 -0
  13. package/dist/esm/sdk/stellar/contracts.js +49 -0
  14. package/dist/esm/sdk/stellar/events/decode.js +415 -0
  15. package/dist/esm/sdk/stellar/events/id.js +62 -0
  16. package/dist/esm/sdk/stellar/events/index.js +2 -0
  17. package/dist/esm/sdk/stellar/governance.js +403 -0
  18. package/dist/esm/sdk/stellar/index.js +26 -0
  19. package/dist/esm/sdk/stellar/lending-api-types.js +3 -0
  20. package/dist/esm/sdk/stellar/lending-read-types.js +1 -0
  21. package/dist/esm/sdk/stellar/lending-read.js +341 -0
  22. package/dist/esm/sdk/stellar/lending.js +358 -0
  23. package/dist/esm/sdk/stellar/math/account.js +256 -0
  24. package/dist/esm/sdk/stellar/math/index.js +11 -0
  25. package/dist/esm/sdk/stellar/math/limits.js +273 -0
  26. package/dist/esm/sdk/stellar/math/projection.js +130 -0
  27. package/dist/esm/sdk/stellar/math/rates.js +62 -0
  28. package/dist/esm/sdk/stellar/math/scaled.js +156 -0
  29. package/dist/esm/sdk/stellar/position-mode.js +11 -0
  30. package/dist/esm/sdk/stellar/prepare.js +47 -0
  31. package/dist/esm/sdk/stellar/quote.js +87 -0
  32. package/dist/esm/sdk/stellar/read/client.js +82 -0
  33. package/dist/esm/sdk/stellar/read/index.js +3 -0
  34. package/dist/esm/sdk/stellar/read/schemas.js +158 -0
  35. package/dist/esm/sdk/stellar/read/types.js +1 -0
  36. package/dist/esm/sdk/stellar/repay-swap.js +12 -0
  37. package/dist/esm/sdk/stellar/route-verify.js +285 -0
  38. package/dist/esm/sdk/stellar/scval-encode.js +464 -0
  39. package/dist/esm/sdk/stellar/swap-history.js +46 -0
  40. package/dist/esm/sdk/stellar/swap-index.js +1 -0
  41. package/dist/esm/sdk/stellar/swap.js +117 -0
  42. package/dist/esm/sdk/stellar/xdr-compat.js +17 -0
  43. package/dist/esm/sdk/stellar-schema.js +129 -0
  44. package/dist/esm/sdk/swagger.js +1957 -0
  45. package/dist/esm/sdk/types.js +1 -0
  46. package/dist/esm/sdk/utils.js +21 -0
  47. package/dist/esm/utils/api.js +169 -0
  48. package/dist/esm/utils/const.js +11 -0
  49. package/dist/esm/utils/errors.js +20 -0
  50. package/dist/esm/utils/guards.js +29 -0
  51. package/dist/esm/utils/helpers.js +20 -0
  52. package/dist/esm/utils/regex.js +14 -0
  53. package/dist/esm/utils/soroban-errors.js +149 -0
  54. package/dist/index.cjs +1 -1
  55. package/dist/index.esm.js +1 -2
  56. package/dist/sdk/stellar/index.cjs +1 -1
  57. package/dist/sdk/stellar/index.d.ts +1 -0
  58. package/dist/sdk/stellar/index.esm.js +1 -1
  59. package/dist/sdk/stellar/read/__tests__/client.test.d.ts +1 -0
  60. package/dist/sdk/stellar/read/client.d.ts +19 -0
  61. package/dist/sdk/stellar/read/index.cjs +1 -0
  62. package/dist/sdk/stellar/read/index.d.ts +4 -0
  63. package/dist/sdk/stellar/read/index.esm.js +1 -0
  64. package/dist/sdk/stellar/read/schemas.d.ts +370 -0
  65. package/dist/sdk/stellar/read/types.d.ts +83 -0
  66. package/package.json +20 -4
  67. 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
+ }