@t2000/sdk 0.14.1 → 0.15.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 +10 -2
- package/dist/adapters/index.d.cts +1 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/{index-B14ZyQZt.d.cts → index-BykavuDO.d.cts} +14 -1
- package/dist/{index-B14ZyQZt.d.ts → index-BykavuDO.d.ts} +14 -1
- package/dist/index.d.cts +19 -6
- package/dist/index.d.ts +19 -6
- package/package.json +1 -1
- package/dist/adapters/index.cjs +0 -1742
- package/dist/adapters/index.cjs.map +0 -1
- package/dist/adapters/index.js +0 -1732
- package/dist/adapters/index.js.map +0 -1
- package/dist/index.cjs +0 -4463
- package/dist/index.cjs.map +0 -1
- package/dist/index.js +0 -4394
- package/dist/index.js.map +0 -1
package/dist/adapters/index.cjs
DELETED
|
@@ -1,1742 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var transactions = require('@mysten/sui/transactions');
|
|
4
|
-
var bcs = require('@mysten/sui/bcs');
|
|
5
|
-
var aggregatorSdk = require('@cetusprotocol/aggregator-sdk');
|
|
6
|
-
var utils = require('@mysten/sui/utils');
|
|
7
|
-
|
|
8
|
-
// src/constants.ts
|
|
9
|
-
var SAVE_FEE_BPS = 10n;
|
|
10
|
-
var SWAP_FEE_BPS = 0n;
|
|
11
|
-
var BORROW_FEE_BPS = 5n;
|
|
12
|
-
var SUPPORTED_ASSETS = {
|
|
13
|
-
USDC: {
|
|
14
|
-
type: "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC",
|
|
15
|
-
decimals: 6,
|
|
16
|
-
symbol: "USDC",
|
|
17
|
-
displayName: "USDC"
|
|
18
|
-
},
|
|
19
|
-
USDT: {
|
|
20
|
-
type: "0x375f70cf2ae4c00bf37117d0c85a2c71545e6ee05c4a5c7d282cd66a4504b068::usdt::USDT",
|
|
21
|
-
decimals: 6,
|
|
22
|
-
symbol: "USDT",
|
|
23
|
-
displayName: "suiUSDT"
|
|
24
|
-
},
|
|
25
|
-
USDe: {
|
|
26
|
-
type: "0x41d587e5336f1c86cad50d38a7136db99333bb9bda91cea4ba69115defeb1402::sui_usde::SUI_USDE",
|
|
27
|
-
decimals: 6,
|
|
28
|
-
symbol: "USDe",
|
|
29
|
-
displayName: "suiUSDe"
|
|
30
|
-
},
|
|
31
|
-
USDsui: {
|
|
32
|
-
type: "0x44f838219cf67b058f3b37907b655f226153c18e33dfcd0da559a844fea9b1c1::usdsui::USDSUI",
|
|
33
|
-
decimals: 6,
|
|
34
|
-
symbol: "USDsui",
|
|
35
|
-
displayName: "USDsui"
|
|
36
|
-
},
|
|
37
|
-
SUI: {
|
|
38
|
-
type: "0x2::sui::SUI",
|
|
39
|
-
decimals: 9,
|
|
40
|
-
symbol: "SUI",
|
|
41
|
-
displayName: "SUI"
|
|
42
|
-
},
|
|
43
|
-
BTC: {
|
|
44
|
-
type: "0xaafb102dd0902f5055cadecd687fb5b71ca82ef0e0285d90afde828ec58ca96b::btc::BTC",
|
|
45
|
-
decimals: 8,
|
|
46
|
-
symbol: "BTC",
|
|
47
|
-
displayName: "Bitcoin"
|
|
48
|
-
},
|
|
49
|
-
ETH: {
|
|
50
|
-
type: "0xd0e89b2af5e4910726fbcd8b8dd37bb79b29e5f83f7491bca830e94f7f226d29::eth::ETH",
|
|
51
|
-
decimals: 8,
|
|
52
|
-
symbol: "ETH",
|
|
53
|
-
displayName: "Ethereum"
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
var STABLE_ASSETS = ["USDC", "USDT", "USDe", "USDsui"];
|
|
57
|
-
var T2000_PACKAGE_ID = process.env.T2000_PACKAGE_ID ?? "0xab92e9f1fe549ad3d6a52924a73181b45791e76120b975138fac9ec9b75db9f3";
|
|
58
|
-
var T2000_CONFIG_ID = process.env.T2000_CONFIG_ID ?? "0x408add9aa9322f93cfd87523d8f603006eb8713894f4c460283c58a6888dae8a";
|
|
59
|
-
var T2000_TREASURY_ID = process.env.T2000_TREASURY_ID ?? "0x3bb501b8300125dca59019247941a42af6b292a150ce3cfcce9449456be2ec91";
|
|
60
|
-
process.env.T2000_API_URL ?? "https://api.t2000.ai";
|
|
61
|
-
var CETUS_USDC_SUI_POOL = "0x51e883ba7c0b566a26cbc8a94cd33eb0abd418a77cc1e60ad22fd9b1f29cd2ab";
|
|
62
|
-
var CETUS_PACKAGE = "0x1eabed72c53feb3805120a081dc15963c204dc8d091542592abaf7a35689b2fb";
|
|
63
|
-
var INVESTMENT_ASSETS = {
|
|
64
|
-
SUI: SUPPORTED_ASSETS.SUI,
|
|
65
|
-
BTC: SUPPORTED_ASSETS.BTC,
|
|
66
|
-
ETH: SUPPORTED_ASSETS.ETH
|
|
67
|
-
};
|
|
68
|
-
var SENTINEL = {
|
|
69
|
-
PACKAGE: "0x88b83f36dafcd5f6dcdcf1d2cb5889b03f61264ab3cee9cae35db7aa940a21b7"};
|
|
70
|
-
|
|
71
|
-
// src/errors.ts
|
|
72
|
-
var T2000Error = class extends Error {
|
|
73
|
-
code;
|
|
74
|
-
data;
|
|
75
|
-
retryable;
|
|
76
|
-
constructor(code, message, data, retryable = false) {
|
|
77
|
-
super(message);
|
|
78
|
-
this.name = "T2000Error";
|
|
79
|
-
this.code = code;
|
|
80
|
-
this.data = data;
|
|
81
|
-
this.retryable = retryable;
|
|
82
|
-
}
|
|
83
|
-
toJSON() {
|
|
84
|
-
return {
|
|
85
|
-
error: this.code,
|
|
86
|
-
message: this.message,
|
|
87
|
-
...this.data && { data: this.data },
|
|
88
|
-
retryable: this.retryable
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
// src/adapters/registry.ts
|
|
94
|
-
var ProtocolRegistry = class {
|
|
95
|
-
lending = /* @__PURE__ */ new Map();
|
|
96
|
-
swap = /* @__PURE__ */ new Map();
|
|
97
|
-
registerLending(adapter) {
|
|
98
|
-
this.lending.set(adapter.id, adapter);
|
|
99
|
-
}
|
|
100
|
-
registerSwap(adapter) {
|
|
101
|
-
this.swap.set(adapter.id, adapter);
|
|
102
|
-
}
|
|
103
|
-
async bestSaveRate(asset) {
|
|
104
|
-
const candidates = [];
|
|
105
|
-
for (const adapter of this.lending.values()) {
|
|
106
|
-
if (!adapter.supportedAssets.includes(asset)) continue;
|
|
107
|
-
if (!adapter.capabilities.includes("save")) continue;
|
|
108
|
-
try {
|
|
109
|
-
const rate = await adapter.getRates(asset);
|
|
110
|
-
candidates.push({ adapter, rate });
|
|
111
|
-
} catch {
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
if (candidates.length === 0) {
|
|
115
|
-
throw new T2000Error("ASSET_NOT_SUPPORTED", `No lending adapter supports saving ${asset}`);
|
|
116
|
-
}
|
|
117
|
-
candidates.sort((a, b) => b.rate.saveApy - a.rate.saveApy);
|
|
118
|
-
return candidates[0];
|
|
119
|
-
}
|
|
120
|
-
async bestBorrowRate(asset, opts) {
|
|
121
|
-
const candidates = [];
|
|
122
|
-
for (const adapter of this.lending.values()) {
|
|
123
|
-
if (!adapter.supportedAssets.includes(asset)) continue;
|
|
124
|
-
if (!adapter.capabilities.includes("borrow")) continue;
|
|
125
|
-
if (opts?.requireSameAssetBorrow && !adapter.supportsSameAssetBorrow) continue;
|
|
126
|
-
try {
|
|
127
|
-
const rate = await adapter.getRates(asset);
|
|
128
|
-
candidates.push({ adapter, rate });
|
|
129
|
-
} catch {
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
if (candidates.length === 0) {
|
|
133
|
-
throw new T2000Error("ASSET_NOT_SUPPORTED", `No lending adapter supports borrowing ${asset}`);
|
|
134
|
-
}
|
|
135
|
-
candidates.sort((a, b) => a.rate.borrowApy - b.rate.borrowApy);
|
|
136
|
-
return candidates[0];
|
|
137
|
-
}
|
|
138
|
-
async bestSwapQuote(from, to, amount) {
|
|
139
|
-
const candidates = [];
|
|
140
|
-
for (const adapter of this.swap.values()) {
|
|
141
|
-
const pairs = adapter.getSupportedPairs();
|
|
142
|
-
if (!pairs.some((p) => p.from === from && p.to === to)) continue;
|
|
143
|
-
try {
|
|
144
|
-
const quote = await adapter.getQuote(from, to, amount);
|
|
145
|
-
candidates.push({ adapter, quote });
|
|
146
|
-
} catch {
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
if (candidates.length === 0) {
|
|
150
|
-
throw new T2000Error("ASSET_NOT_SUPPORTED", `No swap adapter supports ${from} \u2192 ${to}`);
|
|
151
|
-
}
|
|
152
|
-
candidates.sort((a, b) => b.quote.expectedOutput - a.quote.expectedOutput);
|
|
153
|
-
return candidates[0];
|
|
154
|
-
}
|
|
155
|
-
async bestSaveRateAcrossAssets() {
|
|
156
|
-
const candidates = [];
|
|
157
|
-
for (const asset of STABLE_ASSETS) {
|
|
158
|
-
for (const adapter of this.lending.values()) {
|
|
159
|
-
if (!adapter.supportedAssets.includes(asset)) continue;
|
|
160
|
-
if (!adapter.capabilities.includes("save")) continue;
|
|
161
|
-
try {
|
|
162
|
-
const rate = await adapter.getRates(asset);
|
|
163
|
-
candidates.push({ adapter, rate, asset });
|
|
164
|
-
} catch {
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
if (candidates.length === 0) {
|
|
169
|
-
throw new T2000Error("ASSET_NOT_SUPPORTED", "No lending adapter found for any stablecoin");
|
|
170
|
-
}
|
|
171
|
-
candidates.sort((a, b) => b.rate.saveApy - a.rate.saveApy);
|
|
172
|
-
return candidates[0];
|
|
173
|
-
}
|
|
174
|
-
async allRatesAcrossAssets() {
|
|
175
|
-
const results = [];
|
|
176
|
-
for (const asset of STABLE_ASSETS) {
|
|
177
|
-
for (const adapter of this.lending.values()) {
|
|
178
|
-
if (!adapter.supportedAssets.includes(asset)) continue;
|
|
179
|
-
try {
|
|
180
|
-
const rates = await adapter.getRates(asset);
|
|
181
|
-
if (rates.saveApy > 0 || rates.borrowApy > 0) {
|
|
182
|
-
results.push({ protocol: adapter.name, protocolId: adapter.id, asset, rates });
|
|
183
|
-
}
|
|
184
|
-
} catch {
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
return results;
|
|
189
|
-
}
|
|
190
|
-
async allRates(asset) {
|
|
191
|
-
const results = [];
|
|
192
|
-
for (const adapter of this.lending.values()) {
|
|
193
|
-
if (!adapter.supportedAssets.includes(asset)) continue;
|
|
194
|
-
try {
|
|
195
|
-
const rates = await adapter.getRates(asset);
|
|
196
|
-
results.push({ protocol: adapter.name, protocolId: adapter.id, rates });
|
|
197
|
-
} catch {
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
return results;
|
|
201
|
-
}
|
|
202
|
-
async allPositions(address) {
|
|
203
|
-
const results = [];
|
|
204
|
-
for (const adapter of this.lending.values()) {
|
|
205
|
-
try {
|
|
206
|
-
const positions = await adapter.getPositions(address);
|
|
207
|
-
if (positions.supplies.length > 0 || positions.borrows.length > 0) {
|
|
208
|
-
results.push({ protocol: adapter.name, protocolId: adapter.id, positions });
|
|
209
|
-
}
|
|
210
|
-
} catch {
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
return results;
|
|
214
|
-
}
|
|
215
|
-
getLending(id) {
|
|
216
|
-
return this.lending.get(id);
|
|
217
|
-
}
|
|
218
|
-
getSwap(id) {
|
|
219
|
-
return this.swap.get(id);
|
|
220
|
-
}
|
|
221
|
-
listLending() {
|
|
222
|
-
return [...this.lending.values()];
|
|
223
|
-
}
|
|
224
|
-
listSwap() {
|
|
225
|
-
return [...this.swap.values()];
|
|
226
|
-
}
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
// src/utils/format.ts
|
|
230
|
-
function stableToRaw(amount, decimals) {
|
|
231
|
-
return BigInt(Math.round(amount * 10 ** decimals));
|
|
232
|
-
}
|
|
233
|
-
var ASSET_LOOKUP = /* @__PURE__ */ new Map();
|
|
234
|
-
for (const [key, info] of Object.entries(SUPPORTED_ASSETS)) {
|
|
235
|
-
ASSET_LOOKUP.set(key.toUpperCase(), key);
|
|
236
|
-
if (info.displayName && info.displayName.toUpperCase() !== key.toUpperCase()) {
|
|
237
|
-
ASSET_LOOKUP.set(info.displayName.toUpperCase(), key);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
function normalizeAsset(input) {
|
|
241
|
-
return ASSET_LOOKUP.get(input.toUpperCase()) ?? input;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// src/protocols/protocolFee.ts
|
|
245
|
-
var FEE_RATES = {
|
|
246
|
-
save: SAVE_FEE_BPS,
|
|
247
|
-
swap: SWAP_FEE_BPS,
|
|
248
|
-
borrow: BORROW_FEE_BPS
|
|
249
|
-
};
|
|
250
|
-
var OP_CODES = {
|
|
251
|
-
save: 0,
|
|
252
|
-
swap: 1,
|
|
253
|
-
borrow: 2
|
|
254
|
-
};
|
|
255
|
-
function addCollectFeeToTx(tx, paymentCoin, operation) {
|
|
256
|
-
const bps = FEE_RATES[operation];
|
|
257
|
-
if (bps <= 0n) return;
|
|
258
|
-
tx.moveCall({
|
|
259
|
-
target: `${T2000_PACKAGE_ID}::treasury::collect_fee`,
|
|
260
|
-
typeArguments: [SUPPORTED_ASSETS.USDC.type],
|
|
261
|
-
arguments: [
|
|
262
|
-
tx.object(T2000_TREASURY_ID),
|
|
263
|
-
tx.object(T2000_CONFIG_ID),
|
|
264
|
-
paymentCoin,
|
|
265
|
-
tx.pure.u8(OP_CODES[operation])
|
|
266
|
-
]
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
var RATE_DECIMALS = 27;
|
|
270
|
-
var LTV_DECIMALS = 27;
|
|
271
|
-
var MIN_HEALTH_FACTOR = 1.5;
|
|
272
|
-
var WITHDRAW_DUST_BUFFER = 1e-3;
|
|
273
|
-
var CLOCK = "0x06";
|
|
274
|
-
var SUI_SYSTEM_STATE = "0x05";
|
|
275
|
-
var NAVI_BALANCE_DECIMALS = 9;
|
|
276
|
-
var CONFIG_API = "https://open-api.naviprotocol.io/api/navi/config?env=prod";
|
|
277
|
-
var POOLS_API = "https://open-api.naviprotocol.io/api/navi/pools?env=prod";
|
|
278
|
-
var PACKAGE_API = "https://open-api.naviprotocol.io/api/package";
|
|
279
|
-
var packageCache = null;
|
|
280
|
-
function toBigInt(v) {
|
|
281
|
-
if (typeof v === "bigint") return v;
|
|
282
|
-
return BigInt(String(v));
|
|
283
|
-
}
|
|
284
|
-
var UserStateInfo = bcs.bcs.struct("UserStateInfo", {
|
|
285
|
-
asset_id: bcs.bcs.u8(),
|
|
286
|
-
borrow_balance: bcs.bcs.u256(),
|
|
287
|
-
supply_balance: bcs.bcs.u256()
|
|
288
|
-
});
|
|
289
|
-
function decodeDevInspect(result, schema) {
|
|
290
|
-
const rv = result.results?.[0]?.returnValues?.[0];
|
|
291
|
-
if (result.error || !rv) return void 0;
|
|
292
|
-
const bytes = Uint8Array.from(rv[0]);
|
|
293
|
-
return schema.parse(bytes);
|
|
294
|
-
}
|
|
295
|
-
var configCache = null;
|
|
296
|
-
var poolsCache = null;
|
|
297
|
-
var CACHE_TTL = 5 * 6e4;
|
|
298
|
-
async function fetchJson(url) {
|
|
299
|
-
const res = await fetch(url);
|
|
300
|
-
if (!res.ok) throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI API error: ${res.status}`);
|
|
301
|
-
const json = await res.json();
|
|
302
|
-
return json.data ?? json;
|
|
303
|
-
}
|
|
304
|
-
async function getLatestPackageId() {
|
|
305
|
-
if (packageCache && Date.now() - packageCache.ts < CACHE_TTL) return packageCache.id;
|
|
306
|
-
const res = await fetch(PACKAGE_API);
|
|
307
|
-
if (!res.ok) throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI package API error: ${res.status}`);
|
|
308
|
-
const json = await res.json();
|
|
309
|
-
if (!json.packageId) throw new T2000Error("PROTOCOL_UNAVAILABLE", "NAVI package API returned no packageId");
|
|
310
|
-
packageCache = { id: json.packageId, ts: Date.now() };
|
|
311
|
-
return json.packageId;
|
|
312
|
-
}
|
|
313
|
-
async function getConfig(fresh = false) {
|
|
314
|
-
if (configCache && !fresh && Date.now() - configCache.ts < CACHE_TTL) return configCache.data;
|
|
315
|
-
const [data, latestPkg] = await Promise.all([
|
|
316
|
-
fetchJson(CONFIG_API),
|
|
317
|
-
getLatestPackageId()
|
|
318
|
-
]);
|
|
319
|
-
data.package = latestPkg;
|
|
320
|
-
configCache = { data, ts: Date.now() };
|
|
321
|
-
return data;
|
|
322
|
-
}
|
|
323
|
-
async function getPools(fresh = false) {
|
|
324
|
-
if (poolsCache && !fresh && Date.now() - poolsCache.ts < CACHE_TTL) return poolsCache.data;
|
|
325
|
-
const data = await fetchJson(POOLS_API);
|
|
326
|
-
poolsCache = { data, ts: Date.now() };
|
|
327
|
-
return data;
|
|
328
|
-
}
|
|
329
|
-
function matchesCoinType(poolType, targetType) {
|
|
330
|
-
const poolSuffix = poolType.split("::").slice(1).join("::").toLowerCase();
|
|
331
|
-
const targetSuffix = targetType.split("::").slice(1).join("::").toLowerCase();
|
|
332
|
-
return poolSuffix === targetSuffix;
|
|
333
|
-
}
|
|
334
|
-
function resolvePoolSymbol(pool) {
|
|
335
|
-
const coinType = pool.suiCoinType || pool.coinType || "";
|
|
336
|
-
for (const [key, info] of Object.entries(SUPPORTED_ASSETS)) {
|
|
337
|
-
if (matchesCoinType(coinType, info.type)) return key;
|
|
338
|
-
}
|
|
339
|
-
return pool.token?.symbol ?? "UNKNOWN";
|
|
340
|
-
}
|
|
341
|
-
async function getPool(asset = "USDC") {
|
|
342
|
-
const pools = await getPools();
|
|
343
|
-
const targetType = SUPPORTED_ASSETS[asset].type;
|
|
344
|
-
const pool = pools.find(
|
|
345
|
-
(p) => matchesCoinType(p.suiCoinType || p.coinType || "", targetType)
|
|
346
|
-
);
|
|
347
|
-
if (!pool) {
|
|
348
|
-
throw new T2000Error(
|
|
349
|
-
"ASSET_NOT_SUPPORTED",
|
|
350
|
-
`${SUPPORTED_ASSETS[asset].displayName} pool not found on NAVI. Try: ${STABLE_ASSETS.filter((a) => a !== asset).join(", ")}`
|
|
351
|
-
);
|
|
352
|
-
}
|
|
353
|
-
return pool;
|
|
354
|
-
}
|
|
355
|
-
function addOracleUpdate(tx, config, pool) {
|
|
356
|
-
const feed = config.oracle.feeds?.find((f2) => f2.assetId === pool.id);
|
|
357
|
-
if (!feed) {
|
|
358
|
-
throw new T2000Error("PROTOCOL_UNAVAILABLE", `Oracle feed not found for asset ${pool.token?.symbol ?? pool.id}`);
|
|
359
|
-
}
|
|
360
|
-
tx.moveCall({
|
|
361
|
-
target: `${config.oracle.packageId}::oracle_pro::update_single_price_v2`,
|
|
362
|
-
arguments: [
|
|
363
|
-
tx.object(CLOCK),
|
|
364
|
-
tx.object(config.oracle.oracleConfig),
|
|
365
|
-
tx.object(config.oracle.priceOracle),
|
|
366
|
-
tx.object(config.oracle.supraOracleHolder),
|
|
367
|
-
tx.object(feed.pythPriceInfoObject),
|
|
368
|
-
tx.object(config.oracle.switchboardAggregator),
|
|
369
|
-
tx.pure.address(feed.feedId)
|
|
370
|
-
]
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
function refreshStableOracles(tx, config, pools) {
|
|
374
|
-
const stableTypes = STABLE_ASSETS.map((a) => SUPPORTED_ASSETS[a].type);
|
|
375
|
-
const stablePools = pools.filter((p) => {
|
|
376
|
-
const ct = p.suiCoinType || p.coinType || "";
|
|
377
|
-
return stableTypes.some((t) => matchesCoinType(ct, t));
|
|
378
|
-
});
|
|
379
|
-
for (const pool of stablePools) {
|
|
380
|
-
addOracleUpdate(tx, config, pool);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
function rateToApy(rawRate) {
|
|
384
|
-
if (!rawRate || rawRate === "0") return 0;
|
|
385
|
-
return Number(BigInt(rawRate)) / 10 ** RATE_DECIMALS * 100;
|
|
386
|
-
}
|
|
387
|
-
function parseLtv(rawLtv) {
|
|
388
|
-
if (!rawLtv || rawLtv === "0") return 0.75;
|
|
389
|
-
return Number(BigInt(rawLtv)) / 10 ** LTV_DECIMALS;
|
|
390
|
-
}
|
|
391
|
-
function parseLiqThreshold(val) {
|
|
392
|
-
if (typeof val === "number") return val;
|
|
393
|
-
const n = Number(val);
|
|
394
|
-
if (n > 1) return Number(BigInt(val)) / 10 ** LTV_DECIMALS;
|
|
395
|
-
return n;
|
|
396
|
-
}
|
|
397
|
-
function normalizeHealthFactor(raw) {
|
|
398
|
-
const v = raw / 10 ** RATE_DECIMALS;
|
|
399
|
-
return v > 1e5 ? Infinity : v;
|
|
400
|
-
}
|
|
401
|
-
function compoundBalance(rawBalance, currentIndex) {
|
|
402
|
-
if (!rawBalance || !currentIndex || currentIndex === "0") return 0;
|
|
403
|
-
const scale = BigInt("1" + "0".repeat(RATE_DECIMALS));
|
|
404
|
-
const half = scale / 2n;
|
|
405
|
-
const result = (rawBalance * BigInt(currentIndex) + half) / scale;
|
|
406
|
-
return Number(result) / 10 ** NAVI_BALANCE_DECIMALS;
|
|
407
|
-
}
|
|
408
|
-
async function getUserState(client, address) {
|
|
409
|
-
const config = await getConfig();
|
|
410
|
-
const tx = new transactions.Transaction();
|
|
411
|
-
tx.moveCall({
|
|
412
|
-
target: `${config.uiGetter}::getter_unchecked::get_user_state`,
|
|
413
|
-
arguments: [tx.object(config.storage), tx.pure.address(address)]
|
|
414
|
-
});
|
|
415
|
-
const result = await client.devInspectTransactionBlock({
|
|
416
|
-
transactionBlock: tx,
|
|
417
|
-
sender: address
|
|
418
|
-
});
|
|
419
|
-
const decoded = decodeDevInspect(result, bcs.bcs.vector(UserStateInfo));
|
|
420
|
-
if (!decoded) return [];
|
|
421
|
-
const mapped = decoded.map((s) => ({
|
|
422
|
-
assetId: s.asset_id,
|
|
423
|
-
supplyBalance: toBigInt(s.supply_balance),
|
|
424
|
-
borrowBalance: toBigInt(s.borrow_balance)
|
|
425
|
-
}));
|
|
426
|
-
return mapped.filter((s) => s.supplyBalance !== 0n || s.borrowBalance !== 0n);
|
|
427
|
-
}
|
|
428
|
-
async function fetchCoins(client, owner, coinType) {
|
|
429
|
-
const all = [];
|
|
430
|
-
let cursor;
|
|
431
|
-
let hasNext = true;
|
|
432
|
-
while (hasNext) {
|
|
433
|
-
const page = await client.getCoins({ owner, coinType, cursor: cursor ?? void 0 });
|
|
434
|
-
all.push(...page.data.map((c) => ({ coinObjectId: c.coinObjectId, balance: c.balance })));
|
|
435
|
-
cursor = page.nextCursor;
|
|
436
|
-
hasNext = page.hasNextPage;
|
|
437
|
-
}
|
|
438
|
-
return all;
|
|
439
|
-
}
|
|
440
|
-
function mergeCoins(tx, coins) {
|
|
441
|
-
if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No coins to merge");
|
|
442
|
-
const primary = tx.object(coins[0].coinObjectId);
|
|
443
|
-
if (coins.length > 1) {
|
|
444
|
-
tx.mergeCoins(primary, coins.slice(1).map((c) => tx.object(c.coinObjectId)));
|
|
445
|
-
}
|
|
446
|
-
return primary;
|
|
447
|
-
}
|
|
448
|
-
async function buildSaveTx(client, address, amount, options = {}) {
|
|
449
|
-
if (!amount || amount <= 0 || !Number.isFinite(amount)) {
|
|
450
|
-
throw new T2000Error("INVALID_AMOUNT", "Save amount must be a positive number");
|
|
451
|
-
}
|
|
452
|
-
const asset = options.asset ?? "USDC";
|
|
453
|
-
const assetInfo = SUPPORTED_ASSETS[asset];
|
|
454
|
-
const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
|
|
455
|
-
const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
|
|
456
|
-
const coins = await fetchCoins(client, address, assetInfo.type);
|
|
457
|
-
if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins found`);
|
|
458
|
-
const tx = new transactions.Transaction();
|
|
459
|
-
tx.setSender(address);
|
|
460
|
-
const coinObj = mergeCoins(tx, coins);
|
|
461
|
-
if (options.collectFee) {
|
|
462
|
-
addCollectFeeToTx(tx, coinObj, "save");
|
|
463
|
-
}
|
|
464
|
-
tx.moveCall({
|
|
465
|
-
target: `${config.package}::incentive_v3::entry_deposit`,
|
|
466
|
-
arguments: [
|
|
467
|
-
tx.object(CLOCK),
|
|
468
|
-
tx.object(config.storage),
|
|
469
|
-
tx.object(pool.contract.pool),
|
|
470
|
-
tx.pure.u8(pool.id),
|
|
471
|
-
coinObj,
|
|
472
|
-
tx.pure.u64(rawAmount),
|
|
473
|
-
tx.object(config.incentiveV2),
|
|
474
|
-
tx.object(config.incentiveV3)
|
|
475
|
-
],
|
|
476
|
-
typeArguments: [pool.suiCoinType]
|
|
477
|
-
});
|
|
478
|
-
return tx;
|
|
479
|
-
}
|
|
480
|
-
async function buildWithdrawTx(client, address, amount, options = {}) {
|
|
481
|
-
const asset = options.asset ?? "USDC";
|
|
482
|
-
const assetInfo = SUPPORTED_ASSETS[asset];
|
|
483
|
-
const [config, pool, pools, states] = await Promise.all([
|
|
484
|
-
getConfig(),
|
|
485
|
-
getPool(asset),
|
|
486
|
-
getPools(),
|
|
487
|
-
getUserState(client, address)
|
|
488
|
-
]);
|
|
489
|
-
const assetState = states.find((s) => s.assetId === pool.id);
|
|
490
|
-
const deposited = assetState ? compoundBalance(assetState.supplyBalance, pool.currentSupplyIndex) : 0;
|
|
491
|
-
const effectiveAmount = Math.min(amount, Math.max(0, deposited - WITHDRAW_DUST_BUFFER));
|
|
492
|
-
if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on NAVI`);
|
|
493
|
-
const rawAmount = Number(stableToRaw(effectiveAmount, assetInfo.decimals));
|
|
494
|
-
if (rawAmount <= 0) {
|
|
495
|
-
throw new T2000Error("INVALID_AMOUNT", `Withdrawal amount rounds to zero \u2014 balance is dust`);
|
|
496
|
-
}
|
|
497
|
-
const tx = new transactions.Transaction();
|
|
498
|
-
tx.setSender(address);
|
|
499
|
-
refreshStableOracles(tx, config, pools);
|
|
500
|
-
const [balance] = tx.moveCall({
|
|
501
|
-
target: `${config.package}::incentive_v3::withdraw_v2`,
|
|
502
|
-
arguments: [
|
|
503
|
-
tx.object(CLOCK),
|
|
504
|
-
tx.object(config.oracle.priceOracle),
|
|
505
|
-
tx.object(config.storage),
|
|
506
|
-
tx.object(pool.contract.pool),
|
|
507
|
-
tx.pure.u8(pool.id),
|
|
508
|
-
tx.pure.u64(rawAmount),
|
|
509
|
-
tx.object(config.incentiveV2),
|
|
510
|
-
tx.object(config.incentiveV3),
|
|
511
|
-
tx.object(SUI_SYSTEM_STATE)
|
|
512
|
-
],
|
|
513
|
-
typeArguments: [pool.suiCoinType]
|
|
514
|
-
});
|
|
515
|
-
const [coin] = tx.moveCall({
|
|
516
|
-
target: "0x2::coin::from_balance",
|
|
517
|
-
arguments: [balance],
|
|
518
|
-
typeArguments: [pool.suiCoinType]
|
|
519
|
-
});
|
|
520
|
-
tx.transferObjects([coin], address);
|
|
521
|
-
return { tx, effectiveAmount };
|
|
522
|
-
}
|
|
523
|
-
async function addWithdrawToTx(tx, client, address, amount, options = {}) {
|
|
524
|
-
const asset = options.asset ?? "USDC";
|
|
525
|
-
const assetInfo = SUPPORTED_ASSETS[asset];
|
|
526
|
-
const [config, pool, pools, states] = await Promise.all([
|
|
527
|
-
getConfig(),
|
|
528
|
-
getPool(asset),
|
|
529
|
-
getPools(),
|
|
530
|
-
getUserState(client, address)
|
|
531
|
-
]);
|
|
532
|
-
const assetState = states.find((s) => s.assetId === pool.id);
|
|
533
|
-
const deposited = assetState ? compoundBalance(assetState.supplyBalance, pool.currentSupplyIndex) : 0;
|
|
534
|
-
const effectiveAmount = Math.min(amount, Math.max(0, deposited - WITHDRAW_DUST_BUFFER));
|
|
535
|
-
if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on NAVI`);
|
|
536
|
-
const rawAmount = Number(stableToRaw(effectiveAmount, assetInfo.decimals));
|
|
537
|
-
if (rawAmount <= 0) {
|
|
538
|
-
const [coin2] = tx.moveCall({
|
|
539
|
-
target: "0x2::coin::zero",
|
|
540
|
-
typeArguments: [pool.suiCoinType]
|
|
541
|
-
});
|
|
542
|
-
return { coin: coin2, effectiveAmount: 0 };
|
|
543
|
-
}
|
|
544
|
-
refreshStableOracles(tx, config, pools);
|
|
545
|
-
const [balance] = tx.moveCall({
|
|
546
|
-
target: `${config.package}::incentive_v3::withdraw_v2`,
|
|
547
|
-
arguments: [
|
|
548
|
-
tx.object(CLOCK),
|
|
549
|
-
tx.object(config.oracle.priceOracle),
|
|
550
|
-
tx.object(config.storage),
|
|
551
|
-
tx.object(pool.contract.pool),
|
|
552
|
-
tx.pure.u8(pool.id),
|
|
553
|
-
tx.pure.u64(rawAmount),
|
|
554
|
-
tx.object(config.incentiveV2),
|
|
555
|
-
tx.object(config.incentiveV3),
|
|
556
|
-
tx.object(SUI_SYSTEM_STATE)
|
|
557
|
-
],
|
|
558
|
-
typeArguments: [pool.suiCoinType]
|
|
559
|
-
});
|
|
560
|
-
const [coin] = tx.moveCall({
|
|
561
|
-
target: "0x2::coin::from_balance",
|
|
562
|
-
arguments: [balance],
|
|
563
|
-
typeArguments: [pool.suiCoinType]
|
|
564
|
-
});
|
|
565
|
-
return { coin, effectiveAmount };
|
|
566
|
-
}
|
|
567
|
-
async function addSaveToTx(tx, _client, _address, coin, options = {}) {
|
|
568
|
-
const asset = options.asset ?? "USDC";
|
|
569
|
-
const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
|
|
570
|
-
if (options.collectFee) {
|
|
571
|
-
addCollectFeeToTx(tx, coin, "save");
|
|
572
|
-
}
|
|
573
|
-
const [coinValue] = tx.moveCall({
|
|
574
|
-
target: "0x2::coin::value",
|
|
575
|
-
typeArguments: [pool.suiCoinType],
|
|
576
|
-
arguments: [coin]
|
|
577
|
-
});
|
|
578
|
-
tx.moveCall({
|
|
579
|
-
target: `${config.package}::incentive_v3::entry_deposit`,
|
|
580
|
-
arguments: [
|
|
581
|
-
tx.object(CLOCK),
|
|
582
|
-
tx.object(config.storage),
|
|
583
|
-
tx.object(pool.contract.pool),
|
|
584
|
-
tx.pure.u8(pool.id),
|
|
585
|
-
coin,
|
|
586
|
-
coinValue,
|
|
587
|
-
tx.object(config.incentiveV2),
|
|
588
|
-
tx.object(config.incentiveV3)
|
|
589
|
-
],
|
|
590
|
-
typeArguments: [pool.suiCoinType]
|
|
591
|
-
});
|
|
592
|
-
}
|
|
593
|
-
async function addRepayToTx(tx, client, _address, coin, options = {}) {
|
|
594
|
-
const asset = options.asset ?? "USDC";
|
|
595
|
-
const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
|
|
596
|
-
addOracleUpdate(tx, config, pool);
|
|
597
|
-
const [coinValue] = tx.moveCall({
|
|
598
|
-
target: "0x2::coin::value",
|
|
599
|
-
typeArguments: [pool.suiCoinType],
|
|
600
|
-
arguments: [coin]
|
|
601
|
-
});
|
|
602
|
-
tx.moveCall({
|
|
603
|
-
target: `${config.package}::incentive_v3::entry_repay`,
|
|
604
|
-
arguments: [
|
|
605
|
-
tx.object(CLOCK),
|
|
606
|
-
tx.object(config.oracle.priceOracle),
|
|
607
|
-
tx.object(config.storage),
|
|
608
|
-
tx.object(pool.contract.pool),
|
|
609
|
-
tx.pure.u8(pool.id),
|
|
610
|
-
coin,
|
|
611
|
-
coinValue,
|
|
612
|
-
tx.object(config.incentiveV2),
|
|
613
|
-
tx.object(config.incentiveV3)
|
|
614
|
-
],
|
|
615
|
-
typeArguments: [pool.suiCoinType]
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
async function buildBorrowTx(client, address, amount, options = {}) {
|
|
619
|
-
if (!amount || amount <= 0 || !Number.isFinite(amount)) {
|
|
620
|
-
throw new T2000Error("INVALID_AMOUNT", "Borrow amount must be a positive number");
|
|
621
|
-
}
|
|
622
|
-
const asset = options.asset ?? "USDC";
|
|
623
|
-
const assetInfo = SUPPORTED_ASSETS[asset];
|
|
624
|
-
const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
|
|
625
|
-
const [config, pool, pools] = await Promise.all([
|
|
626
|
-
getConfig(),
|
|
627
|
-
getPool(asset),
|
|
628
|
-
getPools()
|
|
629
|
-
]);
|
|
630
|
-
const tx = new transactions.Transaction();
|
|
631
|
-
tx.setSender(address);
|
|
632
|
-
refreshStableOracles(tx, config, pools);
|
|
633
|
-
const [balance] = tx.moveCall({
|
|
634
|
-
target: `${config.package}::incentive_v3::borrow_v2`,
|
|
635
|
-
arguments: [
|
|
636
|
-
tx.object(CLOCK),
|
|
637
|
-
tx.object(config.oracle.priceOracle),
|
|
638
|
-
tx.object(config.storage),
|
|
639
|
-
tx.object(pool.contract.pool),
|
|
640
|
-
tx.pure.u8(pool.id),
|
|
641
|
-
tx.pure.u64(rawAmount),
|
|
642
|
-
tx.object(config.incentiveV2),
|
|
643
|
-
tx.object(config.incentiveV3),
|
|
644
|
-
tx.object(SUI_SYSTEM_STATE)
|
|
645
|
-
],
|
|
646
|
-
typeArguments: [pool.suiCoinType]
|
|
647
|
-
});
|
|
648
|
-
const [borrowedCoin] = tx.moveCall({
|
|
649
|
-
target: "0x2::coin::from_balance",
|
|
650
|
-
arguments: [balance],
|
|
651
|
-
typeArguments: [pool.suiCoinType]
|
|
652
|
-
});
|
|
653
|
-
if (options.collectFee) {
|
|
654
|
-
addCollectFeeToTx(tx, borrowedCoin, "borrow");
|
|
655
|
-
}
|
|
656
|
-
tx.transferObjects([borrowedCoin], address);
|
|
657
|
-
return tx;
|
|
658
|
-
}
|
|
659
|
-
async function buildRepayTx(client, address, amount, options = {}) {
|
|
660
|
-
if (!amount || amount <= 0 || !Number.isFinite(amount)) {
|
|
661
|
-
throw new T2000Error("INVALID_AMOUNT", "Repay amount must be a positive number");
|
|
662
|
-
}
|
|
663
|
-
const asset = options.asset ?? "USDC";
|
|
664
|
-
const assetInfo = SUPPORTED_ASSETS[asset];
|
|
665
|
-
const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
|
|
666
|
-
const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
|
|
667
|
-
const coins = await fetchCoins(client, address, assetInfo.type);
|
|
668
|
-
if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins to repay with`);
|
|
669
|
-
const tx = new transactions.Transaction();
|
|
670
|
-
tx.setSender(address);
|
|
671
|
-
addOracleUpdate(tx, config, pool);
|
|
672
|
-
const coinObj = mergeCoins(tx, coins);
|
|
673
|
-
tx.moveCall({
|
|
674
|
-
target: `${config.package}::incentive_v3::entry_repay`,
|
|
675
|
-
arguments: [
|
|
676
|
-
tx.object(CLOCK),
|
|
677
|
-
tx.object(config.oracle.priceOracle),
|
|
678
|
-
tx.object(config.storage),
|
|
679
|
-
tx.object(pool.contract.pool),
|
|
680
|
-
tx.pure.u8(pool.id),
|
|
681
|
-
coinObj,
|
|
682
|
-
tx.pure.u64(rawAmount),
|
|
683
|
-
tx.object(config.incentiveV2),
|
|
684
|
-
tx.object(config.incentiveV3)
|
|
685
|
-
],
|
|
686
|
-
typeArguments: [pool.suiCoinType]
|
|
687
|
-
});
|
|
688
|
-
return tx;
|
|
689
|
-
}
|
|
690
|
-
async function getHealthFactor(client, addressOrKeypair) {
|
|
691
|
-
const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
|
|
692
|
-
const [config, pools, states] = await Promise.all([
|
|
693
|
-
getConfig(),
|
|
694
|
-
getPools(),
|
|
695
|
-
getUserState(client, address)
|
|
696
|
-
]);
|
|
697
|
-
let supplied = 0;
|
|
698
|
-
let borrowed = 0;
|
|
699
|
-
let weightedLtv = 0;
|
|
700
|
-
let weightedLiqThreshold = 0;
|
|
701
|
-
for (const state of states) {
|
|
702
|
-
const pool = pools.find((p) => p.id === state.assetId);
|
|
703
|
-
if (!pool) continue;
|
|
704
|
-
const supplyBal = compoundBalance(state.supplyBalance, pool.currentSupplyIndex);
|
|
705
|
-
const borrowBal = compoundBalance(state.borrowBalance, pool.currentBorrowIndex);
|
|
706
|
-
const price = pool.token?.price ?? 1;
|
|
707
|
-
supplied += supplyBal * price;
|
|
708
|
-
borrowed += borrowBal * price;
|
|
709
|
-
if (supplyBal > 0) {
|
|
710
|
-
weightedLtv += supplyBal * price * parseLtv(pool.ltv);
|
|
711
|
-
weightedLiqThreshold += supplyBal * price * parseLiqThreshold(pool.liquidationFactor.threshold);
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
const ltv = supplied > 0 ? weightedLtv / supplied : 0.75;
|
|
715
|
-
const liqThreshold = supplied > 0 ? weightedLiqThreshold / supplied : 0.75;
|
|
716
|
-
const maxBorrowVal = Math.max(0, supplied * ltv - borrowed);
|
|
717
|
-
const usdcPool = pools.find((p) => matchesCoinType(p.suiCoinType || p.coinType || "", SUPPORTED_ASSETS.USDC.type));
|
|
718
|
-
let healthFactor;
|
|
719
|
-
if (borrowed <= 0) {
|
|
720
|
-
healthFactor = Infinity;
|
|
721
|
-
} else if (usdcPool) {
|
|
722
|
-
try {
|
|
723
|
-
const tx = new transactions.Transaction();
|
|
724
|
-
tx.moveCall({
|
|
725
|
-
target: `${config.uiGetter}::calculator_unchecked::dynamic_health_factor`,
|
|
726
|
-
arguments: [
|
|
727
|
-
tx.object(CLOCK),
|
|
728
|
-
tx.object(config.storage),
|
|
729
|
-
tx.object(config.oracle.priceOracle),
|
|
730
|
-
tx.pure.u8(usdcPool.id),
|
|
731
|
-
tx.pure.address(address),
|
|
732
|
-
tx.pure.u8(usdcPool.id),
|
|
733
|
-
tx.pure.u64(0),
|
|
734
|
-
tx.pure.u64(0),
|
|
735
|
-
tx.pure.bool(false)
|
|
736
|
-
],
|
|
737
|
-
typeArguments: [usdcPool.suiCoinType]
|
|
738
|
-
});
|
|
739
|
-
const result = await client.devInspectTransactionBlock({
|
|
740
|
-
transactionBlock: tx,
|
|
741
|
-
sender: address
|
|
742
|
-
});
|
|
743
|
-
const decoded = decodeDevInspect(result, bcs.bcs.u256());
|
|
744
|
-
if (decoded !== void 0) {
|
|
745
|
-
healthFactor = normalizeHealthFactor(Number(decoded));
|
|
746
|
-
} else {
|
|
747
|
-
healthFactor = supplied * liqThreshold / borrowed;
|
|
748
|
-
}
|
|
749
|
-
} catch {
|
|
750
|
-
healthFactor = supplied * liqThreshold / borrowed;
|
|
751
|
-
}
|
|
752
|
-
} else {
|
|
753
|
-
healthFactor = supplied * liqThreshold / borrowed;
|
|
754
|
-
}
|
|
755
|
-
return {
|
|
756
|
-
healthFactor,
|
|
757
|
-
supplied,
|
|
758
|
-
borrowed,
|
|
759
|
-
maxBorrow: maxBorrowVal,
|
|
760
|
-
liquidationThreshold: liqThreshold
|
|
761
|
-
};
|
|
762
|
-
}
|
|
763
|
-
async function getRates(client) {
|
|
764
|
-
try {
|
|
765
|
-
const pools = await getPools();
|
|
766
|
-
const result = {};
|
|
767
|
-
for (const asset of STABLE_ASSETS) {
|
|
768
|
-
const targetType = SUPPORTED_ASSETS[asset].type;
|
|
769
|
-
const pool = pools.find((p) => matchesCoinType(p.suiCoinType || p.coinType || "", targetType));
|
|
770
|
-
if (!pool) continue;
|
|
771
|
-
let saveApy = rateToApy(pool.currentSupplyRate);
|
|
772
|
-
let borrowApy = rateToApy(pool.currentBorrowRate);
|
|
773
|
-
if (saveApy <= 0 || saveApy > 100) saveApy = 0;
|
|
774
|
-
if (borrowApy <= 0 || borrowApy > 100) borrowApy = 0;
|
|
775
|
-
result[asset] = { saveApy, borrowApy };
|
|
776
|
-
}
|
|
777
|
-
if (!result.USDC) result.USDC = { saveApy: 4, borrowApy: 6 };
|
|
778
|
-
return result;
|
|
779
|
-
} catch {
|
|
780
|
-
return { USDC: { saveApy: 4, borrowApy: 6 } };
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
async function getPositions(client, addressOrKeypair) {
|
|
784
|
-
const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
|
|
785
|
-
const [states, pools] = await Promise.all([getUserState(client, address), getPools()]);
|
|
786
|
-
const positions = [];
|
|
787
|
-
for (const state of states) {
|
|
788
|
-
const pool = pools.find((p) => p.id === state.assetId);
|
|
789
|
-
if (!pool) continue;
|
|
790
|
-
const symbol = resolvePoolSymbol(pool);
|
|
791
|
-
const supplyBal = compoundBalance(state.supplyBalance, pool.currentSupplyIndex);
|
|
792
|
-
const borrowBal = compoundBalance(state.borrowBalance, pool.currentBorrowIndex);
|
|
793
|
-
if (supplyBal > 1e-4) {
|
|
794
|
-
positions.push({
|
|
795
|
-
protocol: "navi",
|
|
796
|
-
asset: symbol,
|
|
797
|
-
type: "save",
|
|
798
|
-
amount: supplyBal,
|
|
799
|
-
apy: rateToApy(pool.currentSupplyRate)
|
|
800
|
-
});
|
|
801
|
-
}
|
|
802
|
-
if (borrowBal > 1e-4) {
|
|
803
|
-
positions.push({
|
|
804
|
-
protocol: "navi",
|
|
805
|
-
asset: symbol,
|
|
806
|
-
type: "borrow",
|
|
807
|
-
amount: borrowBal,
|
|
808
|
-
apy: rateToApy(pool.currentBorrowRate)
|
|
809
|
-
});
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
return { positions };
|
|
813
|
-
}
|
|
814
|
-
async function maxWithdrawAmount(client, addressOrKeypair) {
|
|
815
|
-
const hf = await getHealthFactor(client, addressOrKeypair);
|
|
816
|
-
const ltv = hf.liquidationThreshold > 0 ? hf.liquidationThreshold : 0.75;
|
|
817
|
-
let maxAmount;
|
|
818
|
-
if (hf.borrowed === 0) {
|
|
819
|
-
maxAmount = hf.supplied;
|
|
820
|
-
} else {
|
|
821
|
-
maxAmount = Math.max(0, hf.supplied - hf.borrowed * MIN_HEALTH_FACTOR / ltv);
|
|
822
|
-
}
|
|
823
|
-
const remainingSupply = hf.supplied - maxAmount;
|
|
824
|
-
const hfAfter = hf.borrowed > 0 ? remainingSupply / hf.borrowed : Infinity;
|
|
825
|
-
return { maxAmount, healthFactorAfter: hfAfter, currentHF: hf.healthFactor };
|
|
826
|
-
}
|
|
827
|
-
async function maxBorrowAmount(client, addressOrKeypair) {
|
|
828
|
-
const hf = await getHealthFactor(client, addressOrKeypair);
|
|
829
|
-
const ltv = hf.liquidationThreshold > 0 ? hf.liquidationThreshold : 0.75;
|
|
830
|
-
const maxAmount = Math.max(0, hf.supplied * ltv / MIN_HEALTH_FACTOR - hf.borrowed);
|
|
831
|
-
return { maxAmount, healthFactorAfter: MIN_HEALTH_FACTOR, currentHF: hf.healthFactor };
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
// src/adapters/navi.ts
|
|
835
|
-
var descriptor = {
|
|
836
|
-
id: "navi",
|
|
837
|
-
name: "NAVI Protocol",
|
|
838
|
-
packages: [],
|
|
839
|
-
dynamicPackageId: true,
|
|
840
|
-
actionMap: {
|
|
841
|
-
"incentive_v3::entry_deposit": "save",
|
|
842
|
-
"incentive_v3::deposit": "save",
|
|
843
|
-
"incentive_v3::withdraw_v2": "withdraw",
|
|
844
|
-
"incentive_v3::entry_withdraw": "withdraw",
|
|
845
|
-
"incentive_v3::borrow_v2": "borrow",
|
|
846
|
-
"incentive_v3::entry_borrow": "borrow",
|
|
847
|
-
"incentive_v3::entry_repay": "repay",
|
|
848
|
-
"incentive_v3::repay": "repay"
|
|
849
|
-
}
|
|
850
|
-
};
|
|
851
|
-
var NaviAdapter = class {
|
|
852
|
-
id = "navi";
|
|
853
|
-
name = "NAVI Protocol";
|
|
854
|
-
version = "1.0.0";
|
|
855
|
-
capabilities = ["save", "withdraw", "borrow", "repay"];
|
|
856
|
-
supportedAssets = [...STABLE_ASSETS];
|
|
857
|
-
supportsSameAssetBorrow = true;
|
|
858
|
-
client;
|
|
859
|
-
async init(client) {
|
|
860
|
-
this.client = client;
|
|
861
|
-
}
|
|
862
|
-
initSync(client) {
|
|
863
|
-
this.client = client;
|
|
864
|
-
}
|
|
865
|
-
async getRates(asset) {
|
|
866
|
-
const rates = await getRates(this.client);
|
|
867
|
-
const normalized = normalizeAsset(asset);
|
|
868
|
-
const r = rates[normalized];
|
|
869
|
-
if (!r) throw new T2000Error("ASSET_NOT_SUPPORTED", `NAVI does not support ${asset}`);
|
|
870
|
-
return { asset: normalized, saveApy: r.saveApy, borrowApy: r.borrowApy };
|
|
871
|
-
}
|
|
872
|
-
async getPositions(address) {
|
|
873
|
-
const result = await getPositions(this.client, address);
|
|
874
|
-
return {
|
|
875
|
-
supplies: result.positions.filter((p) => p.type === "save").map((p) => ({ asset: p.asset, amount: p.amount, apy: p.apy })),
|
|
876
|
-
borrows: result.positions.filter((p) => p.type === "borrow").map((p) => ({ asset: p.asset, amount: p.amount, apy: p.apy }))
|
|
877
|
-
};
|
|
878
|
-
}
|
|
879
|
-
async getHealth(address) {
|
|
880
|
-
return getHealthFactor(this.client, address);
|
|
881
|
-
}
|
|
882
|
-
async buildSaveTx(address, amount, asset, options) {
|
|
883
|
-
const stableAsset = normalizeAsset(asset);
|
|
884
|
-
const tx = await buildSaveTx(this.client, address, amount, { ...options, asset: stableAsset });
|
|
885
|
-
return { tx };
|
|
886
|
-
}
|
|
887
|
-
async buildWithdrawTx(address, amount, asset) {
|
|
888
|
-
const stableAsset = normalizeAsset(asset);
|
|
889
|
-
const result = await buildWithdrawTx(this.client, address, amount, { asset: stableAsset });
|
|
890
|
-
return { tx: result.tx, effectiveAmount: result.effectiveAmount };
|
|
891
|
-
}
|
|
892
|
-
async buildBorrowTx(address, amount, asset, options) {
|
|
893
|
-
const stableAsset = normalizeAsset(asset);
|
|
894
|
-
const tx = await buildBorrowTx(this.client, address, amount, { ...options, asset: stableAsset });
|
|
895
|
-
return { tx };
|
|
896
|
-
}
|
|
897
|
-
async buildRepayTx(address, amount, asset) {
|
|
898
|
-
const stableAsset = normalizeAsset(asset);
|
|
899
|
-
const tx = await buildRepayTx(this.client, address, amount, { asset: stableAsset });
|
|
900
|
-
return { tx };
|
|
901
|
-
}
|
|
902
|
-
async maxWithdraw(address, _asset) {
|
|
903
|
-
return maxWithdrawAmount(this.client, address);
|
|
904
|
-
}
|
|
905
|
-
async maxBorrow(address, _asset) {
|
|
906
|
-
return maxBorrowAmount(this.client, address);
|
|
907
|
-
}
|
|
908
|
-
async addWithdrawToTx(tx, address, amount, asset) {
|
|
909
|
-
const stableAsset = normalizeAsset(asset);
|
|
910
|
-
return addWithdrawToTx(tx, this.client, address, amount, { asset: stableAsset });
|
|
911
|
-
}
|
|
912
|
-
async addSaveToTx(tx, address, coin, asset, options) {
|
|
913
|
-
const stableAsset = normalizeAsset(asset);
|
|
914
|
-
return addSaveToTx(tx, this.client, address, coin, { ...options, asset: stableAsset });
|
|
915
|
-
}
|
|
916
|
-
async addRepayToTx(tx, address, coin, asset) {
|
|
917
|
-
const stableAsset = normalizeAsset(asset);
|
|
918
|
-
return addRepayToTx(tx, this.client, address, coin, { asset: stableAsset });
|
|
919
|
-
}
|
|
920
|
-
};
|
|
921
|
-
var DEFAULT_SLIPPAGE_BPS = 300;
|
|
922
|
-
function createAggregatorClient(client, signer) {
|
|
923
|
-
return new aggregatorSdk.AggregatorClient({
|
|
924
|
-
client,
|
|
925
|
-
signer,
|
|
926
|
-
env: aggregatorSdk.Env.Mainnet
|
|
927
|
-
});
|
|
928
|
-
}
|
|
929
|
-
async function buildSwapTx(params) {
|
|
930
|
-
const { client, address, fromAsset, toAsset, amount, maxSlippageBps = DEFAULT_SLIPPAGE_BPS } = params;
|
|
931
|
-
const fromInfo = SUPPORTED_ASSETS[fromAsset];
|
|
932
|
-
const toInfo = SUPPORTED_ASSETS[toAsset];
|
|
933
|
-
if (!fromInfo || !toInfo) {
|
|
934
|
-
throw new T2000Error("ASSET_NOT_SUPPORTED", `Swap pair ${fromAsset}/${toAsset} is not supported`);
|
|
935
|
-
}
|
|
936
|
-
const rawAmount = BigInt(Math.floor(amount * 10 ** fromInfo.decimals));
|
|
937
|
-
const aggClient = createAggregatorClient(client, address);
|
|
938
|
-
const result = await aggClient.findRouters({
|
|
939
|
-
from: fromInfo.type,
|
|
940
|
-
target: toInfo.type,
|
|
941
|
-
amount: rawAmount,
|
|
942
|
-
byAmountIn: true
|
|
943
|
-
});
|
|
944
|
-
if (!result || result.insufficientLiquidity) {
|
|
945
|
-
throw new T2000Error(
|
|
946
|
-
"ASSET_NOT_SUPPORTED",
|
|
947
|
-
`No swap route found for ${fromAsset} \u2192 ${toAsset}`
|
|
948
|
-
);
|
|
949
|
-
}
|
|
950
|
-
const tx = new transactions.Transaction();
|
|
951
|
-
const slippage = maxSlippageBps / 1e4;
|
|
952
|
-
await aggClient.fastRouterSwap({
|
|
953
|
-
router: result,
|
|
954
|
-
txb: tx,
|
|
955
|
-
slippage
|
|
956
|
-
});
|
|
957
|
-
const estimatedOut = Number(result.amountOut.toString());
|
|
958
|
-
return {
|
|
959
|
-
tx,
|
|
960
|
-
estimatedOut,
|
|
961
|
-
toDecimals: toInfo.decimals
|
|
962
|
-
};
|
|
963
|
-
}
|
|
964
|
-
async function addSwapToTx(params) {
|
|
965
|
-
const { tx, client, address, inputCoin, fromAsset, toAsset, amount, maxSlippageBps = DEFAULT_SLIPPAGE_BPS } = params;
|
|
966
|
-
const fromInfo = SUPPORTED_ASSETS[fromAsset];
|
|
967
|
-
const toInfo = SUPPORTED_ASSETS[toAsset];
|
|
968
|
-
if (!fromInfo || !toInfo) {
|
|
969
|
-
throw new T2000Error("ASSET_NOT_SUPPORTED", `Swap pair ${fromAsset}/${toAsset} is not supported`);
|
|
970
|
-
}
|
|
971
|
-
const rawAmount = BigInt(Math.floor(amount * 10 ** fromInfo.decimals));
|
|
972
|
-
const aggClient = createAggregatorClient(client, address);
|
|
973
|
-
const result = await aggClient.findRouters({
|
|
974
|
-
from: fromInfo.type,
|
|
975
|
-
target: toInfo.type,
|
|
976
|
-
amount: rawAmount,
|
|
977
|
-
byAmountIn: true
|
|
978
|
-
});
|
|
979
|
-
if (!result || result.insufficientLiquidity) {
|
|
980
|
-
throw new T2000Error(
|
|
981
|
-
"ASSET_NOT_SUPPORTED",
|
|
982
|
-
`No swap route found for ${fromAsset} \u2192 ${toAsset}`
|
|
983
|
-
);
|
|
984
|
-
}
|
|
985
|
-
const slippage = maxSlippageBps / 1e4;
|
|
986
|
-
const outputCoin = await aggClient.routerSwap({
|
|
987
|
-
router: result,
|
|
988
|
-
txb: tx,
|
|
989
|
-
inputCoin,
|
|
990
|
-
slippage
|
|
991
|
-
});
|
|
992
|
-
const estimatedOut = Number(result.amountOut.toString());
|
|
993
|
-
return {
|
|
994
|
-
outputCoin,
|
|
995
|
-
estimatedOut,
|
|
996
|
-
toDecimals: toInfo.decimals
|
|
997
|
-
};
|
|
998
|
-
}
|
|
999
|
-
async function getPoolPrice(client) {
|
|
1000
|
-
try {
|
|
1001
|
-
const pool = await client.getObject({
|
|
1002
|
-
id: CETUS_USDC_SUI_POOL,
|
|
1003
|
-
options: { showContent: true }
|
|
1004
|
-
});
|
|
1005
|
-
if (pool.data?.content?.dataType === "moveObject") {
|
|
1006
|
-
const fields = pool.data.content.fields;
|
|
1007
|
-
const currentSqrtPrice = BigInt(String(fields.current_sqrt_price ?? "0"));
|
|
1008
|
-
if (currentSqrtPrice > 0n) {
|
|
1009
|
-
const Q64 = 2n ** 64n;
|
|
1010
|
-
const sqrtPriceFloat = Number(currentSqrtPrice) / Number(Q64);
|
|
1011
|
-
const rawPrice = sqrtPriceFloat * sqrtPriceFloat;
|
|
1012
|
-
const suiPriceUsd = 1e3 / rawPrice;
|
|
1013
|
-
if (suiPriceUsd > 0.01 && suiPriceUsd < 1e3) return suiPriceUsd;
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
} catch {
|
|
1017
|
-
}
|
|
1018
|
-
return 3.5;
|
|
1019
|
-
}
|
|
1020
|
-
async function getSwapQuote(client, fromAsset, toAsset, amount) {
|
|
1021
|
-
const fromInfo = SUPPORTED_ASSETS[fromAsset];
|
|
1022
|
-
const toInfo = SUPPORTED_ASSETS[toAsset];
|
|
1023
|
-
if (!fromInfo || !toInfo) {
|
|
1024
|
-
throw new T2000Error("ASSET_NOT_SUPPORTED", `Swap pair ${fromAsset}/${toAsset} is not supported`);
|
|
1025
|
-
}
|
|
1026
|
-
const rawAmount = BigInt(Math.floor(amount * 10 ** fromInfo.decimals));
|
|
1027
|
-
const poolPrice = await getPoolPrice(client);
|
|
1028
|
-
try {
|
|
1029
|
-
const aggClient = createAggregatorClient(client);
|
|
1030
|
-
const result = await aggClient.findRouters({
|
|
1031
|
-
from: fromInfo.type,
|
|
1032
|
-
target: toInfo.type,
|
|
1033
|
-
amount: rawAmount,
|
|
1034
|
-
byAmountIn: true
|
|
1035
|
-
});
|
|
1036
|
-
if (!result || result.insufficientLiquidity) {
|
|
1037
|
-
return fallbackQuote(fromAsset, amount, poolPrice);
|
|
1038
|
-
}
|
|
1039
|
-
const expectedOutput = Number(result.amountOut.toString()) / 10 ** toInfo.decimals;
|
|
1040
|
-
const priceImpact = result.deviationRatio ?? 0;
|
|
1041
|
-
return { expectedOutput, priceImpact, poolPrice };
|
|
1042
|
-
} catch {
|
|
1043
|
-
return fallbackQuote(fromAsset, amount, poolPrice);
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
function fallbackQuote(fromAsset, amount, poolPrice) {
|
|
1047
|
-
const expectedOutput = fromAsset === "USDC" ? amount / poolPrice : amount * poolPrice;
|
|
1048
|
-
return { expectedOutput, priceImpact: 0, poolPrice };
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
// src/adapters/cetus.ts
|
|
1052
|
-
var descriptor2 = {
|
|
1053
|
-
id: "cetus",
|
|
1054
|
-
name: "Cetus DEX",
|
|
1055
|
-
packages: [CETUS_PACKAGE],
|
|
1056
|
-
actionMap: {
|
|
1057
|
-
"router::swap": "swap",
|
|
1058
|
-
"router::swap_ab_bc": "swap",
|
|
1059
|
-
"router::swap_ab_cb": "swap",
|
|
1060
|
-
"router::swap_ba_bc": "swap",
|
|
1061
|
-
"router::swap_ba_cb": "swap"
|
|
1062
|
-
}
|
|
1063
|
-
};
|
|
1064
|
-
var CetusAdapter = class {
|
|
1065
|
-
id = "cetus";
|
|
1066
|
-
name = "Cetus";
|
|
1067
|
-
version = "1.0.0";
|
|
1068
|
-
capabilities = ["swap"];
|
|
1069
|
-
client;
|
|
1070
|
-
async init(client) {
|
|
1071
|
-
this.client = client;
|
|
1072
|
-
}
|
|
1073
|
-
initSync(client) {
|
|
1074
|
-
this.client = client;
|
|
1075
|
-
}
|
|
1076
|
-
async getQuote(from, to, amount) {
|
|
1077
|
-
return getSwapQuote(this.client, from, to, amount);
|
|
1078
|
-
}
|
|
1079
|
-
async buildSwapTx(address, from, to, amount, maxSlippageBps) {
|
|
1080
|
-
const result = await buildSwapTx({
|
|
1081
|
-
client: this.client,
|
|
1082
|
-
address,
|
|
1083
|
-
fromAsset: from,
|
|
1084
|
-
toAsset: to,
|
|
1085
|
-
amount,
|
|
1086
|
-
maxSlippageBps
|
|
1087
|
-
});
|
|
1088
|
-
return {
|
|
1089
|
-
tx: result.tx,
|
|
1090
|
-
estimatedOut: result.estimatedOut,
|
|
1091
|
-
toDecimals: result.toDecimals
|
|
1092
|
-
};
|
|
1093
|
-
}
|
|
1094
|
-
getSupportedPairs() {
|
|
1095
|
-
const pairs = [];
|
|
1096
|
-
for (const asset of Object.keys(INVESTMENT_ASSETS)) {
|
|
1097
|
-
pairs.push({ from: "USDC", to: asset }, { from: asset, to: "USDC" });
|
|
1098
|
-
}
|
|
1099
|
-
for (const a of STABLE_ASSETS) {
|
|
1100
|
-
for (const b of STABLE_ASSETS) {
|
|
1101
|
-
if (a !== b) pairs.push({ from: a, to: b });
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
return pairs;
|
|
1105
|
-
}
|
|
1106
|
-
async getPoolPrice() {
|
|
1107
|
-
return getPoolPrice(this.client);
|
|
1108
|
-
}
|
|
1109
|
-
async addSwapToTx(tx, address, inputCoin, from, to, amount, maxSlippageBps) {
|
|
1110
|
-
return addSwapToTx({
|
|
1111
|
-
tx,
|
|
1112
|
-
client: this.client,
|
|
1113
|
-
address,
|
|
1114
|
-
inputCoin,
|
|
1115
|
-
fromAsset: from,
|
|
1116
|
-
toAsset: to,
|
|
1117
|
-
amount,
|
|
1118
|
-
maxSlippageBps
|
|
1119
|
-
});
|
|
1120
|
-
}
|
|
1121
|
-
};
|
|
1122
|
-
var WAD = 1e18;
|
|
1123
|
-
var MIN_HEALTH_FACTOR2 = 1.5;
|
|
1124
|
-
var CLOCK2 = "0x6";
|
|
1125
|
-
var LENDING_MARKET_ID = "0x84030d26d85eaa7035084a057f2f11f701b7e2e4eda87551becbc7c97505ece1";
|
|
1126
|
-
var LENDING_MARKET_TYPE = "0xf95b06141ed4a174f239417323bde3f209b972f5930d8521ea38a52aff3a6ddf::suilend::MAIN_POOL";
|
|
1127
|
-
var SUILEND_PACKAGE = "0xf95b06141ed4a174f239417323bde3f209b972f5930d8521ea38a52aff3a6ddf";
|
|
1128
|
-
var UPGRADE_CAP_ID = "0x3d4ef1859c3ee9fc72858f588b56a09da5466e64f8cc4e90a7b3b909fba8a7ae";
|
|
1129
|
-
var FALLBACK_PUBLISHED_AT = "0x3d4353f3bd3565329655e6b77bc2abfd31e558b86662ebd078ae453d416bc10f";
|
|
1130
|
-
var descriptor3 = {
|
|
1131
|
-
id: "suilend",
|
|
1132
|
-
name: "Suilend",
|
|
1133
|
-
packages: [SUILEND_PACKAGE],
|
|
1134
|
-
actionMap: {
|
|
1135
|
-
"lending_market::deposit_liquidity_and_mint_ctokens": "save",
|
|
1136
|
-
"lending_market::deposit_ctokens_into_obligation": "save",
|
|
1137
|
-
"lending_market::create_obligation": "save",
|
|
1138
|
-
"lending_market::withdraw_ctokens": "withdraw",
|
|
1139
|
-
"lending_market::redeem_ctokens_and_withdraw_liquidity": "withdraw",
|
|
1140
|
-
"lending_market::borrow": "borrow",
|
|
1141
|
-
"lending_market::repay": "repay"
|
|
1142
|
-
}
|
|
1143
|
-
};
|
|
1144
|
-
function interpolateRate(utilBreakpoints, aprBreakpoints, utilizationPct) {
|
|
1145
|
-
if (utilBreakpoints.length === 0) return 0;
|
|
1146
|
-
if (utilizationPct <= utilBreakpoints[0]) return aprBreakpoints[0];
|
|
1147
|
-
if (utilizationPct >= utilBreakpoints[utilBreakpoints.length - 1]) {
|
|
1148
|
-
return aprBreakpoints[aprBreakpoints.length - 1];
|
|
1149
|
-
}
|
|
1150
|
-
for (let i = 1; i < utilBreakpoints.length; i++) {
|
|
1151
|
-
if (utilizationPct <= utilBreakpoints[i]) {
|
|
1152
|
-
const t = (utilizationPct - utilBreakpoints[i - 1]) / (utilBreakpoints[i] - utilBreakpoints[i - 1]);
|
|
1153
|
-
return aprBreakpoints[i - 1] + t * (aprBreakpoints[i] - aprBreakpoints[i - 1]);
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
return aprBreakpoints[aprBreakpoints.length - 1];
|
|
1157
|
-
}
|
|
1158
|
-
function computeRates(reserve) {
|
|
1159
|
-
const available = reserve.availableAmount / 10 ** reserve.mintDecimals;
|
|
1160
|
-
const borrowed = reserve.borrowedAmountWad / WAD / 10 ** reserve.mintDecimals;
|
|
1161
|
-
const totalDeposited = available + borrowed;
|
|
1162
|
-
const utilizationPct = totalDeposited > 0 ? borrowed / totalDeposited * 100 : 0;
|
|
1163
|
-
if (reserve.interestRateUtils.length === 0) return { borrowAprPct: 0, depositAprPct: 0 };
|
|
1164
|
-
const aprs = reserve.interestRateAprs.map((a) => a / 100);
|
|
1165
|
-
const borrowAprPct = interpolateRate(reserve.interestRateUtils, aprs, utilizationPct);
|
|
1166
|
-
const depositAprPct = utilizationPct / 100 * (borrowAprPct / 100) * (1 - reserve.spreadFeeBps / 1e4) * 100;
|
|
1167
|
-
return { borrowAprPct, depositAprPct };
|
|
1168
|
-
}
|
|
1169
|
-
function cTokenRatio(reserve) {
|
|
1170
|
-
if (reserve.ctokenSupply === 0) return 1;
|
|
1171
|
-
const totalSupply = reserve.availableAmount + reserve.borrowedAmountWad / WAD - reserve.unclaimedSpreadFeesWad / WAD;
|
|
1172
|
-
return totalSupply / reserve.ctokenSupply;
|
|
1173
|
-
}
|
|
1174
|
-
function f(obj) {
|
|
1175
|
-
if (obj && typeof obj === "object" && "fields" in obj) return obj.fields;
|
|
1176
|
-
return obj;
|
|
1177
|
-
}
|
|
1178
|
-
function str(v) {
|
|
1179
|
-
return String(v ?? "0");
|
|
1180
|
-
}
|
|
1181
|
-
function num(v) {
|
|
1182
|
-
return Number(str(v));
|
|
1183
|
-
}
|
|
1184
|
-
function parseReserve(raw, index) {
|
|
1185
|
-
const r = f(raw);
|
|
1186
|
-
const coinTypeField = f(r.coin_type);
|
|
1187
|
-
const config = f(f(r.config)?.element);
|
|
1188
|
-
return {
|
|
1189
|
-
coinType: str(coinTypeField?.name),
|
|
1190
|
-
mintDecimals: num(r.mint_decimals),
|
|
1191
|
-
availableAmount: num(r.available_amount),
|
|
1192
|
-
borrowedAmountWad: num(f(r.borrowed_amount)?.value),
|
|
1193
|
-
ctokenSupply: num(r.ctoken_supply),
|
|
1194
|
-
unclaimedSpreadFeesWad: num(f(r.unclaimed_spread_fees)?.value),
|
|
1195
|
-
cumulativeBorrowRateWad: num(f(r.cumulative_borrow_rate)?.value),
|
|
1196
|
-
openLtvPct: num(config?.open_ltv_pct),
|
|
1197
|
-
closeLtvPct: num(config?.close_ltv_pct),
|
|
1198
|
-
spreadFeeBps: num(config?.spread_fee_bps),
|
|
1199
|
-
interestRateUtils: Array.isArray(config?.interest_rate_utils) ? config.interest_rate_utils.map(num) : [],
|
|
1200
|
-
interestRateAprs: Array.isArray(config?.interest_rate_aprs) ? config.interest_rate_aprs.map(num) : [],
|
|
1201
|
-
arrayIndex: index
|
|
1202
|
-
};
|
|
1203
|
-
}
|
|
1204
|
-
function parseObligation(raw) {
|
|
1205
|
-
const deposits = Array.isArray(raw.deposits) ? raw.deposits.map((d) => {
|
|
1206
|
-
const df = f(d);
|
|
1207
|
-
return {
|
|
1208
|
-
coinType: str(f(df.coin_type)?.name),
|
|
1209
|
-
ctokenAmount: num(df.deposited_ctoken_amount),
|
|
1210
|
-
reserveIdx: num(df.reserve_array_index)
|
|
1211
|
-
};
|
|
1212
|
-
}) : [];
|
|
1213
|
-
const borrows = Array.isArray(raw.borrows) ? raw.borrows.map((b) => {
|
|
1214
|
-
const bf = f(b);
|
|
1215
|
-
return {
|
|
1216
|
-
coinType: str(f(bf.coin_type)?.name),
|
|
1217
|
-
borrowedWad: num(f(bf.borrowed_amount)?.value),
|
|
1218
|
-
cumBorrowRateWad: num(f(bf.cumulative_borrow_rate)?.value),
|
|
1219
|
-
reserveIdx: num(bf.reserve_array_index)
|
|
1220
|
-
};
|
|
1221
|
-
}) : [];
|
|
1222
|
-
return { deposits, borrows };
|
|
1223
|
-
}
|
|
1224
|
-
var SuilendAdapter = class {
|
|
1225
|
-
id = "suilend";
|
|
1226
|
-
name = "Suilend";
|
|
1227
|
-
version = "2.0.0";
|
|
1228
|
-
capabilities = ["save", "withdraw", "borrow", "repay"];
|
|
1229
|
-
supportedAssets = [...STABLE_ASSETS];
|
|
1230
|
-
supportsSameAssetBorrow = false;
|
|
1231
|
-
client;
|
|
1232
|
-
publishedAt = null;
|
|
1233
|
-
reserveCache = null;
|
|
1234
|
-
async init(client) {
|
|
1235
|
-
this.client = client;
|
|
1236
|
-
}
|
|
1237
|
-
initSync(client) {
|
|
1238
|
-
this.client = client;
|
|
1239
|
-
}
|
|
1240
|
-
// -- On-chain reads -------------------------------------------------------
|
|
1241
|
-
async resolvePackage() {
|
|
1242
|
-
if (this.publishedAt) return this.publishedAt;
|
|
1243
|
-
try {
|
|
1244
|
-
const cap = await this.client.getObject({ id: UPGRADE_CAP_ID, options: { showContent: true } });
|
|
1245
|
-
if (cap.data?.content?.dataType === "moveObject") {
|
|
1246
|
-
const fields = cap.data.content.fields;
|
|
1247
|
-
this.publishedAt = str(fields.package);
|
|
1248
|
-
return this.publishedAt;
|
|
1249
|
-
}
|
|
1250
|
-
} catch {
|
|
1251
|
-
}
|
|
1252
|
-
this.publishedAt = FALLBACK_PUBLISHED_AT;
|
|
1253
|
-
return this.publishedAt;
|
|
1254
|
-
}
|
|
1255
|
-
async loadReserves(fresh = false) {
|
|
1256
|
-
if (this.reserveCache && !fresh) return this.reserveCache;
|
|
1257
|
-
const market = await this.client.getObject({
|
|
1258
|
-
id: LENDING_MARKET_ID,
|
|
1259
|
-
options: { showContent: true }
|
|
1260
|
-
});
|
|
1261
|
-
if (market.data?.content?.dataType !== "moveObject") {
|
|
1262
|
-
throw new T2000Error("PROTOCOL_UNAVAILABLE", "Failed to read Suilend lending market");
|
|
1263
|
-
}
|
|
1264
|
-
const fields = market.data.content.fields;
|
|
1265
|
-
const reservesRaw = fields.reserves;
|
|
1266
|
-
if (!Array.isArray(reservesRaw)) {
|
|
1267
|
-
throw new T2000Error("PROTOCOL_UNAVAILABLE", "Failed to parse Suilend reserves");
|
|
1268
|
-
}
|
|
1269
|
-
this.reserveCache = reservesRaw.map((r, i) => parseReserve(r, i));
|
|
1270
|
-
return this.reserveCache;
|
|
1271
|
-
}
|
|
1272
|
-
findReserve(reserves, asset) {
|
|
1273
|
-
let coinType;
|
|
1274
|
-
if (asset in SUPPORTED_ASSETS) {
|
|
1275
|
-
coinType = SUPPORTED_ASSETS[asset].type;
|
|
1276
|
-
} else if (asset.includes("::")) {
|
|
1277
|
-
coinType = asset;
|
|
1278
|
-
} else {
|
|
1279
|
-
return void 0;
|
|
1280
|
-
}
|
|
1281
|
-
try {
|
|
1282
|
-
const normalized = utils.normalizeStructTag(coinType);
|
|
1283
|
-
return reserves.find((r) => {
|
|
1284
|
-
try {
|
|
1285
|
-
return utils.normalizeStructTag(r.coinType) === normalized;
|
|
1286
|
-
} catch {
|
|
1287
|
-
return false;
|
|
1288
|
-
}
|
|
1289
|
-
});
|
|
1290
|
-
} catch {
|
|
1291
|
-
return void 0;
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
async fetchObligationCaps(address) {
|
|
1295
|
-
const capType = `${SUILEND_PACKAGE}::lending_market::ObligationOwnerCap<${LENDING_MARKET_TYPE}>`;
|
|
1296
|
-
const caps = [];
|
|
1297
|
-
let cursor;
|
|
1298
|
-
let hasNext = true;
|
|
1299
|
-
while (hasNext) {
|
|
1300
|
-
const page = await this.client.getOwnedObjects({
|
|
1301
|
-
owner: address,
|
|
1302
|
-
filter: { StructType: capType },
|
|
1303
|
-
options: { showContent: true },
|
|
1304
|
-
cursor: cursor ?? void 0
|
|
1305
|
-
});
|
|
1306
|
-
for (const item of page.data) {
|
|
1307
|
-
if (item.data?.content?.dataType !== "moveObject") continue;
|
|
1308
|
-
const fields = item.data.content.fields;
|
|
1309
|
-
caps.push({
|
|
1310
|
-
id: item.data.objectId,
|
|
1311
|
-
obligationId: str(fields.obligation_id)
|
|
1312
|
-
});
|
|
1313
|
-
}
|
|
1314
|
-
cursor = page.nextCursor;
|
|
1315
|
-
hasNext = page.hasNextPage;
|
|
1316
|
-
}
|
|
1317
|
-
return caps;
|
|
1318
|
-
}
|
|
1319
|
-
async fetchObligation(obligationId) {
|
|
1320
|
-
const obj = await this.client.getObject({ id: obligationId, options: { showContent: true } });
|
|
1321
|
-
if (obj.data?.content?.dataType !== "moveObject") {
|
|
1322
|
-
throw new T2000Error("PROTOCOL_UNAVAILABLE", "Failed to read Suilend obligation");
|
|
1323
|
-
}
|
|
1324
|
-
return parseObligation(obj.data.content.fields);
|
|
1325
|
-
}
|
|
1326
|
-
resolveSymbol(coinType) {
|
|
1327
|
-
try {
|
|
1328
|
-
const normalized = utils.normalizeStructTag(coinType);
|
|
1329
|
-
for (const [key, info] of Object.entries(SUPPORTED_ASSETS)) {
|
|
1330
|
-
try {
|
|
1331
|
-
if (utils.normalizeStructTag(info.type) === normalized) return key;
|
|
1332
|
-
} catch {
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
} catch {
|
|
1336
|
-
}
|
|
1337
|
-
const parts = coinType.split("::");
|
|
1338
|
-
return parts[parts.length - 1] || "UNKNOWN";
|
|
1339
|
-
}
|
|
1340
|
-
// -- Adapter interface ----------------------------------------------------
|
|
1341
|
-
async getRates(asset) {
|
|
1342
|
-
const reserves = await this.loadReserves();
|
|
1343
|
-
const reserve = this.findReserve(reserves, asset);
|
|
1344
|
-
if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `Suilend does not support ${asset}`);
|
|
1345
|
-
const { borrowAprPct, depositAprPct } = computeRates(reserve);
|
|
1346
|
-
return { asset, saveApy: depositAprPct, borrowApy: borrowAprPct };
|
|
1347
|
-
}
|
|
1348
|
-
async getPositions(address) {
|
|
1349
|
-
const supplies = [];
|
|
1350
|
-
const borrows = [];
|
|
1351
|
-
const caps = await this.fetchObligationCaps(address);
|
|
1352
|
-
if (caps.length === 0) return { supplies, borrows };
|
|
1353
|
-
const [reserves, obligation] = await Promise.all([
|
|
1354
|
-
this.loadReserves(),
|
|
1355
|
-
this.fetchObligation(caps[0].obligationId)
|
|
1356
|
-
]);
|
|
1357
|
-
for (const dep of obligation.deposits) {
|
|
1358
|
-
const reserve = reserves[dep.reserveIdx];
|
|
1359
|
-
if (!reserve) continue;
|
|
1360
|
-
const ratio = cTokenRatio(reserve);
|
|
1361
|
-
const amount = dep.ctokenAmount * ratio / 10 ** reserve.mintDecimals;
|
|
1362
|
-
const { depositAprPct } = computeRates(reserve);
|
|
1363
|
-
supplies.push({ asset: this.resolveSymbol(dep.coinType), amount, apy: depositAprPct });
|
|
1364
|
-
}
|
|
1365
|
-
for (const bor of obligation.borrows) {
|
|
1366
|
-
const reserve = reserves[bor.reserveIdx];
|
|
1367
|
-
if (!reserve) continue;
|
|
1368
|
-
const rawAmount = bor.borrowedWad / WAD / 10 ** reserve.mintDecimals;
|
|
1369
|
-
const reserveRate = reserve.cumulativeBorrowRateWad / WAD;
|
|
1370
|
-
const posRate = bor.cumBorrowRateWad / WAD;
|
|
1371
|
-
const compounded = posRate > 0 ? rawAmount * (reserveRate / posRate) : rawAmount;
|
|
1372
|
-
const { borrowAprPct } = computeRates(reserve);
|
|
1373
|
-
borrows.push({ asset: this.resolveSymbol(bor.coinType), amount: compounded, apy: borrowAprPct });
|
|
1374
|
-
}
|
|
1375
|
-
return { supplies, borrows };
|
|
1376
|
-
}
|
|
1377
|
-
async getHealth(address) {
|
|
1378
|
-
const caps = await this.fetchObligationCaps(address);
|
|
1379
|
-
if (caps.length === 0) {
|
|
1380
|
-
return { healthFactor: Infinity, supplied: 0, borrowed: 0, maxBorrow: 0, liquidationThreshold: 0 };
|
|
1381
|
-
}
|
|
1382
|
-
const [reserves, obligation] = await Promise.all([
|
|
1383
|
-
this.loadReserves(),
|
|
1384
|
-
this.fetchObligation(caps[0].obligationId)
|
|
1385
|
-
]);
|
|
1386
|
-
let supplied = 0;
|
|
1387
|
-
let borrowed = 0;
|
|
1388
|
-
let weightedCloseLtv = 0;
|
|
1389
|
-
let weightedOpenLtv = 0;
|
|
1390
|
-
for (const dep of obligation.deposits) {
|
|
1391
|
-
const reserve = reserves[dep.reserveIdx];
|
|
1392
|
-
if (!reserve) continue;
|
|
1393
|
-
const ratio = cTokenRatio(reserve);
|
|
1394
|
-
const amount = dep.ctokenAmount * ratio / 10 ** reserve.mintDecimals;
|
|
1395
|
-
supplied += amount;
|
|
1396
|
-
weightedCloseLtv += amount * (reserve.closeLtvPct / 100);
|
|
1397
|
-
weightedOpenLtv += amount * (reserve.openLtvPct / 100);
|
|
1398
|
-
}
|
|
1399
|
-
for (const bor of obligation.borrows) {
|
|
1400
|
-
const reserve = reserves[bor.reserveIdx];
|
|
1401
|
-
if (!reserve) continue;
|
|
1402
|
-
const rawAmount = bor.borrowedWad / WAD / 10 ** reserve.mintDecimals;
|
|
1403
|
-
const reserveRate = reserve.cumulativeBorrowRateWad / WAD;
|
|
1404
|
-
const posRate = bor.cumBorrowRateWad / WAD;
|
|
1405
|
-
borrowed += posRate > 0 ? rawAmount * (reserveRate / posRate) : rawAmount;
|
|
1406
|
-
}
|
|
1407
|
-
const liqThreshold = supplied > 0 ? weightedCloseLtv / supplied : 0.75;
|
|
1408
|
-
const openLtv = supplied > 0 ? weightedOpenLtv / supplied : 0.7;
|
|
1409
|
-
const healthFactor = borrowed > 0 ? supplied * liqThreshold / borrowed : Infinity;
|
|
1410
|
-
const maxBorrow = Math.max(0, supplied * openLtv - borrowed);
|
|
1411
|
-
return { healthFactor, supplied, borrowed, maxBorrow, liquidationThreshold: liqThreshold };
|
|
1412
|
-
}
|
|
1413
|
-
async buildSaveTx(address, amount, asset, options) {
|
|
1414
|
-
const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
|
|
1415
|
-
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
1416
|
-
const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
|
|
1417
|
-
const reserve = this.findReserve(reserves, assetKey);
|
|
1418
|
-
if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend. Try: NAVI or a different asset.`);
|
|
1419
|
-
const caps = await this.fetchObligationCaps(address);
|
|
1420
|
-
const tx = new transactions.Transaction();
|
|
1421
|
-
tx.setSender(address);
|
|
1422
|
-
let capRef;
|
|
1423
|
-
if (caps.length === 0) {
|
|
1424
|
-
const [newCap] = tx.moveCall({
|
|
1425
|
-
target: `${pkg}::lending_market::create_obligation`,
|
|
1426
|
-
typeArguments: [LENDING_MARKET_TYPE],
|
|
1427
|
-
arguments: [tx.object(LENDING_MARKET_ID)]
|
|
1428
|
-
});
|
|
1429
|
-
capRef = newCap;
|
|
1430
|
-
} else {
|
|
1431
|
-
capRef = caps[0].id;
|
|
1432
|
-
}
|
|
1433
|
-
const allCoins = await this.fetchAllCoins(address, assetInfo.type);
|
|
1434
|
-
if (allCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins found`);
|
|
1435
|
-
const primaryCoinId = allCoins[0].coinObjectId;
|
|
1436
|
-
if (allCoins.length > 1) {
|
|
1437
|
-
tx.mergeCoins(tx.object(primaryCoinId), allCoins.slice(1).map((c) => tx.object(c.coinObjectId)));
|
|
1438
|
-
}
|
|
1439
|
-
const rawAmount = stableToRaw(amount, assetInfo.decimals).toString();
|
|
1440
|
-
const [depositCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawAmount]);
|
|
1441
|
-
if (options?.collectFee) {
|
|
1442
|
-
addCollectFeeToTx(tx, depositCoin, "save");
|
|
1443
|
-
}
|
|
1444
|
-
const [ctokens] = tx.moveCall({
|
|
1445
|
-
target: `${pkg}::lending_market::deposit_liquidity_and_mint_ctokens`,
|
|
1446
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
1447
|
-
arguments: [
|
|
1448
|
-
tx.object(LENDING_MARKET_ID),
|
|
1449
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
1450
|
-
tx.object(CLOCK2),
|
|
1451
|
-
depositCoin
|
|
1452
|
-
]
|
|
1453
|
-
});
|
|
1454
|
-
tx.moveCall({
|
|
1455
|
-
target: `${pkg}::lending_market::deposit_ctokens_into_obligation`,
|
|
1456
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
1457
|
-
arguments: [
|
|
1458
|
-
tx.object(LENDING_MARKET_ID),
|
|
1459
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
1460
|
-
typeof capRef === "string" ? tx.object(capRef) : capRef,
|
|
1461
|
-
tx.object(CLOCK2),
|
|
1462
|
-
ctokens
|
|
1463
|
-
]
|
|
1464
|
-
});
|
|
1465
|
-
if (typeof capRef !== "string") {
|
|
1466
|
-
tx.transferObjects([capRef], address);
|
|
1467
|
-
}
|
|
1468
|
-
return { tx };
|
|
1469
|
-
}
|
|
1470
|
-
async buildWithdrawTx(address, amount, asset) {
|
|
1471
|
-
const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
|
|
1472
|
-
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
1473
|
-
const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves(true)]);
|
|
1474
|
-
const reserve = this.findReserve(reserves, assetKey);
|
|
1475
|
-
if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
|
|
1476
|
-
const caps = await this.fetchObligationCaps(address);
|
|
1477
|
-
if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found");
|
|
1478
|
-
const positions = await this.getPositions(address);
|
|
1479
|
-
const deposited = positions.supplies.find((s) => s.asset === assetKey)?.amount ?? 0;
|
|
1480
|
-
const effectiveAmount = Math.min(amount, deposited);
|
|
1481
|
-
if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on Suilend`);
|
|
1482
|
-
const ratio = cTokenRatio(reserve);
|
|
1483
|
-
const ctokenAmount = Math.ceil(effectiveAmount * 10 ** reserve.mintDecimals / ratio);
|
|
1484
|
-
const tx = new transactions.Transaction();
|
|
1485
|
-
tx.setSender(address);
|
|
1486
|
-
const [ctokens] = tx.moveCall({
|
|
1487
|
-
target: `${pkg}::lending_market::withdraw_ctokens`,
|
|
1488
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
1489
|
-
arguments: [
|
|
1490
|
-
tx.object(LENDING_MARKET_ID),
|
|
1491
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
1492
|
-
tx.object(caps[0].id),
|
|
1493
|
-
tx.object(CLOCK2),
|
|
1494
|
-
tx.pure.u64(ctokenAmount)
|
|
1495
|
-
]
|
|
1496
|
-
});
|
|
1497
|
-
const exemptionType = `${SUILEND_PACKAGE}::lending_market::RateLimiterExemption<${LENDING_MARKET_TYPE}, ${assetInfo.type}>`;
|
|
1498
|
-
const [none] = tx.moveCall({
|
|
1499
|
-
target: "0x1::option::none",
|
|
1500
|
-
typeArguments: [exemptionType]
|
|
1501
|
-
});
|
|
1502
|
-
const [coin] = tx.moveCall({
|
|
1503
|
-
target: `${pkg}::lending_market::redeem_ctokens_and_withdraw_liquidity`,
|
|
1504
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
1505
|
-
arguments: [
|
|
1506
|
-
tx.object(LENDING_MARKET_ID),
|
|
1507
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
1508
|
-
tx.object(CLOCK2),
|
|
1509
|
-
ctokens,
|
|
1510
|
-
none
|
|
1511
|
-
]
|
|
1512
|
-
});
|
|
1513
|
-
tx.transferObjects([coin], address);
|
|
1514
|
-
return { tx, effectiveAmount };
|
|
1515
|
-
}
|
|
1516
|
-
async addWithdrawToTx(tx, address, amount, asset) {
|
|
1517
|
-
const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
|
|
1518
|
-
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
1519
|
-
const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves(true)]);
|
|
1520
|
-
const reserve = this.findReserve(reserves, assetKey);
|
|
1521
|
-
if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
|
|
1522
|
-
const caps = await this.fetchObligationCaps(address);
|
|
1523
|
-
if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found");
|
|
1524
|
-
const positions = await this.getPositions(address);
|
|
1525
|
-
const deposited = positions.supplies.find((s) => s.asset === assetKey)?.amount ?? 0;
|
|
1526
|
-
const effectiveAmount = Math.min(amount, deposited);
|
|
1527
|
-
if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on Suilend`);
|
|
1528
|
-
const ratio = cTokenRatio(reserve);
|
|
1529
|
-
const ctokenAmount = Math.ceil(effectiveAmount * 10 ** reserve.mintDecimals / ratio);
|
|
1530
|
-
const [ctokens] = tx.moveCall({
|
|
1531
|
-
target: `${pkg}::lending_market::withdraw_ctokens`,
|
|
1532
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
1533
|
-
arguments: [
|
|
1534
|
-
tx.object(LENDING_MARKET_ID),
|
|
1535
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
1536
|
-
tx.object(caps[0].id),
|
|
1537
|
-
tx.object(CLOCK2),
|
|
1538
|
-
tx.pure.u64(ctokenAmount)
|
|
1539
|
-
]
|
|
1540
|
-
});
|
|
1541
|
-
const exemptionType = `${SUILEND_PACKAGE}::lending_market::RateLimiterExemption<${LENDING_MARKET_TYPE}, ${assetInfo.type}>`;
|
|
1542
|
-
const [none] = tx.moveCall({
|
|
1543
|
-
target: "0x1::option::none",
|
|
1544
|
-
typeArguments: [exemptionType]
|
|
1545
|
-
});
|
|
1546
|
-
const [coin] = tx.moveCall({
|
|
1547
|
-
target: `${pkg}::lending_market::redeem_ctokens_and_withdraw_liquidity`,
|
|
1548
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
1549
|
-
arguments: [
|
|
1550
|
-
tx.object(LENDING_MARKET_ID),
|
|
1551
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
1552
|
-
tx.object(CLOCK2),
|
|
1553
|
-
ctokens,
|
|
1554
|
-
none
|
|
1555
|
-
]
|
|
1556
|
-
});
|
|
1557
|
-
return { coin, effectiveAmount };
|
|
1558
|
-
}
|
|
1559
|
-
async addSaveToTx(tx, address, coin, asset, options) {
|
|
1560
|
-
const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
|
|
1561
|
-
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
1562
|
-
const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
|
|
1563
|
-
const reserve = this.findReserve(reserves, assetKey);
|
|
1564
|
-
if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
|
|
1565
|
-
const caps = await this.fetchObligationCaps(address);
|
|
1566
|
-
let capRef;
|
|
1567
|
-
if (caps.length === 0) {
|
|
1568
|
-
const [newCap] = tx.moveCall({
|
|
1569
|
-
target: `${pkg}::lending_market::create_obligation`,
|
|
1570
|
-
typeArguments: [LENDING_MARKET_TYPE],
|
|
1571
|
-
arguments: [tx.object(LENDING_MARKET_ID)]
|
|
1572
|
-
});
|
|
1573
|
-
capRef = newCap;
|
|
1574
|
-
} else {
|
|
1575
|
-
capRef = caps[0].id;
|
|
1576
|
-
}
|
|
1577
|
-
if (options?.collectFee) {
|
|
1578
|
-
addCollectFeeToTx(tx, coin, "save");
|
|
1579
|
-
}
|
|
1580
|
-
const [ctokens] = tx.moveCall({
|
|
1581
|
-
target: `${pkg}::lending_market::deposit_liquidity_and_mint_ctokens`,
|
|
1582
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
1583
|
-
arguments: [
|
|
1584
|
-
tx.object(LENDING_MARKET_ID),
|
|
1585
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
1586
|
-
tx.object(CLOCK2),
|
|
1587
|
-
coin
|
|
1588
|
-
]
|
|
1589
|
-
});
|
|
1590
|
-
tx.moveCall({
|
|
1591
|
-
target: `${pkg}::lending_market::deposit_ctokens_into_obligation`,
|
|
1592
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
1593
|
-
arguments: [
|
|
1594
|
-
tx.object(LENDING_MARKET_ID),
|
|
1595
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
1596
|
-
typeof capRef === "string" ? tx.object(capRef) : capRef,
|
|
1597
|
-
tx.object(CLOCK2),
|
|
1598
|
-
ctokens
|
|
1599
|
-
]
|
|
1600
|
-
});
|
|
1601
|
-
if (typeof capRef !== "string") {
|
|
1602
|
-
tx.transferObjects([capRef], address);
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
async buildBorrowTx(address, amount, asset, options) {
|
|
1606
|
-
const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
|
|
1607
|
-
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
1608
|
-
const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
|
|
1609
|
-
const reserve = this.findReserve(reserves, assetKey);
|
|
1610
|
-
if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend. Try: NAVI or a different asset.`);
|
|
1611
|
-
const caps = await this.fetchObligationCaps(address);
|
|
1612
|
-
if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found. Deposit collateral first with: t2000 save <amount>");
|
|
1613
|
-
const rawAmount = stableToRaw(amount, assetInfo.decimals);
|
|
1614
|
-
const tx = new transactions.Transaction();
|
|
1615
|
-
tx.setSender(address);
|
|
1616
|
-
const [coin] = tx.moveCall({
|
|
1617
|
-
target: `${pkg}::lending_market::borrow`,
|
|
1618
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
1619
|
-
arguments: [
|
|
1620
|
-
tx.object(LENDING_MARKET_ID),
|
|
1621
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
1622
|
-
tx.object(caps[0].id),
|
|
1623
|
-
tx.object(CLOCK2),
|
|
1624
|
-
tx.pure.u64(rawAmount)
|
|
1625
|
-
]
|
|
1626
|
-
});
|
|
1627
|
-
if (options?.collectFee) {
|
|
1628
|
-
addCollectFeeToTx(tx, coin, "borrow");
|
|
1629
|
-
}
|
|
1630
|
-
tx.transferObjects([coin], address);
|
|
1631
|
-
return { tx };
|
|
1632
|
-
}
|
|
1633
|
-
async buildRepayTx(address, amount, asset) {
|
|
1634
|
-
const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
|
|
1635
|
-
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
1636
|
-
const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
|
|
1637
|
-
const reserve = this.findReserve(reserves, assetKey);
|
|
1638
|
-
if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
|
|
1639
|
-
const caps = await this.fetchObligationCaps(address);
|
|
1640
|
-
if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend obligation found");
|
|
1641
|
-
const allCoins = await this.fetchAllCoins(address, assetInfo.type);
|
|
1642
|
-
if (allCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins to repay with`);
|
|
1643
|
-
const rawAmount = stableToRaw(amount, assetInfo.decimals);
|
|
1644
|
-
const tx = new transactions.Transaction();
|
|
1645
|
-
tx.setSender(address);
|
|
1646
|
-
const primaryCoinId = allCoins[0].coinObjectId;
|
|
1647
|
-
if (allCoins.length > 1) {
|
|
1648
|
-
tx.mergeCoins(tx.object(primaryCoinId), allCoins.slice(1).map((c) => tx.object(c.coinObjectId)));
|
|
1649
|
-
}
|
|
1650
|
-
const [repayCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawAmount.toString()]);
|
|
1651
|
-
tx.moveCall({
|
|
1652
|
-
target: `${pkg}::lending_market::repay`,
|
|
1653
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
1654
|
-
arguments: [
|
|
1655
|
-
tx.object(LENDING_MARKET_ID),
|
|
1656
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
1657
|
-
tx.object(caps[0].id),
|
|
1658
|
-
tx.object(CLOCK2),
|
|
1659
|
-
repayCoin
|
|
1660
|
-
]
|
|
1661
|
-
});
|
|
1662
|
-
return { tx };
|
|
1663
|
-
}
|
|
1664
|
-
async addRepayToTx(tx, address, coin, asset) {
|
|
1665
|
-
const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
|
|
1666
|
-
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
1667
|
-
const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
|
|
1668
|
-
const reserve = this.findReserve(reserves, assetKey);
|
|
1669
|
-
if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
|
|
1670
|
-
const caps = await this.fetchObligationCaps(address);
|
|
1671
|
-
if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend obligation found");
|
|
1672
|
-
tx.moveCall({
|
|
1673
|
-
target: `${pkg}::lending_market::repay`,
|
|
1674
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
1675
|
-
arguments: [
|
|
1676
|
-
tx.object(LENDING_MARKET_ID),
|
|
1677
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
1678
|
-
tx.object(caps[0].id),
|
|
1679
|
-
tx.object(CLOCK2),
|
|
1680
|
-
coin
|
|
1681
|
-
]
|
|
1682
|
-
});
|
|
1683
|
-
}
|
|
1684
|
-
async maxWithdraw(address, _asset) {
|
|
1685
|
-
const health = await this.getHealth(address);
|
|
1686
|
-
let maxAmount;
|
|
1687
|
-
if (health.borrowed === 0) {
|
|
1688
|
-
maxAmount = health.supplied;
|
|
1689
|
-
} else {
|
|
1690
|
-
maxAmount = Math.max(0, health.supplied - health.borrowed * MIN_HEALTH_FACTOR2 / health.liquidationThreshold);
|
|
1691
|
-
}
|
|
1692
|
-
const remainingSupply = health.supplied - maxAmount;
|
|
1693
|
-
const hfAfter = health.borrowed > 0 ? remainingSupply * health.liquidationThreshold / health.borrowed : Infinity;
|
|
1694
|
-
return { maxAmount, healthFactorAfter: hfAfter, currentHF: health.healthFactor };
|
|
1695
|
-
}
|
|
1696
|
-
async maxBorrow(address, _asset) {
|
|
1697
|
-
const health = await this.getHealth(address);
|
|
1698
|
-
const maxAmount = health.maxBorrow;
|
|
1699
|
-
return { maxAmount, healthFactorAfter: MIN_HEALTH_FACTOR2, currentHF: health.healthFactor };
|
|
1700
|
-
}
|
|
1701
|
-
async fetchAllCoins(owner, coinType) {
|
|
1702
|
-
const all = [];
|
|
1703
|
-
let cursor = null;
|
|
1704
|
-
let hasNext = true;
|
|
1705
|
-
while (hasNext) {
|
|
1706
|
-
const page = await this.client.getCoins({ owner, coinType, cursor: cursor ?? void 0 });
|
|
1707
|
-
all.push(...page.data.map((c) => ({ coinObjectId: c.coinObjectId, balance: c.balance })));
|
|
1708
|
-
cursor = page.nextCursor;
|
|
1709
|
-
hasNext = page.hasNextPage;
|
|
1710
|
-
}
|
|
1711
|
-
return all;
|
|
1712
|
-
}
|
|
1713
|
-
};
|
|
1714
|
-
var descriptor4 = {
|
|
1715
|
-
id: "sentinel",
|
|
1716
|
-
name: "Sui Sentinel",
|
|
1717
|
-
packages: [SENTINEL.PACKAGE],
|
|
1718
|
-
actionMap: {
|
|
1719
|
-
"sentinel::request_attack": "sentinel_attack",
|
|
1720
|
-
"sentinel::consume_prompt": "sentinel_settle"
|
|
1721
|
-
}
|
|
1722
|
-
};
|
|
1723
|
-
|
|
1724
|
-
// src/adapters/index.ts
|
|
1725
|
-
var allDescriptors = [
|
|
1726
|
-
descriptor,
|
|
1727
|
-
descriptor3,
|
|
1728
|
-
descriptor2,
|
|
1729
|
-
descriptor4
|
|
1730
|
-
];
|
|
1731
|
-
|
|
1732
|
-
exports.CetusAdapter = CetusAdapter;
|
|
1733
|
-
exports.NaviAdapter = NaviAdapter;
|
|
1734
|
-
exports.ProtocolRegistry = ProtocolRegistry;
|
|
1735
|
-
exports.SuilendAdapter = SuilendAdapter;
|
|
1736
|
-
exports.allDescriptors = allDescriptors;
|
|
1737
|
-
exports.cetusDescriptor = descriptor2;
|
|
1738
|
-
exports.naviDescriptor = descriptor;
|
|
1739
|
-
exports.sentinelDescriptor = descriptor4;
|
|
1740
|
-
exports.suilendDescriptor = descriptor3;
|
|
1741
|
-
//# sourceMappingURL=index.cjs.map
|
|
1742
|
-
//# sourceMappingURL=index.cjs.map
|