@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/index.js
CHANGED
|
@@ -8,8 +8,12 @@ import { createHash, randomUUID, randomBytes, createCipheriv, createDecipheriv,
|
|
|
8
8
|
import { access, mkdir, writeFile, readFile } from 'fs/promises';
|
|
9
9
|
import { join, dirname, resolve } from 'path';
|
|
10
10
|
import { homedir } from 'os';
|
|
11
|
+
import { getPools, getLendingPositions, getHealthFactor as getHealthFactor$1, depositCoinPTB, withdrawCoinPTB, borrowCoinPTB, repayCoinPTB, getUserAvailableLendingRewards, summaryLendingRewards, claimLendingRewardsPTB } from '@naviprotocol/lending';
|
|
11
12
|
import { bcs } from '@mysten/sui/bcs';
|
|
12
13
|
import { AggregatorClient, Env } from '@cetusprotocol/aggregator-sdk';
|
|
14
|
+
import { SuilendClient, LENDING_MARKET_ID, LENDING_MARKET_TYPE } from '@suilend/sdk/client';
|
|
15
|
+
import { initializeSuilend, initializeObligations } from '@suilend/sdk/lib/initialize';
|
|
16
|
+
import { Side } from '@suilend/sdk/lib/types';
|
|
13
17
|
import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
14
18
|
|
|
15
19
|
// src/t2000.ts
|
|
@@ -586,80 +590,32 @@ async function reportFee(agentAddress, operation, feeAmount, feeRate, txDigest)
|
|
|
586
590
|
} catch {
|
|
587
591
|
}
|
|
588
592
|
}
|
|
589
|
-
SUPPORTED_ASSETS.USDC.type;
|
|
590
|
-
var RATE_DECIMALS = 27;
|
|
591
|
-
var LTV_DECIMALS = 27;
|
|
592
593
|
var MIN_HEALTH_FACTOR = 1.5;
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
}
|
|
596
|
-
var CLOCK = "0x06";
|
|
597
|
-
var SUI_SYSTEM_STATE = "0x05";
|
|
598
|
-
var NAVI_BALANCE_DECIMALS = 9;
|
|
599
|
-
var CONFIG_API = "https://open-api.naviprotocol.io/api/navi/config?env=prod";
|
|
600
|
-
var POOLS_API = "https://open-api.naviprotocol.io/api/navi/pools?env=prod";
|
|
601
|
-
var PACKAGE_API = "https://open-api.naviprotocol.io/api/package";
|
|
602
|
-
var packageCache = null;
|
|
603
|
-
function toBigInt(v) {
|
|
604
|
-
if (typeof v === "bigint") return v;
|
|
605
|
-
return BigInt(String(v));
|
|
606
|
-
}
|
|
607
|
-
var UserStateInfo = bcs.struct("UserStateInfo", {
|
|
608
|
-
asset_id: bcs.u8(),
|
|
609
|
-
borrow_balance: bcs.u256(),
|
|
610
|
-
supply_balance: bcs.u256()
|
|
611
|
-
});
|
|
612
|
-
function decodeDevInspect(result, schema) {
|
|
613
|
-
const rv = result.results?.[0]?.returnValues?.[0];
|
|
614
|
-
if (result.error || !rv) return void 0;
|
|
615
|
-
const bytes = Uint8Array.from(rv[0]);
|
|
616
|
-
return schema.parse(bytes);
|
|
617
|
-
}
|
|
618
|
-
var configCache = null;
|
|
619
|
-
var poolsCache = null;
|
|
620
|
-
var CACHE_TTL = 5 * 6e4;
|
|
621
|
-
async function fetchJson(url) {
|
|
622
|
-
const res = await fetch(url);
|
|
623
|
-
if (!res.ok) throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI API error: ${res.status}`);
|
|
624
|
-
const json = await res.json();
|
|
625
|
-
return json.data ?? json;
|
|
626
|
-
}
|
|
627
|
-
async function getLatestPackageId() {
|
|
628
|
-
if (packageCache && Date.now() - packageCache.ts < CACHE_TTL) return packageCache.id;
|
|
629
|
-
const res = await fetch(PACKAGE_API);
|
|
630
|
-
if (!res.ok) throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI package API error: ${res.status}`);
|
|
631
|
-
const json = await res.json();
|
|
632
|
-
if (!json.packageId) throw new T2000Error("PROTOCOL_UNAVAILABLE", "NAVI package API returned no packageId");
|
|
633
|
-
packageCache = { id: json.packageId, ts: Date.now() };
|
|
634
|
-
return json.packageId;
|
|
635
|
-
}
|
|
636
|
-
async function getConfig(fresh = false) {
|
|
637
|
-
if (configCache && !fresh && Date.now() - configCache.ts < CACHE_TTL) return configCache.data;
|
|
638
|
-
const [data, latestPkg] = await Promise.all([
|
|
639
|
-
fetchJson(CONFIG_API),
|
|
640
|
-
getLatestPackageId()
|
|
641
|
-
]);
|
|
642
|
-
data.package = latestPkg;
|
|
643
|
-
configCache = { data, ts: Date.now() };
|
|
644
|
-
return data;
|
|
645
|
-
}
|
|
646
|
-
async function getPools(fresh = false) {
|
|
647
|
-
if (poolsCache && !fresh && Date.now() - poolsCache.ts < CACHE_TTL) return poolsCache.data;
|
|
648
|
-
const data = await fetchJson(POOLS_API);
|
|
649
|
-
poolsCache = { data, ts: Date.now() };
|
|
650
|
-
return data;
|
|
651
|
-
}
|
|
652
|
-
function matchesCoinType(poolType, targetType) {
|
|
653
|
-
const poolSuffix = poolType.split("::").slice(1).join("::").toLowerCase();
|
|
654
|
-
const targetSuffix = targetType.split("::").slice(1).join("::").toLowerCase();
|
|
655
|
-
return poolSuffix === targetSuffix;
|
|
594
|
+
var NAVI_SUPPORTED_ASSETS = [...STABLE_ASSETS, "SUI", "ETH", "GOLD"];
|
|
595
|
+
function sdkOptions(client) {
|
|
596
|
+
return { env: "prod", client };
|
|
656
597
|
}
|
|
657
|
-
|
|
658
|
-
|
|
598
|
+
var NAVI_SYMBOL_MAP = {
|
|
599
|
+
nUSDC: "USDC",
|
|
600
|
+
suiUSDT: "USDT",
|
|
601
|
+
suiUSDe: "USDe",
|
|
602
|
+
XAUM: "GOLD",
|
|
603
|
+
WBTC: "BTC",
|
|
604
|
+
suiETH: "ETH",
|
|
605
|
+
WETH: "ETH",
|
|
606
|
+
SUI: "SUI",
|
|
607
|
+
USDC: "USDC",
|
|
608
|
+
USDT: "USDT",
|
|
609
|
+
USDe: "USDe",
|
|
610
|
+
USDsui: "USDsui"
|
|
611
|
+
};
|
|
612
|
+
function resolveNaviSymbol(sdkSymbol, coinType) {
|
|
659
613
|
for (const [key, info] of Object.entries(SUPPORTED_ASSETS)) {
|
|
660
|
-
|
|
614
|
+
const poolSuffix = coinType.split("::").slice(1).join("::").toLowerCase();
|
|
615
|
+
const targetSuffix = info.type.split("::").slice(1).join("::").toLowerCase();
|
|
616
|
+
if (poolSuffix === targetSuffix) return key;
|
|
661
617
|
}
|
|
662
|
-
return
|
|
618
|
+
return NAVI_SYMBOL_MAP[sdkSymbol] ?? sdkSymbol;
|
|
663
619
|
}
|
|
664
620
|
function resolveAssetInfo(asset) {
|
|
665
621
|
if (asset in SUPPORTED_ASSETS) {
|
|
@@ -668,109 +624,6 @@ function resolveAssetInfo(asset) {
|
|
|
668
624
|
}
|
|
669
625
|
throw new T2000Error("ASSET_NOT_SUPPORTED", `Unknown asset: ${asset}`);
|
|
670
626
|
}
|
|
671
|
-
async function getPool(asset = "USDC") {
|
|
672
|
-
const pools = await getPools();
|
|
673
|
-
const { type: targetType, displayName } = resolveAssetInfo(asset);
|
|
674
|
-
const pool = pools.find(
|
|
675
|
-
(p) => matchesCoinType(p.suiCoinType || p.coinType || "", targetType)
|
|
676
|
-
);
|
|
677
|
-
if (!pool) {
|
|
678
|
-
throw new T2000Error(
|
|
679
|
-
"ASSET_NOT_SUPPORTED",
|
|
680
|
-
`${displayName} pool not found on NAVI`
|
|
681
|
-
);
|
|
682
|
-
}
|
|
683
|
-
return pool;
|
|
684
|
-
}
|
|
685
|
-
function addOracleUpdate(tx, config, pool) {
|
|
686
|
-
const feed = config.oracle.feeds?.find((f2) => f2.assetId === pool.id);
|
|
687
|
-
if (!feed) {
|
|
688
|
-
throw new T2000Error("PROTOCOL_UNAVAILABLE", `Oracle feed not found for asset ${pool.token?.symbol ?? pool.id}`);
|
|
689
|
-
}
|
|
690
|
-
tx.moveCall({
|
|
691
|
-
target: `${config.oracle.packageId}::oracle_pro::update_single_price_v2`,
|
|
692
|
-
arguments: [
|
|
693
|
-
tx.object(CLOCK),
|
|
694
|
-
tx.object(config.oracle.oracleConfig),
|
|
695
|
-
tx.object(config.oracle.priceOracle),
|
|
696
|
-
tx.object(config.oracle.supraOracleHolder),
|
|
697
|
-
tx.object(feed.pythPriceInfoObject),
|
|
698
|
-
tx.object(config.oracle.switchboardAggregator),
|
|
699
|
-
tx.pure.address(feed.feedId)
|
|
700
|
-
]
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
function refreshOracles(tx, config, pools, opts) {
|
|
704
|
-
const assetsToRefresh = NAVI_SUPPORTED_ASSETS;
|
|
705
|
-
const targetTypes = assetsToRefresh.map((a) => SUPPORTED_ASSETS[a].type);
|
|
706
|
-
const matchedPools = pools.filter((p) => {
|
|
707
|
-
const ct = p.suiCoinType || p.coinType || "";
|
|
708
|
-
return targetTypes.some((t) => matchesCoinType(ct, t));
|
|
709
|
-
});
|
|
710
|
-
for (const pool of matchedPools) {
|
|
711
|
-
addOracleUpdate(tx, config, pool);
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
function rateToApy(rawRate) {
|
|
715
|
-
if (!rawRate || rawRate === "0") return 0;
|
|
716
|
-
return Number(BigInt(rawRate)) / 10 ** RATE_DECIMALS * 100;
|
|
717
|
-
}
|
|
718
|
-
function poolSaveApy(pool) {
|
|
719
|
-
const incentive = parseFloat(pool.supplyIncentiveApyInfo?.apy ?? "0");
|
|
720
|
-
if (incentive > 0) return incentive;
|
|
721
|
-
return rateToApy(pool.currentSupplyRate);
|
|
722
|
-
}
|
|
723
|
-
function poolBorrowApy(pool) {
|
|
724
|
-
const incentive = parseFloat(pool.borrowIncentiveApyInfo?.apy ?? "0");
|
|
725
|
-
if (incentive > 0) return incentive;
|
|
726
|
-
return rateToApy(pool.currentBorrowRate);
|
|
727
|
-
}
|
|
728
|
-
function parseLtv(rawLtv) {
|
|
729
|
-
if (!rawLtv || rawLtv === "0") return 0.75;
|
|
730
|
-
return Number(BigInt(rawLtv)) / 10 ** LTV_DECIMALS;
|
|
731
|
-
}
|
|
732
|
-
function parseLiqThreshold(val) {
|
|
733
|
-
if (typeof val === "number") return val;
|
|
734
|
-
const n = Number(val);
|
|
735
|
-
if (n > 1) return Number(BigInt(val)) / 10 ** LTV_DECIMALS;
|
|
736
|
-
return n;
|
|
737
|
-
}
|
|
738
|
-
function normalizeHealthFactor(raw) {
|
|
739
|
-
const v = raw / 10 ** RATE_DECIMALS;
|
|
740
|
-
return v > 1e5 ? Infinity : v;
|
|
741
|
-
}
|
|
742
|
-
function naviStorageDecimals(poolId, tokenDecimals) {
|
|
743
|
-
if (poolId <= 10) return NAVI_BALANCE_DECIMALS;
|
|
744
|
-
return tokenDecimals;
|
|
745
|
-
}
|
|
746
|
-
function compoundBalance(rawBalance, currentIndex, pool) {
|
|
747
|
-
if (!rawBalance || !currentIndex || currentIndex === "0") return 0;
|
|
748
|
-
const scale = BigInt("1" + "0".repeat(RATE_DECIMALS));
|
|
749
|
-
const half = scale / 2n;
|
|
750
|
-
const result = (rawBalance * BigInt(currentIndex) + half) / scale;
|
|
751
|
-
const decimals = pool ? naviStorageDecimals(pool.id, pool.token.decimals) : NAVI_BALANCE_DECIMALS;
|
|
752
|
-
return Number(result) / 10 ** decimals;
|
|
753
|
-
}
|
|
754
|
-
async function getUserState(client, address) {
|
|
755
|
-
const config = await getConfig();
|
|
756
|
-
const tx = new Transaction();
|
|
757
|
-
tx.moveCall({
|
|
758
|
-
target: `${config.uiGetter}::getter_unchecked::get_user_state`,
|
|
759
|
-
arguments: [tx.object(config.storage), tx.pure.address(address)]
|
|
760
|
-
});
|
|
761
|
-
const result = await client.devInspectTransactionBlock({
|
|
762
|
-
transactionBlock: tx,
|
|
763
|
-
sender: address
|
|
764
|
-
});
|
|
765
|
-
const decoded = decodeDevInspect(result, bcs.vector(UserStateInfo));
|
|
766
|
-
if (!decoded) return [];
|
|
767
|
-
const mapped = decoded.map((s) => ({
|
|
768
|
-
assetId: s.asset_id,
|
|
769
|
-
supplyBalance: toBigInt(s.supply_balance),
|
|
770
|
-
borrowBalance: toBigInt(s.borrow_balance)
|
|
771
|
-
}));
|
|
772
|
-
return mapped.filter((s) => s.supplyBalance !== 0n || s.borrowBalance !== 0n);
|
|
773
|
-
}
|
|
774
627
|
async function fetchCoins(client, owner, coinType) {
|
|
775
628
|
const all = [];
|
|
776
629
|
let cursor;
|
|
@@ -791,14 +644,98 @@ function mergeCoins(tx, coins) {
|
|
|
791
644
|
}
|
|
792
645
|
return primary;
|
|
793
646
|
}
|
|
647
|
+
async function getPositions(client, addressOrKeypair) {
|
|
648
|
+
const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
|
|
649
|
+
try {
|
|
650
|
+
const naviPositions = await getLendingPositions(address, {
|
|
651
|
+
...sdkOptions(client),
|
|
652
|
+
markets: ["main"]
|
|
653
|
+
});
|
|
654
|
+
const positions = [];
|
|
655
|
+
for (const pos of naviPositions) {
|
|
656
|
+
const data = pos["navi-lending-supply"] ?? pos["navi-lending-emode-supply"] ?? pos["navi-lending-borrow"] ?? pos["navi-lending-emode-borrow"];
|
|
657
|
+
if (!data) continue;
|
|
658
|
+
const isBorrow = pos.type.includes("borrow");
|
|
659
|
+
const symbol = resolveNaviSymbol(data.token.symbol, data.token.coinType);
|
|
660
|
+
const amount = parseFloat(data.amount);
|
|
661
|
+
const amountUsd = parseFloat(data.valueUSD);
|
|
662
|
+
const pool = data.pool;
|
|
663
|
+
const apy = isBorrow ? parseFloat(pool.borrowIncentiveApyInfo?.apy ?? "0") : parseFloat(pool.supplyIncentiveApyInfo?.apy ?? "0");
|
|
664
|
+
if (amount > 1e-4 || amountUsd > 1e-3) {
|
|
665
|
+
positions.push({
|
|
666
|
+
protocol: "navi",
|
|
667
|
+
asset: symbol,
|
|
668
|
+
type: isBorrow ? "borrow" : "save",
|
|
669
|
+
amount,
|
|
670
|
+
amountUsd,
|
|
671
|
+
apy
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return { positions };
|
|
676
|
+
} catch (err) {
|
|
677
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
678
|
+
if (msg.includes("not found") || msg.includes("404")) return { positions: [] };
|
|
679
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI getPositions failed: ${msg}`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
async function getRates(client) {
|
|
683
|
+
try {
|
|
684
|
+
const pools = await getPools(sdkOptions(client));
|
|
685
|
+
const result = {};
|
|
686
|
+
for (const asset of NAVI_SUPPORTED_ASSETS) {
|
|
687
|
+
const targetType = SUPPORTED_ASSETS[asset].type;
|
|
688
|
+
const pool = pools.find((p) => {
|
|
689
|
+
const poolSuffix = (p.suiCoinType || p.coinType || "").split("::").slice(1).join("::").toLowerCase();
|
|
690
|
+
const targetSuffix = targetType.split("::").slice(1).join("::").toLowerCase();
|
|
691
|
+
return poolSuffix === targetSuffix;
|
|
692
|
+
});
|
|
693
|
+
if (!pool) continue;
|
|
694
|
+
const saveApy = parseFloat(pool.supplyIncentiveApyInfo?.apy ?? "0");
|
|
695
|
+
const borrowApy = parseFloat(pool.borrowIncentiveApyInfo?.apy ?? "0");
|
|
696
|
+
if (saveApy >= 0 && saveApy < 200) {
|
|
697
|
+
result[asset] = { saveApy, borrowApy: borrowApy >= 0 && borrowApy < 200 ? borrowApy : 0 };
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if (!result.USDC) result.USDC = { saveApy: 4, borrowApy: 6 };
|
|
701
|
+
return result;
|
|
702
|
+
} catch {
|
|
703
|
+
return { USDC: { saveApy: 4, borrowApy: 6 } };
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
async function getHealthFactor(client, addressOrKeypair) {
|
|
707
|
+
const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
|
|
708
|
+
const posResult = await getPositions(client, address);
|
|
709
|
+
let supplied = 0;
|
|
710
|
+
let borrowed = 0;
|
|
711
|
+
for (const pos of posResult.positions) {
|
|
712
|
+
const usd = pos.amountUsd ?? pos.amount;
|
|
713
|
+
if (pos.type === "save") supplied += usd;
|
|
714
|
+
else if (pos.type === "borrow") borrowed += usd;
|
|
715
|
+
}
|
|
716
|
+
let healthFactor;
|
|
717
|
+
try {
|
|
718
|
+
const hf = await getHealthFactor$1(address, sdkOptions(client));
|
|
719
|
+
healthFactor = hf > 1e5 ? Infinity : hf;
|
|
720
|
+
} catch {
|
|
721
|
+
healthFactor = borrowed > 0 ? supplied * 0.75 / borrowed : Infinity;
|
|
722
|
+
}
|
|
723
|
+
const ltv = 0.75;
|
|
724
|
+
const maxBorrow = Math.max(0, supplied * ltv - borrowed);
|
|
725
|
+
return {
|
|
726
|
+
healthFactor,
|
|
727
|
+
supplied,
|
|
728
|
+
borrowed,
|
|
729
|
+
maxBorrow,
|
|
730
|
+
liquidationThreshold: ltv
|
|
731
|
+
};
|
|
732
|
+
}
|
|
794
733
|
async function buildSaveTx(client, address, amount, options = {}) {
|
|
795
734
|
if (!amount || amount <= 0 || !Number.isFinite(amount)) {
|
|
796
735
|
throw new T2000Error("INVALID_AMOUNT", "Save amount must be a positive number");
|
|
797
736
|
}
|
|
798
737
|
const asset = options.asset ?? "USDC";
|
|
799
738
|
const assetInfo = resolveAssetInfo(asset);
|
|
800
|
-
const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
|
|
801
|
-
const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
|
|
802
739
|
const coins = await fetchCoins(client, address, assetInfo.type);
|
|
803
740
|
if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins found`);
|
|
804
741
|
const tx = new Transaction();
|
|
@@ -807,159 +744,103 @@ async function buildSaveTx(client, address, amount, options = {}) {
|
|
|
807
744
|
if (options.collectFee) {
|
|
808
745
|
addCollectFeeToTx(tx, coinObj, "save");
|
|
809
746
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
tx.object(config.incentiveV3)
|
|
821
|
-
],
|
|
822
|
-
typeArguments: [pool.suiCoinType]
|
|
823
|
-
});
|
|
747
|
+
const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
|
|
748
|
+
try {
|
|
749
|
+
await depositCoinPTB(tx, assetInfo.type, coinObj, {
|
|
750
|
+
...sdkOptions(client),
|
|
751
|
+
amount: rawAmount
|
|
752
|
+
});
|
|
753
|
+
} catch (err) {
|
|
754
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
755
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI deposit failed: ${msg}`);
|
|
756
|
+
}
|
|
824
757
|
return tx;
|
|
825
758
|
}
|
|
826
759
|
async function buildWithdrawTx(client, address, amount, options = {}) {
|
|
827
760
|
const asset = options.asset ?? "USDC";
|
|
828
761
|
const assetInfo = resolveAssetInfo(asset);
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
const
|
|
836
|
-
const deposited = assetState ? compoundBalance(assetState.supplyBalance, pool.currentSupplyIndex, pool) : 0;
|
|
837
|
-
const effectiveAmount = Math.min(amount, Math.max(0, deposited - withdrawDustBuffer(assetInfo.decimals)));
|
|
762
|
+
const posResult = await getPositions(client, address);
|
|
763
|
+
const supply = posResult.positions.find(
|
|
764
|
+
(p) => p.type === "save" && p.asset === asset
|
|
765
|
+
);
|
|
766
|
+
const deposited = supply?.amount ?? 0;
|
|
767
|
+
const dustBuffer = 1e3 / 10 ** assetInfo.decimals;
|
|
768
|
+
const effectiveAmount = Math.min(amount, Math.max(0, deposited - dustBuffer));
|
|
838
769
|
if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on NAVI`);
|
|
839
770
|
const rawAmount = Number(stableToRaw(effectiveAmount, assetInfo.decimals));
|
|
840
771
|
if (rawAmount <= 0) {
|
|
841
|
-
throw new T2000Error("INVALID_AMOUNT",
|
|
772
|
+
throw new T2000Error("INVALID_AMOUNT", "Withdrawal amount rounds to zero \u2014 balance is dust");
|
|
842
773
|
}
|
|
843
774
|
const tx = new Transaction();
|
|
844
775
|
tx.setSender(address);
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
tx.object(SUI_SYSTEM_STATE)
|
|
858
|
-
],
|
|
859
|
-
typeArguments: [pool.suiCoinType]
|
|
860
|
-
});
|
|
861
|
-
const [coin] = tx.moveCall({
|
|
862
|
-
target: "0x2::coin::from_balance",
|
|
863
|
-
arguments: [balance],
|
|
864
|
-
typeArguments: [pool.suiCoinType]
|
|
865
|
-
});
|
|
866
|
-
tx.transferObjects([coin], address);
|
|
776
|
+
try {
|
|
777
|
+
const coinResult = await withdrawCoinPTB(tx, assetInfo.type, rawAmount, sdkOptions(client));
|
|
778
|
+
const [coin] = tx.moveCall({
|
|
779
|
+
target: "0x2::coin::from_balance",
|
|
780
|
+
arguments: [coinResult],
|
|
781
|
+
typeArguments: [assetInfo.type]
|
|
782
|
+
});
|
|
783
|
+
tx.transferObjects([coin], address);
|
|
784
|
+
} catch (err) {
|
|
785
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
786
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI withdraw failed: ${msg}`);
|
|
787
|
+
}
|
|
867
788
|
return { tx, effectiveAmount };
|
|
868
789
|
}
|
|
869
790
|
async function addWithdrawToTx(tx, client, address, amount, options = {}) {
|
|
870
791
|
const asset = options.asset ?? "USDC";
|
|
871
792
|
const assetInfo = resolveAssetInfo(asset);
|
|
872
|
-
const
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
const
|
|
879
|
-
const deposited = assetState ? compoundBalance(assetState.supplyBalance, pool.currentSupplyIndex, pool) : 0;
|
|
880
|
-
const effectiveAmount = Math.min(amount, Math.max(0, deposited - withdrawDustBuffer(assetInfo.decimals)));
|
|
793
|
+
const posResult = await getPositions(client, address);
|
|
794
|
+
const supply = posResult.positions.find(
|
|
795
|
+
(p) => p.type === "save" && p.asset === asset
|
|
796
|
+
);
|
|
797
|
+
const deposited = supply?.amount ?? 0;
|
|
798
|
+
const dustBuffer = 1e3 / 10 ** assetInfo.decimals;
|
|
799
|
+
const effectiveAmount = Math.min(amount, Math.max(0, deposited - dustBuffer));
|
|
881
800
|
if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on NAVI`);
|
|
882
801
|
const rawAmount = Number(stableToRaw(effectiveAmount, assetInfo.decimals));
|
|
883
802
|
if (rawAmount <= 0) {
|
|
884
|
-
const [
|
|
803
|
+
const [coin] = tx.moveCall({
|
|
885
804
|
target: "0x2::coin::zero",
|
|
886
|
-
typeArguments: [
|
|
805
|
+
typeArguments: [assetInfo.type]
|
|
887
806
|
});
|
|
888
|
-
return { coin
|
|
807
|
+
return { coin, effectiveAmount: 0 };
|
|
808
|
+
}
|
|
809
|
+
try {
|
|
810
|
+
const coinResult = await withdrawCoinPTB(tx, assetInfo.type, rawAmount, sdkOptions(client));
|
|
811
|
+
const [coin] = tx.moveCall({
|
|
812
|
+
target: "0x2::coin::from_balance",
|
|
813
|
+
arguments: [coinResult],
|
|
814
|
+
typeArguments: [assetInfo.type]
|
|
815
|
+
});
|
|
816
|
+
return { coin, effectiveAmount };
|
|
817
|
+
} catch (err) {
|
|
818
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
819
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI withdraw failed: ${msg}`);
|
|
889
820
|
}
|
|
890
|
-
refreshOracles(tx, config, pools);
|
|
891
|
-
const [balance] = tx.moveCall({
|
|
892
|
-
target: `${config.package}::incentive_v3::withdraw_v2`,
|
|
893
|
-
arguments: [
|
|
894
|
-
tx.object(CLOCK),
|
|
895
|
-
tx.object(config.oracle.priceOracle),
|
|
896
|
-
tx.object(config.storage),
|
|
897
|
-
tx.object(pool.contract.pool),
|
|
898
|
-
tx.pure.u8(pool.id),
|
|
899
|
-
tx.pure.u64(rawAmount),
|
|
900
|
-
tx.object(config.incentiveV2),
|
|
901
|
-
tx.object(config.incentiveV3),
|
|
902
|
-
tx.object(SUI_SYSTEM_STATE)
|
|
903
|
-
],
|
|
904
|
-
typeArguments: [pool.suiCoinType]
|
|
905
|
-
});
|
|
906
|
-
const [coin] = tx.moveCall({
|
|
907
|
-
target: "0x2::coin::from_balance",
|
|
908
|
-
arguments: [balance],
|
|
909
|
-
typeArguments: [pool.suiCoinType]
|
|
910
|
-
});
|
|
911
|
-
return { coin, effectiveAmount };
|
|
912
821
|
}
|
|
913
822
|
async function addSaveToTx(tx, _client, _address, coin, options = {}) {
|
|
914
823
|
const asset = options.asset ?? "USDC";
|
|
915
|
-
const
|
|
824
|
+
const assetInfo = resolveAssetInfo(asset);
|
|
916
825
|
if (options.collectFee) {
|
|
917
826
|
addCollectFeeToTx(tx, coin, "save");
|
|
918
827
|
}
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
target: `${config.package}::incentive_v3::entry_deposit`,
|
|
926
|
-
arguments: [
|
|
927
|
-
tx.object(CLOCK),
|
|
928
|
-
tx.object(config.storage),
|
|
929
|
-
tx.object(pool.contract.pool),
|
|
930
|
-
tx.pure.u8(pool.id),
|
|
931
|
-
coin,
|
|
932
|
-
coinValue,
|
|
933
|
-
tx.object(config.incentiveV2),
|
|
934
|
-
tx.object(config.incentiveV3)
|
|
935
|
-
],
|
|
936
|
-
typeArguments: [pool.suiCoinType]
|
|
937
|
-
});
|
|
828
|
+
try {
|
|
829
|
+
await depositCoinPTB(tx, assetInfo.type, coin, { env: "prod" });
|
|
830
|
+
} catch (err) {
|
|
831
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
832
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI deposit failed: ${msg}`);
|
|
833
|
+
}
|
|
938
834
|
}
|
|
939
835
|
async function addRepayToTx(tx, _client, _address, coin, options = {}) {
|
|
940
836
|
const asset = options.asset ?? "USDC";
|
|
941
|
-
const
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
}
|
|
948
|
-
tx.moveCall({
|
|
949
|
-
target: `${config.package}::incentive_v3::entry_repay`,
|
|
950
|
-
arguments: [
|
|
951
|
-
tx.object(CLOCK),
|
|
952
|
-
tx.object(config.oracle.priceOracle),
|
|
953
|
-
tx.object(config.storage),
|
|
954
|
-
tx.object(pool.contract.pool),
|
|
955
|
-
tx.pure.u8(pool.id),
|
|
956
|
-
coin,
|
|
957
|
-
coinValue,
|
|
958
|
-
tx.object(config.incentiveV2),
|
|
959
|
-
tx.object(config.incentiveV3)
|
|
960
|
-
],
|
|
961
|
-
typeArguments: [pool.suiCoinType]
|
|
962
|
-
});
|
|
837
|
+
const assetInfo = resolveAssetInfo(asset);
|
|
838
|
+
try {
|
|
839
|
+
await repayCoinPTB(tx, assetInfo.type, coin, { env: "prod" });
|
|
840
|
+
} catch (err) {
|
|
841
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
842
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI repay failed: ${msg}`);
|
|
843
|
+
}
|
|
963
844
|
}
|
|
964
845
|
async function buildBorrowTx(client, address, amount, options = {}) {
|
|
965
846
|
if (!amount || amount <= 0 || !Number.isFinite(amount)) {
|
|
@@ -968,38 +849,23 @@ async function buildBorrowTx(client, address, amount, options = {}) {
|
|
|
968
849
|
const asset = options.asset ?? "USDC";
|
|
969
850
|
const assetInfo = resolveAssetInfo(asset);
|
|
970
851
|
const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
|
|
971
|
-
const [config, pool, pools] = await Promise.all([
|
|
972
|
-
getConfig(),
|
|
973
|
-
getPool(asset),
|
|
974
|
-
getPools()
|
|
975
|
-
]);
|
|
976
852
|
const tx = new Transaction();
|
|
977
853
|
tx.setSender(address);
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
tx
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
typeArguments: [pool.suiCoinType]
|
|
993
|
-
});
|
|
994
|
-
const [borrowedCoin] = tx.moveCall({
|
|
995
|
-
target: "0x2::coin::from_balance",
|
|
996
|
-
arguments: [balance],
|
|
997
|
-
typeArguments: [pool.suiCoinType]
|
|
998
|
-
});
|
|
999
|
-
if (options.collectFee) {
|
|
1000
|
-
addCollectFeeToTx(tx, borrowedCoin, "borrow");
|
|
854
|
+
try {
|
|
855
|
+
const coinResult = await borrowCoinPTB(tx, assetInfo.type, rawAmount, sdkOptions(client));
|
|
856
|
+
const [borrowedCoin] = tx.moveCall({
|
|
857
|
+
target: "0x2::coin::from_balance",
|
|
858
|
+
arguments: [coinResult],
|
|
859
|
+
typeArguments: [assetInfo.type]
|
|
860
|
+
});
|
|
861
|
+
if (options.collectFee) {
|
|
862
|
+
addCollectFeeToTx(tx, borrowedCoin, "borrow");
|
|
863
|
+
}
|
|
864
|
+
tx.transferObjects([borrowedCoin], address);
|
|
865
|
+
} catch (err) {
|
|
866
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
867
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI borrow failed: ${msg}`);
|
|
1001
868
|
}
|
|
1002
|
-
tx.transferObjects([borrowedCoin], address);
|
|
1003
869
|
return tx;
|
|
1004
870
|
}
|
|
1005
871
|
async function buildRepayTx(client, address, amount, options = {}) {
|
|
@@ -1008,163 +874,23 @@ async function buildRepayTx(client, address, amount, options = {}) {
|
|
|
1008
874
|
}
|
|
1009
875
|
const asset = options.asset ?? "USDC";
|
|
1010
876
|
const assetInfo = resolveAssetInfo(asset);
|
|
1011
|
-
const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
|
|
1012
|
-
const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
|
|
1013
877
|
const coins = await fetchCoins(client, address, assetInfo.type);
|
|
1014
878
|
if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins to repay with`);
|
|
1015
879
|
const tx = new Transaction();
|
|
1016
880
|
tx.setSender(address);
|
|
1017
|
-
addOracleUpdate(tx, config, pool);
|
|
1018
881
|
const coinObj = mergeCoins(tx, coins);
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
arguments: [
|
|
1022
|
-
tx.object(CLOCK),
|
|
1023
|
-
tx.object(config.oracle.priceOracle),
|
|
1024
|
-
tx.object(config.storage),
|
|
1025
|
-
tx.object(pool.contract.pool),
|
|
1026
|
-
tx.pure.u8(pool.id),
|
|
1027
|
-
coinObj,
|
|
1028
|
-
tx.pure.u64(rawAmount),
|
|
1029
|
-
tx.object(config.incentiveV2),
|
|
1030
|
-
tx.object(config.incentiveV3)
|
|
1031
|
-
],
|
|
1032
|
-
typeArguments: [pool.suiCoinType]
|
|
1033
|
-
});
|
|
1034
|
-
return tx;
|
|
1035
|
-
}
|
|
1036
|
-
async function getHealthFactor(client, addressOrKeypair) {
|
|
1037
|
-
const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
|
|
1038
|
-
const [config, pools, states] = await Promise.all([
|
|
1039
|
-
getConfig(),
|
|
1040
|
-
getPools(),
|
|
1041
|
-
getUserState(client, address)
|
|
1042
|
-
]);
|
|
1043
|
-
let supplied = 0;
|
|
1044
|
-
let borrowed = 0;
|
|
1045
|
-
let weightedLtv = 0;
|
|
1046
|
-
let weightedLiqThreshold = 0;
|
|
1047
|
-
for (const state of states) {
|
|
1048
|
-
const pool = pools.find((p) => p.id === state.assetId);
|
|
1049
|
-
if (!pool) continue;
|
|
1050
|
-
const supplyBal = compoundBalance(state.supplyBalance, pool.currentSupplyIndex, pool);
|
|
1051
|
-
const borrowBal = compoundBalance(state.borrowBalance, pool.currentBorrowIndex, pool);
|
|
1052
|
-
const price = pool.token?.price ?? 1;
|
|
1053
|
-
supplied += supplyBal * price;
|
|
1054
|
-
borrowed += borrowBal * price;
|
|
1055
|
-
if (supplyBal > 0) {
|
|
1056
|
-
weightedLtv += supplyBal * price * parseLtv(pool.ltv);
|
|
1057
|
-
weightedLiqThreshold += supplyBal * price * parseLiqThreshold(pool.liquidationFactor.threshold);
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
const ltv = supplied > 0 ? weightedLtv / supplied : 0.75;
|
|
1061
|
-
const liqThreshold = supplied > 0 ? weightedLiqThreshold / supplied : 0.75;
|
|
1062
|
-
const maxBorrowVal = Math.max(0, supplied * ltv - borrowed);
|
|
1063
|
-
const usdcPool = pools.find((p) => matchesCoinType(p.suiCoinType || p.coinType || "", SUPPORTED_ASSETS.USDC.type));
|
|
1064
|
-
let healthFactor;
|
|
1065
|
-
if (borrowed <= 0) {
|
|
1066
|
-
healthFactor = Infinity;
|
|
1067
|
-
} else if (usdcPool) {
|
|
1068
|
-
try {
|
|
1069
|
-
const tx = new Transaction();
|
|
1070
|
-
tx.moveCall({
|
|
1071
|
-
target: `${config.uiGetter}::calculator_unchecked::dynamic_health_factor`,
|
|
1072
|
-
arguments: [
|
|
1073
|
-
tx.object(CLOCK),
|
|
1074
|
-
tx.object(config.storage),
|
|
1075
|
-
tx.object(config.oracle.priceOracle),
|
|
1076
|
-
tx.pure.u8(usdcPool.id),
|
|
1077
|
-
tx.pure.address(address),
|
|
1078
|
-
tx.pure.u8(usdcPool.id),
|
|
1079
|
-
tx.pure.u64(0),
|
|
1080
|
-
tx.pure.u64(0),
|
|
1081
|
-
tx.pure.bool(false)
|
|
1082
|
-
],
|
|
1083
|
-
typeArguments: [usdcPool.suiCoinType]
|
|
1084
|
-
});
|
|
1085
|
-
const result = await client.devInspectTransactionBlock({
|
|
1086
|
-
transactionBlock: tx,
|
|
1087
|
-
sender: address
|
|
1088
|
-
});
|
|
1089
|
-
const decoded = decodeDevInspect(result, bcs.u256());
|
|
1090
|
-
if (decoded !== void 0) {
|
|
1091
|
-
healthFactor = normalizeHealthFactor(Number(decoded));
|
|
1092
|
-
} else {
|
|
1093
|
-
healthFactor = supplied * liqThreshold / borrowed;
|
|
1094
|
-
}
|
|
1095
|
-
} catch {
|
|
1096
|
-
healthFactor = supplied * liqThreshold / borrowed;
|
|
1097
|
-
}
|
|
1098
|
-
} else {
|
|
1099
|
-
healthFactor = supplied * liqThreshold / borrowed;
|
|
1100
|
-
}
|
|
1101
|
-
return {
|
|
1102
|
-
healthFactor,
|
|
1103
|
-
supplied,
|
|
1104
|
-
borrowed,
|
|
1105
|
-
maxBorrow: maxBorrowVal,
|
|
1106
|
-
liquidationThreshold: liqThreshold
|
|
1107
|
-
};
|
|
1108
|
-
}
|
|
1109
|
-
var NAVI_SUPPORTED_ASSETS = [...STABLE_ASSETS, "SUI", "ETH", "GOLD"];
|
|
1110
|
-
async function getRates(client) {
|
|
882
|
+
const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
|
|
883
|
+
const [repayCoin] = tx.splitCoins(coinObj, [rawAmount]);
|
|
1111
884
|
try {
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
let borrowApy = poolBorrowApy(pool);
|
|
1120
|
-
if (saveApy <= 0 || saveApy > 200) saveApy = 0;
|
|
1121
|
-
if (borrowApy <= 0 || borrowApy > 200) borrowApy = 0;
|
|
1122
|
-
result[asset] = { saveApy, borrowApy };
|
|
1123
|
-
}
|
|
1124
|
-
if (!result.USDC) result.USDC = { saveApy: 4, borrowApy: 6 };
|
|
1125
|
-
return result;
|
|
1126
|
-
} catch {
|
|
1127
|
-
return { USDC: { saveApy: 4, borrowApy: 6 } };
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
async function getPositions(client, addressOrKeypair) {
|
|
1131
|
-
const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
|
|
1132
|
-
const [states, cachedPools] = await Promise.all([getUserState(client, address), getPools()]);
|
|
1133
|
-
let pools = cachedPools;
|
|
1134
|
-
const unmatchedIds = states.filter((s) => !pools.find((p) => p.id === s.assetId)).map((s) => s.assetId);
|
|
1135
|
-
if (unmatchedIds.length > 0) {
|
|
1136
|
-
pools = await getPools(true);
|
|
1137
|
-
}
|
|
1138
|
-
const positions = [];
|
|
1139
|
-
for (const state of states) {
|
|
1140
|
-
const pool = pools.find((p) => p.id === state.assetId);
|
|
1141
|
-
if (!pool) {
|
|
1142
|
-
console.warn(`[NAVI] No pool found for assetId=${state.assetId} (supply=${state.supplyBalance}, borrow=${state.borrowBalance}) \u2014 funds may be invisible`);
|
|
1143
|
-
continue;
|
|
1144
|
-
}
|
|
1145
|
-
const symbol = resolvePoolSymbol(pool);
|
|
1146
|
-
const supplyBal = compoundBalance(state.supplyBalance, pool.currentSupplyIndex, pool);
|
|
1147
|
-
const borrowBal = compoundBalance(state.borrowBalance, pool.currentBorrowIndex, pool);
|
|
1148
|
-
if (supplyBal > 1e-4) {
|
|
1149
|
-
positions.push({
|
|
1150
|
-
protocol: "navi",
|
|
1151
|
-
asset: symbol,
|
|
1152
|
-
type: "save",
|
|
1153
|
-
amount: supplyBal,
|
|
1154
|
-
apy: poolSaveApy(pool)
|
|
1155
|
-
});
|
|
1156
|
-
}
|
|
1157
|
-
if (borrowBal > 1e-4) {
|
|
1158
|
-
positions.push({
|
|
1159
|
-
protocol: "navi",
|
|
1160
|
-
asset: symbol,
|
|
1161
|
-
type: "borrow",
|
|
1162
|
-
amount: borrowBal,
|
|
1163
|
-
apy: poolBorrowApy(pool)
|
|
1164
|
-
});
|
|
1165
|
-
}
|
|
885
|
+
await repayCoinPTB(tx, assetInfo.type, repayCoin, {
|
|
886
|
+
...sdkOptions(client),
|
|
887
|
+
amount: rawAmount
|
|
888
|
+
});
|
|
889
|
+
} catch (err) {
|
|
890
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
891
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI repay failed: ${msg}`);
|
|
1166
892
|
}
|
|
1167
|
-
return
|
|
893
|
+
return tx;
|
|
1168
894
|
}
|
|
1169
895
|
async function maxWithdrawAmount(client, addressOrKeypair) {
|
|
1170
896
|
const hf = await getHealthFactor(client, addressOrKeypair);
|
|
@@ -1185,170 +911,67 @@ async function maxBorrowAmount(client, addressOrKeypair) {
|
|
|
1185
911
|
const maxAmount = Math.max(0, hf.supplied * ltv / MIN_HEALTH_FACTOR - hf.borrowed);
|
|
1186
912
|
return { maxAmount, healthFactorAfter: MIN_HEALTH_FACTOR, currentHF: hf.healthFactor };
|
|
1187
913
|
}
|
|
1188
|
-
var CERT_TYPE = "0x549e8b69270defbfafd4f94e17ec44cdbdd99820b33bda2278dea3b9a32d3f55::cert::CERT";
|
|
1189
|
-
var DEEP_TYPE = "0xdeeb7a4662eec9f2f3def03fb937a663dddaa2e215b8078a284d026b7946c270::deep::DEEP";
|
|
1190
|
-
var REWARD_FUNDS = {
|
|
1191
|
-
[CERT_TYPE]: "0x7093cf7549d5e5b35bfde2177223d1050f71655c7f676a5e610ee70eb4d93b5c",
|
|
1192
|
-
[DEEP_TYPE]: "0xc889d78b634f954979e80e622a2ae0fece824c0f6d9590044378a2563035f32f"
|
|
1193
|
-
};
|
|
1194
|
-
var REWARD_SYMBOLS = {
|
|
1195
|
-
[CERT_TYPE]: "vSUI",
|
|
1196
|
-
[DEEP_TYPE]: "DEEP"
|
|
1197
|
-
};
|
|
1198
|
-
var incentiveRulesCache = null;
|
|
1199
|
-
async function getIncentiveRules(client) {
|
|
1200
|
-
if (incentiveRulesCache && Date.now() - incentiveRulesCache.ts < CACHE_TTL) {
|
|
1201
|
-
return incentiveRulesCache.data;
|
|
1202
|
-
}
|
|
1203
|
-
const [pools, obj] = await Promise.all([
|
|
1204
|
-
getPools(),
|
|
1205
|
-
client.getObject({
|
|
1206
|
-
id: "0x62982dad27fb10bb314b3384d5de8d2ac2d72ab2dbeae5d801dbdb9efa816c80",
|
|
1207
|
-
options: { showContent: true }
|
|
1208
|
-
})
|
|
1209
|
-
]);
|
|
1210
|
-
const rewardCoinMap = /* @__PURE__ */ new Map();
|
|
1211
|
-
for (const pool of pools) {
|
|
1212
|
-
const ct = (pool.suiCoinType || pool.coinType || "").toLowerCase();
|
|
1213
|
-
const suffix = ct.split("::").slice(1).join("::");
|
|
1214
|
-
const coins = pool.supplyIncentiveApyInfo?.rewardCoin;
|
|
1215
|
-
if (Array.isArray(coins) && coins.length > 0) {
|
|
1216
|
-
rewardCoinMap.set(suffix, coins[0]);
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
const result = /* @__PURE__ */ new Map();
|
|
1220
|
-
if (obj.data?.content?.dataType !== "moveObject") {
|
|
1221
|
-
incentiveRulesCache = { data: result, ts: Date.now() };
|
|
1222
|
-
return result;
|
|
1223
|
-
}
|
|
1224
|
-
const fields = obj.data.content.fields;
|
|
1225
|
-
const poolsObj = fields.pools;
|
|
1226
|
-
const entries = poolsObj?.fields?.contents;
|
|
1227
|
-
if (!Array.isArray(entries)) {
|
|
1228
|
-
incentiveRulesCache = { data: result, ts: Date.now() };
|
|
1229
|
-
return result;
|
|
1230
|
-
}
|
|
1231
|
-
for (const entry of entries) {
|
|
1232
|
-
const ef = entry?.fields;
|
|
1233
|
-
if (!ef) continue;
|
|
1234
|
-
const key = String(ef.key ?? "");
|
|
1235
|
-
const value = ef.value;
|
|
1236
|
-
const rules = value?.fields?.rules;
|
|
1237
|
-
const ruleEntries = rules?.fields?.contents;
|
|
1238
|
-
if (!Array.isArray(ruleEntries)) continue;
|
|
1239
|
-
const ruleIds = ruleEntries.map((re) => {
|
|
1240
|
-
const rf = re?.fields;
|
|
1241
|
-
return String(rf?.key ?? "");
|
|
1242
|
-
}).filter(Boolean);
|
|
1243
|
-
const suffix = key.split("::").slice(1).join("::").toLowerCase();
|
|
1244
|
-
const full = key.toLowerCase();
|
|
1245
|
-
const rewardCoin = rewardCoinMap.get(suffix) ?? rewardCoinMap.get(full) ?? null;
|
|
1246
|
-
result.set(key, { ruleIds, rewardCoinType: rewardCoin });
|
|
1247
|
-
}
|
|
1248
|
-
incentiveRulesCache = { data: result, ts: Date.now() };
|
|
1249
|
-
return result;
|
|
1250
|
-
}
|
|
1251
|
-
function stripPrefix(coinType) {
|
|
1252
|
-
return coinType.replace(/^0x0*/, "");
|
|
1253
|
-
}
|
|
1254
914
|
async function getPendingRewards(client, address) {
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
const rewards = [];
|
|
1260
|
-
const deposited = states.filter((s) => s.supplyBalance > 0n);
|
|
1261
|
-
if (deposited.length === 0) return rewards;
|
|
1262
|
-
for (const state of deposited) {
|
|
1263
|
-
const pool = pools.find((p) => p.id === state.assetId);
|
|
1264
|
-
if (!pool) continue;
|
|
1265
|
-
const boostedApr = parseFloat(pool.supplyIncentiveApyInfo?.boostedApr ?? "0");
|
|
1266
|
-
if (boostedApr <= 0) continue;
|
|
1267
|
-
const rewardCoins = pool.supplyIncentiveApyInfo?.rewardCoin;
|
|
1268
|
-
if (!Array.isArray(rewardCoins) || rewardCoins.length === 0) continue;
|
|
1269
|
-
const rewardType = rewardCoins[0];
|
|
1270
|
-
const assetSymbol = resolvePoolSymbol(pool);
|
|
1271
|
-
rewards.push({
|
|
1272
|
-
protocol: "navi",
|
|
1273
|
-
asset: assetSymbol,
|
|
1274
|
-
coinType: rewardType,
|
|
1275
|
-
symbol: REWARD_SYMBOLS[rewardType] ?? rewardType.split("::").pop() ?? "UNKNOWN",
|
|
1276
|
-
amount: 0,
|
|
1277
|
-
estimatedValueUsd: 0
|
|
915
|
+
try {
|
|
916
|
+
const rewards = await getUserAvailableLendingRewards(address, {
|
|
917
|
+
...sdkOptions(client),
|
|
918
|
+
markets: ["main"]
|
|
1278
919
|
});
|
|
920
|
+
if (!rewards || rewards.length === 0) return [];
|
|
921
|
+
const summary = summaryLendingRewards(rewards);
|
|
922
|
+
const result = [];
|
|
923
|
+
for (const s of summary) {
|
|
924
|
+
for (const rw of s.rewards) {
|
|
925
|
+
const available = Number(rw.available);
|
|
926
|
+
if (available <= 0) continue;
|
|
927
|
+
const symbol = rw.coinType.split("::").pop() ?? "UNKNOWN";
|
|
928
|
+
result.push({
|
|
929
|
+
protocol: "navi",
|
|
930
|
+
asset: String(s.assetId),
|
|
931
|
+
coinType: rw.coinType,
|
|
932
|
+
symbol,
|
|
933
|
+
amount: available,
|
|
934
|
+
estimatedValueUsd: 0
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
return result;
|
|
939
|
+
} catch {
|
|
940
|
+
return [];
|
|
1279
941
|
}
|
|
1280
|
-
return rewards;
|
|
1281
942
|
}
|
|
1282
943
|
async function addClaimRewardsToTx(tx, client, address) {
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
getIncentiveRules(client)
|
|
1288
|
-
]);
|
|
1289
|
-
const deposited = states.filter((s) => s.supplyBalance > 0n);
|
|
1290
|
-
if (deposited.length === 0) return [];
|
|
1291
|
-
const claimGroups = /* @__PURE__ */ new Map();
|
|
1292
|
-
for (const state of deposited) {
|
|
1293
|
-
const pool = pools.find((p) => p.id === state.assetId);
|
|
1294
|
-
if (!pool) continue;
|
|
1295
|
-
const boostedApr = parseFloat(pool.supplyIncentiveApyInfo?.boostedApr ?? "0");
|
|
1296
|
-
if (boostedApr <= 0) continue;
|
|
1297
|
-
const rewardCoins = pool.supplyIncentiveApyInfo?.rewardCoin;
|
|
1298
|
-
if (!Array.isArray(rewardCoins) || rewardCoins.length === 0) continue;
|
|
1299
|
-
const rewardType = rewardCoins[0];
|
|
1300
|
-
const fundId = REWARD_FUNDS[rewardType];
|
|
1301
|
-
if (!fundId) continue;
|
|
1302
|
-
const coinType = pool.suiCoinType || pool.coinType || "";
|
|
1303
|
-
const strippedType = stripPrefix(coinType);
|
|
1304
|
-
const ruleData = Array.from(rules.entries()).find(
|
|
1305
|
-
([key]) => stripPrefix(key) === strippedType || key.split("::").slice(1).join("::").toLowerCase() === coinType.split("::").slice(1).join("::").toLowerCase()
|
|
1306
|
-
);
|
|
1307
|
-
if (!ruleData || ruleData[1].ruleIds.length === 0) continue;
|
|
1308
|
-
const group = claimGroups.get(rewardType) ?? { assets: [], ruleIds: [] };
|
|
1309
|
-
for (const ruleId of ruleData[1].ruleIds) {
|
|
1310
|
-
group.assets.push(strippedType);
|
|
1311
|
-
group.ruleIds.push(ruleId);
|
|
1312
|
-
}
|
|
1313
|
-
claimGroups.set(rewardType, group);
|
|
1314
|
-
}
|
|
1315
|
-
const claimed = [];
|
|
1316
|
-
for (const [rewardType, { assets, ruleIds }] of claimGroups) {
|
|
1317
|
-
const fundId = REWARD_FUNDS[rewardType];
|
|
1318
|
-
const [balance] = tx.moveCall({
|
|
1319
|
-
target: `${config.package}::incentive_v3::claim_reward`,
|
|
1320
|
-
typeArguments: [rewardType],
|
|
1321
|
-
arguments: [
|
|
1322
|
-
tx.object(CLOCK),
|
|
1323
|
-
tx.object(config.incentiveV3),
|
|
1324
|
-
tx.object(config.storage),
|
|
1325
|
-
tx.object(fundId),
|
|
1326
|
-
tx.pure(bcs.vector(bcs.string()).serialize(assets)),
|
|
1327
|
-
tx.pure(bcs.vector(bcs.Address).serialize(ruleIds))
|
|
1328
|
-
]
|
|
944
|
+
try {
|
|
945
|
+
const rewards = await getUserAvailableLendingRewards(address, {
|
|
946
|
+
...sdkOptions(client),
|
|
947
|
+
markets: ["main"]
|
|
1329
948
|
});
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
949
|
+
if (!rewards || rewards.length === 0) return [];
|
|
950
|
+
const claimable = rewards.filter(
|
|
951
|
+
(r) => Number(r.userClaimableReward) > 0
|
|
952
|
+
);
|
|
953
|
+
if (claimable.length === 0) return [];
|
|
954
|
+
const claimed = await claimLendingRewardsPTB(tx, claimable, {
|
|
955
|
+
env: "prod",
|
|
956
|
+
customCoinReceive: { type: "transfer", transfer: address }
|
|
1334
957
|
});
|
|
1335
|
-
|
|
1336
|
-
claimed.push({
|
|
958
|
+
return claimed.map((c) => ({
|
|
1337
959
|
protocol: "navi",
|
|
1338
|
-
asset:
|
|
1339
|
-
coinType:
|
|
1340
|
-
symbol:
|
|
960
|
+
asset: "",
|
|
961
|
+
coinType: "",
|
|
962
|
+
symbol: "REWARD",
|
|
1341
963
|
amount: 0,
|
|
1342
964
|
estimatedValueUsd: 0
|
|
1343
|
-
});
|
|
965
|
+
}));
|
|
966
|
+
} catch {
|
|
967
|
+
return [];
|
|
1344
968
|
}
|
|
1345
|
-
return claimed;
|
|
1346
969
|
}
|
|
1347
970
|
|
|
1348
971
|
// src/protocols/yieldTracker.ts
|
|
1349
972
|
async function getEarnings(client, keypair) {
|
|
1350
973
|
const hf = await getHealthFactor(client, keypair);
|
|
1351
|
-
const rates = await getRates();
|
|
974
|
+
const rates = await getRates(client);
|
|
1352
975
|
const supplied = hf.supplied;
|
|
1353
976
|
const apy = rates.USDC.saveApy / 100;
|
|
1354
977
|
const dailyRate = apy / 365;
|
|
@@ -1751,8 +1374,8 @@ var NaviAdapter = class {
|
|
|
1751
1374
|
async getPositions(address) {
|
|
1752
1375
|
const result = await getPositions(this.client, address);
|
|
1753
1376
|
return {
|
|
1754
|
-
supplies: result.positions.filter((p) => p.type === "save").map((p) => ({ asset: p.asset, amount: p.amount, apy: p.apy })),
|
|
1755
|
-
borrows: result.positions.filter((p) => p.type === "borrow").map((p) => ({ asset: p.asset, amount: p.amount, apy: p.apy }))
|
|
1377
|
+
supplies: result.positions.filter((p) => p.type === "save").map((p) => ({ asset: p.asset, amount: p.amount, amountUsd: p.amountUsd, apy: p.apy })),
|
|
1378
|
+
borrows: result.positions.filter((p) => p.type === "borrow").map((p) => ({ asset: p.asset, amount: p.amount, amountUsd: p.amountUsd, apy: p.apy }))
|
|
1756
1379
|
};
|
|
1757
1380
|
}
|
|
1758
1381
|
async getHealth(address) {
|
|
@@ -2072,16 +1695,8 @@ var CetusAdapter = class {
|
|
|
2072
1695
|
});
|
|
2073
1696
|
}
|
|
2074
1697
|
};
|
|
2075
|
-
SUPPORTED_ASSETS.USDC.type;
|
|
2076
|
-
var WAD = 1e18;
|
|
2077
|
-
var MIN_HEALTH_FACTOR2 = 1.5;
|
|
2078
|
-
var CLOCK2 = "0x6";
|
|
2079
|
-
var SUI_SYSTEM_STATE2 = "0x5";
|
|
2080
|
-
var LENDING_MARKET_ID = "0x84030d26d85eaa7035084a057f2f11f701b7e2e4eda87551becbc7c97505ece1";
|
|
2081
|
-
var LENDING_MARKET_TYPE = "0xf95b06141ed4a174f239417323bde3f209b972f5930d8521ea38a52aff3a6ddf::suilend::MAIN_POOL";
|
|
2082
1698
|
var SUILEND_PACKAGE = "0xf95b06141ed4a174f239417323bde3f209b972f5930d8521ea38a52aff3a6ddf";
|
|
2083
|
-
var
|
|
2084
|
-
var FALLBACK_PUBLISHED_AT = "0x3d4353f3bd3565329655e6b77bc2abfd31e558b86662ebd078ae453d416bc10f";
|
|
1699
|
+
var MIN_HEALTH_FACTOR2 = 1.5;
|
|
2085
1700
|
var descriptor4 = {
|
|
2086
1701
|
id: "suilend",
|
|
2087
1702
|
name: "Suilend",
|
|
@@ -2099,224 +1714,31 @@ var descriptor4 = {
|
|
|
2099
1714
|
"lending_market::repay": "repay"
|
|
2100
1715
|
}
|
|
2101
1716
|
};
|
|
2102
|
-
function interpolateRate(utilBreakpoints, aprBreakpoints, utilizationPct) {
|
|
2103
|
-
if (utilBreakpoints.length === 0) return 0;
|
|
2104
|
-
if (utilizationPct <= utilBreakpoints[0]) return aprBreakpoints[0];
|
|
2105
|
-
if (utilizationPct >= utilBreakpoints[utilBreakpoints.length - 1]) {
|
|
2106
|
-
return aprBreakpoints[aprBreakpoints.length - 1];
|
|
2107
|
-
}
|
|
2108
|
-
for (let i = 1; i < utilBreakpoints.length; i++) {
|
|
2109
|
-
if (utilizationPct <= utilBreakpoints[i]) {
|
|
2110
|
-
const t = (utilizationPct - utilBreakpoints[i - 1]) / (utilBreakpoints[i] - utilBreakpoints[i - 1]);
|
|
2111
|
-
return aprBreakpoints[i - 1] + t * (aprBreakpoints[i] - aprBreakpoints[i - 1]);
|
|
2112
|
-
}
|
|
2113
|
-
}
|
|
2114
|
-
return aprBreakpoints[aprBreakpoints.length - 1];
|
|
2115
|
-
}
|
|
2116
|
-
function computeRates(reserve) {
|
|
2117
|
-
const available = reserve.availableAmount / 10 ** reserve.mintDecimals;
|
|
2118
|
-
const borrowed = reserve.borrowedAmountWad / WAD / 10 ** reserve.mintDecimals;
|
|
2119
|
-
const totalDeposited = available + borrowed;
|
|
2120
|
-
const utilizationPct = totalDeposited > 0 ? borrowed / totalDeposited * 100 : 0;
|
|
2121
|
-
if (reserve.interestRateUtils.length === 0) return { borrowAprPct: 0, depositAprPct: 0 };
|
|
2122
|
-
const aprs = reserve.interestRateAprs.map((a) => a / 100);
|
|
2123
|
-
const borrowAprPct = interpolateRate(reserve.interestRateUtils, aprs, utilizationPct);
|
|
2124
|
-
const depositAprPct = utilizationPct / 100 * (borrowAprPct / 100) * (1 - reserve.spreadFeeBps / 1e4) * 100;
|
|
2125
|
-
return { borrowAprPct, depositAprPct };
|
|
2126
|
-
}
|
|
2127
|
-
var MS_PER_YEAR = 365.25 * 24 * 3600 * 1e3;
|
|
2128
|
-
function computeDepositRewardApr(reserve, allReserves) {
|
|
2129
|
-
if (reserve.depositTotalShares <= 0 || reserve.price <= 0) return 0;
|
|
2130
|
-
const totalDepositValue = reserve.depositTotalShares / 10 ** reserve.mintDecimals * reserve.price;
|
|
2131
|
-
if (totalDepositValue <= 0) return 0;
|
|
2132
|
-
const priceMap = /* @__PURE__ */ new Map();
|
|
2133
|
-
for (const r of allReserves) {
|
|
2134
|
-
if (r.price > 0) priceMap.set(r.coinType, { price: r.price, decimals: r.mintDecimals });
|
|
2135
|
-
}
|
|
2136
|
-
let rewardApr = 0;
|
|
2137
|
-
for (const rw of reserve.depositPoolRewards) {
|
|
2138
|
-
const info = priceMap.get(rw.coinType);
|
|
2139
|
-
if (!info || info.price <= 0) continue;
|
|
2140
|
-
const durationMs = rw.endTimeMs - rw.startTimeMs;
|
|
2141
|
-
if (durationMs <= 0) continue;
|
|
2142
|
-
const annualTokens = rw.totalRewards / 10 ** info.decimals * (MS_PER_YEAR / durationMs);
|
|
2143
|
-
rewardApr += annualTokens * info.price / totalDepositValue * 100;
|
|
2144
|
-
}
|
|
2145
|
-
return rewardApr;
|
|
2146
|
-
}
|
|
2147
|
-
function cTokenRatio(reserve) {
|
|
2148
|
-
if (reserve.ctokenSupply === 0) return 1;
|
|
2149
|
-
const totalSupply = reserve.availableAmount + reserve.borrowedAmountWad / WAD - reserve.unclaimedSpreadFeesWad / WAD;
|
|
2150
|
-
return totalSupply / reserve.ctokenSupply;
|
|
2151
|
-
}
|
|
2152
|
-
function f(obj) {
|
|
2153
|
-
if (obj && typeof obj === "object" && "fields" in obj) return obj.fields;
|
|
2154
|
-
return obj;
|
|
2155
|
-
}
|
|
2156
|
-
function str(v) {
|
|
2157
|
-
return String(v ?? "0");
|
|
2158
|
-
}
|
|
2159
|
-
function num(v) {
|
|
2160
|
-
return Number(str(v));
|
|
2161
|
-
}
|
|
2162
|
-
function parseReserve(raw, index) {
|
|
2163
|
-
const r = f(raw);
|
|
2164
|
-
const coinTypeField = f(r.coin_type);
|
|
2165
|
-
const config = f(f(r.config)?.element);
|
|
2166
|
-
const dMgr = f(r.deposits_pool_reward_manager);
|
|
2167
|
-
const rawRewards = Array.isArray(dMgr?.pool_rewards) ? dMgr.pool_rewards : [];
|
|
2168
|
-
const now = Date.now();
|
|
2169
|
-
const depositPoolRewards = rawRewards.map((rw, idx) => {
|
|
2170
|
-
if (rw === null) return null;
|
|
2171
|
-
const rwf = f(rw);
|
|
2172
|
-
return {
|
|
2173
|
-
coinType: str(f(rwf.coin_type)?.name),
|
|
2174
|
-
totalRewards: num(rwf.total_rewards),
|
|
2175
|
-
startTimeMs: num(rwf.start_time_ms),
|
|
2176
|
-
endTimeMs: num(rwf.end_time_ms),
|
|
2177
|
-
rewardIndex: idx
|
|
2178
|
-
};
|
|
2179
|
-
}).filter((rw) => rw !== null && rw.endTimeMs > now && rw.totalRewards > 0);
|
|
2180
|
-
return {
|
|
2181
|
-
coinType: str(coinTypeField?.name),
|
|
2182
|
-
mintDecimals: num(r.mint_decimals),
|
|
2183
|
-
availableAmount: num(r.available_amount),
|
|
2184
|
-
borrowedAmountWad: num(f(r.borrowed_amount)?.value),
|
|
2185
|
-
ctokenSupply: num(r.ctoken_supply),
|
|
2186
|
-
unclaimedSpreadFeesWad: num(f(r.unclaimed_spread_fees)?.value),
|
|
2187
|
-
cumulativeBorrowRateWad: num(f(r.cumulative_borrow_rate)?.value),
|
|
2188
|
-
openLtvPct: num(config?.open_ltv_pct),
|
|
2189
|
-
closeLtvPct: num(config?.close_ltv_pct),
|
|
2190
|
-
spreadFeeBps: num(config?.spread_fee_bps),
|
|
2191
|
-
interestRateUtils: Array.isArray(config?.interest_rate_utils) ? config.interest_rate_utils.map(num) : [],
|
|
2192
|
-
interestRateAprs: Array.isArray(config?.interest_rate_aprs) ? config.interest_rate_aprs.map(num) : [],
|
|
2193
|
-
arrayIndex: index,
|
|
2194
|
-
price: num(f(r.price)?.value) / WAD,
|
|
2195
|
-
depositTotalShares: num(dMgr?.total_shares),
|
|
2196
|
-
depositPoolRewards
|
|
2197
|
-
};
|
|
2198
|
-
}
|
|
2199
|
-
function parseObligation(raw) {
|
|
2200
|
-
const deposits = Array.isArray(raw.deposits) ? raw.deposits.map((d) => {
|
|
2201
|
-
const df = f(d);
|
|
2202
|
-
return {
|
|
2203
|
-
coinType: str(f(df.coin_type)?.name),
|
|
2204
|
-
ctokenAmount: num(df.deposited_ctoken_amount),
|
|
2205
|
-
reserveIdx: num(df.reserve_array_index)
|
|
2206
|
-
};
|
|
2207
|
-
}) : [];
|
|
2208
|
-
const borrows = Array.isArray(raw.borrows) ? raw.borrows.map((b) => {
|
|
2209
|
-
const bf = f(b);
|
|
2210
|
-
return {
|
|
2211
|
-
coinType: str(f(bf.coin_type)?.name),
|
|
2212
|
-
borrowedWad: num(f(bf.borrowed_amount)?.value),
|
|
2213
|
-
cumBorrowRateWad: num(f(bf.cumulative_borrow_rate)?.value),
|
|
2214
|
-
reserveIdx: num(bf.reserve_array_index)
|
|
2215
|
-
};
|
|
2216
|
-
}) : [];
|
|
2217
|
-
return { deposits, borrows };
|
|
2218
|
-
}
|
|
2219
1717
|
var SuilendAdapter = class {
|
|
2220
1718
|
id = "suilend";
|
|
2221
1719
|
name = "Suilend";
|
|
2222
|
-
version = "
|
|
1720
|
+
version = "3.0.0";
|
|
2223
1721
|
capabilities = ["save", "withdraw", "borrow", "repay"];
|
|
2224
1722
|
supportedAssets = [...STABLE_ASSETS, "SUI", "ETH", "BTC", "GOLD"];
|
|
2225
1723
|
supportsSameAssetBorrow = false;
|
|
2226
1724
|
client;
|
|
2227
|
-
|
|
2228
|
-
reserveCache = null;
|
|
1725
|
+
sdkClient = null;
|
|
2229
1726
|
async init(client) {
|
|
2230
1727
|
this.client = client;
|
|
2231
1728
|
}
|
|
2232
1729
|
initSync(client) {
|
|
2233
1730
|
this.client = client;
|
|
2234
1731
|
}
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
return this.publishedAt;
|
|
2244
|
-
}
|
|
2245
|
-
} catch {
|
|
2246
|
-
}
|
|
2247
|
-
this.publishedAt = FALLBACK_PUBLISHED_AT;
|
|
2248
|
-
return this.publishedAt;
|
|
2249
|
-
}
|
|
2250
|
-
async loadReserves(fresh = false) {
|
|
2251
|
-
if (this.reserveCache && !fresh) return this.reserveCache;
|
|
2252
|
-
const market = await this.client.getObject({
|
|
2253
|
-
id: LENDING_MARKET_ID,
|
|
2254
|
-
options: { showContent: true }
|
|
2255
|
-
});
|
|
2256
|
-
if (market.data?.content?.dataType !== "moveObject") {
|
|
2257
|
-
throw new T2000Error("PROTOCOL_UNAVAILABLE", "Failed to read Suilend lending market");
|
|
2258
|
-
}
|
|
2259
|
-
const fields = market.data.content.fields;
|
|
2260
|
-
const reservesRaw = fields.reserves;
|
|
2261
|
-
if (!Array.isArray(reservesRaw)) {
|
|
2262
|
-
throw new T2000Error("PROTOCOL_UNAVAILABLE", "Failed to parse Suilend reserves");
|
|
2263
|
-
}
|
|
2264
|
-
this.reserveCache = reservesRaw.map((r, i) => parseReserve(r, i));
|
|
2265
|
-
return this.reserveCache;
|
|
2266
|
-
}
|
|
2267
|
-
findReserve(reserves, asset) {
|
|
2268
|
-
let coinType;
|
|
2269
|
-
if (asset in SUPPORTED_ASSETS) {
|
|
2270
|
-
coinType = SUPPORTED_ASSETS[asset].type;
|
|
2271
|
-
} else if (asset.includes("::")) {
|
|
2272
|
-
coinType = asset;
|
|
2273
|
-
} else {
|
|
2274
|
-
return void 0;
|
|
2275
|
-
}
|
|
2276
|
-
try {
|
|
2277
|
-
const normalized = normalizeStructTag(coinType);
|
|
2278
|
-
return reserves.find((r) => {
|
|
2279
|
-
try {
|
|
2280
|
-
return normalizeStructTag(r.coinType) === normalized;
|
|
2281
|
-
} catch {
|
|
2282
|
-
return false;
|
|
2283
|
-
}
|
|
2284
|
-
});
|
|
2285
|
-
} catch {
|
|
2286
|
-
return void 0;
|
|
2287
|
-
}
|
|
2288
|
-
}
|
|
2289
|
-
async fetchObligationCaps(address) {
|
|
2290
|
-
const capType = `${SUILEND_PACKAGE}::lending_market::ObligationOwnerCap<${LENDING_MARKET_TYPE}>`;
|
|
2291
|
-
const caps = [];
|
|
2292
|
-
let cursor;
|
|
2293
|
-
let hasNext = true;
|
|
2294
|
-
while (hasNext) {
|
|
2295
|
-
const page = await this.client.getOwnedObjects({
|
|
2296
|
-
owner: address,
|
|
2297
|
-
filter: { StructType: capType },
|
|
2298
|
-
options: { showContent: true },
|
|
2299
|
-
cursor: cursor ?? void 0
|
|
2300
|
-
});
|
|
2301
|
-
for (const item of page.data) {
|
|
2302
|
-
if (item.data?.content?.dataType !== "moveObject") continue;
|
|
2303
|
-
const fields = item.data.content.fields;
|
|
2304
|
-
caps.push({
|
|
2305
|
-
id: item.data.objectId,
|
|
2306
|
-
obligationId: str(fields.obligation_id)
|
|
2307
|
-
});
|
|
2308
|
-
}
|
|
2309
|
-
cursor = page.nextCursor;
|
|
2310
|
-
hasNext = page.hasNextPage;
|
|
2311
|
-
}
|
|
2312
|
-
return caps;
|
|
2313
|
-
}
|
|
2314
|
-
async fetchObligation(obligationId) {
|
|
2315
|
-
const obj = await this.client.getObject({ id: obligationId, options: { showContent: true } });
|
|
2316
|
-
if (obj.data?.content?.dataType !== "moveObject") {
|
|
2317
|
-
throw new T2000Error("PROTOCOL_UNAVAILABLE", "Failed to read Suilend obligation");
|
|
1732
|
+
async getSdkClient() {
|
|
1733
|
+
if (!this.sdkClient) {
|
|
1734
|
+
this.sdkClient = await SuilendClient.initialize(
|
|
1735
|
+
LENDING_MARKET_ID,
|
|
1736
|
+
LENDING_MARKET_TYPE,
|
|
1737
|
+
this.client,
|
|
1738
|
+
false
|
|
1739
|
+
);
|
|
2318
1740
|
}
|
|
2319
|
-
return
|
|
1741
|
+
return this.sdkClient;
|
|
2320
1742
|
}
|
|
2321
1743
|
resolveSymbol(coinType) {
|
|
2322
1744
|
try {
|
|
@@ -2332,371 +1754,217 @@ var SuilendAdapter = class {
|
|
|
2332
1754
|
const parts = coinType.split("::");
|
|
2333
1755
|
return parts[parts.length - 1] || "UNKNOWN";
|
|
2334
1756
|
}
|
|
2335
|
-
// -- Adapter interface ----------------------------------------------------
|
|
2336
1757
|
async getRates(asset) {
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
1758
|
+
try {
|
|
1759
|
+
const sdk = await this.getSdkClient();
|
|
1760
|
+
const { reserveMap } = await initializeSuilend(this.client, sdk);
|
|
1761
|
+
const assetInfo = SUPPORTED_ASSETS[asset];
|
|
1762
|
+
if (!assetInfo) throw new T2000Error("ASSET_NOT_SUPPORTED", `Suilend does not support ${asset}`);
|
|
1763
|
+
const normalized = normalizeStructTag(assetInfo.type);
|
|
1764
|
+
const reserve = Object.values(reserveMap).find((r) => {
|
|
1765
|
+
try {
|
|
1766
|
+
return normalizeStructTag(r.coinType) === normalized;
|
|
1767
|
+
} catch {
|
|
1768
|
+
return false;
|
|
1769
|
+
}
|
|
1770
|
+
});
|
|
1771
|
+
if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `Suilend does not support ${asset}`);
|
|
1772
|
+
return {
|
|
1773
|
+
asset,
|
|
1774
|
+
saveApy: reserve.depositAprPercent.toNumber(),
|
|
1775
|
+
borrowApy: reserve.borrowAprPercent.toNumber()
|
|
1776
|
+
};
|
|
1777
|
+
} catch (err) {
|
|
1778
|
+
if (err instanceof T2000Error) throw err;
|
|
1779
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1780
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", `Suilend getRates failed: ${msg}`);
|
|
1781
|
+
}
|
|
2343
1782
|
}
|
|
2344
1783
|
async getPositions(address) {
|
|
2345
1784
|
const supplies = [];
|
|
2346
1785
|
const borrows = [];
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
1786
|
+
try {
|
|
1787
|
+
const sdk = await this.getSdkClient();
|
|
1788
|
+
const { reserveMap, refreshedRawReserves } = await initializeSuilend(this.client, sdk);
|
|
1789
|
+
const { obligations, obligationOwnerCaps } = await initializeObligations(
|
|
1790
|
+
this.client,
|
|
1791
|
+
sdk,
|
|
1792
|
+
refreshedRawReserves,
|
|
1793
|
+
reserveMap,
|
|
1794
|
+
address
|
|
1795
|
+
);
|
|
1796
|
+
if (obligationOwnerCaps.length === 0 || obligations.length === 0) {
|
|
1797
|
+
return { supplies, borrows };
|
|
1798
|
+
}
|
|
1799
|
+
const obligation = obligations[0];
|
|
1800
|
+
for (const dep of obligation.deposits) {
|
|
1801
|
+
const symbol = this.resolveSymbol(dep.coinType);
|
|
1802
|
+
const amount = dep.depositedAmount.toNumber();
|
|
1803
|
+
const amountUsd = dep.depositedAmountUsd.toNumber();
|
|
1804
|
+
const apy = dep.reserve.depositAprPercent.toNumber();
|
|
1805
|
+
if (amount > 1e-4) {
|
|
1806
|
+
supplies.push({ asset: symbol, amount, amountUsd, apy });
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
for (const bor of obligation.borrows) {
|
|
1810
|
+
const symbol = this.resolveSymbol(bor.coinType);
|
|
1811
|
+
const amount = bor.borrowedAmount.toNumber();
|
|
1812
|
+
const amountUsd = bor.borrowedAmountUsd.toNumber();
|
|
1813
|
+
const apy = bor.reserve.borrowAprPercent.toNumber();
|
|
1814
|
+
if (amount > 1e-4) {
|
|
1815
|
+
borrows.push({ asset: symbol, amount, amountUsd, apy });
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
} catch (err) {
|
|
1819
|
+
if (err instanceof T2000Error) throw err;
|
|
1820
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1821
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", `Suilend getPositions failed: ${msg}`);
|
|
2371
1822
|
}
|
|
2372
1823
|
return { supplies, borrows };
|
|
2373
1824
|
}
|
|
2374
1825
|
async getHealth(address) {
|
|
2375
|
-
|
|
2376
|
-
|
|
1826
|
+
try {
|
|
1827
|
+
const sdk = await this.getSdkClient();
|
|
1828
|
+
const { reserveMap, refreshedRawReserves } = await initializeSuilend(this.client, sdk);
|
|
1829
|
+
const { obligations, obligationOwnerCaps } = await initializeObligations(
|
|
1830
|
+
this.client,
|
|
1831
|
+
sdk,
|
|
1832
|
+
refreshedRawReserves,
|
|
1833
|
+
reserveMap,
|
|
1834
|
+
address
|
|
1835
|
+
);
|
|
1836
|
+
if (obligationOwnerCaps.length === 0 || obligations.length === 0) {
|
|
1837
|
+
return { healthFactor: Infinity, supplied: 0, borrowed: 0, maxBorrow: 0, liquidationThreshold: 0 };
|
|
1838
|
+
}
|
|
1839
|
+
const ob = obligations[0];
|
|
1840
|
+
const supplied = ob.depositedAmountUsd.toNumber();
|
|
1841
|
+
const borrowed = ob.borrowedAmountUsd.toNumber();
|
|
1842
|
+
const borrowLimit = ob.borrowLimitUsd.toNumber();
|
|
1843
|
+
const unhealthy = ob.unhealthyBorrowValueUsd.toNumber();
|
|
1844
|
+
const liqThreshold = supplied > 0 ? unhealthy / supplied : 0.75;
|
|
1845
|
+
const healthFactor = borrowed > 0 ? unhealthy / borrowed : Infinity;
|
|
1846
|
+
const maxBorrow = Math.max(0, borrowLimit - borrowed);
|
|
1847
|
+
return { healthFactor, supplied, borrowed, maxBorrow, liquidationThreshold: liqThreshold };
|
|
1848
|
+
} catch {
|
|
2377
1849
|
return { healthFactor: Infinity, supplied: 0, borrowed: 0, maxBorrow: 0, liquidationThreshold: 0 };
|
|
2378
1850
|
}
|
|
2379
|
-
const [reserves, obligation] = await Promise.all([
|
|
2380
|
-
this.loadReserves(),
|
|
2381
|
-
this.fetchObligation(caps[0].obligationId)
|
|
2382
|
-
]);
|
|
2383
|
-
let supplied = 0;
|
|
2384
|
-
let borrowed = 0;
|
|
2385
|
-
let weightedCloseLtv = 0;
|
|
2386
|
-
let weightedOpenLtv = 0;
|
|
2387
|
-
for (const dep of obligation.deposits) {
|
|
2388
|
-
const reserve = reserves[dep.reserveIdx];
|
|
2389
|
-
if (!reserve) continue;
|
|
2390
|
-
const ratio = cTokenRatio(reserve);
|
|
2391
|
-
const amount = dep.ctokenAmount * ratio / 10 ** reserve.mintDecimals;
|
|
2392
|
-
supplied += amount;
|
|
2393
|
-
weightedCloseLtv += amount * (reserve.closeLtvPct / 100);
|
|
2394
|
-
weightedOpenLtv += amount * (reserve.openLtvPct / 100);
|
|
2395
|
-
}
|
|
2396
|
-
for (const bor of obligation.borrows) {
|
|
2397
|
-
const reserve = reserves[bor.reserveIdx];
|
|
2398
|
-
if (!reserve) continue;
|
|
2399
|
-
const rawAmount = bor.borrowedWad / WAD / 10 ** reserve.mintDecimals;
|
|
2400
|
-
const reserveRate = reserve.cumulativeBorrowRateWad / WAD;
|
|
2401
|
-
const posRate = bor.cumBorrowRateWad / WAD;
|
|
2402
|
-
borrowed += posRate > 0 ? rawAmount * (reserveRate / posRate) : rawAmount;
|
|
2403
|
-
}
|
|
2404
|
-
const liqThreshold = supplied > 0 ? weightedCloseLtv / supplied : 0.75;
|
|
2405
|
-
const openLtv = supplied > 0 ? weightedOpenLtv / supplied : 0.7;
|
|
2406
|
-
const healthFactor = borrowed > 0 ? supplied * liqThreshold / borrowed : Infinity;
|
|
2407
|
-
const maxBorrow = Math.max(0, supplied * openLtv - borrowed);
|
|
2408
|
-
return { healthFactor, supplied, borrowed, maxBorrow, liquidationThreshold: liqThreshold };
|
|
2409
1851
|
}
|
|
2410
1852
|
async buildSaveTx(address, amount, asset, options) {
|
|
2411
1853
|
const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
|
|
2412
1854
|
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
2413
|
-
const
|
|
2414
|
-
const
|
|
2415
|
-
if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend. Try: NAVI or a different asset.`);
|
|
2416
|
-
const caps = await this.fetchObligationCaps(address);
|
|
1855
|
+
const sdk = await this.getSdkClient();
|
|
1856
|
+
const caps = await SuilendClient.getObligationOwnerCaps(address, [LENDING_MARKET_TYPE], this.client);
|
|
2417
1857
|
const tx = new Transaction();
|
|
2418
1858
|
tx.setSender(address);
|
|
2419
|
-
let capRef;
|
|
2420
1859
|
if (caps.length === 0) {
|
|
2421
|
-
const
|
|
2422
|
-
|
|
2423
|
-
typeArguments: [LENDING_MARKET_TYPE],
|
|
2424
|
-
arguments: [tx.object(LENDING_MARKET_ID)]
|
|
2425
|
-
});
|
|
2426
|
-
capRef = newCap;
|
|
2427
|
-
} else {
|
|
2428
|
-
capRef = caps[0].id;
|
|
1860
|
+
const newCap = sdk.createObligation(tx);
|
|
1861
|
+
tx.transferObjects([newCap], address);
|
|
2429
1862
|
}
|
|
2430
|
-
const
|
|
2431
|
-
if (allCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins found`);
|
|
2432
|
-
const primaryCoinId = allCoins[0].coinObjectId;
|
|
2433
|
-
if (allCoins.length > 1) {
|
|
2434
|
-
tx.mergeCoins(tx.object(primaryCoinId), allCoins.slice(1).map((c) => tx.object(c.coinObjectId)));
|
|
2435
|
-
}
|
|
2436
|
-
const rawAmount = stableToRaw(amount, assetInfo.decimals).toString();
|
|
2437
|
-
const [depositCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawAmount]);
|
|
1863
|
+
const rawValue = stableToRaw(amount, assetInfo.decimals).toString();
|
|
2438
1864
|
if (options?.collectFee) {
|
|
1865
|
+
const allCoins = await this.fetchAllCoins(address, assetInfo.type);
|
|
1866
|
+
if (allCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins found`);
|
|
1867
|
+
const primaryCoinId = allCoins[0].coinObjectId;
|
|
1868
|
+
if (allCoins.length > 1) {
|
|
1869
|
+
tx.mergeCoins(tx.object(primaryCoinId), allCoins.slice(1).map((c) => tx.object(c.coinObjectId)));
|
|
1870
|
+
}
|
|
1871
|
+
const [depositCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawValue]);
|
|
2439
1872
|
addCollectFeeToTx(tx, depositCoin, "save");
|
|
2440
1873
|
}
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
tx.object(LENDING_MARKET_ID),
|
|
2446
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
2447
|
-
tx.object(CLOCK2),
|
|
2448
|
-
depositCoin
|
|
2449
|
-
]
|
|
2450
|
-
});
|
|
2451
|
-
tx.moveCall({
|
|
2452
|
-
target: `${pkg}::lending_market::deposit_ctokens_into_obligation`,
|
|
2453
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
2454
|
-
arguments: [
|
|
2455
|
-
tx.object(LENDING_MARKET_ID),
|
|
2456
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
2457
|
-
typeof capRef === "string" ? tx.object(capRef) : capRef,
|
|
2458
|
-
tx.object(CLOCK2),
|
|
2459
|
-
ctokens
|
|
2460
|
-
]
|
|
2461
|
-
});
|
|
2462
|
-
if (typeof capRef !== "string") {
|
|
2463
|
-
tx.transferObjects([capRef], address);
|
|
1874
|
+
if (caps.length > 0) {
|
|
1875
|
+
await sdk.depositIntoObligation(address, assetInfo.type, rawValue, tx, caps[0].id);
|
|
1876
|
+
} else {
|
|
1877
|
+
await sdk.depositIntoObligation(address, assetInfo.type, rawValue, tx, tx.object(caps[0]?.id ?? ""));
|
|
2464
1878
|
}
|
|
2465
1879
|
return { tx };
|
|
2466
1880
|
}
|
|
2467
1881
|
async buildWithdrawTx(address, amount, asset) {
|
|
2468
1882
|
const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
|
|
2469
1883
|
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
2470
|
-
const
|
|
2471
|
-
const
|
|
2472
|
-
if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
|
|
2473
|
-
const caps = await this.fetchObligationCaps(address);
|
|
1884
|
+
const sdk = await this.getSdkClient();
|
|
1885
|
+
const caps = await SuilendClient.getObligationOwnerCaps(address, [LENDING_MARKET_TYPE], this.client);
|
|
2474
1886
|
if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found");
|
|
2475
|
-
const
|
|
2476
|
-
const dep =
|
|
2477
|
-
const
|
|
2478
|
-
const deposited = dep ? dep.ctokenAmount * ratio / 10 ** reserve.mintDecimals : 0;
|
|
1887
|
+
const positions = await this.getPositions(address);
|
|
1888
|
+
const dep = positions.supplies.find((s) => s.asset === assetKey);
|
|
1889
|
+
const deposited = dep?.amount ?? 0;
|
|
2479
1890
|
const effectiveAmount = Math.min(amount, deposited);
|
|
2480
1891
|
if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on Suilend`);
|
|
2481
|
-
const
|
|
2482
|
-
const isFullWithdraw = dep && effectiveAmount >= deposited * 0.999;
|
|
2483
|
-
const withdrawArg = isFullWithdraw ? U64_MAX : String(Math.floor(effectiveAmount * 10 ** reserve.mintDecimals / ratio));
|
|
1892
|
+
const rawValue = stableToRaw(effectiveAmount, assetInfo.decimals).toString();
|
|
2484
1893
|
const tx = new Transaction();
|
|
2485
1894
|
tx.setSender(address);
|
|
2486
|
-
|
|
2487
|
-
target: `${pkg}::lending_market::withdraw_ctokens`,
|
|
2488
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
2489
|
-
arguments: [
|
|
2490
|
-
tx.object(LENDING_MARKET_ID),
|
|
2491
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
2492
|
-
tx.object(caps[0].id),
|
|
2493
|
-
tx.object(CLOCK2),
|
|
2494
|
-
tx.pure("u64", BigInt(withdrawArg))
|
|
2495
|
-
]
|
|
2496
|
-
});
|
|
2497
|
-
const coin = this.redeemCtokens(tx, pkg, reserve, assetInfo.type, assetKey, ctokens);
|
|
2498
|
-
tx.transferObjects([coin], address);
|
|
1895
|
+
await sdk.withdrawAndSendToUser(address, caps[0].id, caps[0].obligationId, assetInfo.type, rawValue, tx);
|
|
2499
1896
|
return { tx, effectiveAmount };
|
|
2500
1897
|
}
|
|
2501
1898
|
async addWithdrawToTx(tx, address, amount, asset) {
|
|
2502
1899
|
const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
|
|
2503
1900
|
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
2504
|
-
const
|
|
2505
|
-
const
|
|
2506
|
-
if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
|
|
2507
|
-
const caps = await this.fetchObligationCaps(address);
|
|
1901
|
+
const sdk = await this.getSdkClient();
|
|
1902
|
+
const caps = await SuilendClient.getObligationOwnerCaps(address, [LENDING_MARKET_TYPE], this.client);
|
|
2508
1903
|
if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found");
|
|
2509
|
-
const
|
|
2510
|
-
const dep =
|
|
2511
|
-
const
|
|
2512
|
-
const deposited = dep ? dep.ctokenAmount * ratio / 10 ** reserve.mintDecimals : 0;
|
|
1904
|
+
const positions = await this.getPositions(address);
|
|
1905
|
+
const dep = positions.supplies.find((s) => s.asset === assetKey);
|
|
1906
|
+
const deposited = dep?.amount ?? 0;
|
|
2513
1907
|
const effectiveAmount = Math.min(amount, deposited);
|
|
2514
1908
|
if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on Suilend`);
|
|
2515
|
-
const
|
|
2516
|
-
const [
|
|
2517
|
-
target: `${pkg}::lending_market::withdraw_ctokens`,
|
|
2518
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
2519
|
-
arguments: [
|
|
2520
|
-
tx.object(LENDING_MARKET_ID),
|
|
2521
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
2522
|
-
tx.object(caps[0].id),
|
|
2523
|
-
tx.object(CLOCK2),
|
|
2524
|
-
tx.pure.u64(ctokenAmount)
|
|
2525
|
-
]
|
|
2526
|
-
});
|
|
2527
|
-
const coin = this.redeemCtokens(tx, pkg, reserve, assetInfo.type, assetKey, ctokens);
|
|
1909
|
+
const rawValue = stableToRaw(effectiveAmount, assetInfo.decimals).toString();
|
|
1910
|
+
const coin = await sdk.withdraw(caps[0].id, caps[0].obligationId, assetInfo.type, rawValue, tx);
|
|
2528
1911
|
return { coin, effectiveAmount };
|
|
2529
1912
|
}
|
|
2530
|
-
/**
|
|
2531
|
-
* 3-step cToken redemption matching the official Suilend SDK flow:
|
|
2532
|
-
* 1. redeem_ctokens_and_withdraw_liquidity_request — creates a LiquidityRequest
|
|
2533
|
-
* 2. unstake_sui_from_staker — (SUI only) unstakes from validators to replenish available_liquidity
|
|
2534
|
-
* 3. fulfill_liquidity_request — splits underlying tokens from the reserve
|
|
2535
|
-
*/
|
|
2536
|
-
redeemCtokens(tx, pkg, reserve, coinType, assetKey, ctokens) {
|
|
2537
|
-
const exemptionType = `${SUILEND_PACKAGE}::lending_market::RateLimiterExemption<${LENDING_MARKET_TYPE}, ${coinType}>`;
|
|
2538
|
-
const [none] = tx.moveCall({
|
|
2539
|
-
target: "0x1::option::none",
|
|
2540
|
-
typeArguments: [exemptionType]
|
|
2541
|
-
});
|
|
2542
|
-
const [liquidityRequest] = tx.moveCall({
|
|
2543
|
-
target: `${pkg}::lending_market::redeem_ctokens_and_withdraw_liquidity_request`,
|
|
2544
|
-
typeArguments: [LENDING_MARKET_TYPE, coinType],
|
|
2545
|
-
arguments: [
|
|
2546
|
-
tx.object(LENDING_MARKET_ID),
|
|
2547
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
2548
|
-
tx.object(CLOCK2),
|
|
2549
|
-
ctokens,
|
|
2550
|
-
none
|
|
2551
|
-
]
|
|
2552
|
-
});
|
|
2553
|
-
if (assetKey === "SUI") {
|
|
2554
|
-
tx.moveCall({
|
|
2555
|
-
target: `${pkg}::lending_market::unstake_sui_from_staker`,
|
|
2556
|
-
typeArguments: [LENDING_MARKET_TYPE],
|
|
2557
|
-
arguments: [
|
|
2558
|
-
tx.object(LENDING_MARKET_ID),
|
|
2559
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
2560
|
-
liquidityRequest,
|
|
2561
|
-
tx.object(SUI_SYSTEM_STATE2)
|
|
2562
|
-
]
|
|
2563
|
-
});
|
|
2564
|
-
}
|
|
2565
|
-
const [coin] = tx.moveCall({
|
|
2566
|
-
target: `${pkg}::lending_market::fulfill_liquidity_request`,
|
|
2567
|
-
typeArguments: [LENDING_MARKET_TYPE, coinType],
|
|
2568
|
-
arguments: [
|
|
2569
|
-
tx.object(LENDING_MARKET_ID),
|
|
2570
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
2571
|
-
liquidityRequest
|
|
2572
|
-
]
|
|
2573
|
-
});
|
|
2574
|
-
return coin;
|
|
2575
|
-
}
|
|
2576
1913
|
async addSaveToTx(tx, address, coin, asset, options) {
|
|
2577
1914
|
const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
|
|
2578
1915
|
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
2579
|
-
const
|
|
2580
|
-
const
|
|
2581
|
-
if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
|
|
2582
|
-
const caps = await this.fetchObligationCaps(address);
|
|
1916
|
+
const sdk = await this.getSdkClient();
|
|
1917
|
+
const caps = await SuilendClient.getObligationOwnerCaps(address, [LENDING_MARKET_TYPE], this.client);
|
|
2583
1918
|
let capRef;
|
|
2584
1919
|
if (caps.length === 0) {
|
|
2585
|
-
const
|
|
2586
|
-
target: `${pkg}::lending_market::create_obligation`,
|
|
2587
|
-
typeArguments: [LENDING_MARKET_TYPE],
|
|
2588
|
-
arguments: [tx.object(LENDING_MARKET_ID)]
|
|
2589
|
-
});
|
|
1920
|
+
const newCap = sdk.createObligation(tx);
|
|
2590
1921
|
capRef = newCap;
|
|
1922
|
+
tx.transferObjects([newCap], address);
|
|
2591
1923
|
} else {
|
|
2592
1924
|
capRef = caps[0].id;
|
|
2593
1925
|
}
|
|
2594
1926
|
if (options?.collectFee) {
|
|
2595
1927
|
addCollectFeeToTx(tx, coin, "save");
|
|
2596
1928
|
}
|
|
2597
|
-
|
|
2598
|
-
target: `${pkg}::lending_market::deposit_liquidity_and_mint_ctokens`,
|
|
2599
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
2600
|
-
arguments: [
|
|
2601
|
-
tx.object(LENDING_MARKET_ID),
|
|
2602
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
2603
|
-
tx.object(CLOCK2),
|
|
2604
|
-
coin
|
|
2605
|
-
]
|
|
2606
|
-
});
|
|
2607
|
-
tx.moveCall({
|
|
2608
|
-
target: `${pkg}::lending_market::deposit_ctokens_into_obligation`,
|
|
2609
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
2610
|
-
arguments: [
|
|
2611
|
-
tx.object(LENDING_MARKET_ID),
|
|
2612
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
2613
|
-
typeof capRef === "string" ? tx.object(capRef) : capRef,
|
|
2614
|
-
tx.object(CLOCK2),
|
|
2615
|
-
ctokens
|
|
2616
|
-
]
|
|
2617
|
-
});
|
|
2618
|
-
if (typeof capRef !== "string") {
|
|
2619
|
-
tx.transferObjects([capRef], address);
|
|
2620
|
-
}
|
|
1929
|
+
sdk.deposit(coin, assetInfo.type, capRef, tx);
|
|
2621
1930
|
}
|
|
2622
1931
|
async buildBorrowTx(address, amount, asset, options) {
|
|
2623
1932
|
const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
|
|
2624
1933
|
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
2625
|
-
const
|
|
2626
|
-
const
|
|
2627
|
-
if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend. Try: NAVI or a different asset.`);
|
|
2628
|
-
const caps = await this.fetchObligationCaps(address);
|
|
1934
|
+
const sdk = await this.getSdkClient();
|
|
1935
|
+
const caps = await SuilendClient.getObligationOwnerCaps(address, [LENDING_MARKET_TYPE], this.client);
|
|
2629
1936
|
if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found. Deposit collateral first with: t2000 save <amount>");
|
|
2630
|
-
const
|
|
1937
|
+
const rawValue = stableToRaw(amount, assetInfo.decimals).toString();
|
|
2631
1938
|
const tx = new Transaction();
|
|
2632
1939
|
tx.setSender(address);
|
|
2633
|
-
const [coin] = tx.moveCall({
|
|
2634
|
-
target: `${pkg}::lending_market::borrow`,
|
|
2635
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
2636
|
-
arguments: [
|
|
2637
|
-
tx.object(LENDING_MARKET_ID),
|
|
2638
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
2639
|
-
tx.object(caps[0].id),
|
|
2640
|
-
tx.object(CLOCK2),
|
|
2641
|
-
tx.pure.u64(rawAmount)
|
|
2642
|
-
]
|
|
2643
|
-
});
|
|
2644
1940
|
if (options?.collectFee) {
|
|
1941
|
+
const coin = await sdk.borrow(caps[0].id, caps[0].obligationId, assetInfo.type, rawValue, tx);
|
|
2645
1942
|
addCollectFeeToTx(tx, coin, "borrow");
|
|
1943
|
+
tx.transferObjects([coin], address);
|
|
1944
|
+
} else {
|
|
1945
|
+
await sdk.borrowAndSendToUser(address, caps[0].id, caps[0].obligationId, assetInfo.type, rawValue, tx);
|
|
2646
1946
|
}
|
|
2647
|
-
tx.transferObjects([coin], address);
|
|
2648
1947
|
return { tx };
|
|
2649
1948
|
}
|
|
2650
1949
|
async buildRepayTx(address, amount, asset) {
|
|
2651
1950
|
const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
|
|
2652
1951
|
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
2653
|
-
const
|
|
2654
|
-
const
|
|
2655
|
-
if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
|
|
2656
|
-
const caps = await this.fetchObligationCaps(address);
|
|
1952
|
+
const sdk = await this.getSdkClient();
|
|
1953
|
+
const caps = await SuilendClient.getObligationOwnerCaps(address, [LENDING_MARKET_TYPE], this.client);
|
|
2657
1954
|
if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend obligation found");
|
|
2658
|
-
const
|
|
2659
|
-
if (allCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins to repay with`);
|
|
2660
|
-
const rawAmount = stableToRaw(amount, assetInfo.decimals);
|
|
1955
|
+
const rawValue = stableToRaw(amount, assetInfo.decimals).toString();
|
|
2661
1956
|
const tx = new Transaction();
|
|
2662
1957
|
tx.setSender(address);
|
|
2663
|
-
|
|
2664
|
-
if (allCoins.length > 1) {
|
|
2665
|
-
tx.mergeCoins(tx.object(primaryCoinId), allCoins.slice(1).map((c) => tx.object(c.coinObjectId)));
|
|
2666
|
-
}
|
|
2667
|
-
const [repayCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawAmount.toString()]);
|
|
2668
|
-
tx.moveCall({
|
|
2669
|
-
target: `${pkg}::lending_market::repay`,
|
|
2670
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
2671
|
-
arguments: [
|
|
2672
|
-
tx.object(LENDING_MARKET_ID),
|
|
2673
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
2674
|
-
tx.object(caps[0].id),
|
|
2675
|
-
tx.object(CLOCK2),
|
|
2676
|
-
repayCoin
|
|
2677
|
-
]
|
|
2678
|
-
});
|
|
1958
|
+
await sdk.repayIntoObligation(address, caps[0].obligationId, assetInfo.type, rawValue, tx);
|
|
2679
1959
|
return { tx };
|
|
2680
1960
|
}
|
|
2681
1961
|
async addRepayToTx(tx, address, coin, asset) {
|
|
2682
1962
|
const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
|
|
2683
1963
|
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
2684
|
-
const
|
|
2685
|
-
const
|
|
2686
|
-
if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
|
|
2687
|
-
const caps = await this.fetchObligationCaps(address);
|
|
1964
|
+
const sdk = await this.getSdkClient();
|
|
1965
|
+
const caps = await SuilendClient.getObligationOwnerCaps(address, [LENDING_MARKET_TYPE], this.client);
|
|
2688
1966
|
if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend obligation found");
|
|
2689
|
-
|
|
2690
|
-
target: `${pkg}::lending_market::repay`,
|
|
2691
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
2692
|
-
arguments: [
|
|
2693
|
-
tx.object(LENDING_MARKET_ID),
|
|
2694
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
2695
|
-
tx.object(caps[0].id),
|
|
2696
|
-
tx.object(CLOCK2),
|
|
2697
|
-
coin
|
|
2698
|
-
]
|
|
2699
|
-
});
|
|
1967
|
+
sdk.repay(caps[0].obligationId, assetInfo.type, coin, tx);
|
|
2700
1968
|
}
|
|
2701
1969
|
async maxWithdraw(address, _asset) {
|
|
2702
1970
|
const health = await this.getHealth(address);
|
|
@@ -2712,8 +1980,7 @@ var SuilendAdapter = class {
|
|
|
2712
1980
|
}
|
|
2713
1981
|
async maxBorrow(address, _asset) {
|
|
2714
1982
|
const health = await this.getHealth(address);
|
|
2715
|
-
|
|
2716
|
-
return { maxAmount, healthFactorAfter: MIN_HEALTH_FACTOR2, currentHF: health.healthFactor };
|
|
1983
|
+
return { maxAmount: health.maxBorrow, healthFactorAfter: MIN_HEALTH_FACTOR2, currentHF: health.healthFactor };
|
|
2717
1984
|
}
|
|
2718
1985
|
async fetchAllCoins(owner, coinType) {
|
|
2719
1986
|
const all = [];
|
|
@@ -2727,88 +1994,79 @@ var SuilendAdapter = class {
|
|
|
2727
1994
|
}
|
|
2728
1995
|
return all;
|
|
2729
1996
|
}
|
|
2730
|
-
// -- Claim Rewards --------------------------------------------------------
|
|
2731
|
-
isClaimableReward(coinType) {
|
|
2732
|
-
const ct = coinType.toLowerCase();
|
|
2733
|
-
return ct.includes("spring_sui") || ct.includes("deep::deep") || ct.includes("cert::cert");
|
|
2734
|
-
}
|
|
2735
1997
|
async getPendingRewards(address) {
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
const
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
1998
|
+
try {
|
|
1999
|
+
const sdk = await this.getSdkClient();
|
|
2000
|
+
const { reserveMap, refreshedRawReserves } = await initializeSuilend(this.client, sdk);
|
|
2001
|
+
const { obligations, obligationOwnerCaps } = await initializeObligations(
|
|
2002
|
+
this.client,
|
|
2003
|
+
sdk,
|
|
2004
|
+
refreshedRawReserves,
|
|
2005
|
+
reserveMap,
|
|
2006
|
+
address
|
|
2007
|
+
);
|
|
2008
|
+
if (obligationOwnerCaps.length === 0 || obligations.length === 0) return [];
|
|
2009
|
+
const ob = obligations[0];
|
|
2010
|
+
const rewards = [];
|
|
2011
|
+
for (const dep of ob.deposits) {
|
|
2012
|
+
for (const rw of dep.reserve.depositsPoolRewardManager.poolRewards) {
|
|
2013
|
+
if (rw.endTimeMs <= Date.now()) continue;
|
|
2014
|
+
const symbol = rw.symbol || rw.coinType.split("::").pop() || "UNKNOWN";
|
|
2015
|
+
rewards.push({
|
|
2016
|
+
protocol: "suilend",
|
|
2017
|
+
asset: this.resolveSymbol(dep.coinType),
|
|
2018
|
+
coinType: rw.coinType,
|
|
2019
|
+
symbol,
|
|
2020
|
+
amount: 0,
|
|
2021
|
+
estimatedValueUsd: 0
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2760
2024
|
}
|
|
2025
|
+
return rewards;
|
|
2026
|
+
} catch {
|
|
2027
|
+
return [];
|
|
2761
2028
|
}
|
|
2762
|
-
return rewards;
|
|
2763
2029
|
}
|
|
2764
2030
|
async addClaimRewardsToTx(tx, address) {
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
this.
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
});
|
|
2791
|
-
const existing = claimsByToken.get(rw.coinType) ?? [];
|
|
2792
|
-
existing.push(coin);
|
|
2793
|
-
claimsByToken.set(rw.coinType, existing);
|
|
2794
|
-
}
|
|
2795
|
-
}
|
|
2796
|
-
for (const [coinType, coins] of claimsByToken) {
|
|
2797
|
-
if (coins.length > 1) {
|
|
2798
|
-
tx.mergeCoins(coins[0], coins.slice(1));
|
|
2031
|
+
try {
|
|
2032
|
+
const sdk = await this.getSdkClient();
|
|
2033
|
+
const caps = await SuilendClient.getObligationOwnerCaps(address, [LENDING_MARKET_TYPE], this.client);
|
|
2034
|
+
if (caps.length === 0) return [];
|
|
2035
|
+
const { reserveMap, refreshedRawReserves } = await initializeSuilend(this.client, sdk);
|
|
2036
|
+
const { obligations } = await initializeObligations(
|
|
2037
|
+
this.client,
|
|
2038
|
+
sdk,
|
|
2039
|
+
refreshedRawReserves,
|
|
2040
|
+
reserveMap,
|
|
2041
|
+
address
|
|
2042
|
+
);
|
|
2043
|
+
if (obligations.length === 0) return [];
|
|
2044
|
+
const ob = obligations[0];
|
|
2045
|
+
const claimRewards = [];
|
|
2046
|
+
for (const dep of ob.deposits) {
|
|
2047
|
+
for (const rw of dep.reserve.depositsPoolRewardManager.poolRewards) {
|
|
2048
|
+
if (rw.endTimeMs <= Date.now()) continue;
|
|
2049
|
+
claimRewards.push({
|
|
2050
|
+
reserveArrayIndex: dep.reserveArrayIndex,
|
|
2051
|
+
rewardIndex: BigInt(rw.rewardIndex),
|
|
2052
|
+
rewardCoinType: rw.coinType,
|
|
2053
|
+
side: Side.DEPOSIT
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2799
2056
|
}
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2057
|
+
if (claimRewards.length === 0) return [];
|
|
2058
|
+
sdk.claimRewardsAndSendToUser(address, caps[0].id, claimRewards, tx);
|
|
2059
|
+
return claimRewards.map((r) => ({
|
|
2803
2060
|
protocol: "suilend",
|
|
2804
2061
|
asset: "",
|
|
2805
|
-
coinType,
|
|
2806
|
-
symbol,
|
|
2062
|
+
coinType: r.rewardCoinType,
|
|
2063
|
+
symbol: r.rewardCoinType.split("::").pop() ?? "UNKNOWN",
|
|
2807
2064
|
amount: 0,
|
|
2808
2065
|
estimatedValueUsd: 0
|
|
2809
|
-
});
|
|
2066
|
+
}));
|
|
2067
|
+
} catch {
|
|
2068
|
+
return [];
|
|
2810
2069
|
}
|
|
2811
|
-
return claimed;
|
|
2812
2070
|
}
|
|
2813
2071
|
};
|
|
2814
2072
|
function hasLeadingZeroBits(hash, bits) {
|
|
@@ -3880,14 +3138,15 @@ To access invested funds: t2000 invest sell ${params.amount} ${asset}`,
|
|
|
3880
3138
|
try {
|
|
3881
3139
|
const positions = await this.positions();
|
|
3882
3140
|
for (const pos of positions.positions) {
|
|
3141
|
+
const usdValue = pos.amountUsd ?? pos.amount;
|
|
3883
3142
|
if (pos.type === "save") {
|
|
3884
|
-
chainTotal +=
|
|
3143
|
+
chainTotal += usdValue;
|
|
3885
3144
|
if (!earningAssets.has(pos.asset)) {
|
|
3886
|
-
bal.savings +=
|
|
3145
|
+
bal.savings += usdValue;
|
|
3887
3146
|
}
|
|
3888
3147
|
} else if (pos.type === "borrow") {
|
|
3889
|
-
chainTotal -=
|
|
3890
|
-
bal.debt +=
|
|
3148
|
+
chainTotal -= usdValue;
|
|
3149
|
+
bal.debt += usdValue;
|
|
3891
3150
|
}
|
|
3892
3151
|
}
|
|
3893
3152
|
} catch {
|
|
@@ -4098,7 +3357,12 @@ To access invested funds: t2000 invest sell ${params.amount} ${asset}`,
|
|
|
4098
3357
|
if (supplies.length === 0) {
|
|
4099
3358
|
throw new T2000Error("NO_COLLATERAL", "No savings to withdraw");
|
|
4100
3359
|
}
|
|
4101
|
-
supplies.sort((a, b) =>
|
|
3360
|
+
supplies.sort((a, b) => {
|
|
3361
|
+
const aIsUsdc = a.asset === "USDC" ? 0 : 1;
|
|
3362
|
+
const bIsUsdc = b.asset === "USDC" ? 0 : 1;
|
|
3363
|
+
if (aIsUsdc !== bIsUsdc) return aIsUsdc - bIsUsdc;
|
|
3364
|
+
return a.apy - b.apy;
|
|
3365
|
+
});
|
|
4102
3366
|
const target = supplies[0];
|
|
4103
3367
|
const adapter = this.registry.getLending(target.protocolId);
|
|
4104
3368
|
if (!adapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", `Protocol ${target.protocolId} not found`);
|
|
@@ -4144,7 +3408,8 @@ To access invested funds: t2000 invest sell ${params.amount} ${asset}`,
|
|
|
4144
3408
|
coin,
|
|
4145
3409
|
target.asset,
|
|
4146
3410
|
"USDC",
|
|
4147
|
-
effectiveAmount
|
|
3411
|
+
effectiveAmount,
|
|
3412
|
+
500
|
|
4148
3413
|
);
|
|
4149
3414
|
finalAmount = estimatedOut / 10 ** toDecimals;
|
|
4150
3415
|
tx.transferObjects([outputCoin], this._address);
|
|
@@ -4201,16 +3466,19 @@ To access invested funds: t2000 invest sell ${params.amount} ${asset}`,
|
|
|
4201
3466
|
if (entries.length === 0) {
|
|
4202
3467
|
throw new T2000Error("NO_COLLATERAL", "No savings to withdraw across any protocol");
|
|
4203
3468
|
}
|
|
4204
|
-
const
|
|
3469
|
+
const DUST_SWAP_THRESHOLD = 1;
|
|
3470
|
+
const swappableEntries = entries.filter((e) => e.asset === "USDC" || e.maxAmount >= DUST_SWAP_THRESHOLD);
|
|
3471
|
+
const dustEntries = entries.filter((e) => e.asset !== "USDC" && e.maxAmount < DUST_SWAP_THRESHOLD);
|
|
3472
|
+
const hasNonUsdc = swappableEntries.some((e) => e.asset !== "USDC");
|
|
4205
3473
|
const swapAdapter = hasNonUsdc ? this.registry.listSwap()[0] : void 0;
|
|
4206
|
-
const canPTB =
|
|
3474
|
+
const canPTB = swappableEntries.every((e) => e.adapter.addWithdrawToTx) && (!swapAdapter || swapAdapter.addSwapToTx);
|
|
4207
3475
|
let totalUsdcReceived = 0;
|
|
4208
3476
|
const gasResult = await executeWithGas(this.client, this.keypair, async () => {
|
|
4209
3477
|
if (canPTB) {
|
|
4210
3478
|
const tx = new Transaction();
|
|
4211
3479
|
tx.setSender(this._address);
|
|
4212
3480
|
const usdcCoins = [];
|
|
4213
|
-
for (const entry of
|
|
3481
|
+
for (const entry of swappableEntries) {
|
|
4214
3482
|
const { coin, effectiveAmount } = await entry.adapter.addWithdrawToTx(
|
|
4215
3483
|
tx,
|
|
4216
3484
|
this._address,
|
|
@@ -4227,7 +3495,8 @@ To access invested funds: t2000 invest sell ${params.amount} ${asset}`,
|
|
|
4227
3495
|
coin,
|
|
4228
3496
|
entry.asset,
|
|
4229
3497
|
"USDC",
|
|
4230
|
-
effectiveAmount
|
|
3498
|
+
effectiveAmount,
|
|
3499
|
+
500
|
|
4231
3500
|
);
|
|
4232
3501
|
totalUsdcReceived += estimatedOut / 10 ** toDecimals;
|
|
4233
3502
|
usdcCoins.push(outputCoin);
|
|
@@ -4236,6 +3505,18 @@ To access invested funds: t2000 invest sell ${params.amount} ${asset}`,
|
|
|
4236
3505
|
tx.transferObjects([coin], this._address);
|
|
4237
3506
|
}
|
|
4238
3507
|
}
|
|
3508
|
+
for (const dust of dustEntries) {
|
|
3509
|
+
if (dust.adapter.addWithdrawToTx) {
|
|
3510
|
+
const { coin, effectiveAmount } = await dust.adapter.addWithdrawToTx(
|
|
3511
|
+
tx,
|
|
3512
|
+
this._address,
|
|
3513
|
+
dust.maxAmount,
|
|
3514
|
+
dust.asset
|
|
3515
|
+
);
|
|
3516
|
+
totalUsdcReceived += effectiveAmount;
|
|
3517
|
+
tx.transferObjects([coin], this._address);
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
4239
3520
|
if (usdcCoins.length > 1) {
|
|
4240
3521
|
tx.mergeCoins(usdcCoins[0], usdcCoins.slice(1));
|
|
4241
3522
|
}
|
|
@@ -5692,6 +4973,7 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
|
|
|
5692
4973
|
asset: s.asset,
|
|
5693
4974
|
type: "save",
|
|
5694
4975
|
amount: s.amount,
|
|
4976
|
+
amountUsd: s.amountUsd,
|
|
5695
4977
|
apy: s.apy
|
|
5696
4978
|
})),
|
|
5697
4979
|
...p.positions.borrows.filter((b) => b.amount > 5e-3).map((b) => ({
|
|
@@ -5699,6 +4981,7 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
|
|
|
5699
4981
|
asset: b.asset,
|
|
5700
4982
|
type: "borrow",
|
|
5701
4983
|
amount: b.amount,
|
|
4984
|
+
amountUsd: b.amountUsd,
|
|
5702
4985
|
apy: b.apy
|
|
5703
4986
|
}))
|
|
5704
4987
|
]
|