@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.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
- function withdrawDustBuffer(decimals) {
596
- return 1e3 / 10 ** decimals;
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
- function resolvePoolSymbol(pool) {
660
- const coinType = pool.suiCoinType || pool.coinType || "";
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
- if (matchesCoinType(coinType, info.type)) return key;
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 pool.token?.symbol ?? "UNKNOWN";
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
- tx.moveCall({
813
- target: `${config.package}::incentive_v3::entry_deposit`,
814
- arguments: [
815
- tx.object(CLOCK),
816
- tx.object(config.storage),
817
- tx.object(pool.contract.pool),
818
- tx.pure.u8(pool.id),
819
- coinObj,
820
- tx.pure.u64(rawAmount),
821
- tx.object(config.incentiveV2),
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 [config, pool, pools, states] = await Promise.all([
832
- getConfig(),
833
- getPool(asset),
834
- getPools(),
835
- getUserState(client, address)
836
- ]);
837
- const assetState = states.find((s) => s.assetId === pool.id);
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", `Withdrawal amount rounds to zero \u2014 balance is dust`);
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
- refreshOracles(tx, config, pools);
848
- const [balance] = tx.moveCall({
849
- target: `${config.package}::incentive_v3::withdraw_v2`,
850
- arguments: [
851
- tx.object(CLOCK),
852
- tx.object(config.oracle.priceOracle),
853
- tx.object(config.storage),
854
- tx.object(pool.contract.pool),
855
- tx.pure.u8(pool.id),
856
- tx.pure.u64(rawAmount),
857
- tx.object(config.incentiveV2),
858
- tx.object(config.incentiveV3),
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 [config, pool, pools, states] = await Promise.all([
875
- getConfig(),
876
- getPool(asset),
877
- getPools(),
878
- getUserState(client, address)
879
- ]);
880
- const assetState = states.find((s) => s.assetId === pool.id);
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 [coin2] = tx.moveCall({
805
+ const [coin] = tx.moveCall({
887
806
  target: "0x2::coin::zero",
888
- typeArguments: [pool.suiCoinType]
807
+ typeArguments: [assetInfo.type]
889
808
  });
890
- return { coin: coin2, effectiveAmount: 0 };
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 [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
826
+ const assetInfo = resolveAssetInfo(asset);
918
827
  if (options.collectFee) {
919
828
  addCollectFeeToTx(tx, coin, "save");
920
829
  }
921
- const [coinValue] = tx.moveCall({
922
- target: "0x2::coin::value",
923
- typeArguments: [pool.suiCoinType],
924
- arguments: [coin]
925
- });
926
- tx.moveCall({
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 [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
944
- addOracleUpdate(tx, config, pool);
945
- const [coinValue] = tx.moveCall({
946
- target: "0x2::coin::value",
947
- typeArguments: [pool.suiCoinType],
948
- arguments: [coin]
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
- refreshOracles(tx, config, pools);
981
- const [balance] = tx.moveCall({
982
- target: `${config.package}::incentive_v3::borrow_v2`,
983
- arguments: [
984
- tx.object(CLOCK),
985
- tx.object(config.oracle.priceOracle),
986
- tx.object(config.storage),
987
- tx.object(pool.contract.pool),
988
- tx.pure.u8(pool.id),
989
- tx.pure.u64(rawAmount),
990
- tx.object(config.incentiveV2),
991
- tx.object(config.incentiveV3),
992
- tx.object(SUI_SYSTEM_STATE)
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
- tx.moveCall({
1022
- target: `${config.package}::incentive_v3::entry_repay`,
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
- const pools = await getPools();
1115
- const result = {};
1116
- for (const asset of NAVI_SUPPORTED_ASSETS) {
1117
- const targetType = SUPPORTED_ASSETS[asset].type;
1118
- const pool = pools.find((p) => matchesCoinType(p.suiCoinType || p.coinType || "", targetType));
1119
- if (!pool) continue;
1120
- let saveApy = poolSaveApy(pool);
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 { positions };
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
- const [pools, states] = await Promise.all([
1258
- getPools(),
1259
- getUserState(client, address)
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
- const [config, pools, states, rules] = await Promise.all([
1286
- getConfig(),
1287
- getPools(),
1288
- getUserState(client, address),
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
- const [coin] = tx.moveCall({
1333
- target: "0x2::coin::from_balance",
1334
- typeArguments: [rewardType],
1335
- arguments: [balance]
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
- tx.transferObjects([coin], address);
1338
- claimed.push({
960
+ return claimed.map((c) => ({
1339
961
  protocol: "navi",
1340
- asset: assets.join(", "),
1341
- coinType: rewardType,
1342
- symbol: REWARD_SYMBOLS[rewardType] ?? "UNKNOWN",
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 UPGRADE_CAP_ID = "0x3d4ef1859c3ee9fc72858f588b56a09da5466e64f8cc4e90a7b3b909fba8a7ae";
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 = "2.0.0";
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
- publishedAt = null;
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
- // -- On-chain reads -------------------------------------------------------
2238
- async resolvePackage() {
2239
- if (this.publishedAt) return this.publishedAt;
2240
- try {
2241
- const cap = await this.client.getObject({ id: UPGRADE_CAP_ID, options: { showContent: true } });
2242
- if (cap.data?.content?.dataType === "moveObject") {
2243
- const fields = cap.data.content.fields;
2244
- this.publishedAt = str(fields.package);
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 parseObligation(obj.data.content.fields);
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
- const reserves = await this.loadReserves();
2340
- const reserve = this.findReserve(reserves, asset);
2341
- if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `Suilend does not support ${asset}`);
2342
- const { borrowAprPct, depositAprPct } = computeRates(reserve);
2343
- const rewardApr = computeDepositRewardApr(reserve, reserves);
2344
- return { asset, saveApy: depositAprPct + rewardApr, borrowApy: borrowAprPct };
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
- const caps = await this.fetchObligationCaps(address);
2350
- if (caps.length === 0) return { supplies, borrows };
2351
- const [reserves, obligation] = await Promise.all([
2352
- this.loadReserves(),
2353
- this.fetchObligation(caps[0].obligationId)
2354
- ]);
2355
- for (const dep of obligation.deposits) {
2356
- const reserve = reserves[dep.reserveIdx];
2357
- if (!reserve) continue;
2358
- const ratio = cTokenRatio(reserve);
2359
- const amount = dep.ctokenAmount * ratio / 10 ** reserve.mintDecimals;
2360
- const { depositAprPct } = computeRates(reserve);
2361
- const rewardApr = computeDepositRewardApr(reserve, reserves);
2362
- supplies.push({ asset: this.resolveSymbol(dep.coinType), amount, apy: depositAprPct + rewardApr });
2363
- }
2364
- for (const bor of obligation.borrows) {
2365
- const reserve = reserves[bor.reserveIdx];
2366
- if (!reserve) continue;
2367
- const rawAmount = bor.borrowedWad / WAD / 10 ** reserve.mintDecimals;
2368
- const reserveRate = reserve.cumulativeBorrowRateWad / WAD;
2369
- const posRate = bor.cumBorrowRateWad / WAD;
2370
- const compounded = posRate > 0 ? rawAmount * (reserveRate / posRate) : rawAmount;
2371
- const { borrowAprPct } = computeRates(reserve);
2372
- borrows.push({ asset: this.resolveSymbol(bor.coinType), amount: compounded, apy: borrowAprPct });
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
- const caps = await this.fetchObligationCaps(address);
2378
- if (caps.length === 0) {
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 [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
2416
- const reserve = this.findReserve(reserves, assetKey);
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 [newCap] = tx.moveCall({
2424
- target: `${pkg}::lending_market::create_obligation`,
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 allCoins = await this.fetchAllCoins(address, assetInfo.type);
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
- const [ctokens] = tx.moveCall({
2444
- target: `${pkg}::lending_market::deposit_liquidity_and_mint_ctokens`,
2445
- typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
2446
- arguments: [
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 [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves(true)]);
2473
- const reserve = this.findReserve(reserves, assetKey);
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 obligation = await this.fetchObligation(caps[0].obligationId);
2478
- const dep = obligation.deposits.find((d) => d.reserveIdx === reserve.arrayIndex);
2479
- const ratio = cTokenRatio(reserve);
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 U64_MAX = "18446744073709551615";
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
- const [ctokens] = tx.moveCall({
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 [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves(true)]);
2507
- const reserve = this.findReserve(reserves, assetKey);
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 obligation = await this.fetchObligation(caps[0].obligationId);
2512
- const dep = obligation.deposits.find((d) => d.reserveIdx === reserve.arrayIndex);
2513
- const ratio = cTokenRatio(reserve);
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 ctokenAmount = dep && effectiveAmount >= deposited * 0.999 ? dep.ctokenAmount : Math.floor(effectiveAmount * 10 ** reserve.mintDecimals / ratio);
2518
- const [ctokens] = tx.moveCall({
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 [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
2582
- const reserve = this.findReserve(reserves, assetKey);
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 [newCap] = tx.moveCall({
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
- const [ctokens] = tx.moveCall({
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 [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
2628
- const reserve = this.findReserve(reserves, assetKey);
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 rawAmount = stableToRaw(amount, assetInfo.decimals);
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 [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
2656
- const reserve = this.findReserve(reserves, assetKey);
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 allCoins = await this.fetchAllCoins(address, assetInfo.type);
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
- const primaryCoinId = allCoins[0].coinObjectId;
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 [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
2687
- const reserve = this.findReserve(reserves, assetKey);
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
- tx.moveCall({
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
- const maxAmount = health.maxBorrow;
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
- const caps = await this.fetchObligationCaps(address);
2739
- if (caps.length === 0) return [];
2740
- const [reserves, obligation] = await Promise.all([
2741
- this.loadReserves(true),
2742
- this.fetchObligation(caps[0].obligationId)
2743
- ]);
2744
- const rewards = [];
2745
- for (const dep of obligation.deposits) {
2746
- const reserve = reserves[dep.reserveIdx];
2747
- if (!reserve) continue;
2748
- for (const rw of reserve.depositPoolRewards) {
2749
- if (!this.isClaimableReward(rw.coinType)) continue;
2750
- const durationMs = rw.endTimeMs - rw.startTimeMs;
2751
- if (durationMs <= 0) continue;
2752
- const assetSymbol = this.resolveSymbol(dep.coinType);
2753
- if (!(assetSymbol in SUPPORTED_ASSETS)) continue;
2754
- rewards.push({
2755
- protocol: "suilend",
2756
- asset: assetSymbol,
2757
- coinType: rw.coinType,
2758
- symbol: rw.coinType.includes("spring_sui") ? "sSUI" : rw.coinType.includes("deep::") ? "DEEP" : rw.coinType.split("::").pop() ?? "UNKNOWN",
2759
- amount: 0,
2760
- estimatedValueUsd: 0
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
- const caps = await this.fetchObligationCaps(address);
2768
- if (caps.length === 0) return [];
2769
- const [pkg, reserves, obligation] = await Promise.all([
2770
- this.resolvePackage(),
2771
- this.loadReserves(true),
2772
- this.fetchObligation(caps[0].obligationId)
2773
- ]);
2774
- const claimsByToken = /* @__PURE__ */ new Map();
2775
- const claimed = [];
2776
- for (const dep of obligation.deposits) {
2777
- const reserve = reserves[dep.reserveIdx];
2778
- if (!reserve) continue;
2779
- for (const rw of reserve.depositPoolRewards) {
2780
- if (!this.isClaimableReward(rw.coinType)) continue;
2781
- const [coin] = tx.moveCall({
2782
- target: `${pkg}::lending_market::claim_rewards`,
2783
- typeArguments: [LENDING_MARKET_TYPE, rw.coinType],
2784
- arguments: [
2785
- tx.object(LENDING_MARKET_ID),
2786
- tx.object(caps[0].id),
2787
- tx.object(CLOCK2),
2788
- tx.pure.u64(reserve.arrayIndex),
2789
- tx.pure.u64(rw.rewardIndex),
2790
- tx.pure.bool(true)
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
- tx.transferObjects([coins[0]], address);
2803
- const symbol = coinType.includes("spring_sui") ? "SPRING_SUI" : coinType.includes("deep::") ? "DEEP" : coinType.split("::").pop() ?? "UNKNOWN";
2804
- claimed.push({
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 += pos.amount;
3145
+ chainTotal += usdValue;
3887
3146
  if (!earningAssets.has(pos.asset)) {
3888
- bal.savings += pos.amount;
3147
+ bal.savings += usdValue;
3889
3148
  }
3890
3149
  } else if (pos.type === "borrow") {
3891
- chainTotal -= pos.amount;
3892
- bal.debt += pos.amount;
3150
+ chainTotal -= usdValue;
3151
+ bal.debt += usdValue;
3893
3152
  }
3894
3153
  }
3895
3154
  } catch {
@@ -4100,7 +3359,12 @@ To access invested funds: t2000 invest sell ${params.amount} ${asset}`,
4100
3359
  if (supplies.length === 0) {
4101
3360
  throw new T2000Error("NO_COLLATERAL", "No savings to withdraw");
4102
3361
  }
4103
- supplies.sort((a, b) => a.apy - b.apy);
3362
+ supplies.sort((a, b) => {
3363
+ const aIsUsdc = a.asset === "USDC" ? 0 : 1;
3364
+ const bIsUsdc = b.asset === "USDC" ? 0 : 1;
3365
+ if (aIsUsdc !== bIsUsdc) return aIsUsdc - bIsUsdc;
3366
+ return a.apy - b.apy;
3367
+ });
4104
3368
  const target = supplies[0];
4105
3369
  const adapter = this.registry.getLending(target.protocolId);
4106
3370
  if (!adapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", `Protocol ${target.protocolId} not found`);
@@ -4146,7 +3410,8 @@ To access invested funds: t2000 invest sell ${params.amount} ${asset}`,
4146
3410
  coin,
4147
3411
  target.asset,
4148
3412
  "USDC",
4149
- effectiveAmount
3413
+ effectiveAmount,
3414
+ 500
4150
3415
  );
4151
3416
  finalAmount = estimatedOut / 10 ** toDecimals;
4152
3417
  tx.transferObjects([outputCoin], this._address);
@@ -4203,16 +3468,19 @@ To access invested funds: t2000 invest sell ${params.amount} ${asset}`,
4203
3468
  if (entries.length === 0) {
4204
3469
  throw new T2000Error("NO_COLLATERAL", "No savings to withdraw across any protocol");
4205
3470
  }
4206
- const hasNonUsdc = entries.some((e) => e.asset !== "USDC");
3471
+ const DUST_SWAP_THRESHOLD = 1;
3472
+ const swappableEntries = entries.filter((e) => e.asset === "USDC" || e.maxAmount >= DUST_SWAP_THRESHOLD);
3473
+ const dustEntries = entries.filter((e) => e.asset !== "USDC" && e.maxAmount < DUST_SWAP_THRESHOLD);
3474
+ const hasNonUsdc = swappableEntries.some((e) => e.asset !== "USDC");
4207
3475
  const swapAdapter = hasNonUsdc ? this.registry.listSwap()[0] : void 0;
4208
- const canPTB = entries.every((e) => e.adapter.addWithdrawToTx) && (!swapAdapter || swapAdapter.addSwapToTx);
3476
+ const canPTB = swappableEntries.every((e) => e.adapter.addWithdrawToTx) && (!swapAdapter || swapAdapter.addSwapToTx);
4209
3477
  let totalUsdcReceived = 0;
4210
3478
  const gasResult = await executeWithGas(this.client, this.keypair, async () => {
4211
3479
  if (canPTB) {
4212
3480
  const tx = new transactions.Transaction();
4213
3481
  tx.setSender(this._address);
4214
3482
  const usdcCoins = [];
4215
- for (const entry of entries) {
3483
+ for (const entry of swappableEntries) {
4216
3484
  const { coin, effectiveAmount } = await entry.adapter.addWithdrawToTx(
4217
3485
  tx,
4218
3486
  this._address,
@@ -4229,7 +3497,8 @@ To access invested funds: t2000 invest sell ${params.amount} ${asset}`,
4229
3497
  coin,
4230
3498
  entry.asset,
4231
3499
  "USDC",
4232
- effectiveAmount
3500
+ effectiveAmount,
3501
+ 500
4233
3502
  );
4234
3503
  totalUsdcReceived += estimatedOut / 10 ** toDecimals;
4235
3504
  usdcCoins.push(outputCoin);
@@ -4238,6 +3507,18 @@ To access invested funds: t2000 invest sell ${params.amount} ${asset}`,
4238
3507
  tx.transferObjects([coin], this._address);
4239
3508
  }
4240
3509
  }
3510
+ for (const dust of dustEntries) {
3511
+ if (dust.adapter.addWithdrawToTx) {
3512
+ const { coin, effectiveAmount } = await dust.adapter.addWithdrawToTx(
3513
+ tx,
3514
+ this._address,
3515
+ dust.maxAmount,
3516
+ dust.asset
3517
+ );
3518
+ totalUsdcReceived += effectiveAmount;
3519
+ tx.transferObjects([coin], this._address);
3520
+ }
3521
+ }
4241
3522
  if (usdcCoins.length > 1) {
4242
3523
  tx.mergeCoins(usdcCoins[0], usdcCoins.slice(1));
4243
3524
  }
@@ -5694,6 +4975,7 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
5694
4975
  asset: s.asset,
5695
4976
  type: "save",
5696
4977
  amount: s.amount,
4978
+ amountUsd: s.amountUsd,
5697
4979
  apy: s.apy
5698
4980
  })),
5699
4981
  ...p.positions.borrows.filter((b) => b.amount > 5e-3).map((b) => ({
@@ -5701,6 +4983,7 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
5701
4983
  asset: b.asset,
5702
4984
  type: "borrow",
5703
4985
  amount: b.amount,
4986
+ amountUsd: b.amountUsd,
5704
4987
  apy: b.apy
5705
4988
  }))
5706
4989
  ]