@t2000/sdk 0.4.2 → 0.5.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 +1 -1
- package/dist/adapters/index.cjs +485 -228
- package/dist/adapters/index.cjs.map +1 -1
- package/dist/adapters/index.d.cts +2 -2
- package/dist/adapters/index.d.ts +2 -2
- package/dist/adapters/index.js +485 -228
- package/dist/adapters/index.js.map +1 -1
- package/dist/{index-BuaGAa6b.d.cts → index-DMDq8uxe.d.cts} +18 -24
- package/dist/{index-BuaGAa6b.d.ts → index-DMDq8uxe.d.ts} +18 -24
- package/dist/index.cjs +488 -233
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -16
- package/dist/index.d.ts +16 -16
- package/dist/index.js +488 -233
- package/dist/index.js.map +1 -1
- package/package.json +2 -11
package/dist/adapters/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getPool, getLendingState, getHealthFactor as getHealthFactor$1, getCoins, mergeCoinsPTB, depositCoinPTB, withdrawCoinPTB, borrowCoinPTB, repayCoinPTB, getPriceFeeds, filterPriceFeeds, updateOraclePricesPTB } from '@naviprotocol/lending';
|
|
2
1
|
import { Transaction } from '@mysten/sui/transactions';
|
|
2
|
+
import { bcs } from '@mysten/sui/bcs';
|
|
3
3
|
import { AggregatorClient, Env } from '@cetusprotocol/aggregator-sdk';
|
|
4
4
|
import { normalizeStructTag } from '@mysten/sui/utils';
|
|
5
5
|
|
|
@@ -181,15 +181,59 @@ function addCollectFeeToTx(tx, paymentCoin, operation) {
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
// src/protocols/navi.ts
|
|
184
|
-
var ENV = { env: "prod" };
|
|
185
184
|
var USDC_TYPE = SUPPORTED_ASSETS.USDC.type;
|
|
186
185
|
var RATE_DECIMALS = 27;
|
|
187
186
|
var LTV_DECIMALS = 27;
|
|
188
187
|
var MIN_HEALTH_FACTOR = 1.5;
|
|
189
188
|
var WITHDRAW_DUST_BUFFER = 1e-3;
|
|
189
|
+
var CLOCK = "0x06";
|
|
190
|
+
var SUI_SYSTEM_STATE = "0x05";
|
|
190
191
|
var NAVI_BALANCE_DECIMALS = 9;
|
|
191
|
-
|
|
192
|
-
|
|
192
|
+
var CONFIG_API = "https://open-api.naviprotocol.io/api/navi/config?env=prod";
|
|
193
|
+
var POOLS_API = "https://open-api.naviprotocol.io/api/navi/pools?env=prod";
|
|
194
|
+
function toBigInt(v) {
|
|
195
|
+
if (typeof v === "bigint") return v;
|
|
196
|
+
return BigInt(String(v));
|
|
197
|
+
}
|
|
198
|
+
var UserStateInfo = bcs.struct("UserStateInfo", {
|
|
199
|
+
asset_id: bcs.u8(),
|
|
200
|
+
borrow_balance: bcs.u256(),
|
|
201
|
+
supply_balance: bcs.u256()
|
|
202
|
+
});
|
|
203
|
+
function decodeDevInspect(result, schema) {
|
|
204
|
+
const rv = result.results?.[0]?.returnValues?.[0];
|
|
205
|
+
if (result.error || !rv) return void 0;
|
|
206
|
+
const bytes = Uint8Array.from(rv[0]);
|
|
207
|
+
return schema.parse(bytes);
|
|
208
|
+
}
|
|
209
|
+
var configCache = null;
|
|
210
|
+
var poolsCache = null;
|
|
211
|
+
var CACHE_TTL = 5 * 6e4;
|
|
212
|
+
async function fetchJson(url) {
|
|
213
|
+
const res = await fetch(url);
|
|
214
|
+
if (!res.ok) throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI API error: ${res.status}`);
|
|
215
|
+
const json = await res.json();
|
|
216
|
+
return json.data ?? json;
|
|
217
|
+
}
|
|
218
|
+
async function getConfig(fresh = false) {
|
|
219
|
+
if (configCache && !fresh && Date.now() - configCache.ts < CACHE_TTL) return configCache.data;
|
|
220
|
+
const data = await fetchJson(CONFIG_API);
|
|
221
|
+
configCache = { data, ts: Date.now() };
|
|
222
|
+
return data;
|
|
223
|
+
}
|
|
224
|
+
async function getPools(fresh = false) {
|
|
225
|
+
if (poolsCache && !fresh && Date.now() - poolsCache.ts < CACHE_TTL) return poolsCache.data;
|
|
226
|
+
const data = await fetchJson(POOLS_API);
|
|
227
|
+
poolsCache = { data, ts: Date.now() };
|
|
228
|
+
return data;
|
|
229
|
+
}
|
|
230
|
+
async function getUsdcPool() {
|
|
231
|
+
const pools = await getPools();
|
|
232
|
+
const usdc = pools.find(
|
|
233
|
+
(p) => p.token?.symbol === "USDC" || p.coinType?.toLowerCase().includes("usdc")
|
|
234
|
+
);
|
|
235
|
+
if (!usdc) throw new T2000Error("PROTOCOL_UNAVAILABLE", "USDC pool not found on NAVI");
|
|
236
|
+
return usdc;
|
|
193
237
|
}
|
|
194
238
|
function rateToApy(rawRate) {
|
|
195
239
|
if (!rawRate || rawRate === "0") return 0;
|
|
@@ -205,59 +249,146 @@ function parseLiqThreshold(val) {
|
|
|
205
249
|
if (n > 1) return Number(BigInt(val)) / 10 ** LTV_DECIMALS;
|
|
206
250
|
return n;
|
|
207
251
|
}
|
|
208
|
-
function
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
);
|
|
252
|
+
function normalizeHealthFactor(raw) {
|
|
253
|
+
const v = raw / 10 ** RATE_DECIMALS;
|
|
254
|
+
return v > 1e5 ? Infinity : v;
|
|
212
255
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
256
|
+
function compoundBalance(rawBalance, currentIndex) {
|
|
257
|
+
if (!rawBalance || !currentIndex || currentIndex === "0") return 0;
|
|
258
|
+
const scale = BigInt("1" + "0".repeat(RATE_DECIMALS));
|
|
259
|
+
const half = scale / 2n;
|
|
260
|
+
const result = (rawBalance * scale + half) / BigInt(currentIndex);
|
|
261
|
+
return Number(result) / 10 ** NAVI_BALANCE_DECIMALS;
|
|
262
|
+
}
|
|
263
|
+
async function getUserState(client, address) {
|
|
264
|
+
const config = await getConfig();
|
|
265
|
+
const tx = new Transaction();
|
|
266
|
+
tx.moveCall({
|
|
267
|
+
target: `${config.uiGetter}::getter_unchecked::get_user_state`,
|
|
268
|
+
arguments: [tx.object(config.storage), tx.pure.address(address)]
|
|
269
|
+
});
|
|
270
|
+
const result = await client.devInspectTransactionBlock({
|
|
271
|
+
transactionBlock: tx,
|
|
272
|
+
sender: address
|
|
273
|
+
});
|
|
274
|
+
const decoded = decodeDevInspect(result, bcs.vector(UserStateInfo));
|
|
275
|
+
if (!decoded) return [];
|
|
276
|
+
return decoded.map((s) => ({
|
|
277
|
+
assetId: s.asset_id,
|
|
278
|
+
supplyBalance: toBigInt(s.supply_balance),
|
|
279
|
+
borrowBalance: toBigInt(s.borrow_balance)
|
|
280
|
+
})).filter((s) => s.supplyBalance !== 0n || s.borrowBalance !== 0n);
|
|
281
|
+
}
|
|
282
|
+
async function fetchCoins(client, owner, coinType) {
|
|
283
|
+
const all = [];
|
|
284
|
+
let cursor;
|
|
285
|
+
let hasNext = true;
|
|
286
|
+
while (hasNext) {
|
|
287
|
+
const page = await client.getCoins({ owner, coinType, cursor: cursor ?? void 0 });
|
|
288
|
+
all.push(...page.data.map((c) => ({ coinObjectId: c.coinObjectId, balance: c.balance })));
|
|
289
|
+
cursor = page.nextCursor;
|
|
290
|
+
hasNext = page.hasNextPage;
|
|
291
|
+
}
|
|
292
|
+
return all;
|
|
293
|
+
}
|
|
294
|
+
function mergeCoinsPtb(tx, coins, amount) {
|
|
295
|
+
if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No coins to merge");
|
|
296
|
+
const primary = tx.object(coins[0].coinObjectId);
|
|
297
|
+
if (coins.length > 1) {
|
|
298
|
+
tx.mergeCoins(primary, coins.slice(1).map((c) => tx.object(c.coinObjectId)));
|
|
224
299
|
}
|
|
300
|
+
const [split] = tx.splitCoins(primary, [amount]);
|
|
301
|
+
return split;
|
|
225
302
|
}
|
|
226
303
|
async function buildSaveTx(client, address, amount, options = {}) {
|
|
227
304
|
const rawAmount = Number(usdcToRaw(amount));
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
305
|
+
const [config, pool] = await Promise.all([getConfig(), getUsdcPool()]);
|
|
306
|
+
const coins = await fetchCoins(client, address, USDC_TYPE);
|
|
307
|
+
if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins found");
|
|
232
308
|
const tx = new Transaction();
|
|
233
309
|
tx.setSender(address);
|
|
234
|
-
const coinObj =
|
|
310
|
+
const coinObj = mergeCoinsPtb(tx, coins, rawAmount);
|
|
235
311
|
if (options.collectFee) {
|
|
236
312
|
addCollectFeeToTx(tx, coinObj, "save");
|
|
237
313
|
}
|
|
238
|
-
|
|
314
|
+
tx.moveCall({
|
|
315
|
+
target: `${config.package}::incentive_v3::entry_deposit`,
|
|
316
|
+
arguments: [
|
|
317
|
+
tx.object(CLOCK),
|
|
318
|
+
tx.object(config.storage),
|
|
319
|
+
tx.object(pool.contract.pool),
|
|
320
|
+
tx.pure.u8(pool.id),
|
|
321
|
+
coinObj,
|
|
322
|
+
tx.pure.u64(rawAmount),
|
|
323
|
+
tx.object(config.incentiveV2),
|
|
324
|
+
tx.object(config.incentiveV3)
|
|
325
|
+
],
|
|
326
|
+
typeArguments: [pool.suiCoinType]
|
|
327
|
+
});
|
|
239
328
|
return tx;
|
|
240
329
|
}
|
|
241
330
|
async function buildWithdrawTx(client, address, amount) {
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
331
|
+
const [config, pool, pools, states] = await Promise.all([
|
|
332
|
+
getConfig(),
|
|
333
|
+
getUsdcPool(),
|
|
334
|
+
getPools(),
|
|
335
|
+
getUserState(client, address)
|
|
336
|
+
]);
|
|
337
|
+
const usdcState = states.find((s) => s.assetId === pool.id);
|
|
338
|
+
const deposited = usdcState ? compoundBalance(usdcState.supplyBalance, pool.currentSupplyIndex) : 0;
|
|
245
339
|
const effectiveAmount = Math.min(amount, Math.max(0, deposited - WITHDRAW_DUST_BUFFER));
|
|
246
340
|
if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", "Nothing to withdraw");
|
|
247
341
|
const rawAmount = Number(usdcToRaw(effectiveAmount));
|
|
248
342
|
const tx = new Transaction();
|
|
249
343
|
tx.setSender(address);
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
344
|
+
const [balance] = tx.moveCall({
|
|
345
|
+
target: `${config.package}::incentive_v3::withdraw_v2`,
|
|
346
|
+
arguments: [
|
|
347
|
+
tx.object(CLOCK),
|
|
348
|
+
tx.object(config.oracle.priceOracle),
|
|
349
|
+
tx.object(config.storage),
|
|
350
|
+
tx.object(pool.contract.pool),
|
|
351
|
+
tx.pure.u8(pool.id),
|
|
352
|
+
tx.pure.u64(rawAmount),
|
|
353
|
+
tx.object(config.incentiveV2),
|
|
354
|
+
tx.object(config.incentiveV3),
|
|
355
|
+
tx.object(SUI_SYSTEM_STATE)
|
|
356
|
+
],
|
|
357
|
+
typeArguments: [pool.suiCoinType]
|
|
358
|
+
});
|
|
359
|
+
const [coin] = tx.moveCall({
|
|
360
|
+
target: "0x2::coin::from_balance",
|
|
361
|
+
arguments: [balance],
|
|
362
|
+
typeArguments: [pool.suiCoinType]
|
|
363
|
+
});
|
|
364
|
+
tx.transferObjects([coin], address);
|
|
253
365
|
return { tx, effectiveAmount };
|
|
254
366
|
}
|
|
255
367
|
async function buildBorrowTx(client, address, amount, options = {}) {
|
|
256
368
|
const rawAmount = Number(usdcToRaw(amount));
|
|
369
|
+
const [config, pool] = await Promise.all([getConfig(), getUsdcPool()]);
|
|
257
370
|
const tx = new Transaction();
|
|
258
371
|
tx.setSender(address);
|
|
259
|
-
|
|
260
|
-
|
|
372
|
+
const [balance] = tx.moveCall({
|
|
373
|
+
target: `${config.package}::incentive_v3::borrow_v2`,
|
|
374
|
+
arguments: [
|
|
375
|
+
tx.object(CLOCK),
|
|
376
|
+
tx.object(config.oracle.priceOracle),
|
|
377
|
+
tx.object(config.storage),
|
|
378
|
+
tx.object(pool.contract.pool),
|
|
379
|
+
tx.pure.u8(pool.id),
|
|
380
|
+
tx.pure.u64(rawAmount),
|
|
381
|
+
tx.object(config.incentiveV2),
|
|
382
|
+
tx.object(config.incentiveV3),
|
|
383
|
+
tx.object(SUI_SYSTEM_STATE)
|
|
384
|
+
],
|
|
385
|
+
typeArguments: [pool.suiCoinType]
|
|
386
|
+
});
|
|
387
|
+
const [borrowedCoin] = tx.moveCall({
|
|
388
|
+
target: "0x2::coin::from_balance",
|
|
389
|
+
arguments: [balance],
|
|
390
|
+
typeArguments: [pool.suiCoinType]
|
|
391
|
+
});
|
|
261
392
|
if (options.collectFee) {
|
|
262
393
|
addCollectFeeToTx(tx, borrowedCoin, "borrow");
|
|
263
394
|
}
|
|
@@ -266,31 +397,79 @@ async function buildBorrowTx(client, address, amount, options = {}) {
|
|
|
266
397
|
}
|
|
267
398
|
async function buildRepayTx(client, address, amount) {
|
|
268
399
|
const rawAmount = Number(usdcToRaw(amount));
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
400
|
+
const [config, pool] = await Promise.all([getConfig(), getUsdcPool()]);
|
|
401
|
+
const coins = await fetchCoins(client, address, USDC_TYPE);
|
|
402
|
+
if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins to repay with");
|
|
273
403
|
const tx = new Transaction();
|
|
274
404
|
tx.setSender(address);
|
|
275
|
-
const coinObj =
|
|
276
|
-
|
|
405
|
+
const coinObj = mergeCoinsPtb(tx, coins, rawAmount);
|
|
406
|
+
tx.moveCall({
|
|
407
|
+
target: `${config.package}::incentive_v3::entry_repay`,
|
|
408
|
+
arguments: [
|
|
409
|
+
tx.object(CLOCK),
|
|
410
|
+
tx.object(config.oracle.priceOracle),
|
|
411
|
+
tx.object(config.storage),
|
|
412
|
+
tx.object(pool.contract.pool),
|
|
413
|
+
tx.pure.u8(pool.id),
|
|
414
|
+
coinObj,
|
|
415
|
+
tx.pure.u64(rawAmount),
|
|
416
|
+
tx.object(config.incentiveV2),
|
|
417
|
+
tx.object(config.incentiveV3)
|
|
418
|
+
],
|
|
419
|
+
typeArguments: [pool.suiCoinType]
|
|
420
|
+
});
|
|
277
421
|
return tx;
|
|
278
422
|
}
|
|
279
423
|
async function getHealthFactor(client, addressOrKeypair) {
|
|
280
424
|
const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
|
|
281
|
-
const [
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
425
|
+
const [config, pool, states] = await Promise.all([
|
|
426
|
+
getConfig(),
|
|
427
|
+
getUsdcPool(),
|
|
428
|
+
getUserState(client, address)
|
|
285
429
|
]);
|
|
286
|
-
const
|
|
287
|
-
const supplied =
|
|
288
|
-
const borrowed =
|
|
430
|
+
const usdcState = states.find((s) => s.assetId === pool.id);
|
|
431
|
+
const supplied = usdcState ? compoundBalance(usdcState.supplyBalance, pool.currentSupplyIndex) : 0;
|
|
432
|
+
const borrowed = usdcState ? compoundBalance(usdcState.borrowBalance, pool.currentBorrowIndex) : 0;
|
|
289
433
|
const ltv = parseLtv(pool.ltv);
|
|
290
434
|
const liqThreshold = parseLiqThreshold(pool.liquidationFactor.threshold);
|
|
291
435
|
const maxBorrowVal = Math.max(0, supplied * ltv - borrowed);
|
|
436
|
+
let healthFactor;
|
|
437
|
+
if (borrowed <= 0) {
|
|
438
|
+
healthFactor = Infinity;
|
|
439
|
+
} else {
|
|
440
|
+
try {
|
|
441
|
+
const tx = new Transaction();
|
|
442
|
+
tx.moveCall({
|
|
443
|
+
target: `${config.uiGetter}::calculator_unchecked::dynamic_health_factor`,
|
|
444
|
+
arguments: [
|
|
445
|
+
tx.object(CLOCK),
|
|
446
|
+
tx.object(config.storage),
|
|
447
|
+
tx.object(config.oracle.priceOracle),
|
|
448
|
+
tx.pure.u8(pool.id),
|
|
449
|
+
tx.pure.address(address),
|
|
450
|
+
tx.pure.u8(pool.id),
|
|
451
|
+
tx.pure.u64(0),
|
|
452
|
+
tx.pure.u64(0),
|
|
453
|
+
tx.pure.bool(false)
|
|
454
|
+
],
|
|
455
|
+
typeArguments: [pool.suiCoinType]
|
|
456
|
+
});
|
|
457
|
+
const result = await client.devInspectTransactionBlock({
|
|
458
|
+
transactionBlock: tx,
|
|
459
|
+
sender: address
|
|
460
|
+
});
|
|
461
|
+
const decoded = decodeDevInspect(result, bcs.u256());
|
|
462
|
+
if (decoded !== void 0) {
|
|
463
|
+
healthFactor = normalizeHealthFactor(Number(decoded));
|
|
464
|
+
} else {
|
|
465
|
+
healthFactor = borrowed > 0 ? supplied * liqThreshold / borrowed : Infinity;
|
|
466
|
+
}
|
|
467
|
+
} catch {
|
|
468
|
+
healthFactor = borrowed > 0 ? supplied * liqThreshold / borrowed : Infinity;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
292
471
|
return {
|
|
293
|
-
healthFactor
|
|
472
|
+
healthFactor,
|
|
294
473
|
supplied,
|
|
295
474
|
borrowed,
|
|
296
475
|
maxBorrow: maxBorrowVal,
|
|
@@ -299,7 +478,7 @@ async function getHealthFactor(client, addressOrKeypair) {
|
|
|
299
478
|
}
|
|
300
479
|
async function getRates(client) {
|
|
301
480
|
try {
|
|
302
|
-
const pool = await
|
|
481
|
+
const pool = await getUsdcPool();
|
|
303
482
|
let saveApy = rateToApy(pool.currentSupplyRate);
|
|
304
483
|
let borrowApy = rateToApy(pool.currentBorrowRate);
|
|
305
484
|
if (saveApy <= 0 || saveApy > 100) saveApy = 4;
|
|
@@ -311,19 +490,21 @@ async function getRates(client) {
|
|
|
311
490
|
}
|
|
312
491
|
async function getPositions(client, addressOrKeypair) {
|
|
313
492
|
const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
|
|
314
|
-
const
|
|
493
|
+
const [states, pools] = await Promise.all([getUserState(client, address), getPools()]);
|
|
315
494
|
const positions = [];
|
|
316
|
-
for (const
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
const
|
|
495
|
+
for (const state of states) {
|
|
496
|
+
const pool = pools.find((p) => p.id === state.assetId);
|
|
497
|
+
if (!pool) continue;
|
|
498
|
+
const symbol = pool.token?.symbol ?? "UNKNOWN";
|
|
499
|
+
const supplyBal = compoundBalance(state.supplyBalance, pool.currentSupplyIndex);
|
|
500
|
+
const borrowBal = compoundBalance(state.borrowBalance, pool.currentBorrowIndex);
|
|
320
501
|
if (supplyBal > 1e-4) {
|
|
321
502
|
positions.push({
|
|
322
503
|
protocol: "navi",
|
|
323
504
|
asset: symbol,
|
|
324
505
|
type: "save",
|
|
325
506
|
amount: supplyBal,
|
|
326
|
-
apy: rateToApy(
|
|
507
|
+
apy: rateToApy(pool.currentSupplyRate)
|
|
327
508
|
});
|
|
328
509
|
}
|
|
329
510
|
if (borrowBal > 1e-4) {
|
|
@@ -332,7 +513,7 @@ async function getPositions(client, addressOrKeypair) {
|
|
|
332
513
|
asset: symbol,
|
|
333
514
|
type: "borrow",
|
|
334
515
|
amount: borrowBal,
|
|
335
|
-
apy: rateToApy(
|
|
516
|
+
apy: rateToApy(pool.currentBorrowRate)
|
|
336
517
|
});
|
|
337
518
|
}
|
|
338
519
|
}
|
|
@@ -349,21 +530,13 @@ async function maxWithdrawAmount(client, addressOrKeypair) {
|
|
|
349
530
|
}
|
|
350
531
|
const remainingSupply = hf.supplied - maxAmount;
|
|
351
532
|
const hfAfter = hf.borrowed > 0 ? remainingSupply / hf.borrowed : Infinity;
|
|
352
|
-
return {
|
|
353
|
-
maxAmount,
|
|
354
|
-
healthFactorAfter: hfAfter,
|
|
355
|
-
currentHF: hf.healthFactor
|
|
356
|
-
};
|
|
533
|
+
return { maxAmount, healthFactorAfter: hfAfter, currentHF: hf.healthFactor };
|
|
357
534
|
}
|
|
358
535
|
async function maxBorrowAmount(client, addressOrKeypair) {
|
|
359
536
|
const hf = await getHealthFactor(client, addressOrKeypair);
|
|
360
537
|
const ltv = hf.liquidationThreshold > 0 ? hf.liquidationThreshold : 0.75;
|
|
361
538
|
const maxAmount = Math.max(0, hf.supplied * ltv / MIN_HEALTH_FACTOR - hf.borrowed);
|
|
362
|
-
return {
|
|
363
|
-
maxAmount,
|
|
364
|
-
healthFactorAfter: MIN_HEALTH_FACTOR,
|
|
365
|
-
currentHF: hf.healthFactor
|
|
366
|
-
};
|
|
539
|
+
return { maxAmount, healthFactorAfter: MIN_HEALTH_FACTOR, currentHF: hf.healthFactor };
|
|
367
540
|
}
|
|
368
541
|
|
|
369
542
|
// src/adapters/navi.ts
|
|
@@ -559,6 +732,12 @@ var CetusAdapter = class {
|
|
|
559
732
|
var USDC_TYPE2 = SUPPORTED_ASSETS.USDC.type;
|
|
560
733
|
var WAD = 1e18;
|
|
561
734
|
var MIN_HEALTH_FACTOR2 = 1.5;
|
|
735
|
+
var CLOCK2 = "0x6";
|
|
736
|
+
var LENDING_MARKET_ID = "0x84030d26d85eaa7035084a057f2f11f701b7e2e4eda87551becbc7c97505ece1";
|
|
737
|
+
var LENDING_MARKET_TYPE = "0xf95b06141ed4a174f239417323bde3f209b972f5930d8521ea38a52aff3a6ddf::suilend::MAIN_POOL";
|
|
738
|
+
var SUILEND_PACKAGE = "0xf95b06141ed4a174f239417323bde3f209b972f5930d8521ea38a52aff3a6ddf";
|
|
739
|
+
var UPGRADE_CAP_ID = "0x3d4ef1859c3ee9fc72858f588b56a09da5466e64f8cc4e90a7b3b909fba8a7ae";
|
|
740
|
+
var FALLBACK_PUBLISHED_AT = "0xd2a67633ccb8de063163e25bcfca242929caf5cf1a26c2929dab519ee0b8f331";
|
|
562
741
|
function interpolateRate(utilBreakpoints, aprBreakpoints, utilizationPct) {
|
|
563
742
|
if (utilBreakpoints.length === 0) return 0;
|
|
564
743
|
if (utilizationPct <= utilBreakpoints[0]) return aprBreakpoints[0];
|
|
@@ -573,85 +752,121 @@ function interpolateRate(utilBreakpoints, aprBreakpoints, utilizationPct) {
|
|
|
573
752
|
}
|
|
574
753
|
return aprBreakpoints[aprBreakpoints.length - 1];
|
|
575
754
|
}
|
|
576
|
-
function
|
|
577
|
-
const
|
|
578
|
-
const
|
|
579
|
-
const borrowed = Number(reserve.borrowedAmount.value) / WAD / 10 ** decimals;
|
|
755
|
+
function computeRates(reserve) {
|
|
756
|
+
const available = reserve.availableAmount / 10 ** reserve.mintDecimals;
|
|
757
|
+
const borrowed = reserve.borrowedAmountWad / WAD / 10 ** reserve.mintDecimals;
|
|
580
758
|
const totalDeposited = available + borrowed;
|
|
581
759
|
const utilizationPct = totalDeposited > 0 ? borrowed / totalDeposited * 100 : 0;
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
const spreadFeeBps = Number(config.spreadFeeBps);
|
|
588
|
-
const depositAprPct = utilizationPct / 100 * (borrowAprPct / 100) * (1 - spreadFeeBps / 1e4) * 100;
|
|
589
|
-
return { borrowAprPct, depositAprPct, utilizationPct };
|
|
760
|
+
if (reserve.interestRateUtils.length === 0) return { borrowAprPct: 0, depositAprPct: 0 };
|
|
761
|
+
const aprs = reserve.interestRateAprs.map((a) => a / 100);
|
|
762
|
+
const borrowAprPct = interpolateRate(reserve.interestRateUtils, aprs, utilizationPct);
|
|
763
|
+
const depositAprPct = utilizationPct / 100 * (borrowAprPct / 100) * (1 - reserve.spreadFeeBps / 1e4) * 100;
|
|
764
|
+
return { borrowAprPct, depositAprPct };
|
|
590
765
|
}
|
|
591
766
|
function cTokenRatio(reserve) {
|
|
592
|
-
if (reserve.ctokenSupply ===
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
767
|
+
if (reserve.ctokenSupply === 0) return 1;
|
|
768
|
+
const totalSupply = reserve.availableAmount + reserve.borrowedAmountWad / WAD - reserve.unclaimedSpreadFeesWad / WAD;
|
|
769
|
+
return totalSupply / reserve.ctokenSupply;
|
|
770
|
+
}
|
|
771
|
+
function f(obj) {
|
|
772
|
+
if (obj && typeof obj === "object" && "fields" in obj) return obj.fields;
|
|
773
|
+
return obj;
|
|
774
|
+
}
|
|
775
|
+
function str(v) {
|
|
776
|
+
return String(v ?? "0");
|
|
777
|
+
}
|
|
778
|
+
function num(v) {
|
|
779
|
+
return Number(str(v));
|
|
780
|
+
}
|
|
781
|
+
function parseReserve(raw, index) {
|
|
782
|
+
const r = f(raw);
|
|
783
|
+
const coinTypeField = f(r.coin_type);
|
|
784
|
+
const config = f(f(r.config)?.element);
|
|
785
|
+
return {
|
|
786
|
+
coinType: str(coinTypeField?.name),
|
|
787
|
+
mintDecimals: num(r.mint_decimals),
|
|
788
|
+
availableAmount: num(r.available_amount),
|
|
789
|
+
borrowedAmountWad: num(f(r.borrowed_amount)?.value),
|
|
790
|
+
ctokenSupply: num(r.ctoken_supply),
|
|
791
|
+
unclaimedSpreadFeesWad: num(f(r.unclaimed_spread_fees)?.value),
|
|
792
|
+
cumulativeBorrowRateWad: num(f(r.cumulative_borrow_rate)?.value),
|
|
793
|
+
openLtvPct: num(config?.open_ltv_pct),
|
|
794
|
+
closeLtvPct: num(config?.close_ltv_pct),
|
|
795
|
+
spreadFeeBps: num(config?.spread_fee_bps),
|
|
796
|
+
interestRateUtils: Array.isArray(config?.interest_rate_utils) ? config.interest_rate_utils.map(num) : [],
|
|
797
|
+
interestRateAprs: Array.isArray(config?.interest_rate_aprs) ? config.interest_rate_aprs.map(num) : [],
|
|
798
|
+
arrayIndex: index
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
function parseObligation(raw) {
|
|
802
|
+
const deposits = Array.isArray(raw.deposits) ? raw.deposits.map((d) => {
|
|
803
|
+
const df = f(d);
|
|
804
|
+
return {
|
|
805
|
+
coinType: str(f(df.coin_type)?.name),
|
|
806
|
+
ctokenAmount: num(df.deposited_ctoken_amount),
|
|
807
|
+
reserveIdx: num(df.reserve_array_index)
|
|
808
|
+
};
|
|
809
|
+
}) : [];
|
|
810
|
+
const borrows = Array.isArray(raw.borrows) ? raw.borrows.map((b) => {
|
|
811
|
+
const bf = f(b);
|
|
812
|
+
return {
|
|
813
|
+
coinType: str(f(bf.coin_type)?.name),
|
|
814
|
+
borrowedWad: num(f(bf.borrowed_amount)?.value),
|
|
815
|
+
cumBorrowRateWad: num(f(bf.cumulative_borrow_rate)?.value),
|
|
816
|
+
reserveIdx: num(bf.reserve_array_index)
|
|
817
|
+
};
|
|
818
|
+
}) : [];
|
|
819
|
+
return { deposits, borrows };
|
|
598
820
|
}
|
|
599
821
|
var SuilendAdapter = class {
|
|
600
822
|
id = "suilend";
|
|
601
823
|
name = "Suilend";
|
|
602
|
-
version = "
|
|
824
|
+
version = "2.0.0";
|
|
603
825
|
capabilities = ["save", "withdraw"];
|
|
604
826
|
supportedAssets = ["USDC"];
|
|
605
827
|
supportsSameAssetBorrow = false;
|
|
606
828
|
client;
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
initialized = false;
|
|
610
|
-
initPromise = null;
|
|
829
|
+
publishedAt = null;
|
|
830
|
+
reserveCache = null;
|
|
611
831
|
async init(client) {
|
|
612
832
|
this.client = client;
|
|
613
|
-
await this.lazyInit();
|
|
614
833
|
}
|
|
615
834
|
initSync(client) {
|
|
616
835
|
this.client = client;
|
|
617
836
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
if (this.
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
"PROTOCOL_UNAVAILABLE",
|
|
628
|
-
"Suilend SDK not installed. Run: npm install @suilend/sdk@^1"
|
|
629
|
-
);
|
|
630
|
-
}
|
|
631
|
-
this.lendingMarketType = sdk.LENDING_MARKET_TYPE;
|
|
632
|
-
try {
|
|
633
|
-
this.suilend = await sdk.SuilendClient.initialize(
|
|
634
|
-
sdk.LENDING_MARKET_ID,
|
|
635
|
-
sdk.LENDING_MARKET_TYPE,
|
|
636
|
-
this.client
|
|
637
|
-
);
|
|
638
|
-
} catch (err) {
|
|
639
|
-
this.initPromise = null;
|
|
640
|
-
throw new T2000Error(
|
|
641
|
-
"PROTOCOL_UNAVAILABLE",
|
|
642
|
-
`Failed to initialize Suilend: ${err instanceof Error ? err.message : String(err)}`
|
|
643
|
-
);
|
|
837
|
+
// -- On-chain reads -------------------------------------------------------
|
|
838
|
+
async resolvePackage() {
|
|
839
|
+
if (this.publishedAt) return this.publishedAt;
|
|
840
|
+
try {
|
|
841
|
+
const cap = await this.client.getObject({ id: UPGRADE_CAP_ID, options: { showContent: true } });
|
|
842
|
+
if (cap.data?.content?.dataType === "moveObject") {
|
|
843
|
+
const fields = cap.data.content.fields;
|
|
844
|
+
this.publishedAt = str(fields.package);
|
|
845
|
+
return this.publishedAt;
|
|
644
846
|
}
|
|
645
|
-
|
|
646
|
-
}
|
|
647
|
-
|
|
847
|
+
} catch {
|
|
848
|
+
}
|
|
849
|
+
this.publishedAt = FALLBACK_PUBLISHED_AT;
|
|
850
|
+
return this.publishedAt;
|
|
648
851
|
}
|
|
649
|
-
async
|
|
650
|
-
if (
|
|
651
|
-
|
|
852
|
+
async loadReserves(fresh = false) {
|
|
853
|
+
if (this.reserveCache && !fresh) return this.reserveCache;
|
|
854
|
+
const market = await this.client.getObject({
|
|
855
|
+
id: LENDING_MARKET_ID,
|
|
856
|
+
options: { showContent: true }
|
|
857
|
+
});
|
|
858
|
+
if (market.data?.content?.dataType !== "moveObject") {
|
|
859
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", "Failed to read Suilend lending market");
|
|
652
860
|
}
|
|
861
|
+
const fields = market.data.content.fields;
|
|
862
|
+
const reservesRaw = fields.reserves;
|
|
863
|
+
if (!Array.isArray(reservesRaw)) {
|
|
864
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", "Failed to parse Suilend reserves");
|
|
865
|
+
}
|
|
866
|
+
this.reserveCache = reservesRaw.map((r, i) => parseReserve(r, i));
|
|
867
|
+
return this.reserveCache;
|
|
653
868
|
}
|
|
654
|
-
findReserve(asset) {
|
|
869
|
+
findReserve(reserves, asset) {
|
|
655
870
|
const upper = asset.toUpperCase();
|
|
656
871
|
let coinType;
|
|
657
872
|
if (upper === "USDC") coinType = USDC_TYPE2;
|
|
@@ -660,187 +875,229 @@ var SuilendAdapter = class {
|
|
|
660
875
|
else return void 0;
|
|
661
876
|
try {
|
|
662
877
|
const normalized = normalizeStructTag(coinType);
|
|
663
|
-
return
|
|
664
|
-
|
|
665
|
-
|
|
878
|
+
return reserves.find((r) => {
|
|
879
|
+
try {
|
|
880
|
+
return normalizeStructTag(r.coinType) === normalized;
|
|
881
|
+
} catch {
|
|
882
|
+
return false;
|
|
883
|
+
}
|
|
884
|
+
});
|
|
666
885
|
} catch {
|
|
667
886
|
return void 0;
|
|
668
887
|
}
|
|
669
888
|
}
|
|
670
|
-
async
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
889
|
+
async fetchObligationCaps(address) {
|
|
890
|
+
const capType = `${SUILEND_PACKAGE}::lending_market::ObligationOwnerCap<${LENDING_MARKET_TYPE}>`;
|
|
891
|
+
const caps = [];
|
|
892
|
+
let cursor;
|
|
893
|
+
let hasNext = true;
|
|
894
|
+
while (hasNext) {
|
|
895
|
+
const page = await this.client.getOwnedObjects({
|
|
896
|
+
owner: address,
|
|
897
|
+
filter: { StructType: capType },
|
|
898
|
+
options: { showContent: true },
|
|
899
|
+
cursor: cursor ?? void 0
|
|
900
|
+
});
|
|
901
|
+
for (const item of page.data) {
|
|
902
|
+
if (item.data?.content?.dataType !== "moveObject") continue;
|
|
903
|
+
const fields = item.data.content.fields;
|
|
904
|
+
caps.push({
|
|
905
|
+
id: item.data.objectId,
|
|
906
|
+
obligationId: str(fields.obligation_id)
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
cursor = page.nextCursor;
|
|
910
|
+
hasNext = page.hasNextPage;
|
|
911
|
+
}
|
|
912
|
+
return caps;
|
|
913
|
+
}
|
|
914
|
+
async fetchObligation(obligationId) {
|
|
915
|
+
const obj = await this.client.getObject({ id: obligationId, options: { showContent: true } });
|
|
916
|
+
if (obj.data?.content?.dataType !== "moveObject") {
|
|
917
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", "Failed to read Suilend obligation");
|
|
918
|
+
}
|
|
919
|
+
return parseObligation(obj.data.content.fields);
|
|
677
920
|
}
|
|
678
921
|
resolveSymbol(coinType) {
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
922
|
+
try {
|
|
923
|
+
const normalized = normalizeStructTag(coinType);
|
|
924
|
+
if (normalized === normalizeStructTag(USDC_TYPE2)) return "USDC";
|
|
925
|
+
if (normalized === normalizeStructTag("0x2::sui::SUI")) return "SUI";
|
|
926
|
+
} catch {
|
|
927
|
+
}
|
|
682
928
|
const parts = coinType.split("::");
|
|
683
929
|
return parts[parts.length - 1] || "UNKNOWN";
|
|
684
930
|
}
|
|
931
|
+
// -- Adapter interface ----------------------------------------------------
|
|
685
932
|
async getRates(asset) {
|
|
686
|
-
await this.
|
|
687
|
-
const reserve = this.findReserve(asset);
|
|
688
|
-
if (!reserve) {
|
|
689
|
-
|
|
690
|
-
}
|
|
691
|
-
const { borrowAprPct, depositAprPct } = computeRatesFromReserve(reserve);
|
|
692
|
-
return {
|
|
693
|
-
asset,
|
|
694
|
-
saveApy: depositAprPct,
|
|
695
|
-
borrowApy: borrowAprPct
|
|
696
|
-
};
|
|
933
|
+
const reserves = await this.loadReserves();
|
|
934
|
+
const reserve = this.findReserve(reserves, asset);
|
|
935
|
+
if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `Suilend does not support ${asset}`);
|
|
936
|
+
const { borrowAprPct, depositAprPct } = computeRates(reserve);
|
|
937
|
+
return { asset, saveApy: depositAprPct, borrowApy: borrowAprPct };
|
|
697
938
|
}
|
|
698
939
|
async getPositions(address) {
|
|
699
|
-
await this.ensureInit();
|
|
700
940
|
const supplies = [];
|
|
701
941
|
const borrows = [];
|
|
702
|
-
const caps = await this.
|
|
942
|
+
const caps = await this.fetchObligationCaps(address);
|
|
703
943
|
if (caps.length === 0) return { supplies, borrows };
|
|
704
|
-
const obligation = await
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
944
|
+
const [reserves, obligation] = await Promise.all([
|
|
945
|
+
this.loadReserves(),
|
|
946
|
+
this.fetchObligation(caps[0].obligationId)
|
|
947
|
+
]);
|
|
948
|
+
for (const dep of obligation.deposits) {
|
|
949
|
+
const reserve = reserves[dep.reserveIdx];
|
|
710
950
|
if (!reserve) continue;
|
|
711
|
-
const ctokenAmount = Number(deposit.depositedCtokenAmount.toString());
|
|
712
951
|
const ratio = cTokenRatio(reserve);
|
|
713
|
-
const amount = ctokenAmount * ratio / 10 ** reserve.mintDecimals;
|
|
714
|
-
const { depositAprPct } =
|
|
715
|
-
supplies.push({ asset: this.resolveSymbol(coinType), amount, apy: depositAprPct });
|
|
952
|
+
const amount = dep.ctokenAmount * ratio / 10 ** reserve.mintDecimals;
|
|
953
|
+
const { depositAprPct } = computeRates(reserve);
|
|
954
|
+
supplies.push({ asset: this.resolveSymbol(dep.coinType), amount, apy: depositAprPct });
|
|
716
955
|
}
|
|
717
|
-
for (const
|
|
718
|
-
const
|
|
719
|
-
const reserve = this.suilend.lendingMarket.reserves.find(
|
|
720
|
-
(r) => normalizeStructTag(r.coinType.name) === coinType
|
|
721
|
-
);
|
|
956
|
+
for (const bor of obligation.borrows) {
|
|
957
|
+
const reserve = reserves[bor.reserveIdx];
|
|
722
958
|
if (!reserve) continue;
|
|
723
|
-
const
|
|
724
|
-
const
|
|
725
|
-
const
|
|
726
|
-
const
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
borrows.push({ asset: this.resolveSymbol(coinType), amount: compounded, apy: borrowAprPct });
|
|
959
|
+
const rawAmount = bor.borrowedWad / WAD / 10 ** reserve.mintDecimals;
|
|
960
|
+
const reserveRate = reserve.cumulativeBorrowRateWad / WAD;
|
|
961
|
+
const posRate = bor.cumBorrowRateWad / WAD;
|
|
962
|
+
const compounded = posRate > 0 ? rawAmount * (reserveRate / posRate) : rawAmount;
|
|
963
|
+
const { borrowAprPct } = computeRates(reserve);
|
|
964
|
+
borrows.push({ asset: this.resolveSymbol(bor.coinType), amount: compounded, apy: borrowAprPct });
|
|
730
965
|
}
|
|
731
966
|
return { supplies, borrows };
|
|
732
967
|
}
|
|
733
968
|
async getHealth(address) {
|
|
734
|
-
await this.
|
|
735
|
-
const caps = await this.getObligationCaps(address);
|
|
969
|
+
const caps = await this.fetchObligationCaps(address);
|
|
736
970
|
if (caps.length === 0) {
|
|
737
971
|
return { healthFactor: Infinity, supplied: 0, borrowed: 0, maxBorrow: 0, liquidationThreshold: 0 };
|
|
738
972
|
}
|
|
739
973
|
const positions = await this.getPositions(address);
|
|
740
974
|
const supplied = positions.supplies.reduce((s, p) => s + p.amount, 0);
|
|
741
975
|
const borrowed = positions.borrows.reduce((s, p) => s + p.amount, 0);
|
|
742
|
-
const
|
|
743
|
-
const
|
|
744
|
-
const
|
|
976
|
+
const reserves = await this.loadReserves();
|
|
977
|
+
const reserve = this.findReserve(reserves, "USDC");
|
|
978
|
+
const closeLtv = reserve?.closeLtvPct ?? 75;
|
|
979
|
+
const openLtv = reserve?.openLtvPct ?? 70;
|
|
745
980
|
const liqThreshold = closeLtv / 100;
|
|
746
981
|
const healthFactor = borrowed > 0 ? supplied * liqThreshold / borrowed : Infinity;
|
|
747
982
|
const maxBorrow = Math.max(0, supplied * (openLtv / 100) - borrowed);
|
|
748
983
|
return { healthFactor, supplied, borrowed, maxBorrow, liquidationThreshold: liqThreshold };
|
|
749
984
|
}
|
|
750
985
|
async buildSaveTx(address, amount, _asset, options) {
|
|
751
|
-
await this.
|
|
752
|
-
const
|
|
986
|
+
const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
|
|
987
|
+
const usdcReserve = this.findReserve(reserves, "USDC");
|
|
988
|
+
if (!usdcReserve) throw new T2000Error("ASSET_NOT_SUPPORTED", "USDC reserve not found on Suilend");
|
|
989
|
+
const caps = await this.fetchObligationCaps(address);
|
|
753
990
|
const tx = new Transaction();
|
|
754
991
|
tx.setSender(address);
|
|
755
|
-
const caps = await this.getObligationCaps(address);
|
|
756
992
|
let capRef;
|
|
757
993
|
if (caps.length === 0) {
|
|
758
|
-
const [newCap] =
|
|
994
|
+
const [newCap] = tx.moveCall({
|
|
995
|
+
target: `${pkg}::lending_market::create_obligation`,
|
|
996
|
+
typeArguments: [LENDING_MARKET_TYPE],
|
|
997
|
+
arguments: [tx.object(LENDING_MARKET_ID)]
|
|
998
|
+
});
|
|
759
999
|
capRef = newCap;
|
|
760
1000
|
} else {
|
|
761
1001
|
capRef = caps[0].id;
|
|
762
1002
|
}
|
|
763
1003
|
const allCoins = await this.fetchAllCoins(address, USDC_TYPE2);
|
|
764
|
-
if (allCoins.length === 0)
|
|
765
|
-
throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins found");
|
|
766
|
-
}
|
|
1004
|
+
if (allCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins found");
|
|
767
1005
|
const primaryCoinId = allCoins[0].coinObjectId;
|
|
768
1006
|
if (allCoins.length > 1) {
|
|
769
|
-
tx.mergeCoins(
|
|
770
|
-
tx.object(primaryCoinId),
|
|
771
|
-
allCoins.slice(1).map((c) => tx.object(c.coinObjectId))
|
|
772
|
-
);
|
|
1007
|
+
tx.mergeCoins(tx.object(primaryCoinId), allCoins.slice(1).map((c) => tx.object(c.coinObjectId)));
|
|
773
1008
|
}
|
|
1009
|
+
const rawAmount = usdcToRaw(amount).toString();
|
|
774
1010
|
const [depositCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawAmount]);
|
|
775
1011
|
if (options?.collectFee) {
|
|
776
1012
|
addCollectFeeToTx(tx, depositCoin, "save");
|
|
777
1013
|
}
|
|
778
|
-
|
|
1014
|
+
const [ctokens] = tx.moveCall({
|
|
1015
|
+
target: `${pkg}::lending_market::deposit_liquidity_and_mint_ctokens`,
|
|
1016
|
+
typeArguments: [LENDING_MARKET_TYPE, USDC_TYPE2],
|
|
1017
|
+
arguments: [
|
|
1018
|
+
tx.object(LENDING_MARKET_ID),
|
|
1019
|
+
tx.pure.u64(usdcReserve.arrayIndex),
|
|
1020
|
+
tx.object(CLOCK2),
|
|
1021
|
+
depositCoin
|
|
1022
|
+
]
|
|
1023
|
+
});
|
|
1024
|
+
tx.moveCall({
|
|
1025
|
+
target: `${pkg}::lending_market::deposit_ctokens_into_obligation`,
|
|
1026
|
+
typeArguments: [LENDING_MARKET_TYPE, USDC_TYPE2],
|
|
1027
|
+
arguments: [
|
|
1028
|
+
tx.object(LENDING_MARKET_ID),
|
|
1029
|
+
tx.pure.u64(usdcReserve.arrayIndex),
|
|
1030
|
+
typeof capRef === "string" ? tx.object(capRef) : capRef,
|
|
1031
|
+
tx.object(CLOCK2),
|
|
1032
|
+
ctokens
|
|
1033
|
+
]
|
|
1034
|
+
});
|
|
779
1035
|
return { tx };
|
|
780
1036
|
}
|
|
781
1037
|
async buildWithdrawTx(address, amount, _asset) {
|
|
782
|
-
await this.
|
|
783
|
-
const
|
|
784
|
-
if (
|
|
785
|
-
|
|
786
|
-
|
|
1038
|
+
const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves(true)]);
|
|
1039
|
+
const usdcReserve = this.findReserve(reserves, "USDC");
|
|
1040
|
+
if (!usdcReserve) throw new T2000Error("ASSET_NOT_SUPPORTED", "USDC reserve not found on Suilend");
|
|
1041
|
+
const caps = await this.fetchObligationCaps(address);
|
|
1042
|
+
if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found");
|
|
787
1043
|
const positions = await this.getPositions(address);
|
|
788
|
-
const
|
|
789
|
-
const deposited = usdcSupply?.amount ?? 0;
|
|
1044
|
+
const deposited = positions.supplies.find((s) => s.asset === "USDC")?.amount ?? 0;
|
|
790
1045
|
const effectiveAmount = Math.min(amount, deposited);
|
|
791
|
-
if (effectiveAmount <= 0)
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
const rawAmount = usdcToRaw(effectiveAmount).toString();
|
|
1046
|
+
if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", "Nothing to withdraw from Suilend");
|
|
1047
|
+
const ratio = cTokenRatio(usdcReserve);
|
|
1048
|
+
const ctokenAmount = Math.ceil(effectiveAmount * 10 ** usdcReserve.mintDecimals / ratio);
|
|
795
1049
|
const tx = new Transaction();
|
|
796
1050
|
tx.setSender(address);
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
1051
|
+
const [ctokens] = tx.moveCall({
|
|
1052
|
+
target: `${pkg}::lending_market::withdraw_ctokens`,
|
|
1053
|
+
typeArguments: [LENDING_MARKET_TYPE, USDC_TYPE2],
|
|
1054
|
+
arguments: [
|
|
1055
|
+
tx.object(LENDING_MARKET_ID),
|
|
1056
|
+
tx.pure.u64(usdcReserve.arrayIndex),
|
|
1057
|
+
tx.object(caps[0].id),
|
|
1058
|
+
tx.object(CLOCK2),
|
|
1059
|
+
tx.pure.u64(ctokenAmount)
|
|
1060
|
+
]
|
|
1061
|
+
});
|
|
1062
|
+
const exemptionType = `${SUILEND_PACKAGE}::lending_market::RateLimiterExemption<${LENDING_MARKET_TYPE}, ${USDC_TYPE2}>`;
|
|
1063
|
+
const [none] = tx.moveCall({
|
|
1064
|
+
target: "0x1::option::none",
|
|
1065
|
+
typeArguments: [exemptionType]
|
|
1066
|
+
});
|
|
1067
|
+
const [coin] = tx.moveCall({
|
|
1068
|
+
target: `${pkg}::lending_market::redeem_ctokens_and_withdraw_liquidity`,
|
|
1069
|
+
typeArguments: [LENDING_MARKET_TYPE, USDC_TYPE2],
|
|
1070
|
+
arguments: [
|
|
1071
|
+
tx.object(LENDING_MARKET_ID),
|
|
1072
|
+
tx.pure.u64(usdcReserve.arrayIndex),
|
|
1073
|
+
tx.object(CLOCK2),
|
|
1074
|
+
ctokens,
|
|
1075
|
+
none
|
|
1076
|
+
]
|
|
1077
|
+
});
|
|
1078
|
+
tx.transferObjects([coin], address);
|
|
805
1079
|
return { tx, effectiveAmount };
|
|
806
1080
|
}
|
|
807
1081
|
async buildBorrowTx(_address, _amount, _asset, _options) {
|
|
808
|
-
throw new T2000Error(
|
|
809
|
-
"ASSET_NOT_SUPPORTED",
|
|
810
|
-
"SuilendAdapter.buildBorrowTx() not available \u2014 Suilend requires different collateral/borrow assets. Deferred to Phase 10."
|
|
811
|
-
);
|
|
1082
|
+
throw new T2000Error("ASSET_NOT_SUPPORTED", "Suilend borrow requires different collateral/borrow assets. Deferred to Phase 10.");
|
|
812
1083
|
}
|
|
813
1084
|
async buildRepayTx(_address, _amount, _asset) {
|
|
814
|
-
throw new T2000Error(
|
|
815
|
-
"ASSET_NOT_SUPPORTED",
|
|
816
|
-
"SuilendAdapter.buildRepayTx() not available \u2014 deferred to Phase 10."
|
|
817
|
-
);
|
|
1085
|
+
throw new T2000Error("ASSET_NOT_SUPPORTED", "Suilend repay deferred to Phase 10.");
|
|
818
1086
|
}
|
|
819
1087
|
async maxWithdraw(address, _asset) {
|
|
820
|
-
await this.ensureInit();
|
|
821
1088
|
const health = await this.getHealth(address);
|
|
822
1089
|
let maxAmount;
|
|
823
1090
|
if (health.borrowed === 0) {
|
|
824
1091
|
maxAmount = health.supplied;
|
|
825
1092
|
} else {
|
|
826
|
-
maxAmount = Math.max(
|
|
827
|
-
0,
|
|
828
|
-
health.supplied - health.borrowed * MIN_HEALTH_FACTOR2 / health.liquidationThreshold
|
|
829
|
-
);
|
|
1093
|
+
maxAmount = Math.max(0, health.supplied - health.borrowed * MIN_HEALTH_FACTOR2 / health.liquidationThreshold);
|
|
830
1094
|
}
|
|
831
1095
|
const remainingSupply = health.supplied - maxAmount;
|
|
832
1096
|
const hfAfter = health.borrowed > 0 ? remainingSupply * health.liquidationThreshold / health.borrowed : Infinity;
|
|
833
|
-
return {
|
|
834
|
-
maxAmount,
|
|
835
|
-
healthFactorAfter: hfAfter,
|
|
836
|
-
currentHF: health.healthFactor
|
|
837
|
-
};
|
|
1097
|
+
return { maxAmount, healthFactorAfter: hfAfter, currentHF: health.healthFactor };
|
|
838
1098
|
}
|
|
839
1099
|
async maxBorrow(_address, _asset) {
|
|
840
|
-
throw new T2000Error(
|
|
841
|
-
"ASSET_NOT_SUPPORTED",
|
|
842
|
-
"SuilendAdapter.maxBorrow() not available \u2014 deferred to Phase 10."
|
|
843
|
-
);
|
|
1100
|
+
throw new T2000Error("ASSET_NOT_SUPPORTED", "Suilend maxBorrow deferred to Phase 10.");
|
|
844
1101
|
}
|
|
845
1102
|
async fetchAllCoins(owner, coinType) {
|
|
846
1103
|
const all = [];
|