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