@instadapp/fluid-sui-read-sdk 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,3171 @@
1
+ import { t as __exportAll } from "./chunk-15K8U1wQ.mjs";
2
+ import { deriveDynamicFieldID, deriveObjectID, normalizeStructTag, normalizeSuiAddress, normalizeSuiObjectId, parseStructTag, toHex } from "@mysten/sui/utils";
3
+ import { SuiGrpcClient } from "@mysten/sui/grpc";
4
+ import { SuiJsonRpcClient } from "@mysten/sui/jsonRpc";
5
+ import { bcs } from "@mysten/sui/bcs";
6
+ import { ObjectError } from "@mysten/sui/client";
7
+ import { Transaction } from "@mysten/sui/transactions";
8
+
9
+ //#region src/config.ts
10
+ const same = (id) => ({
11
+ packageId: id,
12
+ originalId: id
13
+ });
14
+ const TOKENS = [
15
+ "0x2::sui::SUI",
16
+ "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC",
17
+ "0x44f838219cf67b058f3b37907b655f226153c18e33dfcd0da559a844fea9b1c1::usdsui::USDSUI"
18
+ ];
19
+ const COMMON_IDS = {
20
+ library: same("0xc3715ec6e2efe31fce7096f93e5986fb57e32ea4d252618563ba0c7ded59565c"),
21
+ liquidity: same("0xd0f7e9baf5bb8c13444fc7abc562103b7632acea5f290ed7ab6bbe6f2191febb"),
22
+ lending: same("0x00207c696eaa1b80e6e001cb0739dff0a554b5561265008d33ca9b071fd1d202")
23
+ };
24
+ /**
25
+ * STAGING deployment on Sui mainnet (staging deployment gist, 2026-09-30). NOT a final production
26
+ * deployment: ids change at the production launch. Build
27
+ * your own config from a deployment record with `configFromDeployment`.
28
+ *
29
+ * Fluid Sui staging-on-mainnet: library / liquidity / lending
30
+ * from 2026-09-18; oracle (RedStone main + Pyth guard) and vaults redeployed 2026-09-30.
31
+ * Vault 1 = VaultMarket<SUI, USDC> 0x3ba55ebc…72b8 on oracle nonce 1 (0x1a690d20…b279).
32
+ * Vault 2 = VaultMarket<SUI, USDSUI> 0x5120…0efa. fTokens: fUSDC, fUSDSUI.
33
+ * Ids match the write SDK (fluid-packages-sui deployments/mainnet.json @ 06828ff, 2026-10-01) and
34
+ * the live staging deployment (re-read on chain 2026-10-10). Alias: network "staging".
35
+ */
36
+ const STAGING = {
37
+ network: "mainnet",
38
+ packages: {
39
+ ...COMMON_IDS,
40
+ lendingRewards: same("0x3a54319b9983e52db29b13bec9952713186cbb1910d223e0358f9cf58af9e199"),
41
+ oracle: same("0xc1e7632c9847370a0f00298defa15d42fcb0e3191cad3bee8451cab5ce144814"),
42
+ vaults: same("0x08a3f6777d66c82ded41f5c02f902be34407005b2e53649b4e4da0bb63ef6f20")
43
+ },
44
+ objects: {
45
+ liquidityRegistry: "0x2b712ae7c38f4b4a09498565a4df8184bda875506b9f65ba8f64cf5662731ccc",
46
+ lendingRegistry: "0xc4f71cb64e6c01216f18fbea012176aa2f3a0a747347c914627cbb5aa0e569d2",
47
+ oracleRegistry: "0x3e431dbb50af805875f60f5207aa2515604a2da9eb91ade21e11cc5a5e30b1bf",
48
+ vaultRegistry: "0x429c416e9c42a96b1c0004bd9b756055636824ec4c08e1500403c7b11da8c8cb",
49
+ liquidityAuth: "0x85b19be86131a7a4b378c1b78e8e9b765a30c8c770de0645bfe0e1cb4da5ddb5",
50
+ lendingAuth: "0xc145f5f8aae7d72bf56a2ba82708976c057367160857146beedfa0b5409a5702",
51
+ rewardsAuth: "0x1da732261bfacdcee28be5ac5fd68917b2fdcaff295a7f1128c0a86d912b44b2",
52
+ oracleAuth: "0x6133bd13291e1d04526c69957b27cf0a9d7422059873ad00e3e34ff1b1ba07d9",
53
+ vaultAdmin: "0x72ef539f4827cd055e38a3c2385a313831652fe4bde650a5d099dfdb95fc98d3"
54
+ },
55
+ oracleVariant: "v2",
56
+ oracleV2: {
57
+ redstoneAdapter: "0x22794c3a37c5320e5acb6b9cdba6e256bc08867e9de8afd2a4b5d8ea7061fea3",
58
+ redstonePackage: "0xfa84e5620fda505bbeb9178316a555e1db36f8932dee44210dd88a6464a76ef6",
59
+ pythPriceInfoObjects: { "0x23d7315113f5b1d3ba7a83604c44b94d79f4fd69af77f804fc7f920a6dc65744": "0x89b2add829cb6fcd017153fff428bc9faec4d06d643ecfc435af5b55a9e987f0" },
60
+ pythStateId: "0x03719fae774ddab3cfcaa53bbc046f0cbe21410019b6280811bf3f9f4b05839d",
61
+ wormholeStateId: "0xdbca52b9fb4f712e25f61f974586d93ac541bcf8389564f0323bb07215168b5c"
62
+ },
63
+ tokens: TOKENS,
64
+ rpcUrls: ["https://fullnode.mainnet.sui.io:443"],
65
+ graphqlUrl: "https://graphql.mainnet.sui.io/graphql"
66
+ };
67
+ /**
68
+ * SUPERSEDED. 2026-09-18 deployment: interim admin oracle (`view::quote`, vault 1 on
69
+ * 0xf36d0d95…651f) + the vaults package built on it. Objects still exist on chain; kept only to
70
+ * read historical state.
71
+ */
72
+ const STAGING_LEGACY = {
73
+ network: "mainnet",
74
+ packages: {
75
+ ...COMMON_IDS,
76
+ oracle: same("0xebe2409c7ea9636f0e446797515cbee2ab809d898b06d99179d9eda59e1e42d5"),
77
+ vaults: same("0xf0f5a9c105384d5b79e212a9c9d3adfa5d030ef4c0cf9e22fa48674f9ad3652a")
78
+ },
79
+ objects: {
80
+ liquidityRegistry: "0x2b712ae7c38f4b4a09498565a4df8184bda875506b9f65ba8f64cf5662731ccc",
81
+ lendingRegistry: "0xc4f71cb64e6c01216f18fbea012176aa2f3a0a747347c914627cbb5aa0e569d2",
82
+ oracleRegistry: "0xe645573b6923398b4fbe952fe7e4aa4acc312b0a1c74c7d7417f812613f3173a",
83
+ vaultRegistry: "0xef8792ea0c069b7bd8e5c86291180135ff391ce5d5bdfaf205aeee593a486f24",
84
+ liquidityAuth: "0x85b19be86131a7a4b378c1b78e8e9b765a30c8c770de0645bfe0e1cb4da5ddb5",
85
+ lendingAuth: "0xc145f5f8aae7d72bf56a2ba82708976c057367160857146beedfa0b5409a5702",
86
+ vaultAdmin: "0x3ab15bc5037f34fcc5b763e4af4c83f2166811444859616d3ffe5049ef808dd1"
87
+ },
88
+ oracleVariant: "admin",
89
+ tokens: TOKENS,
90
+ rpcUrls: ["https://fullnode.mainnet.sui.io:443"],
91
+ graphqlUrl: "https://graphql.mainnet.sui.io/graphql"
92
+ };
93
+ /**
94
+ * @deprecated Misleading name kept for compatibility: this is the STAGING deployment
95
+ * (`STAGING`), not production. Use `STAGING` / network "staging".
96
+ */
97
+ const MAINNET = STAGING;
98
+ /** Default built-in network. Points at the current staging deployment until production ships. */
99
+ const DEFAULT_NETWORK = "staging";
100
+ const CONFIGS = {
101
+ staging: STAGING,
102
+ mainnet: STAGING,
103
+ "staging-legacy": STAGING_LEGACY
104
+ };
105
+ /**
106
+ * Build a read config from a deployment record instead of the built-in ids, e.g.
107
+ * `configFromDeployment(JSON.parse(readFileSync(path, "utf8")), { network: "mainnet", oracleV2 })`.
108
+ * The record has no RedStone package or Pyth feed map, so `oracleV2` comes from the caller.
109
+ * Throws once, listing every required field the record is missing.
110
+ */
111
+ function configFromDeployment(rec, opts) {
112
+ const missing = [];
113
+ const ids = (k) => {
114
+ const p = rec.packages[k];
115
+ if (!p?.packageId) missing.push(`packages.${k}.packageId`);
116
+ return {
117
+ packageId: p?.packageId ?? "",
118
+ originalId: p?.originalId ?? p?.packageId ?? ""
119
+ };
120
+ };
121
+ const obj = (k, name, required = true) => {
122
+ const v = rec.packages[k]?.objects?.[name];
123
+ if (!v && required) missing.push(`packages.${k}.objects.${name}`);
124
+ return v;
125
+ };
126
+ const rw = rec.packages.lending_rewards;
127
+ const config = {
128
+ network: opts.network,
129
+ packages: {
130
+ library: ids("library"),
131
+ liquidity: ids("liquidity"),
132
+ lending: ids("lending"),
133
+ ...rw ? { lendingRewards: ids("lending_rewards") } : {},
134
+ oracle: ids("oracle"),
135
+ vaults: ids("vaults")
136
+ },
137
+ objects: {
138
+ liquidityRegistry: obj("liquidity", "registry"),
139
+ lendingRegistry: obj("lending", "registry"),
140
+ oracleRegistry: obj("oracle", "registry"),
141
+ vaultRegistry: obj("vaults", "registry"),
142
+ liquidityAuth: obj("liquidity", "auth", false),
143
+ lendingAuth: obj("lending", "auth", false),
144
+ rewardsAuth: rw?.objects?.rewardsAuth,
145
+ oracleAuth: obj("oracle", "auth", false),
146
+ vaultAdmin: obj("vaults", "admin", false)
147
+ },
148
+ oracleVariant: "v2",
149
+ oracleV2: opts.oracleV2,
150
+ tokens: opts.tokens ?? TOKENS,
151
+ rpcUrls: opts.rpcUrls,
152
+ graphqlUrl: opts.graphqlUrl
153
+ };
154
+ if (missing.length) throw new Error(`deployment record is missing ${missing.length} required field(s): ${missing.join(", ")}. Package ids alone are not enough; the registry object ids are needed too (see "Deployment record shape" in the README).`);
155
+ return config;
156
+ }
157
+ function resolveConfig(networkOrConfig) {
158
+ const cfg = typeof networkOrConfig === "string" ? CONFIGS[networkOrConfig] : networkOrConfig;
159
+ if (!cfg) throw new Error(`no built-in Fluid config for network "${String(networkOrConfig)}"`);
160
+ const n = (p) => ({
161
+ packageId: normalizeSuiObjectId(p.packageId),
162
+ originalId: normalizeSuiObjectId(p.originalId)
163
+ });
164
+ return {
165
+ ...cfg,
166
+ packages: {
167
+ library: n(cfg.packages.library),
168
+ liquidity: n(cfg.packages.liquidity),
169
+ lending: n(cfg.packages.lending),
170
+ ...cfg.packages.lendingRewards ? { lendingRewards: n(cfg.packages.lendingRewards) } : {},
171
+ oracle: n(cfg.packages.oracle),
172
+ vaults: n(cfg.packages.vaults)
173
+ }
174
+ };
175
+ }
176
+
177
+ //#endregion
178
+ //#region src/math/core.ts
179
+ /** Integer helpers that mirror `fluid_library::math` (all bigint, floor unless named ceil). */
180
+ const FOUR_DECIMALS = 10000n;
181
+ const SECONDS_PER_YEAR = 31536000n;
182
+ /** Liquidity / vault / lending exchange-price precision (1e12). */
183
+ const EXCHANGE_PRICES_PRECISION = 1000000000000n;
184
+ /** `exchange_price_rate_output_decimals` in liquidity (1e17). */
185
+ const RATE_OUTPUT_DECIMALS = 100000000000000000n;
186
+ /** Vault exchange-price scale factor (1e18). */
187
+ const EXCHANGE_PRICE_SCALE_FACTOR = 1000000000000000000n;
188
+ /** Lending return precision (1e14 = 1e12 * 100). */
189
+ const RETURN_PERCENT_PRECISION = 100000000000000n;
190
+ /** Lending max rewards rate (50% in 1e12). */
191
+ const MAX_REWARDS_RATE = 50000000000000n;
192
+ function mulDiv(a, b, c) {
193
+ if (c === 0n) throw new RangeError("mulDiv: division by zero");
194
+ return a * b / c;
195
+ }
196
+ function divCeil(a, b) {
197
+ if (b === 0n) throw new RangeError("divCeil: division by zero");
198
+ const q = a / b;
199
+ return a % b === 0n ? q : q + 1n;
200
+ }
201
+ function mulDivCeil(a, b, c) {
202
+ return divCeil(a * b, c);
203
+ }
204
+ const max = (a, b) => a > b ? a : b;
205
+ const min = (a, b) => a < b ? a : b;
206
+ /** Current unix time in seconds (the default `now` for time-dependent reads). */
207
+ const nowSeconds = () => BigInt(Math.floor(Date.now() / 1e3));
208
+
209
+ //#endregion
210
+ //#region src/context.ts
211
+ const RANK = {
212
+ state: 0,
213
+ fullnode: 1,
214
+ indexer: 2
215
+ };
216
+ var NeedsDiscoveryError = class extends Error {
217
+ constructor(what, needed, onchainFix) {
218
+ super(`${what} cannot be resolved from object state alone. Pass { discovery: "${needed}" } to allow it, or make it pure on-chain: ${onchainFix}`);
219
+ this.what = what;
220
+ this.needed = needed;
221
+ this.onchainFix = onchainFix;
222
+ this.name = "NeedsDiscoveryError";
223
+ }
224
+ };
225
+ function makeContext(client, config, derive, now, discovery = "state") {
226
+ return {
227
+ client,
228
+ config,
229
+ derive,
230
+ now: now ?? nowSeconds,
231
+ discovery,
232
+ allows: (l) => RANK[discovery] >= RANK[l]
233
+ };
234
+ }
235
+
236
+ //#endregion
237
+ //#region src/transport.ts
238
+ var AllEndpointsFailedError = class extends Error {
239
+ constructor(method, errors) {
240
+ super(`all ${errors.length} RPC attempts failed for core.${method}: ` + errors.map((e) => `${e.endpoint}: ${e.error instanceof Error ? e.error.message : String(e.error)}`).join(" | "));
241
+ this.method = method;
242
+ this.errors = errors;
243
+ this.name = "AllEndpointsFailedError";
244
+ }
245
+ };
246
+ function clientForEndpoint(endpoint, network) {
247
+ if (endpoint.startsWith("jsonrpc+")) return new SuiJsonRpcClient({
248
+ url: endpoint.slice(8),
249
+ network
250
+ });
251
+ return new SuiGrpcClient({
252
+ network,
253
+ baseUrl: endpoint
254
+ });
255
+ }
256
+ function withTimeout(p, ms, what) {
257
+ let t;
258
+ return Promise.race([p.finally(() => clearTimeout(t)), new Promise((_, rej) => {
259
+ t = setTimeout(() => rej(/* @__PURE__ */ new Error(`${what} timed out after ${ms}ms`)), ms);
260
+ })]);
261
+ }
262
+ /**
263
+ * One `ClientWithCoreApi` over many endpoints. Accepts endpoint strings or ready clients
264
+ * (label = index). Synchronous core members (e.g. `resolveTransactionPlugin`) come from the
265
+ * currently preferred endpoint.
266
+ */
267
+ function createFailoverClient(endpoints, opts) {
268
+ if (endpoints.length === 0) throw new Error("createFailoverClient: at least one endpoint required");
269
+ const labels = endpoints.map((e, i) => typeof e === "string" ? e : `client#${i}`);
270
+ const clients = endpoints.map((e) => typeof e === "string" ? clientForEndpoint(e, opts.network) : e);
271
+ if (clients.length === 1) return Object.assign(clients[0], {
272
+ endpoints: labels,
273
+ preferred: () => labels[0]
274
+ });
275
+ const timeoutMs = opts.timeoutMs ?? 15e3;
276
+ const rounds = opts.rounds ?? 2;
277
+ let preferred = 0;
278
+ const core = new Proxy({}, { get(_t, prop) {
279
+ const first = clients[preferred].core;
280
+ const member = first[prop];
281
+ if (typeof member !== "function") return member;
282
+ return (...args) => {
283
+ const res = member.apply(first, args);
284
+ if (!(res instanceof Promise)) return res;
285
+ return (async () => {
286
+ const errors = [];
287
+ for (let attempt = 0; attempt < clients.length * rounds; attempt++) {
288
+ const i = (preferred + attempt) % clients.length;
289
+ const c = clients[i].core;
290
+ try {
291
+ const out = await withTimeout(attempt === 0 ? res : c[prop].apply(c, args), timeoutMs, `${labels[i]} core.${String(prop)}`);
292
+ preferred = i;
293
+ return out;
294
+ } catch (error) {
295
+ errors.push({
296
+ endpoint: labels[i],
297
+ error
298
+ });
299
+ }
300
+ }
301
+ throw new AllEndpointsFailedError(String(prop), errors);
302
+ })();
303
+ };
304
+ } });
305
+ return new Proxy(clients[0], { get(_t, prop) {
306
+ if (prop === "core") return core;
307
+ if (prop === "endpoints") return labels;
308
+ if (prop === "preferred") return () => labels[preferred];
309
+ const c = clients[preferred];
310
+ const v = c[prop];
311
+ return typeof v === "function" ? v.bind(c) : v;
312
+ } });
313
+ }
314
+
315
+ //#endregion
316
+ //#region src/bcs.ts
317
+ /**
318
+ * BCS layouts of every Fluid object the reader decodes, transcribed from the DEPLOYED
319
+ * packages (`sui_getNormalizedMoveModulesByPackage`, mainnet v1, 2026-09-30) — not from
320
+ * source, so a source-only change cannot silently break decoding.
321
+ * Single-field wrappers (UID, ID, TimestampS, DurationS, Balance, Supply) encode as their field.
322
+ */
323
+ var bcs_exports = /* @__PURE__ */ __exportAll({
324
+ AdminOracle: () => AdminOracle,
325
+ Branch: () => Branch,
326
+ BranchStatus: () => BranchStatus,
327
+ CapRecord: () => CapRecord,
328
+ Field: () => Field,
329
+ Holder: () => Holder,
330
+ LendingRegistry: () => LendingRegistry,
331
+ LendingState: () => LendingState,
332
+ LiquidityRegistry: () => LiquidityRegistry,
333
+ MoveTick: () => MoveTick,
334
+ OracleRegistry: () => OracleRegistry,
335
+ OracleSource: () => OracleSource,
336
+ OracleSourceType: () => OracleSourceType,
337
+ OracleV2: () => OracleV2,
338
+ PauseFlags: () => PauseFlags,
339
+ Position: () => Position,
340
+ PositionIndex: () => PositionIndex$1,
341
+ PositionIndexEntry: () => PositionIndexEntry,
342
+ PositionIndexField: () => PositionIndexField,
343
+ PriceQuote: () => PriceQuote,
344
+ PriceQuoteV2: () => PriceQuoteV2,
345
+ PythPriceInfoObject: () => PythPriceInfoObject,
346
+ RateDataV1Params: () => RateDataV1Params,
347
+ RateDataV2Params: () => RateDataV2Params,
348
+ RateMagnifier: () => RateMagnifier,
349
+ RewardsSchedule: () => RewardsSchedule,
350
+ RiskConfig: () => RiskConfig,
351
+ Table: () => Table,
352
+ TickIdLiquidation: () => TickIdLiquidation,
353
+ TickIdLiquidationKey: () => TickIdLiquidationKey,
354
+ TokenReserve: () => TokenReserve,
355
+ TypeName: () => TypeName,
356
+ UnitKey: () => UnitKey,
357
+ UserBorrowConfig: () => UserBorrowConfig,
358
+ UserBorrowPosition: () => UserBorrowPosition,
359
+ UserSupplyConfig: () => UserSupplyConfig,
360
+ UserSupplyPosition: () => UserSupplyPosition,
361
+ VaultMarket: () => VaultMarket,
362
+ VaultRegistry: () => VaultRegistry,
363
+ VaultTick: () => VaultTick,
364
+ bcsId: () => bcsId,
365
+ bcsU16: () => bcsU16,
366
+ bcsU256: () => bcsU256,
367
+ bcsU64: () => bcsU64
368
+ });
369
+ const Address = bcs.Address;
370
+ const U64 = bcs.u64();
371
+ const U128 = bcs.u128();
372
+ const TypeName = bcs.struct("TypeName", { name: bcs.string() });
373
+ const MoveTick = bcs.struct("Tick", {
374
+ mag: bcs.u32(),
375
+ neg: bcs.bool()
376
+ });
377
+ const Table = bcs.struct("Table", {
378
+ id: Address,
379
+ size: U64
380
+ });
381
+ const PauseFlags = bcs.struct("PauseFlags", {
382
+ supply: bcs.bool(),
383
+ borrow: bcs.bool(),
384
+ payback: bcs.bool(),
385
+ withdraw: bcs.bool()
386
+ });
387
+ const Holder = bcs.enum("Holder", {
388
+ Object: Address,
389
+ Type: TypeName
390
+ });
391
+ const LiquidityRegistry = bcs.struct("Registry", {
392
+ id: Address,
393
+ version: U64,
394
+ pause: PauseFlags,
395
+ revenue_collector: Address,
396
+ active_caps: Table
397
+ });
398
+ const TokenReserve = bcs.struct("TokenReserve", {
399
+ id: Address,
400
+ pause: PauseFlags,
401
+ vault: U64,
402
+ borrow_rate: U64,
403
+ fee: U64,
404
+ last_utilization: U64,
405
+ last_update_timestamp: U64,
406
+ supply_exchange_price: U64,
407
+ borrow_exchange_price: U64,
408
+ max_utilization: U64,
409
+ total_supply_with_interest: U64,
410
+ total_supply_interest_free: U64,
411
+ total_borrow_with_interest: U64,
412
+ total_borrow_interest_free: U64,
413
+ rate_data_version: bcs.u8(),
414
+ supply_positions: Table,
415
+ borrow_positions: Table
416
+ });
417
+ const UserSupplyConfig = bcs.struct("UserSupplyConfig", {
418
+ mode: bcs.u8(),
419
+ expand_percent: U64,
420
+ expand_duration: U64,
421
+ base_withdrawal_limit: U64
422
+ });
423
+ const UserBorrowConfig = bcs.struct("UserBorrowConfig", {
424
+ mode: bcs.u8(),
425
+ expand_percent: U64,
426
+ expand_duration: U64,
427
+ base_debt_ceiling: U64,
428
+ max_debt_ceiling: U64
429
+ });
430
+ const UserSupplyPosition = bcs.struct("UserSupplyPosition", {
431
+ config: bcs.option(UserSupplyConfig),
432
+ paused_supply: bcs.bool(),
433
+ paused_withdraw: bcs.bool(),
434
+ amount: U64,
435
+ withdrawal_limit: U64,
436
+ last_update: U64,
437
+ decay_amount: U64,
438
+ decay_duration: U64
439
+ });
440
+ const UserBorrowPosition = bcs.struct("UserBorrowPosition", {
441
+ config: bcs.option(UserBorrowConfig),
442
+ paused_borrow: bcs.bool(),
443
+ paused_payback: bcs.bool(),
444
+ amount: U64,
445
+ debt_ceiling: U64,
446
+ last_update: U64
447
+ });
448
+ const RateDataV1Params = bcs.struct("RateDataV1Params", {
449
+ kink: U64,
450
+ rate_at_utilization_zero: U64,
451
+ rate_at_utilization_kink: U64,
452
+ rate_at_utilization_max: U64
453
+ });
454
+ const RateDataV2Params = bcs.struct("RateDataV2Params", {
455
+ kink1: U64,
456
+ kink2: U64,
457
+ rate_at_utilization_zero: U64,
458
+ rate_at_utilization_kink1: U64,
459
+ rate_at_utilization_kink2: U64,
460
+ rate_at_utilization_max: U64
461
+ });
462
+ const CapRecord = bcs.struct("CapRecord", {
463
+ token: TypeName,
464
+ holder: Holder
465
+ });
466
+ const LendingState = bcs.struct("LendingState", {
467
+ id: Address,
468
+ supply: U64,
469
+ reserve_id: Address,
470
+ rewards_writer: bcs.option(Address),
471
+ token_exchange_price: U64,
472
+ liquidity_exchange_price: U64,
473
+ last_update_s: U64
474
+ });
475
+ const RewardsSchedule = bcs.struct("RewardsSchedule", {
476
+ start_tvl: U64,
477
+ start_s: U64,
478
+ end_s: U64,
479
+ yearly_reward: U64,
480
+ next_start_s: U64,
481
+ next_end_s: U64,
482
+ next_yearly_reward: U64
483
+ });
484
+ const LendingRegistry = bcs.struct("LendingRegistry", {
485
+ id: Address,
486
+ version: U64
487
+ });
488
+ const RiskConfig = bcs.struct("RiskConfig", {
489
+ collateral_factor: bcs.u16(),
490
+ liquidation_threshold: bcs.u16(),
491
+ liquidation_max_limit: bcs.u16(),
492
+ withdraw_gap: bcs.u16(),
493
+ liquidation_penalty: bcs.u16(),
494
+ borrow_fee: bcs.u8()
495
+ });
496
+ const RateMagnifier = bcs.struct("RateMagnifier", {
497
+ magnitude: bcs.u16(),
498
+ negative: bcs.bool()
499
+ });
500
+ const VaultMarket = bcs.struct("VaultMarket", {
501
+ id: Address,
502
+ vault_id: U64,
503
+ risk: RiskConfig,
504
+ supply_rate_magnifier: RateMagnifier,
505
+ borrow_rate_magnifier: RateMagnifier,
506
+ vault_type: bcs.u8(),
507
+ oracle: Address,
508
+ liquidity_program: Address,
509
+ col_decimals: bcs.u8(),
510
+ debt_decimals: bcs.u8(),
511
+ branch_liquidated: bcs.bool(),
512
+ topmost_tick: MoveTick,
513
+ current_branch_id: U64,
514
+ total_branch_id: U64,
515
+ total_supply: U64,
516
+ total_borrow: U64,
517
+ absorbed_debt_amount: U128,
518
+ absorbed_col_amount: U128,
519
+ absorbed_dust_debt: U64,
520
+ liquidity_supply_exchange_price: U64,
521
+ liquidity_borrow_exchange_price: U64,
522
+ vault_supply_exchange_price: U64,
523
+ vault_borrow_exchange_price: U64,
524
+ last_update_timestamp: U64,
525
+ ticks: Table,
526
+ branches: Table,
527
+ liq_snaps: Table,
528
+ tick_bitmap: Table
529
+ });
530
+ const VaultTick = bcs.struct("Tick", {
531
+ is_liquidated: bcs.bool(),
532
+ total_ids: bcs.u32(),
533
+ raw_debt: U64,
534
+ is_fully_liquidated: bcs.bool(),
535
+ liquidation_branch_id: U64,
536
+ debt_factor: U64
537
+ });
538
+ const TickIdLiquidation = bcs.struct("TickIdLiquidation", {
539
+ is_fully_liquidated: bcs.bool(),
540
+ liquidation_branch_id: U64,
541
+ debt_factor: U64
542
+ });
543
+ const TickIdLiquidationKey = bcs.struct("TickIdLiquidationKey", {
544
+ tick: MoveTick,
545
+ tick_map: bcs.u32()
546
+ });
547
+ const BranchStatus = bcs.enum("BranchStatus", {
548
+ NotLiquidated: null,
549
+ Liquidated: null,
550
+ Merged: null,
551
+ Closed: null
552
+ });
553
+ const Branch = bcs.struct("Branch", {
554
+ status: BranchStatus,
555
+ minima_tick: MoveTick,
556
+ minima_tick_partials: bcs.u32(),
557
+ debt_liquidity: U64,
558
+ debt_factor: U64,
559
+ connected_branch_id: U64,
560
+ connected_minima_tick: MoveTick
561
+ });
562
+ const Position = bcs.struct("Position", {
563
+ id: Address,
564
+ vault_id: Address,
565
+ position_mint: Address,
566
+ is_supply_only_position: bcs.bool(),
567
+ tick: MoveTick,
568
+ tick_id: bcs.u32(),
569
+ supply_amount: U64,
570
+ dust_debt_amount: U64
571
+ });
572
+ const VaultRegistry = bcs.struct("VaultRegistry", {
573
+ id: Address,
574
+ next_vault_id: U64,
575
+ version: U64
576
+ });
577
+ const AdminOracle = bcs.struct("Oracle", {
578
+ id: Address,
579
+ name: bcs.string(),
580
+ rate: U128,
581
+ version: U64
582
+ });
583
+ const OracleRegistry = bcs.struct("OracleRegistry", {
584
+ id: Address,
585
+ version: U64,
586
+ total_oracles: U64
587
+ });
588
+ const PriceQuote = bcs.struct("PriceQuote", {
589
+ oracle_id: Address,
590
+ rate_liquidate: U128,
591
+ rate_operate: U128
592
+ });
593
+ const OracleSourceType = bcs.enum("SourceType", {
594
+ Redstone: bcs.struct("Redstone", { adapter_id: Address }),
595
+ Pyth: null
596
+ });
597
+ const OracleSource = bcs.struct("Source", {
598
+ feed_id: bcs.vector(bcs.u8()),
599
+ invert: bcs.bool(),
600
+ source_type: OracleSourceType
601
+ });
602
+ /** `fluid_oracle::oracle::Oracle` (v2): derived from the registry by nonce. */
603
+ const OracleV2 = bcs.struct("Oracle", {
604
+ id: Address,
605
+ nonce: bcs.u16(),
606
+ main: OracleSource,
607
+ guard: bcs.option(OracleSource)
608
+ });
609
+ /** v2 `view::PriceQuote`: each rate is optional (operate-only / liquidate-only quotes). */
610
+ const PriceQuoteV2 = bcs.struct("PriceQuote", {
611
+ oracle_id: Address,
612
+ rate_liquidate: bcs.option(U128),
613
+ rate_operate: bcs.option(U128)
614
+ });
615
+ const PythI64 = bcs.struct("I64", {
616
+ negative: bcs.bool(),
617
+ magnitude: U64
618
+ });
619
+ const PythPrice = bcs.struct("Price", {
620
+ price: PythI64,
621
+ conf: U64,
622
+ expo: PythI64,
623
+ timestamp: U64
624
+ });
625
+ /** `pyth::price_info::PriceInfoObject`. */
626
+ const PythPriceInfoObject = bcs.struct("PriceInfoObject", {
627
+ id: Address,
628
+ price_info: bcs.struct("PriceInfo", {
629
+ attestation_time: U64,
630
+ arrival_time: U64,
631
+ price_feed: bcs.struct("PriceFeed", {
632
+ price_identifier: bcs.struct("PriceIdentifier", { bytes: bcs.vector(bcs.u8()) }),
633
+ price: PythPrice,
634
+ ema_price: PythPrice
635
+ })
636
+ })
637
+ });
638
+ /** `0x2::dynamic_field::Field<K, V>` wrapper. */
639
+ function Field(name, value) {
640
+ return bcs.struct("Field", {
641
+ id: Address,
642
+ name,
643
+ value
644
+ });
645
+ }
646
+ const bcsU64 = bcs.u64();
647
+ const bcsU16 = bcs.u16();
648
+ const bcsU256 = bcs.u256();
649
+ const bcsId = bcs.Address;
650
+ /** Unit key struct (`RateDataKey`, `RewardsKey`, ...): BCS `dummy_field: bool`. */
651
+ const UnitKey = bcs.bool();
652
+ /** Proposed `fluid_vaults::position_index::PositionIndex` (docs/onchain-position-index.md). */
653
+ const PositionIndex$1 = bcs.struct("PositionIndex", {
654
+ next_position_id: U64,
655
+ caps: Table,
656
+ ordinals: Table
657
+ });
658
+ const PositionIndexField = Field(UnitKey, PositionIndex$1);
659
+ /** `caps: Table<u64, ID>` entry: ordinal -> PositionOwnerCap id. */
660
+ const PositionIndexEntry = Field(U64, Address);
661
+
662
+ //#endregion
663
+ //#region src/math/tick.ts
664
+ var tick_exports = /* @__PURE__ */ __exportAll({
665
+ COLD_TICK: () => COLD_TICK,
666
+ MAX_MASK_DEBT_FACTOR: () => MAX_MASK_DEBT_FACTOR,
667
+ MAX_TICK: () => MAX_TICK,
668
+ MIN_TICK: () => MIN_TICK,
669
+ SHIFT: () => SHIFT,
670
+ TICK_SPACING: () => TICK_SPACING,
671
+ X30: () => X30,
672
+ ZERO_TICK_SCALED_RATIO: () => ZERO_TICK_SCALED_RATIO,
673
+ getRatioAtTick: () => getRatioAtTick,
674
+ mulBigNumber: () => mulBigNumber,
675
+ mulDivNormal: () => mulDivNormal,
676
+ tickFromMove: () => tickFromMove,
677
+ tickToMove: () => tickToMove
678
+ });
679
+ /** Port of `fluid_library::tick_math` + `fluid_library::big_number` (the parts a reader needs). */
680
+ const MAX_TICK = 16383;
681
+ const MIN_TICK = -16383;
682
+ /** `tick_math::cold_tick()` = { mag: 2^31, neg: true }: "no tick" (supply-only / fully liquidated). */
683
+ const COLD_TICK = -2147483648;
684
+ const TICK_SPACING = 10015n;
685
+ const ZERO_TICK_SCALED_RATIO = 281474976710656n;
686
+ const SHIFT = 48n;
687
+ const X30 = 1073741823n;
688
+ const MAX_U128 = (1n << 128n) - 1n;
689
+ const FACTORS = [
690
+ 18419115400608638658n,
691
+ 18391528108445969703n,
692
+ 18336477419114433396n,
693
+ 18226869890870665593n,
694
+ 18009616477100071088n,
695
+ 17582847377087825313n,
696
+ 16759408633341240198n,
697
+ 15226414841393184936n,
698
+ 12568272644527235157n,
699
+ 8563108841104354677n,
700
+ 3975055583337633975n,
701
+ 856577552520149366n,
702
+ 39775317560084773n,
703
+ 85764505686420n
704
+ ];
705
+ /** Move `Tick { mag, neg }` → signed number. */
706
+ function tickFromMove(t) {
707
+ return t.neg ? -t.mag : t.mag;
708
+ }
709
+ /** Signed number → Move `Tick { mag, neg }` (zero is `neg: false`). */
710
+ function tickToMove(tick) {
711
+ return tick < 0 ? {
712
+ mag: -tick,
713
+ neg: true
714
+ } : {
715
+ mag: tick,
716
+ neg: false
717
+ };
718
+ }
719
+ /** ratioX48 = 1.0015^tick * 2^48, bit-exact with `tick_math::get_ratio_at_tick`. */
720
+ function getRatioAtTick(tick) {
721
+ const abs$1 = Math.abs(tick);
722
+ if (abs$1 > MAX_TICK) throw new RangeError(`tick ${tick} out of bounds`);
723
+ let factor = 1n << 64n;
724
+ if (abs$1 & 1) factor = FACTORS[0];
725
+ for (let i = 1; i < FACTORS.length; i++) if (abs$1 & 1 << i) factor = factor * FACTORS[i] >> 64n;
726
+ let precision = 0n;
727
+ if (tick >= 0) {
728
+ factor = MAX_U128 / factor;
729
+ if (factor % 65536n !== 0n) precision = 1n;
730
+ }
731
+ return (factor >> 16n) + precision;
732
+ }
733
+ const EXPONENT_SIZE = 15n;
734
+ const EXPONENT_MAX = 32767n;
735
+ const DECIMALS_DEBT_FACTOR = 16384n;
736
+ const MAX_MASK_DEBT_FACTOR = 1125899906842623n;
737
+ const COEFFICIENT_SIZE = 35n;
738
+ const TWO_POWER_69_MINUS_1 = 590295810358705651711n;
739
+ function msb(x) {
740
+ return x === 0n ? 0n : BigInt(x.toString(2).length);
741
+ }
742
+ /** `big_number::mul_big_number`. */
743
+ function mulBigNumber(a, b) {
744
+ const c = (a >> EXPONENT_SIZE) * (b >> EXPONENT_SIZE);
745
+ const overflowLen = c > TWO_POWER_69_MINUS_1 ? COEFFICIENT_SIZE : COEFFICIENT_SIZE - 1n;
746
+ const adjusted = c >> overflowLen;
747
+ const resExp = (a & EXPONENT_MAX) + (b & EXPONENT_MAX) + overflowLen;
748
+ if (resExp < DECIMALS_DEBT_FACTOR) throw new RangeError("mulBigNumber: exponent underflow");
749
+ const finalExp = resExp - DECIMALS_DEBT_FACTOR;
750
+ if (finalExp > EXPONENT_MAX) return MAX_MASK_DEBT_FACTOR;
751
+ return adjusted << EXPONENT_SIZE | finalExp;
752
+ }
753
+ /** `big_number::mul_div_normal`: normal * bn1 / bn2. */
754
+ function mulDivNormal(normal, bn1, bn2) {
755
+ if (bn1 === 0n || bn2 === 0n) return 0n;
756
+ const e1 = bn1 & EXPONENT_MAX;
757
+ const e2 = bn2 & EXPONENT_MAX;
758
+ if (e2 < e1) throw new RangeError("mulDivNormal: exponent order");
759
+ const net = e2 - e1;
760
+ if (net >= 129n) return 0n;
761
+ const c1 = bn1 >> EXPONENT_SIZE;
762
+ const c2 = bn2 >> EXPONENT_SIZE;
763
+ if (net > 128n - msb(c2)) return 0n;
764
+ const denominator = c2 << net;
765
+ if (denominator === 0n) throw new RangeError("mulDivNormal: division by zero");
766
+ return normal * c1 / denominator;
767
+ }
768
+
769
+ //#endregion
770
+ //#region src/derive.ts
771
+ /** Offline object ids (the Sui equivalent of Solana PDAs). */
772
+ const UNIT = UnitKey.serialize(false).toBytes();
773
+ function createDerive(cfg) {
774
+ const { packages: p, objects: o } = cfg;
775
+ const liq = p.liquidity.originalId;
776
+ const lend = p.lending.originalId;
777
+ const vaults = p.vaults.originalId;
778
+ const lib = p.library.originalId;
779
+ return {
780
+ tokenReserveId: (coinType) => deriveObjectID(o.liquidityRegistry, `${liq}::keys::ReserveKey<${normalizeStructTag(coinType)}>`, UNIT),
781
+ lendingStateId: (coinType) => deriveObjectID(o.lendingRegistry, `${lend}::keys::LendingStateKey<${normalizeStructTag(coinType)}>`, UNIT),
782
+ vaultMarketId: (vaultId) => deriveObjectID(o.vaultRegistry, `${vaults}::keys::VaultKey`, bcs.u64().serialize(BigInt(vaultId)).toBytes()),
783
+ positionId: (vaultMarketId, capId) => deriveObjectID(normalizeSuiAddress(vaultMarketId), `${vaults}::keys::PositionKey`, bcs.Address.serialize(capId).toBytes()),
784
+ holderKeyType: `${liq}::keys::Holder`,
785
+ holderEntryId: (tableId, holderObjectId) => deriveDynamicFieldID(tableId, `${liq}::keys::Holder`, Holder.serialize({ Object: holderObjectId }).toBytes()),
786
+ rateDataId: (reserveId) => deriveDynamicFieldID(reserveId, `${liq}::keys::RateDataKey`, UNIT),
787
+ rewardsId: (lendingStateId) => deriveDynamicFieldID(lendingStateId, `${lend}::keys::RewardsKey`, UNIT),
788
+ tickEntryId: (ticksTableId, tick) => deriveDynamicFieldID(ticksTableId, `${lib}::tick_math::Tick`, MoveTick.serialize(tickToMove(tick)).toBytes()),
789
+ branchEntryId: (branchesTableId, branchId) => deriveDynamicFieldID(branchesTableId, "u64", bcs.u64().serialize(BigInt(branchId)).toBytes()),
790
+ liqSnapEntryId: (snapsTableId, tick, tickId) => deriveDynamicFieldID(snapsTableId, `${vaults}::tick::TickIdLiquidationKey`, TickIdLiquidationKey.serialize({
791
+ tick: tickToMove(tick),
792
+ tick_map: tickId
793
+ }).toBytes()),
794
+ bitmapWordId: (bitmapTableId, word) => deriveDynamicFieldID(bitmapTableId, "u16", bcs.u16().serialize(word).toBytes()),
795
+ positionIndexKeyType: cfg.positionIndexKeyType ?? `${vaults}::position_index::PositionIndexKey`,
796
+ positionIndexId: (vaultMarketId) => deriveDynamicFieldID(normalizeSuiAddress(vaultMarketId), cfg.positionIndexKeyType ?? `${vaults}::position_index::PositionIndexKey`, UNIT),
797
+ positionIndexEntryId: (capsTableId, n) => deriveDynamicFieldID(capsTableId, "u64", bcs.u64().serialize(BigInt(n)).toBytes()),
798
+ oracleId: (nonce) => deriveObjectID(o.oracleRegistry, `${p.oracle.originalId}::keys::OracleKey`, bcs.u16().serialize(nonce).toBytes()),
799
+ fTokenType: (coinType) => `${lend}::lending_state::FToken<${normalizeStructTag(coinType)}>`,
800
+ positionCapType: `${vaults}::position::PositionOwnerCap`
801
+ };
802
+ }
803
+
804
+ //#endregion
805
+ //#region src/math/liquidity.ts
806
+ /** Liquidity-layer maths. Exchange prices + revenue are bit-exact ports of
807
+ * `fluid_liquidity::reserve` / `liquidity_calcs`; the user/overall shaping follows
808
+ * `@jup-ag/lend-read` so consumers get identical field semantics. */
809
+ var liquidity_exports = /* @__PURE__ */ __exportAll({
810
+ calcBorrowLimitBeforeOperate: () => calcBorrowLimitBeforeOperate,
811
+ calcRevenue: () => calcRevenue,
812
+ calcWithdrawalLimitBeforeOperate: () => calcWithdrawalLimitBeforeOperate,
813
+ calculateExchangePrice: () => calculateExchangePrice,
814
+ emptyUserBorrowData: () => emptyUserBorrowData,
815
+ emptyUserSupplyData: () => emptyUserSupplyData,
816
+ processOverallTokenData: () => processOverallTokenData,
817
+ processUserBorrowData: () => processUserBorrowData,
818
+ processUserSupplyData: () => processUserSupplyData,
819
+ totalAmountsOf: () => totalAmountsOf,
820
+ totalBorrowNormal: () => totalBorrowNormal,
821
+ totalSupplyNormal: () => totalSupplyNormal
822
+ });
823
+ function withInterestVsFreeRatio(withInterest, interestFree) {
824
+ if (withInterest > interestFree) return interestFree * FOUR_DECIMALS / withInterest;
825
+ if (withInterest < interestFree) return withInterest * FOUR_DECIMALS / interestFree;
826
+ return withInterest > 0n ? FOUR_DECIMALS : 0n;
827
+ }
828
+ /** `reserve::calculate_exchange_prices(now)`. */
829
+ function calculateExchangePrice(r, now) {
830
+ let supplyExchangePrice = r.supplyExchangePrice;
831
+ let borrowExchangePrice = r.borrowExchangePrice;
832
+ if (supplyExchangePrice === 0n || borrowExchangePrice === 0n) throw new RangeError("exchange price zero (reserve never initialised)");
833
+ const elapsed = now > r.lastUpdateTimestamp ? now - r.lastUpdateTimestamp : 0n;
834
+ if (elapsed === 0n || r.borrowRate === 0n || r.totalBorrowWithInterest === 0n) return {
835
+ supplyExchangePrice,
836
+ borrowExchangePrice
837
+ };
838
+ borrowExchangePrice += divCeil(borrowExchangePrice * r.borrowRate * elapsed, SECONDS_PER_YEAR * FOUR_DECIMALS);
839
+ if (r.totalSupplyWithInterest === 0n) return {
840
+ supplyExchangePrice,
841
+ borrowExchangePrice
842
+ };
843
+ const supplyWithInterestNormal = divCeil(r.totalSupplyWithInterest * supplyExchangePrice, EXCHANGE_PRICES_PRECISION);
844
+ let supplyRatio = withInterestVsFreeRatio(supplyWithInterestNormal, r.totalSupplyInterestFree);
845
+ let ratioSupplyYield;
846
+ if (supplyWithInterestNormal < r.totalSupplyInterestFree) {
847
+ if (supplyRatio === 0n) return {
848
+ supplyExchangePrice,
849
+ borrowExchangePrice
850
+ };
851
+ supplyRatio = RATE_OUTPUT_DECIMALS * FOUR_DECIMALS / supplyRatio;
852
+ ratioSupplyYield = r.lastUtilization * (RATE_OUTPUT_DECIMALS + supplyRatio) / FOUR_DECIMALS;
853
+ } else ratioSupplyYield = r.lastUtilization * RATE_OUTPUT_DECIMALS * (FOUR_DECIMALS + supplyRatio) / (FOUR_DECIMALS * FOUR_DECIMALS);
854
+ const borrowWithInterestNormal = r.totalBorrowWithInterest * borrowExchangePrice / EXCHANGE_PRICES_PRECISION;
855
+ let borrowRatio = withInterestVsFreeRatio(borrowWithInterestNormal, r.totalBorrowInterestFree);
856
+ borrowRatio = borrowWithInterestNormal < r.totalBorrowInterestFree ? borrowRatio * RATE_OUTPUT_DECIMALS / (FOUR_DECIMALS + borrowRatio) : RATE_OUTPUT_DECIMALS - borrowRatio * RATE_OUTPUT_DECIMALS / (FOUR_DECIMALS + borrowRatio);
857
+ ratioSupplyYield = mulDiv(ratioSupplyYield, borrowRatio, RATE_OUTPUT_DECIMALS);
858
+ ratioSupplyYield = ratioSupplyYield * FOUR_DECIMALS / RATE_OUTPUT_DECIMALS;
859
+ const supplyRate = r.borrowRate * ratioSupplyYield * (FOUR_DECIMALS - r.fee);
860
+ supplyExchangePrice += supplyExchangePrice * supplyRate * elapsed / (SECONDS_PER_YEAR * FOUR_DECIMALS) / (FOUR_DECIMALS * FOUR_DECIMALS);
861
+ return {
862
+ supplyExchangePrice,
863
+ borrowExchangePrice
864
+ };
865
+ }
866
+ /** `liquidity_calcs::get_total_supply` (floor). */
867
+ const totalSupplyNormal = (r, supplyEp) => r.totalSupplyInterestFree + r.totalSupplyWithInterest * supplyEp / EXCHANGE_PRICES_PRECISION;
868
+ /** `liquidity_calcs::get_total_borrow` (floor). */
869
+ const totalBorrowNormal = (r, borrowEp) => r.totalBorrowInterestFree + r.totalBorrowWithInterest * borrowEp / EXCHANGE_PRICES_PRECISION;
870
+ /** `reserve::calc_revenue` (supply side ceiled, saturating at 0). */
871
+ function calcRevenue(r, supplyEp, borrowEp) {
872
+ const totalSupply = r.totalSupplyInterestFree + divCeil(r.totalSupplyWithInterest * supplyEp, EXCHANGE_PRICES_PRECISION);
873
+ if (totalSupply === 0n) return r.vaultBalance;
874
+ const revenueAmount = r.vaultBalance + totalBorrowNormal(r, borrowEp);
875
+ return revenueAmount > totalSupply ? revenueAmount - totalSupply : 0n;
876
+ }
877
+ function totalAmountsOf(r) {
878
+ return {
879
+ supplyRawInterest: r.totalSupplyWithInterest,
880
+ supplyInterestFree: r.totalSupplyInterestFree,
881
+ borrowRawInterest: r.totalBorrowWithInterest,
882
+ borrowInterestFree: r.totalBorrowInterestFree,
883
+ totalClaimAmount: 0n
884
+ };
885
+ }
886
+ /** Same shape/semantics as the Solana reader's `processOverallTokenData`. */
887
+ function processOverallTokenData(r, rateData, now) {
888
+ const { supplyExchangePrice, borrowExchangePrice } = calculateExchangePrice(r, now);
889
+ let supplyRate = 0n;
890
+ if (r.totalSupplyWithInterest !== 0n) {
891
+ const borrowForRate = r.totalBorrowWithInterest * r.borrowExchangePrice / EXCHANGE_PRICES_PRECISION;
892
+ const supplyForRate = r.totalSupplyWithInterest * r.supplyExchangePrice / EXCHANGE_PRICES_PRECISION;
893
+ if (supplyForRate !== 0n) supplyRate = r.borrowRate * (FOUR_DECIMALS - r.fee) * borrowForRate / (supplyForRate * FOUR_DECIMALS);
894
+ }
895
+ return {
896
+ rateData,
897
+ supplyExchangePrice,
898
+ borrowExchangePrice,
899
+ borrowRate: r.borrowRate,
900
+ fee: r.fee,
901
+ lastStoredUtilization: r.lastUtilization,
902
+ lastUpdateTimestamp: r.lastUpdateTimestamp,
903
+ maxUtilization: r.maxUtilization,
904
+ supplyRawInterest: r.totalSupplyWithInterest,
905
+ supplyInterestFree: r.totalSupplyInterestFree,
906
+ borrowRawInterest: r.totalBorrowWithInterest,
907
+ borrowInterestFree: r.totalBorrowInterestFree,
908
+ totalSupply: totalSupplyNormal(r, supplyExchangePrice),
909
+ totalBorrow: totalBorrowNormal(r, borrowExchangePrice),
910
+ revenue: calcRevenue(r, supplyExchangePrice, borrowExchangePrice),
911
+ supplyRate
912
+ };
913
+ }
914
+ /** `liquidity_calcs::calc_withdrawal_limit_before_operate`. */
915
+ function calcWithdrawalLimitBeforeOperate(userAmount, lastLimit, expandPercent, expandDuration, lastUpdate, now) {
916
+ if (lastLimit === 0n) return 0n;
917
+ const maxWithdrawable = userAmount * expandPercent / FOUR_DECIMALS;
918
+ const elapsed = now > lastUpdate ? now - lastUpdate : 0n;
919
+ const withdrawableAmount = expandDuration === 0n ? maxWithdrawable : mulDiv(maxWithdrawable, elapsed, expandDuration);
920
+ let limit = lastLimit > withdrawableAmount ? lastLimit - withdrawableAmount : 0n;
921
+ const minLimit = userAmount - maxWithdrawable;
922
+ if (minLimit > limit) limit = minLimit;
923
+ return limit;
924
+ }
925
+ /** `liquidity_calcs::calc_borrow_limit_before_operate`. */
926
+ function calcBorrowLimitBeforeOperate(userAmount, lastLimit, expandPercent, expandDuration, baseLimit, maxLimit, lastUpdate, now) {
927
+ const maxExpansion = userAmount * expandPercent / FOUR_DECIMALS;
928
+ const maxExpanded = userAmount + maxExpansion;
929
+ if (maxExpanded < baseLimit) return baseLimit;
930
+ if (expandDuration === 0n) return baseLimit;
931
+ let limit = mulDiv(maxExpansion, now > lastUpdate ? now - lastUpdate : 0n, expandDuration) + lastLimit;
932
+ if (limit > maxExpanded) limit = maxExpanded;
933
+ if (limit > maxLimit) return maxLimit;
934
+ return limit;
935
+ }
936
+ const toNormal = (raw, ep) => raw * ep / EXCHANGE_PRICES_PRECISION;
937
+ function emptyUserSupplyData() {
938
+ return {
939
+ modeWithInterest: false,
940
+ supply: 0n,
941
+ withdrawalLimit: 0n,
942
+ lastUpdateTimestamp: 0n,
943
+ expandPercent: 0n,
944
+ expandDuration: 0n,
945
+ baseWithdrawalLimit: 0n,
946
+ withdrawableUntilLimit: 0n,
947
+ withdrawable: 0n,
948
+ decayAmount: 0n,
949
+ decayEndTimestamp: 0n
950
+ };
951
+ }
952
+ function emptyUserBorrowData() {
953
+ return {
954
+ modeWithInterest: false,
955
+ borrow: 0n,
956
+ borrowLimit: 0n,
957
+ lastUpdateTimestamp: 0n,
958
+ expandPercent: 0n,
959
+ expandDuration: 0n,
960
+ baseBorrowLimit: 0n,
961
+ maxBorrowLimit: 0n,
962
+ borrowLimitUtilization: 0n,
963
+ borrowableUntilLimit: 0n,
964
+ borrowable: 0n
965
+ };
966
+ }
967
+ /** Solana `processUserSupplyData`, with Sui's decay fields. */
968
+ function processUserSupplyData(acc, overall, liquidityBalance, now) {
969
+ const d = emptyUserSupplyData();
970
+ if (!acc) return d;
971
+ d.modeWithInterest = acc.modeWithInterest;
972
+ d.supply = acc.amount;
973
+ d.withdrawalLimit = calcWithdrawalLimitBeforeOperate(acc.amount, acc.withdrawalLimit, acc.expandPercent, acc.expandDuration, acc.lastUpdateTimestamp, now);
974
+ d.lastUpdateTimestamp = acc.lastUpdateTimestamp;
975
+ d.expandPercent = acc.expandPercent;
976
+ d.expandDuration = acc.expandDuration;
977
+ d.baseWithdrawalLimit = acc.baseWithdrawalLimit;
978
+ let decay = acc.decayAmount;
979
+ let decayEnd = now;
980
+ if (decay > 0n && acc.decayDuration > 0n) {
981
+ const elapsed = now > acc.lastUpdateTimestamp ? now - acc.lastUpdateTimestamp : 0n;
982
+ if (elapsed < acc.decayDuration) {
983
+ decay -= decay * elapsed / acc.decayDuration;
984
+ decayEnd = acc.lastUpdateTimestamp + acc.decayDuration;
985
+ } else decay = 0n;
986
+ }
987
+ d.decayAmount = decay;
988
+ d.decayEndTimestamp = decayEnd;
989
+ if (d.modeWithInterest) {
990
+ const ep = overall.supplyExchangePrice;
991
+ d.supply = toNormal(d.supply, ep);
992
+ d.withdrawalLimit = toNormal(d.withdrawalLimit, ep);
993
+ d.baseWithdrawalLimit = toNormal(d.baseWithdrawalLimit, ep);
994
+ d.decayAmount = toNormal(d.decayAmount, ep);
995
+ }
996
+ d.withdrawableUntilLimit = d.supply > d.withdrawalLimit ? d.supply - d.withdrawalLimit : 0n;
997
+ if (d.withdrawalLimit === 0n) d.withdrawalLimit = d.supply;
998
+ d.withdrawable = liquidityBalance > d.withdrawableUntilLimit ? d.withdrawableUntilLimit : liquidityBalance;
999
+ return d;
1000
+ }
1001
+ /** Solana `processUserBorrowData`. */
1002
+ function processUserBorrowData(acc, overall, liquidityBalance, now) {
1003
+ const d = emptyUserBorrowData();
1004
+ if (!acc) return d;
1005
+ d.modeWithInterest = acc.modeWithInterest;
1006
+ d.borrow = acc.amount;
1007
+ d.borrowLimit = calcBorrowLimitBeforeOperate(acc.amount, acc.borrowLimit, acc.expandPercent, acc.expandDuration, acc.baseBorrowLimit, acc.maxBorrowLimit, acc.lastUpdateTimestamp, now);
1008
+ d.lastUpdateTimestamp = acc.lastUpdateTimestamp;
1009
+ d.expandPercent = acc.expandPercent;
1010
+ d.expandDuration = acc.expandDuration;
1011
+ d.baseBorrowLimit = acc.baseBorrowLimit;
1012
+ d.maxBorrowLimit = acc.maxBorrowLimit;
1013
+ if (d.modeWithInterest) {
1014
+ const ep = overall.borrowExchangePrice;
1015
+ d.borrow = toNormal(d.borrow, ep);
1016
+ d.borrowLimit = toNormal(d.borrowLimit, ep);
1017
+ d.baseBorrowLimit = toNormal(d.baseBorrowLimit, ep);
1018
+ d.maxBorrowLimit = toNormal(d.maxBorrowLimit, ep);
1019
+ }
1020
+ d.borrowLimitUtilization = overall.maxUtilization * overall.totalSupply / FOUR_DECIMALS;
1021
+ const untilUtil = d.borrowLimitUtilization > overall.totalBorrow ? d.borrowLimitUtilization - overall.totalBorrow : 0n;
1022
+ const untilLimit = d.borrowLimit > d.borrow ? d.borrowLimit - d.borrow : 0n;
1023
+ d.borrowableUntilLimit = untilLimit > untilUtil ? untilUtil : untilLimit;
1024
+ d.borrowable = liquidityBalance > d.borrowableUntilLimit ? d.borrowableUntilLimit : liquidityBalance;
1025
+ return d;
1026
+ }
1027
+
1028
+ //#endregion
1029
+ //#region src/math/lending.ts
1030
+ /** Lending (fToken) maths: bit-exact ports of `fluid_lending::accounting`, `lending_calcs`
1031
+ * and `rewards::get_rate_segments`. */
1032
+ var lending_exports = /* @__PURE__ */ __exportAll({
1033
+ accruedTokenExchangePrice: () => accruedTokenExchangePrice,
1034
+ currentRewardsRate: () => currentRewardsRate,
1035
+ previews: () => previews,
1036
+ rateSegments: () => rateSegments,
1037
+ rewardsRateFor: () => rewardsRateFor,
1038
+ toAssets: () => toAssets,
1039
+ toShares: () => toShares
1040
+ });
1041
+ const toShares = (assets, ep, roundUp) => roundUp ? mulDivCeil(assets, EXCHANGE_PRICES_PRECISION, ep) : mulDiv(assets, EXCHANGE_PRICES_PRECISION, ep);
1042
+ const toAssets = (shares, ep, roundUp) => roundUp ? mulDivCeil(shares, ep, EXCHANGE_PRICES_PRECISION) : mulDiv(shares, ep, EXCHANGE_PRICES_PRECISION);
1043
+ /** `lending_calcs::calc_rewards_rate` via `rewards::rate_for`. */
1044
+ function rewardsRateFor(yearlyReward, totalAssets, startTvl) {
1045
+ if (totalAssets < startTvl || totalAssets === 0n) return 0n;
1046
+ const rate = mulDiv(yearlyReward, RETURN_PERCENT_PRECISION, totalAssets);
1047
+ return rate > MAX_REWARDS_RATE ? MAX_REWARDS_RATE : rate;
1048
+ }
1049
+ /** `rewards::get_rate_segments`. */
1050
+ function rateSegments(s, totalAssets, fromS, nowS) {
1051
+ const out = [];
1052
+ const push = (rate, from, to) => {
1053
+ if (to > from && rate > 0n) out.push({
1054
+ rate,
1055
+ fromS: from,
1056
+ toS: to
1057
+ });
1058
+ };
1059
+ push(rewardsRateFor(s.yearlyReward, totalAssets, s.startTvl), maxB(fromS, s.startS), minB(nowS, s.endS));
1060
+ push(rewardsRateFor(s.nextYearlyReward, totalAssets, s.startTvl), maxB(fromS, s.nextStartS), minB(nowS, s.nextEndS));
1061
+ return out;
1062
+ }
1063
+ const maxB = (a, b) => a > b ? a : b;
1064
+ const minB = (a, b) => a < b ? a : b;
1065
+ /** Current rewards rate (1e12 = 100%) at `now`, from the active schedule segment. */
1066
+ function currentRewardsRate(state, now) {
1067
+ if (!state.rewards) return 0n;
1068
+ const tvl = toAssets(state.totalShares, state.tokenExchangePrice, false);
1069
+ const s = state.rewards;
1070
+ if (now >= s.startS && now < s.endS) return rewardsRateFor(s.yearlyReward, tvl, s.startTvl);
1071
+ if (now >= s.nextStartS && now < s.nextEndS) return rewardsRateFor(s.nextYearlyReward, tvl, s.startTvl);
1072
+ return 0n;
1073
+ }
1074
+ /** `accounting::accrued_token_exchange_price`. */
1075
+ function accruedTokenExchangePrice(state, newLiquidityExchangePrice, nowS) {
1076
+ const oldLiq = state.liquidityExchangePrice;
1077
+ const oldToken = state.tokenExchangePrice;
1078
+ if (newLiquidityExchangePrice < oldLiq) throw new RangeError("liquidity exchange price went backwards");
1079
+ let totalReturn = mulDiv(newLiquidityExchangePrice - oldLiq, RETURN_PERCENT_PRECISION, oldLiq);
1080
+ if (state.rewards) {
1081
+ const tvl = toAssets(state.totalShares, oldToken, false);
1082
+ for (const seg of rateSegments(state.rewards, tvl, state.lastUpdateS, nowS)) totalReturn += mulDiv(seg.rate, seg.toS - seg.fromS, SECONDS_PER_YEAR);
1083
+ }
1084
+ return oldToken + mulDiv(oldToken, totalReturn, RETURN_PERCENT_PRECISION);
1085
+ }
1086
+ /** `accounting::preview_*` given the reserve's current liquidity (supply) exchange price. */
1087
+ function previews(state, liquidityExchangePrice, nowS, assets, shares) {
1088
+ const tokenEp = accruedTokenExchangePrice(state, liquidityExchangePrice, nowS);
1089
+ return {
1090
+ tokenExchangePrice: tokenEp,
1091
+ previewDeposit: toShares(toAssets(toShares(assets, liquidityExchangePrice, false), liquidityExchangePrice, false), tokenEp, false),
1092
+ previewMint: toAssets(shares, tokenEp, true),
1093
+ previewWithdraw: toShares(assets, tokenEp, true),
1094
+ previewRedeem: toAssets(shares, tokenEp, false)
1095
+ };
1096
+ }
1097
+
1098
+ //#endregion
1099
+ //#region src/objects.ts
1100
+ /** `getObjects` accepts at most 50 ids per call. */
1101
+ const BATCH = 50;
1102
+ /** Batched fetch. Missing/deleted ids come back as `null` in input order; other errors throw. */
1103
+ async function fetchObjects(client, ids) {
1104
+ const out = [];
1105
+ const chunks = [];
1106
+ for (let i = 0; i < ids.length; i += BATCH) chunks.push(ids.slice(i, i + BATCH));
1107
+ const pages = await Promise.all(chunks.map((c) => client.core.getObjects({
1108
+ objectIds: c,
1109
+ include: { content: true }
1110
+ })));
1111
+ for (const { objects } of pages) for (const o of objects) {
1112
+ if (o instanceof Error) {
1113
+ if (o instanceof ObjectError && (o.reason === "notFound" || o.reason === "deleted")) {
1114
+ out.push(null);
1115
+ continue;
1116
+ }
1117
+ throw o;
1118
+ }
1119
+ out.push({
1120
+ objectId: o.objectId,
1121
+ type: o.type,
1122
+ content: o.content,
1123
+ owner: o.owner
1124
+ });
1125
+ }
1126
+ return out;
1127
+ }
1128
+ var ObjectNotFoundError = class extends Error {
1129
+ constructor(objectId, what) {
1130
+ super(`${what} not found: ${objectId}`);
1131
+ this.objectId = objectId;
1132
+ this.what = what;
1133
+ this.name = "ObjectNotFoundError";
1134
+ }
1135
+ };
1136
+ async function fetchObject(client, id, what) {
1137
+ const [o] = await fetchObjects(client, [id]);
1138
+ if (!o) throw new ObjectNotFoundError(id, what);
1139
+ return o;
1140
+ }
1141
+ /** Every dynamic-field id under a parent (Table), paginated. */
1142
+ async function listDynamicFieldIds(client, parentId) {
1143
+ const ids = [];
1144
+ let cursor = null;
1145
+ do {
1146
+ const page = await client.core.listDynamicFields({
1147
+ parentId,
1148
+ cursor,
1149
+ limit: 50
1150
+ });
1151
+ for (const f of page.dynamicFields) ids.push(f.fieldId);
1152
+ cursor = page.hasNextPage ? page.cursor : null;
1153
+ } while (cursor !== null);
1154
+ return ids;
1155
+ }
1156
+ /** Owner address (or parent object id) from a core-API owner union. */
1157
+ function ownerAddress(owner) {
1158
+ if (!owner || typeof owner !== "object") return null;
1159
+ const o = owner;
1160
+ for (const k of [
1161
+ "AddressOwner",
1162
+ "ObjectOwner",
1163
+ "ConsensusAddressOwner"
1164
+ ]) {
1165
+ const v = o[k];
1166
+ if (typeof v === "string") return v;
1167
+ if (v && typeof v === "object" && typeof v.owner === "string") return v.owner;
1168
+ }
1169
+ if (o.$kind === "Shared") return null;
1170
+ return null;
1171
+ }
1172
+
1173
+ //#endregion
1174
+ //#region src/view.ts
1175
+ /** Any address works as the simulated sender; nothing is executed. */
1176
+ const VIEW_SENDER = "0x0000000000000000000000000000000000000000000000000000000000000000";
1177
+ var ViewCallError = class extends Error {
1178
+ constructor(message, details) {
1179
+ super(message);
1180
+ this.details = details;
1181
+ this.name = "ViewCallError";
1182
+ }
1183
+ };
1184
+ /**
1185
+ * Build a PTB with `build`, simulate it, and return every command's return values as raw BCS.
1186
+ * `results[i][j]` = return value j of command i.
1187
+ */
1188
+ async function simulateView(client, build) {
1189
+ const tx = new Transaction();
1190
+ tx.setSender(VIEW_SENDER);
1191
+ build(tx);
1192
+ const res = await client.core.simulateTransaction({
1193
+ transaction: tx,
1194
+ include: {
1195
+ effects: true,
1196
+ commandResults: true
1197
+ },
1198
+ checksEnabled: false
1199
+ });
1200
+ if (res.$kind !== "Transaction") {
1201
+ const status = res.FailedTransaction?.effects?.status;
1202
+ throw new ViewCallError(`view call failed: ${JSON.stringify(status)}`, status);
1203
+ }
1204
+ return (res.commandResults ?? []).map((c) => c.returnValues.map((v) => v.bcs));
1205
+ }
1206
+
1207
+ //#endregion
1208
+ //#region src/lending.ts
1209
+ /** Lending resolver: mirrors `@jup-ag/lend-read` `Lending`. Sui's share token is
1210
+ * `Coin<FToken<T>>`; Solana `jlToken` names survive only as `@deprecated` aliases of the `fToken` ones. */
1211
+ const RewardsField = Field(UnitKey, RewardsSchedule);
1212
+ var Lending = class {
1213
+ constructor(ctx, liquidity) {
1214
+ this.ctx = ctx;
1215
+ this.liquidity = liquidity;
1216
+ }
1217
+ /** LendingStates (+ rewards fields) for `tokens`; `null` where no pool exists. */
1218
+ async loadLendings(tokens) {
1219
+ const d = this.ctx.derive;
1220
+ const norm = tokens.map((t) => normalizeStructTag(t));
1221
+ const ids = norm.map((t) => d.lendingStateId(t));
1222
+ const objs = await fetchObjects(this.ctx.client, [...ids, ...ids.map((id) => d.rewardsId(id))]);
1223
+ return norm.map((token, i) => {
1224
+ const o = objs[i];
1225
+ if (!o) return null;
1226
+ const raw = LendingState.parse(o.content);
1227
+ const rf = objs[ids.length + i];
1228
+ const rw = rf ? RewardsField.parse(rf.content).value : null;
1229
+ return {
1230
+ token,
1231
+ id: ids[i],
1232
+ raw,
1233
+ snapshot: {
1234
+ totalShares: BigInt(raw.supply),
1235
+ liquidityExchangePrice: BigInt(raw.liquidity_exchange_price),
1236
+ tokenExchangePrice: BigInt(raw.token_exchange_price),
1237
+ lastUpdateS: BigInt(raw.last_update_s),
1238
+ rewards: rw && {
1239
+ startTvl: BigInt(rw.start_tvl),
1240
+ startS: BigInt(rw.start_s),
1241
+ endS: BigInt(rw.end_s),
1242
+ yearlyReward: BigInt(rw.yearly_reward),
1243
+ nextStartS: BigInt(rw.next_start_s),
1244
+ nextEndS: BigInt(rw.next_end_s),
1245
+ nextYearlyReward: BigInt(rw.next_yearly_reward)
1246
+ }
1247
+ }
1248
+ };
1249
+ });
1250
+ }
1251
+ async loadOne(token) {
1252
+ const [[lending], reserve] = await Promise.all([this.loadLendings([token]), this.liquidity.loadReserve(token)]);
1253
+ if (!lending) throw new Error(`no LendingState for ${token}`);
1254
+ return {
1255
+ lending,
1256
+ reserve
1257
+ };
1258
+ }
1259
+ async getAllLendings() {
1260
+ const tokens = await this.liquidity.listedTokens();
1261
+ const recs = (await this.loadLendings(tokens)).filter((r) => r !== null);
1262
+ const metas = await Promise.all(recs.map((r) => this.ctx.client.core.getCoinMetadata({ coinType: r.token })));
1263
+ return recs.map((r, i) => ({
1264
+ id: r.id,
1265
+ mint: r.token,
1266
+ fTokenMint: this.ctx.derive.fTokenType(r.token),
1267
+ decimals: metas[i].coinMetadata?.decimals ?? 0,
1268
+ totalShares: r.snapshot.totalShares,
1269
+ liquidityExchangePrice: r.snapshot.liquidityExchangePrice,
1270
+ tokenExchangePrice: r.snapshot.tokenExchangePrice,
1271
+ lastUpdateTimestamp: r.snapshot.lastUpdateS,
1272
+ tokenReservesLiquidity: r.raw.reserve_id,
1273
+ rewardsWriter: r.raw.rewards_writer ?? null
1274
+ }));
1275
+ }
1276
+ /** Underlying coin types that have a lending pool. */
1277
+ async getAllFTokens() {
1278
+ return (await this.getAllLendings()).map((l) => l.mint);
1279
+ }
1280
+ /** @deprecated Solana name; use `getAllFTokens`. */
1281
+ getAllJlTokens = () => this.getAllFTokens();
1282
+ /** Token exchange price accrued to now (`accounting::calculate_new_token_exchange_price`). */
1283
+ async getExchangePrice(token) {
1284
+ const { lending, reserve } = await this.loadOne(token);
1285
+ const now = this.ctx.now();
1286
+ const liqEp = calculateExchangePrice(reserve.snapshot, now).supplyExchangePrice;
1287
+ return accruedTokenExchangePrice(lending.snapshot, liqEp, now);
1288
+ }
1289
+ async getPreviews(token, assets, shares) {
1290
+ const { lending, reserve } = await this.loadOne(token);
1291
+ const now = this.ctx.now();
1292
+ const liqEp = calculateExchangePrice(reserve.snapshot, now).supplyExchangePrice;
1293
+ const p = previews(lending.snapshot, liqEp, now, assets, shares);
1294
+ return {
1295
+ previewDeposit: p.previewDeposit,
1296
+ previewMint: p.previewMint,
1297
+ previewWithdraw: p.previewWithdraw,
1298
+ previewRedeem: p.previewRedeem
1299
+ };
1300
+ }
1301
+ detailsFromRecords(lending, reserve, supplyEntry, meta, now = this.ctx.now()) {
1302
+ const { userSupplyData, overallTokenData } = this.liquidity.supplyDataFromRecords(reserve, supplyEntry, now);
1303
+ const tokenEp = accruedTokenExchangePrice(lending.snapshot, overallTokenData.supplyExchangePrice, now);
1304
+ const totalAssets = toAssets(lending.snapshot.totalShares, tokenEp, false);
1305
+ const one = 10n ** BigInt(meta.decimals);
1306
+ return {
1307
+ tokenAddress: this.ctx.derive.fTokenType(lending.token),
1308
+ lendingStateId: lending.id,
1309
+ name: meta.name,
1310
+ symbol: meta.symbol,
1311
+ decimals: meta.decimals,
1312
+ underlyingAddress: lending.token,
1313
+ totalAssets,
1314
+ totalSupply: lending.snapshot.totalShares,
1315
+ conversionRateToShares: toShares(one, tokenEp, false),
1316
+ conversionRateToAssets: toAssets(one, tokenEp, false),
1317
+ rewardsRate: currentRewardsRate(lending.snapshot, now),
1318
+ supplyRate: overallTokenData.supplyRate,
1319
+ rebalanceDifference: userSupplyData.supply - totalAssets,
1320
+ userSupplyData
1321
+ };
1322
+ }
1323
+ async fTokenMeta(token) {
1324
+ const [f, u] = await Promise.all([this.ctx.client.core.getCoinMetadata({ coinType: this.ctx.derive.fTokenType(token) }).catch(() => ({ coinMetadata: null })), this.ctx.client.core.getCoinMetadata({ coinType: token })]);
1325
+ const under = u.coinMetadata;
1326
+ return {
1327
+ name: f.coinMetadata?.name ?? `Fluid ${under?.name ?? ""}`.trim(),
1328
+ symbol: f.coinMetadata?.symbol ?? `f${under?.symbol ?? ""}`,
1329
+ decimals: f.coinMetadata?.decimals ?? under?.decimals ?? 0
1330
+ };
1331
+ }
1332
+ async getFTokenDetails(token) {
1333
+ const { lending, reserve } = await this.loadOne(token);
1334
+ const [[entry], meta] = await Promise.all([this.liquidity.loadUserEntries([{
1335
+ holder: lending.id,
1336
+ reserve
1337
+ }]), this.fTokenMeta(lending.token)]);
1338
+ return this.detailsFromRecords(lending, reserve, entry.supply, meta);
1339
+ }
1340
+ /** @deprecated Solana name; use `getFTokenDetails`. */
1341
+ getJlTokenDetails = (token) => this.getFTokenDetails(token);
1342
+ async getAllFTokenDetails() {
1343
+ return Promise.all((await this.getAllFTokens()).map((t) => this.getFTokenDetails(t)));
1344
+ }
1345
+ /** @deprecated Solana name; use `getAllFTokenDetails`. */
1346
+ getAllJlTokenDetails = () => this.getAllFTokenDetails();
1347
+ async getFTokenInternalData(token) {
1348
+ const { lending, reserve } = await this.loadOne(token);
1349
+ const [entry] = await this.liquidity.loadUserEntries([{
1350
+ holder: lending.id,
1351
+ reserve
1352
+ }]);
1353
+ const now = this.ctx.now();
1354
+ const { userSupplyData } = this.liquidity.supplyDataFromRecords(reserve, entry.supply, now);
1355
+ const liqEp = calculateExchangePrice(reserve.snapshot, now).supplyExchangePrice;
1356
+ return {
1357
+ liquidity: reserve.id,
1358
+ lendingFactory: this.ctx.config.objects.lendingRegistry,
1359
+ lendingRewardsRateModel: lending.raw.rewards_writer ?? null,
1360
+ rebalancer: "",
1361
+ liquidityBalance: userSupplyData.supply,
1362
+ liquidityExchangePrice: liqEp,
1363
+ tokenExchangePrice: accruedTokenExchangePrice(lending.snapshot, liqEp, now)
1364
+ };
1365
+ }
1366
+ /** @deprecated Solana name; use `getFTokenInternalData`. */
1367
+ getJlTokenInternalData = (token) => this.getFTokenInternalData(token);
1368
+ async getFTokenRewards(token) {
1369
+ const { lending } = await this.loadOne(token);
1370
+ return [lending.raw.rewards_writer ?? null, currentRewardsRate(lending.snapshot, this.ctx.now())];
1371
+ }
1372
+ /** @deprecated Solana name; use `getFTokenRewards`. */
1373
+ getJlTokenRewards = (token) => this.getFTokenRewards(token);
1374
+ async getFTokenRewardsRateModelConfig(token) {
1375
+ const { lending } = await this.loadOne(token);
1376
+ const r = lending.snapshot.rewards;
1377
+ if (!r) return null;
1378
+ return {
1379
+ duration: r.endS - r.startS,
1380
+ startTime: r.startS,
1381
+ endTime: r.endS,
1382
+ startTvl: r.startTvl,
1383
+ maxRate: 50000000000000n,
1384
+ rewardAmount: r.yearlyReward,
1385
+ nextStartTime: r.nextStartS,
1386
+ nextEndTime: r.nextEndS,
1387
+ nextRewardAmount: r.nextYearlyReward
1388
+ };
1389
+ }
1390
+ /** @deprecated Solana name; use `getFTokenRewardsRateModelConfig`. */
1391
+ getJlTokenRewardsRateModelConfig = (token) => this.getFTokenRewardsRateModelConfig(token);
1392
+ async getUserPosition(token, user) {
1393
+ const [ep, shares, under] = await Promise.all([
1394
+ this.getExchangePrice(token),
1395
+ this.ctx.client.core.getBalance({
1396
+ owner: user,
1397
+ coinType: this.ctx.derive.fTokenType(token)
1398
+ }),
1399
+ this.ctx.client.core.getBalance({
1400
+ owner: user,
1401
+ coinType: normalizeStructTag(token)
1402
+ })
1403
+ ]);
1404
+ const s = BigInt(shares.balance.balance);
1405
+ return {
1406
+ fTokenShares: s,
1407
+ underlyingAssets: toAssets(s, ep, false),
1408
+ underlyingBalance: BigInt(under.balance.balance),
1409
+ allowance: 0n
1410
+ };
1411
+ }
1412
+ async getUserPositions(user) {
1413
+ const tokens = await this.getAllFTokens();
1414
+ return Promise.all(tokens.map(async (t) => ({
1415
+ fTokenDetails: await this.getFTokenDetails(t),
1416
+ userPosition: await this.getUserPosition(t, user)
1417
+ })));
1418
+ }
1419
+ /** `lending::auth::is_admin(&Auth, addr)` via simulation. */
1420
+ async isLendingAdmin(addr) {
1421
+ return this.authView("is_admin", addr);
1422
+ }
1423
+ /** `lending::auth::has_role(&Auth, addr)` (any role) via simulation. */
1424
+ async isLendingAuth(addr) {
1425
+ return this.authView("has_role", addr);
1426
+ }
1427
+ async authView(fn, addr) {
1428
+ const auth = this.ctx.config.objects.lendingAuth;
1429
+ if (!auth) throw new Error("config.objects.lendingAuth not set");
1430
+ const pkg = this.ctx.config.packages.lending.packageId;
1431
+ return (await simulateView(this.ctx.client, (tx) => {
1432
+ tx.moveCall({
1433
+ target: `${pkg}::auth::${fn}`,
1434
+ arguments: [tx.object(auth), tx.pure.address(addr)]
1435
+ });
1436
+ }))[0][0][0] === 1;
1437
+ }
1438
+ };
1439
+
1440
+ //#endregion
1441
+ //#region src/liquidity.ts
1442
+ /** Liquidity resolver: mirrors `@jup-ag/lend-read` `Liquidity`. `user` = a Holder object id
1443
+ * (a VaultMarket id or a LendingState id); `token` = a coin type. */
1444
+ const SupplyField = Field(Holder, UserSupplyPosition);
1445
+ const BorrowField = Field(Holder, UserBorrowPosition);
1446
+ const RateV1Field = Field(UnitKey, RateDataV1Params);
1447
+ const RateV2Field = Field(UnitKey, RateDataV2Params);
1448
+ const CapField = Field(bcsId, CapRecord);
1449
+ const typeNameToCoinType = (name) => normalizeStructTag(name.startsWith("0x") ? name : `0x${name}`);
1450
+ function snapshotOf(r) {
1451
+ return {
1452
+ supplyExchangePrice: BigInt(r.supply_exchange_price),
1453
+ borrowExchangePrice: BigInt(r.borrow_exchange_price),
1454
+ borrowRate: BigInt(r.borrow_rate),
1455
+ fee: BigInt(r.fee),
1456
+ lastUtilization: BigInt(r.last_utilization),
1457
+ lastUpdateTimestamp: BigInt(r.last_update_timestamp),
1458
+ maxUtilization: BigInt(r.max_utilization),
1459
+ totalSupplyWithInterest: BigInt(r.total_supply_with_interest),
1460
+ totalSupplyInterestFree: BigInt(r.total_supply_interest_free),
1461
+ totalBorrowWithInterest: BigInt(r.total_borrow_with_interest),
1462
+ totalBorrowInterestFree: BigInt(r.total_borrow_interest_free),
1463
+ vaultBalance: BigInt(r.vault)
1464
+ };
1465
+ }
1466
+ function rateDataOf(token, version, field) {
1467
+ const v1 = {
1468
+ token,
1469
+ rateAtUtilizationZero: 0n,
1470
+ kink: 0n,
1471
+ rateAtUtilizationKink: 0n,
1472
+ rateAtUtilizationMax: 0n
1473
+ };
1474
+ const v2 = {
1475
+ token,
1476
+ rateAtUtilizationZero: 0n,
1477
+ kink1: 0n,
1478
+ rateAtUtilizationKink1: 0n,
1479
+ kink2: 0n,
1480
+ rateAtUtilizationKink2: 0n,
1481
+ rateAtUtilizationMax: 0n
1482
+ };
1483
+ if (field && version === 1) {
1484
+ const p = RateV1Field.parse(field.content).value;
1485
+ Object.assign(v1, {
1486
+ rateAtUtilizationZero: BigInt(p.rate_at_utilization_zero),
1487
+ kink: BigInt(p.kink),
1488
+ rateAtUtilizationKink: BigInt(p.rate_at_utilization_kink),
1489
+ rateAtUtilizationMax: BigInt(p.rate_at_utilization_max)
1490
+ });
1491
+ } else if (field && version === 2) {
1492
+ const p = RateV2Field.parse(field.content).value;
1493
+ Object.assign(v2, {
1494
+ rateAtUtilizationZero: BigInt(p.rate_at_utilization_zero),
1495
+ kink1: BigInt(p.kink1),
1496
+ rateAtUtilizationKink1: BigInt(p.rate_at_utilization_kink1),
1497
+ kink2: BigInt(p.kink2),
1498
+ rateAtUtilizationKink2: BigInt(p.rate_at_utilization_kink2),
1499
+ rateAtUtilizationMax: BigInt(p.rate_at_utilization_max)
1500
+ });
1501
+ }
1502
+ return {
1503
+ token,
1504
+ version,
1505
+ rateDataV1: v1,
1506
+ rateDataV2: v2
1507
+ };
1508
+ }
1509
+ function supplyAccountOf(user, token, o) {
1510
+ if (!o) return null;
1511
+ const p = SupplyField.parse(o.content).value;
1512
+ return {
1513
+ user,
1514
+ mint: token,
1515
+ amount: BigInt(p.amount),
1516
+ withdrawalLimit: BigInt(p.withdrawal_limit),
1517
+ baseWithdrawalLimit: BigInt(p.config?.base_withdrawal_limit ?? 0),
1518
+ lastUpdateTimestamp: BigInt(p.last_update),
1519
+ expandPercent: BigInt(p.config?.expand_percent ?? 0),
1520
+ expandDuration: BigInt(p.config?.expand_duration ?? 0),
1521
+ modeWithInterest: p.config ? p.config.mode === 1 : false,
1522
+ decayAmount: BigInt(p.decay_amount),
1523
+ decayDuration: BigInt(p.decay_duration),
1524
+ configured: p.config !== null,
1525
+ pausedSupply: p.paused_supply,
1526
+ pausedWithdraw: p.paused_withdraw
1527
+ };
1528
+ }
1529
+ function borrowAccountOf(user, token, o) {
1530
+ if (!o) return null;
1531
+ const p = BorrowField.parse(o.content).value;
1532
+ return {
1533
+ user,
1534
+ mint: token,
1535
+ amount: BigInt(p.amount),
1536
+ borrowLimit: BigInt(p.debt_ceiling),
1537
+ baseBorrowLimit: BigInt(p.config?.base_debt_ceiling ?? 0),
1538
+ maxBorrowLimit: BigInt(p.config?.max_debt_ceiling ?? 0),
1539
+ lastUpdateTimestamp: BigInt(p.last_update),
1540
+ expandPercent: BigInt(p.config?.expand_percent ?? 0),
1541
+ expandDuration: BigInt(p.config?.expand_duration ?? 0),
1542
+ modeWithInterest: p.config ? p.config.mode === 1 : false,
1543
+ configured: p.config !== null,
1544
+ pausedBorrow: p.paused_borrow,
1545
+ pausedPayback: p.paused_payback
1546
+ };
1547
+ }
1548
+ var Liquidity = class {
1549
+ constructor(ctx) {
1550
+ this.ctx = ctx;
1551
+ }
1552
+ /** Reserves + their rate-data fields in 2 batched round trips. */
1553
+ async loadReserves(tokens) {
1554
+ const norm = tokens.map((t) => normalizeStructTag(t));
1555
+ const ids = norm.map((t) => this.ctx.derive.tokenReserveId(t));
1556
+ const objs = await fetchObjects(this.ctx.client, [...ids, ...ids.map((id) => this.ctx.derive.rateDataId(id))]);
1557
+ return norm.map((token, i) => {
1558
+ const o = objs[i];
1559
+ if (!o) throw new Error(`no TokenReserve for ${token} (${ids[i]})`);
1560
+ const raw = TokenReserve.parse(o.content);
1561
+ return {
1562
+ token,
1563
+ id: ids[i],
1564
+ raw,
1565
+ snapshot: snapshotOf(raw),
1566
+ rateData: rateDataOf(token, raw.rate_data_version, objs[ids.length + i] ?? null)
1567
+ };
1568
+ });
1569
+ }
1570
+ async loadReserve(token) {
1571
+ return (await this.loadReserves([token]))[0];
1572
+ }
1573
+ /** Supply/borrow Table entries of `holders` on `reserves` (one batched round trip). */
1574
+ async loadUserEntries(pairs) {
1575
+ const d = this.ctx.derive;
1576
+ const ids = pairs.flatMap(({ holder, reserve }) => [d.holderEntryId(reserve.raw.supply_positions.id, holder), d.holderEntryId(reserve.raw.borrow_positions.id, holder)]);
1577
+ const objs = await fetchObjects(this.ctx.client, ids);
1578
+ return pairs.map(({ holder, reserve }, i) => ({
1579
+ supply: supplyAccountOf(holder, reserve.token, objs[2 * i] ?? null),
1580
+ borrow: borrowAccountOf(holder, reserve.token, objs[2 * i + 1] ?? null)
1581
+ }));
1582
+ }
1583
+ async getLiquidityAccount() {
1584
+ const o = await fetchObject(this.ctx.client, this.ctx.config.objects.liquidityRegistry, "liquidity Registry");
1585
+ const r = LiquidityRegistry.parse(o.content);
1586
+ return {
1587
+ id: r.id,
1588
+ version: BigInt(r.version),
1589
+ revenueCollector: r.revenue_collector,
1590
+ status: r.pause,
1591
+ activeCapsTableId: r.active_caps.id,
1592
+ activeCapsCount: BigInt(r.active_caps.size)
1593
+ };
1594
+ }
1595
+ async getRevenueCollector() {
1596
+ return (await this.getLiquidityAccount()).revenueCollector;
1597
+ }
1598
+ /**
1599
+ * Protocol holders discoverable from object state alone: every VaultMarket (ids 1..total from
1600
+ * `VaultRegistry.next_vault_id`, derived) and every LendingState of a candidate token
1601
+ * (derived). Pure state, no list query.
1602
+ */
1603
+ async discoverProtocolHolders(seedTokens = this.ctx.config.tokens ?? []) {
1604
+ const cfg = this.ctx.config;
1605
+ const reg = VaultRegistry.parse((await fetchObject(this.ctx.client, cfg.objects.vaultRegistry, "VaultRegistry")).content);
1606
+ const n = Number(reg.next_vault_id) - 1;
1607
+ const vaultIds = Array.from({ length: n }, (_, i) => this.ctx.derive.vaultMarketId(i + 1));
1608
+ const vaultObjs = await fetchObjects(this.ctx.client, vaultIds);
1609
+ const vm = cfg.packages.vaults.originalId;
1610
+ const UNIT$1 = UnitKey.serialize(false).toBytes();
1611
+ const capFieldIds = vaultIds.flatMap((id) => [deriveDynamicFieldID(id, `${vm}::vault_market::ColCapKey`, UNIT$1), deriveDynamicFieldID(id, `${vm}::vault_market::DebtCapKey`, UNIT$1)]);
1612
+ const capFields = await fetchObjects(this.ctx.client, capFieldIds);
1613
+ const vaults = vaultObjs.flatMap((o, i) => {
1614
+ if (!o) return [];
1615
+ const [col, debt] = parseStructTag(o.type).typeParams;
1616
+ return [{
1617
+ vaultId: i + 1,
1618
+ id: o.objectId,
1619
+ collateralType: normalizeStructTag(col),
1620
+ debtType: normalizeStructTag(debt),
1621
+ capCount: (capFields[2 * i] ? 1 : 0) + (capFields[2 * i + 1] ? 1 : 0)
1622
+ }];
1623
+ });
1624
+ const tokens = [...new Set([...seedTokens.map((t) => normalizeStructTag(t)), ...vaults.flatMap((v) => [v.collateralType, v.debtType])])];
1625
+ const lendIds = tokens.map((t) => this.ctx.derive.lendingStateId(t));
1626
+ const lendObjs = await fetchObjects(this.ctx.client, lendIds);
1627
+ return {
1628
+ vaults,
1629
+ lendings: tokens.flatMap((token, i) => lendObjs[i] ? [{
1630
+ token,
1631
+ id: lendIds[i]
1632
+ }] : [])
1633
+ };
1634
+ }
1635
+ /**
1636
+ * Token discovery from object state alone (default, no indexer, no list query):
1637
+ * seed tokens (`config.tokens`) ∪ collateral/debt types of every VaultMarket, kept if their
1638
+ * `TokenReserve` exists (derived id). Coverage check: every liquidity user cap is counted in
1639
+ * `Registry.active_caps.size`; caps held by known holders (2 per vault, 1 per LendingState)
1640
+ * are subtracted, and `untrackedCaps > 0` means some other holder exists that state alone
1641
+ * cannot name (a token listed only by a non-Fluid integrator, or a test holder).
1642
+ */
1643
+ async discoverTokens() {
1644
+ const [acc, holders] = await Promise.all([this.getLiquidityAccount(), this.discoverProtocolHolders()]);
1645
+ const candidates = [...new Set([...(this.ctx.config.tokens ?? []).map((t) => normalizeStructTag(t)), ...holders.vaults.flatMap((v) => [v.collateralType, v.debtType])])];
1646
+ const reserves = await fetchObjects(this.ctx.client, candidates.map((t) => this.ctx.derive.tokenReserveId(t)));
1647
+ const tokens = new Set(candidates.filter((_, i) => reserves[i] !== null));
1648
+ const known = holders.vaults.reduce((s, v) => s + v.capCount, 0) + holders.lendings.length;
1649
+ const activeCaps = Number(acc.activeCapsCount);
1650
+ let untrackedCaps = Math.max(0, activeCaps - known);
1651
+ let source = "state";
1652
+ if (untrackedCaps > 0 && this.ctx.allows("fullnode")) {
1653
+ const ids = await listDynamicFieldIds(this.ctx.client, acc.activeCapsTableId);
1654
+ for (const o of await fetchObjects(this.ctx.client, ids)) if (o) tokens.add(typeNameToCoinType(CapField.parse(o.content).value.token.name));
1655
+ untrackedCaps = 0;
1656
+ source = "state+fullnode-index";
1657
+ }
1658
+ return {
1659
+ tokens: [...tokens].sort(),
1660
+ activeCaps,
1661
+ untrackedCaps,
1662
+ source
1663
+ };
1664
+ }
1665
+ /** Listed tokens (see `discoverTokens`; pure state by default). */
1666
+ async listedTokens() {
1667
+ return (await this.discoverTokens()).tokens;
1668
+ }
1669
+ async getExchangePricesAndConfig(token) {
1670
+ const { snapshot: s } = await this.loadReserve(token);
1671
+ return {
1672
+ supplyExchangePrice: s.supplyExchangePrice,
1673
+ borrowExchangePrice: s.borrowExchangePrice,
1674
+ borrowRate: s.borrowRate,
1675
+ fee: s.fee,
1676
+ lastStoredUtilization: s.lastUtilization,
1677
+ lastUpdateTimestamp: s.lastUpdateTimestamp,
1678
+ maxUtilization: s.maxUtilization
1679
+ };
1680
+ }
1681
+ /** Pure: accrue a reserve snapshot to `now` (defaults to the context clock). */
1682
+ calculateExchangePrice(snapshot, now) {
1683
+ return calculateExchangePrice(snapshot, now ?? this.ctx.now());
1684
+ }
1685
+ async getRateConfig(token) {
1686
+ const { rateData: r } = await this.loadReserve(token);
1687
+ if (r.version === 1) return {
1688
+ mint: r.token,
1689
+ version: 1,
1690
+ ...pick(r.rateDataV1, [
1691
+ "rateAtUtilizationZero",
1692
+ "kink",
1693
+ "rateAtUtilizationKink",
1694
+ "rateAtUtilizationMax"
1695
+ ])
1696
+ };
1697
+ if (r.version === 2) return {
1698
+ mint: r.token,
1699
+ version: 2,
1700
+ ...pick(r.rateDataV2, [
1701
+ "rateAtUtilizationZero",
1702
+ "kink1",
1703
+ "rateAtUtilizationKink1",
1704
+ "kink2",
1705
+ "rateAtUtilizationKink2",
1706
+ "rateAtUtilizationMax"
1707
+ ])
1708
+ };
1709
+ return null;
1710
+ }
1711
+ async getTotalAmounts(token) {
1712
+ return totalAmountsOf((await this.loadReserve(token)).snapshot);
1713
+ }
1714
+ async getConfigs2(token) {
1715
+ return { maxUtilization: (await this.loadReserve(token)).snapshot.maxUtilization };
1716
+ }
1717
+ async getRevenue(token) {
1718
+ const { snapshot } = await this.loadReserve(token);
1719
+ const ep = calculateExchangePrice(snapshot, this.ctx.now());
1720
+ return calcRevenue(snapshot, ep.supplyExchangePrice, ep.borrowExchangePrice);
1721
+ }
1722
+ async getTokenRateData(token) {
1723
+ return (await this.loadReserve(token)).rateData;
1724
+ }
1725
+ async getTokensRateData(tokens) {
1726
+ return (await this.loadReserves(tokens)).map((r) => r.rateData);
1727
+ }
1728
+ overallFromRecord(r, now = this.ctx.now()) {
1729
+ return processOverallTokenData(r.snapshot, r.rateData, now);
1730
+ }
1731
+ async getOverallTokenData(token) {
1732
+ return this.overallFromRecord(await this.loadReserve(token));
1733
+ }
1734
+ async getOverallTokensData(tokens) {
1735
+ const now = this.ctx.now();
1736
+ return (await this.loadReserves(tokens)).map((r) => this.overallFromRecord(r, now));
1737
+ }
1738
+ async getAllOverallTokensData() {
1739
+ return this.getOverallTokensData(await this.listedTokens());
1740
+ }
1741
+ /** Raw supply position of a holder, or `0n` when none exists (Solana returns BN 0). */
1742
+ async getUserSupply(user, token) {
1743
+ const reserve = await this.loadReserve(token);
1744
+ const [e] = await this.loadUserEntries([{
1745
+ holder: user,
1746
+ reserve
1747
+ }]);
1748
+ return e.supply ?? 0n;
1749
+ }
1750
+ async getUserBorrow(user, token) {
1751
+ const reserve = await this.loadReserve(token);
1752
+ const [e] = await this.loadUserEntries([{
1753
+ holder: user,
1754
+ reserve
1755
+ }]);
1756
+ return e.borrow ?? 0n;
1757
+ }
1758
+ supplyDataFromRecords(reserve, acc, now = this.ctx.now()) {
1759
+ const overallTokenData = this.overallFromRecord(reserve, now);
1760
+ return {
1761
+ userSupplyData: processUserSupplyData(acc, overallTokenData, reserve.snapshot.vaultBalance, now),
1762
+ overallTokenData
1763
+ };
1764
+ }
1765
+ borrowDataFromRecords(reserve, acc, now = this.ctx.now()) {
1766
+ const overallTokenData = this.overallFromRecord(reserve, now);
1767
+ return {
1768
+ userBorrowData: processUserBorrowData(acc, overallTokenData, reserve.snapshot.vaultBalance, now),
1769
+ overallTokenData
1770
+ };
1771
+ }
1772
+ async getUserSupplyData(user, token) {
1773
+ const reserve = await this.loadReserve(token);
1774
+ const [e] = await this.loadUserEntries([{
1775
+ holder: user,
1776
+ reserve
1777
+ }]);
1778
+ return this.supplyDataFromRecords(reserve, e.supply);
1779
+ }
1780
+ async getUserBorrowData(user, token) {
1781
+ const reserve = await this.loadReserve(token);
1782
+ const [e] = await this.loadUserEntries([{
1783
+ holder: user,
1784
+ reserve
1785
+ }]);
1786
+ return this.borrowDataFromRecords(reserve, e.borrow);
1787
+ }
1788
+ async getUserMultipleSupplyData(user, tokens) {
1789
+ const reserves = await this.loadReserves(tokens);
1790
+ const entries = await this.loadUserEntries(reserves.map((reserve) => ({
1791
+ holder: user,
1792
+ reserve
1793
+ })));
1794
+ const now = this.ctx.now();
1795
+ const out = reserves.map((r, i) => this.supplyDataFromRecords(r, entries[i].supply, now));
1796
+ return {
1797
+ userSuppliesData: out.map((o) => o.userSupplyData),
1798
+ overallTokensData: out.map((o) => o.overallTokenData)
1799
+ };
1800
+ }
1801
+ async getUserMultipleBorrowData(user, tokens) {
1802
+ const reserves = await this.loadReserves(tokens);
1803
+ const entries = await this.loadUserEntries(reserves.map((reserve) => ({
1804
+ holder: user,
1805
+ reserve
1806
+ })));
1807
+ const now = this.ctx.now();
1808
+ const out = reserves.map((r, i) => this.borrowDataFromRecords(r, entries[i].borrow, now));
1809
+ return {
1810
+ userBorrowingsData: out.map((o) => o.userBorrowData),
1811
+ overallTokensData: out.map((o) => o.overallTokenData)
1812
+ };
1813
+ }
1814
+ async getUserMultipleBorrowSupplyData(user, supplyTokens, borrowTokens) {
1815
+ const [s, b] = await Promise.all([this.getUserMultipleSupplyData(user, supplyTokens), this.getUserMultipleBorrowData(user, borrowTokens)]);
1816
+ return {
1817
+ userSuppliesData: s.userSuppliesData,
1818
+ overallSupplyTokensData: s.overallTokensData,
1819
+ userBorrowingsData: b.userBorrowingsData,
1820
+ overallBorrowTokensData: b.overallTokensData
1821
+ };
1822
+ }
1823
+ /**
1824
+ * Every supply/borrow position on every listed reserve, grouped by holder.
1825
+ *
1826
+ * Default ("state"): holders = every VaultMarket + LendingState (derived), entries read by
1827
+ * derived Table-entry id. `untracked` = `Table.size` minus entries found, per reserve: rows of
1828
+ * holders that object state cannot name (non-Fluid integrators). With `discovery: "fullnode"`
1829
+ * the tables are listed and `untracked` is always empty.
1830
+ */
1831
+ async getAllUserPositionsDetailed() {
1832
+ const reserves = await this.loadReserves(await this.listedTokens());
1833
+ if (this.ctx.allows("fullnode")) return {
1834
+ positions: await this.listAllUserPositions(reserves),
1835
+ untracked: [],
1836
+ source: "fullnode-index"
1837
+ };
1838
+ const holders = await this.discoverProtocolHolders(reserves.map((r) => r.token));
1839
+ const holderIds = [...holders.vaults.map((v) => v.id), ...holders.lendings.map((l) => l.id)];
1840
+ const pairs = reserves.flatMap((reserve) => holderIds.map((holder) => ({
1841
+ holder,
1842
+ reserve
1843
+ })));
1844
+ const entries = await this.loadUserEntries(pairs);
1845
+ const byUser = /* @__PURE__ */ new Map();
1846
+ const found = /* @__PURE__ */ new Map();
1847
+ entries.forEach((e, i) => {
1848
+ const { holder, reserve } = pairs[i];
1849
+ const f = found.get(reserve.token) ?? {
1850
+ s: 0,
1851
+ b: 0
1852
+ };
1853
+ found.set(reserve.token, f);
1854
+ if (!e.supply && !e.borrow) return;
1855
+ let u = byUser.get(holder);
1856
+ if (!u) byUser.set(holder, u = {
1857
+ user: holder,
1858
+ supply: {},
1859
+ borrow: {}
1860
+ });
1861
+ if (e.supply) u.supply[reserve.token] = e.supply, f.s++;
1862
+ if (e.borrow) u.borrow[reserve.token] = e.borrow, f.b++;
1863
+ });
1864
+ const untracked = reserves.map((r) => ({
1865
+ token: r.token,
1866
+ supply: Number(r.raw.supply_positions.size) - (found.get(r.token)?.s ?? 0),
1867
+ borrow: Number(r.raw.borrow_positions.size) - (found.get(r.token)?.b ?? 0)
1868
+ })).filter((u) => u.supply > 0 || u.borrow > 0);
1869
+ return {
1870
+ positions: [...byUser.values()],
1871
+ untracked,
1872
+ source: "state"
1873
+ };
1874
+ }
1875
+ /** Positions only (see `getAllUserPositionsDetailed`). */
1876
+ async getAllUserPositions() {
1877
+ return (await this.getAllUserPositionsDetailed()).positions;
1878
+ }
1879
+ /** Fullnode-index tier: lists every Table entry. */
1880
+ async listAllUserPositions(reserves) {
1881
+ const byUser = /* @__PURE__ */ new Map();
1882
+ const get = (u) => {
1883
+ let e = byUser.get(u);
1884
+ if (!e) byUser.set(u, e = {
1885
+ user: u,
1886
+ supply: {},
1887
+ borrow: {}
1888
+ });
1889
+ return e;
1890
+ };
1891
+ for (const r of reserves) {
1892
+ const [sIds, bIds] = await Promise.all([listDynamicFieldIds(this.ctx.client, r.raw.supply_positions.id), listDynamicFieldIds(this.ctx.client, r.raw.borrow_positions.id)]);
1893
+ (await fetchObjects(this.ctx.client, [...sIds, ...bIds])).forEach((o, i) => {
1894
+ if (!o) return;
1895
+ const isSupply = i < sIds.length;
1896
+ const f = (isSupply ? SupplyField : BorrowField).parse(o.content);
1897
+ const user = f.name.$kind === "Object" ? f.name.Object : typeNameToCoinType(f.name.Type.name);
1898
+ if (isSupply) get(user).supply[r.token] = supplyAccountOf(user, r.token, o);
1899
+ else get(user).borrow[r.token] = borrowAccountOf(user, r.token, o);
1900
+ });
1901
+ }
1902
+ return [...byUser.values()];
1903
+ }
1904
+ };
1905
+ function pick(o, keys) {
1906
+ const out = {};
1907
+ for (const k of keys) out[k] = o[k];
1908
+ return out;
1909
+ }
1910
+
1911
+ //#endregion
1912
+ //#region src/oracle.ts
1913
+ /**
1914
+ * Oracle reads.
1915
+ *
1916
+ * v2 (mainnet since 2026-09-30): `fluid_oracle::oracle::Oracle { nonce, main: Source, guard: Option<Source> }`.
1917
+ * A Source is a RedStone feed (on a pinned PriceAdapter) or a Pyth feed (PriceInfoObject). Vaults
1918
+ * consume `view::get_exchange_rate_operate` / `get_exchange_rate_liquidate`:
1919
+ * - operate = min(main, guard); either route stale (>600s) / invalid aborts the quote.
1920
+ * - liquidate = min of the routes still usable at the looser bound (7200s); both dead aborts.
1921
+ * `getOraclePrice` simulates both views exactly as a vault would call them (nothing sent), and
1922
+ * returns `null` for a side whose view aborts, with the abort in `operateError` / `liquidateError`.
1923
+ * `getOracleRoutes` reads each route on its own (RedStone via the adapter's view, Pyth by decoding
1924
+ * the PriceInfoObject) so callers see which route set the price and whether the guard is live.
1925
+ *
1926
+ * admin (2026-09-18 preset only): `Oracle { name, rate }`, `view::quote(&Oracle)`.
1927
+ */
1928
+ /** Oracle rates are debt-per-collateral in the vault's internal 1e9 space, scaled by 1e15. */
1929
+ const ORACLE_RATE_PRECISION = 1000000000000000n;
1930
+ /** Mirrors `fluid_oracle::constants` of the deployed v2 package. */
1931
+ const ORACLE_V2_LIMITS = {
1932
+ rateOutputDecimals: 15,
1933
+ maxAgeOperateS: 600n,
1934
+ maxAgeLiquidateS: 7200n,
1935
+ maxPythArrivalLagS: 60n,
1936
+ confidenceScaleOperate: 50n,
1937
+ confidenceScaleLiquidate: 25n,
1938
+ maxPythExponent: 12n,
1939
+ redstoneDecimals: 8,
1940
+ maxInvertibleRate: 10n ** 24n
1941
+ };
1942
+ const hex = (b) => `0x${toHex(Uint8Array.from(b))}`;
1943
+ function toSource(s) {
1944
+ const st = s.source_type;
1945
+ return {
1946
+ feedId: hex(s.feed_id),
1947
+ invert: s.invert,
1948
+ provider: "Redstone" in st && st.Redstone ? "redstone" : "pyth",
1949
+ ..."Redstone" in st && st.Redstone ? { adapterId: normalizeSuiObjectId(st.Redstone.adapter_id) } : {}
1950
+ };
1951
+ }
1952
+ /** `rate::scale_and_invert`: raw provider price with `decimals` → 1e15 rate, optionally inverted. */
1953
+ function scaleAndInvert(raw, decimals, invert) {
1954
+ const out = ORACLE_V2_LIMITS.rateOutputDecimals;
1955
+ let rate = decimals > out ? raw / 10n ** BigInt(decimals - out) : raw * 10n ** BigInt(out - decimals);
1956
+ if (rate > 0n && invert) rate = rate > ORACLE_V2_LIMITS.maxInvertibleRate ? 0n : ORACLE_RATE_PRECISION * ORACLE_RATE_PRECISION / rate;
1957
+ return rate;
1958
+ }
1959
+ function abortText(e) {
1960
+ if (e instanceof ViewCallError) {
1961
+ const s = JSON.stringify(e.details ?? e.message);
1962
+ const code = /abort[^0-9]*(\d+)/i.exec(s)?.[1] ?? /"abortCode":"?(\d+)/.exec(s)?.[1];
1963
+ return code ? `abort ${code}` : s.slice(0, 200);
1964
+ }
1965
+ return e instanceof Error ? e.message : String(e);
1966
+ }
1967
+ var Oracle = class {
1968
+ constructor(ctx) {
1969
+ this.ctx = ctx;
1970
+ }
1971
+ get v2() {
1972
+ const c = this.ctx.config.oracleV2;
1973
+ if (this.ctx.config.oracleVariant !== "v2" || !c) throw new Error("config has no v2 oracle (oracleVariant/oracleV2)");
1974
+ return c;
1975
+ }
1976
+ /** Admin oracle object (2026-09-18 preset). */
1977
+ async getOracle(oracleId) {
1978
+ const id = normalizeSuiObjectId(oracleId);
1979
+ const o = AdminOracle.parse((await fetchObject(this.ctx.client, id, "Oracle")).content);
1980
+ return {
1981
+ id,
1982
+ name: o.name,
1983
+ rate: BigInt(o.rate),
1984
+ version: BigInt(o.version)
1985
+ };
1986
+ }
1987
+ /** v2 oracle object: nonce + main / guard routes. */
1988
+ async getOracleV2(oracleId) {
1989
+ const id = normalizeSuiObjectId(oracleId);
1990
+ const o = OracleV2.parse((await fetchObject(this.ctx.client, id, "Oracle")).content);
1991
+ return {
1992
+ id,
1993
+ nonce: o.nonce,
1994
+ main: toSource(o.main),
1995
+ guard: o.guard ? toSource(o.guard) : null
1996
+ };
1997
+ }
1998
+ /** Provider objects the v2 views take for this oracle. */
1999
+ providerArgs(o) {
2000
+ const cfg = this.v2;
2001
+ const adapter = [o.main, o.guard].find((s) => s?.provider === "redstone")?.adapterId ?? cfg.redstoneAdapter;
2002
+ const pythFeed = [o.main, o.guard].find((s) => s?.provider === "pyth")?.feedId;
2003
+ const byFeed = Object.fromEntries(Object.entries(cfg.pythPriceInfoObjects).map(([k, v]) => [k.toLowerCase(), v]));
2004
+ const pythInfo = pythFeed ? byFeed[pythFeed.toLowerCase()] : Object.values(cfg.pythPriceInfoObjects)[0];
2005
+ if (!pythInfo) throw new Error(`no Pyth PriceInfoObject configured for feed ${pythFeed ?? "(any)"} (config.oracleV2.pythPriceInfoObjects)`);
2006
+ return {
2007
+ adapter: normalizeSuiObjectId(adapter),
2008
+ pythInfo: normalizeSuiObjectId(pythInfo)
2009
+ };
2010
+ }
2011
+ async quoteV2(o, fn) {
2012
+ const pkg = this.ctx.config.packages.oracle.packageId;
2013
+ const { adapter, pythInfo } = this.providerArgs(o);
2014
+ const res = await simulateView(this.ctx.client, (tx) => {
2015
+ tx.moveCall({
2016
+ target: `${pkg}::view::${fn}`,
2017
+ arguments: [
2018
+ tx.object(o.id),
2019
+ tx.object(adapter),
2020
+ tx.object(pythInfo),
2021
+ tx.object.clock()
2022
+ ]
2023
+ });
2024
+ });
2025
+ const q = PriceQuoteV2.parse(res[0][0]);
2026
+ const r = fn === "get_exchange_rate_operate" ? q.rate_operate : q.rate_liquidate;
2027
+ if (r == null) throw new Error(`${fn} returned no rate`);
2028
+ return BigInt(r);
2029
+ }
2030
+ /** Price exactly as vaults see it. v2: operate and liquidate simulated separately (one can abort alone). */
2031
+ async getOraclePrice(oracleId) {
2032
+ const id = normalizeSuiObjectId(oracleId);
2033
+ if (this.ctx.config.oracleVariant === "admin") {
2034
+ const pkg = this.ctx.config.packages.oracle.packageId;
2035
+ const res = await simulateView(this.ctx.client, (tx) => {
2036
+ tx.moveCall({
2037
+ target: `${pkg}::view::quote`,
2038
+ arguments: [tx.object(id)]
2039
+ });
2040
+ });
2041
+ const q = PriceQuote.parse(res[0][0]);
2042
+ return {
2043
+ oracle: q.oracle_id,
2044
+ operate: BigInt(q.rate_operate),
2045
+ liquidate: BigInt(q.rate_liquidate),
2046
+ source: "admin"
2047
+ };
2048
+ }
2049
+ const o = await this.getOracleV2(id);
2050
+ const [op, liq] = await Promise.allSettled([this.quoteV2(o, "get_exchange_rate_operate"), this.quoteV2(o, "get_exchange_rate_liquidate")]);
2051
+ return {
2052
+ oracle: id,
2053
+ operate: op.status === "fulfilled" ? op.value : null,
2054
+ liquidate: liq.status === "fulfilled" ? liq.value : null,
2055
+ source: "v2",
2056
+ ...op.status === "rejected" ? { operateError: abortText(op.reason) } : {},
2057
+ ...liq.status === "rejected" ? { liquidateError: abortText(liq.reason) } : {}
2058
+ };
2059
+ }
2060
+ /**
2061
+ * Each v2 route read on its own, at the chain's clock: raw price, 1e15 rate, age, and whether it
2062
+ * passes the operate / liquidate checks (`*_core::reading_error`, mirrored off-chain).
2063
+ * Operate price = min over routes; liquidate = min over routes usable for liquidate.
2064
+ */
2065
+ async getOracleRoutes(oracleId) {
2066
+ const o = await this.getOracleV2(oracleId);
2067
+ const cfg = this.v2;
2068
+ const { pythInfo } = this.providerArgs(o);
2069
+ const redstone = [o.main, o.guard].filter((s) => s?.provider === "redstone");
2070
+ const res = await simulateView(this.ctx.client, (tx) => {
2071
+ tx.moveCall({
2072
+ target: "0x2::clock::timestamp_ms",
2073
+ arguments: [tx.object.clock()]
2074
+ });
2075
+ for (const s of redstone) {
2076
+ const feed = tx.pure.vector("u8", Array.from(Buffer.from(s.feedId.slice(2), "hex")));
2077
+ tx.moveCall({
2078
+ target: `${cfg.redstonePackage}::price_adapter::price_and_timestamp`,
2079
+ arguments: [tx.object(s.adapterId), feed]
2080
+ });
2081
+ const feed2 = tx.pure.vector("u8", Array.from(Buffer.from(s.feedId.slice(2), "hex")));
2082
+ tx.moveCall({
2083
+ target: `${cfg.redstonePackage}::price_adapter::get_price_feed_write_timestamp`,
2084
+ arguments: [tx.object(s.adapterId), feed2]
2085
+ });
2086
+ }
2087
+ });
2088
+ const nowMs = BigInt(bcs.u64().parse(res[0][0]));
2089
+ const pyth = o.main.provider === "pyth" || o.guard?.provider === "pyth" ? PythPriceInfoObject.parse((await fetchObject(this.ctx.client, pythInfo, "PriceInfoObject")).content) : null;
2090
+ let rsIdx = 0;
2091
+ const route = (s) => {
2092
+ const L = ORACLE_V2_LIMITS;
2093
+ if (s.provider === "redstone") {
2094
+ const k = 1 + 2 * rsIdx++;
2095
+ const value = BigInt(bcs.u256().parse(res[k][0]));
2096
+ const dataTs = BigInt(bcs.u64().parse(res[k][1]));
2097
+ const wts = bcs.option(bcs.u64()).parse(res[k + 1][0]);
2098
+ const writeTs = wts == null ? dataTs : BigInt(wts);
2099
+ const publishedMs = dataTs < writeTs ? dataTs : writeTs;
2100
+ const ageS = nowMs >= publishedMs ? (nowMs - publishedMs) / 1000n : -1n;
2101
+ const bad = value === 0n || value > 2n ** 64n - 1n;
2102
+ const fresh = (max$1) => nowMs >= publishedMs && nowMs - publishedMs <= max$1 * 1000n;
2103
+ return {
2104
+ ...s,
2105
+ rawPrice: value,
2106
+ decimals: L.redstoneDecimals,
2107
+ rate: bad ? 0n : scaleAndInvert(value, L.redstoneDecimals, s.invert),
2108
+ publishedMs,
2109
+ ageS,
2110
+ usableOperate: !bad && fresh(L.maxAgeOperateS),
2111
+ usableLiquidate: !bad && fresh(L.maxAgeLiquidateS)
2112
+ };
2113
+ }
2114
+ const info = pyth.price_info;
2115
+ const feed = hex(info.price_feed.price_identifier.bytes);
2116
+ if (feed.toLowerCase() !== s.feedId.toLowerCase()) throw new Error(`Pyth PriceInfoObject ${pythInfo} is feed ${feed}, oracle wants ${s.feedId}`);
2117
+ const p = info.price_feed.price;
2118
+ const mag = BigInt(p.price.magnitude);
2119
+ const expoNeg = p.expo.negative;
2120
+ const expo = BigInt(p.expo.magnitude);
2121
+ const conf = BigInt(p.conf);
2122
+ const publishedS = BigInt(p.timestamp);
2123
+ const arrivalS = BigInt(info.arrival_time);
2124
+ const nowS = nowMs / 1000n;
2125
+ const decimals = expoNeg ? Number(expo) : 0;
2126
+ const valid = !p.price.negative && mag > 0n && (expoNeg ? expo <= L.maxPythExponent : expo === 0n) && arrivalS <= publishedS + L.maxPythArrivalLagS;
2127
+ const ok = (maxAge, confScale) => valid && nowS >= publishedS && nowS - publishedS <= maxAge && conf * confScale <= mag;
2128
+ return {
2129
+ ...s,
2130
+ rawPrice: mag,
2131
+ decimals,
2132
+ rate: valid ? scaleAndInvert(mag, decimals, s.invert) : 0n,
2133
+ publishedMs: publishedS * 1000n,
2134
+ ageS: nowS >= publishedS ? nowS - publishedS : -1n,
2135
+ confidence: conf,
2136
+ usableOperate: ok(L.maxAgeOperateS, L.confidenceScaleOperate),
2137
+ usableLiquidate: ok(L.maxAgeLiquidateS, L.confidenceScaleLiquidate)
2138
+ };
2139
+ };
2140
+ return {
2141
+ oracle: o,
2142
+ nowMs,
2143
+ main: route(o.main),
2144
+ guard: o.guard ? route(o.guard) : null
2145
+ };
2146
+ }
2147
+ /** Human price (collateral priced in debt units) from a raw 1e15 rate. */
2148
+ static toNumber(rate) {
2149
+ return Number(rate) / Number(ORACLE_RATE_PRECISION);
2150
+ }
2151
+ };
2152
+
2153
+ //#endregion
2154
+ //#region src/math/vault.ts
2155
+ /** Vault maths: bit-exact ports of `fluid_vaults::exchange_price`, `position::get_position_info`
2156
+ * and `operate_utils::fetch_latest_position`. */
2157
+ var vault_exports = /* @__PURE__ */ __exportAll({
2158
+ BRANCH_STATUS: () => BRANCH_STATUS,
2159
+ currentPositionState: () => currentPositionState,
2160
+ fetchLatestPosition: () => fetchLatestPosition,
2161
+ loadVaultExchangePrices: () => loadVaultExchangePrices,
2162
+ minimumDebt: () => minimumDebt,
2163
+ needsLiquidationSettle: () => needsLiquidationSettle,
2164
+ positionInfo: () => positionInfo,
2165
+ rawToNormal: () => rawToNormal,
2166
+ unscale: () => unscale
2167
+ });
2168
+ const BRANCH_STATUS = {
2169
+ NotLiquidated: 0,
2170
+ Liquidated: 1,
2171
+ Merged: 2,
2172
+ Closed: 3
2173
+ };
2174
+ function rateChange(oldPrice, timeDiff, magnitude) {
2175
+ return oldPrice * timeDiff * magnitude / FOUR_DECIMALS / SECONDS_PER_YEAR;
2176
+ }
2177
+ /** `exchange_price::load_exchange_prices` → `(vaultSupplyExPrice, vaultBorrowExPrice)`. */
2178
+ function loadVaultExchangePrices(p) {
2179
+ if (p.liqSupplyExPrice < p.oldLiqSupplyExPrice || p.liqBorrowExPrice < p.oldLiqBorrowExPrice) throw new RangeError("liquidity exchange price went backwards");
2180
+ const S = EXCHANGE_PRICE_SCALE_FACTOR;
2181
+ let supply = mulDiv(p.vaultSupplyExPrice, mulDiv(p.liqSupplyExPrice, S, p.oldLiqSupplyExPrice), S);
2182
+ if (p.supplyRateMagnifier !== 0n) {
2183
+ const ch = rateChange(p.vaultSupplyExPrice, p.timeDiff, abs(p.supplyRateMagnifier));
2184
+ supply = p.supplyRateMagnifier < 0n ? supply - ch : supply + ch;
2185
+ }
2186
+ let borrow = mulDivCeil(p.vaultBorrowExPrice, mulDiv(p.liqBorrowExPrice, S, p.oldLiqBorrowExPrice), S);
2187
+ if (p.borrowRateMagnifier !== 0n) {
2188
+ const ch = rateChange(p.vaultBorrowExPrice, p.timeDiff, abs(p.borrowRateMagnifier));
2189
+ borrow = p.borrowRateMagnifier < 0n ? borrow - ch : borrow + ch;
2190
+ }
2191
+ return {
2192
+ vaultSupplyExchangePrice: supply,
2193
+ vaultBorrowExchangePrice: borrow
2194
+ };
2195
+ }
2196
+ const abs = (x) => x < 0n ? -x : x;
2197
+ /** `position::get_position_info`: (colRaw, dustDebtRaw, debtRaw, tick, tickId). Debt is rounded up. */
2198
+ function positionInfo(pos) {
2199
+ const colRaw = pos.supplyAmount;
2200
+ const dustDebtRaw = pos.dustDebtAmount;
2201
+ const tick = pos.isSupplyOnlyPosition ? COLD_TICK : pos.tick;
2202
+ const tickId = pos.isSupplyOnlyPosition ? 0 : pos.tickId;
2203
+ return {
2204
+ colRaw,
2205
+ dustDebtRaw,
2206
+ debtRaw: tick > COLD_TICK ? (getRatioAtTick(tick) * (colRaw + 1n) >> SHIFT) + 1n : 0n,
2207
+ tick,
2208
+ tickId
2209
+ };
2210
+ }
2211
+ /**
2212
+ * `operate_utils::fetch_latest_position`: the position's debt/col after liquidations, as
2213
+ * `operate` would settle it. `debtRaw` is the position's pre-liquidation raw debt.
2214
+ */
2215
+ function fetchLatestPosition(debtRaw, tickId, l) {
2216
+ let isFullyLiquidated;
2217
+ let branchId;
2218
+ let connectionFactor;
2219
+ if (l.tick.totalIds === tickId) {
2220
+ isFullyLiquidated = l.tick.isFullyLiquidated;
2221
+ branchId = l.tick.liquidationBranchId;
2222
+ connectionFactor = l.tick.debtFactor;
2223
+ } else {
2224
+ if (!l.snap) throw new Error(`missing liquidation snapshot for tick id ${tickId}`);
2225
+ isFullyLiquidated = l.snap.isFullyLiquidated;
2226
+ branchId = l.snap.liquidationBranchId;
2227
+ connectionFactor = l.snap.debtFactor;
2228
+ }
2229
+ if (isFullyLiquidated) return {
2230
+ tick: COLD_TICK,
2231
+ colRaw: 0n,
2232
+ debtRaw: 0n,
2233
+ branchId
2234
+ };
2235
+ let factor = connectionFactor;
2236
+ let branch = l.branch(branchId);
2237
+ while (branch.status === BRANCH_STATUS.Merged) {
2238
+ factor = mulBigNumber(factor, branch.debtFactor);
2239
+ if (factor === MAX_MASK_DEBT_FACTOR) break;
2240
+ branchId = branch.connectedBranchId;
2241
+ branch = l.branch(branchId);
2242
+ }
2243
+ if (branch.status === BRANCH_STATUS.Closed || factor === MAX_MASK_DEBT_FACTOR) return {
2244
+ tick: COLD_TICK,
2245
+ colRaw: 0n,
2246
+ debtRaw: 0n,
2247
+ branchId
2248
+ };
2249
+ let posDebt = mulDivNormal(debtRaw, branch.debtFactor, factor);
2250
+ posDebt = posDebt > debtRaw / 100n ? mulDiv(posDebt, FOUR_DECIMALS - 1n, FOUR_DECIMALS) : 0n;
2251
+ if (posDebt === 0n) return {
2252
+ tick: COLD_TICK,
2253
+ colRaw: 0n,
2254
+ debtRaw: 0n,
2255
+ branchId
2256
+ };
2257
+ const tick = branch.minimaTick;
2258
+ const ratioAtTick = getRatioAtTick(tick);
2259
+ const ratioOneLess = mulDiv(ratioAtTick, FOUR_DECIMALS, TICK_SPACING);
2260
+ const finalRatio = ratioOneLess + mulDiv(ratioAtTick - ratioOneLess, BigInt(branch.minimaTickPartials), X30);
2261
+ return {
2262
+ tick,
2263
+ colRaw: mulDiv(posDebt, ZERO_TICK_SCALED_RATIO, finalRatio),
2264
+ debtRaw: posDebt,
2265
+ branchId
2266
+ };
2267
+ }
2268
+ /** Whether `operate` would run `fetch_latest_position` for this position. */
2269
+ const needsLiquidationSettle = (tick, tickId) => tick.isLiquidated || tick.totalIds > tickId;
2270
+ /** Current raw state of a position (Solana `getCurrentPositionState`). */
2271
+ function currentPositionState(pos, lookups, debtDecimals = 9) {
2272
+ const info = positionInfo(pos);
2273
+ if (pos.isSupplyOnlyPosition || info.tick === COLD_TICK) return {
2274
+ tick: COLD_TICK,
2275
+ tickId: 0,
2276
+ colRaw: info.colRaw,
2277
+ debtRaw: 0n,
2278
+ dustDebtRaw: 0n,
2279
+ finalAmount: info.colRaw,
2280
+ isSupplyOnlyPosition: pos.isSupplyOnlyPosition,
2281
+ userLiquidationStatus: false,
2282
+ postLiquidationBranchId: 0
2283
+ };
2284
+ let { colRaw, debtRaw, tick } = info;
2285
+ let dustDebtRaw = info.dustDebtRaw;
2286
+ let liquidated = false;
2287
+ let branchId = 0;
2288
+ if (lookups && needsLiquidationSettle(lookups.tick, info.tickId)) {
2289
+ liquidated = true;
2290
+ const latest = fetchLatestPosition(debtRaw, info.tickId, lookups);
2291
+ colRaw = latest.colRaw;
2292
+ debtRaw = latest.debtRaw;
2293
+ tick = latest.tick;
2294
+ branchId = latest.branchId;
2295
+ if (debtRaw < dustDebtRaw || debtRaw < 2n * minimumDebt(debtDecimals)) {
2296
+ debtRaw = 0n;
2297
+ colRaw = 0n;
2298
+ dustDebtRaw = 0n;
2299
+ tick = COLD_TICK;
2300
+ }
2301
+ }
2302
+ const netDebt = debtRaw > dustDebtRaw ? debtRaw - dustDebtRaw : 0n;
2303
+ return {
2304
+ tick,
2305
+ tickId: info.tickId,
2306
+ colRaw,
2307
+ debtRaw,
2308
+ dustDebtRaw,
2309
+ finalAmount: netDebt > 0n ? colRaw : 0n,
2310
+ isSupplyOnlyPosition: false,
2311
+ userLiquidationStatus: liquidated,
2312
+ postLiquidationBranchId: branchId
2313
+ };
2314
+ }
2315
+ /** raw (internal 1e9 space) → normal (still internal space) through a vault exchange price. */
2316
+ const rawToNormal = (raw, exPrice) => raw * exPrice / EXCHANGE_PRICES_PRECISION;
2317
+ /** `constants::get_minimum_debt(decimals)` in the internal 1e9 space. */
2318
+ const minimumDebt = (decimals) => (decimals >= 4 ? 1000n : 10n) * 10n ** BigInt(9 - decimals);
2319
+ /** Internal 1e9 space → token units (`constants::unscale_amounts`, floor). */
2320
+ const unscale = (amount, decimals) => amount / 10n ** BigInt(9 - decimals);
2321
+
2322
+ //#endregion
2323
+ //#region src/positionIndex.ts
2324
+ /**
2325
+ * Per-vault position enumeration + counts on Sui.
2326
+ *
2327
+ * A `Position` is a derived object keyed by its (random) `PositionOwnerCap` id, and the deployed
2328
+ * `VaultMarket` carries no counter or list. So, unlike EVM (`totalPositions` = 32 bits of
2329
+ * `vaultVariables`, +1 per mint) and Solana (`next_position_id` in vault state), the deployed
2330
+ * Sui package gives NO way to enumerate positions from object state alone.
2331
+ *
2332
+ * Resolution order (the first one available under the client's `discovery` level wins):
2333
+ *
2334
+ * 1. "state" - proposed on-chain `PositionIndex` dynamic field on the VaultMarket
2335
+ * (docs/onchain-position-index.md): counter + `Table<u64, ID>` of cap ids, all
2336
+ * read by DERIVED id. Pure state, no list query. Used automatically once shipped.
2337
+ * 2. "fullnode" - `listDynamicFields(vaultMarket)`: every position claims a
2338
+ * `0x2::derived_object::Claimed(position_id)` field on its VaultMarket, and the
2339
+ * claim is never removed (even after close/delete), so the claim set = every
2340
+ * position ever opened (EVM `totalPositions` semantics). Served by the fullnode
2341
+ * itself, same endpoints/failover as object reads.
2342
+ * 3. "indexer" - GraphQL event scan (`LogPositionOpened` / `LogClosePosition`).
2343
+ *
2344
+ * In every tier, "open" is decided by object existence (close deletes the Position).
2345
+ * Under "state" with no on-chain index, `counts` throws `NeedsDiscoveryError`.
2346
+ */
2347
+ const POSITION_INDEX_FIX = "add the PositionIndex dynamic field (next_position_id + Table<u64, ID> of cap ids, written in user::init_position) plus `vault_market::total_positions` / `next_position_id` getters, see docs/onchain-position-index.md";
2348
+ const CLAIMED = "0x0000000000000000000000000000000000000000000000000000000000000002::derived_object::Claimed";
2349
+ const QUERY = `query($t:String!,$a:String,$n:Int){events(filter:{type:$t},first:$n,after:$a){pageInfo{hasNextPage endCursor}nodes{sequenceNumber transaction{digest} contents{json}}}}`;
2350
+ var PositionIndex = class {
2351
+ streams = /* @__PURE__ */ new Map();
2352
+ inflight = /* @__PURE__ */ new Map();
2353
+ constructor(ctx, opts = {}) {
2354
+ this.ctx = ctx;
2355
+ this.opts = opts;
2356
+ }
2357
+ /**
2358
+ * Reads the proposed on-chain index by derived ids. `null` when the vault has no index yet.
2359
+ * Returns ordinal -> cap id for every position ever opened, and the stored counter.
2360
+ */
2361
+ async readOnchainIndex(vaultMarketId) {
2362
+ const d = this.ctx.derive;
2363
+ const [field] = await fetchObjects(this.ctx.client, [d.positionIndexId(vaultMarketId)]);
2364
+ if (!field) return null;
2365
+ const idx = PositionIndexField.parse(field.content).value;
2366
+ const next = Number(idx.next_position_id);
2367
+ const ordinals = Array.from({ length: Math.max(0, next - 1) }, (_, i) => i + 1);
2368
+ return {
2369
+ nextPositionId: next,
2370
+ capIds: (await fetchObjects(this.ctx.client, ordinals.map((n) => d.positionIndexEntryId(idx.caps.id, n)))).map((e, i) => {
2371
+ if (!e) throw new Error(`position index entry ${ordinals[i]} missing in ${idx.caps.id}`);
2372
+ return normalizeSuiObjectId(PositionIndexEntry.parse(e.content).value);
2373
+ })
2374
+ };
2375
+ }
2376
+ /** Every position id ever claimed on `vaultMarketId` (`derived_object::Claimed` fields). */
2377
+ async claimedIds(vaultMarketId) {
2378
+ const ids = [];
2379
+ let cursor = null;
2380
+ do {
2381
+ const page = await this.ctx.client.core.listDynamicFields({
2382
+ parentId: vaultMarketId,
2383
+ cursor,
2384
+ limit: 50
2385
+ });
2386
+ for (const f of page.dynamicFields) {
2387
+ const name = f.name;
2388
+ if (normalizeClaimed(name.type) !== CLAIMED) continue;
2389
+ ids.push(normalizeSuiObjectId("0x" + Buffer.from(name.bcs).toString("hex")));
2390
+ }
2391
+ cursor = page.hasNextPage ? page.cursor : null;
2392
+ } while (cursor !== null);
2393
+ return ids;
2394
+ }
2395
+ async counts(vaultId, vaultMarketId) {
2396
+ const onchain = await this.readOnchainIndex(vaultMarketId);
2397
+ if (onchain) {
2398
+ const positionIds = onchain.capIds.map((cap) => this.ctx.derive.positionId(vaultMarketId, cap));
2399
+ return this.finish(vaultId, positionIds, onchain.nextPositionId, "state");
2400
+ }
2401
+ if (this.ctx.allows("fullnode")) {
2402
+ const claimed = await this.claimedIds(vaultMarketId);
2403
+ return this.finish(vaultId, claimed, claimed.length + 1, "fullnode-index");
2404
+ }
2405
+ if (this.ctx.allows("indexer")) {
2406
+ const [opened, closed] = await Promise.all([this.openedIds(vaultMarketId), this.closedIds(vaultId)]);
2407
+ return {
2408
+ ...await this.finish(vaultId, opened.filter((id) => !closed.has(id)), opened.length + 1, "events+objects"),
2409
+ totalPositions: opened.length
2410
+ };
2411
+ }
2412
+ throw new NeedsDiscoveryError(`positions of vault ${vaultId}`, "fullnode", POSITION_INDEX_FIX);
2413
+ }
2414
+ async finish(vaultId, everOpened, next, source) {
2415
+ const objs = await fetchObjects(this.ctx.client, everOpened);
2416
+ const openPositionIds = everOpened.filter((_, i) => objs[i] !== null);
2417
+ return {
2418
+ vaultId,
2419
+ totalPositions: everOpened.length,
2420
+ openPositions: openPositionIds.length,
2421
+ nextPositionId: next,
2422
+ openPositionIds,
2423
+ source
2424
+ };
2425
+ }
2426
+ /** Lowest discovery level `counts` would need right now for this vault. */
2427
+ async requiredLevel(vaultMarketId) {
2428
+ return await this.readOnchainIndex(vaultMarketId) ? "state" : "fullnode";
2429
+ }
2430
+ get url() {
2431
+ const u = this.opts.graphqlUrl ?? this.ctx.config.graphqlUrl;
2432
+ if (!u) throw new Error("graphqlUrl required for event scans (config.graphqlUrl or PositionIndex opts)");
2433
+ return u;
2434
+ }
2435
+ type(name) {
2436
+ return `${this.ctx.config.packages.vaults.originalId}::events::${name}`;
2437
+ }
2438
+ async post(body) {
2439
+ const f = this.opts.fetchImpl ?? fetch;
2440
+ const retries = this.opts.retries ?? 4;
2441
+ const base = this.opts.backoffMs ?? 400;
2442
+ let last;
2443
+ for (let attempt = 0; attempt <= retries; attempt++) try {
2444
+ const res = await f(this.url, {
2445
+ method: "POST",
2446
+ headers: { "content-type": "application/json" },
2447
+ body: JSON.stringify(body)
2448
+ });
2449
+ if (res.status === 429 || res.status >= 500) throw new Error(`graphql ${res.status}`);
2450
+ if (!res.ok) throw Object.assign(/* @__PURE__ */ new Error(`graphql ${res.status}`), { fatal: true });
2451
+ const j = await res.json();
2452
+ if (!j.data) throw new Error(`graphql error: ${JSON.stringify(j.errors)}`);
2453
+ return j.data;
2454
+ } catch (e) {
2455
+ last = e;
2456
+ if (e.fatal || attempt === retries) break;
2457
+ await new Promise((r) => setTimeout(r, base * 2 ** attempt));
2458
+ }
2459
+ throw last;
2460
+ }
2461
+ /** All events of `type`, resuming from the last cursor. Concurrent callers share one scan. */
2462
+ sync(type) {
2463
+ const running = this.inflight.get(type);
2464
+ if (running) return running;
2465
+ const p = (async () => {
2466
+ const s = this.streams.get(type) ?? {
2467
+ cursor: null,
2468
+ seen: /* @__PURE__ */ new Set(),
2469
+ events: []
2470
+ };
2471
+ this.streams.set(type, s);
2472
+ for (;;) {
2473
+ const page = (await this.post({
2474
+ query: QUERY,
2475
+ variables: {
2476
+ t: type,
2477
+ a: s.cursor,
2478
+ n: this.opts.pageSize ?? 50
2479
+ }
2480
+ })).events;
2481
+ for (const n of page.nodes) {
2482
+ const key = `${n.transaction?.digest ?? "?"}:${n.sequenceNumber}`;
2483
+ if (s.seen.has(key)) continue;
2484
+ s.seen.add(key);
2485
+ s.events.push({
2486
+ key,
2487
+ json: n.contents.json
2488
+ });
2489
+ }
2490
+ if (page.pageInfo.endCursor) s.cursor = page.pageInfo.endCursor;
2491
+ if (!page.pageInfo.hasNextPage) return s.events;
2492
+ }
2493
+ })().finally(() => this.inflight.delete(type));
2494
+ this.inflight.set(type, p);
2495
+ return p;
2496
+ }
2497
+ /** Every position id ever opened in `vaultMarketId` (event order, deduped). Indexer tier. */
2498
+ async openedIds(vaultMarketId) {
2499
+ const vm = normalizeSuiObjectId(vaultMarketId);
2500
+ const ids = /* @__PURE__ */ new Set();
2501
+ for (const e of await this.sync(this.type("LogPositionOpened"))) if (normalizeSuiObjectId(String(e.json.vault)) === vm) ids.add(normalizeSuiObjectId(String(e.json.position)));
2502
+ return [...ids];
2503
+ }
2504
+ /** Position ids with a `LogClosePosition` for numeric `vaultId`. Indexer tier. */
2505
+ async closedIds(vaultId) {
2506
+ const out = /* @__PURE__ */ new Set();
2507
+ for (const e of await this.sync(this.type("LogClosePosition"))) if (Number(e.json.vault_id) === vaultId) out.add(normalizeSuiObjectId(String(e.json.position_id)));
2508
+ return out;
2509
+ }
2510
+ /** Drop cached cursors (forces a full rescan next call). */
2511
+ reset() {
2512
+ this.streams.clear();
2513
+ }
2514
+ };
2515
+ function normalizeClaimed(t) {
2516
+ return /^0x0*2::derived_object::Claimed$/.exec(t) ? CLAIMED : t;
2517
+ }
2518
+
2519
+ //#endregion
2520
+ //#region src/vault.ts
2521
+ /**
2522
+ * Vault resolver: mirrors `@jup-ag/lend-read` `Vault`.
2523
+ * Units: like Solana, vault amounts (position supply/borrow, vault totals, absorbed) are in the
2524
+ * vault's internal 1e9 space (`constants::scale_amounts`); liquidity-side amounts
2525
+ * (`totalSupplyLiquidityOrDex`, limits) are in token units. Use `unscale()` to convert.
2526
+ */
2527
+ const TickField = Field(MoveTick, VaultTick);
2528
+ const BranchField = Field(bcsU64, Branch);
2529
+ const SnapField = Field(TickIdLiquidationKey, TickIdLiquidation);
2530
+ const BitmapField = Field(bcsU16, bcsU256);
2531
+ const magnifier = (m) => m.negative ? -BigInt(m.magnitude) : BigInt(m.magnitude);
2532
+ const BRANCH_STATUS_INDEX = {
2533
+ NotLiquidated: 0,
2534
+ Liquidated: 1,
2535
+ Merged: 2,
2536
+ Closed: 3
2537
+ };
2538
+ function decodeTick(o) {
2539
+ const t = TickField.parse(o.content).value;
2540
+ return {
2541
+ isLiquidated: t.is_liquidated,
2542
+ totalIds: t.total_ids,
2543
+ rawDebt: BigInt(t.raw_debt),
2544
+ isFullyLiquidated: t.is_fully_liquidated,
2545
+ liquidationBranchId: Number(t.liquidation_branch_id),
2546
+ debtFactor: BigInt(t.debt_factor)
2547
+ };
2548
+ }
2549
+ function decodeBranch(o) {
2550
+ const f = BranchField.parse(o.content);
2551
+ const b = f.value;
2552
+ return {
2553
+ branchId: Number(f.name),
2554
+ status: BRANCH_STATUS_INDEX[b.status.$kind],
2555
+ minimaTick: tickFromMove(b.minima_tick),
2556
+ minimaTickPartials: b.minima_tick_partials,
2557
+ debtLiquidity: BigInt(b.debt_liquidity),
2558
+ debtFactor: BigInt(b.debt_factor),
2559
+ connectedBranchId: Number(b.connected_branch_id),
2560
+ connectedMinimaTick: tickFromMove(b.connected_minima_tick)
2561
+ };
2562
+ }
2563
+ function decodePosition(o, vaultId) {
2564
+ const p = Position.parse(o.content);
2565
+ return {
2566
+ vaultId,
2567
+ positionId: p.id,
2568
+ vault: p.vault_id,
2569
+ positionMint: p.position_mint,
2570
+ isSupplyOnlyPosition: p.is_supply_only_position,
2571
+ tick: tickFromMove(p.tick),
2572
+ tickId: p.tick_id,
2573
+ supplyAmount: BigInt(p.supply_amount),
2574
+ dustDebtAmount: BigInt(p.dust_debt_amount)
2575
+ };
2576
+ }
2577
+ var Vault = class {
2578
+ /** Derived per-vault position counts (no counter exists on-chain on Sui). */
2579
+ positions;
2580
+ constructor(ctx, liquidity, oracle) {
2581
+ this.ctx = ctx;
2582
+ this.liquidity = liquidity;
2583
+ this.oracle = oracle;
2584
+ this.positions = new PositionIndex(ctx);
2585
+ }
2586
+ /**
2587
+ * totalPositions (ever opened, EVM parity), openPositions (object exists now), nextPositionId
2588
+ * (stored counter once the on-chain index ships, else totalPositions + 1). Pure state when the
2589
+ * vault has the on-chain index; otherwise needs `discovery: "fullnode"` (derived_object claims)
2590
+ * or "indexer" (events), and throws `NeedsDiscoveryError` under the default "state".
2591
+ */
2592
+ async getVaultPositionCounts(vaultId) {
2593
+ return this.positions.counts(vaultId, this.ctx.derive.vaultMarketId(vaultId));
2594
+ }
2595
+ /** Counts for state fills: never lets an event-endpoint outage break a vault read (-1 then). */
2596
+ async countsOrNull(vaults, want) {
2597
+ if (want === false) return vaults.map(() => null);
2598
+ return Promise.all(vaults.map((v) => this.positions.counts(v.vaultId, v.id).catch(() => null)));
2599
+ }
2600
+ async getTotalVaults() {
2601
+ const o = await fetchObject(this.ctx.client, this.ctx.config.objects.vaultRegistry, "VaultRegistry");
2602
+ return Number(VaultRegistry.parse(o.content).next_vault_id) - 1;
2603
+ }
2604
+ async loadVaults(vaultIds) {
2605
+ const ids = vaultIds.map((v) => this.ctx.derive.vaultMarketId(v));
2606
+ return (await fetchObjects(this.ctx.client, ids)).map((o, i) => {
2607
+ if (!o) throw new Error(`vault ${vaultIds[i]} not found (${ids[i]})`);
2608
+ const [col, debt] = parseStructTag(o.type).typeParams;
2609
+ return {
2610
+ vaultId: vaultIds[i],
2611
+ id: o.objectId,
2612
+ collateralType: normalizeStructTag(col),
2613
+ debtType: normalizeStructTag(debt),
2614
+ raw: VaultMarket.parse(o.content)
2615
+ };
2616
+ });
2617
+ }
2618
+ async loadVault(vaultId) {
2619
+ return (await this.loadVaults([vaultId]))[0];
2620
+ }
2621
+ configFromRecord(v) {
2622
+ const r = v.raw;
2623
+ return {
2624
+ vaultId: v.vaultId,
2625
+ vault: v.id,
2626
+ supplyRateMagnifier: Number(magnifier(r.supply_rate_magnifier)),
2627
+ borrowRateMagnifier: Number(magnifier(r.borrow_rate_magnifier)),
2628
+ collateralFactor: r.risk.collateral_factor,
2629
+ liquidationThreshold: r.risk.liquidation_threshold,
2630
+ liquidationMaxLimit: r.risk.liquidation_max_limit,
2631
+ withdrawGap: r.risk.withdraw_gap,
2632
+ liquidationPenalty: r.risk.liquidation_penalty,
2633
+ borrowFee: r.risk.borrow_fee,
2634
+ oracle: r.oracle,
2635
+ rebalancer: "",
2636
+ liquidityProgram: r.liquidity_program,
2637
+ oracleProgram: this.ctx.config.packages.oracle.packageId,
2638
+ supplyToken: v.collateralType,
2639
+ borrowToken: v.debtType,
2640
+ vaultType: r.vault_type,
2641
+ collateralDecimals: r.col_decimals,
2642
+ debtDecimals: r.debt_decimals
2643
+ };
2644
+ }
2645
+ async getVaultConfig(vaultId) {
2646
+ return this.configFromRecord(await this.loadVault(vaultId));
2647
+ }
2648
+ async batchGetTicks(vaultId, ticks) {
2649
+ const v = await this.loadVault(vaultId);
2650
+ return this.ticksOf(v, ticks);
2651
+ }
2652
+ async ticksOf(v, ticks) {
2653
+ return (await fetchObjects(this.ctx.client, ticks.map((t) => this.ctx.derive.tickEntryId(v.raw.ticks.id, t)))).map((o) => o ? decodeTick(o) : null);
2654
+ }
2655
+ async getTick({ vaultId, tick }) {
2656
+ return (await this.batchGetTicks(vaultId, [tick]))[0];
2657
+ }
2658
+ async batchGetBranches(vaultId, branchIds) {
2659
+ return this.branchesOf(await this.loadVault(vaultId), branchIds);
2660
+ }
2661
+ async branchesOf(v, branchIds) {
2662
+ return (await fetchObjects(this.ctx.client, branchIds.map((b) => this.ctx.derive.branchEntryId(v.raw.branches.id, b)))).map((o) => o ? decodeBranch(o) : null);
2663
+ }
2664
+ async getBranch({ vaultId, branchId }) {
2665
+ return (await this.batchGetBranches(vaultId, [branchId]))[0];
2666
+ }
2667
+ /** Branches 1..total_branch_id. */
2668
+ async getAllBranches(vaultId) {
2669
+ const v = await this.loadVault(vaultId);
2670
+ const n = Number(v.raw.total_branch_id);
2671
+ const ids = Array.from({ length: n }, (_, i) => i + 1);
2672
+ return (await this.branchesOf(v, ids)).filter((b) => b !== null);
2673
+ }
2674
+ async getTickIdLiquidation({ vaultId, tick, tickId }) {
2675
+ const v = await this.loadVault(vaultId);
2676
+ const [o] = await fetchObjects(this.ctx.client, [this.ctx.derive.liqSnapEntryId(v.raw.liq_snaps.id, tick, tickId)]);
2677
+ if (!o) return null;
2678
+ const s = SnapField.parse(o.content).value;
2679
+ return {
2680
+ isFullyLiquidated: s.is_fully_liquidated,
2681
+ liquidationBranchId: Number(s.liquidation_branch_id),
2682
+ debtFactor: BigInt(s.debt_factor)
2683
+ };
2684
+ }
2685
+ /** Raw `tick_bitmap` words (u256) for `words`; missing words = 0. */
2686
+ async getTickHasDebtArray(vaultId, words) {
2687
+ const v = await this.loadVault(vaultId);
2688
+ const objs = await fetchObjects(this.ctx.client, words.map((w) => this.ctx.derive.bitmapWordId(v.raw.tick_bitmap.id, w)));
2689
+ return words.map((word, i) => ({
2690
+ word,
2691
+ bitmap: objs[i] ? BigInt(BitmapField.parse(objs[i].content).value) : 0n
2692
+ }));
2693
+ }
2694
+ stateFromRecord(v, branch, counts = null) {
2695
+ const r = v.raw;
2696
+ let currentBranchState;
2697
+ if (branch) currentBranchState = {
2698
+ status: branch.status,
2699
+ minimaTick: branch.minimaTick,
2700
+ debtFactor: branch.debtFactor,
2701
+ partials: BigInt(branch.minimaTickPartials),
2702
+ debtLiquidity: branch.debtLiquidity,
2703
+ baseBranchId: branch.connectedBranchId,
2704
+ baseBranchMinima: branch.connectedMinimaTick
2705
+ };
2706
+ return {
2707
+ topTick: tickFromMove(r.topmost_tick),
2708
+ currentBranch: Number(r.current_branch_id),
2709
+ totalBranch: Number(r.total_branch_id),
2710
+ totalSupply: BigInt(r.total_supply),
2711
+ totalBorrow: BigInt(r.total_borrow),
2712
+ totalPositions: counts?.totalPositions ?? -1,
2713
+ currentBranchState,
2714
+ branchLiquidated: r.branch_liquidated,
2715
+ absorbedDebtAmount: BigInt(r.absorbed_debt_amount),
2716
+ absorbedColAmount: BigInt(r.absorbed_col_amount),
2717
+ absorbedDustDebt: BigInt(r.absorbed_dust_debt),
2718
+ liquiditySupplyExchangePrice: BigInt(r.liquidity_supply_exchange_price),
2719
+ liquidityBorrowExchangePrice: BigInt(r.liquidity_borrow_exchange_price),
2720
+ vaultSupplyExchangePrice: BigInt(r.vault_supply_exchange_price),
2721
+ vaultBorrowExchangePrice: BigInt(r.vault_borrow_exchange_price),
2722
+ nextPositionId: counts?.nextPositionId ?? -1,
2723
+ lastUpdateTimestamp: BigInt(r.last_update_timestamp)
2724
+ };
2725
+ }
2726
+ async getVaultState(vaultId, opts = {}) {
2727
+ const v = await this.loadVault(vaultId);
2728
+ const [[branch], [counts]] = await Promise.all([this.branchesOf(v, [Number(v.raw.current_branch_id)]), this.countsOrNull([v], opts.positionCounts)]);
2729
+ return this.stateFromRecord(v, branch ?? null, counts ?? null);
2730
+ }
2731
+ /** `user::get_exchange_prices`: (liqSupply, liqBorrow, vaultSupply, vaultBorrow) accrued to `now`. */
2732
+ exchangePricesFromRecords(v, colReserve, debtReserve, now = this.ctx.now()) {
2733
+ const r = v.raw;
2734
+ const liqSupply = calculateExchangePrice(colReserve.snapshot, now).supplyExchangePrice;
2735
+ const liqBorrow = calculateExchangePrice(debtReserve.snapshot, now).borrowExchangePrice;
2736
+ const last = BigInt(r.last_update_timestamp);
2737
+ const vp = loadVaultExchangePrices({
2738
+ vaultSupplyExPrice: BigInt(r.vault_supply_exchange_price),
2739
+ vaultBorrowExPrice: BigInt(r.vault_borrow_exchange_price),
2740
+ oldLiqSupplyExPrice: BigInt(r.liquidity_supply_exchange_price),
2741
+ oldLiqBorrowExPrice: BigInt(r.liquidity_borrow_exchange_price),
2742
+ liqSupplyExPrice: liqSupply,
2743
+ liqBorrowExPrice: liqBorrow,
2744
+ timeDiff: now > last ? now - last : 0n,
2745
+ supplyRateMagnifier: magnifier(r.supply_rate_magnifier),
2746
+ borrowRateMagnifier: magnifier(r.borrow_rate_magnifier)
2747
+ });
2748
+ return {
2749
+ liquiditySupplyExchangePrice: liqSupply,
2750
+ liquidityBorrowExchangePrice: liqBorrow,
2751
+ vaultSupplyExchangePrice: vp.vaultSupplyExchangePrice,
2752
+ vaultBorrowExchangePrice: vp.vaultBorrowExchangePrice
2753
+ };
2754
+ }
2755
+ /** Full vault view. `withOracle` (default true) adds the oracle price via one simulation. */
2756
+ async getVaultEntireData(vaultId, opts = {}) {
2757
+ return (await this.entireDataFor([await this.loadVault(vaultId)], opts))[0];
2758
+ }
2759
+ async getVaultEntireDataV2(vaultId, opts = {}) {
2760
+ return this.toV2(await this.getVaultEntireData(vaultId, opts));
2761
+ }
2762
+ async getAllVaults(opts = {}) {
2763
+ const n = await this.getTotalVaults();
2764
+ if (n <= 0) return [];
2765
+ const recs = await this.loadVaults(Array.from({ length: n }, (_, i) => i + 1));
2766
+ return this.entireDataFor(recs, opts);
2767
+ }
2768
+ async getAllVaultsV2(opts = {}) {
2769
+ return (await this.getAllVaults(opts)).map((v) => this.toV2(v));
2770
+ }
2771
+ getVaultByVaultId = (vaultId) => this.getVaultEntireData(vaultId);
2772
+ toV2(d) {
2773
+ return {
2774
+ ...d,
2775
+ vaultId: d.constantViews.vaultId,
2776
+ vaultType: d.constantViews.vaultType,
2777
+ supplyDex: null,
2778
+ borrowDex: null,
2779
+ supplyDexPosition: null,
2780
+ borrowDexPosition: null
2781
+ };
2782
+ }
2783
+ async entireDataFor(vaults, opts) {
2784
+ const now = this.ctx.now();
2785
+ const tokens = [...new Set(vaults.flatMap((v) => [v.collateralType, v.debtType]))];
2786
+ const reserves = new Map((await this.liquidity.loadReserves(tokens)).map((r) => [r.token, r]));
2787
+ const pairs = vaults.flatMap((v) => [{
2788
+ holder: v.id,
2789
+ reserve: reserves.get(v.collateralType)
2790
+ }, {
2791
+ holder: v.id,
2792
+ reserve: reserves.get(v.debtType)
2793
+ }]);
2794
+ const [entries, branches, prices, counts] = await Promise.all([
2795
+ this.liquidity.loadUserEntries(pairs),
2796
+ fetchObjects(this.ctx.client, vaults.map((v) => this.ctx.derive.branchEntryId(v.raw.branches.id, v.raw.current_branch_id))),
2797
+ opts.withOracle === false ? Promise.resolve(vaults.map(() => null)) : Promise.all(vaults.map((v) => this.oracle.getOraclePrice(v.raw.oracle).catch(() => null))),
2798
+ this.countsOrNull(vaults, opts.positionCounts)
2799
+ ]);
2800
+ return vaults.map((v, i) => {
2801
+ const col = reserves.get(v.collateralType);
2802
+ const debt = reserves.get(v.debtType);
2803
+ const sup = this.liquidity.supplyDataFromRecords(col, entries[2 * i].supply, now);
2804
+ const bor = this.liquidity.borrowDataFromRecords(debt, entries[2 * i + 1].borrow, now);
2805
+ const ep = this.exchangePricesFromRecords(v, col, debt, now);
2806
+ const cfg = this.configFromRecord(v);
2807
+ const state = this.stateFromRecord(v, branches[i] ? decodeBranch(branches[i]) : null, counts[i] ?? null);
2808
+ const price = prices[i];
2809
+ const supMag = BigInt(cfg.supplyRateMagnifier);
2810
+ const borMag = BigInt(cfg.borrowRateMagnifier);
2811
+ const exchangePricesAndRates = {
2812
+ lastStoredLiquiditySupplyExchangePrice: state.liquiditySupplyExchangePrice,
2813
+ lastStoredLiquidityBorrowExchangePrice: state.liquidityBorrowExchangePrice,
2814
+ lastStoredVaultSupplyExchangePrice: state.vaultSupplyExchangePrice,
2815
+ lastStoredVaultBorrowExchangePrice: state.vaultBorrowExchangePrice,
2816
+ ...ep,
2817
+ supplyRateLiquidity: sup.overallTokenData.supplyRate,
2818
+ borrowRateLiquidity: bor.overallTokenData.borrowRate,
2819
+ supplyRateVault: sup.overallTokenData.supplyRate + supMag,
2820
+ borrowRateVault: bor.overallTokenData.borrowRate + borMag,
2821
+ rewardsOrFeeRateSupply: supMag,
2822
+ rewardsOrFeeRateBorrow: borMag
2823
+ };
2824
+ const limits = {
2825
+ withdrawLimit: 0n,
2826
+ withdrawableUntilLimit: 0n,
2827
+ withdrawable: 0n,
2828
+ borrowLimit: 0n,
2829
+ borrowLimitUtilization: 0n,
2830
+ borrowableUntilLimit: 0n,
2831
+ borrowable: 0n,
2832
+ minimumBorrowing: rawToNormal(minimumDebt(cfg.debtDecimals), ep.vaultBorrowExchangePrice)
2833
+ };
2834
+ const us = sup.userSupplyData;
2835
+ if (us.supply > 0n) {
2836
+ limits.withdrawLimit = us.withdrawalLimit;
2837
+ const gap = us.supply * BigInt(cfg.withdrawGap) / 1000n;
2838
+ limits.withdrawableUntilLimit = us.withdrawableUntilLimit > gap ? (us.withdrawableUntilLimit - gap) * 999999n / 1000000n : 0n;
2839
+ limits.withdrawable = col.snapshot.vaultBalance < limits.withdrawableUntilLimit ? col.snapshot.vaultBalance : limits.withdrawableUntilLimit;
2840
+ }
2841
+ const ub = bor.userBorrowData;
2842
+ if (ub.borrow > 0n) {
2843
+ limits.borrowLimit = ub.borrowLimit;
2844
+ limits.borrowLimitUtilization = ub.borrowLimitUtilization;
2845
+ limits.borrowableUntilLimit = ub.borrowableUntilLimit * 999999n / 1000000n;
2846
+ limits.borrowable = debt.snapshot.vaultBalance < limits.borrowableUntilLimit ? debt.snapshot.vaultBalance : limits.borrowableUntilLimit;
2847
+ }
2848
+ return {
2849
+ vault: v.id,
2850
+ isSmartCol: false,
2851
+ isSmartDebt: false,
2852
+ constantViews: {
2853
+ liquidity: v.raw.liquidity_program,
2854
+ supply: col.id,
2855
+ borrow: debt.id,
2856
+ supplyToken: v.collateralType,
2857
+ borrowToken: v.debtType,
2858
+ vaultId: v.vaultId,
2859
+ vaultType: v.raw.vault_type
2860
+ },
2861
+ configs: {
2862
+ supplyRateMagnifier: supMag,
2863
+ borrowRateMagnifier: borMag,
2864
+ collateralFactor: BigInt(cfg.collateralFactor),
2865
+ liquidationThreshold: BigInt(cfg.liquidationThreshold),
2866
+ liquidationMaxLimit: BigInt(cfg.liquidationMaxLimit),
2867
+ withdrawalGap: BigInt(cfg.withdrawGap),
2868
+ liquidationPenalty: BigInt(cfg.liquidationPenalty),
2869
+ borrowFee: BigInt(cfg.borrowFee),
2870
+ oracle: cfg.oracle,
2871
+ rebalancer: "",
2872
+ ...price?.operate != null ? { oraclePriceOperate: price.operate } : {},
2873
+ ...price?.liquidate != null ? { oraclePriceLiquidate: price.liquidate } : {},
2874
+ lastUpdateTimestamp: state.lastUpdateTimestamp
2875
+ },
2876
+ exchangePricesAndRates,
2877
+ limitsAndAvailability: limits,
2878
+ liquidityUserSupplyData: us,
2879
+ liquidityUserBorrowData: ub,
2880
+ vaultState: state,
2881
+ totalSupplyAndBorrow: {
2882
+ totalSupplyVault: rawToNormal(state.totalSupply, ep.vaultSupplyExchangePrice),
2883
+ totalBorrowVault: rawToNormal(state.totalBorrow, ep.vaultBorrowExchangePrice),
2884
+ totalSupplyLiquidityOrDex: us.supply,
2885
+ totalBorrowLiquidityOrDex: ub.borrow,
2886
+ absorbedSupply: rawToNormal(state.absorbedColAmount, ep.vaultSupplyExchangePrice),
2887
+ absorbedBorrow: rawToNormal(state.absorbedDebtAmount, ep.vaultBorrowExchangePrice)
2888
+ }
2889
+ };
2890
+ });
2891
+ }
2892
+ /** Raw Position object. `vaultId` is resolved from the position's VaultMarket if omitted. */
2893
+ async getUserPosition({ positionId, vaultId }) {
2894
+ const [o] = await fetchObjects(this.ctx.client, [normalizeSuiObjectId(positionId)]);
2895
+ if (!o) return null;
2896
+ const p = decodePosition(o, vaultId ?? 0);
2897
+ if (vaultId === void 0) p.vaultId = await this.vaultIdOf(p.vault);
2898
+ return p;
2899
+ }
2900
+ async batchGetUserPositions(positionIds) {
2901
+ const out = (await fetchObjects(this.ctx.client, positionIds.map((p) => normalizeSuiObjectId(p)))).map((o) => o ? decodePosition(o, 0) : null);
2902
+ const vaultIds = /* @__PURE__ */ new Map();
2903
+ for (const p of out) if (p && !vaultIds.has(p.vault)) vaultIds.set(p.vault, await this.vaultIdOf(p.vault));
2904
+ for (const p of out) if (p) p.vaultId = vaultIds.get(p.vault);
2905
+ return out;
2906
+ }
2907
+ async vaultIdOf(vaultMarketId) {
2908
+ const o = await fetchObject(this.ctx.client, vaultMarketId, "VaultMarket");
2909
+ return Number(VaultMarket.parse(o.content).vault_id);
2910
+ }
2911
+ /** Everything `fetch_latest_position` needs for one position (tick, snapshot, branch chain). */
2912
+ async liquidationLookups(v, pos) {
2913
+ if (pos.isSupplyOnlyPosition || pos.tick === COLD_TICK) return null;
2914
+ const [tick] = await this.ticksOf(v, [pos.tick]);
2915
+ if (!tick) throw new Error(`tick ${pos.tick} missing in vault ${v.vaultId}`);
2916
+ if (!needsLiquidationSettle(tick, pos.tickId)) return {
2917
+ tick,
2918
+ branch: () => {
2919
+ throw new Error("unused");
2920
+ }
2921
+ };
2922
+ let snap = null;
2923
+ if (tick.totalIds !== pos.tickId) {
2924
+ const [o] = await fetchObjects(this.ctx.client, [this.ctx.derive.liqSnapEntryId(v.raw.liq_snaps.id, pos.tick, pos.tickId)]);
2925
+ if (o) {
2926
+ const s = SnapField.parse(o.content).value;
2927
+ snap = {
2928
+ isFullyLiquidated: s.is_fully_liquidated,
2929
+ liquidationBranchId: Number(s.liquidation_branch_id),
2930
+ debtFactor: BigInt(s.debt_factor)
2931
+ };
2932
+ }
2933
+ }
2934
+ const n = Number(v.raw.total_branch_id);
2935
+ const all = await this.branchesOf(v, Array.from({ length: n }, (_, i) => i + 1));
2936
+ const byId = new Map(all.filter((b) => b !== null).map((b) => [b.branchId, b]));
2937
+ return {
2938
+ tick,
2939
+ snap,
2940
+ branch: (id) => {
2941
+ const b = byId.get(id);
2942
+ if (!b) throw new Error(`branch ${id} missing in vault ${v.vaultId}`);
2943
+ return b;
2944
+ }
2945
+ };
2946
+ }
2947
+ /** Solana `getCurrentPositionState`: raw col/debt after any liquidation, as operate would settle it. */
2948
+ async getCurrentPositionState({ vaultId, position }) {
2949
+ const v = await this.loadVault(vaultId);
2950
+ return currentPositionState(position, await this.liquidationLookups(v, position), v.raw.debt_decimals);
2951
+ }
2952
+ async getFinalPosition({ vaultId, positionId }) {
2953
+ const position = await this.getUserPosition({
2954
+ vaultId,
2955
+ positionId
2956
+ });
2957
+ if (!position) throw new Error(`position ${positionId} not found`);
2958
+ return this.getCurrentPositionState({
2959
+ vaultId,
2960
+ position
2961
+ });
2962
+ }
2963
+ /** Owner of a PositionOwnerCap (Solana `getNftOwner`). */
2964
+ async getPositionOwner(capId) {
2965
+ const [o] = await fetchObjects(this.ctx.client, [normalizeSuiObjectId(capId)]);
2966
+ return o ? ownerAddress(o.owner) : null;
2967
+ }
2968
+ getNftOwner = (capId) => this.getPositionOwner(capId);
2969
+ /** Solana `buildNftPositionFromPositionAccount` semantics, on a fetched vault context. */
2970
+ async buildNftPosition(pos, v, vault, owner) {
2971
+ const ep = vault.exchangePricesAndRates;
2972
+ const out = {
2973
+ nftId: pos.positionId,
2974
+ owner: owner !== void 0 ? owner : await this.getPositionOwner(pos.positionMint),
2975
+ capId: pos.positionMint,
2976
+ isSupplyPosition: pos.isSupplyOnlyPosition,
2977
+ supply: pos.supplyAmount,
2978
+ beforeSupply: pos.supplyAmount,
2979
+ dustBorrow: pos.dustDebtAmount,
2980
+ beforeDustBorrow: pos.dustDebtAmount,
2981
+ tick: pos.tick,
2982
+ tickId: pos.tickId,
2983
+ borrow: 0n,
2984
+ beforeBorrow: 0n,
2985
+ isLiquidated: false
2986
+ };
2987
+ if (!out.isSupplyPosition && pos.tick !== COLD_TICK) {
2988
+ out.borrow = getRatioAtTick(pos.tick) * out.supply >> SHIFT;
2989
+ out.beforeBorrow = out.borrow - out.dustBorrow;
2990
+ const lookups = await this.liquidationLookups(v, pos);
2991
+ if (lookups && needsLiquidationSettle(lookups.tick, pos.tickId)) {
2992
+ out.isLiquidated = true;
2993
+ const cur = currentPositionState(pos, lookups, v.raw.debt_decimals);
2994
+ out.tick = cur.tick;
2995
+ out.supply = cur.colRaw;
2996
+ out.borrow = cur.debtRaw;
2997
+ out.dustBorrow = cur.dustDebtRaw;
2998
+ }
2999
+ if (out.borrow > out.dustBorrow) out.borrow -= out.dustBorrow;
3000
+ else {
3001
+ out.borrow = 0n;
3002
+ out.dustBorrow = 0n;
3003
+ }
3004
+ }
3005
+ out.beforeSupply = rawToNormal(out.beforeSupply, ep.vaultSupplyExchangePrice);
3006
+ out.beforeBorrow = rawToNormal(out.beforeBorrow, ep.vaultBorrowExchangePrice);
3007
+ out.beforeDustBorrow = rawToNormal(out.beforeDustBorrow, ep.vaultBorrowExchangePrice);
3008
+ out.supply = rawToNormal(out.supply, ep.vaultSupplyExchangePrice);
3009
+ out.borrow = rawToNormal(out.borrow, ep.vaultBorrowExchangePrice);
3010
+ out.dustBorrow = rawToNormal(out.dustBorrow, ep.vaultBorrowExchangePrice);
3011
+ return out;
3012
+ }
3013
+ /** Solana `getPositionByVaultId(vaultId, nftId)`; Sui takes the Position object id. */
3014
+ async getPositionByVaultId(vaultId, positionId) {
3015
+ const pos = await this.getUserPosition({
3016
+ vaultId,
3017
+ positionId
3018
+ });
3019
+ if (!pos) throw new Error(`position ${positionId} not found`);
3020
+ const v = await this.loadVault(vaultId);
3021
+ if (normalizeSuiObjectId(pos.vault) !== v.id) throw new Error(`position ${positionId} is not in vault ${vaultId}`);
3022
+ const vault = (await this.entireDataFor([v], {}))[0];
3023
+ return {
3024
+ ...await this.buildNftPosition(pos, v, vault),
3025
+ vault
3026
+ };
3027
+ }
3028
+ async getPositionByVaultIdV2(vaultId, positionId) {
3029
+ const p = await this.getPositionByVaultId(vaultId, positionId);
3030
+ return {
3031
+ ...p,
3032
+ vault: this.toV2(p.vault)
3033
+ };
3034
+ }
3035
+ /**
3036
+ * Every position whose PositionOwnerCap `user` owns, across all vaults.
3037
+ *
3038
+ * Pure state when every vault has the on-chain position index: all cap ids come from the
3039
+ * index, their owners from the cap objects themselves. Otherwise needs `discovery: "fullnode"`
3040
+ * (`listOwnedObjects`). If the caller already knows the cap ids (a wallet does), use
3041
+ * `getUserPositionsByCapIds`, which is always pure state.
3042
+ */
3043
+ async getAllUserPositions(user) {
3044
+ const owner = normalizeSuiObjectId(user);
3045
+ const n = await this.getTotalVaults();
3046
+ const markets = Array.from({ length: n }, (_, i) => this.ctx.derive.vaultMarketId(i + 1));
3047
+ const indexes = await Promise.all(markets.map((m) => this.positions.readOnchainIndex(m)));
3048
+ let capIds = [];
3049
+ if (indexes.every((x) => x !== null)) {
3050
+ const all = indexes.flatMap((x) => x.capIds);
3051
+ const caps = await fetchObjects(this.ctx.client, all);
3052
+ capIds = all.filter((_, i) => caps[i] && ownerAddress(caps[i].owner) === owner);
3053
+ } else if (this.ctx.allows("fullnode")) {
3054
+ let cursor = null;
3055
+ do {
3056
+ const page = await this.ctx.client.core.listOwnedObjects({
3057
+ owner: user,
3058
+ type: this.ctx.derive.positionCapType,
3059
+ cursor
3060
+ });
3061
+ capIds.push(...page.objects.map((o) => o.objectId));
3062
+ cursor = page.hasNextPage ? page.cursor : null;
3063
+ } while (cursor !== null);
3064
+ } else throw new NeedsDiscoveryError(`positions owned by ${user}`, "fullnode", POSITION_INDEX_FIX);
3065
+ return this.getUserPositionsByCapIds(capIds, owner);
3066
+ }
3067
+ /** Positions for known PositionOwnerCap ids (pure state: derived position ids only). */
3068
+ async getUserPositionsByCapIds(capIds, user) {
3069
+ if (capIds.length === 0) return [];
3070
+ const n = await this.getTotalVaults();
3071
+ const vaults = await this.loadVaults(Array.from({ length: n }, (_, i) => i + 1));
3072
+ const candidates = capIds.flatMap((capId) => vaults.map((v) => ({
3073
+ v,
3074
+ id: this.ctx.derive.positionId(v.id, capId)
3075
+ })));
3076
+ const found = await fetchObjects(this.ctx.client, candidates.map((c) => c.id));
3077
+ const hits = candidates.flatMap((c, i) => found[i] ? [{
3078
+ v: c.v,
3079
+ pos: decodePosition(found[i], c.v.vaultId)
3080
+ }] : []);
3081
+ const entire = /* @__PURE__ */ new Map();
3082
+ for (const vd of await this.entireDataFor([...new Map(hits.map((h) => [h.v.id, h.v])).values()], {})) entire.set(vd.vault, vd);
3083
+ return Promise.all(hits.map(async ({ v, pos }) => ({
3084
+ ...await this.buildNftPosition(pos, v, entire.get(v.id), user === void 0 ? void 0 : normalizeSuiObjectId(user)),
3085
+ vault: entire.get(v.id)
3086
+ })));
3087
+ }
3088
+ getAllUserPositionsV2 = async (user) => (await this.getAllUserPositions(user)).map((p) => ({
3089
+ ...p,
3090
+ vault: this.toV2(p.vault)
3091
+ }));
3092
+ /** Open Position ids in a vault: opened events, minus closes, verified by object existence. */
3093
+ async getAllPositionIdsForVault(vaultId) {
3094
+ return (await this.getVaultPositionCounts(vaultId)).openPositionIds;
3095
+ }
3096
+ /** All open positions of a vault with `riskRatio` = debt / (col × liquidate price), 1e4 scale. */
3097
+ async getAllPositionsWithRiskRatio(vaultId) {
3098
+ const ids = await this.getAllPositionIdsForVault(vaultId);
3099
+ const v = await this.loadVault(vaultId);
3100
+ const vault = (await this.entireDataFor([v], {}))[0];
3101
+ const price = vault.configs.oraclePriceLiquidate ?? 0n;
3102
+ const positions = (await this.batchGetUserPositions(ids)).filter((p) => p !== null);
3103
+ return Promise.all(positions.map(async (pos) => {
3104
+ const nft = await this.buildNftPosition({
3105
+ ...pos,
3106
+ vaultId
3107
+ }, v, vault);
3108
+ const colValue = nft.supply * price;
3109
+ const riskRatio = colValue === 0n ? 0n : nft.borrow * ORACLE_RATE_PRECISION * FOUR_DECIMALS / colValue;
3110
+ return {
3111
+ ...nft,
3112
+ vaultId,
3113
+ riskRatio
3114
+ };
3115
+ }));
3116
+ }
3117
+ /** Oracle price of a vault's oracle (as vaults consume it). */
3118
+ async getOraclePrice(vaultId) {
3119
+ const v = await this.loadVault(vaultId);
3120
+ return this.oracle.getOraclePrice(v.raw.oracle);
3121
+ }
3122
+ /** Raw vault-internal 1e9 amounts → token units (`constants::unscale_amounts`). */
3123
+ static unscale(amount, decimals) {
3124
+ return amount / 10n ** BigInt(9 - decimals);
3125
+ }
3126
+ };
3127
+
3128
+ //#endregion
3129
+ //#region src/client.ts
3130
+ const DEFAULT_RPC_URL = "https://fullnode.mainnet.sui.io:443";
3131
+ /**
3132
+ * Unified reader, same shape as `@jup-ag/lend-read`'s `Client`:
3133
+ * `client.liquidity`, `client.lending`, `client.vault` (+ `client.oracle`, Sui-only).
3134
+ *
3135
+ * @example
3136
+ * const client = new Client(); // staging deployment, Sui mainnet gRPC
3137
+ * const client = new Client("https://my-fullnode:443");
3138
+ * const client = new Client(existingSuiGrpcClient, { network: "staging" });
3139
+ * const vault = await client.vault.getVaultEntireData(1);
3140
+ */
3141
+ var Client = class {
3142
+ liquidity;
3143
+ lending;
3144
+ vault;
3145
+ oracle;
3146
+ context;
3147
+ constructor(rpc, opts = {}) {
3148
+ const config = resolveConfig(opts.network ?? DEFAULT_NETWORK);
3149
+ this.context = makeContext(createFailoverClient(rpc === void 0 ? config.rpcUrls ?? [DEFAULT_RPC_URL] : typeof rpc === "string" ? [rpc] : Array.isArray(rpc) ? rpc : [rpc], {
3150
+ network: config.network,
3151
+ timeoutMs: opts.timeoutMs
3152
+ }), config, createDerive(config), opts.now, opts.discovery ?? "state");
3153
+ this.liquidity = new Liquidity(this.context);
3154
+ this.oracle = new Oracle(this.context);
3155
+ this.lending = new Lending(this.context, this.liquidity);
3156
+ this.vault = new Vault(this.context, this.liquidity, this.oracle);
3157
+ }
3158
+ };
3159
+ /**
3160
+ * `$extend` form, same pattern as the write SDK's `fluid()`:
3161
+ * `const c = new SuiGrpcClient({...}).$extend(fluidRead()); c.fluidRead.vault...`
3162
+ */
3163
+ function fluidRead(opts = {}) {
3164
+ return {
3165
+ name: "fluidRead",
3166
+ register: (client) => new Client(client, opts)
3167
+ };
3168
+ }
3169
+
3170
+ //#endregion
3171
+ export { AllEndpointsFailedError, CONFIGS, Client, DEFAULT_NETWORK, DEFAULT_RPC_URL, EXCHANGE_PRICES_PRECISION, EXCHANGE_PRICE_SCALE_FACTOR, FOUR_DECIMALS, Lending, Liquidity, MAINNET, MAX_REWARDS_RATE, NeedsDiscoveryError, ORACLE_RATE_PRECISION, ORACLE_V2_LIMITS, ObjectNotFoundError, Oracle, POSITION_INDEX_FIX, PositionIndex, RATE_OUTPUT_DECIMALS, RETURN_PERCENT_PRECISION, SECONDS_PER_YEAR, STAGING, STAGING_LEGACY, Vault, ViewCallError, bcs_exports as bcsTypes, clientForEndpoint, configFromDeployment, createDerive, createFailoverClient, divCeil, fluidRead, lending_exports as lendingMath, liquidity_exports as liquidityMath, max, min, mulDiv, mulDivCeil, nowSeconds, resolveConfig, scaleAndInvert, simulateView, tick_exports as tickMath, vault_exports as vaultMath };