@t2000/sdk 0.18.10 → 0.18.11
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 +17 -2
- package/dist/adapters/index.cjs +458 -1196
- package/dist/adapters/index.cjs.map +1 -1
- package/dist/adapters/index.d.cts +1 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.js +458 -1196
- package/dist/adapters/index.js.map +1 -1
- package/dist/{index-Co0lp99l.d.cts → index-YBZIJANR.d.cts} +6 -20
- package/dist/{index-Co0lp99l.d.ts → index-YBZIJANR.d.ts} +6 -20
- package/dist/index.cjs +463 -1202
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +463 -1202
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
package/dist/adapters/index.cjs
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var transactions = require('@mysten/sui/transactions');
|
|
4
|
-
var
|
|
4
|
+
var lending = require('@naviprotocol/lending');
|
|
5
5
|
var aggregatorSdk = require('@cetusprotocol/aggregator-sdk');
|
|
6
6
|
var utils = require('@mysten/sui/utils');
|
|
7
|
+
var client = require('@suilend/sdk/client');
|
|
8
|
+
var initialize = require('@suilend/sdk/lib/initialize');
|
|
9
|
+
var types = require('@suilend/sdk/lib/types');
|
|
10
|
+
require('@mysten/sui/bcs');
|
|
7
11
|
|
|
8
12
|
// src/constants.ts
|
|
9
13
|
var SAVE_FEE_BPS = 10n;
|
|
@@ -282,79 +286,34 @@ function addCollectFeeToTx(tx, paymentCoin, operation) {
|
|
|
282
286
|
]
|
|
283
287
|
});
|
|
284
288
|
}
|
|
285
|
-
|
|
286
|
-
|
|
289
|
+
|
|
290
|
+
// src/protocols/navi.ts
|
|
287
291
|
var MIN_HEALTH_FACTOR = 1.5;
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
var CLOCK = "0x06";
|
|
292
|
-
var SUI_SYSTEM_STATE = "0x05";
|
|
293
|
-
var NAVI_BALANCE_DECIMALS = 9;
|
|
294
|
-
var CONFIG_API = "https://open-api.naviprotocol.io/api/navi/config?env=prod";
|
|
295
|
-
var POOLS_API = "https://open-api.naviprotocol.io/api/navi/pools?env=prod";
|
|
296
|
-
var PACKAGE_API = "https://open-api.naviprotocol.io/api/package";
|
|
297
|
-
var packageCache = null;
|
|
298
|
-
function toBigInt(v) {
|
|
299
|
-
if (typeof v === "bigint") return v;
|
|
300
|
-
return BigInt(String(v));
|
|
301
|
-
}
|
|
302
|
-
var UserStateInfo = bcs.bcs.struct("UserStateInfo", {
|
|
303
|
-
asset_id: bcs.bcs.u8(),
|
|
304
|
-
borrow_balance: bcs.bcs.u256(),
|
|
305
|
-
supply_balance: bcs.bcs.u256()
|
|
306
|
-
});
|
|
307
|
-
function decodeDevInspect(result, schema) {
|
|
308
|
-
const rv = result.results?.[0]?.returnValues?.[0];
|
|
309
|
-
if (result.error || !rv) return void 0;
|
|
310
|
-
const bytes = Uint8Array.from(rv[0]);
|
|
311
|
-
return schema.parse(bytes);
|
|
312
|
-
}
|
|
313
|
-
var configCache = null;
|
|
314
|
-
var poolsCache = null;
|
|
315
|
-
var CACHE_TTL = 5 * 6e4;
|
|
316
|
-
async function fetchJson(url) {
|
|
317
|
-
const res = await fetch(url);
|
|
318
|
-
if (!res.ok) throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI API error: ${res.status}`);
|
|
319
|
-
const json = await res.json();
|
|
320
|
-
return json.data ?? json;
|
|
321
|
-
}
|
|
322
|
-
async function getLatestPackageId() {
|
|
323
|
-
if (packageCache && Date.now() - packageCache.ts < CACHE_TTL) return packageCache.id;
|
|
324
|
-
const res = await fetch(PACKAGE_API);
|
|
325
|
-
if (!res.ok) throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI package API error: ${res.status}`);
|
|
326
|
-
const json = await res.json();
|
|
327
|
-
if (!json.packageId) throw new T2000Error("PROTOCOL_UNAVAILABLE", "NAVI package API returned no packageId");
|
|
328
|
-
packageCache = { id: json.packageId, ts: Date.now() };
|
|
329
|
-
return json.packageId;
|
|
330
|
-
}
|
|
331
|
-
async function getConfig(fresh = false) {
|
|
332
|
-
if (configCache && !fresh && Date.now() - configCache.ts < CACHE_TTL) return configCache.data;
|
|
333
|
-
const [data, latestPkg] = await Promise.all([
|
|
334
|
-
fetchJson(CONFIG_API),
|
|
335
|
-
getLatestPackageId()
|
|
336
|
-
]);
|
|
337
|
-
data.package = latestPkg;
|
|
338
|
-
configCache = { data, ts: Date.now() };
|
|
339
|
-
return data;
|
|
340
|
-
}
|
|
341
|
-
async function getPools(fresh = false) {
|
|
342
|
-
if (poolsCache && !fresh && Date.now() - poolsCache.ts < CACHE_TTL) return poolsCache.data;
|
|
343
|
-
const data = await fetchJson(POOLS_API);
|
|
344
|
-
poolsCache = { data, ts: Date.now() };
|
|
345
|
-
return data;
|
|
346
|
-
}
|
|
347
|
-
function matchesCoinType(poolType, targetType) {
|
|
348
|
-
const poolSuffix = poolType.split("::").slice(1).join("::").toLowerCase();
|
|
349
|
-
const targetSuffix = targetType.split("::").slice(1).join("::").toLowerCase();
|
|
350
|
-
return poolSuffix === targetSuffix;
|
|
292
|
+
var NAVI_SUPPORTED_ASSETS = [...STABLE_ASSETS, "SUI", "ETH", "GOLD"];
|
|
293
|
+
function sdkOptions(client) {
|
|
294
|
+
return { env: "prod", client };
|
|
351
295
|
}
|
|
352
|
-
|
|
353
|
-
|
|
296
|
+
var NAVI_SYMBOL_MAP = {
|
|
297
|
+
nUSDC: "USDC",
|
|
298
|
+
suiUSDT: "USDT",
|
|
299
|
+
suiUSDe: "USDe",
|
|
300
|
+
XAUM: "GOLD",
|
|
301
|
+
WBTC: "BTC",
|
|
302
|
+
suiETH: "ETH",
|
|
303
|
+
WETH: "ETH",
|
|
304
|
+
SUI: "SUI",
|
|
305
|
+
USDC: "USDC",
|
|
306
|
+
USDT: "USDT",
|
|
307
|
+
USDe: "USDe",
|
|
308
|
+
USDsui: "USDsui"
|
|
309
|
+
};
|
|
310
|
+
function resolveNaviSymbol(sdkSymbol, coinType) {
|
|
354
311
|
for (const [key, info] of Object.entries(SUPPORTED_ASSETS)) {
|
|
355
|
-
|
|
312
|
+
const poolSuffix = coinType.split("::").slice(1).join("::").toLowerCase();
|
|
313
|
+
const targetSuffix = info.type.split("::").slice(1).join("::").toLowerCase();
|
|
314
|
+
if (poolSuffix === targetSuffix) return key;
|
|
356
315
|
}
|
|
357
|
-
return
|
|
316
|
+
return NAVI_SYMBOL_MAP[sdkSymbol] ?? sdkSymbol;
|
|
358
317
|
}
|
|
359
318
|
function resolveAssetInfo(asset) {
|
|
360
319
|
if (asset in SUPPORTED_ASSETS) {
|
|
@@ -363,109 +322,6 @@ function resolveAssetInfo(asset) {
|
|
|
363
322
|
}
|
|
364
323
|
throw new T2000Error("ASSET_NOT_SUPPORTED", `Unknown asset: ${asset}`);
|
|
365
324
|
}
|
|
366
|
-
async function getPool(asset = "USDC") {
|
|
367
|
-
const pools = await getPools();
|
|
368
|
-
const { type: targetType, displayName } = resolveAssetInfo(asset);
|
|
369
|
-
const pool = pools.find(
|
|
370
|
-
(p) => matchesCoinType(p.suiCoinType || p.coinType || "", targetType)
|
|
371
|
-
);
|
|
372
|
-
if (!pool) {
|
|
373
|
-
throw new T2000Error(
|
|
374
|
-
"ASSET_NOT_SUPPORTED",
|
|
375
|
-
`${displayName} pool not found on NAVI`
|
|
376
|
-
);
|
|
377
|
-
}
|
|
378
|
-
return pool;
|
|
379
|
-
}
|
|
380
|
-
function addOracleUpdate(tx, config, pool) {
|
|
381
|
-
const feed = config.oracle.feeds?.find((f2) => f2.assetId === pool.id);
|
|
382
|
-
if (!feed) {
|
|
383
|
-
throw new T2000Error("PROTOCOL_UNAVAILABLE", `Oracle feed not found for asset ${pool.token?.symbol ?? pool.id}`);
|
|
384
|
-
}
|
|
385
|
-
tx.moveCall({
|
|
386
|
-
target: `${config.oracle.packageId}::oracle_pro::update_single_price_v2`,
|
|
387
|
-
arguments: [
|
|
388
|
-
tx.object(CLOCK),
|
|
389
|
-
tx.object(config.oracle.oracleConfig),
|
|
390
|
-
tx.object(config.oracle.priceOracle),
|
|
391
|
-
tx.object(config.oracle.supraOracleHolder),
|
|
392
|
-
tx.object(feed.pythPriceInfoObject),
|
|
393
|
-
tx.object(config.oracle.switchboardAggregator),
|
|
394
|
-
tx.pure.address(feed.feedId)
|
|
395
|
-
]
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
function refreshOracles(tx, config, pools, opts) {
|
|
399
|
-
const assetsToRefresh = NAVI_SUPPORTED_ASSETS;
|
|
400
|
-
const targetTypes = assetsToRefresh.map((a) => SUPPORTED_ASSETS[a].type);
|
|
401
|
-
const matchedPools = pools.filter((p) => {
|
|
402
|
-
const ct = p.suiCoinType || p.coinType || "";
|
|
403
|
-
return targetTypes.some((t) => matchesCoinType(ct, t));
|
|
404
|
-
});
|
|
405
|
-
for (const pool of matchedPools) {
|
|
406
|
-
addOracleUpdate(tx, config, pool);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
function rateToApy(rawRate) {
|
|
410
|
-
if (!rawRate || rawRate === "0") return 0;
|
|
411
|
-
return Number(BigInt(rawRate)) / 10 ** RATE_DECIMALS * 100;
|
|
412
|
-
}
|
|
413
|
-
function poolSaveApy(pool) {
|
|
414
|
-
const incentive = parseFloat(pool.supplyIncentiveApyInfo?.apy ?? "0");
|
|
415
|
-
if (incentive > 0) return incentive;
|
|
416
|
-
return rateToApy(pool.currentSupplyRate);
|
|
417
|
-
}
|
|
418
|
-
function poolBorrowApy(pool) {
|
|
419
|
-
const incentive = parseFloat(pool.borrowIncentiveApyInfo?.apy ?? "0");
|
|
420
|
-
if (incentive > 0) return incentive;
|
|
421
|
-
return rateToApy(pool.currentBorrowRate);
|
|
422
|
-
}
|
|
423
|
-
function parseLtv(rawLtv) {
|
|
424
|
-
if (!rawLtv || rawLtv === "0") return 0.75;
|
|
425
|
-
return Number(BigInt(rawLtv)) / 10 ** LTV_DECIMALS;
|
|
426
|
-
}
|
|
427
|
-
function parseLiqThreshold(val) {
|
|
428
|
-
if (typeof val === "number") return val;
|
|
429
|
-
const n = Number(val);
|
|
430
|
-
if (n > 1) return Number(BigInt(val)) / 10 ** LTV_DECIMALS;
|
|
431
|
-
return n;
|
|
432
|
-
}
|
|
433
|
-
function normalizeHealthFactor(raw) {
|
|
434
|
-
const v = raw / 10 ** RATE_DECIMALS;
|
|
435
|
-
return v > 1e5 ? Infinity : v;
|
|
436
|
-
}
|
|
437
|
-
function naviStorageDecimals(poolId, tokenDecimals) {
|
|
438
|
-
if (poolId <= 10) return NAVI_BALANCE_DECIMALS;
|
|
439
|
-
return tokenDecimals;
|
|
440
|
-
}
|
|
441
|
-
function compoundBalance(rawBalance, currentIndex, pool) {
|
|
442
|
-
if (!rawBalance || !currentIndex || currentIndex === "0") return 0;
|
|
443
|
-
const scale = BigInt("1" + "0".repeat(RATE_DECIMALS));
|
|
444
|
-
const half = scale / 2n;
|
|
445
|
-
const result = (rawBalance * BigInt(currentIndex) + half) / scale;
|
|
446
|
-
const decimals = pool ? naviStorageDecimals(pool.id, pool.token.decimals) : NAVI_BALANCE_DECIMALS;
|
|
447
|
-
return Number(result) / 10 ** decimals;
|
|
448
|
-
}
|
|
449
|
-
async function getUserState(client, address) {
|
|
450
|
-
const config = await getConfig();
|
|
451
|
-
const tx = new transactions.Transaction();
|
|
452
|
-
tx.moveCall({
|
|
453
|
-
target: `${config.uiGetter}::getter_unchecked::get_user_state`,
|
|
454
|
-
arguments: [tx.object(config.storage), tx.pure.address(address)]
|
|
455
|
-
});
|
|
456
|
-
const result = await client.devInspectTransactionBlock({
|
|
457
|
-
transactionBlock: tx,
|
|
458
|
-
sender: address
|
|
459
|
-
});
|
|
460
|
-
const decoded = decodeDevInspect(result, bcs.bcs.vector(UserStateInfo));
|
|
461
|
-
if (!decoded) return [];
|
|
462
|
-
const mapped = decoded.map((s) => ({
|
|
463
|
-
assetId: s.asset_id,
|
|
464
|
-
supplyBalance: toBigInt(s.supply_balance),
|
|
465
|
-
borrowBalance: toBigInt(s.borrow_balance)
|
|
466
|
-
}));
|
|
467
|
-
return mapped.filter((s) => s.supplyBalance !== 0n || s.borrowBalance !== 0n);
|
|
468
|
-
}
|
|
469
325
|
async function fetchCoins(client, owner, coinType) {
|
|
470
326
|
const all = [];
|
|
471
327
|
let cursor;
|
|
@@ -486,14 +342,98 @@ function mergeCoins(tx, coins) {
|
|
|
486
342
|
}
|
|
487
343
|
return primary;
|
|
488
344
|
}
|
|
345
|
+
async function getPositions(client, addressOrKeypair) {
|
|
346
|
+
const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
|
|
347
|
+
try {
|
|
348
|
+
const naviPositions = await lending.getLendingPositions(address, {
|
|
349
|
+
...sdkOptions(client),
|
|
350
|
+
markets: ["main"]
|
|
351
|
+
});
|
|
352
|
+
const positions = [];
|
|
353
|
+
for (const pos of naviPositions) {
|
|
354
|
+
const data = pos["navi-lending-supply"] ?? pos["navi-lending-emode-supply"] ?? pos["navi-lending-borrow"] ?? pos["navi-lending-emode-borrow"];
|
|
355
|
+
if (!data) continue;
|
|
356
|
+
const isBorrow = pos.type.includes("borrow");
|
|
357
|
+
const symbol = resolveNaviSymbol(data.token.symbol, data.token.coinType);
|
|
358
|
+
const amount = parseFloat(data.amount);
|
|
359
|
+
const amountUsd = parseFloat(data.valueUSD);
|
|
360
|
+
const pool = data.pool;
|
|
361
|
+
const apy = isBorrow ? parseFloat(pool.borrowIncentiveApyInfo?.apy ?? "0") : parseFloat(pool.supplyIncentiveApyInfo?.apy ?? "0");
|
|
362
|
+
if (amount > 1e-4 || amountUsd > 1e-3) {
|
|
363
|
+
positions.push({
|
|
364
|
+
protocol: "navi",
|
|
365
|
+
asset: symbol,
|
|
366
|
+
type: isBorrow ? "borrow" : "save",
|
|
367
|
+
amount,
|
|
368
|
+
amountUsd,
|
|
369
|
+
apy
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return { positions };
|
|
374
|
+
} catch (err) {
|
|
375
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
376
|
+
if (msg.includes("not found") || msg.includes("404")) return { positions: [] };
|
|
377
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI getPositions failed: ${msg}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
async function getRates(client) {
|
|
381
|
+
try {
|
|
382
|
+
const pools = await lending.getPools(sdkOptions(client));
|
|
383
|
+
const result = {};
|
|
384
|
+
for (const asset of NAVI_SUPPORTED_ASSETS) {
|
|
385
|
+
const targetType = SUPPORTED_ASSETS[asset].type;
|
|
386
|
+
const pool = pools.find((p) => {
|
|
387
|
+
const poolSuffix = (p.suiCoinType || p.coinType || "").split("::").slice(1).join("::").toLowerCase();
|
|
388
|
+
const targetSuffix = targetType.split("::").slice(1).join("::").toLowerCase();
|
|
389
|
+
return poolSuffix === targetSuffix;
|
|
390
|
+
});
|
|
391
|
+
if (!pool) continue;
|
|
392
|
+
const saveApy = parseFloat(pool.supplyIncentiveApyInfo?.apy ?? "0");
|
|
393
|
+
const borrowApy = parseFloat(pool.borrowIncentiveApyInfo?.apy ?? "0");
|
|
394
|
+
if (saveApy >= 0 && saveApy < 200) {
|
|
395
|
+
result[asset] = { saveApy, borrowApy: borrowApy >= 0 && borrowApy < 200 ? borrowApy : 0 };
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (!result.USDC) result.USDC = { saveApy: 4, borrowApy: 6 };
|
|
399
|
+
return result;
|
|
400
|
+
} catch {
|
|
401
|
+
return { USDC: { saveApy: 4, borrowApy: 6 } };
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
async function getHealthFactor(client, addressOrKeypair) {
|
|
405
|
+
const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
|
|
406
|
+
const posResult = await getPositions(client, address);
|
|
407
|
+
let supplied = 0;
|
|
408
|
+
let borrowed = 0;
|
|
409
|
+
for (const pos of posResult.positions) {
|
|
410
|
+
const usd = pos.amountUsd ?? pos.amount;
|
|
411
|
+
if (pos.type === "save") supplied += usd;
|
|
412
|
+
else if (pos.type === "borrow") borrowed += usd;
|
|
413
|
+
}
|
|
414
|
+
let healthFactor;
|
|
415
|
+
try {
|
|
416
|
+
const hf = await lending.getHealthFactor(address, sdkOptions(client));
|
|
417
|
+
healthFactor = hf > 1e5 ? Infinity : hf;
|
|
418
|
+
} catch {
|
|
419
|
+
healthFactor = borrowed > 0 ? supplied * 0.75 / borrowed : Infinity;
|
|
420
|
+
}
|
|
421
|
+
const ltv = 0.75;
|
|
422
|
+
const maxBorrow = Math.max(0, supplied * ltv - borrowed);
|
|
423
|
+
return {
|
|
424
|
+
healthFactor,
|
|
425
|
+
supplied,
|
|
426
|
+
borrowed,
|
|
427
|
+
maxBorrow,
|
|
428
|
+
liquidationThreshold: ltv
|
|
429
|
+
};
|
|
430
|
+
}
|
|
489
431
|
async function buildSaveTx(client, address, amount, options = {}) {
|
|
490
432
|
if (!amount || amount <= 0 || !Number.isFinite(amount)) {
|
|
491
433
|
throw new T2000Error("INVALID_AMOUNT", "Save amount must be a positive number");
|
|
492
434
|
}
|
|
493
435
|
const asset = options.asset ?? "USDC";
|
|
494
436
|
const assetInfo = resolveAssetInfo(asset);
|
|
495
|
-
const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
|
|
496
|
-
const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
|
|
497
437
|
const coins = await fetchCoins(client, address, assetInfo.type);
|
|
498
438
|
if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins found`);
|
|
499
439
|
const tx = new transactions.Transaction();
|
|
@@ -502,159 +442,103 @@ async function buildSaveTx(client, address, amount, options = {}) {
|
|
|
502
442
|
if (options.collectFee) {
|
|
503
443
|
addCollectFeeToTx(tx, coinObj, "save");
|
|
504
444
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
tx.object(config.incentiveV3)
|
|
516
|
-
],
|
|
517
|
-
typeArguments: [pool.suiCoinType]
|
|
518
|
-
});
|
|
445
|
+
const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
|
|
446
|
+
try {
|
|
447
|
+
await lending.depositCoinPTB(tx, assetInfo.type, coinObj, {
|
|
448
|
+
...sdkOptions(client),
|
|
449
|
+
amount: rawAmount
|
|
450
|
+
});
|
|
451
|
+
} catch (err) {
|
|
452
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
453
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI deposit failed: ${msg}`);
|
|
454
|
+
}
|
|
519
455
|
return tx;
|
|
520
456
|
}
|
|
521
457
|
async function buildWithdrawTx(client, address, amount, options = {}) {
|
|
522
458
|
const asset = options.asset ?? "USDC";
|
|
523
459
|
const assetInfo = resolveAssetInfo(asset);
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
const
|
|
531
|
-
const deposited = assetState ? compoundBalance(assetState.supplyBalance, pool.currentSupplyIndex, pool) : 0;
|
|
532
|
-
const effectiveAmount = Math.min(amount, Math.max(0, deposited - withdrawDustBuffer(assetInfo.decimals)));
|
|
460
|
+
const posResult = await getPositions(client, address);
|
|
461
|
+
const supply = posResult.positions.find(
|
|
462
|
+
(p) => p.type === "save" && p.asset === asset
|
|
463
|
+
);
|
|
464
|
+
const deposited = supply?.amount ?? 0;
|
|
465
|
+
const dustBuffer = 1e3 / 10 ** assetInfo.decimals;
|
|
466
|
+
const effectiveAmount = Math.min(amount, Math.max(0, deposited - dustBuffer));
|
|
533
467
|
if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on NAVI`);
|
|
534
468
|
const rawAmount = Number(stableToRaw(effectiveAmount, assetInfo.decimals));
|
|
535
469
|
if (rawAmount <= 0) {
|
|
536
|
-
throw new T2000Error("INVALID_AMOUNT",
|
|
470
|
+
throw new T2000Error("INVALID_AMOUNT", "Withdrawal amount rounds to zero \u2014 balance is dust");
|
|
537
471
|
}
|
|
538
472
|
const tx = new transactions.Transaction();
|
|
539
473
|
tx.setSender(address);
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
tx.object(SUI_SYSTEM_STATE)
|
|
553
|
-
],
|
|
554
|
-
typeArguments: [pool.suiCoinType]
|
|
555
|
-
});
|
|
556
|
-
const [coin] = tx.moveCall({
|
|
557
|
-
target: "0x2::coin::from_balance",
|
|
558
|
-
arguments: [balance],
|
|
559
|
-
typeArguments: [pool.suiCoinType]
|
|
560
|
-
});
|
|
561
|
-
tx.transferObjects([coin], address);
|
|
474
|
+
try {
|
|
475
|
+
const coinResult = await lending.withdrawCoinPTB(tx, assetInfo.type, rawAmount, sdkOptions(client));
|
|
476
|
+
const [coin] = tx.moveCall({
|
|
477
|
+
target: "0x2::coin::from_balance",
|
|
478
|
+
arguments: [coinResult],
|
|
479
|
+
typeArguments: [assetInfo.type]
|
|
480
|
+
});
|
|
481
|
+
tx.transferObjects([coin], address);
|
|
482
|
+
} catch (err) {
|
|
483
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
484
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI withdraw failed: ${msg}`);
|
|
485
|
+
}
|
|
562
486
|
return { tx, effectiveAmount };
|
|
563
487
|
}
|
|
564
488
|
async function addWithdrawToTx(tx, client, address, amount, options = {}) {
|
|
565
489
|
const asset = options.asset ?? "USDC";
|
|
566
490
|
const assetInfo = resolveAssetInfo(asset);
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
const
|
|
574
|
-
const deposited = assetState ? compoundBalance(assetState.supplyBalance, pool.currentSupplyIndex, pool) : 0;
|
|
575
|
-
const effectiveAmount = Math.min(amount, Math.max(0, deposited - withdrawDustBuffer(assetInfo.decimals)));
|
|
491
|
+
const posResult = await getPositions(client, address);
|
|
492
|
+
const supply = posResult.positions.find(
|
|
493
|
+
(p) => p.type === "save" && p.asset === asset
|
|
494
|
+
);
|
|
495
|
+
const deposited = supply?.amount ?? 0;
|
|
496
|
+
const dustBuffer = 1e3 / 10 ** assetInfo.decimals;
|
|
497
|
+
const effectiveAmount = Math.min(amount, Math.max(0, deposited - dustBuffer));
|
|
576
498
|
if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on NAVI`);
|
|
577
499
|
const rawAmount = Number(stableToRaw(effectiveAmount, assetInfo.decimals));
|
|
578
500
|
if (rawAmount <= 0) {
|
|
579
|
-
const [
|
|
501
|
+
const [coin] = tx.moveCall({
|
|
580
502
|
target: "0x2::coin::zero",
|
|
581
|
-
typeArguments: [
|
|
503
|
+
typeArguments: [assetInfo.type]
|
|
582
504
|
});
|
|
583
|
-
return { coin
|
|
505
|
+
return { coin, effectiveAmount: 0 };
|
|
506
|
+
}
|
|
507
|
+
try {
|
|
508
|
+
const coinResult = await lending.withdrawCoinPTB(tx, assetInfo.type, rawAmount, sdkOptions(client));
|
|
509
|
+
const [coin] = tx.moveCall({
|
|
510
|
+
target: "0x2::coin::from_balance",
|
|
511
|
+
arguments: [coinResult],
|
|
512
|
+
typeArguments: [assetInfo.type]
|
|
513
|
+
});
|
|
514
|
+
return { coin, effectiveAmount };
|
|
515
|
+
} catch (err) {
|
|
516
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
517
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI withdraw failed: ${msg}`);
|
|
584
518
|
}
|
|
585
|
-
refreshOracles(tx, config, pools);
|
|
586
|
-
const [balance] = tx.moveCall({
|
|
587
|
-
target: `${config.package}::incentive_v3::withdraw_v2`,
|
|
588
|
-
arguments: [
|
|
589
|
-
tx.object(CLOCK),
|
|
590
|
-
tx.object(config.oracle.priceOracle),
|
|
591
|
-
tx.object(config.storage),
|
|
592
|
-
tx.object(pool.contract.pool),
|
|
593
|
-
tx.pure.u8(pool.id),
|
|
594
|
-
tx.pure.u64(rawAmount),
|
|
595
|
-
tx.object(config.incentiveV2),
|
|
596
|
-
tx.object(config.incentiveV3),
|
|
597
|
-
tx.object(SUI_SYSTEM_STATE)
|
|
598
|
-
],
|
|
599
|
-
typeArguments: [pool.suiCoinType]
|
|
600
|
-
});
|
|
601
|
-
const [coin] = tx.moveCall({
|
|
602
|
-
target: "0x2::coin::from_balance",
|
|
603
|
-
arguments: [balance],
|
|
604
|
-
typeArguments: [pool.suiCoinType]
|
|
605
|
-
});
|
|
606
|
-
return { coin, effectiveAmount };
|
|
607
519
|
}
|
|
608
520
|
async function addSaveToTx(tx, _client, _address, coin, options = {}) {
|
|
609
521
|
const asset = options.asset ?? "USDC";
|
|
610
|
-
const
|
|
522
|
+
const assetInfo = resolveAssetInfo(asset);
|
|
611
523
|
if (options.collectFee) {
|
|
612
524
|
addCollectFeeToTx(tx, coin, "save");
|
|
613
525
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
target: `${config.package}::incentive_v3::entry_deposit`,
|
|
621
|
-
arguments: [
|
|
622
|
-
tx.object(CLOCK),
|
|
623
|
-
tx.object(config.storage),
|
|
624
|
-
tx.object(pool.contract.pool),
|
|
625
|
-
tx.pure.u8(pool.id),
|
|
626
|
-
coin,
|
|
627
|
-
coinValue,
|
|
628
|
-
tx.object(config.incentiveV2),
|
|
629
|
-
tx.object(config.incentiveV3)
|
|
630
|
-
],
|
|
631
|
-
typeArguments: [pool.suiCoinType]
|
|
632
|
-
});
|
|
526
|
+
try {
|
|
527
|
+
await lending.depositCoinPTB(tx, assetInfo.type, coin, { env: "prod" });
|
|
528
|
+
} catch (err) {
|
|
529
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
530
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI deposit failed: ${msg}`);
|
|
531
|
+
}
|
|
633
532
|
}
|
|
634
533
|
async function addRepayToTx(tx, _client, _address, coin, options = {}) {
|
|
635
534
|
const asset = options.asset ?? "USDC";
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
}
|
|
643
|
-
tx.moveCall({
|
|
644
|
-
target: `${config.package}::incentive_v3::entry_repay`,
|
|
645
|
-
arguments: [
|
|
646
|
-
tx.object(CLOCK),
|
|
647
|
-
tx.object(config.oracle.priceOracle),
|
|
648
|
-
tx.object(config.storage),
|
|
649
|
-
tx.object(pool.contract.pool),
|
|
650
|
-
tx.pure.u8(pool.id),
|
|
651
|
-
coin,
|
|
652
|
-
coinValue,
|
|
653
|
-
tx.object(config.incentiveV2),
|
|
654
|
-
tx.object(config.incentiveV3)
|
|
655
|
-
],
|
|
656
|
-
typeArguments: [pool.suiCoinType]
|
|
657
|
-
});
|
|
535
|
+
const assetInfo = resolveAssetInfo(asset);
|
|
536
|
+
try {
|
|
537
|
+
await lending.repayCoinPTB(tx, assetInfo.type, coin, { env: "prod" });
|
|
538
|
+
} catch (err) {
|
|
539
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
540
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI repay failed: ${msg}`);
|
|
541
|
+
}
|
|
658
542
|
}
|
|
659
543
|
async function buildBorrowTx(client, address, amount, options = {}) {
|
|
660
544
|
if (!amount || amount <= 0 || !Number.isFinite(amount)) {
|
|
@@ -663,38 +547,23 @@ async function buildBorrowTx(client, address, amount, options = {}) {
|
|
|
663
547
|
const asset = options.asset ?? "USDC";
|
|
664
548
|
const assetInfo = resolveAssetInfo(asset);
|
|
665
549
|
const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
|
|
666
|
-
const [config, pool, pools] = await Promise.all([
|
|
667
|
-
getConfig(),
|
|
668
|
-
getPool(asset),
|
|
669
|
-
getPools()
|
|
670
|
-
]);
|
|
671
550
|
const tx = new transactions.Transaction();
|
|
672
551
|
tx.setSender(address);
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
tx
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
typeArguments: [pool.suiCoinType]
|
|
688
|
-
});
|
|
689
|
-
const [borrowedCoin] = tx.moveCall({
|
|
690
|
-
target: "0x2::coin::from_balance",
|
|
691
|
-
arguments: [balance],
|
|
692
|
-
typeArguments: [pool.suiCoinType]
|
|
693
|
-
});
|
|
694
|
-
if (options.collectFee) {
|
|
695
|
-
addCollectFeeToTx(tx, borrowedCoin, "borrow");
|
|
552
|
+
try {
|
|
553
|
+
const coinResult = await lending.borrowCoinPTB(tx, assetInfo.type, rawAmount, sdkOptions(client));
|
|
554
|
+
const [borrowedCoin] = tx.moveCall({
|
|
555
|
+
target: "0x2::coin::from_balance",
|
|
556
|
+
arguments: [coinResult],
|
|
557
|
+
typeArguments: [assetInfo.type]
|
|
558
|
+
});
|
|
559
|
+
if (options.collectFee) {
|
|
560
|
+
addCollectFeeToTx(tx, borrowedCoin, "borrow");
|
|
561
|
+
}
|
|
562
|
+
tx.transferObjects([borrowedCoin], address);
|
|
563
|
+
} catch (err) {
|
|
564
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
565
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI borrow failed: ${msg}`);
|
|
696
566
|
}
|
|
697
|
-
tx.transferObjects([borrowedCoin], address);
|
|
698
567
|
return tx;
|
|
699
568
|
}
|
|
700
569
|
async function buildRepayTx(client, address, amount, options = {}) {
|
|
@@ -703,163 +572,23 @@ async function buildRepayTx(client, address, amount, options = {}) {
|
|
|
703
572
|
}
|
|
704
573
|
const asset = options.asset ?? "USDC";
|
|
705
574
|
const assetInfo = resolveAssetInfo(asset);
|
|
706
|
-
const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
|
|
707
|
-
const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
|
|
708
575
|
const coins = await fetchCoins(client, address, assetInfo.type);
|
|
709
576
|
if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins to repay with`);
|
|
710
577
|
const tx = new transactions.Transaction();
|
|
711
578
|
tx.setSender(address);
|
|
712
|
-
addOracleUpdate(tx, config, pool);
|
|
713
579
|
const coinObj = mergeCoins(tx, coins);
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
arguments: [
|
|
717
|
-
tx.object(CLOCK),
|
|
718
|
-
tx.object(config.oracle.priceOracle),
|
|
719
|
-
tx.object(config.storage),
|
|
720
|
-
tx.object(pool.contract.pool),
|
|
721
|
-
tx.pure.u8(pool.id),
|
|
722
|
-
coinObj,
|
|
723
|
-
tx.pure.u64(rawAmount),
|
|
724
|
-
tx.object(config.incentiveV2),
|
|
725
|
-
tx.object(config.incentiveV3)
|
|
726
|
-
],
|
|
727
|
-
typeArguments: [pool.suiCoinType]
|
|
728
|
-
});
|
|
729
|
-
return tx;
|
|
730
|
-
}
|
|
731
|
-
async function getHealthFactor(client, addressOrKeypair) {
|
|
732
|
-
const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
|
|
733
|
-
const [config, pools, states] = await Promise.all([
|
|
734
|
-
getConfig(),
|
|
735
|
-
getPools(),
|
|
736
|
-
getUserState(client, address)
|
|
737
|
-
]);
|
|
738
|
-
let supplied = 0;
|
|
739
|
-
let borrowed = 0;
|
|
740
|
-
let weightedLtv = 0;
|
|
741
|
-
let weightedLiqThreshold = 0;
|
|
742
|
-
for (const state of states) {
|
|
743
|
-
const pool = pools.find((p) => p.id === state.assetId);
|
|
744
|
-
if (!pool) continue;
|
|
745
|
-
const supplyBal = compoundBalance(state.supplyBalance, pool.currentSupplyIndex, pool);
|
|
746
|
-
const borrowBal = compoundBalance(state.borrowBalance, pool.currentBorrowIndex, pool);
|
|
747
|
-
const price = pool.token?.price ?? 1;
|
|
748
|
-
supplied += supplyBal * price;
|
|
749
|
-
borrowed += borrowBal * price;
|
|
750
|
-
if (supplyBal > 0) {
|
|
751
|
-
weightedLtv += supplyBal * price * parseLtv(pool.ltv);
|
|
752
|
-
weightedLiqThreshold += supplyBal * price * parseLiqThreshold(pool.liquidationFactor.threshold);
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
const ltv = supplied > 0 ? weightedLtv / supplied : 0.75;
|
|
756
|
-
const liqThreshold = supplied > 0 ? weightedLiqThreshold / supplied : 0.75;
|
|
757
|
-
const maxBorrowVal = Math.max(0, supplied * ltv - borrowed);
|
|
758
|
-
const usdcPool = pools.find((p) => matchesCoinType(p.suiCoinType || p.coinType || "", SUPPORTED_ASSETS.USDC.type));
|
|
759
|
-
let healthFactor;
|
|
760
|
-
if (borrowed <= 0) {
|
|
761
|
-
healthFactor = Infinity;
|
|
762
|
-
} else if (usdcPool) {
|
|
763
|
-
try {
|
|
764
|
-
const tx = new transactions.Transaction();
|
|
765
|
-
tx.moveCall({
|
|
766
|
-
target: `${config.uiGetter}::calculator_unchecked::dynamic_health_factor`,
|
|
767
|
-
arguments: [
|
|
768
|
-
tx.object(CLOCK),
|
|
769
|
-
tx.object(config.storage),
|
|
770
|
-
tx.object(config.oracle.priceOracle),
|
|
771
|
-
tx.pure.u8(usdcPool.id),
|
|
772
|
-
tx.pure.address(address),
|
|
773
|
-
tx.pure.u8(usdcPool.id),
|
|
774
|
-
tx.pure.u64(0),
|
|
775
|
-
tx.pure.u64(0),
|
|
776
|
-
tx.pure.bool(false)
|
|
777
|
-
],
|
|
778
|
-
typeArguments: [usdcPool.suiCoinType]
|
|
779
|
-
});
|
|
780
|
-
const result = await client.devInspectTransactionBlock({
|
|
781
|
-
transactionBlock: tx,
|
|
782
|
-
sender: address
|
|
783
|
-
});
|
|
784
|
-
const decoded = decodeDevInspect(result, bcs.bcs.u256());
|
|
785
|
-
if (decoded !== void 0) {
|
|
786
|
-
healthFactor = normalizeHealthFactor(Number(decoded));
|
|
787
|
-
} else {
|
|
788
|
-
healthFactor = supplied * liqThreshold / borrowed;
|
|
789
|
-
}
|
|
790
|
-
} catch {
|
|
791
|
-
healthFactor = supplied * liqThreshold / borrowed;
|
|
792
|
-
}
|
|
793
|
-
} else {
|
|
794
|
-
healthFactor = supplied * liqThreshold / borrowed;
|
|
795
|
-
}
|
|
796
|
-
return {
|
|
797
|
-
healthFactor,
|
|
798
|
-
supplied,
|
|
799
|
-
borrowed,
|
|
800
|
-
maxBorrow: maxBorrowVal,
|
|
801
|
-
liquidationThreshold: liqThreshold
|
|
802
|
-
};
|
|
803
|
-
}
|
|
804
|
-
var NAVI_SUPPORTED_ASSETS = [...STABLE_ASSETS, "SUI", "ETH", "GOLD"];
|
|
805
|
-
async function getRates(client) {
|
|
580
|
+
const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
|
|
581
|
+
const [repayCoin] = tx.splitCoins(coinObj, [rawAmount]);
|
|
806
582
|
try {
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
let borrowApy = poolBorrowApy(pool);
|
|
815
|
-
if (saveApy <= 0 || saveApy > 200) saveApy = 0;
|
|
816
|
-
if (borrowApy <= 0 || borrowApy > 200) borrowApy = 0;
|
|
817
|
-
result[asset] = { saveApy, borrowApy };
|
|
818
|
-
}
|
|
819
|
-
if (!result.USDC) result.USDC = { saveApy: 4, borrowApy: 6 };
|
|
820
|
-
return result;
|
|
821
|
-
} catch {
|
|
822
|
-
return { USDC: { saveApy: 4, borrowApy: 6 } };
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
async function getPositions(client, addressOrKeypair) {
|
|
826
|
-
const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
|
|
827
|
-
const [states, cachedPools] = await Promise.all([getUserState(client, address), getPools()]);
|
|
828
|
-
let pools = cachedPools;
|
|
829
|
-
const unmatchedIds = states.filter((s) => !pools.find((p) => p.id === s.assetId)).map((s) => s.assetId);
|
|
830
|
-
if (unmatchedIds.length > 0) {
|
|
831
|
-
pools = await getPools(true);
|
|
832
|
-
}
|
|
833
|
-
const positions = [];
|
|
834
|
-
for (const state of states) {
|
|
835
|
-
const pool = pools.find((p) => p.id === state.assetId);
|
|
836
|
-
if (!pool) {
|
|
837
|
-
console.warn(`[NAVI] No pool found for assetId=${state.assetId} (supply=${state.supplyBalance}, borrow=${state.borrowBalance}) \u2014 funds may be invisible`);
|
|
838
|
-
continue;
|
|
839
|
-
}
|
|
840
|
-
const symbol = resolvePoolSymbol(pool);
|
|
841
|
-
const supplyBal = compoundBalance(state.supplyBalance, pool.currentSupplyIndex, pool);
|
|
842
|
-
const borrowBal = compoundBalance(state.borrowBalance, pool.currentBorrowIndex, pool);
|
|
843
|
-
if (supplyBal > 1e-4) {
|
|
844
|
-
positions.push({
|
|
845
|
-
protocol: "navi",
|
|
846
|
-
asset: symbol,
|
|
847
|
-
type: "save",
|
|
848
|
-
amount: supplyBal,
|
|
849
|
-
apy: poolSaveApy(pool)
|
|
850
|
-
});
|
|
851
|
-
}
|
|
852
|
-
if (borrowBal > 1e-4) {
|
|
853
|
-
positions.push({
|
|
854
|
-
protocol: "navi",
|
|
855
|
-
asset: symbol,
|
|
856
|
-
type: "borrow",
|
|
857
|
-
amount: borrowBal,
|
|
858
|
-
apy: poolBorrowApy(pool)
|
|
859
|
-
});
|
|
860
|
-
}
|
|
583
|
+
await lending.repayCoinPTB(tx, assetInfo.type, repayCoin, {
|
|
584
|
+
...sdkOptions(client),
|
|
585
|
+
amount: rawAmount
|
|
586
|
+
});
|
|
587
|
+
} catch (err) {
|
|
588
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
589
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI repay failed: ${msg}`);
|
|
861
590
|
}
|
|
862
|
-
return
|
|
591
|
+
return tx;
|
|
863
592
|
}
|
|
864
593
|
async function maxWithdrawAmount(client, addressOrKeypair) {
|
|
865
594
|
const hf = await getHealthFactor(client, addressOrKeypair);
|
|
@@ -880,164 +609,61 @@ async function maxBorrowAmount(client, addressOrKeypair) {
|
|
|
880
609
|
const maxAmount = Math.max(0, hf.supplied * ltv / MIN_HEALTH_FACTOR - hf.borrowed);
|
|
881
610
|
return { maxAmount, healthFactorAfter: MIN_HEALTH_FACTOR, currentHF: hf.healthFactor };
|
|
882
611
|
}
|
|
883
|
-
var CERT_TYPE = "0x549e8b69270defbfafd4f94e17ec44cdbdd99820b33bda2278dea3b9a32d3f55::cert::CERT";
|
|
884
|
-
var DEEP_TYPE = "0xdeeb7a4662eec9f2f3def03fb937a663dddaa2e215b8078a284d026b7946c270::deep::DEEP";
|
|
885
|
-
var REWARD_FUNDS = {
|
|
886
|
-
[CERT_TYPE]: "0x7093cf7549d5e5b35bfde2177223d1050f71655c7f676a5e610ee70eb4d93b5c",
|
|
887
|
-
[DEEP_TYPE]: "0xc889d78b634f954979e80e622a2ae0fece824c0f6d9590044378a2563035f32f"
|
|
888
|
-
};
|
|
889
|
-
var REWARD_SYMBOLS = {
|
|
890
|
-
[CERT_TYPE]: "vSUI",
|
|
891
|
-
[DEEP_TYPE]: "DEEP"
|
|
892
|
-
};
|
|
893
|
-
var incentiveRulesCache = null;
|
|
894
|
-
async function getIncentiveRules(client) {
|
|
895
|
-
if (incentiveRulesCache && Date.now() - incentiveRulesCache.ts < CACHE_TTL) {
|
|
896
|
-
return incentiveRulesCache.data;
|
|
897
|
-
}
|
|
898
|
-
const [pools, obj] = await Promise.all([
|
|
899
|
-
getPools(),
|
|
900
|
-
client.getObject({
|
|
901
|
-
id: "0x62982dad27fb10bb314b3384d5de8d2ac2d72ab2dbeae5d801dbdb9efa816c80",
|
|
902
|
-
options: { showContent: true }
|
|
903
|
-
})
|
|
904
|
-
]);
|
|
905
|
-
const rewardCoinMap = /* @__PURE__ */ new Map();
|
|
906
|
-
for (const pool of pools) {
|
|
907
|
-
const ct = (pool.suiCoinType || pool.coinType || "").toLowerCase();
|
|
908
|
-
const suffix = ct.split("::").slice(1).join("::");
|
|
909
|
-
const coins = pool.supplyIncentiveApyInfo?.rewardCoin;
|
|
910
|
-
if (Array.isArray(coins) && coins.length > 0) {
|
|
911
|
-
rewardCoinMap.set(suffix, coins[0]);
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
const result = /* @__PURE__ */ new Map();
|
|
915
|
-
if (obj.data?.content?.dataType !== "moveObject") {
|
|
916
|
-
incentiveRulesCache = { data: result, ts: Date.now() };
|
|
917
|
-
return result;
|
|
918
|
-
}
|
|
919
|
-
const fields = obj.data.content.fields;
|
|
920
|
-
const poolsObj = fields.pools;
|
|
921
|
-
const entries = poolsObj?.fields?.contents;
|
|
922
|
-
if (!Array.isArray(entries)) {
|
|
923
|
-
incentiveRulesCache = { data: result, ts: Date.now() };
|
|
924
|
-
return result;
|
|
925
|
-
}
|
|
926
|
-
for (const entry of entries) {
|
|
927
|
-
const ef = entry?.fields;
|
|
928
|
-
if (!ef) continue;
|
|
929
|
-
const key = String(ef.key ?? "");
|
|
930
|
-
const value = ef.value;
|
|
931
|
-
const rules = value?.fields?.rules;
|
|
932
|
-
const ruleEntries = rules?.fields?.contents;
|
|
933
|
-
if (!Array.isArray(ruleEntries)) continue;
|
|
934
|
-
const ruleIds = ruleEntries.map((re) => {
|
|
935
|
-
const rf = re?.fields;
|
|
936
|
-
return String(rf?.key ?? "");
|
|
937
|
-
}).filter(Boolean);
|
|
938
|
-
const suffix = key.split("::").slice(1).join("::").toLowerCase();
|
|
939
|
-
const full = key.toLowerCase();
|
|
940
|
-
const rewardCoin = rewardCoinMap.get(suffix) ?? rewardCoinMap.get(full) ?? null;
|
|
941
|
-
result.set(key, { ruleIds, rewardCoinType: rewardCoin });
|
|
942
|
-
}
|
|
943
|
-
incentiveRulesCache = { data: result, ts: Date.now() };
|
|
944
|
-
return result;
|
|
945
|
-
}
|
|
946
|
-
function stripPrefix(coinType) {
|
|
947
|
-
return coinType.replace(/^0x0*/, "");
|
|
948
|
-
}
|
|
949
612
|
async function getPendingRewards(client, address) {
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
const rewards = [];
|
|
955
|
-
const deposited = states.filter((s) => s.supplyBalance > 0n);
|
|
956
|
-
if (deposited.length === 0) return rewards;
|
|
957
|
-
for (const state of deposited) {
|
|
958
|
-
const pool = pools.find((p) => p.id === state.assetId);
|
|
959
|
-
if (!pool) continue;
|
|
960
|
-
const boostedApr = parseFloat(pool.supplyIncentiveApyInfo?.boostedApr ?? "0");
|
|
961
|
-
if (boostedApr <= 0) continue;
|
|
962
|
-
const rewardCoins = pool.supplyIncentiveApyInfo?.rewardCoin;
|
|
963
|
-
if (!Array.isArray(rewardCoins) || rewardCoins.length === 0) continue;
|
|
964
|
-
const rewardType = rewardCoins[0];
|
|
965
|
-
const assetSymbol = resolvePoolSymbol(pool);
|
|
966
|
-
rewards.push({
|
|
967
|
-
protocol: "navi",
|
|
968
|
-
asset: assetSymbol,
|
|
969
|
-
coinType: rewardType,
|
|
970
|
-
symbol: REWARD_SYMBOLS[rewardType] ?? rewardType.split("::").pop() ?? "UNKNOWN",
|
|
971
|
-
amount: 0,
|
|
972
|
-
estimatedValueUsd: 0
|
|
613
|
+
try {
|
|
614
|
+
const rewards = await lending.getUserAvailableLendingRewards(address, {
|
|
615
|
+
...sdkOptions(client),
|
|
616
|
+
markets: ["main"]
|
|
973
617
|
});
|
|
618
|
+
if (!rewards || rewards.length === 0) return [];
|
|
619
|
+
const summary = lending.summaryLendingRewards(rewards);
|
|
620
|
+
const result = [];
|
|
621
|
+
for (const s of summary) {
|
|
622
|
+
for (const rw of s.rewards) {
|
|
623
|
+
const available = Number(rw.available);
|
|
624
|
+
if (available <= 0) continue;
|
|
625
|
+
const symbol = rw.coinType.split("::").pop() ?? "UNKNOWN";
|
|
626
|
+
result.push({
|
|
627
|
+
protocol: "navi",
|
|
628
|
+
asset: String(s.assetId),
|
|
629
|
+
coinType: rw.coinType,
|
|
630
|
+
symbol,
|
|
631
|
+
amount: available,
|
|
632
|
+
estimatedValueUsd: 0
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return result;
|
|
637
|
+
} catch {
|
|
638
|
+
return [];
|
|
974
639
|
}
|
|
975
|
-
return rewards;
|
|
976
640
|
}
|
|
977
641
|
async function addClaimRewardsToTx(tx, client, address) {
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
getIncentiveRules(client)
|
|
983
|
-
]);
|
|
984
|
-
const deposited = states.filter((s) => s.supplyBalance > 0n);
|
|
985
|
-
if (deposited.length === 0) return [];
|
|
986
|
-
const claimGroups = /* @__PURE__ */ new Map();
|
|
987
|
-
for (const state of deposited) {
|
|
988
|
-
const pool = pools.find((p) => p.id === state.assetId);
|
|
989
|
-
if (!pool) continue;
|
|
990
|
-
const boostedApr = parseFloat(pool.supplyIncentiveApyInfo?.boostedApr ?? "0");
|
|
991
|
-
if (boostedApr <= 0) continue;
|
|
992
|
-
const rewardCoins = pool.supplyIncentiveApyInfo?.rewardCoin;
|
|
993
|
-
if (!Array.isArray(rewardCoins) || rewardCoins.length === 0) continue;
|
|
994
|
-
const rewardType = rewardCoins[0];
|
|
995
|
-
const fundId = REWARD_FUNDS[rewardType];
|
|
996
|
-
if (!fundId) continue;
|
|
997
|
-
const coinType = pool.suiCoinType || pool.coinType || "";
|
|
998
|
-
const strippedType = stripPrefix(coinType);
|
|
999
|
-
const ruleData = Array.from(rules.entries()).find(
|
|
1000
|
-
([key]) => stripPrefix(key) === strippedType || key.split("::").slice(1).join("::").toLowerCase() === coinType.split("::").slice(1).join("::").toLowerCase()
|
|
1001
|
-
);
|
|
1002
|
-
if (!ruleData || ruleData[1].ruleIds.length === 0) continue;
|
|
1003
|
-
const group = claimGroups.get(rewardType) ?? { assets: [], ruleIds: [] };
|
|
1004
|
-
for (const ruleId of ruleData[1].ruleIds) {
|
|
1005
|
-
group.assets.push(strippedType);
|
|
1006
|
-
group.ruleIds.push(ruleId);
|
|
1007
|
-
}
|
|
1008
|
-
claimGroups.set(rewardType, group);
|
|
1009
|
-
}
|
|
1010
|
-
const claimed = [];
|
|
1011
|
-
for (const [rewardType, { assets, ruleIds }] of claimGroups) {
|
|
1012
|
-
const fundId = REWARD_FUNDS[rewardType];
|
|
1013
|
-
const [balance] = tx.moveCall({
|
|
1014
|
-
target: `${config.package}::incentive_v3::claim_reward`,
|
|
1015
|
-
typeArguments: [rewardType],
|
|
1016
|
-
arguments: [
|
|
1017
|
-
tx.object(CLOCK),
|
|
1018
|
-
tx.object(config.incentiveV3),
|
|
1019
|
-
tx.object(config.storage),
|
|
1020
|
-
tx.object(fundId),
|
|
1021
|
-
tx.pure(bcs.bcs.vector(bcs.bcs.string()).serialize(assets)),
|
|
1022
|
-
tx.pure(bcs.bcs.vector(bcs.bcs.Address).serialize(ruleIds))
|
|
1023
|
-
]
|
|
642
|
+
try {
|
|
643
|
+
const rewards = await lending.getUserAvailableLendingRewards(address, {
|
|
644
|
+
...sdkOptions(client),
|
|
645
|
+
markets: ["main"]
|
|
1024
646
|
});
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
647
|
+
if (!rewards || rewards.length === 0) return [];
|
|
648
|
+
const claimable = rewards.filter(
|
|
649
|
+
(r) => Number(r.userClaimableReward) > 0
|
|
650
|
+
);
|
|
651
|
+
if (claimable.length === 0) return [];
|
|
652
|
+
const claimed = await lending.claimLendingRewardsPTB(tx, claimable, {
|
|
653
|
+
env: "prod",
|
|
654
|
+
customCoinReceive: { type: "transfer", transfer: address }
|
|
1029
655
|
});
|
|
1030
|
-
|
|
1031
|
-
claimed.push({
|
|
656
|
+
return claimed.map((c) => ({
|
|
1032
657
|
protocol: "navi",
|
|
1033
|
-
asset:
|
|
1034
|
-
coinType:
|
|
1035
|
-
symbol:
|
|
658
|
+
asset: "",
|
|
659
|
+
coinType: "",
|
|
660
|
+
symbol: "REWARD",
|
|
1036
661
|
amount: 0,
|
|
1037
662
|
estimatedValueUsd: 0
|
|
1038
|
-
});
|
|
663
|
+
}));
|
|
664
|
+
} catch {
|
|
665
|
+
return [];
|
|
1039
666
|
}
|
|
1040
|
-
return claimed;
|
|
1041
667
|
}
|
|
1042
668
|
|
|
1043
669
|
// src/adapters/navi.ts
|
|
@@ -1081,8 +707,8 @@ var NaviAdapter = class {
|
|
|
1081
707
|
async getPositions(address) {
|
|
1082
708
|
const result = await getPositions(this.client, address);
|
|
1083
709
|
return {
|
|
1084
|
-
supplies: result.positions.filter((p) => p.type === "save").map((p) => ({ asset: p.asset, amount: p.amount, apy: p.apy })),
|
|
1085
|
-
borrows: result.positions.filter((p) => p.type === "borrow").map((p) => ({ asset: p.asset, amount: p.amount, apy: p.apy }))
|
|
710
|
+
supplies: result.positions.filter((p) => p.type === "save").map((p) => ({ asset: p.asset, amount: p.amount, amountUsd: p.amountUsd, apy: p.apy })),
|
|
711
|
+
borrows: result.positions.filter((p) => p.type === "borrow").map((p) => ({ asset: p.asset, amount: p.amount, amountUsd: p.amountUsd, apy: p.apy }))
|
|
1086
712
|
};
|
|
1087
713
|
}
|
|
1088
714
|
async getHealth(address) {
|
|
@@ -1363,15 +989,8 @@ var CetusAdapter = class {
|
|
|
1363
989
|
});
|
|
1364
990
|
}
|
|
1365
991
|
};
|
|
1366
|
-
var WAD = 1e18;
|
|
1367
|
-
var MIN_HEALTH_FACTOR2 = 1.5;
|
|
1368
|
-
var CLOCK2 = "0x6";
|
|
1369
|
-
var SUI_SYSTEM_STATE2 = "0x5";
|
|
1370
|
-
var LENDING_MARKET_ID = "0x84030d26d85eaa7035084a057f2f11f701b7e2e4eda87551becbc7c97505ece1";
|
|
1371
|
-
var LENDING_MARKET_TYPE = "0xf95b06141ed4a174f239417323bde3f209b972f5930d8521ea38a52aff3a6ddf::suilend::MAIN_POOL";
|
|
1372
992
|
var SUILEND_PACKAGE = "0xf95b06141ed4a174f239417323bde3f209b972f5930d8521ea38a52aff3a6ddf";
|
|
1373
|
-
var
|
|
1374
|
-
var FALLBACK_PUBLISHED_AT = "0x3d4353f3bd3565329655e6b77bc2abfd31e558b86662ebd078ae453d416bc10f";
|
|
993
|
+
var MIN_HEALTH_FACTOR2 = 1.5;
|
|
1375
994
|
var descriptor3 = {
|
|
1376
995
|
id: "suilend",
|
|
1377
996
|
name: "Suilend",
|
|
@@ -1389,224 +1008,31 @@ var descriptor3 = {
|
|
|
1389
1008
|
"lending_market::repay": "repay"
|
|
1390
1009
|
}
|
|
1391
1010
|
};
|
|
1392
|
-
function interpolateRate(utilBreakpoints, aprBreakpoints, utilizationPct) {
|
|
1393
|
-
if (utilBreakpoints.length === 0) return 0;
|
|
1394
|
-
if (utilizationPct <= utilBreakpoints[0]) return aprBreakpoints[0];
|
|
1395
|
-
if (utilizationPct >= utilBreakpoints[utilBreakpoints.length - 1]) {
|
|
1396
|
-
return aprBreakpoints[aprBreakpoints.length - 1];
|
|
1397
|
-
}
|
|
1398
|
-
for (let i = 1; i < utilBreakpoints.length; i++) {
|
|
1399
|
-
if (utilizationPct <= utilBreakpoints[i]) {
|
|
1400
|
-
const t = (utilizationPct - utilBreakpoints[i - 1]) / (utilBreakpoints[i] - utilBreakpoints[i - 1]);
|
|
1401
|
-
return aprBreakpoints[i - 1] + t * (aprBreakpoints[i] - aprBreakpoints[i - 1]);
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
return aprBreakpoints[aprBreakpoints.length - 1];
|
|
1405
|
-
}
|
|
1406
|
-
function computeRates(reserve) {
|
|
1407
|
-
const available = reserve.availableAmount / 10 ** reserve.mintDecimals;
|
|
1408
|
-
const borrowed = reserve.borrowedAmountWad / WAD / 10 ** reserve.mintDecimals;
|
|
1409
|
-
const totalDeposited = available + borrowed;
|
|
1410
|
-
const utilizationPct = totalDeposited > 0 ? borrowed / totalDeposited * 100 : 0;
|
|
1411
|
-
if (reserve.interestRateUtils.length === 0) return { borrowAprPct: 0, depositAprPct: 0 };
|
|
1412
|
-
const aprs = reserve.interestRateAprs.map((a) => a / 100);
|
|
1413
|
-
const borrowAprPct = interpolateRate(reserve.interestRateUtils, aprs, utilizationPct);
|
|
1414
|
-
const depositAprPct = utilizationPct / 100 * (borrowAprPct / 100) * (1 - reserve.spreadFeeBps / 1e4) * 100;
|
|
1415
|
-
return { borrowAprPct, depositAprPct };
|
|
1416
|
-
}
|
|
1417
|
-
var MS_PER_YEAR = 365.25 * 24 * 3600 * 1e3;
|
|
1418
|
-
function computeDepositRewardApr(reserve, allReserves) {
|
|
1419
|
-
if (reserve.depositTotalShares <= 0 || reserve.price <= 0) return 0;
|
|
1420
|
-
const totalDepositValue = reserve.depositTotalShares / 10 ** reserve.mintDecimals * reserve.price;
|
|
1421
|
-
if (totalDepositValue <= 0) return 0;
|
|
1422
|
-
const priceMap = /* @__PURE__ */ new Map();
|
|
1423
|
-
for (const r of allReserves) {
|
|
1424
|
-
if (r.price > 0) priceMap.set(r.coinType, { price: r.price, decimals: r.mintDecimals });
|
|
1425
|
-
}
|
|
1426
|
-
let rewardApr = 0;
|
|
1427
|
-
for (const rw of reserve.depositPoolRewards) {
|
|
1428
|
-
const info = priceMap.get(rw.coinType);
|
|
1429
|
-
if (!info || info.price <= 0) continue;
|
|
1430
|
-
const durationMs = rw.endTimeMs - rw.startTimeMs;
|
|
1431
|
-
if (durationMs <= 0) continue;
|
|
1432
|
-
const annualTokens = rw.totalRewards / 10 ** info.decimals * (MS_PER_YEAR / durationMs);
|
|
1433
|
-
rewardApr += annualTokens * info.price / totalDepositValue * 100;
|
|
1434
|
-
}
|
|
1435
|
-
return rewardApr;
|
|
1436
|
-
}
|
|
1437
|
-
function cTokenRatio(reserve) {
|
|
1438
|
-
if (reserve.ctokenSupply === 0) return 1;
|
|
1439
|
-
const totalSupply = reserve.availableAmount + reserve.borrowedAmountWad / WAD - reserve.unclaimedSpreadFeesWad / WAD;
|
|
1440
|
-
return totalSupply / reserve.ctokenSupply;
|
|
1441
|
-
}
|
|
1442
|
-
function f(obj) {
|
|
1443
|
-
if (obj && typeof obj === "object" && "fields" in obj) return obj.fields;
|
|
1444
|
-
return obj;
|
|
1445
|
-
}
|
|
1446
|
-
function str(v) {
|
|
1447
|
-
return String(v ?? "0");
|
|
1448
|
-
}
|
|
1449
|
-
function num(v) {
|
|
1450
|
-
return Number(str(v));
|
|
1451
|
-
}
|
|
1452
|
-
function parseReserve(raw, index) {
|
|
1453
|
-
const r = f(raw);
|
|
1454
|
-
const coinTypeField = f(r.coin_type);
|
|
1455
|
-
const config = f(f(r.config)?.element);
|
|
1456
|
-
const dMgr = f(r.deposits_pool_reward_manager);
|
|
1457
|
-
const rawRewards = Array.isArray(dMgr?.pool_rewards) ? dMgr.pool_rewards : [];
|
|
1458
|
-
const now = Date.now();
|
|
1459
|
-
const depositPoolRewards = rawRewards.map((rw, idx) => {
|
|
1460
|
-
if (rw === null) return null;
|
|
1461
|
-
const rwf = f(rw);
|
|
1462
|
-
return {
|
|
1463
|
-
coinType: str(f(rwf.coin_type)?.name),
|
|
1464
|
-
totalRewards: num(rwf.total_rewards),
|
|
1465
|
-
startTimeMs: num(rwf.start_time_ms),
|
|
1466
|
-
endTimeMs: num(rwf.end_time_ms),
|
|
1467
|
-
rewardIndex: idx
|
|
1468
|
-
};
|
|
1469
|
-
}).filter((rw) => rw !== null && rw.endTimeMs > now && rw.totalRewards > 0);
|
|
1470
|
-
return {
|
|
1471
|
-
coinType: str(coinTypeField?.name),
|
|
1472
|
-
mintDecimals: num(r.mint_decimals),
|
|
1473
|
-
availableAmount: num(r.available_amount),
|
|
1474
|
-
borrowedAmountWad: num(f(r.borrowed_amount)?.value),
|
|
1475
|
-
ctokenSupply: num(r.ctoken_supply),
|
|
1476
|
-
unclaimedSpreadFeesWad: num(f(r.unclaimed_spread_fees)?.value),
|
|
1477
|
-
cumulativeBorrowRateWad: num(f(r.cumulative_borrow_rate)?.value),
|
|
1478
|
-
openLtvPct: num(config?.open_ltv_pct),
|
|
1479
|
-
closeLtvPct: num(config?.close_ltv_pct),
|
|
1480
|
-
spreadFeeBps: num(config?.spread_fee_bps),
|
|
1481
|
-
interestRateUtils: Array.isArray(config?.interest_rate_utils) ? config.interest_rate_utils.map(num) : [],
|
|
1482
|
-
interestRateAprs: Array.isArray(config?.interest_rate_aprs) ? config.interest_rate_aprs.map(num) : [],
|
|
1483
|
-
arrayIndex: index,
|
|
1484
|
-
price: num(f(r.price)?.value) / WAD,
|
|
1485
|
-
depositTotalShares: num(dMgr?.total_shares),
|
|
1486
|
-
depositPoolRewards
|
|
1487
|
-
};
|
|
1488
|
-
}
|
|
1489
|
-
function parseObligation(raw) {
|
|
1490
|
-
const deposits = Array.isArray(raw.deposits) ? raw.deposits.map((d) => {
|
|
1491
|
-
const df = f(d);
|
|
1492
|
-
return {
|
|
1493
|
-
coinType: str(f(df.coin_type)?.name),
|
|
1494
|
-
ctokenAmount: num(df.deposited_ctoken_amount),
|
|
1495
|
-
reserveIdx: num(df.reserve_array_index)
|
|
1496
|
-
};
|
|
1497
|
-
}) : [];
|
|
1498
|
-
const borrows = Array.isArray(raw.borrows) ? raw.borrows.map((b) => {
|
|
1499
|
-
const bf = f(b);
|
|
1500
|
-
return {
|
|
1501
|
-
coinType: str(f(bf.coin_type)?.name),
|
|
1502
|
-
borrowedWad: num(f(bf.borrowed_amount)?.value),
|
|
1503
|
-
cumBorrowRateWad: num(f(bf.cumulative_borrow_rate)?.value),
|
|
1504
|
-
reserveIdx: num(bf.reserve_array_index)
|
|
1505
|
-
};
|
|
1506
|
-
}) : [];
|
|
1507
|
-
return { deposits, borrows };
|
|
1508
|
-
}
|
|
1509
1011
|
var SuilendAdapter = class {
|
|
1510
1012
|
id = "suilend";
|
|
1511
1013
|
name = "Suilend";
|
|
1512
|
-
version = "
|
|
1014
|
+
version = "3.0.0";
|
|
1513
1015
|
capabilities = ["save", "withdraw", "borrow", "repay"];
|
|
1514
1016
|
supportedAssets = [...STABLE_ASSETS, "SUI", "ETH", "BTC", "GOLD"];
|
|
1515
1017
|
supportsSameAssetBorrow = false;
|
|
1516
1018
|
client;
|
|
1517
|
-
|
|
1518
|
-
reserveCache = null;
|
|
1019
|
+
sdkClient = null;
|
|
1519
1020
|
async init(client) {
|
|
1520
1021
|
this.client = client;
|
|
1521
1022
|
}
|
|
1522
1023
|
initSync(client) {
|
|
1523
1024
|
this.client = client;
|
|
1524
1025
|
}
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
return this.publishedAt;
|
|
1534
|
-
}
|
|
1535
|
-
} catch {
|
|
1026
|
+
async getSdkClient() {
|
|
1027
|
+
if (!this.sdkClient) {
|
|
1028
|
+
this.sdkClient = await client.SuilendClient.initialize(
|
|
1029
|
+
client.LENDING_MARKET_ID,
|
|
1030
|
+
client.LENDING_MARKET_TYPE,
|
|
1031
|
+
this.client,
|
|
1032
|
+
false
|
|
1033
|
+
);
|
|
1536
1034
|
}
|
|
1537
|
-
this.
|
|
1538
|
-
return this.publishedAt;
|
|
1539
|
-
}
|
|
1540
|
-
async loadReserves(fresh = false) {
|
|
1541
|
-
if (this.reserveCache && !fresh) return this.reserveCache;
|
|
1542
|
-
const market = await this.client.getObject({
|
|
1543
|
-
id: LENDING_MARKET_ID,
|
|
1544
|
-
options: { showContent: true }
|
|
1545
|
-
});
|
|
1546
|
-
if (market.data?.content?.dataType !== "moveObject") {
|
|
1547
|
-
throw new T2000Error("PROTOCOL_UNAVAILABLE", "Failed to read Suilend lending market");
|
|
1548
|
-
}
|
|
1549
|
-
const fields = market.data.content.fields;
|
|
1550
|
-
const reservesRaw = fields.reserves;
|
|
1551
|
-
if (!Array.isArray(reservesRaw)) {
|
|
1552
|
-
throw new T2000Error("PROTOCOL_UNAVAILABLE", "Failed to parse Suilend reserves");
|
|
1553
|
-
}
|
|
1554
|
-
this.reserveCache = reservesRaw.map((r, i) => parseReserve(r, i));
|
|
1555
|
-
return this.reserveCache;
|
|
1556
|
-
}
|
|
1557
|
-
findReserve(reserves, asset) {
|
|
1558
|
-
let coinType;
|
|
1559
|
-
if (asset in SUPPORTED_ASSETS) {
|
|
1560
|
-
coinType = SUPPORTED_ASSETS[asset].type;
|
|
1561
|
-
} else if (asset.includes("::")) {
|
|
1562
|
-
coinType = asset;
|
|
1563
|
-
} else {
|
|
1564
|
-
return void 0;
|
|
1565
|
-
}
|
|
1566
|
-
try {
|
|
1567
|
-
const normalized = utils.normalizeStructTag(coinType);
|
|
1568
|
-
return reserves.find((r) => {
|
|
1569
|
-
try {
|
|
1570
|
-
return utils.normalizeStructTag(r.coinType) === normalized;
|
|
1571
|
-
} catch {
|
|
1572
|
-
return false;
|
|
1573
|
-
}
|
|
1574
|
-
});
|
|
1575
|
-
} catch {
|
|
1576
|
-
return void 0;
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
async fetchObligationCaps(address) {
|
|
1580
|
-
const capType = `${SUILEND_PACKAGE}::lending_market::ObligationOwnerCap<${LENDING_MARKET_TYPE}>`;
|
|
1581
|
-
const caps = [];
|
|
1582
|
-
let cursor;
|
|
1583
|
-
let hasNext = true;
|
|
1584
|
-
while (hasNext) {
|
|
1585
|
-
const page = await this.client.getOwnedObjects({
|
|
1586
|
-
owner: address,
|
|
1587
|
-
filter: { StructType: capType },
|
|
1588
|
-
options: { showContent: true },
|
|
1589
|
-
cursor: cursor ?? void 0
|
|
1590
|
-
});
|
|
1591
|
-
for (const item of page.data) {
|
|
1592
|
-
if (item.data?.content?.dataType !== "moveObject") continue;
|
|
1593
|
-
const fields = item.data.content.fields;
|
|
1594
|
-
caps.push({
|
|
1595
|
-
id: item.data.objectId,
|
|
1596
|
-
obligationId: str(fields.obligation_id)
|
|
1597
|
-
});
|
|
1598
|
-
}
|
|
1599
|
-
cursor = page.nextCursor;
|
|
1600
|
-
hasNext = page.hasNextPage;
|
|
1601
|
-
}
|
|
1602
|
-
return caps;
|
|
1603
|
-
}
|
|
1604
|
-
async fetchObligation(obligationId) {
|
|
1605
|
-
const obj = await this.client.getObject({ id: obligationId, options: { showContent: true } });
|
|
1606
|
-
if (obj.data?.content?.dataType !== "moveObject") {
|
|
1607
|
-
throw new T2000Error("PROTOCOL_UNAVAILABLE", "Failed to read Suilend obligation");
|
|
1608
|
-
}
|
|
1609
|
-
return parseObligation(obj.data.content.fields);
|
|
1035
|
+
return this.sdkClient;
|
|
1610
1036
|
}
|
|
1611
1037
|
resolveSymbol(coinType) {
|
|
1612
1038
|
try {
|
|
@@ -1622,371 +1048,217 @@ var SuilendAdapter = class {
|
|
|
1622
1048
|
const parts = coinType.split("::");
|
|
1623
1049
|
return parts[parts.length - 1] || "UNKNOWN";
|
|
1624
1050
|
}
|
|
1625
|
-
// -- Adapter interface ----------------------------------------------------
|
|
1626
1051
|
async getRates(asset) {
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1052
|
+
try {
|
|
1053
|
+
const sdk = await this.getSdkClient();
|
|
1054
|
+
const { reserveMap } = await initialize.initializeSuilend(this.client, sdk);
|
|
1055
|
+
const assetInfo = SUPPORTED_ASSETS[asset];
|
|
1056
|
+
if (!assetInfo) throw new T2000Error("ASSET_NOT_SUPPORTED", `Suilend does not support ${asset}`);
|
|
1057
|
+
const normalized = utils.normalizeStructTag(assetInfo.type);
|
|
1058
|
+
const reserve = Object.values(reserveMap).find((r) => {
|
|
1059
|
+
try {
|
|
1060
|
+
return utils.normalizeStructTag(r.coinType) === normalized;
|
|
1061
|
+
} catch {
|
|
1062
|
+
return false;
|
|
1063
|
+
}
|
|
1064
|
+
});
|
|
1065
|
+
if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `Suilend does not support ${asset}`);
|
|
1066
|
+
return {
|
|
1067
|
+
asset,
|
|
1068
|
+
saveApy: reserve.depositAprPercent.toNumber(),
|
|
1069
|
+
borrowApy: reserve.borrowAprPercent.toNumber()
|
|
1070
|
+
};
|
|
1071
|
+
} catch (err) {
|
|
1072
|
+
if (err instanceof T2000Error) throw err;
|
|
1073
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1074
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", `Suilend getRates failed: ${msg}`);
|
|
1075
|
+
}
|
|
1633
1076
|
}
|
|
1634
1077
|
async getPositions(address) {
|
|
1635
1078
|
const supplies = [];
|
|
1636
1079
|
const borrows = [];
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1080
|
+
try {
|
|
1081
|
+
const sdk = await this.getSdkClient();
|
|
1082
|
+
const { reserveMap, refreshedRawReserves } = await initialize.initializeSuilend(this.client, sdk);
|
|
1083
|
+
const { obligations, obligationOwnerCaps } = await initialize.initializeObligations(
|
|
1084
|
+
this.client,
|
|
1085
|
+
sdk,
|
|
1086
|
+
refreshedRawReserves,
|
|
1087
|
+
reserveMap,
|
|
1088
|
+
address
|
|
1089
|
+
);
|
|
1090
|
+
if (obligationOwnerCaps.length === 0 || obligations.length === 0) {
|
|
1091
|
+
return { supplies, borrows };
|
|
1092
|
+
}
|
|
1093
|
+
const obligation = obligations[0];
|
|
1094
|
+
for (const dep of obligation.deposits) {
|
|
1095
|
+
const symbol = this.resolveSymbol(dep.coinType);
|
|
1096
|
+
const amount = dep.depositedAmount.toNumber();
|
|
1097
|
+
const amountUsd = dep.depositedAmountUsd.toNumber();
|
|
1098
|
+
const apy = dep.reserve.depositAprPercent.toNumber();
|
|
1099
|
+
if (amount > 1e-4) {
|
|
1100
|
+
supplies.push({ asset: symbol, amount, amountUsd, apy });
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
for (const bor of obligation.borrows) {
|
|
1104
|
+
const symbol = this.resolveSymbol(bor.coinType);
|
|
1105
|
+
const amount = bor.borrowedAmount.toNumber();
|
|
1106
|
+
const amountUsd = bor.borrowedAmountUsd.toNumber();
|
|
1107
|
+
const apy = bor.reserve.borrowAprPercent.toNumber();
|
|
1108
|
+
if (amount > 1e-4) {
|
|
1109
|
+
borrows.push({ asset: symbol, amount, amountUsd, apy });
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
} catch (err) {
|
|
1113
|
+
if (err instanceof T2000Error) throw err;
|
|
1114
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1115
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", `Suilend getPositions failed: ${msg}`);
|
|
1661
1116
|
}
|
|
1662
1117
|
return { supplies, borrows };
|
|
1663
1118
|
}
|
|
1664
1119
|
async getHealth(address) {
|
|
1665
|
-
|
|
1666
|
-
|
|
1120
|
+
try {
|
|
1121
|
+
const sdk = await this.getSdkClient();
|
|
1122
|
+
const { reserveMap, refreshedRawReserves } = await initialize.initializeSuilend(this.client, sdk);
|
|
1123
|
+
const { obligations, obligationOwnerCaps } = await initialize.initializeObligations(
|
|
1124
|
+
this.client,
|
|
1125
|
+
sdk,
|
|
1126
|
+
refreshedRawReserves,
|
|
1127
|
+
reserveMap,
|
|
1128
|
+
address
|
|
1129
|
+
);
|
|
1130
|
+
if (obligationOwnerCaps.length === 0 || obligations.length === 0) {
|
|
1131
|
+
return { healthFactor: Infinity, supplied: 0, borrowed: 0, maxBorrow: 0, liquidationThreshold: 0 };
|
|
1132
|
+
}
|
|
1133
|
+
const ob = obligations[0];
|
|
1134
|
+
const supplied = ob.depositedAmountUsd.toNumber();
|
|
1135
|
+
const borrowed = ob.borrowedAmountUsd.toNumber();
|
|
1136
|
+
const borrowLimit = ob.borrowLimitUsd.toNumber();
|
|
1137
|
+
const unhealthy = ob.unhealthyBorrowValueUsd.toNumber();
|
|
1138
|
+
const liqThreshold = supplied > 0 ? unhealthy / supplied : 0.75;
|
|
1139
|
+
const healthFactor = borrowed > 0 ? unhealthy / borrowed : Infinity;
|
|
1140
|
+
const maxBorrow = Math.max(0, borrowLimit - borrowed);
|
|
1141
|
+
return { healthFactor, supplied, borrowed, maxBorrow, liquidationThreshold: liqThreshold };
|
|
1142
|
+
} catch {
|
|
1667
1143
|
return { healthFactor: Infinity, supplied: 0, borrowed: 0, maxBorrow: 0, liquidationThreshold: 0 };
|
|
1668
1144
|
}
|
|
1669
|
-
const [reserves, obligation] = await Promise.all([
|
|
1670
|
-
this.loadReserves(),
|
|
1671
|
-
this.fetchObligation(caps[0].obligationId)
|
|
1672
|
-
]);
|
|
1673
|
-
let supplied = 0;
|
|
1674
|
-
let borrowed = 0;
|
|
1675
|
-
let weightedCloseLtv = 0;
|
|
1676
|
-
let weightedOpenLtv = 0;
|
|
1677
|
-
for (const dep of obligation.deposits) {
|
|
1678
|
-
const reserve = reserves[dep.reserveIdx];
|
|
1679
|
-
if (!reserve) continue;
|
|
1680
|
-
const ratio = cTokenRatio(reserve);
|
|
1681
|
-
const amount = dep.ctokenAmount * ratio / 10 ** reserve.mintDecimals;
|
|
1682
|
-
supplied += amount;
|
|
1683
|
-
weightedCloseLtv += amount * (reserve.closeLtvPct / 100);
|
|
1684
|
-
weightedOpenLtv += amount * (reserve.openLtvPct / 100);
|
|
1685
|
-
}
|
|
1686
|
-
for (const bor of obligation.borrows) {
|
|
1687
|
-
const reserve = reserves[bor.reserveIdx];
|
|
1688
|
-
if (!reserve) continue;
|
|
1689
|
-
const rawAmount = bor.borrowedWad / WAD / 10 ** reserve.mintDecimals;
|
|
1690
|
-
const reserveRate = reserve.cumulativeBorrowRateWad / WAD;
|
|
1691
|
-
const posRate = bor.cumBorrowRateWad / WAD;
|
|
1692
|
-
borrowed += posRate > 0 ? rawAmount * (reserveRate / posRate) : rawAmount;
|
|
1693
|
-
}
|
|
1694
|
-
const liqThreshold = supplied > 0 ? weightedCloseLtv / supplied : 0.75;
|
|
1695
|
-
const openLtv = supplied > 0 ? weightedOpenLtv / supplied : 0.7;
|
|
1696
|
-
const healthFactor = borrowed > 0 ? supplied * liqThreshold / borrowed : Infinity;
|
|
1697
|
-
const maxBorrow = Math.max(0, supplied * openLtv - borrowed);
|
|
1698
|
-
return { healthFactor, supplied, borrowed, maxBorrow, liquidationThreshold: liqThreshold };
|
|
1699
1145
|
}
|
|
1700
1146
|
async buildSaveTx(address, amount, asset, options) {
|
|
1701
1147
|
const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
|
|
1702
1148
|
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
1703
|
-
const
|
|
1704
|
-
const
|
|
1705
|
-
if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend. Try: NAVI or a different asset.`);
|
|
1706
|
-
const caps = await this.fetchObligationCaps(address);
|
|
1149
|
+
const sdk = await this.getSdkClient();
|
|
1150
|
+
const caps = await client.SuilendClient.getObligationOwnerCaps(address, [client.LENDING_MARKET_TYPE], this.client);
|
|
1707
1151
|
const tx = new transactions.Transaction();
|
|
1708
1152
|
tx.setSender(address);
|
|
1709
|
-
let capRef;
|
|
1710
1153
|
if (caps.length === 0) {
|
|
1711
|
-
const
|
|
1712
|
-
|
|
1713
|
-
typeArguments: [LENDING_MARKET_TYPE],
|
|
1714
|
-
arguments: [tx.object(LENDING_MARKET_ID)]
|
|
1715
|
-
});
|
|
1716
|
-
capRef = newCap;
|
|
1717
|
-
} else {
|
|
1718
|
-
capRef = caps[0].id;
|
|
1154
|
+
const newCap = sdk.createObligation(tx);
|
|
1155
|
+
tx.transferObjects([newCap], address);
|
|
1719
1156
|
}
|
|
1720
|
-
const
|
|
1721
|
-
if (allCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins found`);
|
|
1722
|
-
const primaryCoinId = allCoins[0].coinObjectId;
|
|
1723
|
-
if (allCoins.length > 1) {
|
|
1724
|
-
tx.mergeCoins(tx.object(primaryCoinId), allCoins.slice(1).map((c) => tx.object(c.coinObjectId)));
|
|
1725
|
-
}
|
|
1726
|
-
const rawAmount = stableToRaw(amount, assetInfo.decimals).toString();
|
|
1727
|
-
const [depositCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawAmount]);
|
|
1157
|
+
const rawValue = stableToRaw(amount, assetInfo.decimals).toString();
|
|
1728
1158
|
if (options?.collectFee) {
|
|
1159
|
+
const allCoins = await this.fetchAllCoins(address, assetInfo.type);
|
|
1160
|
+
if (allCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins found`);
|
|
1161
|
+
const primaryCoinId = allCoins[0].coinObjectId;
|
|
1162
|
+
if (allCoins.length > 1) {
|
|
1163
|
+
tx.mergeCoins(tx.object(primaryCoinId), allCoins.slice(1).map((c) => tx.object(c.coinObjectId)));
|
|
1164
|
+
}
|
|
1165
|
+
const [depositCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawValue]);
|
|
1729
1166
|
addCollectFeeToTx(tx, depositCoin, "save");
|
|
1730
1167
|
}
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
tx.object(LENDING_MARKET_ID),
|
|
1736
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
1737
|
-
tx.object(CLOCK2),
|
|
1738
|
-
depositCoin
|
|
1739
|
-
]
|
|
1740
|
-
});
|
|
1741
|
-
tx.moveCall({
|
|
1742
|
-
target: `${pkg}::lending_market::deposit_ctokens_into_obligation`,
|
|
1743
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
1744
|
-
arguments: [
|
|
1745
|
-
tx.object(LENDING_MARKET_ID),
|
|
1746
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
1747
|
-
typeof capRef === "string" ? tx.object(capRef) : capRef,
|
|
1748
|
-
tx.object(CLOCK2),
|
|
1749
|
-
ctokens
|
|
1750
|
-
]
|
|
1751
|
-
});
|
|
1752
|
-
if (typeof capRef !== "string") {
|
|
1753
|
-
tx.transferObjects([capRef], address);
|
|
1168
|
+
if (caps.length > 0) {
|
|
1169
|
+
await sdk.depositIntoObligation(address, assetInfo.type, rawValue, tx, caps[0].id);
|
|
1170
|
+
} else {
|
|
1171
|
+
await sdk.depositIntoObligation(address, assetInfo.type, rawValue, tx, tx.object(caps[0]?.id ?? ""));
|
|
1754
1172
|
}
|
|
1755
1173
|
return { tx };
|
|
1756
1174
|
}
|
|
1757
1175
|
async buildWithdrawTx(address, amount, asset) {
|
|
1758
1176
|
const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
|
|
1759
1177
|
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
1760
|
-
const
|
|
1761
|
-
const
|
|
1762
|
-
if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
|
|
1763
|
-
const caps = await this.fetchObligationCaps(address);
|
|
1178
|
+
const sdk = await this.getSdkClient();
|
|
1179
|
+
const caps = await client.SuilendClient.getObligationOwnerCaps(address, [client.LENDING_MARKET_TYPE], this.client);
|
|
1764
1180
|
if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found");
|
|
1765
|
-
const
|
|
1766
|
-
const dep =
|
|
1767
|
-
const
|
|
1768
|
-
const deposited = dep ? dep.ctokenAmount * ratio / 10 ** reserve.mintDecimals : 0;
|
|
1181
|
+
const positions = await this.getPositions(address);
|
|
1182
|
+
const dep = positions.supplies.find((s) => s.asset === assetKey);
|
|
1183
|
+
const deposited = dep?.amount ?? 0;
|
|
1769
1184
|
const effectiveAmount = Math.min(amount, deposited);
|
|
1770
1185
|
if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on Suilend`);
|
|
1771
|
-
const
|
|
1772
|
-
const isFullWithdraw = dep && effectiveAmount >= deposited * 0.999;
|
|
1773
|
-
const withdrawArg = isFullWithdraw ? U64_MAX : String(Math.floor(effectiveAmount * 10 ** reserve.mintDecimals / ratio));
|
|
1186
|
+
const rawValue = stableToRaw(effectiveAmount, assetInfo.decimals).toString();
|
|
1774
1187
|
const tx = new transactions.Transaction();
|
|
1775
1188
|
tx.setSender(address);
|
|
1776
|
-
|
|
1777
|
-
target: `${pkg}::lending_market::withdraw_ctokens`,
|
|
1778
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
1779
|
-
arguments: [
|
|
1780
|
-
tx.object(LENDING_MARKET_ID),
|
|
1781
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
1782
|
-
tx.object(caps[0].id),
|
|
1783
|
-
tx.object(CLOCK2),
|
|
1784
|
-
tx.pure("u64", BigInt(withdrawArg))
|
|
1785
|
-
]
|
|
1786
|
-
});
|
|
1787
|
-
const coin = this.redeemCtokens(tx, pkg, reserve, assetInfo.type, assetKey, ctokens);
|
|
1788
|
-
tx.transferObjects([coin], address);
|
|
1189
|
+
await sdk.withdrawAndSendToUser(address, caps[0].id, caps[0].obligationId, assetInfo.type, rawValue, tx);
|
|
1789
1190
|
return { tx, effectiveAmount };
|
|
1790
1191
|
}
|
|
1791
1192
|
async addWithdrawToTx(tx, address, amount, asset) {
|
|
1792
1193
|
const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
|
|
1793
1194
|
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
1794
|
-
const
|
|
1795
|
-
const
|
|
1796
|
-
if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
|
|
1797
|
-
const caps = await this.fetchObligationCaps(address);
|
|
1195
|
+
const sdk = await this.getSdkClient();
|
|
1196
|
+
const caps = await client.SuilendClient.getObligationOwnerCaps(address, [client.LENDING_MARKET_TYPE], this.client);
|
|
1798
1197
|
if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found");
|
|
1799
|
-
const
|
|
1800
|
-
const dep =
|
|
1801
|
-
const
|
|
1802
|
-
const deposited = dep ? dep.ctokenAmount * ratio / 10 ** reserve.mintDecimals : 0;
|
|
1198
|
+
const positions = await this.getPositions(address);
|
|
1199
|
+
const dep = positions.supplies.find((s) => s.asset === assetKey);
|
|
1200
|
+
const deposited = dep?.amount ?? 0;
|
|
1803
1201
|
const effectiveAmount = Math.min(amount, deposited);
|
|
1804
1202
|
if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on Suilend`);
|
|
1805
|
-
const
|
|
1806
|
-
const [
|
|
1807
|
-
target: `${pkg}::lending_market::withdraw_ctokens`,
|
|
1808
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
1809
|
-
arguments: [
|
|
1810
|
-
tx.object(LENDING_MARKET_ID),
|
|
1811
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
1812
|
-
tx.object(caps[0].id),
|
|
1813
|
-
tx.object(CLOCK2),
|
|
1814
|
-
tx.pure.u64(ctokenAmount)
|
|
1815
|
-
]
|
|
1816
|
-
});
|
|
1817
|
-
const coin = this.redeemCtokens(tx, pkg, reserve, assetInfo.type, assetKey, ctokens);
|
|
1203
|
+
const rawValue = stableToRaw(effectiveAmount, assetInfo.decimals).toString();
|
|
1204
|
+
const coin = await sdk.withdraw(caps[0].id, caps[0].obligationId, assetInfo.type, rawValue, tx);
|
|
1818
1205
|
return { coin, effectiveAmount };
|
|
1819
1206
|
}
|
|
1820
|
-
/**
|
|
1821
|
-
* 3-step cToken redemption matching the official Suilend SDK flow:
|
|
1822
|
-
* 1. redeem_ctokens_and_withdraw_liquidity_request — creates a LiquidityRequest
|
|
1823
|
-
* 2. unstake_sui_from_staker — (SUI only) unstakes from validators to replenish available_liquidity
|
|
1824
|
-
* 3. fulfill_liquidity_request — splits underlying tokens from the reserve
|
|
1825
|
-
*/
|
|
1826
|
-
redeemCtokens(tx, pkg, reserve, coinType, assetKey, ctokens) {
|
|
1827
|
-
const exemptionType = `${SUILEND_PACKAGE}::lending_market::RateLimiterExemption<${LENDING_MARKET_TYPE}, ${coinType}>`;
|
|
1828
|
-
const [none] = tx.moveCall({
|
|
1829
|
-
target: "0x1::option::none",
|
|
1830
|
-
typeArguments: [exemptionType]
|
|
1831
|
-
});
|
|
1832
|
-
const [liquidityRequest] = tx.moveCall({
|
|
1833
|
-
target: `${pkg}::lending_market::redeem_ctokens_and_withdraw_liquidity_request`,
|
|
1834
|
-
typeArguments: [LENDING_MARKET_TYPE, coinType],
|
|
1835
|
-
arguments: [
|
|
1836
|
-
tx.object(LENDING_MARKET_ID),
|
|
1837
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
1838
|
-
tx.object(CLOCK2),
|
|
1839
|
-
ctokens,
|
|
1840
|
-
none
|
|
1841
|
-
]
|
|
1842
|
-
});
|
|
1843
|
-
if (assetKey === "SUI") {
|
|
1844
|
-
tx.moveCall({
|
|
1845
|
-
target: `${pkg}::lending_market::unstake_sui_from_staker`,
|
|
1846
|
-
typeArguments: [LENDING_MARKET_TYPE],
|
|
1847
|
-
arguments: [
|
|
1848
|
-
tx.object(LENDING_MARKET_ID),
|
|
1849
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
1850
|
-
liquidityRequest,
|
|
1851
|
-
tx.object(SUI_SYSTEM_STATE2)
|
|
1852
|
-
]
|
|
1853
|
-
});
|
|
1854
|
-
}
|
|
1855
|
-
const [coin] = tx.moveCall({
|
|
1856
|
-
target: `${pkg}::lending_market::fulfill_liquidity_request`,
|
|
1857
|
-
typeArguments: [LENDING_MARKET_TYPE, coinType],
|
|
1858
|
-
arguments: [
|
|
1859
|
-
tx.object(LENDING_MARKET_ID),
|
|
1860
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
1861
|
-
liquidityRequest
|
|
1862
|
-
]
|
|
1863
|
-
});
|
|
1864
|
-
return coin;
|
|
1865
|
-
}
|
|
1866
1207
|
async addSaveToTx(tx, address, coin, asset, options) {
|
|
1867
1208
|
const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
|
|
1868
1209
|
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
1869
|
-
const
|
|
1870
|
-
const
|
|
1871
|
-
if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
|
|
1872
|
-
const caps = await this.fetchObligationCaps(address);
|
|
1210
|
+
const sdk = await this.getSdkClient();
|
|
1211
|
+
const caps = await client.SuilendClient.getObligationOwnerCaps(address, [client.LENDING_MARKET_TYPE], this.client);
|
|
1873
1212
|
let capRef;
|
|
1874
1213
|
if (caps.length === 0) {
|
|
1875
|
-
const
|
|
1876
|
-
target: `${pkg}::lending_market::create_obligation`,
|
|
1877
|
-
typeArguments: [LENDING_MARKET_TYPE],
|
|
1878
|
-
arguments: [tx.object(LENDING_MARKET_ID)]
|
|
1879
|
-
});
|
|
1214
|
+
const newCap = sdk.createObligation(tx);
|
|
1880
1215
|
capRef = newCap;
|
|
1216
|
+
tx.transferObjects([newCap], address);
|
|
1881
1217
|
} else {
|
|
1882
1218
|
capRef = caps[0].id;
|
|
1883
1219
|
}
|
|
1884
1220
|
if (options?.collectFee) {
|
|
1885
1221
|
addCollectFeeToTx(tx, coin, "save");
|
|
1886
1222
|
}
|
|
1887
|
-
|
|
1888
|
-
target: `${pkg}::lending_market::deposit_liquidity_and_mint_ctokens`,
|
|
1889
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
1890
|
-
arguments: [
|
|
1891
|
-
tx.object(LENDING_MARKET_ID),
|
|
1892
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
1893
|
-
tx.object(CLOCK2),
|
|
1894
|
-
coin
|
|
1895
|
-
]
|
|
1896
|
-
});
|
|
1897
|
-
tx.moveCall({
|
|
1898
|
-
target: `${pkg}::lending_market::deposit_ctokens_into_obligation`,
|
|
1899
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
1900
|
-
arguments: [
|
|
1901
|
-
tx.object(LENDING_MARKET_ID),
|
|
1902
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
1903
|
-
typeof capRef === "string" ? tx.object(capRef) : capRef,
|
|
1904
|
-
tx.object(CLOCK2),
|
|
1905
|
-
ctokens
|
|
1906
|
-
]
|
|
1907
|
-
});
|
|
1908
|
-
if (typeof capRef !== "string") {
|
|
1909
|
-
tx.transferObjects([capRef], address);
|
|
1910
|
-
}
|
|
1223
|
+
sdk.deposit(coin, assetInfo.type, capRef, tx);
|
|
1911
1224
|
}
|
|
1912
1225
|
async buildBorrowTx(address, amount, asset, options) {
|
|
1913
1226
|
const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
|
|
1914
1227
|
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
1915
|
-
const
|
|
1916
|
-
const
|
|
1917
|
-
if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend. Try: NAVI or a different asset.`);
|
|
1918
|
-
const caps = await this.fetchObligationCaps(address);
|
|
1228
|
+
const sdk = await this.getSdkClient();
|
|
1229
|
+
const caps = await client.SuilendClient.getObligationOwnerCaps(address, [client.LENDING_MARKET_TYPE], this.client);
|
|
1919
1230
|
if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found. Deposit collateral first with: t2000 save <amount>");
|
|
1920
|
-
const
|
|
1231
|
+
const rawValue = stableToRaw(amount, assetInfo.decimals).toString();
|
|
1921
1232
|
const tx = new transactions.Transaction();
|
|
1922
1233
|
tx.setSender(address);
|
|
1923
|
-
const [coin] = tx.moveCall({
|
|
1924
|
-
target: `${pkg}::lending_market::borrow`,
|
|
1925
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
1926
|
-
arguments: [
|
|
1927
|
-
tx.object(LENDING_MARKET_ID),
|
|
1928
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
1929
|
-
tx.object(caps[0].id),
|
|
1930
|
-
tx.object(CLOCK2),
|
|
1931
|
-
tx.pure.u64(rawAmount)
|
|
1932
|
-
]
|
|
1933
|
-
});
|
|
1934
1234
|
if (options?.collectFee) {
|
|
1235
|
+
const coin = await sdk.borrow(caps[0].id, caps[0].obligationId, assetInfo.type, rawValue, tx);
|
|
1935
1236
|
addCollectFeeToTx(tx, coin, "borrow");
|
|
1237
|
+
tx.transferObjects([coin], address);
|
|
1238
|
+
} else {
|
|
1239
|
+
await sdk.borrowAndSendToUser(address, caps[0].id, caps[0].obligationId, assetInfo.type, rawValue, tx);
|
|
1936
1240
|
}
|
|
1937
|
-
tx.transferObjects([coin], address);
|
|
1938
1241
|
return { tx };
|
|
1939
1242
|
}
|
|
1940
1243
|
async buildRepayTx(address, amount, asset) {
|
|
1941
1244
|
const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
|
|
1942
1245
|
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
1943
|
-
const
|
|
1944
|
-
const
|
|
1945
|
-
if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
|
|
1946
|
-
const caps = await this.fetchObligationCaps(address);
|
|
1246
|
+
const sdk = await this.getSdkClient();
|
|
1247
|
+
const caps = await client.SuilendClient.getObligationOwnerCaps(address, [client.LENDING_MARKET_TYPE], this.client);
|
|
1947
1248
|
if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend obligation found");
|
|
1948
|
-
const
|
|
1949
|
-
if (allCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins to repay with`);
|
|
1950
|
-
const rawAmount = stableToRaw(amount, assetInfo.decimals);
|
|
1249
|
+
const rawValue = stableToRaw(amount, assetInfo.decimals).toString();
|
|
1951
1250
|
const tx = new transactions.Transaction();
|
|
1952
1251
|
tx.setSender(address);
|
|
1953
|
-
|
|
1954
|
-
if (allCoins.length > 1) {
|
|
1955
|
-
tx.mergeCoins(tx.object(primaryCoinId), allCoins.slice(1).map((c) => tx.object(c.coinObjectId)));
|
|
1956
|
-
}
|
|
1957
|
-
const [repayCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawAmount.toString()]);
|
|
1958
|
-
tx.moveCall({
|
|
1959
|
-
target: `${pkg}::lending_market::repay`,
|
|
1960
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
1961
|
-
arguments: [
|
|
1962
|
-
tx.object(LENDING_MARKET_ID),
|
|
1963
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
1964
|
-
tx.object(caps[0].id),
|
|
1965
|
-
tx.object(CLOCK2),
|
|
1966
|
-
repayCoin
|
|
1967
|
-
]
|
|
1968
|
-
});
|
|
1252
|
+
await sdk.repayIntoObligation(address, caps[0].obligationId, assetInfo.type, rawValue, tx);
|
|
1969
1253
|
return { tx };
|
|
1970
1254
|
}
|
|
1971
1255
|
async addRepayToTx(tx, address, coin, asset) {
|
|
1972
1256
|
const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
|
|
1973
1257
|
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
1974
|
-
const
|
|
1975
|
-
const
|
|
1976
|
-
if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
|
|
1977
|
-
const caps = await this.fetchObligationCaps(address);
|
|
1258
|
+
const sdk = await this.getSdkClient();
|
|
1259
|
+
const caps = await client.SuilendClient.getObligationOwnerCaps(address, [client.LENDING_MARKET_TYPE], this.client);
|
|
1978
1260
|
if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend obligation found");
|
|
1979
|
-
|
|
1980
|
-
target: `${pkg}::lending_market::repay`,
|
|
1981
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
1982
|
-
arguments: [
|
|
1983
|
-
tx.object(LENDING_MARKET_ID),
|
|
1984
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
1985
|
-
tx.object(caps[0].id),
|
|
1986
|
-
tx.object(CLOCK2),
|
|
1987
|
-
coin
|
|
1988
|
-
]
|
|
1989
|
-
});
|
|
1261
|
+
sdk.repay(caps[0].obligationId, assetInfo.type, coin, tx);
|
|
1990
1262
|
}
|
|
1991
1263
|
async maxWithdraw(address, _asset) {
|
|
1992
1264
|
const health = await this.getHealth(address);
|
|
@@ -2002,8 +1274,7 @@ var SuilendAdapter = class {
|
|
|
2002
1274
|
}
|
|
2003
1275
|
async maxBorrow(address, _asset) {
|
|
2004
1276
|
const health = await this.getHealth(address);
|
|
2005
|
-
|
|
2006
|
-
return { maxAmount, healthFactorAfter: MIN_HEALTH_FACTOR2, currentHF: health.healthFactor };
|
|
1277
|
+
return { maxAmount: health.maxBorrow, healthFactorAfter: MIN_HEALTH_FACTOR2, currentHF: health.healthFactor };
|
|
2007
1278
|
}
|
|
2008
1279
|
async fetchAllCoins(owner, coinType) {
|
|
2009
1280
|
const all = [];
|
|
@@ -2017,88 +1288,79 @@ var SuilendAdapter = class {
|
|
|
2017
1288
|
}
|
|
2018
1289
|
return all;
|
|
2019
1290
|
}
|
|
2020
|
-
// -- Claim Rewards --------------------------------------------------------
|
|
2021
|
-
isClaimableReward(coinType) {
|
|
2022
|
-
const ct = coinType.toLowerCase();
|
|
2023
|
-
return ct.includes("spring_sui") || ct.includes("deep::deep") || ct.includes("cert::cert");
|
|
2024
|
-
}
|
|
2025
1291
|
async getPendingRewards(address) {
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
const
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
1292
|
+
try {
|
|
1293
|
+
const sdk = await this.getSdkClient();
|
|
1294
|
+
const { reserveMap, refreshedRawReserves } = await initialize.initializeSuilend(this.client, sdk);
|
|
1295
|
+
const { obligations, obligationOwnerCaps } = await initialize.initializeObligations(
|
|
1296
|
+
this.client,
|
|
1297
|
+
sdk,
|
|
1298
|
+
refreshedRawReserves,
|
|
1299
|
+
reserveMap,
|
|
1300
|
+
address
|
|
1301
|
+
);
|
|
1302
|
+
if (obligationOwnerCaps.length === 0 || obligations.length === 0) return [];
|
|
1303
|
+
const ob = obligations[0];
|
|
1304
|
+
const rewards = [];
|
|
1305
|
+
for (const dep of ob.deposits) {
|
|
1306
|
+
for (const rw of dep.reserve.depositsPoolRewardManager.poolRewards) {
|
|
1307
|
+
if (rw.endTimeMs <= Date.now()) continue;
|
|
1308
|
+
const symbol = rw.symbol || rw.coinType.split("::").pop() || "UNKNOWN";
|
|
1309
|
+
rewards.push({
|
|
1310
|
+
protocol: "suilend",
|
|
1311
|
+
asset: this.resolveSymbol(dep.coinType),
|
|
1312
|
+
coinType: rw.coinType,
|
|
1313
|
+
symbol,
|
|
1314
|
+
amount: 0,
|
|
1315
|
+
estimatedValueUsd: 0
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
2050
1318
|
}
|
|
1319
|
+
return rewards;
|
|
1320
|
+
} catch {
|
|
1321
|
+
return [];
|
|
2051
1322
|
}
|
|
2052
|
-
return rewards;
|
|
2053
1323
|
}
|
|
2054
1324
|
async addClaimRewardsToTx(tx, address) {
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
this.
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
});
|
|
2081
|
-
const existing = claimsByToken.get(rw.coinType) ?? [];
|
|
2082
|
-
existing.push(coin);
|
|
2083
|
-
claimsByToken.set(rw.coinType, existing);
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2086
|
-
for (const [coinType, coins] of claimsByToken) {
|
|
2087
|
-
if (coins.length > 1) {
|
|
2088
|
-
tx.mergeCoins(coins[0], coins.slice(1));
|
|
1325
|
+
try {
|
|
1326
|
+
const sdk = await this.getSdkClient();
|
|
1327
|
+
const caps = await client.SuilendClient.getObligationOwnerCaps(address, [client.LENDING_MARKET_TYPE], this.client);
|
|
1328
|
+
if (caps.length === 0) return [];
|
|
1329
|
+
const { reserveMap, refreshedRawReserves } = await initialize.initializeSuilend(this.client, sdk);
|
|
1330
|
+
const { obligations } = await initialize.initializeObligations(
|
|
1331
|
+
this.client,
|
|
1332
|
+
sdk,
|
|
1333
|
+
refreshedRawReserves,
|
|
1334
|
+
reserveMap,
|
|
1335
|
+
address
|
|
1336
|
+
);
|
|
1337
|
+
if (obligations.length === 0) return [];
|
|
1338
|
+
const ob = obligations[0];
|
|
1339
|
+
const claimRewards = [];
|
|
1340
|
+
for (const dep of ob.deposits) {
|
|
1341
|
+
for (const rw of dep.reserve.depositsPoolRewardManager.poolRewards) {
|
|
1342
|
+
if (rw.endTimeMs <= Date.now()) continue;
|
|
1343
|
+
claimRewards.push({
|
|
1344
|
+
reserveArrayIndex: dep.reserveArrayIndex,
|
|
1345
|
+
rewardIndex: BigInt(rw.rewardIndex),
|
|
1346
|
+
rewardCoinType: rw.coinType,
|
|
1347
|
+
side: types.Side.DEPOSIT
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
2089
1350
|
}
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
1351
|
+
if (claimRewards.length === 0) return [];
|
|
1352
|
+
sdk.claimRewardsAndSendToUser(address, caps[0].id, claimRewards, tx);
|
|
1353
|
+
return claimRewards.map((r) => ({
|
|
2093
1354
|
protocol: "suilend",
|
|
2094
1355
|
asset: "",
|
|
2095
|
-
coinType,
|
|
2096
|
-
symbol,
|
|
1356
|
+
coinType: r.rewardCoinType,
|
|
1357
|
+
symbol: r.rewardCoinType.split("::").pop() ?? "UNKNOWN",
|
|
2097
1358
|
amount: 0,
|
|
2098
1359
|
estimatedValueUsd: 0
|
|
2099
|
-
});
|
|
1360
|
+
}));
|
|
1361
|
+
} catch {
|
|
1362
|
+
return [];
|
|
2100
1363
|
}
|
|
2101
|
-
return claimed;
|
|
2102
1364
|
}
|
|
2103
1365
|
};
|
|
2104
1366
|
var descriptor4 = {
|