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