@instadapp/fluid-sui-read-sdk 0.1.0 → 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.
Files changed (4) hide show
  1. package/README.md +147 -21
  2. package/dist/index.d.mts +1229 -998
  3. package/dist/index.mjs +688 -318
  4. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  import { t as __exportAll } from "./chunk-15K8U1wQ.mjs";
2
- import { deriveDynamicFieldID, deriveObjectID, normalizeStructTag, normalizeSuiAddress, normalizeSuiObjectId, parseStructTag, toHex } from "@mysten/sui/utils";
2
+ import { deriveDynamicFieldID, deriveObjectID, fromBase64, normalizeStructTag, normalizeSuiAddress, normalizeSuiObjectId, parseStructTag, toHex } from "@mysten/sui/utils";
3
+ import { ObjectError } from "@mysten/sui/client";
3
4
  import { SuiGrpcClient } from "@mysten/sui/grpc";
4
5
  import { SuiJsonRpcClient } from "@mysten/sui/jsonRpc";
5
6
  import { bcs } from "@mysten/sui/bcs";
6
- import { ObjectError } from "@mysten/sui/client";
7
7
  import { Transaction } from "@mysten/sui/transactions";
8
8
 
9
9
  //#region src/config.ts
@@ -11,6 +11,8 @@ const same = (id) => ({
11
11
  packageId: id,
12
12
  originalId: id
13
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")]);
14
16
  const TOKENS = [
15
17
  "0x2::sui::SUI",
16
18
  "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC",
@@ -41,6 +43,7 @@ const STAGING = {
41
43
  oracle: same("0xc1e7632c9847370a0f00298defa15d42fcb0e3191cad3bee8451cab5ce144814"),
42
44
  vaults: same("0x08a3f6777d66c82ded41f5c02f902be34407005b2e53649b4e4da0bb63ef6f20")
43
45
  },
46
+ mathVariant: "pre-audit",
44
47
  objects: {
45
48
  liquidityRegistry: "0x2b712ae7c38f4b4a09498565a4df8184bda875506b9f65ba8f64cf5662731ccc",
46
49
  lendingRegistry: "0xc4f71cb64e6c01216f18fbea012176aa2f3a0a747347c914627cbb5aa0e569d2",
@@ -52,8 +55,7 @@ const STAGING = {
52
55
  oracleAuth: "0x6133bd13291e1d04526c69957b27cf0a9d7422059873ad00e3e34ff1b1ba07d9",
53
56
  vaultAdmin: "0x72ef539f4827cd055e38a3c2385a313831652fe4bde650a5d099dfdb95fc98d3"
54
57
  },
55
- oracleVariant: "v2",
56
- oracleV2: {
58
+ oracle: {
57
59
  redstoneAdapter: "0x22794c3a37c5320e5acb6b9cdba6e256bc08867e9de8afd2a4b5d8ea7061fea3",
58
60
  redstonePackage: "0xfa84e5620fda505bbeb9178316a555e1db36f8932dee44210dd88a6464a76ef6",
59
61
  pythPriceInfoObjects: { "0x23d7315113f5b1d3ba7a83604c44b94d79f4fd69af77f804fc7f920a6dc65744": "0x89b2add829cb6fcd017153fff428bc9faec4d06d643ecfc435af5b55a9e987f0" },
@@ -65,47 +67,15 @@ const STAGING = {
65
67
  graphqlUrl: "https://graphql.mainnet.sui.io/graphql"
66
68
  };
67
69
  /**
68
- * SUPERSEDED. 2026-09-18 deployment: interim admin oracle (`view::quote`, vault 1 on
69
- * 0xf36d0d95…651f) + the vaults package built on it. Objects still exist on chain; kept only to
70
- * read historical state.
71
- */
72
- const STAGING_LEGACY = {
73
- network: "mainnet",
74
- packages: {
75
- ...COMMON_IDS,
76
- oracle: same("0xebe2409c7ea9636f0e446797515cbee2ab809d898b06d99179d9eda59e1e42d5"),
77
- vaults: same("0xf0f5a9c105384d5b79e212a9c9d3adfa5d030ef4c0cf9e22fa48674f9ad3652a")
78
- },
79
- objects: {
80
- liquidityRegistry: "0x2b712ae7c38f4b4a09498565a4df8184bda875506b9f65ba8f64cf5662731ccc",
81
- lendingRegistry: "0xc4f71cb64e6c01216f18fbea012176aa2f3a0a747347c914627cbb5aa0e569d2",
82
- oracleRegistry: "0xe645573b6923398b4fbe952fe7e4aa4acc312b0a1c74c7d7417f812613f3173a",
83
- vaultRegistry: "0xef8792ea0c069b7bd8e5c86291180135ff391ce5d5bdfaf205aeee593a486f24",
84
- liquidityAuth: "0x85b19be86131a7a4b378c1b78e8e9b765a30c8c770de0645bfe0e1cb4da5ddb5",
85
- lendingAuth: "0xc145f5f8aae7d72bf56a2ba82708976c057367160857146beedfa0b5409a5702",
86
- vaultAdmin: "0x3ab15bc5037f34fcc5b763e4af4c83f2166811444859616d3ffe5049ef808dd1"
87
- },
88
- oracleVariant: "admin",
89
- tokens: TOKENS,
90
- rpcUrls: ["https://fullnode.mainnet.sui.io:443"],
91
- graphqlUrl: "https://graphql.mainnet.sui.io/graphql"
92
- };
93
- /**
94
70
  * @deprecated Misleading name kept for compatibility: this is the STAGING deployment
95
71
  * (`STAGING`), not production. Use `STAGING` / network "staging".
96
72
  */
97
73
  const MAINNET = STAGING;
98
- /** Default built-in network. Points at the current staging deployment until production ships. */
99
- const DEFAULT_NETWORK = "staging";
100
- const CONFIGS = {
101
- staging: STAGING,
102
- mainnet: STAGING,
103
- "staging-legacy": STAGING_LEGACY
104
- };
74
+ const CONFIGS = { staging: STAGING };
105
75
  /**
106
76
  * Build a read config from a deployment record instead of the built-in ids, e.g.
107
- * `configFromDeployment(JSON.parse(readFileSync(path, "utf8")), { network: "mainnet", oracleV2 })`.
108
- * The record has no RedStone package or Pyth feed map, so `oracleV2` comes from the caller.
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.
109
79
  * Throws once, listing every required field the record is missing.
110
80
  */
111
81
  function configFromDeployment(rec, opts) {
@@ -145,16 +115,18 @@ function configFromDeployment(rec, opts) {
145
115
  oracleAuth: obj("oracle", "auth", false),
146
116
  vaultAdmin: obj("vaults", "admin", false)
147
117
  },
148
- oracleVariant: "v2",
149
- oracleV2: opts.oracleV2,
118
+ oracle: opts.oracle,
150
119
  tokens: opts.tokens ?? TOKENS,
151
120
  rpcUrls: opts.rpcUrls,
152
- graphqlUrl: opts.graphqlUrl
121
+ graphqlUrl: opts.graphqlUrl,
122
+ mathVariant: opts.mathVariant ?? (PRE_AUDIT_LIQUIDITY.has(normalizeSuiObjectId(ids("liquidity").packageId)) ? "pre-audit" : "audited")
153
123
  };
154
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).`);
155
125
  return config;
156
126
  }
157
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.");
158
130
  const cfg = typeof networkOrConfig === "string" ? CONFIGS[networkOrConfig] : networkOrConfig;
159
131
  if (!cfg) throw new Error(`no built-in Fluid config for network "${String(networkOrConfig)}"`);
160
132
  const n = (p) => ({
@@ -187,7 +159,18 @@ const RATE_OUTPUT_DECIMALS = 100000000000000000n;
187
159
  const EXCHANGE_PRICE_SCALE_FACTOR = 1000000000000000000n;
188
160
  /** Lending return precision (1e14 = 1e12 * 100). */
189
161
  const RETURN_PERCENT_PRECISION = 100000000000000n;
190
- /** Lending max rewards rate (50% in 1e12). */
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%). */
191
174
  const MAX_REWARDS_RATE = 50000000000000n;
192
175
  function mulDiv(a, b, c) {
193
176
  if (c === 0n) throw new RangeError("mulDiv: division by zero");
@@ -206,8 +189,94 @@ const min = (a, b) => a < b ? a : b;
206
189
  /** Current unix time in seconds (the default `now` for time-dependent reads). */
207
190
  const nowSeconds = () => BigInt(Math.floor(Date.now() / 1e3));
208
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
+
209
267
  //#endregion
210
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
+ }
211
280
  const RANK = {
212
281
  state: 0,
213
282
  fullnode: 1,
@@ -222,19 +291,42 @@ var NeedsDiscoveryError = class extends Error {
222
291
  this.name = "NeedsDiscoveryError";
223
292
  }
224
293
  };
225
- function makeContext(client, config, derive, now, discovery = "state") {
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
+ };
226
310
  return {
227
311
  client,
228
312
  config,
229
313
  derive,
230
- now: now ?? nowSeconds,
314
+ now,
231
315
  discovery,
232
- allows: (l) => RANK[discovery] >= RANK[l]
316
+ allows: (l) => RANK[discovery] >= RANK[l],
317
+ math: config.mathVariant ?? "audited",
318
+ coinMetadata
233
319
  };
234
320
  }
235
321
 
236
322
  //#endregion
237
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
+ }
238
330
  var AllEndpointsFailedError = class extends Error {
239
331
  constructor(method, errors) {
240
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(" | "));
@@ -268,12 +360,10 @@ function createFailoverClient(endpoints, opts) {
268
360
  if (endpoints.length === 0) throw new Error("createFailoverClient: at least one endpoint required");
269
361
  const labels = endpoints.map((e, i) => typeof e === "string" ? e : `client#${i}`);
270
362
  const clients = endpoints.map((e) => typeof e === "string" ? clientForEndpoint(e, opts.network) : e);
271
- if (clients.length === 1) return Object.assign(clients[0], {
272
- endpoints: labels,
273
- preferred: () => labels[0]
274
- });
275
363
  const timeoutMs = opts.timeoutMs ?? 15e3;
276
- const rounds = opts.rounds ?? 2;
364
+ const rounds = opts.rounds ?? 3;
365
+ const backoffMs = opts.backoffMs ?? 250;
366
+ const maxBackoffMs = opts.maxBackoffMs ?? 4e3;
277
367
  let preferred = 0;
278
368
  const core = new Proxy({}, { get(_t, prop) {
279
369
  const first = clients[preferred].core;
@@ -286,6 +376,7 @@ function createFailoverClient(endpoints, opts) {
286
376
  const errors = [];
287
377
  for (let attempt = 0; attempt < clients.length * rounds; attempt++) {
288
378
  const i = (preferred + attempt) % clients.length;
379
+ if (attempt > 0 && attempt % clients.length === 0) await sleep(backoffDelay(attempt / clients.length + 1, backoffMs, maxBackoffMs));
289
380
  const c = clients[i].core;
290
381
  try {
291
382
  const out = await withTimeout(attempt === 0 ? res : c[prop].apply(c, args), timeoutMs, `${labels[i]} core.${String(prop)}`);
@@ -312,6 +403,151 @@ function createFailoverClient(endpoints, opts) {
312
403
  } });
313
404
  }
314
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
+
315
551
  //#endregion
316
552
  //#region src/bcs.ts
317
553
  /**
@@ -321,7 +557,6 @@ function createFailoverClient(endpoints, opts) {
321
557
  * Single-field wrappers (UID, ID, TimestampS, DurationS, Balance, Supply) encode as their field.
322
558
  */
323
559
  var bcs_exports = /* @__PURE__ */ __exportAll({
324
- AdminOracle: () => AdminOracle,
325
560
  Branch: () => Branch,
326
561
  BranchStatus: () => BranchStatus,
327
562
  CapRecord: () => CapRecord,
@@ -331,17 +566,16 @@ var bcs_exports = /* @__PURE__ */ __exportAll({
331
566
  LendingState: () => LendingState,
332
567
  LiquidityRegistry: () => LiquidityRegistry,
333
568
  MoveTick: () => MoveTick,
569
+ OracleObject: () => OracleObject,
334
570
  OracleRegistry: () => OracleRegistry,
335
571
  OracleSource: () => OracleSource,
336
572
  OracleSourceType: () => OracleSourceType,
337
- OracleV2: () => OracleV2,
338
573
  PauseFlags: () => PauseFlags,
339
574
  Position: () => Position,
340
575
  PositionIndex: () => PositionIndex$1,
341
576
  PositionIndexEntry: () => PositionIndexEntry,
342
577
  PositionIndexField: () => PositionIndexField,
343
578
  PriceQuote: () => PriceQuote,
344
- PriceQuoteV2: () => PriceQuoteV2,
345
579
  PythPriceInfoObject: () => PythPriceInfoObject,
346
580
  RateDataV1Params: () => RateDataV1Params,
347
581
  RateDataV2Params: () => RateDataV2Params,
@@ -574,22 +808,11 @@ const VaultRegistry = bcs.struct("VaultRegistry", {
574
808
  next_vault_id: U64,
575
809
  version: U64
576
810
  });
577
- const AdminOracle = bcs.struct("Oracle", {
578
- id: Address,
579
- name: bcs.string(),
580
- rate: U128,
581
- version: U64
582
- });
583
811
  const OracleRegistry = bcs.struct("OracleRegistry", {
584
812
  id: Address,
585
813
  version: U64,
586
814
  total_oracles: U64
587
815
  });
588
- const PriceQuote = bcs.struct("PriceQuote", {
589
- oracle_id: Address,
590
- rate_liquidate: U128,
591
- rate_operate: U128
592
- });
593
816
  const OracleSourceType = bcs.enum("SourceType", {
594
817
  Redstone: bcs.struct("Redstone", { adapter_id: Address }),
595
818
  Pyth: null
@@ -599,15 +822,15 @@ const OracleSource = bcs.struct("Source", {
599
822
  invert: bcs.bool(),
600
823
  source_type: OracleSourceType
601
824
  });
602
- /** `fluid_oracle::oracle::Oracle` (v2): derived from the registry by nonce. */
603
- const OracleV2 = bcs.struct("Oracle", {
825
+ /** `fluid_oracle::oracle::Oracle` derived from the registry by nonce. */
826
+ const OracleObject = bcs.struct("Oracle", {
604
827
  id: Address,
605
828
  nonce: bcs.u16(),
606
829
  main: OracleSource,
607
830
  guard: bcs.option(OracleSource)
608
831
  });
609
- /** v2 `view::PriceQuote`: each rate is optional (operate-only / liquidate-only quotes). */
610
- const PriceQuoteV2 = bcs.struct("PriceQuote", {
832
+ /** `view::PriceQuote`: each rate is optional (operate-only / liquidate-only quotes). */
833
+ const PriceQuote = bcs.struct("PriceQuote", {
611
834
  oracle_id: Address,
612
835
  rate_liquidate: bcs.option(U128),
613
836
  rate_operate: bcs.option(U128)
@@ -816,6 +1039,8 @@ var liquidity_exports = /* @__PURE__ */ __exportAll({
816
1039
  processOverallTokenData: () => processOverallTokenData,
817
1040
  processUserBorrowData: () => processUserBorrowData,
818
1041
  processUserSupplyData: () => processUserSupplyData,
1042
+ supplyRateAt: () => supplyRateAt,
1043
+ supplyRateRaw: () => supplyRateRaw,
819
1044
  totalAmountsOf: () => totalAmountsOf,
820
1045
  totalBorrowNormal: () => totalBorrowNormal,
821
1046
  totalSupplyNormal: () => totalSupplyNormal
@@ -825,8 +1050,31 @@ function withInterestVsFreeRatio(withInterest, interestFree) {
825
1050
  if (withInterest < interestFree) return withInterest * FOUR_DECIMALS / interestFree;
826
1051
  return withInterest > 0n ? FOUR_DECIMALS : 0n;
827
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
+ }
828
1076
  /** `reserve::calculate_exchange_prices(now)`. */
829
- function calculateExchangePrice(r, now) {
1077
+ function calculateExchangePrice(r, now, variant = "audited") {
830
1078
  let supplyExchangePrice = r.supplyExchangePrice;
831
1079
  let borrowExchangePrice = r.borrowExchangePrice;
832
1080
  if (supplyExchangePrice === 0n || borrowExchangePrice === 0n) throw new RangeError("exchange price zero (reserve never initialised)");
@@ -835,34 +1083,23 @@ function calculateExchangePrice(r, now) {
835
1083
  supplyExchangePrice,
836
1084
  borrowExchangePrice
837
1085
  };
1086
+ const borrowBefore = borrowExchangePrice;
838
1087
  borrowExchangePrice += divCeil(borrowExchangePrice * r.borrowRate * elapsed, SECONDS_PER_YEAR * FOUR_DECIMALS);
839
- if (r.totalSupplyWithInterest === 0n) return {
840
- supplyExchangePrice,
841
- borrowExchangePrice
842
- };
843
- const supplyWithInterestNormal = divCeil(r.totalSupplyWithInterest * supplyExchangePrice, EXCHANGE_PRICES_PRECISION);
844
- let supplyRatio = withInterestVsFreeRatio(supplyWithInterestNormal, r.totalSupplyInterestFree);
845
- let ratioSupplyYield;
846
- if (supplyWithInterestNormal < r.totalSupplyInterestFree) {
847
- if (supplyRatio === 0n) return {
848
- supplyExchangePrice,
849
- borrowExchangePrice
850
- };
851
- supplyRatio = RATE_OUTPUT_DECIMALS * FOUR_DECIMALS / supplyRatio;
852
- ratioSupplyYield = r.lastUtilization * (RATE_OUTPUT_DECIMALS + supplyRatio) / FOUR_DECIMALS;
853
- } else ratioSupplyYield = r.lastUtilization * RATE_OUTPUT_DECIMALS * (FOUR_DECIMALS + supplyRatio) / (FOUR_DECIMALS * FOUR_DECIMALS);
854
- const borrowWithInterestNormal = r.totalBorrowWithInterest * borrowExchangePrice / EXCHANGE_PRICES_PRECISION;
855
- let borrowRatio = withInterestVsFreeRatio(borrowWithInterestNormal, r.totalBorrowInterestFree);
856
- borrowRatio = borrowWithInterestNormal < r.totalBorrowInterestFree ? borrowRatio * RATE_OUTPUT_DECIMALS / (FOUR_DECIMALS + borrowRatio) : RATE_OUTPUT_DECIMALS - borrowRatio * RATE_OUTPUT_DECIMALS / (FOUR_DECIMALS + borrowRatio);
857
- ratioSupplyYield = mulDiv(ratioSupplyYield, borrowRatio, RATE_OUTPUT_DECIMALS);
858
- ratioSupplyYield = ratioSupplyYield * FOUR_DECIMALS / RATE_OUTPUT_DECIMALS;
859
- const supplyRate = r.borrowRate * ratioSupplyYield * (FOUR_DECIMALS - r.fee);
860
- supplyExchangePrice += supplyExchangePrice * supplyRate * elapsed / (SECONDS_PER_YEAR * FOUR_DECIMALS) / (FOUR_DECIMALS * FOUR_DECIMALS);
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);
861
1090
  return {
862
1091
  supplyExchangePrice,
863
1092
  borrowExchangePrice
864
1093
  };
865
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
+ }
866
1103
  /** `liquidity_calcs::get_total_supply` (floor). */
867
1104
  const totalSupplyNormal = (r, supplyEp) => r.totalSupplyInterestFree + r.totalSupplyWithInterest * supplyEp / EXCHANGE_PRICES_PRECISION;
868
1105
  /** `liquidity_calcs::get_total_borrow` (floor). */
@@ -883,15 +1120,13 @@ function totalAmountsOf(r) {
883
1120
  totalClaimAmount: 0n
884
1121
  };
885
1122
  }
886
- /** Same shape/semantics as the Solana reader's `processOverallTokenData`. */
887
- function processOverallTokenData(r, rateData, now) {
888
- const { supplyExchangePrice, borrowExchangePrice } = calculateExchangePrice(r, now);
889
- let supplyRate = 0n;
890
- if (r.totalSupplyWithInterest !== 0n) {
891
- const borrowForRate = r.totalBorrowWithInterest * r.borrowExchangePrice / EXCHANGE_PRICES_PRECISION;
892
- const supplyForRate = r.totalSupplyWithInterest * r.supplyExchangePrice / EXCHANGE_PRICES_PRECISION;
893
- if (supplyForRate !== 0n) supplyRate = r.borrowRate * (FOUR_DECIMALS - r.fee) * borrowForRate / (supplyForRate * FOUR_DECIMALS);
894
- }
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));
895
1130
  return {
896
1131
  rateData,
897
1132
  supplyExchangePrice,
@@ -1035,6 +1270,7 @@ var lending_exports = /* @__PURE__ */ __exportAll({
1035
1270
  previews: () => previews,
1036
1271
  rateSegments: () => rateSegments,
1037
1272
  rewardsRateFor: () => rewardsRateFor,
1273
+ rewardsTvl: () => rewardsTvl,
1038
1274
  toAssets: () => toAssets,
1039
1275
  toShares: () => toShares
1040
1276
  });
@@ -1062,30 +1298,40 @@ function rateSegments(s, totalAssets, fromS, nowS) {
1062
1298
  }
1063
1299
  const maxB = (a, b) => a > b ? a : b;
1064
1300
  const minB = (a, b) => a < b ? a : b;
1065
- /** Current rewards rate (1e12 = 100%) at `now`, from the active schedule segment. */
1066
- function currentRewardsRate(state, now) {
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") {
1067
1313
  if (!state.rewards) return 0n;
1068
- const tvl = toAssets(state.totalShares, state.tokenExchangePrice, false);
1314
+ const tvl = rewardsTvl(state, variant);
1069
1315
  const s = state.rewards;
1070
1316
  if (now >= s.startS && now < s.endS) return rewardsRateFor(s.yearlyReward, tvl, s.startTvl);
1071
1317
  if (now >= s.nextStartS && now < s.nextEndS) return rewardsRateFor(s.nextYearlyReward, tvl, s.startTvl);
1072
1318
  return 0n;
1073
1319
  }
1074
1320
  /** `accounting::accrued_token_exchange_price`. */
1075
- function accruedTokenExchangePrice(state, newLiquidityExchangePrice, nowS) {
1321
+ function accruedTokenExchangePrice(state, newLiquidityExchangePrice, nowS, variant = "audited") {
1076
1322
  const oldLiq = state.liquidityExchangePrice;
1077
1323
  const oldToken = state.tokenExchangePrice;
1078
1324
  if (newLiquidityExchangePrice < oldLiq) throw new RangeError("liquidity exchange price went backwards");
1079
1325
  let totalReturn = mulDiv(newLiquidityExchangePrice - oldLiq, RETURN_PERCENT_PRECISION, oldLiq);
1080
1326
  if (state.rewards) {
1081
- const tvl = toAssets(state.totalShares, oldToken, false);
1327
+ const tvl = rewardsTvl(state, variant);
1082
1328
  for (const seg of rateSegments(state.rewards, tvl, state.lastUpdateS, nowS)) totalReturn += mulDiv(seg.rate, seg.toS - seg.fromS, SECONDS_PER_YEAR);
1083
1329
  }
1084
1330
  return oldToken + mulDiv(oldToken, totalReturn, RETURN_PERCENT_PRECISION);
1085
1331
  }
1086
1332
  /** `accounting::preview_*` given the reserve's current liquidity (supply) exchange price. */
1087
- function previews(state, liquidityExchangePrice, nowS, assets, shares) {
1088
- const tokenEp = accruedTokenExchangePrice(state, liquidityExchangePrice, nowS);
1333
+ function previews(state, liquidityExchangePrice, nowS, assets, shares, variant = "audited") {
1334
+ const tokenEp = accruedTokenExchangePrice(state, liquidityExchangePrice, nowS, variant);
1089
1335
  return {
1090
1336
  tokenExchangePrice: tokenEp,
1091
1337
  previewDeposit: toShares(toAssets(toShares(assets, liquidityExchangePrice, false), liquidityExchangePrice, false), tokenEp, false),
@@ -1095,81 +1341,6 @@ function previews(state, liquidityExchangePrice, nowS, assets, shares) {
1095
1341
  };
1096
1342
  }
1097
1343
 
1098
- //#endregion
1099
- //#region src/objects.ts
1100
- /** `getObjects` accepts at most 50 ids per call. */
1101
- const BATCH = 50;
1102
- /** Batched fetch. Missing/deleted ids come back as `null` in input order; other errors throw. */
1103
- async function fetchObjects(client, ids) {
1104
- const out = [];
1105
- const chunks = [];
1106
- for (let i = 0; i < ids.length; i += BATCH) chunks.push(ids.slice(i, i + BATCH));
1107
- const pages = await Promise.all(chunks.map((c) => client.core.getObjects({
1108
- objectIds: c,
1109
- include: { content: true }
1110
- })));
1111
- for (const { objects } of pages) for (const o of objects) {
1112
- if (o instanceof Error) {
1113
- if (o instanceof ObjectError && (o.reason === "notFound" || o.reason === "deleted")) {
1114
- out.push(null);
1115
- continue;
1116
- }
1117
- throw o;
1118
- }
1119
- out.push({
1120
- objectId: o.objectId,
1121
- type: o.type,
1122
- content: o.content,
1123
- owner: o.owner
1124
- });
1125
- }
1126
- return out;
1127
- }
1128
- var ObjectNotFoundError = class extends Error {
1129
- constructor(objectId, what) {
1130
- super(`${what} not found: ${objectId}`);
1131
- this.objectId = objectId;
1132
- this.what = what;
1133
- this.name = "ObjectNotFoundError";
1134
- }
1135
- };
1136
- async function fetchObject(client, id, what) {
1137
- const [o] = await fetchObjects(client, [id]);
1138
- if (!o) throw new ObjectNotFoundError(id, what);
1139
- return o;
1140
- }
1141
- /** Every dynamic-field id under a parent (Table), paginated. */
1142
- async function listDynamicFieldIds(client, parentId) {
1143
- const ids = [];
1144
- let cursor = null;
1145
- do {
1146
- const page = await client.core.listDynamicFields({
1147
- parentId,
1148
- cursor,
1149
- limit: 50
1150
- });
1151
- for (const f of page.dynamicFields) ids.push(f.fieldId);
1152
- cursor = page.hasNextPage ? page.cursor : null;
1153
- } while (cursor !== null);
1154
- return ids;
1155
- }
1156
- /** Owner address (or parent object id) from a core-API owner union. */
1157
- function ownerAddress(owner) {
1158
- if (!owner || typeof owner !== "object") return null;
1159
- const o = owner;
1160
- for (const k of [
1161
- "AddressOwner",
1162
- "ObjectOwner",
1163
- "ConsensusAddressOwner"
1164
- ]) {
1165
- const v = o[k];
1166
- if (typeof v === "string") return v;
1167
- if (v && typeof v === "object" && typeof v.owner === "string") return v.owner;
1168
- }
1169
- if (o.$kind === "Shared") return null;
1170
- return null;
1171
- }
1172
-
1173
1344
  //#endregion
1174
1345
  //#region src/view.ts
1175
1346
  /** Any address works as the simulated sender; nothing is executed. */
@@ -1214,12 +1385,18 @@ var Lending = class {
1214
1385
  this.ctx = ctx;
1215
1386
  this.liquidity = liquidity;
1216
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
+ }
1217
1393
  /** LendingStates (+ rewards fields) for `tokens`; `null` where no pool exists. */
1218
1394
  async loadLendings(tokens) {
1219
- const d = this.ctx.derive;
1395
+ return this.lendingsFromObjects(tokens, await fetchObjects(this.ctx.client, this.lendingObjectIds(tokens)));
1396
+ }
1397
+ lendingsFromObjects(tokens, objs) {
1220
1398
  const norm = tokens.map((t) => normalizeStructTag(t));
1221
- const ids = norm.map((t) => d.lendingStateId(t));
1222
- const objs = await fetchObjects(this.ctx.client, [...ids, ...ids.map((id) => d.rewardsId(id))]);
1399
+ const ids = norm.map((t) => this.ctx.derive.lendingStateId(t));
1223
1400
  return norm.map((token, i) => {
1224
1401
  const o = objs[i];
1225
1402
  if (!o) return null;
@@ -1248,23 +1425,54 @@ var Lending = class {
1248
1425
  };
1249
1426
  });
1250
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
+ }
1251
1460
  async loadOne(token) {
1252
- const [[lending], reserve] = await Promise.all([this.loadLendings([token]), this.liquidity.loadReserve(token)]);
1253
- if (!lending) throw new Error(`no LendingState for ${token}`);
1461
+ const { pools, now } = await this.loadPools([token]);
1254
1462
  return {
1255
- lending,
1256
- reserve
1463
+ ...pools[0],
1464
+ now
1257
1465
  };
1258
1466
  }
1259
1467
  async getAllLendings() {
1260
1468
  const tokens = await this.liquidity.listedTokens();
1261
1469
  const recs = (await this.loadLendings(tokens)).filter((r) => r !== null);
1262
- const metas = await Promise.all(recs.map((r) => this.ctx.client.core.getCoinMetadata({ coinType: r.token })));
1470
+ const metas = await Promise.all(recs.map((r) => this.ctx.coinMetadata(r.token)));
1263
1471
  return recs.map((r, i) => ({
1264
1472
  id: r.id,
1265
1473
  mint: r.token,
1266
1474
  fTokenMint: this.ctx.derive.fTokenType(r.token),
1267
- decimals: metas[i].coinMetadata?.decimals ?? 0,
1475
+ decimals: metas[i]?.decimals ?? 0,
1268
1476
  totalShares: r.snapshot.totalShares,
1269
1477
  liquidityExchangePrice: r.snapshot.liquidityExchangePrice,
1270
1478
  tokenExchangePrice: r.snapshot.tokenExchangePrice,
@@ -1281,16 +1489,14 @@ var Lending = class {
1281
1489
  getAllJlTokens = () => this.getAllFTokens();
1282
1490
  /** Token exchange price accrued to now (`accounting::calculate_new_token_exchange_price`). */
1283
1491
  async getExchangePrice(token) {
1284
- const { lending, reserve } = await this.loadOne(token);
1285
- const now = this.ctx.now();
1286
- const liqEp = calculateExchangePrice(reserve.snapshot, now).supplyExchangePrice;
1287
- return accruedTokenExchangePrice(lending.snapshot, liqEp, now);
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);
1288
1495
  }
1289
1496
  async getPreviews(token, assets, shares) {
1290
- const { lending, reserve } = await this.loadOne(token);
1291
- const now = this.ctx.now();
1292
- const liqEp = calculateExchangePrice(reserve.snapshot, now).supplyExchangePrice;
1293
- const p = previews(lending.snapshot, liqEp, now, assets, shares);
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);
1294
1500
  return {
1295
1501
  previewDeposit: p.previewDeposit,
1296
1502
  previewMint: p.previewMint,
@@ -1298,11 +1504,13 @@ var Lending = class {
1298
1504
  previewRedeem: p.previewRedeem
1299
1505
  };
1300
1506
  }
1301
- detailsFromRecords(lending, reserve, supplyEntry, meta, now = this.ctx.now()) {
1507
+ detailsFromRecords(lending, reserve, supplyEntry, meta, now) {
1302
1508
  const { userSupplyData, overallTokenData } = this.liquidity.supplyDataFromRecords(reserve, supplyEntry, now);
1303
- const tokenEp = accruedTokenExchangePrice(lending.snapshot, overallTokenData.supplyExchangePrice, now);
1509
+ const tokenEp = accruedTokenExchangePrice(lending.snapshot, overallTokenData.supplyExchangePrice, now, this.ctx.math);
1304
1510
  const totalAssets = toAssets(lending.snapshot.totalShares, tokenEp, false);
1305
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));
1306
1514
  return {
1307
1515
  tokenAddress: this.ctx.derive.fTokenType(lending.token),
1308
1516
  lendingStateId: lending.id,
@@ -1314,60 +1522,207 @@ var Lending = class {
1314
1522
  totalSupply: lending.snapshot.totalShares,
1315
1523
  conversionRateToShares: toShares(one, tokenEp, false),
1316
1524
  conversionRateToAssets: toAssets(one, tokenEp, false),
1317
- rewardsRate: currentRewardsRate(lending.snapshot, now),
1318
- supplyRate: overallTokenData.supplyRate,
1525
+ rewardsRate,
1526
+ supplyRate,
1527
+ totalRate: supplyRate + rewardsRate,
1319
1528
  rebalanceDifference: userSupplyData.supply - totalAssets,
1320
1529
  userSupplyData
1321
1530
  };
1322
1531
  }
1532
+ /** fToken name/symbol/decimals (cached; falls back to the underlying's metadata). */
1323
1533
  async fTokenMeta(token) {
1324
- const [f, u] = await Promise.all([this.ctx.client.core.getCoinMetadata({ coinType: this.ctx.derive.fTokenType(token) }).catch(() => ({ coinMetadata: null })), this.ctx.client.core.getCoinMetadata({ coinType: token })]);
1325
- const under = u.coinMetadata;
1534
+ const [f, under] = await Promise.all([this.ctx.coinMetadata(this.ctx.derive.fTokenType(token)).catch(() => null), this.ctx.coinMetadata(token)]);
1326
1535
  return {
1327
- name: f.coinMetadata?.name ?? `Fluid ${under?.name ?? ""}`.trim(),
1328
- symbol: f.coinMetadata?.symbol ?? `f${under?.symbol ?? ""}`,
1329
- decimals: f.coinMetadata?.decimals ?? under?.decimals ?? 0
1536
+ name: f?.name ?? `Fluid ${under?.name ?? ""}`.trim(),
1537
+ symbol: f?.symbol ?? `f${under?.symbol ?? ""}`,
1538
+ decimals: f?.decimals ?? under?.decimals ?? 0
1330
1539
  };
1331
1540
  }
1332
1541
  async getFTokenDetails(token) {
1333
- const { lending, reserve } = await this.loadOne(token);
1334
- const [[entry], meta] = await Promise.all([this.liquidity.loadUserEntries([{
1335
- holder: lending.id,
1336
- reserve
1337
- }]), this.fTokenMeta(lending.token)]);
1338
- return this.detailsFromRecords(lending, reserve, entry.supply, meta);
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
+ });
1339
1558
  }
1340
1559
  /** @deprecated Solana name; use `getFTokenDetails`. */
1341
1560
  getJlTokenDetails = (token) => this.getFTokenDetails(token);
1342
1561
  async getAllFTokenDetails() {
1343
- return Promise.all((await this.getAllFTokens()).map((t) => this.getFTokenDetails(t)));
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);
1344
1570
  }
1345
1571
  /** @deprecated Solana name; use `getAllFTokenDetails`. */
1346
1572
  getAllJlTokenDetails = () => this.getAllFTokenDetails();
1347
1573
  async getFTokenInternalData(token) {
1348
- const { lending, reserve } = await this.loadOne(token);
1349
- const [entry] = await this.liquidity.loadUserEntries([{
1350
- holder: lending.id,
1351
- reserve
1352
- }]);
1353
- const now = this.ctx.now();
1574
+ const { pools, now } = await this.loadPools([token], true);
1575
+ const { lending, reserve, supplyEntry } = pools[0];
1576
+ const entry = { supply: supplyEntry };
1354
1577
  const { userSupplyData } = this.liquidity.supplyDataFromRecords(reserve, entry.supply, now);
1355
- const liqEp = calculateExchangePrice(reserve.snapshot, now).supplyExchangePrice;
1578
+ const liqEp = calculateExchangePrice(reserve.snapshot, now, this.ctx.math).supplyExchangePrice;
1356
1579
  return {
1357
1580
  liquidity: reserve.id,
1358
1581
  lendingFactory: this.ctx.config.objects.lendingRegistry,
1359
1582
  lendingRewardsRateModel: lending.raw.rewards_writer ?? null,
1360
- rebalancer: "",
1361
1583
  liquidityBalance: userSupplyData.supply,
1362
1584
  liquidityExchangePrice: liqEp,
1363
- tokenExchangePrice: accruedTokenExchangePrice(lending.snapshot, liqEp, now)
1585
+ tokenExchangePrice: accruedTokenExchangePrice(lending.snapshot, liqEp, now, this.ctx.math)
1364
1586
  };
1365
1587
  }
1366
1588
  /** @deprecated Solana name; use `getFTokenInternalData`. */
1367
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]`. */
1368
1723
  async getFTokenRewards(token) {
1369
- const { lending } = await this.loadOne(token);
1370
- return [lending.raw.rewards_writer ?? null, currentRewardsRate(lending.snapshot, this.ctx.now())];
1724
+ const { lending, now } = await this.loadOne(token);
1725
+ return [lending.raw.rewards_writer ?? null, rewardsRateToBps(currentRewardsRate(lending.snapshot, now, this.ctx.math))];
1371
1726
  }
1372
1727
  /** @deprecated Solana name; use `getFTokenRewards`. */
1373
1728
  getJlTokenRewards = (token) => this.getFTokenRewards(token);
@@ -1380,7 +1735,7 @@ var Lending = class {
1380
1735
  startTime: r.startS,
1381
1736
  endTime: r.endS,
1382
1737
  startTvl: r.startTvl,
1383
- maxRate: 50000000000000n,
1738
+ maxRate: rewardsRateToBps(MAX_REWARDS_RATE),
1384
1739
  rewardAmount: r.yearlyReward,
1385
1740
  nextStartTime: r.nextStartS,
1386
1741
  nextEndTime: r.nextEndS,
@@ -1405,16 +1760,30 @@ var Lending = class {
1405
1760
  return {
1406
1761
  fTokenShares: s,
1407
1762
  underlyingAssets: toAssets(s, ep, false),
1408
- underlyingBalance: BigInt(under.balance.balance),
1409
- allowance: 0n
1763
+ underlyingBalance: BigInt(under.balance.balance)
1410
1764
  };
1411
1765
  }
1412
1766
  async getUserPositions(user) {
1413
- const tokens = await this.getAllFTokens();
1414
- return Promise.all(tokens.map(async (t) => ({
1415
- fTokenDetails: await this.getFTokenDetails(t),
1416
- userPosition: await this.getUserPosition(t, user)
1417
- })));
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
+ });
1418
1787
  }
1419
1788
  /** `lending::auth::is_admin(&Auth, addr)` via simulation. */
1420
1789
  async isLendingAdmin(addr) {
@@ -1549,11 +1918,15 @@ var Liquidity = class {
1549
1918
  constructor(ctx) {
1550
1919
  this.ctx = ctx;
1551
1920
  }
1552
- /** Reserves + their rate-data fields in 2 batched round trips. */
1553
- async loadReserves(tokens) {
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) {
1554
1928
  const norm = tokens.map((t) => normalizeStructTag(t));
1555
1929
  const ids = norm.map((t) => this.ctx.derive.tokenReserveId(t));
1556
- const objs = await fetchObjects(this.ctx.client, [...ids, ...ids.map((id) => this.ctx.derive.rateDataId(id))]);
1557
1930
  return norm.map((token, i) => {
1558
1931
  const o = objs[i];
1559
1932
  if (!o) throw new Error(`no TokenReserve for ${token} (${ids[i]})`);
@@ -1567,6 +1940,10 @@ var Liquidity = class {
1567
1940
  };
1568
1941
  });
1569
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
+ }
1570
1947
  async loadReserve(token) {
1571
1948
  return (await this.loadReserves([token]))[0];
1572
1949
  }
@@ -1678,9 +2055,9 @@ var Liquidity = class {
1678
2055
  maxUtilization: s.maxUtilization
1679
2056
  };
1680
2057
  }
1681
- /** Pure: accrue a reserve snapshot to `now` (defaults to the context clock). */
2058
+ /** Pure: accrue a reserve snapshot to `now` (unix seconds; use `await ctx.now()` for chain time). */
1682
2059
  calculateExchangePrice(snapshot, now) {
1683
- return calculateExchangePrice(snapshot, now ?? this.ctx.now());
2060
+ return calculateExchangePrice(snapshot, now, this.ctx.math);
1684
2061
  }
1685
2062
  async getRateConfig(token) {
1686
2063
  const { rateData: r } = await this.loadReserve(token);
@@ -1715,8 +2092,8 @@ var Liquidity = class {
1715
2092
  return { maxUtilization: (await this.loadReserve(token)).snapshot.maxUtilization };
1716
2093
  }
1717
2094
  async getRevenue(token) {
1718
- const { snapshot } = await this.loadReserve(token);
1719
- const ep = calculateExchangePrice(snapshot, this.ctx.now());
2095
+ const [{ snapshot }, now] = await Promise.all([this.loadReserve(token), this.ctx.now()]);
2096
+ const ep = calculateExchangePrice(snapshot, now, this.ctx.math);
1720
2097
  return calcRevenue(snapshot, ep.supplyExchangePrice, ep.borrowExchangePrice);
1721
2098
  }
1722
2099
  async getTokenRateData(token) {
@@ -1725,15 +2102,16 @@ var Liquidity = class {
1725
2102
  async getTokensRateData(tokens) {
1726
2103
  return (await this.loadReserves(tokens)).map((r) => r.rateData);
1727
2104
  }
1728
- overallFromRecord(r, now = this.ctx.now()) {
1729
- return processOverallTokenData(r.snapshot, r.rateData, now);
2105
+ overallFromRecord(r, now) {
2106
+ return processOverallTokenData(r.snapshot, r.rateData, now, this.ctx.math);
1730
2107
  }
1731
2108
  async getOverallTokenData(token) {
1732
- return this.overallFromRecord(await this.loadReserve(token));
2109
+ const [r, now] = await Promise.all([this.loadReserve(token), this.ctx.now()]);
2110
+ return this.overallFromRecord(r, now);
1733
2111
  }
1734
2112
  async getOverallTokensData(tokens) {
1735
- const now = this.ctx.now();
1736
- return (await this.loadReserves(tokens)).map((r) => this.overallFromRecord(r, now));
2113
+ const [rs, now] = await Promise.all([this.loadReserves(tokens), this.ctx.now()]);
2114
+ return rs.map((r) => this.overallFromRecord(r, now));
1737
2115
  }
1738
2116
  async getAllOverallTokensData() {
1739
2117
  return this.getOverallTokensData(await this.listedTokens());
@@ -1755,14 +2133,14 @@ var Liquidity = class {
1755
2133
  }]);
1756
2134
  return e.borrow ?? 0n;
1757
2135
  }
1758
- supplyDataFromRecords(reserve, acc, now = this.ctx.now()) {
2136
+ supplyDataFromRecords(reserve, acc, now) {
1759
2137
  const overallTokenData = this.overallFromRecord(reserve, now);
1760
2138
  return {
1761
2139
  userSupplyData: processUserSupplyData(acc, overallTokenData, reserve.snapshot.vaultBalance, now),
1762
2140
  overallTokenData
1763
2141
  };
1764
2142
  }
1765
- borrowDataFromRecords(reserve, acc, now = this.ctx.now()) {
2143
+ borrowDataFromRecords(reserve, acc, now) {
1766
2144
  const overallTokenData = this.overallFromRecord(reserve, now);
1767
2145
  return {
1768
2146
  userBorrowData: processUserBorrowData(acc, overallTokenData, reserve.snapshot.vaultBalance, now),
@@ -1770,28 +2148,27 @@ var Liquidity = class {
1770
2148
  };
1771
2149
  }
1772
2150
  async getUserSupplyData(user, token) {
1773
- const reserve = await this.loadReserve(token);
2151
+ const [reserve, now] = await Promise.all([this.loadReserve(token), this.ctx.now()]);
1774
2152
  const [e] = await this.loadUserEntries([{
1775
2153
  holder: user,
1776
2154
  reserve
1777
2155
  }]);
1778
- return this.supplyDataFromRecords(reserve, e.supply);
2156
+ return this.supplyDataFromRecords(reserve, e.supply, now);
1779
2157
  }
1780
2158
  async getUserBorrowData(user, token) {
1781
- const reserve = await this.loadReserve(token);
2159
+ const [reserve, now] = await Promise.all([this.loadReserve(token), this.ctx.now()]);
1782
2160
  const [e] = await this.loadUserEntries([{
1783
2161
  holder: user,
1784
2162
  reserve
1785
2163
  }]);
1786
- return this.borrowDataFromRecords(reserve, e.borrow);
2164
+ return this.borrowDataFromRecords(reserve, e.borrow, now);
1787
2165
  }
1788
2166
  async getUserMultipleSupplyData(user, tokens) {
1789
- const reserves = await this.loadReserves(tokens);
2167
+ const [reserves, now] = await Promise.all([this.loadReserves(tokens), this.ctx.now()]);
1790
2168
  const entries = await this.loadUserEntries(reserves.map((reserve) => ({
1791
2169
  holder: user,
1792
2170
  reserve
1793
2171
  })));
1794
- const now = this.ctx.now();
1795
2172
  const out = reserves.map((r, i) => this.supplyDataFromRecords(r, entries[i].supply, now));
1796
2173
  return {
1797
2174
  userSuppliesData: out.map((o) => o.userSupplyData),
@@ -1799,12 +2176,11 @@ var Liquidity = class {
1799
2176
  };
1800
2177
  }
1801
2178
  async getUserMultipleBorrowData(user, tokens) {
1802
- const reserves = await this.loadReserves(tokens);
2179
+ const [reserves, now] = await Promise.all([this.loadReserves(tokens), this.ctx.now()]);
1803
2180
  const entries = await this.loadUserEntries(reserves.map((reserve) => ({
1804
2181
  holder: user,
1805
2182
  reserve
1806
2183
  })));
1807
- const now = this.ctx.now();
1808
2184
  const out = reserves.map((r, i) => this.borrowDataFromRecords(r, entries[i].borrow, now));
1809
2185
  return {
1810
2186
  userBorrowingsData: out.map((o) => o.userBorrowData),
@@ -1922,13 +2298,11 @@ function pick(o, keys) {
1922
2298
  * returns `null` for a side whose view aborts, with the abort in `operateError` / `liquidateError`.
1923
2299
  * `getOracleRoutes` reads each route on its own (RedStone via the adapter's view, Pyth by decoding
1924
2300
  * the PriceInfoObject) so callers see which route set the price and whether the guard is live.
1925
- *
1926
- * admin (2026-09-18 preset only): `Oracle { name, rate }`, `view::quote(&Oracle)`.
1927
2301
  */
1928
2302
  /** Oracle rates are debt-per-collateral in the vault's internal 1e9 space, scaled by 1e15. */
1929
2303
  const ORACLE_RATE_PRECISION = 1000000000000000n;
1930
2304
  /** Mirrors `fluid_oracle::constants` of the deployed v2 package. */
1931
- const ORACLE_V2_LIMITS = {
2305
+ const ORACLE_LIMITS = {
1932
2306
  rateOutputDecimals: 15,
1933
2307
  maxAgeOperateS: 600n,
1934
2308
  maxAgeLiquidateS: 7200n,
@@ -1951,9 +2325,9 @@ function toSource(s) {
1951
2325
  }
1952
2326
  /** `rate::scale_and_invert`: raw provider price with `decimals` → 1e15 rate, optionally inverted. */
1953
2327
  function scaleAndInvert(raw, decimals, invert) {
1954
- const out = ORACLE_V2_LIMITS.rateOutputDecimals;
2328
+ const out = ORACLE_LIMITS.rateOutputDecimals;
1955
2329
  let rate = decimals > out ? raw / 10n ** BigInt(decimals - out) : raw * 10n ** BigInt(out - decimals);
1956
- if (rate > 0n && invert) rate = rate > ORACLE_V2_LIMITS.maxInvertibleRate ? 0n : ORACLE_RATE_PRECISION * ORACLE_RATE_PRECISION / rate;
2330
+ if (rate > 0n && invert) rate = rate > ORACLE_LIMITS.maxInvertibleRate ? 0n : ORACLE_RATE_PRECISION * ORACLE_RATE_PRECISION / rate;
1957
2331
  return rate;
1958
2332
  }
1959
2333
  function abortText(e) {
@@ -1968,26 +2342,15 @@ var Oracle = class {
1968
2342
  constructor(ctx) {
1969
2343
  this.ctx = ctx;
1970
2344
  }
1971
- get v2() {
1972
- const c = this.ctx.config.oracleV2;
1973
- if (this.ctx.config.oracleVariant !== "v2" || !c) throw new Error("config has no v2 oracle (oracleVariant/oracleV2)");
2345
+ get providers() {
2346
+ const c = this.ctx.config.oracle;
2347
+ if (!c) throw new Error("config has no oracle (config.oracle)");
1974
2348
  return c;
1975
2349
  }
1976
- /** Admin oracle object (2026-09-18 preset). */
1977
- async getOracle(oracleId) {
1978
- const id = normalizeSuiObjectId(oracleId);
1979
- const o = AdminOracle.parse((await fetchObject(this.ctx.client, id, "Oracle")).content);
1980
- return {
1981
- id,
1982
- name: o.name,
1983
- rate: BigInt(o.rate),
1984
- version: BigInt(o.version)
1985
- };
1986
- }
1987
2350
  /** v2 oracle object: nonce + main / guard routes. */
1988
- async getOracleV2(oracleId) {
2351
+ async getOracle(oracleId) {
1989
2352
  const id = normalizeSuiObjectId(oracleId);
1990
- const o = OracleV2.parse((await fetchObject(this.ctx.client, id, "Oracle")).content);
2353
+ const o = OracleObject.parse((await fetchObject(this.ctx.client, id, "Oracle")).content);
1991
2354
  return {
1992
2355
  id,
1993
2356
  nonce: o.nonce,
@@ -1997,18 +2360,18 @@ var Oracle = class {
1997
2360
  }
1998
2361
  /** Provider objects the v2 views take for this oracle. */
1999
2362
  providerArgs(o) {
2000
- const cfg = this.v2;
2363
+ const cfg = this.providers;
2001
2364
  const adapter = [o.main, o.guard].find((s) => s?.provider === "redstone")?.adapterId ?? cfg.redstoneAdapter;
2002
2365
  const pythFeed = [o.main, o.guard].find((s) => s?.provider === "pyth")?.feedId;
2003
2366
  const byFeed = Object.fromEntries(Object.entries(cfg.pythPriceInfoObjects).map(([k, v]) => [k.toLowerCase(), v]));
2004
2367
  const pythInfo = pythFeed ? byFeed[pythFeed.toLowerCase()] : Object.values(cfg.pythPriceInfoObjects)[0];
2005
- if (!pythInfo) throw new Error(`no Pyth PriceInfoObject configured for feed ${pythFeed ?? "(any)"} (config.oracleV2.pythPriceInfoObjects)`);
2368
+ if (!pythInfo) throw new Error(`no Pyth PriceInfoObject configured for feed ${pythFeed ?? "(any)"} (config.oracle.pythPriceInfoObjects)`);
2006
2369
  return {
2007
2370
  adapter: normalizeSuiObjectId(adapter),
2008
2371
  pythInfo: normalizeSuiObjectId(pythInfo)
2009
2372
  };
2010
2373
  }
2011
- async quoteV2(o, fn) {
2374
+ async quote(o, fn) {
2012
2375
  const pkg = this.ctx.config.packages.oracle.packageId;
2013
2376
  const { adapter, pythInfo } = this.providerArgs(o);
2014
2377
  const res = await simulateView(this.ctx.client, (tx) => {
@@ -2022,7 +2385,7 @@ var Oracle = class {
2022
2385
  ]
2023
2386
  });
2024
2387
  });
2025
- const q = PriceQuoteV2.parse(res[0][0]);
2388
+ const q = PriceQuote.parse(res[0][0]);
2026
2389
  const r = fn === "get_exchange_rate_operate" ? q.rate_operate : q.rate_liquidate;
2027
2390
  if (r == null) throw new Error(`${fn} returned no rate`);
2028
2391
  return BigInt(r);
@@ -2030,29 +2393,12 @@ var Oracle = class {
2030
2393
  /** Price exactly as vaults see it. v2: operate and liquidate simulated separately (one can abort alone). */
2031
2394
  async getOraclePrice(oracleId) {
2032
2395
  const id = normalizeSuiObjectId(oracleId);
2033
- if (this.ctx.config.oracleVariant === "admin") {
2034
- const pkg = this.ctx.config.packages.oracle.packageId;
2035
- const res = await simulateView(this.ctx.client, (tx) => {
2036
- tx.moveCall({
2037
- target: `${pkg}::view::quote`,
2038
- arguments: [tx.object(id)]
2039
- });
2040
- });
2041
- const q = PriceQuote.parse(res[0][0]);
2042
- return {
2043
- oracle: q.oracle_id,
2044
- operate: BigInt(q.rate_operate),
2045
- liquidate: BigInt(q.rate_liquidate),
2046
- source: "admin"
2047
- };
2048
- }
2049
- const o = await this.getOracleV2(id);
2050
- const [op, liq] = await Promise.allSettled([this.quoteV2(o, "get_exchange_rate_operate"), this.quoteV2(o, "get_exchange_rate_liquidate")]);
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")]);
2051
2398
  return {
2052
2399
  oracle: id,
2053
2400
  operate: op.status === "fulfilled" ? op.value : null,
2054
2401
  liquidate: liq.status === "fulfilled" ? liq.value : null,
2055
- source: "v2",
2056
2402
  ...op.status === "rejected" ? { operateError: abortText(op.reason) } : {},
2057
2403
  ...liq.status === "rejected" ? { liquidateError: abortText(liq.reason) } : {}
2058
2404
  };
@@ -2063,8 +2409,8 @@ var Oracle = class {
2063
2409
  * Operate price = min over routes; liquidate = min over routes usable for liquidate.
2064
2410
  */
2065
2411
  async getOracleRoutes(oracleId) {
2066
- const o = await this.getOracleV2(oracleId);
2067
- const cfg = this.v2;
2412
+ const o = await this.getOracle(oracleId);
2413
+ const cfg = this.providers;
2068
2414
  const { pythInfo } = this.providerArgs(o);
2069
2415
  const redstone = [o.main, o.guard].filter((s) => s?.provider === "redstone");
2070
2416
  const res = await simulateView(this.ctx.client, (tx) => {
@@ -2089,7 +2435,7 @@ var Oracle = class {
2089
2435
  const pyth = o.main.provider === "pyth" || o.guard?.provider === "pyth" ? PythPriceInfoObject.parse((await fetchObject(this.ctx.client, pythInfo, "PriceInfoObject")).content) : null;
2090
2436
  let rsIdx = 0;
2091
2437
  const route = (s) => {
2092
- const L = ORACLE_V2_LIMITS;
2438
+ const L = ORACLE_LIMITS;
2093
2439
  if (s.provider === "redstone") {
2094
2440
  const k = 1 + 2 * rsIdx++;
2095
2441
  const value = BigInt(bcs.u256().parse(res[k][0]));
@@ -2632,7 +2978,6 @@ var Vault = class {
2632
2978
  liquidationPenalty: r.risk.liquidation_penalty,
2633
2979
  borrowFee: r.risk.borrow_fee,
2634
2980
  oracle: r.oracle,
2635
- rebalancer: "",
2636
2981
  liquidityProgram: r.liquidity_program,
2637
2982
  oracleProgram: this.ctx.config.packages.oracle.packageId,
2638
2983
  supplyToken: v.collateralType,
@@ -2729,10 +3074,10 @@ var Vault = class {
2729
3074
  return this.stateFromRecord(v, branch ?? null, counts ?? null);
2730
3075
  }
2731
3076
  /** `user::get_exchange_prices`: (liqSupply, liqBorrow, vaultSupply, vaultBorrow) accrued to `now`. */
2732
- exchangePricesFromRecords(v, colReserve, debtReserve, now = this.ctx.now()) {
3077
+ exchangePricesFromRecords(v, colReserve, debtReserve, now) {
2733
3078
  const r = v.raw;
2734
- const liqSupply = calculateExchangePrice(colReserve.snapshot, now).supplyExchangePrice;
2735
- const liqBorrow = calculateExchangePrice(debtReserve.snapshot, now).borrowExchangePrice;
3079
+ const liqSupply = calculateExchangePrice(colReserve.snapshot, now, this.ctx.math).supplyExchangePrice;
3080
+ const liqBorrow = calculateExchangePrice(debtReserve.snapshot, now, this.ctx.math).borrowExchangePrice;
2736
3081
  const last = BigInt(r.last_update_timestamp);
2737
3082
  const vp = loadVaultExchangePrices({
2738
3083
  vaultSupplyExPrice: BigInt(r.vault_supply_exchange_price),
@@ -2781,7 +3126,7 @@ var Vault = class {
2781
3126
  };
2782
3127
  }
2783
3128
  async entireDataFor(vaults, opts) {
2784
- const now = this.ctx.now();
3129
+ const nowP = this.ctx.now();
2785
3130
  const tokens = [...new Set(vaults.flatMap((v) => [v.collateralType, v.debtType]))];
2786
3131
  const reserves = new Map((await this.liquidity.loadReserves(tokens)).map((r) => [r.token, r]));
2787
3132
  const pairs = vaults.flatMap((v) => [{
@@ -2797,6 +3142,7 @@ var Vault = class {
2797
3142
  opts.withOracle === false ? Promise.resolve(vaults.map(() => null)) : Promise.all(vaults.map((v) => this.oracle.getOraclePrice(v.raw.oracle).catch(() => null))),
2798
3143
  this.countsOrNull(vaults, opts.positionCounts)
2799
3144
  ]);
3145
+ const now = await nowP;
2800
3146
  return vaults.map((v, i) => {
2801
3147
  const col = reserves.get(v.collateralType);
2802
3148
  const debt = reserves.get(v.debtType);
@@ -2868,7 +3214,6 @@ var Vault = class {
2868
3214
  liquidationPenalty: BigInt(cfg.liquidationPenalty),
2869
3215
  borrowFee: BigInt(cfg.borrowFee),
2870
3216
  oracle: cfg.oracle,
2871
- rebalancer: "",
2872
3217
  ...price?.operate != null ? { oraclePriceOperate: price.operate } : {},
2873
3218
  ...price?.liquidate != null ? { oraclePriceLiquidate: price.liquidate } : {},
2874
3219
  lastUpdateTimestamp: state.lastUpdateTimestamp
@@ -3133,34 +3478,59 @@ const DEFAULT_RPC_URL = "https://fullnode.mainnet.sui.io:443";
3133
3478
  * `client.liquidity`, `client.lending`, `client.vault` (+ `client.oracle`, Sui-only).
3134
3479
  *
3135
3480
  * @example
3136
- * const client = new Client(); // staging deployment, Sui mainnet gRPC
3137
- * const client = new Client("https://my-fullnode:443");
3481
+ * const client = new Client(undefined, { network: "staging" }); // staging deployment, Sui mainnet gRPC
3482
+ * const client = new Client("https://my-fullnode:443", { network: prodConfig });
3138
3483
  * const client = new Client(existingSuiGrpcClient, { network: "staging" });
3139
3484
  * const vault = await client.vault.getVaultEntireData(1);
3140
3485
  */
3141
- var Client = class {
3486
+ var Client = class Client {
3142
3487
  liquidity;
3143
3488
  lending;
3144
3489
  vault;
3145
3490
  oracle;
3146
3491
  context;
3147
- constructor(rpc, opts = {}) {
3148
- const config = resolveConfig(opts.network ?? DEFAULT_NETWORK);
3492
+ constructor(rpc, opts) {
3493
+ const config = resolveConfig(opts?.network);
3149
3494
  this.context = makeContext(createFailoverClient(rpc === void 0 ? config.rpcUrls ?? [DEFAULT_RPC_URL] : typeof rpc === "string" ? [rpc] : Array.isArray(rpc) ? rpc : [rpc], {
3150
3495
  network: config.network,
3151
- timeoutMs: opts.timeoutMs
3496
+ timeoutMs: opts.timeoutMs,
3497
+ ...opts.retry
3152
3498
  }), config, createDerive(config), opts.now, opts.discovery ?? "state");
3153
3499
  this.liquidity = new Liquidity(this.context);
3154
3500
  this.oracle = new Oracle(this.context);
3155
3501
  this.lending = new Lending(this.context, this.liquidity);
3156
3502
  this.vault = new Vault(this.context, this.liquidity, this.oracle);
3157
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
+ }
3158
3528
  };
3159
3529
  /**
3160
3530
  * `$extend` form, same pattern as the write SDK's `fluid()`:
3161
3531
  * `const c = new SuiGrpcClient({...}).$extend(fluidRead()); c.fluidRead.vault...`
3162
3532
  */
3163
- function fluidRead(opts = {}) {
3533
+ function fluidRead(opts) {
3164
3534
  return {
3165
3535
  name: "fluidRead",
3166
3536
  register: (client) => new Client(client, opts)
@@ -3168,4 +3538,4 @@ function fluidRead(opts = {}) {
3168
3538
  }
3169
3539
 
3170
3540
  //#endregion
3171
- export { AllEndpointsFailedError, CONFIGS, Client, DEFAULT_NETWORK, DEFAULT_RPC_URL, EXCHANGE_PRICES_PRECISION, EXCHANGE_PRICE_SCALE_FACTOR, FOUR_DECIMALS, Lending, Liquidity, MAINNET, MAX_REWARDS_RATE, NeedsDiscoveryError, ORACLE_RATE_PRECISION, ORACLE_V2_LIMITS, ObjectNotFoundError, Oracle, POSITION_INDEX_FIX, PositionIndex, RATE_OUTPUT_DECIMALS, RETURN_PERCENT_PRECISION, SECONDS_PER_YEAR, STAGING, STAGING_LEGACY, Vault, ViewCallError, bcs_exports as bcsTypes, clientForEndpoint, configFromDeployment, createDerive, createFailoverClient, divCeil, fluidRead, lending_exports as lendingMath, liquidity_exports as liquidityMath, max, min, mulDiv, mulDivCeil, nowSeconds, resolveConfig, scaleAndInvert, simulateView, tick_exports as tickMath, vault_exports as vaultMath };
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 };