@instadapp/fluid-sui-read-sdk 0.1.0 → 0.3.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 +148 -21
- package/dist/index.d.mts +1229 -995
- package/dist/index.mjs +719 -311
- 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" },
|
|
@@ -64,48 +66,55 @@ const STAGING = {
|
|
|
64
66
|
rpcUrls: ["https://fullnode.mainnet.sui.io:443"],
|
|
65
67
|
graphqlUrl: "https://graphql.mainnet.sui.io/graphql"
|
|
66
68
|
};
|
|
69
|
+
const USDC = "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC";
|
|
70
|
+
const HBTC = "0xd30fb3f8914efdbeb7b8e85fae1d65dc584b44a1281f66737b92bb9f03a59d06::btc::BTC";
|
|
67
71
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
72
|
+
* PRODUCTION deployment on Sui mainnet (2026-11-02), same ids as fluid-packages-sui
|
|
73
|
+
* deployments/mainnet.json + mainnet-markets.json and the write SDK. Day one: fUSDC and
|
|
74
|
+
* vault 1 = VaultMarket<hBTC, USDC> 0x1aa7…a624 on oracle nonce 1 0x6f74…3784 (RedStone BTC
|
|
75
|
+
* main + Pyth BTC/USD guard). Audited maths. Network "mainnet".
|
|
71
76
|
*/
|
|
72
|
-
const
|
|
77
|
+
const PROD = {
|
|
73
78
|
network: "mainnet",
|
|
74
79
|
packages: {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
library: same("0xdb1caa95193a0bda87e56a9aaf9878672ca8bea7039447e5c789a572a0621969"),
|
|
81
|
+
liquidity: same("0x03dbce0f4cbd6325ca81414cc63a82f35e32bb941610b9120fab0997bc70daa3"),
|
|
82
|
+
lending: same("0xc63078660d7e3ed001e565b8bd7e2e6eb7313ef809051165cda901621e1db842"),
|
|
83
|
+
lendingRewards: same("0x8269897ee68730b4bde175c883719f1fa108509113c2c177ad1b456bb3782ba2"),
|
|
84
|
+
oracle: same("0x2caaa38d2c1c424d8ec79750c110d2211f2e06d66a9a261204f721175da15bcc"),
|
|
85
|
+
vaults: same("0x937156ee38d5c0a9e061dedd549867aeee88c478fad7f3213b66ca4f95906743")
|
|
78
86
|
},
|
|
79
87
|
objects: {
|
|
80
|
-
liquidityRegistry: "
|
|
81
|
-
lendingRegistry: "
|
|
82
|
-
oracleRegistry: "
|
|
83
|
-
vaultRegistry: "
|
|
84
|
-
liquidityAuth: "
|
|
85
|
-
lendingAuth: "
|
|
86
|
-
|
|
88
|
+
liquidityRegistry: "0xddeb96aeff09698e7fdcdefb6923642d0e2da1febcaf656371cf7e55bb339a0a",
|
|
89
|
+
lendingRegistry: "0x98307ab7e769b60257f4b12c20e0976fb57bbe898409b568a3044cf94b3f5647",
|
|
90
|
+
oracleRegistry: "0x359f818813a10a4bc3862536dafa6bc2bef0bf3597b090539fd05a89cf8fb368",
|
|
91
|
+
vaultRegistry: "0xe598cc749e0907d6454fdf779611b2efee7fdb2d756ff2706385bfed493bc608",
|
|
92
|
+
liquidityAuth: "0x1fc3d11285cd7b7ed44bf92f5df3a856c1a45699f59168fd878dfa4eae8e459f",
|
|
93
|
+
lendingAuth: "0x6e8ca10c5e6f49984106e38a3b6ef49c05a5a5a01f77b04c754e4d6c1e0ec8f7",
|
|
94
|
+
rewardsAuth: "0xaaf5efdfa300286a719ca8131bcc5f70bc1d3b1fb18a2886f4da04072b5b3bd1",
|
|
95
|
+
oracleAuth: "0xb498982435b0fdc18126d5febf1327af5dcd02b2e3377b6e5c37ab3eddf08ca8",
|
|
96
|
+
vaultAdmin: "0x2b3d9ccfb6e1da6ff8acfcb9c56c898a06ae7f3d6f37e3e64e7df34a60ccd887"
|
|
87
97
|
},
|
|
88
|
-
|
|
89
|
-
|
|
98
|
+
oracle: {
|
|
99
|
+
redstoneAdapter: STAGING.oracle.redstoneAdapter,
|
|
100
|
+
redstonePackage: STAGING.oracle.redstonePackage,
|
|
101
|
+
pythPriceInfoObjects: { "0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43": "0xe64832d1b4c9a75139a7313aee69da891297d8397fda2863b5bdcc4af0b79758" },
|
|
102
|
+
pythStateId: STAGING.oracle.pythStateId,
|
|
103
|
+
wormholeStateId: STAGING.oracle.wormholeStateId
|
|
104
|
+
},
|
|
105
|
+
tokens: [USDC, HBTC],
|
|
90
106
|
rpcUrls: ["https://fullnode.mainnet.sui.io:443"],
|
|
91
107
|
graphqlUrl: "https://graphql.mainnet.sui.io/graphql"
|
|
92
108
|
};
|
|
93
|
-
|
|
94
|
-
* @deprecated Misleading name kept for compatibility: this is the STAGING deployment
|
|
95
|
-
* (`STAGING`), not production. Use `STAGING` / network "staging".
|
|
96
|
-
*/
|
|
97
|
-
const MAINNET = STAGING;
|
|
98
|
-
/** Default built-in network. Points at the current staging deployment until production ships. */
|
|
99
|
-
const DEFAULT_NETWORK = "staging";
|
|
109
|
+
const MAINNET = PROD;
|
|
100
110
|
const CONFIGS = {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
"staging-legacy": STAGING_LEGACY
|
|
111
|
+
mainnet: PROD,
|
|
112
|
+
staging: STAGING
|
|
104
113
|
};
|
|
105
114
|
/**
|
|
106
115
|
* 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 `
|
|
116
|
+
* `configFromDeployment(JSON.parse(readFileSync(path, "utf8")), { network: "mainnet", oracle })`.
|
|
117
|
+
* The record has no RedStone package or Pyth feed map, so `oracle` comes from the caller.
|
|
109
118
|
* Throws once, listing every required field the record is missing.
|
|
110
119
|
*/
|
|
111
120
|
function configFromDeployment(rec, opts) {
|
|
@@ -145,16 +154,17 @@ function configFromDeployment(rec, opts) {
|
|
|
145
154
|
oracleAuth: obj("oracle", "auth", false),
|
|
146
155
|
vaultAdmin: obj("vaults", "admin", false)
|
|
147
156
|
},
|
|
148
|
-
|
|
149
|
-
oracleV2: opts.oracleV2,
|
|
157
|
+
oracle: opts.oracle,
|
|
150
158
|
tokens: opts.tokens ?? TOKENS,
|
|
151
159
|
rpcUrls: opts.rpcUrls,
|
|
152
|
-
graphqlUrl: opts.graphqlUrl
|
|
160
|
+
graphqlUrl: opts.graphqlUrl,
|
|
161
|
+
mathVariant: opts.mathVariant ?? (PRE_AUDIT_LIQUIDITY.has(normalizeSuiObjectId(ids("liquidity").packageId)) ? "pre-audit" : "audited")
|
|
153
162
|
};
|
|
154
163
|
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
164
|
return config;
|
|
156
165
|
}
|
|
157
166
|
function resolveConfig(networkOrConfig) {
|
|
167
|
+
if (networkOrConfig === void 0 || networkOrConfig === null || networkOrConfig === "") throw new Error("Fluid network required: pass { network: \"mainnet\" } (production), { network: \"staging\" } (the staging-on-mainnet deployment) or a config from configFromDeployment(...). There is no default.");
|
|
158
168
|
const cfg = typeof networkOrConfig === "string" ? CONFIGS[networkOrConfig] : networkOrConfig;
|
|
159
169
|
if (!cfg) throw new Error(`no built-in Fluid config for network "${String(networkOrConfig)}"`);
|
|
160
170
|
const n = (p) => ({
|
|
@@ -187,7 +197,18 @@ const RATE_OUTPUT_DECIMALS = 100000000000000000n;
|
|
|
187
197
|
const EXCHANGE_PRICE_SCALE_FACTOR = 1000000000000000000n;
|
|
188
198
|
/** Lending return precision (1e14 = 1e12 * 100). */
|
|
189
199
|
const RETURN_PERCENT_PRECISION = 100000000000000n;
|
|
190
|
-
/**
|
|
200
|
+
/**
|
|
201
|
+
* The two raw on-chain rate scales differ, which is why every rate FIELD the SDK returns is bps
|
|
202
|
+
* (1e4 = 100% per year, floored):
|
|
203
|
+
* - liquidity supply rate (`reserve::calculate_exchange_prices`): 1e12 = 100% -> SUPPLY_RATE_PRECISION
|
|
204
|
+
* - lending rewards rate (`rewards`, `lending_calcs`): 1e14 = 100% -> RETURN_PERCENT_PRECISION
|
|
205
|
+
*/
|
|
206
|
+
const SUPPLY_RATE_PRECISION = 1000000000000n;
|
|
207
|
+
/** Raw liquidity supply rate (1e12 = 100%) -> bps, floored. */
|
|
208
|
+
const supplyRateToBps = (raw) => raw / (SUPPLY_RATE_PRECISION / FOUR_DECIMALS);
|
|
209
|
+
/** Raw lending rewards rate (1e14 = 100%) -> bps, floored. */
|
|
210
|
+
const rewardsRateToBps = (raw) => raw / (RETURN_PERCENT_PRECISION / FOUR_DECIMALS);
|
|
211
|
+
/** Lending max rewards rate: 50% at RETURN_PERCENT_PRECISION (1e14 = 100%). */
|
|
191
212
|
const MAX_REWARDS_RATE = 50000000000000n;
|
|
192
213
|
function mulDiv(a, b, c) {
|
|
193
214
|
if (c === 0n) throw new RangeError("mulDiv: division by zero");
|
|
@@ -206,8 +227,94 @@ const min = (a, b) => a < b ? a : b;
|
|
|
206
227
|
/** Current unix time in seconds (the default `now` for time-dependent reads). */
|
|
207
228
|
const nowSeconds = () => BigInt(Math.floor(Date.now() / 1e3));
|
|
208
229
|
|
|
230
|
+
//#endregion
|
|
231
|
+
//#region src/objects.ts
|
|
232
|
+
/** `getObjects` accepts at most 50 ids per call. */
|
|
233
|
+
const BATCH = 50;
|
|
234
|
+
/** Batched fetch. Missing/deleted ids come back as `null` in input order; other errors throw. */
|
|
235
|
+
async function fetchObjects(client, ids) {
|
|
236
|
+
const out = [];
|
|
237
|
+
const chunks = [];
|
|
238
|
+
for (let i = 0; i < ids.length; i += BATCH) chunks.push(ids.slice(i, i + BATCH));
|
|
239
|
+
const pages = await Promise.all(chunks.map((c) => client.core.getObjects({
|
|
240
|
+
objectIds: c,
|
|
241
|
+
include: { content: true }
|
|
242
|
+
})));
|
|
243
|
+
for (const { objects } of pages) for (const o of objects) {
|
|
244
|
+
if (o instanceof Error) {
|
|
245
|
+
if (o instanceof ObjectError && (o.reason === "notFound" || o.reason === "deleted")) {
|
|
246
|
+
out.push(null);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
throw o;
|
|
250
|
+
}
|
|
251
|
+
out.push({
|
|
252
|
+
objectId: o.objectId,
|
|
253
|
+
type: o.type,
|
|
254
|
+
content: o.content,
|
|
255
|
+
owner: o.owner
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
var ObjectNotFoundError = class extends Error {
|
|
261
|
+
constructor(objectId, what) {
|
|
262
|
+
super(`${what} not found: ${objectId}`);
|
|
263
|
+
this.objectId = objectId;
|
|
264
|
+
this.what = what;
|
|
265
|
+
this.name = "ObjectNotFoundError";
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
async function fetchObject(client, id, what) {
|
|
269
|
+
const [o] = await fetchObjects(client, [id]);
|
|
270
|
+
if (!o) throw new ObjectNotFoundError(id, what);
|
|
271
|
+
return o;
|
|
272
|
+
}
|
|
273
|
+
/** Every dynamic-field id under a parent (Table), paginated. */
|
|
274
|
+
async function listDynamicFieldIds(client, parentId) {
|
|
275
|
+
const ids = [];
|
|
276
|
+
let cursor = null;
|
|
277
|
+
do {
|
|
278
|
+
const page = await client.core.listDynamicFields({
|
|
279
|
+
parentId,
|
|
280
|
+
cursor,
|
|
281
|
+
limit: 50
|
|
282
|
+
});
|
|
283
|
+
for (const f of page.dynamicFields) ids.push(f.fieldId);
|
|
284
|
+
cursor = page.hasNextPage ? page.cursor : null;
|
|
285
|
+
} while (cursor !== null);
|
|
286
|
+
return ids;
|
|
287
|
+
}
|
|
288
|
+
/** Owner address (or parent object id) from a core-API owner union. */
|
|
289
|
+
function ownerAddress(owner) {
|
|
290
|
+
if (!owner || typeof owner !== "object") return null;
|
|
291
|
+
const o = owner;
|
|
292
|
+
for (const k of [
|
|
293
|
+
"AddressOwner",
|
|
294
|
+
"ObjectOwner",
|
|
295
|
+
"ConsensusAddressOwner"
|
|
296
|
+
]) {
|
|
297
|
+
const v = o[k];
|
|
298
|
+
if (typeof v === "string") return v;
|
|
299
|
+
if (v && typeof v === "object" && typeof v.owner === "string") return v.owner;
|
|
300
|
+
}
|
|
301
|
+
if (o.$kind === "Shared") return null;
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
209
305
|
//#endregion
|
|
210
306
|
//#region src/context.ts
|
|
307
|
+
/** The shared `0x2::clock::Clock` object. */
|
|
308
|
+
const CLOCK_ID = "0x0000000000000000000000000000000000000000000000000000000000000006";
|
|
309
|
+
/**
|
|
310
|
+
* Chain time in seconds from the on-chain `Clock` (0x6) as the fullnode last saw it, i.e. the
|
|
311
|
+
* timestamp of its latest executed checkpoint. Same value a Move call would read via
|
|
312
|
+
* `units::from_clock(clock)` (ms / 1000, floored).
|
|
313
|
+
*/
|
|
314
|
+
async function chainNowSeconds(client) {
|
|
315
|
+
const o = await fetchObject(client, CLOCK_ID, "Clock");
|
|
316
|
+
return new DataView(o.content.buffer, o.content.byteOffset + 32, 8).getBigUint64(0, true) / 1000n;
|
|
317
|
+
}
|
|
211
318
|
const RANK = {
|
|
212
319
|
state: 0,
|
|
213
320
|
fullnode: 1,
|
|
@@ -222,19 +329,42 @@ var NeedsDiscoveryError = class extends Error {
|
|
|
222
329
|
this.name = "NeedsDiscoveryError";
|
|
223
330
|
}
|
|
224
331
|
};
|
|
225
|
-
function makeContext(client, config, derive,
|
|
332
|
+
function makeContext(client, config, derive, clock = "chain", discovery = "state") {
|
|
333
|
+
const now = clock === "chain" ? () => chainNowSeconds(client) : clock === "local" ? async () => nowSeconds() : async () => clock();
|
|
334
|
+
const metaCache = /* @__PURE__ */ new Map();
|
|
335
|
+
const coinMetadata = (coinType) => {
|
|
336
|
+
let p = metaCache.get(coinType);
|
|
337
|
+
if (!p) {
|
|
338
|
+
p = client.core.getCoinMetadata({ coinType }).then(({ coinMetadata: m }) => m ? {
|
|
339
|
+
name: m.name,
|
|
340
|
+
symbol: m.symbol,
|
|
341
|
+
decimals: m.decimals
|
|
342
|
+
} : null);
|
|
343
|
+
p.catch(() => metaCache.delete(coinType));
|
|
344
|
+
metaCache.set(coinType, p);
|
|
345
|
+
}
|
|
346
|
+
return p;
|
|
347
|
+
};
|
|
226
348
|
return {
|
|
227
349
|
client,
|
|
228
350
|
config,
|
|
229
351
|
derive,
|
|
230
|
-
now
|
|
352
|
+
now,
|
|
231
353
|
discovery,
|
|
232
|
-
allows: (l) => RANK[discovery] >= RANK[l]
|
|
354
|
+
allows: (l) => RANK[discovery] >= RANK[l],
|
|
355
|
+
math: config.mathVariant ?? "audited",
|
|
356
|
+
coinMetadata
|
|
233
357
|
};
|
|
234
358
|
}
|
|
235
359
|
|
|
236
360
|
//#endregion
|
|
237
361
|
//#region src/transport.ts
|
|
362
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
363
|
+
/** Wait before pass `pass` (1-based, pass >= 2): exponential, capped, +/-25% jitter. */
|
|
364
|
+
function backoffDelay(pass, base, cap) {
|
|
365
|
+
const d = Math.min(cap, base * 2 ** (pass - 2));
|
|
366
|
+
return Math.round(d * (.75 + Math.random() * .5));
|
|
367
|
+
}
|
|
238
368
|
var AllEndpointsFailedError = class extends Error {
|
|
239
369
|
constructor(method, errors) {
|
|
240
370
|
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 +398,10 @@ function createFailoverClient(endpoints, opts) {
|
|
|
268
398
|
if (endpoints.length === 0) throw new Error("createFailoverClient: at least one endpoint required");
|
|
269
399
|
const labels = endpoints.map((e, i) => typeof e === "string" ? e : `client#${i}`);
|
|
270
400
|
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
401
|
const timeoutMs = opts.timeoutMs ?? 15e3;
|
|
276
|
-
const rounds = opts.rounds ??
|
|
402
|
+
const rounds = opts.rounds ?? 3;
|
|
403
|
+
const backoffMs = opts.backoffMs ?? 250;
|
|
404
|
+
const maxBackoffMs = opts.maxBackoffMs ?? 4e3;
|
|
277
405
|
let preferred = 0;
|
|
278
406
|
const core = new Proxy({}, { get(_t, prop) {
|
|
279
407
|
const first = clients[preferred].core;
|
|
@@ -286,6 +414,7 @@ function createFailoverClient(endpoints, opts) {
|
|
|
286
414
|
const errors = [];
|
|
287
415
|
for (let attempt = 0; attempt < clients.length * rounds; attempt++) {
|
|
288
416
|
const i = (preferred + attempt) % clients.length;
|
|
417
|
+
if (attempt > 0 && attempt % clients.length === 0) await sleep(backoffDelay(attempt / clients.length + 1, backoffMs, maxBackoffMs));
|
|
289
418
|
const c = clients[i].core;
|
|
290
419
|
try {
|
|
291
420
|
const out = await withTimeout(attempt === 0 ? res : c[prop].apply(c, args), timeoutMs, `${labels[i]} core.${String(prop)}`);
|
|
@@ -312,6 +441,151 @@ function createFailoverClient(endpoints, opts) {
|
|
|
312
441
|
} });
|
|
313
442
|
}
|
|
314
443
|
|
|
444
|
+
//#endregion
|
|
445
|
+
//#region src/graphql.ts
|
|
446
|
+
/** Minimal Sui GraphQL client (fetch), with the same retry + backoff policy as the RPC failover. */
|
|
447
|
+
var GraphQLError = class extends Error {
|
|
448
|
+
constructor(message, errors) {
|
|
449
|
+
super(message);
|
|
450
|
+
this.errors = errors;
|
|
451
|
+
this.name = "GraphQLError";
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
/**
|
|
455
|
+
* POST one query. Network errors, timeouts, HTTP 429/5xx are retried with backoff; a GraphQL
|
|
456
|
+
* `errors` payload (bad query, unknown object) is NOT retried.
|
|
457
|
+
*/
|
|
458
|
+
async function graphqlQuery(url, query, variables = {}, opts = {}) {
|
|
459
|
+
const rounds = opts.rounds ?? 3;
|
|
460
|
+
const timeoutMs = opts.timeoutMs ?? 15e3;
|
|
461
|
+
let last;
|
|
462
|
+
for (let pass = 1; pass <= rounds; pass++) {
|
|
463
|
+
if (pass > 1) await new Promise((r) => setTimeout(r, backoffDelay(pass, opts.backoffMs ?? 250, opts.maxBackoffMs ?? 4e3)));
|
|
464
|
+
let res;
|
|
465
|
+
try {
|
|
466
|
+
res = await fetch(url, {
|
|
467
|
+
method: "POST",
|
|
468
|
+
headers: { "content-type": "application/json" },
|
|
469
|
+
body: JSON.stringify({
|
|
470
|
+
query,
|
|
471
|
+
variables
|
|
472
|
+
}),
|
|
473
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
474
|
+
});
|
|
475
|
+
} catch (e) {
|
|
476
|
+
last = e;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (res.status === 429 || res.status >= 500) {
|
|
480
|
+
last = new GraphQLError(`GraphQL HTTP ${res.status}`);
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
const body = await res.json();
|
|
484
|
+
if (body.errors?.length) throw new GraphQLError(`GraphQL: ${body.errors.map((e) => e.message).join("; ")}`, body.errors);
|
|
485
|
+
if (!body.data) throw new GraphQLError(`GraphQL HTTP ${res.status}: no data`);
|
|
486
|
+
return body.data;
|
|
487
|
+
}
|
|
488
|
+
throw last instanceof Error ? last : new GraphQLError(String(last));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
//#endregion
|
|
492
|
+
//#region src/checkpoint.ts
|
|
493
|
+
var CheckpointReadError = class extends Error {
|
|
494
|
+
constructor(method, checkpoint) {
|
|
495
|
+
super(`core.${method} is not available for historical reads (checkpoint ${checkpoint}); only object state is`);
|
|
496
|
+
this.name = "CheckpointReadError";
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
/** GraphQL `multiGetObjects` takes at most 50 keys per query. */
|
|
500
|
+
const KEYS_PER_QUERY = 50;
|
|
501
|
+
const OBJECT_FIELDS = `address version digest
|
|
502
|
+
owner { __typename ... on AddressOwner { address { address } } ... on ObjectOwner { address { address } }
|
|
503
|
+
... on Shared { initialSharedVersion } ... on ConsensusAddressOwner { startVersion address { address } } }
|
|
504
|
+
asMoveObject { contents { type { repr } bcs } }`;
|
|
505
|
+
function coreOwner(o) {
|
|
506
|
+
switch (o?.__typename) {
|
|
507
|
+
case "AddressOwner": return {
|
|
508
|
+
$kind: "AddressOwner",
|
|
509
|
+
AddressOwner: o.address.address
|
|
510
|
+
};
|
|
511
|
+
case "ObjectOwner": return {
|
|
512
|
+
$kind: "ObjectOwner",
|
|
513
|
+
ObjectOwner: o.address.address
|
|
514
|
+
};
|
|
515
|
+
case "Shared": return {
|
|
516
|
+
$kind: "Shared",
|
|
517
|
+
Shared: { initialSharedVersion: String(o.initialSharedVersion) }
|
|
518
|
+
};
|
|
519
|
+
case "Immutable": return {
|
|
520
|
+
$kind: "Immutable",
|
|
521
|
+
Immutable: true
|
|
522
|
+
};
|
|
523
|
+
case "ConsensusAddressOwner": return {
|
|
524
|
+
$kind: "ConsensusAddressOwner",
|
|
525
|
+
ConsensusAddressOwner: {
|
|
526
|
+
startVersion: String(o.startVersion),
|
|
527
|
+
owner: o.address.address
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
default: return { $kind: "Unknown" };
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
/** Timestamp of `checkpoint` in unix seconds (floored, as `units::from_clock` reads it). */
|
|
534
|
+
async function checkpointTimestampSeconds(url, checkpoint, opts) {
|
|
535
|
+
const d = await graphqlQuery(url, `query($c: UInt53!) { checkpoint(sequenceNumber: $c) { timestamp } }`, { c: Number(checkpoint) }, opts);
|
|
536
|
+
if (!d.checkpoint) throw new Error(`checkpoint ${checkpoint} not found on ${url}`);
|
|
537
|
+
return BigInt(Math.floor(Date.parse(d.checkpoint.timestamp) / 1e3));
|
|
538
|
+
}
|
|
539
|
+
/** Latest checkpoint the GraphQL service has indexed. */
|
|
540
|
+
async function latestCheckpoint(url, opts) {
|
|
541
|
+
const d = await graphqlQuery(url, `{ checkpoint { sequenceNumber } }`, {}, opts);
|
|
542
|
+
return BigInt(d.checkpoint.sequenceNumber);
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* A core client pinned to `checkpoint`: `getObjects` via GraphQL at that checkpoint,
|
|
546
|
+
* `getCoinMetadata` from `live` (immutable data), every other core read throws.
|
|
547
|
+
*/
|
|
548
|
+
function checkpointClient(url, checkpoint, live, opts) {
|
|
549
|
+
const getObjects = async ({ objectIds }) => {
|
|
550
|
+
const objects = [];
|
|
551
|
+
for (let i = 0; i < objectIds.length; i += KEYS_PER_QUERY) {
|
|
552
|
+
const ids = objectIds.slice(i, i + KEYS_PER_QUERY);
|
|
553
|
+
(await graphqlQuery(url, `query($k: [ObjectKey!]!) { multiGetObjects(keys: $k) { ${OBJECT_FIELDS} } }`, { k: ids.map((address) => ({
|
|
554
|
+
address,
|
|
555
|
+
atCheckpoint: Number(checkpoint)
|
|
556
|
+
})) }, opts)).multiGetObjects.forEach((o, j) => {
|
|
557
|
+
if (!o?.asMoveObject) {
|
|
558
|
+
objects.push(new ObjectError("notFound", `object ${ids[j]} not found at checkpoint ${checkpoint}`, {
|
|
559
|
+
reason: "notFound",
|
|
560
|
+
objectId: ids[j]
|
|
561
|
+
}));
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
objects.push({
|
|
565
|
+
objectId: o.address,
|
|
566
|
+
version: String(o.version),
|
|
567
|
+
digest: o.digest ?? "",
|
|
568
|
+
type: o.asMoveObject.contents.type.repr,
|
|
569
|
+
owner: coreOwner(o.owner),
|
|
570
|
+
content: fromBase64(o.asMoveObject.contents.bcs)
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
return { objects };
|
|
575
|
+
};
|
|
576
|
+
return {
|
|
577
|
+
core: new Proxy({}, { get(_t, prop) {
|
|
578
|
+
if (prop === "getObjects") return getObjects;
|
|
579
|
+
if (prop === "getCoinMetadata") return live.core.getCoinMetadata.bind(live.core);
|
|
580
|
+
if (typeof prop === "symbol" || prop === "then") return void 0;
|
|
581
|
+
return () => {
|
|
582
|
+
throw new CheckpointReadError(String(prop), checkpoint);
|
|
583
|
+
};
|
|
584
|
+
} }),
|
|
585
|
+
checkpoint
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
315
589
|
//#endregion
|
|
316
590
|
//#region src/bcs.ts
|
|
317
591
|
/**
|
|
@@ -321,7 +595,6 @@ function createFailoverClient(endpoints, opts) {
|
|
|
321
595
|
* Single-field wrappers (UID, ID, TimestampS, DurationS, Balance, Supply) encode as their field.
|
|
322
596
|
*/
|
|
323
597
|
var bcs_exports = /* @__PURE__ */ __exportAll({
|
|
324
|
-
AdminOracle: () => AdminOracle,
|
|
325
598
|
Branch: () => Branch,
|
|
326
599
|
BranchStatus: () => BranchStatus,
|
|
327
600
|
CapRecord: () => CapRecord,
|
|
@@ -331,17 +604,16 @@ var bcs_exports = /* @__PURE__ */ __exportAll({
|
|
|
331
604
|
LendingState: () => LendingState,
|
|
332
605
|
LiquidityRegistry: () => LiquidityRegistry,
|
|
333
606
|
MoveTick: () => MoveTick,
|
|
607
|
+
OracleObject: () => OracleObject,
|
|
334
608
|
OracleRegistry: () => OracleRegistry,
|
|
335
609
|
OracleSource: () => OracleSource,
|
|
336
610
|
OracleSourceType: () => OracleSourceType,
|
|
337
|
-
OracleV2: () => OracleV2,
|
|
338
611
|
PauseFlags: () => PauseFlags,
|
|
339
612
|
Position: () => Position,
|
|
340
613
|
PositionIndex: () => PositionIndex$1,
|
|
341
614
|
PositionIndexEntry: () => PositionIndexEntry,
|
|
342
615
|
PositionIndexField: () => PositionIndexField,
|
|
343
616
|
PriceQuote: () => PriceQuote,
|
|
344
|
-
PriceQuoteV2: () => PriceQuoteV2,
|
|
345
617
|
PythPriceInfoObject: () => PythPriceInfoObject,
|
|
346
618
|
RateDataV1Params: () => RateDataV1Params,
|
|
347
619
|
RateDataV2Params: () => RateDataV2Params,
|
|
@@ -574,22 +846,11 @@ const VaultRegistry = bcs.struct("VaultRegistry", {
|
|
|
574
846
|
next_vault_id: U64,
|
|
575
847
|
version: U64
|
|
576
848
|
});
|
|
577
|
-
const AdminOracle = bcs.struct("Oracle", {
|
|
578
|
-
id: Address,
|
|
579
|
-
name: bcs.string(),
|
|
580
|
-
rate: U128,
|
|
581
|
-
version: U64
|
|
582
|
-
});
|
|
583
849
|
const OracleRegistry = bcs.struct("OracleRegistry", {
|
|
584
850
|
id: Address,
|
|
585
851
|
version: U64,
|
|
586
852
|
total_oracles: U64
|
|
587
853
|
});
|
|
588
|
-
const PriceQuote = bcs.struct("PriceQuote", {
|
|
589
|
-
oracle_id: Address,
|
|
590
|
-
rate_liquidate: U128,
|
|
591
|
-
rate_operate: U128
|
|
592
|
-
});
|
|
593
854
|
const OracleSourceType = bcs.enum("SourceType", {
|
|
594
855
|
Redstone: bcs.struct("Redstone", { adapter_id: Address }),
|
|
595
856
|
Pyth: null
|
|
@@ -599,15 +860,15 @@ const OracleSource = bcs.struct("Source", {
|
|
|
599
860
|
invert: bcs.bool(),
|
|
600
861
|
source_type: OracleSourceType
|
|
601
862
|
});
|
|
602
|
-
/** `fluid_oracle::oracle::Oracle`
|
|
603
|
-
const
|
|
863
|
+
/** `fluid_oracle::oracle::Oracle` derived from the registry by nonce. */
|
|
864
|
+
const OracleObject = bcs.struct("Oracle", {
|
|
604
865
|
id: Address,
|
|
605
866
|
nonce: bcs.u16(),
|
|
606
867
|
main: OracleSource,
|
|
607
868
|
guard: bcs.option(OracleSource)
|
|
608
869
|
});
|
|
609
|
-
/**
|
|
610
|
-
const
|
|
870
|
+
/** `view::PriceQuote`: each rate is optional (operate-only / liquidate-only quotes). */
|
|
871
|
+
const PriceQuote = bcs.struct("PriceQuote", {
|
|
611
872
|
oracle_id: Address,
|
|
612
873
|
rate_liquidate: bcs.option(U128),
|
|
613
874
|
rate_operate: bcs.option(U128)
|
|
@@ -816,6 +1077,8 @@ var liquidity_exports = /* @__PURE__ */ __exportAll({
|
|
|
816
1077
|
processOverallTokenData: () => processOverallTokenData,
|
|
817
1078
|
processUserBorrowData: () => processUserBorrowData,
|
|
818
1079
|
processUserSupplyData: () => processUserSupplyData,
|
|
1080
|
+
supplyRateAt: () => supplyRateAt,
|
|
1081
|
+
supplyRateRaw: () => supplyRateRaw,
|
|
819
1082
|
totalAmountsOf: () => totalAmountsOf,
|
|
820
1083
|
totalBorrowNormal: () => totalBorrowNormal,
|
|
821
1084
|
totalSupplyNormal: () => totalSupplyNormal
|
|
@@ -825,8 +1088,31 @@ function withInterestVsFreeRatio(withInterest, interestFree) {
|
|
|
825
1088
|
if (withInterest < interestFree) return withInterest * FOUR_DECIMALS / interestFree;
|
|
826
1089
|
return withInterest > 0n ? FOUR_DECIMALS : 0n;
|
|
827
1090
|
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Supply rate the reserve credits for a period starting from these exchange prices, at
|
|
1093
|
+
* `SUPPLY_RATE_PRECISION` (1e12 = 100% per year): exactly `borrow_rate * ratio_supply_yield * (1e4 - fee)`
|
|
1094
|
+
* from `reserve::calculate_exchange_prices` (interest-free ratios on both sides, fee applied).
|
|
1095
|
+
* `borrowEpForRatio` is the borrow exchange price the contract normalises the borrow ratio with.
|
|
1096
|
+
*/
|
|
1097
|
+
function supplyRateRaw(r, supplyExchangePrice, borrowEpForRatio, variant) {
|
|
1098
|
+
if (r.borrowRate === 0n || r.totalBorrowWithInterest === 0n || r.totalSupplyWithInterest === 0n) return 0n;
|
|
1099
|
+
const supplyWithInterestNormal = divCeil(r.totalSupplyWithInterest * supplyExchangePrice, EXCHANGE_PRICES_PRECISION);
|
|
1100
|
+
const supplyRatio = withInterestVsFreeRatio(supplyWithInterestNormal, r.totalSupplyInterestFree);
|
|
1101
|
+
let ratioSupplyYield;
|
|
1102
|
+
if (supplyWithInterestNormal < r.totalSupplyInterestFree) {
|
|
1103
|
+
if (supplyRatio === 0n) return 0n;
|
|
1104
|
+
const inverted = variant === "audited" ? mulDiv(r.totalSupplyInterestFree, RATE_OUTPUT_DECIMALS, supplyWithInterestNormal) : RATE_OUTPUT_DECIMALS * FOUR_DECIMALS / supplyRatio;
|
|
1105
|
+
ratioSupplyYield = r.lastUtilization * (RATE_OUTPUT_DECIMALS + inverted) / FOUR_DECIMALS;
|
|
1106
|
+
} else ratioSupplyYield = r.lastUtilization * RATE_OUTPUT_DECIMALS * (FOUR_DECIMALS + supplyRatio) / (FOUR_DECIMALS * FOUR_DECIMALS);
|
|
1107
|
+
const borrowWithInterestNormal = r.totalBorrowWithInterest * borrowEpForRatio / EXCHANGE_PRICES_PRECISION;
|
|
1108
|
+
let borrowRatio = withInterestVsFreeRatio(borrowWithInterestNormal, r.totalBorrowInterestFree);
|
|
1109
|
+
borrowRatio = borrowWithInterestNormal < r.totalBorrowInterestFree ? borrowRatio * RATE_OUTPUT_DECIMALS / (FOUR_DECIMALS + borrowRatio) : RATE_OUTPUT_DECIMALS - borrowRatio * RATE_OUTPUT_DECIMALS / (FOUR_DECIMALS + borrowRatio);
|
|
1110
|
+
ratioSupplyYield = mulDiv(ratioSupplyYield, borrowRatio, RATE_OUTPUT_DECIMALS);
|
|
1111
|
+
ratioSupplyYield = ratioSupplyYield * FOUR_DECIMALS / RATE_OUTPUT_DECIMALS;
|
|
1112
|
+
return r.borrowRate * ratioSupplyYield * (FOUR_DECIMALS - r.fee);
|
|
1113
|
+
}
|
|
828
1114
|
/** `reserve::calculate_exchange_prices(now)`. */
|
|
829
|
-
function calculateExchangePrice(r, now) {
|
|
1115
|
+
function calculateExchangePrice(r, now, variant = "audited") {
|
|
830
1116
|
let supplyExchangePrice = r.supplyExchangePrice;
|
|
831
1117
|
let borrowExchangePrice = r.borrowExchangePrice;
|
|
832
1118
|
if (supplyExchangePrice === 0n || borrowExchangePrice === 0n) throw new RangeError("exchange price zero (reserve never initialised)");
|
|
@@ -835,34 +1121,23 @@ function calculateExchangePrice(r, now) {
|
|
|
835
1121
|
supplyExchangePrice,
|
|
836
1122
|
borrowExchangePrice
|
|
837
1123
|
};
|
|
1124
|
+
const borrowBefore = borrowExchangePrice;
|
|
838
1125
|
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);
|
|
1126
|
+
const rate = supplyRateRaw(r, supplyExchangePrice, variant === "audited" ? borrowBefore : borrowExchangePrice, variant);
|
|
1127
|
+
supplyExchangePrice += supplyExchangePrice * rate * elapsed / (SECONDS_PER_YEAR * FOUR_DECIMALS) / (FOUR_DECIMALS * FOUR_DECIMALS);
|
|
861
1128
|
return {
|
|
862
1129
|
supplyExchangePrice,
|
|
863
1130
|
borrowExchangePrice
|
|
864
1131
|
};
|
|
865
1132
|
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Instantaneous liquidity supply rate at `now` (1e12 = 100%): the rate the contract would credit
|
|
1135
|
+
* for the period starting at `now`, i.e. `supplyRateRaw` on both exchange prices accrued to `now`.
|
|
1136
|
+
*/
|
|
1137
|
+
function supplyRateAt(r, now, variant = "audited") {
|
|
1138
|
+
const ep = calculateExchangePrice(r, now, variant);
|
|
1139
|
+
return supplyRateRaw(r, ep.supplyExchangePrice, ep.borrowExchangePrice, variant);
|
|
1140
|
+
}
|
|
866
1141
|
/** `liquidity_calcs::get_total_supply` (floor). */
|
|
867
1142
|
const totalSupplyNormal = (r, supplyEp) => r.totalSupplyInterestFree + r.totalSupplyWithInterest * supplyEp / EXCHANGE_PRICES_PRECISION;
|
|
868
1143
|
/** `liquidity_calcs::get_total_borrow` (floor). */
|
|
@@ -883,15 +1158,13 @@ function totalAmountsOf(r) {
|
|
|
883
1158
|
totalClaimAmount: 0n
|
|
884
1159
|
};
|
|
885
1160
|
}
|
|
886
|
-
/**
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
if (supplyForRate !== 0n) supplyRate = r.borrowRate * (FOUR_DECIMALS - r.fee) * borrowForRate / (supplyForRate * FOUR_DECIMALS);
|
|
894
|
-
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Same shape as the Solana reader's `processOverallTokenData`. `supplyRate` (bps) is the exact
|
|
1163
|
+
* on-chain supply rate at `now` (`supplyRateAt`, floored to bps), not the Solana approximation.
|
|
1164
|
+
*/
|
|
1165
|
+
function processOverallTokenData(r, rateData, now, variant = "audited") {
|
|
1166
|
+
const { supplyExchangePrice, borrowExchangePrice } = calculateExchangePrice(r, now, variant);
|
|
1167
|
+
const supplyRate = supplyRateToBps(supplyRateRaw(r, supplyExchangePrice, borrowExchangePrice, variant));
|
|
895
1168
|
return {
|
|
896
1169
|
rateData,
|
|
897
1170
|
supplyExchangePrice,
|
|
@@ -1035,6 +1308,7 @@ var lending_exports = /* @__PURE__ */ __exportAll({
|
|
|
1035
1308
|
previews: () => previews,
|
|
1036
1309
|
rateSegments: () => rateSegments,
|
|
1037
1310
|
rewardsRateFor: () => rewardsRateFor,
|
|
1311
|
+
rewardsTvl: () => rewardsTvl,
|
|
1038
1312
|
toAssets: () => toAssets,
|
|
1039
1313
|
toShares: () => toShares
|
|
1040
1314
|
});
|
|
@@ -1062,30 +1336,40 @@ function rateSegments(s, totalAssets, fromS, nowS) {
|
|
|
1062
1336
|
}
|
|
1063
1337
|
const maxB = (a, b) => a > b ? a : b;
|
|
1064
1338
|
const minB = (a, b) => a < b ? a : b;
|
|
1065
|
-
/**
|
|
1066
|
-
|
|
1339
|
+
/**
|
|
1340
|
+
* TVL the contract prices rewards on: total shares at the STORED (pre-accrual) token exchange
|
|
1341
|
+
* price, as `accounting::accrued_token_exchange_price` does. Rounded up on "audited" packages
|
|
1342
|
+
* (742ea21+), floored on the 09-18 "pre-audit" packages.
|
|
1343
|
+
*/
|
|
1344
|
+
const rewardsTvl = (state, variant = "audited") => toAssets(state.totalShares, state.tokenExchangePrice, variant === "audited");
|
|
1345
|
+
/**
|
|
1346
|
+
* Current rewards rate (raw, RETURN_PERCENT_PRECISION: 1e14 = 100%) at `now`, from the active schedule segment, priced on
|
|
1347
|
+
* the pre-accrual TVL (`rewardsTvl`) exactly as the contract prices the segment it will credit.
|
|
1348
|
+
* Windows are [start, end): the rate is live from `startS` and gone at `endS`.
|
|
1349
|
+
*/
|
|
1350
|
+
function currentRewardsRate(state, now, variant = "audited") {
|
|
1067
1351
|
if (!state.rewards) return 0n;
|
|
1068
|
-
const tvl =
|
|
1352
|
+
const tvl = rewardsTvl(state, variant);
|
|
1069
1353
|
const s = state.rewards;
|
|
1070
1354
|
if (now >= s.startS && now < s.endS) return rewardsRateFor(s.yearlyReward, tvl, s.startTvl);
|
|
1071
1355
|
if (now >= s.nextStartS && now < s.nextEndS) return rewardsRateFor(s.nextYearlyReward, tvl, s.startTvl);
|
|
1072
1356
|
return 0n;
|
|
1073
1357
|
}
|
|
1074
1358
|
/** `accounting::accrued_token_exchange_price`. */
|
|
1075
|
-
function accruedTokenExchangePrice(state, newLiquidityExchangePrice, nowS) {
|
|
1359
|
+
function accruedTokenExchangePrice(state, newLiquidityExchangePrice, nowS, variant = "audited") {
|
|
1076
1360
|
const oldLiq = state.liquidityExchangePrice;
|
|
1077
1361
|
const oldToken = state.tokenExchangePrice;
|
|
1078
1362
|
if (newLiquidityExchangePrice < oldLiq) throw new RangeError("liquidity exchange price went backwards");
|
|
1079
1363
|
let totalReturn = mulDiv(newLiquidityExchangePrice - oldLiq, RETURN_PERCENT_PRECISION, oldLiq);
|
|
1080
1364
|
if (state.rewards) {
|
|
1081
|
-
const tvl =
|
|
1365
|
+
const tvl = rewardsTvl(state, variant);
|
|
1082
1366
|
for (const seg of rateSegments(state.rewards, tvl, state.lastUpdateS, nowS)) totalReturn += mulDiv(seg.rate, seg.toS - seg.fromS, SECONDS_PER_YEAR);
|
|
1083
1367
|
}
|
|
1084
1368
|
return oldToken + mulDiv(oldToken, totalReturn, RETURN_PERCENT_PRECISION);
|
|
1085
1369
|
}
|
|
1086
1370
|
/** `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);
|
|
1371
|
+
function previews(state, liquidityExchangePrice, nowS, assets, shares, variant = "audited") {
|
|
1372
|
+
const tokenEp = accruedTokenExchangePrice(state, liquidityExchangePrice, nowS, variant);
|
|
1089
1373
|
return {
|
|
1090
1374
|
tokenExchangePrice: tokenEp,
|
|
1091
1375
|
previewDeposit: toShares(toAssets(toShares(assets, liquidityExchangePrice, false), liquidityExchangePrice, false), tokenEp, false),
|
|
@@ -1095,81 +1379,6 @@ function previews(state, liquidityExchangePrice, nowS, assets, shares) {
|
|
|
1095
1379
|
};
|
|
1096
1380
|
}
|
|
1097
1381
|
|
|
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
1382
|
//#endregion
|
|
1174
1383
|
//#region src/view.ts
|
|
1175
1384
|
/** Any address works as the simulated sender; nothing is executed. */
|
|
@@ -1214,12 +1423,18 @@ var Lending = class {
|
|
|
1214
1423
|
this.ctx = ctx;
|
|
1215
1424
|
this.liquidity = liquidity;
|
|
1216
1425
|
}
|
|
1426
|
+
/** Object ids `loadLendings` reads: every LendingState, then every rewards field. */
|
|
1427
|
+
lendingObjectIds(tokens) {
|
|
1428
|
+
const ids = tokens.map((t) => this.ctx.derive.lendingStateId(normalizeStructTag(t)));
|
|
1429
|
+
return [...ids, ...ids.map((id) => this.ctx.derive.rewardsId(id))];
|
|
1430
|
+
}
|
|
1217
1431
|
/** LendingStates (+ rewards fields) for `tokens`; `null` where no pool exists. */
|
|
1218
1432
|
async loadLendings(tokens) {
|
|
1219
|
-
|
|
1433
|
+
return this.lendingsFromObjects(tokens, await fetchObjects(this.ctx.client, this.lendingObjectIds(tokens)));
|
|
1434
|
+
}
|
|
1435
|
+
lendingsFromObjects(tokens, objs) {
|
|
1220
1436
|
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))]);
|
|
1437
|
+
const ids = norm.map((t) => this.ctx.derive.lendingStateId(t));
|
|
1223
1438
|
return norm.map((token, i) => {
|
|
1224
1439
|
const o = objs[i];
|
|
1225
1440
|
if (!o) return null;
|
|
@@ -1248,23 +1463,54 @@ var Lending = class {
|
|
|
1248
1463
|
};
|
|
1249
1464
|
});
|
|
1250
1465
|
}
|
|
1466
|
+
/**
|
|
1467
|
+
* Everything a pool read needs in ONE `getObjects` batch (LendingState, rewards field,
|
|
1468
|
+
* TokenReserve, rate data per token; 50 ids per request, chunks in parallel), plus the chain
|
|
1469
|
+
* clock in parallel. With `withEntries`, a second batch reads each pool's liquidity supply entry.
|
|
1470
|
+
*/
|
|
1471
|
+
async loadPools(tokens, withEntries = false) {
|
|
1472
|
+
const norm = tokens.map((t) => normalizeStructTag(t));
|
|
1473
|
+
const lIds = this.lendingObjectIds(norm);
|
|
1474
|
+
const [objs, now] = await Promise.all([fetchObjects(this.ctx.client, [...lIds, ...this.liquidity.reserveObjectIds(norm)]), this.ctx.now()]);
|
|
1475
|
+
const lendings = this.lendingsFromObjects(norm, objs.slice(0, lIds.length));
|
|
1476
|
+
const reserves = this.liquidity.reservesFromObjects(norm, objs.slice(lIds.length));
|
|
1477
|
+
const pools = norm.map((token, i) => {
|
|
1478
|
+
const lending = lendings[i];
|
|
1479
|
+
if (!lending) throw new Error(`no LendingState for ${token}`);
|
|
1480
|
+
return {
|
|
1481
|
+
lending,
|
|
1482
|
+
reserve: reserves[i],
|
|
1483
|
+
supplyEntry: null
|
|
1484
|
+
};
|
|
1485
|
+
});
|
|
1486
|
+
if (withEntries) {
|
|
1487
|
+
const entries = await this.liquidity.loadUserEntries(pools.map((p) => ({
|
|
1488
|
+
holder: p.lending.id,
|
|
1489
|
+
reserve: p.reserve
|
|
1490
|
+
})));
|
|
1491
|
+
pools.forEach((p, i) => p.supplyEntry = entries[i].supply);
|
|
1492
|
+
}
|
|
1493
|
+
return {
|
|
1494
|
+
pools,
|
|
1495
|
+
now
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1251
1498
|
async loadOne(token) {
|
|
1252
|
-
const
|
|
1253
|
-
if (!lending) throw new Error(`no LendingState for ${token}`);
|
|
1499
|
+
const { pools, now } = await this.loadPools([token]);
|
|
1254
1500
|
return {
|
|
1255
|
-
|
|
1256
|
-
|
|
1501
|
+
...pools[0],
|
|
1502
|
+
now
|
|
1257
1503
|
};
|
|
1258
1504
|
}
|
|
1259
1505
|
async getAllLendings() {
|
|
1260
1506
|
const tokens = await this.liquidity.listedTokens();
|
|
1261
1507
|
const recs = (await this.loadLendings(tokens)).filter((r) => r !== null);
|
|
1262
|
-
const metas = await Promise.all(recs.map((r) => this.ctx.
|
|
1508
|
+
const metas = await Promise.all(recs.map((r) => this.ctx.coinMetadata(r.token)));
|
|
1263
1509
|
return recs.map((r, i) => ({
|
|
1264
1510
|
id: r.id,
|
|
1265
1511
|
mint: r.token,
|
|
1266
1512
|
fTokenMint: this.ctx.derive.fTokenType(r.token),
|
|
1267
|
-
decimals: metas[i]
|
|
1513
|
+
decimals: metas[i]?.decimals ?? 0,
|
|
1268
1514
|
totalShares: r.snapshot.totalShares,
|
|
1269
1515
|
liquidityExchangePrice: r.snapshot.liquidityExchangePrice,
|
|
1270
1516
|
tokenExchangePrice: r.snapshot.tokenExchangePrice,
|
|
@@ -1281,16 +1527,14 @@ var Lending = class {
|
|
|
1281
1527
|
getAllJlTokens = () => this.getAllFTokens();
|
|
1282
1528
|
/** Token exchange price accrued to now (`accounting::calculate_new_token_exchange_price`). */
|
|
1283
1529
|
async getExchangePrice(token) {
|
|
1284
|
-
const { lending, reserve } = await this.loadOne(token);
|
|
1285
|
-
const
|
|
1286
|
-
|
|
1287
|
-
return accruedTokenExchangePrice(lending.snapshot, liqEp, now);
|
|
1530
|
+
const { lending, reserve, now } = await this.loadOne(token);
|
|
1531
|
+
const liqEp = calculateExchangePrice(reserve.snapshot, now, this.ctx.math).supplyExchangePrice;
|
|
1532
|
+
return accruedTokenExchangePrice(lending.snapshot, liqEp, now, this.ctx.math);
|
|
1288
1533
|
}
|
|
1289
1534
|
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);
|
|
1535
|
+
const { lending, reserve, now } = await this.loadOne(token);
|
|
1536
|
+
const liqEp = calculateExchangePrice(reserve.snapshot, now, this.ctx.math).supplyExchangePrice;
|
|
1537
|
+
const p = previews(lending.snapshot, liqEp, now, assets, shares, this.ctx.math);
|
|
1294
1538
|
return {
|
|
1295
1539
|
previewDeposit: p.previewDeposit,
|
|
1296
1540
|
previewMint: p.previewMint,
|
|
@@ -1298,11 +1542,13 @@ var Lending = class {
|
|
|
1298
1542
|
previewRedeem: p.previewRedeem
|
|
1299
1543
|
};
|
|
1300
1544
|
}
|
|
1301
|
-
detailsFromRecords(lending, reserve, supplyEntry, meta, now
|
|
1545
|
+
detailsFromRecords(lending, reserve, supplyEntry, meta, now) {
|
|
1302
1546
|
const { userSupplyData, overallTokenData } = this.liquidity.supplyDataFromRecords(reserve, supplyEntry, now);
|
|
1303
|
-
const tokenEp = accruedTokenExchangePrice(lending.snapshot, overallTokenData.supplyExchangePrice, now);
|
|
1547
|
+
const tokenEp = accruedTokenExchangePrice(lending.snapshot, overallTokenData.supplyExchangePrice, now, this.ctx.math);
|
|
1304
1548
|
const totalAssets = toAssets(lending.snapshot.totalShares, tokenEp, false);
|
|
1305
1549
|
const one = 10n ** BigInt(meta.decimals);
|
|
1550
|
+
const supplyRate = supplyRateToBps(supplyRateRaw(reserve.snapshot, overallTokenData.supplyExchangePrice, overallTokenData.borrowExchangePrice, this.ctx.math));
|
|
1551
|
+
const rewardsRate = rewardsRateToBps(currentRewardsRate(lending.snapshot, now, this.ctx.math));
|
|
1306
1552
|
return {
|
|
1307
1553
|
tokenAddress: this.ctx.derive.fTokenType(lending.token),
|
|
1308
1554
|
lendingStateId: lending.id,
|
|
@@ -1314,60 +1560,207 @@ var Lending = class {
|
|
|
1314
1560
|
totalSupply: lending.snapshot.totalShares,
|
|
1315
1561
|
conversionRateToShares: toShares(one, tokenEp, false),
|
|
1316
1562
|
conversionRateToAssets: toAssets(one, tokenEp, false),
|
|
1317
|
-
rewardsRate
|
|
1318
|
-
supplyRate
|
|
1563
|
+
rewardsRate,
|
|
1564
|
+
supplyRate,
|
|
1565
|
+
totalRate: supplyRate + rewardsRate,
|
|
1319
1566
|
rebalanceDifference: userSupplyData.supply - totalAssets,
|
|
1320
1567
|
userSupplyData
|
|
1321
1568
|
};
|
|
1322
1569
|
}
|
|
1570
|
+
/** fToken name/symbol/decimals (cached; falls back to the underlying's metadata). */
|
|
1323
1571
|
async fTokenMeta(token) {
|
|
1324
|
-
const [f,
|
|
1325
|
-
const under = u.coinMetadata;
|
|
1572
|
+
const [f, under] = await Promise.all([this.ctx.coinMetadata(this.ctx.derive.fTokenType(token)).catch(() => null), this.ctx.coinMetadata(token)]);
|
|
1326
1573
|
return {
|
|
1327
|
-
name: f
|
|
1328
|
-
symbol: f
|
|
1329
|
-
decimals: f
|
|
1574
|
+
name: f?.name ?? `Fluid ${under?.name ?? ""}`.trim(),
|
|
1575
|
+
symbol: f?.symbol ?? `f${under?.symbol ?? ""}`,
|
|
1576
|
+
decimals: f?.decimals ?? under?.decimals ?? 0
|
|
1330
1577
|
};
|
|
1331
1578
|
}
|
|
1332
1579
|
async getFTokenDetails(token) {
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1580
|
+
return (await this.getFTokensDetails([token]))[0];
|
|
1581
|
+
}
|
|
1582
|
+
/** Details for many fTokens: 2 batched object reads + the clock, metadata cached. */
|
|
1583
|
+
async getFTokensDetails(tokens) {
|
|
1584
|
+
return (await this.detailsWithPrices(tokens)).map((d) => d.details);
|
|
1585
|
+
}
|
|
1586
|
+
async detailsWithPrices(tokens) {
|
|
1587
|
+
const [{ pools, now }, metas] = await Promise.all([this.loadPools(tokens, true), Promise.all(tokens.map((t) => this.fTokenMeta(normalizeStructTag(t))))]);
|
|
1588
|
+
return pools.map((p, i) => {
|
|
1589
|
+
const details = this.detailsFromRecords(p.lending, p.reserve, p.supplyEntry, metas[i], now);
|
|
1590
|
+
const liqEp = calculateExchangePrice(p.reserve.snapshot, now, this.ctx.math).supplyExchangePrice;
|
|
1591
|
+
return {
|
|
1592
|
+
details,
|
|
1593
|
+
tokenEp: accruedTokenExchangePrice(p.lending.snapshot, liqEp, now, this.ctx.math)
|
|
1594
|
+
};
|
|
1595
|
+
});
|
|
1339
1596
|
}
|
|
1340
1597
|
/** @deprecated Solana name; use `getFTokenDetails`. */
|
|
1341
1598
|
getJlTokenDetails = (token) => this.getFTokenDetails(token);
|
|
1342
1599
|
async getAllFTokenDetails() {
|
|
1343
|
-
return
|
|
1600
|
+
return this.getFTokensDetails(await this.listedLendingTokens());
|
|
1601
|
+
}
|
|
1602
|
+
/** Underlying coin types that have a lending pool (pure state: one batched existence read). */
|
|
1603
|
+
async listedLendingTokens() {
|
|
1604
|
+
const tokens = await this.liquidity.listedTokens();
|
|
1605
|
+
const ids = tokens.map((t) => this.ctx.derive.lendingStateId(normalizeStructTag(t)));
|
|
1606
|
+
const objs = await fetchObjects(this.ctx.client, ids);
|
|
1607
|
+
return tokens.map((t) => normalizeStructTag(t)).filter((_, i) => objs[i] !== null);
|
|
1344
1608
|
}
|
|
1345
1609
|
/** @deprecated Solana name; use `getAllFTokenDetails`. */
|
|
1346
1610
|
getAllJlTokenDetails = () => this.getAllFTokenDetails();
|
|
1347
1611
|
async getFTokenInternalData(token) {
|
|
1348
|
-
const {
|
|
1349
|
-
const
|
|
1350
|
-
|
|
1351
|
-
reserve
|
|
1352
|
-
}]);
|
|
1353
|
-
const now = this.ctx.now();
|
|
1612
|
+
const { pools, now } = await this.loadPools([token], true);
|
|
1613
|
+
const { lending, reserve, supplyEntry } = pools[0];
|
|
1614
|
+
const entry = { supply: supplyEntry };
|
|
1354
1615
|
const { userSupplyData } = this.liquidity.supplyDataFromRecords(reserve, entry.supply, now);
|
|
1355
|
-
const liqEp = calculateExchangePrice(reserve.snapshot, now).supplyExchangePrice;
|
|
1616
|
+
const liqEp = calculateExchangePrice(reserve.snapshot, now, this.ctx.math).supplyExchangePrice;
|
|
1356
1617
|
return {
|
|
1357
1618
|
liquidity: reserve.id,
|
|
1358
1619
|
lendingFactory: this.ctx.config.objects.lendingRegistry,
|
|
1359
1620
|
lendingRewardsRateModel: lending.raw.rewards_writer ?? null,
|
|
1360
|
-
rebalancer: "",
|
|
1361
1621
|
liquidityBalance: userSupplyData.supply,
|
|
1362
1622
|
liquidityExchangePrice: liqEp,
|
|
1363
|
-
tokenExchangePrice: accruedTokenExchangePrice(lending.snapshot, liqEp, now)
|
|
1623
|
+
tokenExchangePrice: accruedTokenExchangePrice(lending.snapshot, liqEp, now, this.ctx.math)
|
|
1364
1624
|
};
|
|
1365
1625
|
}
|
|
1366
1626
|
/** @deprecated Solana name; use `getFTokenInternalData`. */
|
|
1367
1627
|
getJlTokenInternalData = (token) => this.getFTokenInternalData(token);
|
|
1628
|
+
/**
|
|
1629
|
+
* Generic lending state per fToken (identity, accrued + stored exchange prices, TVL, rates in
|
|
1630
|
+
* bps, rewards schedule) for `tokens` (underlying coin types), or every listed pool when
|
|
1631
|
+
* omitted. One batched `getObjects` for all pools + the on-chain Clock; metadata cached.
|
|
1632
|
+
*/
|
|
1633
|
+
async getLendingStateData(tokens) {
|
|
1634
|
+
const list = (tokens ?? await this.listedLendingTokens()).map((t) => normalizeStructTag(t));
|
|
1635
|
+
const [{ pools, now }, metas] = await Promise.all([this.loadPools(list), Promise.all(list.map((t) => this.fTokenMeta(t)))]);
|
|
1636
|
+
return pools.map(({ lending, reserve }, i) => this.stateDataFrom(lending, reserve, metas[i].decimals, now));
|
|
1637
|
+
}
|
|
1638
|
+
/** Pure: `LendingStateData` from loaded records at `now`. */
|
|
1639
|
+
stateDataFrom(lending, reserve, decimals, now) {
|
|
1640
|
+
const m = this.ctx.math;
|
|
1641
|
+
const liq = calculateExchangePrice(reserve.snapshot, now, m);
|
|
1642
|
+
const tokenEp = accruedTokenExchangePrice(lending.snapshot, liq.supplyExchangePrice, now, m);
|
|
1643
|
+
const supplyRate = supplyRateToBps(supplyRateRaw(reserve.snapshot, liq.supplyExchangePrice, liq.borrowExchangePrice, m));
|
|
1644
|
+
const rewardsRate = rewardsRateToBps(currentRewardsRate(lending.snapshot, now, m));
|
|
1645
|
+
return {
|
|
1646
|
+
fTokenType: this.ctx.derive.fTokenType(lending.token),
|
|
1647
|
+
asset: lending.token,
|
|
1648
|
+
decimals,
|
|
1649
|
+
lendingStateId: lending.id,
|
|
1650
|
+
timestamp: now,
|
|
1651
|
+
tokenExchangePrice: tokenEp,
|
|
1652
|
+
liquidityExchangePrice: liq.supplyExchangePrice,
|
|
1653
|
+
storedTokenExchangePrice: lending.snapshot.tokenExchangePrice,
|
|
1654
|
+
storedLiquidityExchangePrice: lending.snapshot.liquidityExchangePrice,
|
|
1655
|
+
lastUpdateS: lending.snapshot.lastUpdateS,
|
|
1656
|
+
liquidityLastUpdateS: reserve.snapshot.lastUpdateTimestamp,
|
|
1657
|
+
totalShares: lending.snapshot.totalShares,
|
|
1658
|
+
totalAssets: toAssets(lending.snapshot.totalShares, tokenEp, false),
|
|
1659
|
+
supplyRate,
|
|
1660
|
+
rewardsRate,
|
|
1661
|
+
totalRate: supplyRate + rewardsRate,
|
|
1662
|
+
rewards: lending.snapshot.rewards ? { ...lending.snapshot.rewards } : null
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1666
|
+
* Wallet holders of `Coin<FToken<T>>` for `token` (underlying coin type), via the GraphQL
|
|
1667
|
+
* indexer (`config.graphqlUrl`; needs `discovery: "indexer"`). Lists every Coin<FToken<T>>
|
|
1668
|
+
* object by type and keeps the ones with an ADDRESS owner (AddressOwner or
|
|
1669
|
+
* ConsensusAddressOwner), aggregated by owner, all pages at one checkpoint (consistent cursors).
|
|
1670
|
+
*
|
|
1671
|
+
* LIMITS (documented, by design): coins owned by other objects, shared/immutable coins and
|
|
1672
|
+
* wrapped `Balance<FToken<T>>` (inside other protocols' objects) are NOT attributed to a wallet;
|
|
1673
|
+
* nor are address balances (funds credited to an address without a coin object), since they are
|
|
1674
|
+
* not objects and cannot be enumerated by type. `totalSupply` is returned so a caller can
|
|
1675
|
+
* compare it with the sum of the wallets. With `verify` (default true) each wallet is cross-checked against
|
|
1676
|
+
* fullnode `getBalance` (coinBalance + addressBalance), `concurrency` at a time (default 8).
|
|
1677
|
+
*/
|
|
1678
|
+
async getFTokenHolders(token, opts = {}) {
|
|
1679
|
+
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");
|
|
1680
|
+
const url = this.ctx.config.graphqlUrl;
|
|
1681
|
+
if (!url) throw new Error("config.graphqlUrl not set (needed for the holder list)");
|
|
1682
|
+
const asset = normalizeStructTag(token);
|
|
1683
|
+
const fTokenType = this.ctx.derive.fTokenType(asset);
|
|
1684
|
+
const stateId = this.ctx.derive.lendingStateId(asset);
|
|
1685
|
+
const coinType = `0x2::coin::Coin<${fTokenType}>`;
|
|
1686
|
+
const NODES = `pageInfo { hasNextPage endCursor }
|
|
1687
|
+
nodes { owner { __typename ... on AddressOwner { address { address } } ... on ConsensusAddressOwner { address { address } } ... on ObjectOwner { address { address } } }
|
|
1688
|
+
asMoveObject { contents { json } } }`;
|
|
1689
|
+
const first = `query($t: String!, $s: SuiAddress!, $n: Int!) {
|
|
1690
|
+
checkpoint { sequenceNumber }
|
|
1691
|
+
state: object(address: $s) { asMoveObject { contents { json } } }
|
|
1692
|
+
objects(first: $n, filter: { type: $t }) { ${NODES} } }`;
|
|
1693
|
+
const next = `query($t: String!, $n: Int!, $a: String) { objects(first: $n, after: $a, filter: { type: $t }) { ${NODES} } }`;
|
|
1694
|
+
const n = opts.pageSize ?? 50;
|
|
1695
|
+
const byOwner = /* @__PURE__ */ new Map();
|
|
1696
|
+
let page = await graphqlQuery(url, first, {
|
|
1697
|
+
t: coinType,
|
|
1698
|
+
s: stateId,
|
|
1699
|
+
n
|
|
1700
|
+
});
|
|
1701
|
+
const checkpoint = BigInt(page.checkpoint.sequenceNumber);
|
|
1702
|
+
const supplyJson = page.state?.asMoveObject?.contents.json.supply;
|
|
1703
|
+
if (supplyJson === void 0) throw new Error(`no LendingState ${stateId} for ${asset}`);
|
|
1704
|
+
for (;;) {
|
|
1705
|
+
for (const node of page.objects.nodes) {
|
|
1706
|
+
const bal = BigInt(node.asMoveObject?.contents.json.balance ?? 0);
|
|
1707
|
+
const kind = node.owner?.__typename;
|
|
1708
|
+
if ((kind === "AddressOwner" || kind === "ConsensusAddressOwner") && node.owner?.address) {
|
|
1709
|
+
const k = normalizeSuiAddress(node.owner.address.address);
|
|
1710
|
+
const e = byOwner.get(k) ?? {
|
|
1711
|
+
balance: 0n,
|
|
1712
|
+
coins: 0
|
|
1713
|
+
};
|
|
1714
|
+
e.balance += bal;
|
|
1715
|
+
e.coins += 1;
|
|
1716
|
+
byOwner.set(k, e);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
if (!page.objects.pageInfo.hasNextPage) break;
|
|
1720
|
+
page = await graphqlQuery(url, next, {
|
|
1721
|
+
t: coinType,
|
|
1722
|
+
n,
|
|
1723
|
+
a: page.objects.pageInfo.endCursor
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
const holders = [...byOwner.entries()].map(([owner, e]) => ({
|
|
1727
|
+
owner,
|
|
1728
|
+
balance: e.balance,
|
|
1729
|
+
coins: e.coins
|
|
1730
|
+
})).sort((a, b) => a.balance === b.balance ? 0 : a.balance > b.balance ? -1 : 1);
|
|
1731
|
+
if (opts.verify !== false) {
|
|
1732
|
+
const limit = Math.max(1, opts.concurrency ?? 8);
|
|
1733
|
+
let i = 0;
|
|
1734
|
+
const worker = async () => {
|
|
1735
|
+
while (i < holders.length) {
|
|
1736
|
+
const h = holders[i++];
|
|
1737
|
+
const { balance: b } = await this.ctx.client.core.getBalance({
|
|
1738
|
+
owner: h.owner,
|
|
1739
|
+
coinType: fTokenType
|
|
1740
|
+
});
|
|
1741
|
+
h.rpc = {
|
|
1742
|
+
balance: BigInt(b.balance),
|
|
1743
|
+
coinBalance: BigInt(b.coinBalance ?? b.balance),
|
|
1744
|
+
addressBalance: BigInt(b.addressBalance ?? 0)
|
|
1745
|
+
};
|
|
1746
|
+
h.agrees = h.rpc.coinBalance === h.balance && h.rpc.addressBalance === 0n;
|
|
1747
|
+
}
|
|
1748
|
+
};
|
|
1749
|
+
await Promise.all(Array.from({ length: Math.min(limit, holders.length) }, worker));
|
|
1750
|
+
}
|
|
1751
|
+
return {
|
|
1752
|
+
fTokenType,
|
|
1753
|
+
asset,
|
|
1754
|
+
checkpoint,
|
|
1755
|
+
holders,
|
|
1756
|
+
countedTotal: holders.reduce((a, h) => a + h.balance, 0n),
|
|
1757
|
+
totalSupply: BigInt(typeof supplyJson === "object" ? supplyJson.value : supplyJson)
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
/** `[rewards writer cap id, current rewards rate in bps]`. */
|
|
1368
1761
|
async getFTokenRewards(token) {
|
|
1369
|
-
const { lending } = await this.loadOne(token);
|
|
1370
|
-
return [lending.raw.rewards_writer ?? null, currentRewardsRate(lending.snapshot, this.ctx.
|
|
1762
|
+
const { lending, now } = await this.loadOne(token);
|
|
1763
|
+
return [lending.raw.rewards_writer ?? null, rewardsRateToBps(currentRewardsRate(lending.snapshot, now, this.ctx.math))];
|
|
1371
1764
|
}
|
|
1372
1765
|
/** @deprecated Solana name; use `getFTokenRewards`. */
|
|
1373
1766
|
getJlTokenRewards = (token) => this.getFTokenRewards(token);
|
|
@@ -1380,7 +1773,7 @@ var Lending = class {
|
|
|
1380
1773
|
startTime: r.startS,
|
|
1381
1774
|
endTime: r.endS,
|
|
1382
1775
|
startTvl: r.startTvl,
|
|
1383
|
-
maxRate:
|
|
1776
|
+
maxRate: rewardsRateToBps(MAX_REWARDS_RATE),
|
|
1384
1777
|
rewardAmount: r.yearlyReward,
|
|
1385
1778
|
nextStartTime: r.nextStartS,
|
|
1386
1779
|
nextEndTime: r.nextEndS,
|
|
@@ -1405,16 +1798,30 @@ var Lending = class {
|
|
|
1405
1798
|
return {
|
|
1406
1799
|
fTokenShares: s,
|
|
1407
1800
|
underlyingAssets: toAssets(s, ep, false),
|
|
1408
|
-
underlyingBalance: BigInt(under.balance.balance)
|
|
1409
|
-
allowance: 0n
|
|
1801
|
+
underlyingBalance: BigInt(under.balance.balance)
|
|
1410
1802
|
};
|
|
1411
1803
|
}
|
|
1412
1804
|
async getUserPositions(user) {
|
|
1413
|
-
const tokens = await this.
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1805
|
+
const tokens = await this.listedLendingTokens();
|
|
1806
|
+
const details = await this.detailsWithPrices(tokens);
|
|
1807
|
+
const balances = await Promise.all(tokens.flatMap((t) => [this.ctx.client.core.getBalance({
|
|
1808
|
+
owner: user,
|
|
1809
|
+
coinType: this.ctx.derive.fTokenType(t)
|
|
1810
|
+
}), this.ctx.client.core.getBalance({
|
|
1811
|
+
owner: user,
|
|
1812
|
+
coinType: t
|
|
1813
|
+
})]));
|
|
1814
|
+
return details.map(({ details: d, tokenEp }, i) => {
|
|
1815
|
+
const shares = BigInt(balances[2 * i].balance.balance);
|
|
1816
|
+
return {
|
|
1817
|
+
fTokenDetails: d,
|
|
1818
|
+
userPosition: {
|
|
1819
|
+
fTokenShares: shares,
|
|
1820
|
+
underlyingAssets: toAssets(shares, tokenEp, false),
|
|
1821
|
+
underlyingBalance: BigInt(balances[2 * i + 1].balance.balance)
|
|
1822
|
+
}
|
|
1823
|
+
};
|
|
1824
|
+
});
|
|
1418
1825
|
}
|
|
1419
1826
|
/** `lending::auth::is_admin(&Auth, addr)` via simulation. */
|
|
1420
1827
|
async isLendingAdmin(addr) {
|
|
@@ -1549,11 +1956,15 @@ var Liquidity = class {
|
|
|
1549
1956
|
constructor(ctx) {
|
|
1550
1957
|
this.ctx = ctx;
|
|
1551
1958
|
}
|
|
1552
|
-
/**
|
|
1553
|
-
|
|
1959
|
+
/** Object ids `loadReserves` reads: every reserve, then every reserve's rate-data field. */
|
|
1960
|
+
reserveObjectIds(tokens) {
|
|
1961
|
+
const ids = tokens.map((t) => this.ctx.derive.tokenReserveId(normalizeStructTag(t)));
|
|
1962
|
+
return [...ids, ...ids.map((id) => this.ctx.derive.rateDataId(id))];
|
|
1963
|
+
}
|
|
1964
|
+
/** Parse objects fetched for `reserveObjectIds(tokens)` (same order). */
|
|
1965
|
+
reservesFromObjects(tokens, objs) {
|
|
1554
1966
|
const norm = tokens.map((t) => normalizeStructTag(t));
|
|
1555
1967
|
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
1968
|
return norm.map((token, i) => {
|
|
1558
1969
|
const o = objs[i];
|
|
1559
1970
|
if (!o) throw new Error(`no TokenReserve for ${token} (${ids[i]})`);
|
|
@@ -1567,6 +1978,10 @@ var Liquidity = class {
|
|
|
1567
1978
|
};
|
|
1568
1979
|
});
|
|
1569
1980
|
}
|
|
1981
|
+
/** Reserves + their rate-data fields in one batched `getObjects`. */
|
|
1982
|
+
async loadReserves(tokens) {
|
|
1983
|
+
return this.reservesFromObjects(tokens, await fetchObjects(this.ctx.client, this.reserveObjectIds(tokens)));
|
|
1984
|
+
}
|
|
1570
1985
|
async loadReserve(token) {
|
|
1571
1986
|
return (await this.loadReserves([token]))[0];
|
|
1572
1987
|
}
|
|
@@ -1678,9 +2093,9 @@ var Liquidity = class {
|
|
|
1678
2093
|
maxUtilization: s.maxUtilization
|
|
1679
2094
|
};
|
|
1680
2095
|
}
|
|
1681
|
-
/** Pure: accrue a reserve snapshot to `now` (
|
|
2096
|
+
/** Pure: accrue a reserve snapshot to `now` (unix seconds; use `await ctx.now()` for chain time). */
|
|
1682
2097
|
calculateExchangePrice(snapshot, now) {
|
|
1683
|
-
return calculateExchangePrice(snapshot, now
|
|
2098
|
+
return calculateExchangePrice(snapshot, now, this.ctx.math);
|
|
1684
2099
|
}
|
|
1685
2100
|
async getRateConfig(token) {
|
|
1686
2101
|
const { rateData: r } = await this.loadReserve(token);
|
|
@@ -1715,8 +2130,8 @@ var Liquidity = class {
|
|
|
1715
2130
|
return { maxUtilization: (await this.loadReserve(token)).snapshot.maxUtilization };
|
|
1716
2131
|
}
|
|
1717
2132
|
async getRevenue(token) {
|
|
1718
|
-
const { snapshot } = await this.loadReserve(token);
|
|
1719
|
-
const ep = calculateExchangePrice(snapshot, this.ctx.
|
|
2133
|
+
const [{ snapshot }, now] = await Promise.all([this.loadReserve(token), this.ctx.now()]);
|
|
2134
|
+
const ep = calculateExchangePrice(snapshot, now, this.ctx.math);
|
|
1720
2135
|
return calcRevenue(snapshot, ep.supplyExchangePrice, ep.borrowExchangePrice);
|
|
1721
2136
|
}
|
|
1722
2137
|
async getTokenRateData(token) {
|
|
@@ -1725,15 +2140,16 @@ var Liquidity = class {
|
|
|
1725
2140
|
async getTokensRateData(tokens) {
|
|
1726
2141
|
return (await this.loadReserves(tokens)).map((r) => r.rateData);
|
|
1727
2142
|
}
|
|
1728
|
-
overallFromRecord(r, now
|
|
1729
|
-
return processOverallTokenData(r.snapshot, r.rateData, now);
|
|
2143
|
+
overallFromRecord(r, now) {
|
|
2144
|
+
return processOverallTokenData(r.snapshot, r.rateData, now, this.ctx.math);
|
|
1730
2145
|
}
|
|
1731
2146
|
async getOverallTokenData(token) {
|
|
1732
|
-
|
|
2147
|
+
const [r, now] = await Promise.all([this.loadReserve(token), this.ctx.now()]);
|
|
2148
|
+
return this.overallFromRecord(r, now);
|
|
1733
2149
|
}
|
|
1734
2150
|
async getOverallTokensData(tokens) {
|
|
1735
|
-
const now = this.ctx.now();
|
|
1736
|
-
return
|
|
2151
|
+
const [rs, now] = await Promise.all([this.loadReserves(tokens), this.ctx.now()]);
|
|
2152
|
+
return rs.map((r) => this.overallFromRecord(r, now));
|
|
1737
2153
|
}
|
|
1738
2154
|
async getAllOverallTokensData() {
|
|
1739
2155
|
return this.getOverallTokensData(await this.listedTokens());
|
|
@@ -1755,14 +2171,14 @@ var Liquidity = class {
|
|
|
1755
2171
|
}]);
|
|
1756
2172
|
return e.borrow ?? 0n;
|
|
1757
2173
|
}
|
|
1758
|
-
supplyDataFromRecords(reserve, acc, now
|
|
2174
|
+
supplyDataFromRecords(reserve, acc, now) {
|
|
1759
2175
|
const overallTokenData = this.overallFromRecord(reserve, now);
|
|
1760
2176
|
return {
|
|
1761
2177
|
userSupplyData: processUserSupplyData(acc, overallTokenData, reserve.snapshot.vaultBalance, now),
|
|
1762
2178
|
overallTokenData
|
|
1763
2179
|
};
|
|
1764
2180
|
}
|
|
1765
|
-
borrowDataFromRecords(reserve, acc, now
|
|
2181
|
+
borrowDataFromRecords(reserve, acc, now) {
|
|
1766
2182
|
const overallTokenData = this.overallFromRecord(reserve, now);
|
|
1767
2183
|
return {
|
|
1768
2184
|
userBorrowData: processUserBorrowData(acc, overallTokenData, reserve.snapshot.vaultBalance, now),
|
|
@@ -1770,28 +2186,27 @@ var Liquidity = class {
|
|
|
1770
2186
|
};
|
|
1771
2187
|
}
|
|
1772
2188
|
async getUserSupplyData(user, token) {
|
|
1773
|
-
const reserve = await this.loadReserve(token);
|
|
2189
|
+
const [reserve, now] = await Promise.all([this.loadReserve(token), this.ctx.now()]);
|
|
1774
2190
|
const [e] = await this.loadUserEntries([{
|
|
1775
2191
|
holder: user,
|
|
1776
2192
|
reserve
|
|
1777
2193
|
}]);
|
|
1778
|
-
return this.supplyDataFromRecords(reserve, e.supply);
|
|
2194
|
+
return this.supplyDataFromRecords(reserve, e.supply, now);
|
|
1779
2195
|
}
|
|
1780
2196
|
async getUserBorrowData(user, token) {
|
|
1781
|
-
const reserve = await this.loadReserve(token);
|
|
2197
|
+
const [reserve, now] = await Promise.all([this.loadReserve(token), this.ctx.now()]);
|
|
1782
2198
|
const [e] = await this.loadUserEntries([{
|
|
1783
2199
|
holder: user,
|
|
1784
2200
|
reserve
|
|
1785
2201
|
}]);
|
|
1786
|
-
return this.borrowDataFromRecords(reserve, e.borrow);
|
|
2202
|
+
return this.borrowDataFromRecords(reserve, e.borrow, now);
|
|
1787
2203
|
}
|
|
1788
2204
|
async getUserMultipleSupplyData(user, tokens) {
|
|
1789
|
-
const reserves = await this.loadReserves(tokens);
|
|
2205
|
+
const [reserves, now] = await Promise.all([this.loadReserves(tokens), this.ctx.now()]);
|
|
1790
2206
|
const entries = await this.loadUserEntries(reserves.map((reserve) => ({
|
|
1791
2207
|
holder: user,
|
|
1792
2208
|
reserve
|
|
1793
2209
|
})));
|
|
1794
|
-
const now = this.ctx.now();
|
|
1795
2210
|
const out = reserves.map((r, i) => this.supplyDataFromRecords(r, entries[i].supply, now));
|
|
1796
2211
|
return {
|
|
1797
2212
|
userSuppliesData: out.map((o) => o.userSupplyData),
|
|
@@ -1799,12 +2214,11 @@ var Liquidity = class {
|
|
|
1799
2214
|
};
|
|
1800
2215
|
}
|
|
1801
2216
|
async getUserMultipleBorrowData(user, tokens) {
|
|
1802
|
-
const reserves = await this.loadReserves(tokens);
|
|
2217
|
+
const [reserves, now] = await Promise.all([this.loadReserves(tokens), this.ctx.now()]);
|
|
1803
2218
|
const entries = await this.loadUserEntries(reserves.map((reserve) => ({
|
|
1804
2219
|
holder: user,
|
|
1805
2220
|
reserve
|
|
1806
2221
|
})));
|
|
1807
|
-
const now = this.ctx.now();
|
|
1808
2222
|
const out = reserves.map((r, i) => this.borrowDataFromRecords(r, entries[i].borrow, now));
|
|
1809
2223
|
return {
|
|
1810
2224
|
userBorrowingsData: out.map((o) => o.userBorrowData),
|
|
@@ -1922,13 +2336,11 @@ function pick(o, keys) {
|
|
|
1922
2336
|
* returns `null` for a side whose view aborts, with the abort in `operateError` / `liquidateError`.
|
|
1923
2337
|
* `getOracleRoutes` reads each route on its own (RedStone via the adapter's view, Pyth by decoding
|
|
1924
2338
|
* 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
2339
|
*/
|
|
1928
2340
|
/** Oracle rates are debt-per-collateral in the vault's internal 1e9 space, scaled by 1e15. */
|
|
1929
2341
|
const ORACLE_RATE_PRECISION = 1000000000000000n;
|
|
1930
2342
|
/** Mirrors `fluid_oracle::constants` of the deployed v2 package. */
|
|
1931
|
-
const
|
|
2343
|
+
const ORACLE_LIMITS = {
|
|
1932
2344
|
rateOutputDecimals: 15,
|
|
1933
2345
|
maxAgeOperateS: 600n,
|
|
1934
2346
|
maxAgeLiquidateS: 7200n,
|
|
@@ -1951,9 +2363,9 @@ function toSource(s) {
|
|
|
1951
2363
|
}
|
|
1952
2364
|
/** `rate::scale_and_invert`: raw provider price with `decimals` → 1e15 rate, optionally inverted. */
|
|
1953
2365
|
function scaleAndInvert(raw, decimals, invert) {
|
|
1954
|
-
const out =
|
|
2366
|
+
const out = ORACLE_LIMITS.rateOutputDecimals;
|
|
1955
2367
|
let rate = decimals > out ? raw / 10n ** BigInt(decimals - out) : raw * 10n ** BigInt(out - decimals);
|
|
1956
|
-
if (rate > 0n && invert) rate = rate >
|
|
2368
|
+
if (rate > 0n && invert) rate = rate > ORACLE_LIMITS.maxInvertibleRate ? 0n : ORACLE_RATE_PRECISION * ORACLE_RATE_PRECISION / rate;
|
|
1957
2369
|
return rate;
|
|
1958
2370
|
}
|
|
1959
2371
|
function abortText(e) {
|
|
@@ -1968,26 +2380,15 @@ var Oracle = class {
|
|
|
1968
2380
|
constructor(ctx) {
|
|
1969
2381
|
this.ctx = ctx;
|
|
1970
2382
|
}
|
|
1971
|
-
get
|
|
1972
|
-
const c = this.ctx.config.
|
|
1973
|
-
if (
|
|
2383
|
+
get providers() {
|
|
2384
|
+
const c = this.ctx.config.oracle;
|
|
2385
|
+
if (!c) throw new Error("config has no oracle (config.oracle)");
|
|
1974
2386
|
return c;
|
|
1975
2387
|
}
|
|
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
2388
|
/** v2 oracle object: nonce + main / guard routes. */
|
|
1988
|
-
async
|
|
2389
|
+
async getOracle(oracleId) {
|
|
1989
2390
|
const id = normalizeSuiObjectId(oracleId);
|
|
1990
|
-
const o =
|
|
2391
|
+
const o = OracleObject.parse((await fetchObject(this.ctx.client, id, "Oracle")).content);
|
|
1991
2392
|
return {
|
|
1992
2393
|
id,
|
|
1993
2394
|
nonce: o.nonce,
|
|
@@ -1997,18 +2398,18 @@ var Oracle = class {
|
|
|
1997
2398
|
}
|
|
1998
2399
|
/** Provider objects the v2 views take for this oracle. */
|
|
1999
2400
|
providerArgs(o) {
|
|
2000
|
-
const cfg = this.
|
|
2401
|
+
const cfg = this.providers;
|
|
2001
2402
|
const adapter = [o.main, o.guard].find((s) => s?.provider === "redstone")?.adapterId ?? cfg.redstoneAdapter;
|
|
2002
2403
|
const pythFeed = [o.main, o.guard].find((s) => s?.provider === "pyth")?.feedId;
|
|
2003
2404
|
const byFeed = Object.fromEntries(Object.entries(cfg.pythPriceInfoObjects).map(([k, v]) => [k.toLowerCase(), v]));
|
|
2004
2405
|
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.
|
|
2406
|
+
if (!pythInfo) throw new Error(`no Pyth PriceInfoObject configured for feed ${pythFeed ?? "(any)"} (config.oracle.pythPriceInfoObjects)`);
|
|
2006
2407
|
return {
|
|
2007
2408
|
adapter: normalizeSuiObjectId(adapter),
|
|
2008
2409
|
pythInfo: normalizeSuiObjectId(pythInfo)
|
|
2009
2410
|
};
|
|
2010
2411
|
}
|
|
2011
|
-
async
|
|
2412
|
+
async quote(o, fn) {
|
|
2012
2413
|
const pkg = this.ctx.config.packages.oracle.packageId;
|
|
2013
2414
|
const { adapter, pythInfo } = this.providerArgs(o);
|
|
2014
2415
|
const res = await simulateView(this.ctx.client, (tx) => {
|
|
@@ -2022,7 +2423,7 @@ var Oracle = class {
|
|
|
2022
2423
|
]
|
|
2023
2424
|
});
|
|
2024
2425
|
});
|
|
2025
|
-
const q =
|
|
2426
|
+
const q = PriceQuote.parse(res[0][0]);
|
|
2026
2427
|
const r = fn === "get_exchange_rate_operate" ? q.rate_operate : q.rate_liquidate;
|
|
2027
2428
|
if (r == null) throw new Error(`${fn} returned no rate`);
|
|
2028
2429
|
return BigInt(r);
|
|
@@ -2030,29 +2431,12 @@ var Oracle = class {
|
|
|
2030
2431
|
/** Price exactly as vaults see it. v2: operate and liquidate simulated separately (one can abort alone). */
|
|
2031
2432
|
async getOraclePrice(oracleId) {
|
|
2032
2433
|
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")]);
|
|
2434
|
+
const o = await this.getOracle(id);
|
|
2435
|
+
const [op, liq] = await Promise.allSettled([this.quote(o, "get_exchange_rate_operate"), this.quote(o, "get_exchange_rate_liquidate")]);
|
|
2051
2436
|
return {
|
|
2052
2437
|
oracle: id,
|
|
2053
2438
|
operate: op.status === "fulfilled" ? op.value : null,
|
|
2054
2439
|
liquidate: liq.status === "fulfilled" ? liq.value : null,
|
|
2055
|
-
source: "v2",
|
|
2056
2440
|
...op.status === "rejected" ? { operateError: abortText(op.reason) } : {},
|
|
2057
2441
|
...liq.status === "rejected" ? { liquidateError: abortText(liq.reason) } : {}
|
|
2058
2442
|
};
|
|
@@ -2063,8 +2447,8 @@ var Oracle = class {
|
|
|
2063
2447
|
* Operate price = min over routes; liquidate = min over routes usable for liquidate.
|
|
2064
2448
|
*/
|
|
2065
2449
|
async getOracleRoutes(oracleId) {
|
|
2066
|
-
const o = await this.
|
|
2067
|
-
const cfg = this.
|
|
2450
|
+
const o = await this.getOracle(oracleId);
|
|
2451
|
+
const cfg = this.providers;
|
|
2068
2452
|
const { pythInfo } = this.providerArgs(o);
|
|
2069
2453
|
const redstone = [o.main, o.guard].filter((s) => s?.provider === "redstone");
|
|
2070
2454
|
const res = await simulateView(this.ctx.client, (tx) => {
|
|
@@ -2089,7 +2473,7 @@ var Oracle = class {
|
|
|
2089
2473
|
const pyth = o.main.provider === "pyth" || o.guard?.provider === "pyth" ? PythPriceInfoObject.parse((await fetchObject(this.ctx.client, pythInfo, "PriceInfoObject")).content) : null;
|
|
2090
2474
|
let rsIdx = 0;
|
|
2091
2475
|
const route = (s) => {
|
|
2092
|
-
const L =
|
|
2476
|
+
const L = ORACLE_LIMITS;
|
|
2093
2477
|
if (s.provider === "redstone") {
|
|
2094
2478
|
const k = 1 + 2 * rsIdx++;
|
|
2095
2479
|
const value = BigInt(bcs.u256().parse(res[k][0]));
|
|
@@ -2632,7 +3016,6 @@ var Vault = class {
|
|
|
2632
3016
|
liquidationPenalty: r.risk.liquidation_penalty,
|
|
2633
3017
|
borrowFee: r.risk.borrow_fee,
|
|
2634
3018
|
oracle: r.oracle,
|
|
2635
|
-
rebalancer: "",
|
|
2636
3019
|
liquidityProgram: r.liquidity_program,
|
|
2637
3020
|
oracleProgram: this.ctx.config.packages.oracle.packageId,
|
|
2638
3021
|
supplyToken: v.collateralType,
|
|
@@ -2729,10 +3112,10 @@ var Vault = class {
|
|
|
2729
3112
|
return this.stateFromRecord(v, branch ?? null, counts ?? null);
|
|
2730
3113
|
}
|
|
2731
3114
|
/** `user::get_exchange_prices`: (liqSupply, liqBorrow, vaultSupply, vaultBorrow) accrued to `now`. */
|
|
2732
|
-
exchangePricesFromRecords(v, colReserve, debtReserve, now
|
|
3115
|
+
exchangePricesFromRecords(v, colReserve, debtReserve, now) {
|
|
2733
3116
|
const r = v.raw;
|
|
2734
|
-
const liqSupply = calculateExchangePrice(colReserve.snapshot, now).supplyExchangePrice;
|
|
2735
|
-
const liqBorrow = calculateExchangePrice(debtReserve.snapshot, now).borrowExchangePrice;
|
|
3117
|
+
const liqSupply = calculateExchangePrice(colReserve.snapshot, now, this.ctx.math).supplyExchangePrice;
|
|
3118
|
+
const liqBorrow = calculateExchangePrice(debtReserve.snapshot, now, this.ctx.math).borrowExchangePrice;
|
|
2736
3119
|
const last = BigInt(r.last_update_timestamp);
|
|
2737
3120
|
const vp = loadVaultExchangePrices({
|
|
2738
3121
|
vaultSupplyExPrice: BigInt(r.vault_supply_exchange_price),
|
|
@@ -2781,7 +3164,7 @@ var Vault = class {
|
|
|
2781
3164
|
};
|
|
2782
3165
|
}
|
|
2783
3166
|
async entireDataFor(vaults, opts) {
|
|
2784
|
-
const
|
|
3167
|
+
const nowP = this.ctx.now();
|
|
2785
3168
|
const tokens = [...new Set(vaults.flatMap((v) => [v.collateralType, v.debtType]))];
|
|
2786
3169
|
const reserves = new Map((await this.liquidity.loadReserves(tokens)).map((r) => [r.token, r]));
|
|
2787
3170
|
const pairs = vaults.flatMap((v) => [{
|
|
@@ -2797,6 +3180,7 @@ var Vault = class {
|
|
|
2797
3180
|
opts.withOracle === false ? Promise.resolve(vaults.map(() => null)) : Promise.all(vaults.map((v) => this.oracle.getOraclePrice(v.raw.oracle).catch(() => null))),
|
|
2798
3181
|
this.countsOrNull(vaults, opts.positionCounts)
|
|
2799
3182
|
]);
|
|
3183
|
+
const now = await nowP;
|
|
2800
3184
|
return vaults.map((v, i) => {
|
|
2801
3185
|
const col = reserves.get(v.collateralType);
|
|
2802
3186
|
const debt = reserves.get(v.debtType);
|
|
@@ -2868,7 +3252,6 @@ var Vault = class {
|
|
|
2868
3252
|
liquidationPenalty: BigInt(cfg.liquidationPenalty),
|
|
2869
3253
|
borrowFee: BigInt(cfg.borrowFee),
|
|
2870
3254
|
oracle: cfg.oracle,
|
|
2871
|
-
rebalancer: "",
|
|
2872
3255
|
...price?.operate != null ? { oraclePriceOperate: price.operate } : {},
|
|
2873
3256
|
...price?.liquidate != null ? { oraclePriceLiquidate: price.liquidate } : {},
|
|
2874
3257
|
lastUpdateTimestamp: state.lastUpdateTimestamp
|
|
@@ -3133,34 +3516,59 @@ const DEFAULT_RPC_URL = "https://fullnode.mainnet.sui.io:443";
|
|
|
3133
3516
|
* `client.liquidity`, `client.lending`, `client.vault` (+ `client.oracle`, Sui-only).
|
|
3134
3517
|
*
|
|
3135
3518
|
* @example
|
|
3136
|
-
* const client = new Client();
|
|
3137
|
-
* const client = new Client("https://my-fullnode:443");
|
|
3519
|
+
* const client = new Client(undefined, { network: "mainnet" }); // production deployment, Sui mainnet gRPC
|
|
3520
|
+
* const client = new Client("https://my-fullnode:443", { network: customConfig });
|
|
3138
3521
|
* const client = new Client(existingSuiGrpcClient, { network: "staging" });
|
|
3139
3522
|
* const vault = await client.vault.getVaultEntireData(1);
|
|
3140
3523
|
*/
|
|
3141
|
-
var Client = class {
|
|
3524
|
+
var Client = class Client {
|
|
3142
3525
|
liquidity;
|
|
3143
3526
|
lending;
|
|
3144
3527
|
vault;
|
|
3145
3528
|
oracle;
|
|
3146
3529
|
context;
|
|
3147
|
-
constructor(rpc, opts
|
|
3148
|
-
const config = resolveConfig(opts
|
|
3530
|
+
constructor(rpc, opts) {
|
|
3531
|
+
const config = resolveConfig(opts?.network);
|
|
3149
3532
|
this.context = makeContext(createFailoverClient(rpc === void 0 ? config.rpcUrls ?? [DEFAULT_RPC_URL] : typeof rpc === "string" ? [rpc] : Array.isArray(rpc) ? rpc : [rpc], {
|
|
3150
3533
|
network: config.network,
|
|
3151
|
-
timeoutMs: opts.timeoutMs
|
|
3534
|
+
timeoutMs: opts.timeoutMs,
|
|
3535
|
+
...opts.retry
|
|
3152
3536
|
}), config, createDerive(config), opts.now, opts.discovery ?? "state");
|
|
3153
3537
|
this.liquidity = new Liquidity(this.context);
|
|
3154
3538
|
this.oracle = new Oracle(this.context);
|
|
3155
3539
|
this.lending = new Lending(this.context, this.liquidity);
|
|
3156
3540
|
this.vault = new Vault(this.context, this.liquidity, this.oracle);
|
|
3157
3541
|
}
|
|
3542
|
+
/**
|
|
3543
|
+
* The same reader pinned to a past `checkpoint`: every object read comes from the GraphQL service
|
|
3544
|
+
* (`config.graphqlUrl`) at that checkpoint and the clock is the checkpoint's timestamp, so
|
|
3545
|
+
* `lending.getLendingStateData()`, rates, reserves and vault state are what the chain held then.
|
|
3546
|
+
* Move views / oracle quotes / balances are not available historically (`CheckpointReadError`).
|
|
3547
|
+
* Needs `discovery: "indexer"` (it is a GraphQL read).
|
|
3548
|
+
*
|
|
3549
|
+
* @example
|
|
3550
|
+
* const past = await client.atCheckpoint(330_000_000);
|
|
3551
|
+
* const state = await past.lending.getLendingStateData();
|
|
3552
|
+
*/
|
|
3553
|
+
async atCheckpoint(checkpoint) {
|
|
3554
|
+
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");
|
|
3555
|
+
const url = this.context.config.graphqlUrl;
|
|
3556
|
+
if (!url) throw new Error("config.graphqlUrl not set (needed for checkpoint reads)");
|
|
3557
|
+
const cp = BigInt(checkpoint);
|
|
3558
|
+
const ts = await checkpointTimestampSeconds(url, cp);
|
|
3559
|
+
return new Client(checkpointClient(url, cp, this.context.client), {
|
|
3560
|
+
network: this.context.config,
|
|
3561
|
+
now: () => ts,
|
|
3562
|
+
discovery: "state",
|
|
3563
|
+
retry: { rounds: 1 }
|
|
3564
|
+
});
|
|
3565
|
+
}
|
|
3158
3566
|
};
|
|
3159
3567
|
/**
|
|
3160
3568
|
* `$extend` form, same pattern as the write SDK's `fluid()`:
|
|
3161
3569
|
* `const c = new SuiGrpcClient({...}).$extend(fluidRead()); c.fluidRead.vault...`
|
|
3162
3570
|
*/
|
|
3163
|
-
function fluidRead(opts
|
|
3571
|
+
function fluidRead(opts) {
|
|
3164
3572
|
return {
|
|
3165
3573
|
name: "fluidRead",
|
|
3166
3574
|
register: (client) => new Client(client, opts)
|
|
@@ -3168,4 +3576,4 @@ function fluidRead(opts = {}) {
|
|
|
3168
3576
|
}
|
|
3169
3577
|
|
|
3170
3578
|
//#endregion
|
|
3171
|
-
export { AllEndpointsFailedError, CONFIGS,
|
|
3579
|
+
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, PROD, 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 };
|