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