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