@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/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
- function withdrawDustBuffer(decimals) {
594
- return 1e3 / 10 ** decimals;
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
- function resolvePoolSymbol(pool) {
658
- const coinType = pool.suiCoinType || pool.coinType || "";
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
- if (matchesCoinType(coinType, info.type)) return key;
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 pool.token?.symbol ?? "UNKNOWN";
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
- tx.moveCall({
811
- target: `${config.package}::incentive_v3::entry_deposit`,
812
- arguments: [
813
- tx.object(CLOCK),
814
- tx.object(config.storage),
815
- tx.object(pool.contract.pool),
816
- tx.pure.u8(pool.id),
817
- coinObj,
818
- tx.pure.u64(rawAmount),
819
- tx.object(config.incentiveV2),
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 [config, pool, pools, states] = await Promise.all([
830
- getConfig(),
831
- getPool(asset),
832
- getPools(),
833
- getUserState(client, address)
834
- ]);
835
- const assetState = states.find((s) => s.assetId === pool.id);
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", `Withdrawal amount rounds to zero \u2014 balance is dust`);
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
- refreshOracles(tx, config, pools);
846
- const [balance] = tx.moveCall({
847
- target: `${config.package}::incentive_v3::withdraw_v2`,
848
- arguments: [
849
- tx.object(CLOCK),
850
- tx.object(config.oracle.priceOracle),
851
- tx.object(config.storage),
852
- tx.object(pool.contract.pool),
853
- tx.pure.u8(pool.id),
854
- tx.pure.u64(rawAmount),
855
- tx.object(config.incentiveV2),
856
- tx.object(config.incentiveV3),
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 [config, pool, pools, states] = await Promise.all([
873
- getConfig(),
874
- getPool(asset),
875
- getPools(),
876
- getUserState(client, address)
877
- ]);
878
- const assetState = states.find((s) => s.assetId === pool.id);
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 [coin2] = tx.moveCall({
803
+ const [coin] = tx.moveCall({
885
804
  target: "0x2::coin::zero",
886
- typeArguments: [pool.suiCoinType]
805
+ typeArguments: [assetInfo.type]
887
806
  });
888
- return { coin: coin2, effectiveAmount: 0 };
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 [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
824
+ const assetInfo = resolveAssetInfo(asset);
916
825
  if (options.collectFee) {
917
826
  addCollectFeeToTx(tx, coin, "save");
918
827
  }
919
- const [coinValue] = tx.moveCall({
920
- target: "0x2::coin::value",
921
- typeArguments: [pool.suiCoinType],
922
- arguments: [coin]
923
- });
924
- tx.moveCall({
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 [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
942
- addOracleUpdate(tx, config, pool);
943
- const [coinValue] = tx.moveCall({
944
- target: "0x2::coin::value",
945
- typeArguments: [pool.suiCoinType],
946
- arguments: [coin]
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
- refreshOracles(tx, config, pools);
979
- const [balance] = tx.moveCall({
980
- target: `${config.package}::incentive_v3::borrow_v2`,
981
- arguments: [
982
- tx.object(CLOCK),
983
- tx.object(config.oracle.priceOracle),
984
- tx.object(config.storage),
985
- tx.object(pool.contract.pool),
986
- tx.pure.u8(pool.id),
987
- tx.pure.u64(rawAmount),
988
- tx.object(config.incentiveV2),
989
- tx.object(config.incentiveV3),
990
- tx.object(SUI_SYSTEM_STATE)
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
- tx.moveCall({
1020
- target: `${config.package}::incentive_v3::entry_repay`,
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
- const pools = await getPools();
1113
- const result = {};
1114
- for (const asset of NAVI_SUPPORTED_ASSETS) {
1115
- const targetType = SUPPORTED_ASSETS[asset].type;
1116
- const pool = pools.find((p) => matchesCoinType(p.suiCoinType || p.coinType || "", targetType));
1117
- if (!pool) continue;
1118
- let saveApy = poolSaveApy(pool);
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 { positions };
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
- const [pools, states] = await Promise.all([
1256
- getPools(),
1257
- getUserState(client, address)
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
- const [config, pools, states, rules] = await Promise.all([
1284
- getConfig(),
1285
- getPools(),
1286
- getUserState(client, address),
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
- const [coin] = tx.moveCall({
1331
- target: "0x2::coin::from_balance",
1332
- typeArguments: [rewardType],
1333
- arguments: [balance]
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
- tx.transferObjects([coin], address);
1336
- claimed.push({
958
+ return claimed.map((c) => ({
1337
959
  protocol: "navi",
1338
- asset: assets.join(", "),
1339
- coinType: rewardType,
1340
- symbol: REWARD_SYMBOLS[rewardType] ?? "UNKNOWN",
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 UPGRADE_CAP_ID = "0x3d4ef1859c3ee9fc72858f588b56a09da5466e64f8cc4e90a7b3b909fba8a7ae";
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 = "2.0.0";
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
- publishedAt = null;
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
- // -- On-chain reads -------------------------------------------------------
2236
- async resolvePackage() {
2237
- if (this.publishedAt) return this.publishedAt;
2238
- try {
2239
- const cap = await this.client.getObject({ id: UPGRADE_CAP_ID, options: { showContent: true } });
2240
- if (cap.data?.content?.dataType === "moveObject") {
2241
- const fields = cap.data.content.fields;
2242
- this.publishedAt = str(fields.package);
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 parseObligation(obj.data.content.fields);
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
- const reserves = await this.loadReserves();
2338
- const reserve = this.findReserve(reserves, asset);
2339
- if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `Suilend does not support ${asset}`);
2340
- const { borrowAprPct, depositAprPct } = computeRates(reserve);
2341
- const rewardApr = computeDepositRewardApr(reserve, reserves);
2342
- return { asset, saveApy: depositAprPct + rewardApr, borrowApy: borrowAprPct };
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
- const caps = await this.fetchObligationCaps(address);
2348
- if (caps.length === 0) return { supplies, borrows };
2349
- const [reserves, obligation] = await Promise.all([
2350
- this.loadReserves(),
2351
- this.fetchObligation(caps[0].obligationId)
2352
- ]);
2353
- for (const dep of obligation.deposits) {
2354
- const reserve = reserves[dep.reserveIdx];
2355
- if (!reserve) continue;
2356
- const ratio = cTokenRatio(reserve);
2357
- const amount = dep.ctokenAmount * ratio / 10 ** reserve.mintDecimals;
2358
- const { depositAprPct } = computeRates(reserve);
2359
- const rewardApr = computeDepositRewardApr(reserve, reserves);
2360
- supplies.push({ asset: this.resolveSymbol(dep.coinType), amount, apy: depositAprPct + rewardApr });
2361
- }
2362
- for (const bor of obligation.borrows) {
2363
- const reserve = reserves[bor.reserveIdx];
2364
- if (!reserve) continue;
2365
- const rawAmount = bor.borrowedWad / WAD / 10 ** reserve.mintDecimals;
2366
- const reserveRate = reserve.cumulativeBorrowRateWad / WAD;
2367
- const posRate = bor.cumBorrowRateWad / WAD;
2368
- const compounded = posRate > 0 ? rawAmount * (reserveRate / posRate) : rawAmount;
2369
- const { borrowAprPct } = computeRates(reserve);
2370
- borrows.push({ asset: this.resolveSymbol(bor.coinType), amount: compounded, apy: borrowAprPct });
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
- const caps = await this.fetchObligationCaps(address);
2376
- if (caps.length === 0) {
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 [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
2414
- const reserve = this.findReserve(reserves, assetKey);
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 [newCap] = tx.moveCall({
2422
- target: `${pkg}::lending_market::create_obligation`,
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 allCoins = await this.fetchAllCoins(address, assetInfo.type);
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
- const [ctokens] = tx.moveCall({
2442
- target: `${pkg}::lending_market::deposit_liquidity_and_mint_ctokens`,
2443
- typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
2444
- arguments: [
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 [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves(true)]);
2471
- const reserve = this.findReserve(reserves, assetKey);
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 obligation = await this.fetchObligation(caps[0].obligationId);
2476
- const dep = obligation.deposits.find((d) => d.reserveIdx === reserve.arrayIndex);
2477
- const ratio = cTokenRatio(reserve);
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 U64_MAX = "18446744073709551615";
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
- const [ctokens] = tx.moveCall({
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 [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves(true)]);
2505
- const reserve = this.findReserve(reserves, assetKey);
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 obligation = await this.fetchObligation(caps[0].obligationId);
2510
- const dep = obligation.deposits.find((d) => d.reserveIdx === reserve.arrayIndex);
2511
- const ratio = cTokenRatio(reserve);
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 ctokenAmount = dep && effectiveAmount >= deposited * 0.999 ? dep.ctokenAmount : Math.floor(effectiveAmount * 10 ** reserve.mintDecimals / ratio);
2516
- const [ctokens] = tx.moveCall({
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 [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
2580
- const reserve = this.findReserve(reserves, assetKey);
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 [newCap] = tx.moveCall({
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
- const [ctokens] = tx.moveCall({
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 [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
2626
- const reserve = this.findReserve(reserves, assetKey);
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 rawAmount = stableToRaw(amount, assetInfo.decimals);
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 [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
2654
- const reserve = this.findReserve(reserves, assetKey);
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 allCoins = await this.fetchAllCoins(address, assetInfo.type);
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
- const primaryCoinId = allCoins[0].coinObjectId;
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 [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
2685
- const reserve = this.findReserve(reserves, assetKey);
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
- tx.moveCall({
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
- const maxAmount = health.maxBorrow;
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
- const caps = await this.fetchObligationCaps(address);
2737
- if (caps.length === 0) return [];
2738
- const [reserves, obligation] = await Promise.all([
2739
- this.loadReserves(true),
2740
- this.fetchObligation(caps[0].obligationId)
2741
- ]);
2742
- const rewards = [];
2743
- for (const dep of obligation.deposits) {
2744
- const reserve = reserves[dep.reserveIdx];
2745
- if (!reserve) continue;
2746
- for (const rw of reserve.depositPoolRewards) {
2747
- if (!this.isClaimableReward(rw.coinType)) continue;
2748
- const durationMs = rw.endTimeMs - rw.startTimeMs;
2749
- if (durationMs <= 0) continue;
2750
- const assetSymbol = this.resolveSymbol(dep.coinType);
2751
- if (!(assetSymbol in SUPPORTED_ASSETS)) continue;
2752
- rewards.push({
2753
- protocol: "suilend",
2754
- asset: assetSymbol,
2755
- coinType: rw.coinType,
2756
- symbol: rw.coinType.includes("spring_sui") ? "sSUI" : rw.coinType.includes("deep::") ? "DEEP" : rw.coinType.split("::").pop() ?? "UNKNOWN",
2757
- amount: 0,
2758
- estimatedValueUsd: 0
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
- const caps = await this.fetchObligationCaps(address);
2766
- if (caps.length === 0) return [];
2767
- const [pkg, reserves, obligation] = await Promise.all([
2768
- this.resolvePackage(),
2769
- this.loadReserves(true),
2770
- this.fetchObligation(caps[0].obligationId)
2771
- ]);
2772
- const claimsByToken = /* @__PURE__ */ new Map();
2773
- const claimed = [];
2774
- for (const dep of obligation.deposits) {
2775
- const reserve = reserves[dep.reserveIdx];
2776
- if (!reserve) continue;
2777
- for (const rw of reserve.depositPoolRewards) {
2778
- if (!this.isClaimableReward(rw.coinType)) continue;
2779
- const [coin] = tx.moveCall({
2780
- target: `${pkg}::lending_market::claim_rewards`,
2781
- typeArguments: [LENDING_MARKET_TYPE, rw.coinType],
2782
- arguments: [
2783
- tx.object(LENDING_MARKET_ID),
2784
- tx.object(caps[0].id),
2785
- tx.object(CLOCK2),
2786
- tx.pure.u64(reserve.arrayIndex),
2787
- tx.pure.u64(rw.rewardIndex),
2788
- tx.pure.bool(true)
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
- tx.transferObjects([coins[0]], address);
2801
- const symbol = coinType.includes("spring_sui") ? "SPRING_SUI" : coinType.includes("deep::") ? "DEEP" : coinType.split("::").pop() ?? "UNKNOWN";
2802
- claimed.push({
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 += pos.amount;
3143
+ chainTotal += usdValue;
3885
3144
  if (!earningAssets.has(pos.asset)) {
3886
- bal.savings += pos.amount;
3145
+ bal.savings += usdValue;
3887
3146
  }
3888
3147
  } else if (pos.type === "borrow") {
3889
- chainTotal -= pos.amount;
3890
- bal.debt += pos.amount;
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) => a.apy - b.apy);
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 hasNonUsdc = entries.some((e) => e.asset !== "USDC");
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 = entries.every((e) => e.adapter.addWithdrawToTx) && (!swapAdapter || swapAdapter.addSwapToTx);
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 entries) {
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
  ]