@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.
- package/README.md +147 -21
- package/dist/index.d.mts +1229 -998
- package/dist/index.mjs +688 -318
- 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
|
-
|
|
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
|
-
|
|
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",
|
|
108
|
-
* The record has no RedStone package or Pyth feed map, so `
|
|
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
|
-
|
|
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
|
-
/**
|
|
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,
|
|
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
|
|
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 ??
|
|
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`
|
|
603
|
-
const
|
|
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
|
-
/**
|
|
610
|
-
const
|
|
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
|
-
|
|
840
|
-
|
|
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
|
-
/**
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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
|
-
/**
|
|
1066
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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) =>
|
|
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
|
|
1253
|
-
if (!lending) throw new Error(`no LendingState for ${token}`);
|
|
1461
|
+
const { pools, now } = await this.loadPools([token]);
|
|
1254
1462
|
return {
|
|
1255
|
-
|
|
1256
|
-
|
|
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.
|
|
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]
|
|
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
|
|
1286
|
-
|
|
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
|
|
1292
|
-
const
|
|
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
|
|
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
|
|
1318
|
-
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,
|
|
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
|
|
1328
|
-
symbol: f
|
|
1329
|
-
decimals: f
|
|
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
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
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
|
|
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 {
|
|
1349
|
-
const
|
|
1350
|
-
|
|
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.
|
|
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:
|
|
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.
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
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
|
-
/**
|
|
1553
|
-
|
|
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` (
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 >
|
|
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
|
|
1972
|
-
const c = this.ctx.config.
|
|
1973
|
-
if (
|
|
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
|
|
2351
|
+
async getOracle(oracleId) {
|
|
1989
2352
|
const id = normalizeSuiObjectId(oracleId);
|
|
1990
|
-
const o =
|
|
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.
|
|
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.
|
|
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
|
|
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 =
|
|
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
|
-
|
|
2034
|
-
|
|
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.
|
|
2067
|
-
const cfg = this.
|
|
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 =
|
|
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
|
|
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
|
|
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();
|
|
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
|
|
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,
|
|
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 };
|