@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,358 @@
1
+ /**
2
+ * Stellar lending user-operation transaction builders.
3
+ *
4
+ * Each builder takes a typed Stellar argument object plus shared
5
+ * `StellarLendingBuilderOptions` ({ network, caller, sourceSequence,
6
+ * controllerAddress, ... }) and returns
7
+ * unsigned transaction XDR that must be prepared before wallet signing.
8
+ *
9
+ * i128 values cross the boundary as decimal strings, encoded via
10
+ * `new ScInt(str).toI128()`. Addresses (Stellar `G...` accounts and Soroban
11
+ * `C...` contracts) are encoded via `new Address(str).toScVal()`.
12
+ *
13
+ * Builders are RPC-free: they accept a caller-supplied `sourceSequence` so the
14
+ * returned XDR is deterministic and snapshot-testable. The caller fetches the
15
+ * current sequence via `rpc.Server.getAccount(caller)` and runs
16
+ * `rpc.Server.prepareTransaction` (simulation + Soroban footprint/auth/resource
17
+ * fee) before handing the XDR to the wallet to sign.
18
+ */ import { Account, BASE_FEE, Contract, scValToNative, TransactionBuilder, xdr } from "@stellar/stellar-sdk";
19
+ import { STELLAR_NETWORK_PASSPHRASE } from "./contracts.js";
20
+ import { addr, asStellarBytes, asStellarStrategySwapBytes, bool, hubAsset, i128, option, seizeMode, tupleAddrAmountVec, tupleHubAssetAmount, tupleHubAssetAmountVec, u32, u64, vec } from "./scval-encode.js";
21
+ // -----------------------------------------------------------------------------
22
+ // Transaction assembly
23
+ // -----------------------------------------------------------------------------
24
+ /**
25
+ * Assemble an unsigned XDR that invokes a single Soroban contract method.
26
+ *
27
+ * Uses a synthetic `Account(caller, sourceSequence)` — no RPC call. The
28
+ * returned XDR still needs preparation (simulation to populate Soroban
29
+ * footprint + auth entries + resource fees) before signing. The UI layer
30
+ * does this via `rpc.Server.prepareTransaction`.
31
+ */ export function buildTx(opts, method, params) {
32
+ if (!opts.controllerAddress) {
33
+ throw new Error('Stellar lending controllerAddress is required');
34
+ }
35
+ const contract = new Contract(opts.controllerAddress);
36
+ const source = new Account(opts.caller, opts.sourceSequence);
37
+ const tx = new TransactionBuilder(source, {
38
+ fee: opts.fee ?? BASE_FEE,
39
+ networkPassphrase: STELLAR_NETWORK_PASSPHRASE[opts.network]
40
+ }).addOperation(contract.call(method, ...params)).setTimeout(opts.timeoutSeconds ?? 300).build();
41
+ return {
42
+ xdr: tx.toXDR()
43
+ };
44
+ }
45
+ // -----------------------------------------------------------------------------
46
+ // Builders — 11 entry points, 1 : 1 with the multi-hub Stellar controller
47
+ // -----------------------------------------------------------------------------
48
+ function encodeSpokeId(value) {
49
+ if (!Number.isInteger(value) || value < 1 || value > 0xffffffff) {
50
+ throw new Error('Stellar builder: spokeId must be an integer from 1 to 4294967295');
51
+ }
52
+ return u32(value);
53
+ }
54
+ /**
55
+ * supply(caller, account_id: u64, spoke_id: u32, assets: Vec<(HubAssetKey, i128)>)
56
+ * @param opts - Network, caller, current source sequence and deployment addresses.
57
+ * @param args - Operation arguments; token amounts are decimal base-unit strings.
58
+ * @returns Unsigned XDR; prepare with prepareStellarBuiltTx before wallet signing.
59
+ * @category Lending transactions
60
+ */ export function buildStellarSupplyBatchTx(opts, args) {
61
+ const accountId = args.accountNonce ?? 0;
62
+ return buildTx(opts, 'supply', [
63
+ addr(opts.caller),
64
+ u64(accountId),
65
+ encodeSpokeId(args.spokeId),
66
+ tupleHubAssetAmountVec([
67
+ ...args.assets
68
+ ])
69
+ ]);
70
+ }
71
+ /** Single-asset `supply` — wraps the asset in a 1-element batch.
72
+ * @param opts - Network, caller, current source sequence and deployment addresses.
73
+ * @param args - Operation arguments; token amounts are decimal base-unit strings.
74
+ * @returns Unsigned XDR; prepare with prepareStellarBuiltTx before wallet signing.
75
+ * @category Lending transactions
76
+ */ export function buildStellarSupplyTx(opts, args) {
77
+ return buildStellarSupplyBatchTx(opts, {
78
+ accountNonce: args.accountNonce,
79
+ spokeId: args.spokeId,
80
+ assets: [
81
+ {
82
+ hubId: args.hubId,
83
+ asset: args.asset,
84
+ amount: args.amount
85
+ }
86
+ ]
87
+ });
88
+ }
89
+ /**
90
+ * borrow(caller, account_id: u64, borrows: Vec<(HubAssetKey, i128)>,
91
+ * to: Option<Address>) — `to` is always sent; absent means the caller
92
+ * receives the funds.
93
+ * @param opts - Network, caller, current source sequence and deployment addresses.
94
+ * @param args - Operation arguments; token amounts are decimal base-unit strings.
95
+ * @returns Unsigned XDR; prepare with prepareStellarBuiltTx before wallet signing.
96
+ * @category Lending transactions
97
+ */ export function buildStellarBorrowBatchTx(opts, args) {
98
+ return buildTx(opts, 'borrow', [
99
+ addr(opts.caller),
100
+ u64(args.accountNonce),
101
+ tupleHubAssetAmountVec([
102
+ ...args.borrows
103
+ ]),
104
+ option(args.to, addr)
105
+ ]);
106
+ }
107
+ /** Single-asset `borrow`.
108
+ * @param opts - Network, caller, current source sequence and deployment addresses.
109
+ * @param args - Operation arguments; token amounts are decimal base-unit strings.
110
+ * @returns Unsigned XDR; prepare with prepareStellarBuiltTx before wallet signing.
111
+ * @category Lending transactions
112
+ */ export function buildStellarBorrowTx(opts, args) {
113
+ return buildStellarBorrowBatchTx(opts, {
114
+ accountNonce: args.accountNonce,
115
+ borrows: [
116
+ {
117
+ hubId: args.hubId,
118
+ asset: args.asset,
119
+ amount: args.amount
120
+ }
121
+ ],
122
+ to: args.to
123
+ });
124
+ }
125
+ /**
126
+ * withdraw(caller, account_id: u64, withdrawals: Vec<(HubAssetKey, i128)>,
127
+ * to: Option<Address>) — `to` is always sent; absent means the caller
128
+ * receives the funds.
129
+ * @param opts - Network, caller, current source sequence and deployment addresses.
130
+ * @param args - Operation arguments; token amounts are decimal base-unit strings.
131
+ * @returns Unsigned XDR; prepare with prepareStellarBuiltTx before wallet signing.
132
+ * @category Lending transactions
133
+ */ export function buildStellarWithdrawBatchTx(opts, args) {
134
+ return buildTx(opts, 'withdraw', [
135
+ addr(opts.caller),
136
+ u64(args.accountNonce),
137
+ tupleHubAssetAmountVec([
138
+ ...args.withdrawals
139
+ ]),
140
+ option(args.to, addr)
141
+ ]);
142
+ }
143
+ /** Single-asset `withdraw`. Amount "0" withdraws the full supplied position.
144
+ * @param opts - Network, caller, current source sequence and deployment addresses.
145
+ * @param args - Operation arguments; token amounts are decimal base-unit strings.
146
+ * @returns Unsigned XDR; prepare with prepareStellarBuiltTx before wallet signing.
147
+ * @category Lending transactions
148
+ */ export function buildStellarWithdrawTx(opts, args) {
149
+ return buildStellarWithdrawBatchTx(opts, {
150
+ accountNonce: args.accountNonce,
151
+ withdrawals: [
152
+ {
153
+ hubId: args.hubId,
154
+ asset: args.asset,
155
+ amount: args.amount
156
+ }
157
+ ],
158
+ to: args.to
159
+ });
160
+ }
161
+ /**
162
+ * repay(caller, account_id: u64, payments: Vec<(HubAssetKey, i128)>)
163
+ * @param opts - Network, caller, current source sequence and deployment addresses.
164
+ * @param args - Operation arguments; token amounts are decimal base-unit strings.
165
+ * @returns Unsigned XDR; prepare with prepareStellarBuiltTx before wallet signing.
166
+ * @category Lending transactions
167
+ */ export function buildStellarRepayBatchTx(opts, args) {
168
+ return buildTx(opts, 'repay', [
169
+ addr(opts.caller),
170
+ u64(args.accountNonce),
171
+ tupleHubAssetAmountVec([
172
+ ...args.payments
173
+ ])
174
+ ]);
175
+ }
176
+ /** Single-asset `repay`.
177
+ * @param opts - Network, caller, current source sequence and deployment addresses.
178
+ * @param args - Operation arguments; token amounts are decimal base-unit strings.
179
+ * @returns Unsigned XDR; prepare with prepareStellarBuiltTx before wallet signing.
180
+ * @category Lending transactions
181
+ */ export function buildStellarRepayTx(opts, args) {
182
+ return buildStellarRepayBatchTx(opts, {
183
+ accountNonce: args.accountNonce,
184
+ payments: [
185
+ {
186
+ hubId: args.hubId,
187
+ asset: args.asset,
188
+ amount: args.amount
189
+ }
190
+ ]
191
+ });
192
+ }
193
+ /**
194
+ * liquidate(liquidator, account_id: u64, debt_payments: Vec<(HubAssetKey, i128)>,
195
+ * seize_mode: SeizeMode) -> u64
196
+ *
197
+ * The `u64` return is the account credited with the seized supply shares under
198
+ * `SeizeMode::Credit` (freshly opened when `Credit(0)` was passed), and `0`
199
+ * under `SeizeMode::Transfer`. Read it from the simulation / tx result with
200
+ * `decodeStellarLiquidateReturn`.
201
+ * @param opts - Network, caller, current source sequence and deployment addresses.
202
+ * @param args - Operation arguments; token amounts are decimal base-unit strings.
203
+ * @returns Unsigned XDR; prepare with prepareStellarBuiltTx before wallet signing.
204
+ * @category Lending transactions
205
+ */ export function buildStellarLiquidateTx(opts, args) {
206
+ return buildTx(opts, 'liquidate', [
207
+ addr(opts.caller),
208
+ u64(args.accountNonce),
209
+ tupleHubAssetAmountVec([
210
+ ...args.debtPayments
211
+ ]),
212
+ seizeMode(args.seizeMode ?? 'Transfer')
213
+ ]);
214
+ }
215
+ /**
216
+ * Decode `liquidate`'s `u64` return value from its base64 XDR — the receiving
217
+ * account id, as a decimal string. `'0'` means `SeizeMode::Transfer` (the
218
+ * liquidator was paid in underlying tokens, no account was touched).
219
+ *
220
+ * Takes base64 XDR (as Soroban RPC delivers `returnValue` / `result.retval`)
221
+ * so no live `xdr.ScVal` crosses the consumer boundary — same contract as
222
+ * `decodeStellarLendingEvent`. Hold a parsed ScVal? Pass `toBase64Xdr(scv)`.
223
+ */ export function decodeStellarLiquidateReturn(returnValueB64) {
224
+ const native = scValToNative(xdr.ScVal.fromXDR(returnValueB64, 'base64'));
225
+ if (typeof native === 'bigint' || typeof native === 'number') {
226
+ return native.toString();
227
+ }
228
+ throw new Error(`Stellar builder: liquidate return value must be a u64, got ${typeof native}`);
229
+ }
230
+ /**
231
+ * flash_loan(caller, asset: HubAssetKey, amount: i128, receiver, data: Bytes)
232
+ * @param opts - Network, caller, current source sequence and deployment addresses.
233
+ * @param args - Operation arguments; token amounts are decimal base-unit strings.
234
+ * @returns Unsigned XDR; prepare with prepareStellarBuiltTx before wallet signing.
235
+ * @category Lending transactions
236
+ */ export function buildStellarFlashLoanTx(opts, args) {
237
+ return buildTx(opts, 'flash_loan', [
238
+ addr(opts.caller),
239
+ hubAsset(args.hubId, args.asset),
240
+ i128(args.amount),
241
+ addr(args.receiver),
242
+ asStellarBytes(args.data)
243
+ ]);
244
+ }
245
+ /**
246
+ * Build a `migrate_from_blend` controller invocation: atomically moves a Blend
247
+ * V2 position (collateral + supply + debt) into XOXNO at zero flash-loan fee.
248
+ *
249
+ * ABI: `migrate_from_blend(caller, account_id, spoke_id, hub_id, blend_pool,
250
+ * collateral_assets, supply_assets, debt_caps: Vec<(Address, i128)>)`. Pass
251
+ * `accountId = "0"` to open a new account. Each debt cap should slightly exceed
252
+ * the live Blend debt — Blend refunds the excess, reconciled on-chain.
253
+ *
254
+ * Like every builder this emits an unsigned invoke; the nested `submit(from =
255
+ * user)` authorization is materialized by `prepareTransaction` (simulation) and
256
+ * signed by the wallet over the whole envelope. The Blend pool must be approved
257
+ * by the controller. Use an explicit destination hub and spoke; this operation
258
+ * does not consume aggregator route bytes.
259
+ * @param opts - Network, caller, current source sequence and deployment addresses.
260
+ * @param args - Operation arguments; token amounts are decimal base-unit strings.
261
+ * @returns Unsigned XDR; prepare with prepareStellarBuiltTx before wallet signing.
262
+ * @category Lending transactions
263
+ */ export function buildStellarMigrateFromBlendTx(opts, args) {
264
+ return buildTx(opts, 'migrate_from_blend', [
265
+ addr(opts.caller),
266
+ u64(args.accountId),
267
+ encodeSpokeId(args.spokeId),
268
+ u32(args.hubId),
269
+ addr(args.blendPool),
270
+ vec(args.collateralTokens.map(addr)),
271
+ vec(args.supplyTokens.map(addr)),
272
+ tupleAddrAmountVec(args.debtCaps.map((d)=>({
273
+ token: d.token,
274
+ amount: d.cap
275
+ })))
276
+ ]);
277
+ }
278
+ /**
279
+ * multiply(caller, account_id, spoke_id, collateral: HubAssetKey,
280
+ * debt_to_flash_loan: i128, debt: HubAssetKey, mode: PositionMode,
281
+ * swap: Bytes, initial_payment: Option<(HubAssetKey, i128)>,
282
+ * convert_swap: Option<Bytes>) -> u64
283
+ *
284
+ * `mode` is a repr(u32) `PositionMode` → encoded as `scvU32`. The two trailing
285
+ * `Option`s seed an optional initial collateral payment and a secondary swap
286
+ * converting it into the collateral token; both omit to Soroban `Void`.
287
+ * @param opts - Network, caller, current source sequence and deployment addresses.
288
+ * @param args - Operation arguments; token amounts are decimal base-unit strings.
289
+ * @returns Unsigned XDR; prepare with prepareStellarBuiltTx before wallet signing.
290
+ * @category Lending transactions
291
+ */ export function buildStellarMultiplyTx(opts, args) {
292
+ const accountId = args.accountNonce ?? 0;
293
+ return buildTx(opts, 'multiply', [
294
+ addr(opts.caller),
295
+ u64(accountId),
296
+ encodeSpokeId(args.spokeId),
297
+ hubAsset(args.collateral.hubId, args.collateral.asset),
298
+ i128(args.debtToFlashLoan),
299
+ hubAsset(args.debt.hubId, args.debt.asset),
300
+ u32(args.mode),
301
+ asStellarStrategySwapBytes(args.steps),
302
+ option(args.initialPayment, (p)=>tupleHubAssetAmount(p.hubId, p.asset, p.amount)),
303
+ option(args.convertSwap, asStellarStrategySwapBytes)
304
+ ]);
305
+ }
306
+ /**
307
+ * swap_debt(caller, account_id, existing_debt: HubAssetKey, amount: i128,
308
+ * new_debt: HubAssetKey, swap: Bytes)
309
+ * @param opts - Network, caller, current source sequence and deployment addresses.
310
+ * @param args - Operation arguments; token amounts are decimal base-unit strings.
311
+ * @returns Unsigned XDR; prepare with prepareStellarBuiltTx before wallet signing.
312
+ * @category Lending transactions
313
+ */ export function buildStellarSwapDebtTx(opts, args) {
314
+ return buildTx(opts, 'swap_debt', [
315
+ addr(opts.caller),
316
+ u64(args.accountNonce),
317
+ hubAsset(args.existingDebt.hubId, args.existingDebt.asset),
318
+ i128(args.newDebtAmount),
319
+ hubAsset(args.newDebt.hubId, args.newDebt.asset),
320
+ asStellarStrategySwapBytes(args.steps)
321
+ ]);
322
+ }
323
+ /**
324
+ * swap_collateral(caller, account_id, current: HubAssetKey, amount: i128,
325
+ * new: HubAssetKey, swap: Bytes)
326
+ * @param opts - Network, caller, current source sequence and deployment addresses.
327
+ * @param args - Operation arguments; token amounts are decimal base-unit strings.
328
+ * @returns Unsigned XDR; prepare with prepareStellarBuiltTx before wallet signing.
329
+ * @category Lending transactions
330
+ */ export function buildStellarSwapCollateralTx(opts, args) {
331
+ return buildTx(opts, 'swap_collateral', [
332
+ addr(opts.caller),
333
+ u64(args.accountNonce),
334
+ hubAsset(args.current.hubId, args.current.asset),
335
+ i128(args.fromAmount),
336
+ hubAsset(args.newCollateral.hubId, args.newCollateral.asset),
337
+ asStellarStrategySwapBytes(args.steps)
338
+ ]);
339
+ }
340
+ /**
341
+ * repay_debt_with_collateral(caller, account_id, collateral: HubAssetKey,
342
+ * collateral_amount: i128, debt: HubAssetKey,
343
+ * swap: Bytes, close_position: bool)
344
+ * @param opts - Network, caller, current source sequence and deployment addresses.
345
+ * @param args - Operation arguments; token amounts are decimal base-unit strings.
346
+ * @returns Unsigned XDR; prepare with prepareStellarBuiltTx before wallet signing.
347
+ * @category Lending transactions
348
+ */ export function buildStellarRepayDebtWithCollateralTx(opts, args) {
349
+ return buildTx(opts, 'repay_debt_with_collateral', [
350
+ addr(opts.caller),
351
+ u64(args.accountNonce),
352
+ hubAsset(args.collateral.hubId, args.collateral.asset),
353
+ i128(args.collateralAmount),
354
+ hubAsset(args.debt.hubId, args.debt.asset),
355
+ asStellarStrategySwapBytes(args.steps),
356
+ bool(args.closePosition)
357
+ ]);
358
+ }
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Account valuation and risk — the client mirror of
3
+ * `rs-lending-xlm/contracts/controller/src/risk/totals.rs`
4
+ * (`calculate_account_risk_totals`) and the post-action gate in
5
+ * `contracts/controller/src/risk/validation.rs` (`require_post_pool_risk_gates`),
6
+ * as specified in `docs/reference/formulas.md` §Valuation and health.
7
+ *
8
+ * Inputs are the generated API DTOs; all integer math runs in `BigInt` on the
9
+ * raw string fields. Outputs carry both the raw WAD strings and display numbers.
10
+ */ import { BPS, RAY, WAD, baseToRay, baseToShort, mulDivCeil, mulDivFloor, numberToWad, positionValueWad, toBigInt, unscale, wadToNumber } from "./scaled.js";
11
+ /**
12
+ * `common/src/constants/shared.rs::DEFAULT_MIN_BORROW_COLLATERAL_USD_WAD` —
13
+ * the floor `get_min_borrow_collateral_usd` returns until governance changes
14
+ * it. Used only when `liveState.minBorrowCollateralUsdWad` is absent or null.
15
+ */ export const DEFAULT_MIN_BORROW_COLLATERAL_USD_WAD = 5n * WAD;
16
+ /** Key used by `StellarLendingContextDto.reserveDetailsByKey`. */ export const reserveKey = (spokeId, hubId, asset)=>`${spokeId}:${hubId}:${asset}`;
17
+ /** Find the reserve backing a `(spokeId, hubId, asset)` coordinate; throws when missing. */ export function resolveReserve(reserves, spokeId, hubId, asset) {
18
+ const list = Array.isArray(reserves) ? reserves : undefined;
19
+ const found = list ? list.find((r)=>r.spokeId === spokeId && r.hubId === hubId && r.asset === asset) : reserves[reserveKey(spokeId, hubId, asset)] ?? Object.values(reserves).find((r)=>r.spokeId === spokeId && r.hubId === hubId && r.asset === asset);
20
+ if (!found) {
21
+ throw new Error(`Stellar math: no reserve for ${reserveKey(spokeId, hubId, asset)}`);
22
+ }
23
+ return found;
24
+ }
25
+ // ---------------------------------------------------------------------------
26
+ // Live-state divergence guard — mirror of xoxno-ui `use-scaled-amount.ts`
27
+ // `buildStellarLiveSnapshot`: exact per-hub-asset indexes, asset-level price
28
+ // consistency, and the controller oracle flags (a row the contract's
29
+ // fail-closed solvency path would reject must not feed valuations either).
30
+ // ---------------------------------------------------------------------------
31
+ const POSITIVE_INTEGER = /^0*[1-9]\d*$/;
32
+ const normalizedInteger = (value)=>typeof value === 'string' && POSITIVE_INTEGER.test(value) ? BigInt(value).toString() : null;
33
+ export const liveIndexKey = (hubId, asset)=>`${hubId}:${asset}`;
34
+ /** Build the divergence-guarded snapshot from `liveState.indexes`. */ export function buildLiveSnapshot(liveState) {
35
+ const indexes = new Map();
36
+ const prices = new Map();
37
+ const rejectedIndexKeys = new Set();
38
+ const rejectedAssets = new Set();
39
+ for (const row of liveState?.indexes ?? []){
40
+ const key = liveIndexKey(row.hubId, row.asset);
41
+ const supply = normalizedInteger(row.supplyIndex);
42
+ const borrow = normalizedInteger(row.borrowIndex);
43
+ const price = normalizedInteger(row.usdPrice);
44
+ const existing = indexes.get(key);
45
+ const sameAsExisting = existing && normalizedInteger(existing.supplyIndex) === supply && normalizedInteger(existing.borrowIndex) === borrow && normalizedInteger(existing.usdPrice) === price;
46
+ if (supply == null || borrow == null || price == null || existing && !sameAsExisting) {
47
+ rejectedIndexKeys.add(key);
48
+ } else if (!existing) {
49
+ indexes.set(key, row);
50
+ }
51
+ const oracleRejected = row.valid === false || row.stale === true || row.deviation === true;
52
+ const existingPrice = prices.get(row.asset);
53
+ if (price == null || oracleRejected || existingPrice !== undefined && existingPrice.toString() !== price) {
54
+ rejectedAssets.add(row.asset);
55
+ } else if (existingPrice === undefined) {
56
+ prices.set(row.asset, BigInt(price));
57
+ }
58
+ }
59
+ for (const key of rejectedIndexKeys)indexes.delete(key);
60
+ for (const asset of rejectedAssets)prices.delete(asset);
61
+ return {
62
+ indexes,
63
+ prices,
64
+ rejectedIndexKeys,
65
+ rejectedAssets
66
+ };
67
+ }
68
+ /** Weight a floor-valued gate USD by a BPS ratio, flooring (`Bps::apply_to_wad_floor`). */ const weightFloor = (gateUsdWad, bps)=>mulDivFloor(gateUsdWad, BigInt(bps), BPS);
69
+ function finishPositionValue(seed, amountBase, usdWad, gateUsdWad) {
70
+ const isSupply = seed.side === 'supply';
71
+ const effectiveLtv = Math.min(seed.entryLtvBps, seed.entryLiquidationThresholdBps);
72
+ return {
73
+ side: seed.side,
74
+ hubId: seed.hubId,
75
+ asset: seed.asset,
76
+ decimals: seed.decimals,
77
+ scaledRay: seed.scaledRay.toString(),
78
+ indexRay: seed.indexRay.toString(),
79
+ usdPriceWad: seed.priceWad.toString(),
80
+ usdPrice: wadToNumber(seed.priceWad),
81
+ amountBase: amountBase.toString(),
82
+ amountShort: baseToShort(amountBase, seed.decimals),
83
+ usdWad: usdWad.toString(),
84
+ usd: wadToNumber(usdWad),
85
+ gateUsdWad: gateUsdWad.toString(),
86
+ entryLtvBps: seed.entryLtvBps,
87
+ entryLiquidationThresholdBps: seed.entryLiquidationThresholdBps,
88
+ entryLiquidationBonusBps: seed.entryLiquidationBonusBps,
89
+ entryLiquidationFeesBps: seed.entryLiquidationFeesBps,
90
+ ltvWeightedUsdWad: isSupply ? weightFloor(gateUsdWad, effectiveLtv).toString() : '0',
91
+ liquidationWeightedUsdWad: isSupply ? weightFloor(gateUsdWad, seed.entryLiquidationThresholdBps).toString() : '0',
92
+ liveDataUsed: seed.liveDataUsed
93
+ };
94
+ }
95
+ /**
96
+ * Value scaled shares exactly as `calculate_account_risk_totals` does:
97
+ * supply → `position_value` (half-up display/total) and
98
+ * `position_value_floor` (gate); borrow → `position_value_ceil`.
99
+ */ export function valueScaledPosition(seed) {
100
+ if (seed.side === 'supply') {
101
+ const amountBase = unscale(seed.scaledRay, seed.indexRay, seed.decimals, 'floor');
102
+ const usdWad = positionValueWad(seed.scaledRay, seed.indexRay, seed.priceWad, 'halfUp');
103
+ const gateUsdWad = positionValueWad(seed.scaledRay, seed.indexRay, seed.priceWad, 'floor');
104
+ return finishPositionValue(seed, amountBase, usdWad, gateUsdWad);
105
+ }
106
+ const amountBase = unscale(seed.scaledRay, seed.indexRay, seed.decimals, 'ceil');
107
+ const usdWad = positionValueWad(seed.scaledRay, seed.indexRay, seed.priceWad, 'ceil');
108
+ return finishPositionValue(seed, amountBase, usdWad, usdWad);
109
+ }
110
+ /**
111
+ * Shares minted when `amountBase` enters a leg — `scaling.rs`
112
+ * `calculate_scaled_supply` (floor) / `calculate_scaled_borrow` (ceil), the
113
+ * only entry conversions the pool performs (`ops/supply.rs`, `ops/borrow.rs`).
114
+ */ export function mintShares(side, amountBase, decimals, indexRay) {
115
+ const amountRay = baseToRay(amountBase, decimals);
116
+ return side === 'supply' ? mulDivFloor(amountRay, RAY, indexRay) : mulDivCeil(amountRay, RAY, indexRay);
117
+ }
118
+ /**
119
+ * Shares burned by a partial exit of `amountBase` — `formulas.md` rounding
120
+ * table: "Partial withdrawal burn: ceil", "Partial repayment burn: floor".
121
+ * Full closes (withdraw ≥ half-up balance, repay ≥ ceil debt) burn every
122
+ * share instead; callers handle that boundary.
123
+ */ export function burnShares(side, amountBase, decimals, indexRay) {
124
+ const amountRay = baseToRay(amountBase, decimals);
125
+ return side === 'supply' ? mulDivCeil(amountRay, RAY, indexRay) : mulDivFloor(amountRay, RAY, indexRay);
126
+ }
127
+ /**
128
+ * Value a fresh leg entered with `amountBase`: shares come from
129
+ * {@link mintShares} and the valuation runs on those shares exactly as
130
+ * {@link valueScaledPosition} does (`position_value_*` never sees the base
131
+ * amount). `amountBase` on the result is therefore the share-derived balance,
132
+ * which can differ from the input by rounding when the index is not `RAY`.
133
+ */ export function valueBasePosition(seed, amountBase) {
134
+ return valueScaledPosition({
135
+ ...seed,
136
+ scaledRay: mintShares(seed.side, amountBase, seed.decimals, seed.indexRay)
137
+ });
138
+ }
139
+ const firstIndex = (...candidates)=>{
140
+ for (const c of candidates){
141
+ const n = normalizedInteger(c);
142
+ if (n != null) return BigInt(n);
143
+ }
144
+ return RAY;
145
+ };
146
+ function valueDtoPosition(side, pos, reserve, snapshot) {
147
+ const scaledRay = toBigInt(side === 'supply' ? pos.supplyScaledRay : pos.borrowScaledRay, `${side}ScaledRay`);
148
+ if (scaledRay === 0n) return null;
149
+ const liveRow = snapshot.indexes.get(liveIndexKey(pos.hubId, pos.asset));
150
+ const livePrice = snapshot.prices.get(pos.asset);
151
+ const indexRay = side === 'supply' ? firstIndex(liveRow?.supplyIndex, pos.liveSupplyIndexRay, reserve.liveSupplyIndexRay, pos.supplyIndexRay) : firstIndex(liveRow?.borrowIndex, pos.liveBorrowIndexRay, reserve.liveBorrowIndexRay, pos.borrowIndexRay);
152
+ const priceWad = livePrice ?? numberToWad(reserve.usdPrice);
153
+ return valueScaledPosition({
154
+ side,
155
+ hubId: pos.hubId,
156
+ asset: pos.asset,
157
+ decimals: reserve.assetDecimals,
158
+ scaledRay,
159
+ indexRay,
160
+ priceWad,
161
+ entryLtvBps: pos.entryLtvBps,
162
+ entryLiquidationThresholdBps: pos.entryLiquidationThresholdBps,
163
+ entryLiquidationBonusBps: pos.entryLiquidationBonusBps,
164
+ entryLiquidationFeesBps: pos.entryLiquidationFeesBps,
165
+ liveDataUsed: liveRow !== undefined && livePrice !== undefined
166
+ });
167
+ }
168
+ const sumWad = (positions, pick)=>positions.reduce((acc, p)=>acc + BigInt(pick(p)), 0n);
169
+ /**
170
+ * Fold valued positions into the contract's risk totals and the derived
171
+ * limits. `formulas.md`: `health_factor_wad = floor(weighted_collateral_wad *
172
+ * WAD / debt_wad)`; the post-action gate requires `debt <= ltv_collateral`,
173
+ * `HF >= 1 WAD` and `ltv_collateral >= floor` while debt remains.
174
+ */ export function aggregateAccountRisk(identity, supplies, borrows, minBorrowCollateralUsdWad, liveDataValid) {
175
+ const totalCollateral = sumWad(supplies, (p)=>p.usdWad);
176
+ const ltvCollateral = sumWad(supplies, (p)=>p.ltvWeightedUsdWad);
177
+ const weightedCollateral = sumWad(supplies, (p)=>p.liquidationWeightedUsdWad);
178
+ const totalDebt = sumWad(borrows, (p)=>p.usdWad);
179
+ const healthFactorWad = totalDebt === 0n ? null : mulDivFloor(weightedCollateral, WAD, totalDebt);
180
+ const healthPercent = weightedCollateral === 0n ? 0 : Number(totalDebt) / Number(weightedCollateral) * 100;
181
+ const belowFloor = ltvCollateral < minBorrowCollateralUsdWad;
182
+ const availableBorrow = belowFloor || ltvCollateral <= totalDebt ? 0n : ltvCollateral - totalDebt;
183
+ return {
184
+ ...identity,
185
+ supplies,
186
+ borrows,
187
+ totalCollateralUsdWad: totalCollateral.toString(),
188
+ totalCollateralUsd: wadToNumber(totalCollateral),
189
+ ltvWeightedCollateralUsdWad: ltvCollateral.toString(),
190
+ ltvWeightedCollateralUsd: wadToNumber(ltvCollateral),
191
+ liquidationWeightedCollateralUsdWad: weightedCollateral.toString(),
192
+ liquidationWeightedCollateralUsd: wadToNumber(weightedCollateral),
193
+ totalDebtUsdWad: totalDebt.toString(),
194
+ totalDebtUsd: wadToNumber(totalDebt),
195
+ healthFactorWad: healthFactorWad === null ? null : healthFactorWad.toString(),
196
+ healthFactor: healthFactorWad === null ? null : wadToNumber(healthFactorWad),
197
+ healthPercent,
198
+ borrowLimitUsdWad: ltvCollateral.toString(),
199
+ borrowLimitUsd: wadToNumber(ltvCollateral),
200
+ availableBorrowUsdWad: availableBorrow.toString(),
201
+ availableBorrowUsd: wadToNumber(availableBorrow),
202
+ minBorrowCollateralUsdWad: minBorrowCollateralUsdWad.toString(),
203
+ minBorrowCollateralUsd: wadToNumber(minBorrowCollateralUsdWad),
204
+ isLiquidatable: healthFactorWad !== null && healthFactorWad < WAD,
205
+ liveDataValid
206
+ };
207
+ }
208
+ /**
209
+ * Value one account exactly as the controller's risk gates do.
210
+ *
211
+ * - Index: live-state row for `(hubId, asset)` when it passed the divergence
212
+ * guard, else the position's `liveSupplyIndexRay` / `liveBorrowIndexRay`,
213
+ * then the reserve's `live*IndexRay`, and last the position's stored
214
+ * `supplyIndexRay` / `borrowIndexRay` (undocumented in the API schema; the
215
+ * documented fields say "stored index on fallback", so it is treated as the
216
+ * stalest snapshot). The contract accrues to the current index before every
217
+ * gate, so fresher candidates win.
218
+ * - Price: live-state `usdPrice` (WAD) when accepted, else `reserve.usdPrice`
219
+ * converted to WAD.
220
+ * - Weights: the position's own `entry*Bps`, as the contract stores them.
221
+ * - Min-collateral floor: `liveState.minBorrowCollateralUsdWad`, defaulting
222
+ * to 5 USD (`DEFAULT_MIN_BORROW_COLLATERAL_USD_WAD`) when absent or null.
223
+ *
224
+ * Throws when `positions` is empty, mixes account ids, or names a reserve
225
+ * missing from `reserves`.
226
+ */ export function computeAccountRisk(input) {
227
+ const { positions, reserves, liveState } = input;
228
+ const first = positions[0];
229
+ if (!first) throw new Error('Stellar math: computeAccountRisk needs at least one position row');
230
+ for (const p of positions){
231
+ if (p.accountId !== first.accountId) {
232
+ throw new Error(`Stellar math: computeAccountRisk received mixed account ids (${first.accountId}, ${p.accountId})`);
233
+ }
234
+ }
235
+ const snapshot = buildLiveSnapshot(liveState);
236
+ const supplies = [];
237
+ const borrows = [];
238
+ for (const p of positions){
239
+ const reserve = resolveReserve(reserves, p.spokeId, p.hubId, p.asset);
240
+ const supply = valueDtoPosition('supply', p, reserve, snapshot);
241
+ if (supply) supplies.push(supply);
242
+ const borrow = valueDtoPosition('borrow', p, reserve, snapshot);
243
+ if (borrow) borrows.push(borrow);
244
+ }
245
+ const floorWad = liveState?.minBorrowCollateralUsdWad != null ? toBigInt(liveState.minBorrowCollateralUsdWad, 'minBorrowCollateralUsdWad') : DEFAULT_MIN_BORROW_COLLATERAL_USD_WAD;
246
+ const liveDataValid = liveState !== undefined && [
247
+ ...supplies,
248
+ ...borrows
249
+ ].every((p)=>p.liveDataUsed);
250
+ return aggregateAccountRisk({
251
+ accountId: first.accountId,
252
+ owner: first.owner,
253
+ spokeId: first.spokeId,
254
+ mode: first.positionMode
255
+ }, supplies, borrows, floorWad, liveDataValid);
256
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Stellar lending math: contract-exact share conversion, rate curve, account
3
+ * risk, per-action limits and projections in native `BigInt`. Ground truth is
4
+ * `rs-lending-xlm` (`common/src/rates/*.rs`, `contracts/controller/src/risk/*`,
5
+ * `docs/reference/formulas.md`); each function names the source it mirrors.
6
+ * @module Stellar lending math
7
+ */ export * from "./scaled.js";
8
+ export * from "./rates.js";
9
+ export * from "./account.js";
10
+ export * from "./limits.js";
11
+ export * from "./projection.js";