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