@t2000/sdk 0.14.1 → 0.15.1

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.
@@ -223,6 +223,9 @@ interface InvestmentPosition {
223
223
  unrealizedPnL: number;
224
224
  unrealizedPnLPct: number;
225
225
  trades: InvestmentTrade[];
226
+ earning?: boolean;
227
+ earningProtocol?: string;
228
+ earningApy?: number;
226
229
  }
227
230
  interface PortfolioResult {
228
231
  positions: InvestmentPosition[];
@@ -246,6 +249,16 @@ interface InvestResult {
246
249
  realizedPnL?: number;
247
250
  position: InvestmentPosition;
248
251
  }
252
+ interface InvestEarnResult {
253
+ success: boolean;
254
+ tx: string;
255
+ asset: string;
256
+ amount: number;
257
+ protocol: string;
258
+ apy: number;
259
+ gasCost: number;
260
+ gasMethod: GasMethod;
261
+ }
249
262
  type PositionSide = 'long' | 'short';
250
263
  interface PerpsPosition {
251
264
  market: string;
@@ -608,4 +621,4 @@ declare function attack(client: SuiJsonRpcClient, signer: Ed25519Keypair, sentin
608
621
  /** All registered protocol descriptors — used by the indexer for event classification */
609
622
  declare const allDescriptors: ProtocolDescriptor[];
610
623
 
611
- export { requestAttack as $, type AdapterCapability as A, type BalanceResponse as B, CetusAdapter as C, type DepositInfo as D, type EarningsResult as E, type FundStatusResult as F, type GasMethod as G, type HealthFactorResult as H, type InvestmentTrade as I, type SentinelVerdict as J, SuilendAdapter as K, type LendingAdapter as L, type MaxWithdrawResult as M, NaviAdapter as N, type SwapQuote as O, type PortfolioResult as P, type TradePositionsResult as Q, type RepayResult as R, type SendResult as S, type T2000Options as T, type TradeResult as U, allDescriptors as V, type WithdrawResult as W, descriptor$2 as X, getSentinelInfo as Y, listSentinels as Z, descriptor$3 as _, type TransactionRecord as a, attack as a0, descriptor as a1, settleAttack as a2, submitPrompt as a3, descriptor$1 as a4, type SwapAdapter as b, type SaveResult as c, type BorrowResult as d, type MaxBorrowResult as e, type SwapResult as f, type InvestResult as g, type PositionsResult as h, type RatesResult as i, type LendingRates as j, type RebalanceResult as k, type SentinelAgent as l, type SentinelAttackResult as m, type AdapterPositions as n, type AdapterTxResult as o, type AssetRates as p, type GasReserve as q, type HealthInfo as r, type InvestmentPosition as s, type PerpsAdapter as t, type PerpsPosition as u, type PositionEntry as v, type PositionSide as w, type ProtocolDescriptor as x, ProtocolRegistry as y, type RebalanceStep as z };
624
+ export { descriptor$3 as $, type AdapterCapability as A, type BalanceResponse as B, CetusAdapter as C, type DepositInfo as D, type EarningsResult as E, type FundStatusResult as F, type GasMethod as G, type HealthFactorResult as H, type InvestmentTrade as I, type RebalanceStep as J, type SentinelVerdict as K, type LendingAdapter as L, type MaxWithdrawResult as M, NaviAdapter as N, SuilendAdapter as O, type PortfolioResult as P, type SwapQuote as Q, type RepayResult as R, type SendResult as S, type T2000Options as T, type TradePositionsResult as U, type TradeResult as V, type WithdrawResult as W, allDescriptors as X, descriptor$2 as Y, getSentinelInfo as Z, listSentinels as _, type TransactionRecord as a, requestAttack as a0, attack as a1, descriptor as a2, settleAttack as a3, submitPrompt as a4, descriptor$1 as a5, type SwapAdapter as b, type SaveResult as c, type BorrowResult as d, type MaxBorrowResult as e, type SwapResult as f, type InvestResult as g, type InvestEarnResult as h, type PositionsResult as i, type RatesResult as j, type LendingRates as k, type RebalanceResult as l, type SentinelAgent as m, type SentinelAttackResult as n, type AdapterPositions as o, type AdapterTxResult as p, type AssetRates as q, type GasReserve as r, type HealthInfo as s, type InvestmentPosition as t, type PerpsAdapter as u, type PerpsPosition as v, type PositionEntry as w, type PositionSide as x, type ProtocolDescriptor as y, ProtocolRegistry as z };
@@ -223,6 +223,9 @@ interface InvestmentPosition {
223
223
  unrealizedPnL: number;
224
224
  unrealizedPnLPct: number;
225
225
  trades: InvestmentTrade[];
226
+ earning?: boolean;
227
+ earningProtocol?: string;
228
+ earningApy?: number;
226
229
  }
227
230
  interface PortfolioResult {
228
231
  positions: InvestmentPosition[];
@@ -246,6 +249,16 @@ interface InvestResult {
246
249
  realizedPnL?: number;
247
250
  position: InvestmentPosition;
248
251
  }
252
+ interface InvestEarnResult {
253
+ success: boolean;
254
+ tx: string;
255
+ asset: string;
256
+ amount: number;
257
+ protocol: string;
258
+ apy: number;
259
+ gasCost: number;
260
+ gasMethod: GasMethod;
261
+ }
249
262
  type PositionSide = 'long' | 'short';
250
263
  interface PerpsPosition {
251
264
  market: string;
@@ -608,4 +621,4 @@ declare function attack(client: SuiJsonRpcClient, signer: Ed25519Keypair, sentin
608
621
  /** All registered protocol descriptors — used by the indexer for event classification */
609
622
  declare const allDescriptors: ProtocolDescriptor[];
610
623
 
611
- export { requestAttack as $, type AdapterCapability as A, type BalanceResponse as B, CetusAdapter as C, type DepositInfo as D, type EarningsResult as E, type FundStatusResult as F, type GasMethod as G, type HealthFactorResult as H, type InvestmentTrade as I, type SentinelVerdict as J, SuilendAdapter as K, type LendingAdapter as L, type MaxWithdrawResult as M, NaviAdapter as N, type SwapQuote as O, type PortfolioResult as P, type TradePositionsResult as Q, type RepayResult as R, type SendResult as S, type T2000Options as T, type TradeResult as U, allDescriptors as V, type WithdrawResult as W, descriptor$2 as X, getSentinelInfo as Y, listSentinels as Z, descriptor$3 as _, type TransactionRecord as a, attack as a0, descriptor as a1, settleAttack as a2, submitPrompt as a3, descriptor$1 as a4, type SwapAdapter as b, type SaveResult as c, type BorrowResult as d, type MaxBorrowResult as e, type SwapResult as f, type InvestResult as g, type PositionsResult as h, type RatesResult as i, type LendingRates as j, type RebalanceResult as k, type SentinelAgent as l, type SentinelAttackResult as m, type AdapterPositions as n, type AdapterTxResult as o, type AssetRates as p, type GasReserve as q, type HealthInfo as r, type InvestmentPosition as s, type PerpsAdapter as t, type PerpsPosition as u, type PositionEntry as v, type PositionSide as w, type ProtocolDescriptor as x, ProtocolRegistry as y, type RebalanceStep as z };
624
+ export { descriptor$3 as $, type AdapterCapability as A, type BalanceResponse as B, CetusAdapter as C, type DepositInfo as D, type EarningsResult as E, type FundStatusResult as F, type GasMethod as G, type HealthFactorResult as H, type InvestmentTrade as I, type RebalanceStep as J, type SentinelVerdict as K, type LendingAdapter as L, type MaxWithdrawResult as M, NaviAdapter as N, SuilendAdapter as O, type PortfolioResult as P, type SwapQuote as Q, type RepayResult as R, type SendResult as S, type T2000Options as T, type TradePositionsResult as U, type TradeResult as V, type WithdrawResult as W, allDescriptors as X, descriptor$2 as Y, getSentinelInfo as Z, listSentinels as _, type TransactionRecord as a, requestAttack as a0, attack as a1, descriptor as a2, settleAttack as a3, submitPrompt as a4, descriptor$1 as a5, type SwapAdapter as b, type SaveResult as c, type BorrowResult as d, type MaxBorrowResult as e, type SwapResult as f, type InvestResult as g, type InvestEarnResult as h, type PositionsResult as i, type RatesResult as j, type LendingRates as k, type RebalanceResult as l, type SentinelAgent as m, type SentinelAttackResult as n, type AdapterPositions as o, type AdapterTxResult as p, type AssetRates as q, type GasReserve as r, type HealthInfo as s, type InvestmentPosition as t, type PerpsAdapter as u, type PerpsPosition as v, type PositionEntry as w, type PositionSide as x, type ProtocolDescriptor as y, ProtocolRegistry as z };
package/dist/index.cjs CHANGED
@@ -60,7 +60,7 @@ var SUPPORTED_ASSETS = {
60
60
  displayName: "SUI"
61
61
  },
62
62
  BTC: {
63
- type: "0xaafb102dd0902f5055cadecd687fb5b71ca82ef0e0285d90afde828ec58ca96b::btc::BTC",
63
+ type: "0x0041f9f9344cac094454cd574e333c4fdb132d7bcc9379bcd4aab485b2a63942::wbtc::WBTC",
64
64
  decimals: 8,
65
65
  symbol: "BTC",
66
66
  displayName: "Bitcoin"
@@ -602,16 +602,23 @@ function resolvePoolSymbol(pool) {
602
602
  }
603
603
  return pool.token?.symbol ?? "UNKNOWN";
604
604
  }
605
+ function resolveAssetInfo(asset) {
606
+ if (asset in SUPPORTED_ASSETS) {
607
+ const info = SUPPORTED_ASSETS[asset];
608
+ return { type: info.type, decimals: info.decimals, displayName: info.displayName };
609
+ }
610
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `Unknown asset: ${asset}`);
611
+ }
605
612
  async function getPool(asset = "USDC") {
606
613
  const pools = await getPools();
607
- const targetType = SUPPORTED_ASSETS[asset].type;
614
+ const { type: targetType, displayName } = resolveAssetInfo(asset);
608
615
  const pool = pools.find(
609
616
  (p) => matchesCoinType(p.suiCoinType || p.coinType || "", targetType)
610
617
  );
611
618
  if (!pool) {
612
619
  throw new T2000Error(
613
620
  "ASSET_NOT_SUPPORTED",
614
- `${SUPPORTED_ASSETS[asset].displayName} pool not found on NAVI. Try: ${STABLE_ASSETS.filter((a) => a !== asset).join(", ")}`
621
+ `${displayName} pool not found on NAVI`
615
622
  );
616
623
  }
617
624
  return pool;
@@ -634,13 +641,14 @@ function addOracleUpdate(tx, config, pool) {
634
641
  ]
635
642
  });
636
643
  }
637
- function refreshStableOracles(tx, config, pools) {
638
- const stableTypes = STABLE_ASSETS.map((a) => SUPPORTED_ASSETS[a].type);
639
- const stablePools = pools.filter((p) => {
644
+ function refreshOracles(tx, config, pools, opts) {
645
+ const assetsToRefresh = NAVI_SUPPORTED_ASSETS;
646
+ const targetTypes = assetsToRefresh.map((a) => SUPPORTED_ASSETS[a].type);
647
+ const matchedPools = pools.filter((p) => {
640
648
  const ct = p.suiCoinType || p.coinType || "";
641
- return stableTypes.some((t) => matchesCoinType(ct, t));
649
+ return targetTypes.some((t) => matchesCoinType(ct, t));
642
650
  });
643
- for (const pool of stablePools) {
651
+ for (const pool of matchedPools) {
644
652
  addOracleUpdate(tx, config, pool);
645
653
  }
646
654
  }
@@ -714,7 +722,7 @@ async function buildSaveTx(client, address, amount, options = {}) {
714
722
  throw new T2000Error("INVALID_AMOUNT", "Save amount must be a positive number");
715
723
  }
716
724
  const asset = options.asset ?? "USDC";
717
- const assetInfo = SUPPORTED_ASSETS[asset];
725
+ const assetInfo = resolveAssetInfo(asset);
718
726
  const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
719
727
  const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
720
728
  const coins = await fetchCoins(client, address, assetInfo.type);
@@ -743,7 +751,7 @@ async function buildSaveTx(client, address, amount, options = {}) {
743
751
  }
744
752
  async function buildWithdrawTx(client, address, amount, options = {}) {
745
753
  const asset = options.asset ?? "USDC";
746
- const assetInfo = SUPPORTED_ASSETS[asset];
754
+ const assetInfo = resolveAssetInfo(asset);
747
755
  const [config, pool, pools, states] = await Promise.all([
748
756
  getConfig(),
749
757
  getPool(asset),
@@ -760,7 +768,7 @@ async function buildWithdrawTx(client, address, amount, options = {}) {
760
768
  }
761
769
  const tx = new transactions.Transaction();
762
770
  tx.setSender(address);
763
- refreshStableOracles(tx, config, pools);
771
+ refreshOracles(tx, config, pools);
764
772
  const [balance] = tx.moveCall({
765
773
  target: `${config.package}::incentive_v3::withdraw_v2`,
766
774
  arguments: [
@@ -786,7 +794,7 @@ async function buildWithdrawTx(client, address, amount, options = {}) {
786
794
  }
787
795
  async function addWithdrawToTx(tx, client, address, amount, options = {}) {
788
796
  const asset = options.asset ?? "USDC";
789
- const assetInfo = SUPPORTED_ASSETS[asset];
797
+ const assetInfo = resolveAssetInfo(asset);
790
798
  const [config, pool, pools, states] = await Promise.all([
791
799
  getConfig(),
792
800
  getPool(asset),
@@ -805,7 +813,7 @@ async function addWithdrawToTx(tx, client, address, amount, options = {}) {
805
813
  });
806
814
  return { coin: coin2, effectiveAmount: 0 };
807
815
  }
808
- refreshStableOracles(tx, config, pools);
816
+ refreshOracles(tx, config, pools);
809
817
  const [balance] = tx.moveCall({
810
818
  target: `${config.package}::incentive_v3::withdraw_v2`,
811
819
  arguments: [
@@ -854,7 +862,7 @@ async function addSaveToTx(tx, _client, _address, coin, options = {}) {
854
862
  typeArguments: [pool.suiCoinType]
855
863
  });
856
864
  }
857
- async function addRepayToTx(tx, client, _address, coin, options = {}) {
865
+ async function addRepayToTx(tx, _client, _address, coin, options = {}) {
858
866
  const asset = options.asset ?? "USDC";
859
867
  const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
860
868
  addOracleUpdate(tx, config, pool);
@@ -884,7 +892,7 @@ async function buildBorrowTx(client, address, amount, options = {}) {
884
892
  throw new T2000Error("INVALID_AMOUNT", "Borrow amount must be a positive number");
885
893
  }
886
894
  const asset = options.asset ?? "USDC";
887
- const assetInfo = SUPPORTED_ASSETS[asset];
895
+ const assetInfo = resolveAssetInfo(asset);
888
896
  const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
889
897
  const [config, pool, pools] = await Promise.all([
890
898
  getConfig(),
@@ -893,7 +901,7 @@ async function buildBorrowTx(client, address, amount, options = {}) {
893
901
  ]);
894
902
  const tx = new transactions.Transaction();
895
903
  tx.setSender(address);
896
- refreshStableOracles(tx, config, pools);
904
+ refreshOracles(tx, config, pools);
897
905
  const [balance] = tx.moveCall({
898
906
  target: `${config.package}::incentive_v3::borrow_v2`,
899
907
  arguments: [
@@ -925,7 +933,7 @@ async function buildRepayTx(client, address, amount, options = {}) {
925
933
  throw new T2000Error("INVALID_AMOUNT", "Repay amount must be a positive number");
926
934
  }
927
935
  const asset = options.asset ?? "USDC";
928
- const assetInfo = SUPPORTED_ASSETS[asset];
936
+ const assetInfo = resolveAssetInfo(asset);
929
937
  const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
930
938
  const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
931
939
  const coins = await fetchCoins(client, address, assetInfo.type);
@@ -1024,11 +1032,12 @@ async function getHealthFactor(client, addressOrKeypair) {
1024
1032
  liquidationThreshold: liqThreshold
1025
1033
  };
1026
1034
  }
1035
+ var NAVI_SUPPORTED_ASSETS = [...STABLE_ASSETS, "SUI", "ETH"];
1027
1036
  async function getRates(client) {
1028
1037
  try {
1029
1038
  const pools = await getPools();
1030
1039
  const result = {};
1031
- for (const asset of STABLE_ASSETS) {
1040
+ for (const asset of NAVI_SUPPORTED_ASSETS) {
1032
1041
  const targetType = SUPPORTED_ASSETS[asset].type;
1033
1042
  const pool = pools.find((p) => matchesCoinType(p.suiCoinType || p.coinType || "", targetType));
1034
1043
  if (!pool) continue;
@@ -1398,7 +1407,11 @@ var ProtocolRegistry = class {
1398
1407
  }
1399
1408
  async allRatesAcrossAssets() {
1400
1409
  const results = [];
1401
- for (const asset of STABLE_ASSETS) {
1410
+ const allAssets = [...STABLE_ASSETS, ...Object.keys(INVESTMENT_ASSETS)];
1411
+ const seen = /* @__PURE__ */ new Set();
1412
+ for (const asset of allAssets) {
1413
+ if (seen.has(asset)) continue;
1414
+ seen.add(asset);
1402
1415
  for (const adapter of this.lending.values()) {
1403
1416
  if (!adapter.supportedAssets.includes(asset)) continue;
1404
1417
  try {
@@ -1473,7 +1486,7 @@ var NaviAdapter = class {
1473
1486
  name = "NAVI Protocol";
1474
1487
  version = "1.0.0";
1475
1488
  capabilities = ["save", "withdraw", "borrow", "repay"];
1476
- supportedAssets = [...STABLE_ASSETS];
1489
+ supportedAssets = [...STABLE_ASSETS, "SUI", "ETH"];
1477
1490
  supportsSameAssetBorrow = true;
1478
1491
  client;
1479
1492
  async init(client) {
@@ -1500,23 +1513,23 @@ var NaviAdapter = class {
1500
1513
  return getHealthFactor(this.client, address);
1501
1514
  }
1502
1515
  async buildSaveTx(address, amount, asset, options) {
1503
- const stableAsset = normalizeAsset(asset);
1504
- const tx = await buildSaveTx(this.client, address, amount, { ...options, asset: stableAsset });
1516
+ const normalized = normalizeAsset(asset);
1517
+ const tx = await buildSaveTx(this.client, address, amount, { ...options, asset: normalized });
1505
1518
  return { tx };
1506
1519
  }
1507
1520
  async buildWithdrawTx(address, amount, asset) {
1508
- const stableAsset = normalizeAsset(asset);
1509
- const result = await buildWithdrawTx(this.client, address, amount, { asset: stableAsset });
1521
+ const normalized = normalizeAsset(asset);
1522
+ const result = await buildWithdrawTx(this.client, address, amount, { asset: normalized });
1510
1523
  return { tx: result.tx, effectiveAmount: result.effectiveAmount };
1511
1524
  }
1512
1525
  async buildBorrowTx(address, amount, asset, options) {
1513
- const stableAsset = normalizeAsset(asset);
1514
- const tx = await buildBorrowTx(this.client, address, amount, { ...options, asset: stableAsset });
1526
+ const normalized = normalizeAsset(asset);
1527
+ const tx = await buildBorrowTx(this.client, address, amount, { ...options, asset: normalized });
1515
1528
  return { tx };
1516
1529
  }
1517
1530
  async buildRepayTx(address, amount, asset) {
1518
- const stableAsset = normalizeAsset(asset);
1519
- const tx = await buildRepayTx(this.client, address, amount, { asset: stableAsset });
1531
+ const normalized = normalizeAsset(asset);
1532
+ const tx = await buildRepayTx(this.client, address, amount, { asset: normalized });
1520
1533
  return { tx };
1521
1534
  }
1522
1535
  async maxWithdraw(address, _asset) {
@@ -1526,16 +1539,16 @@ var NaviAdapter = class {
1526
1539
  return maxBorrowAmount(this.client, address);
1527
1540
  }
1528
1541
  async addWithdrawToTx(tx, address, amount, asset) {
1529
- const stableAsset = normalizeAsset(asset);
1530
- return addWithdrawToTx(tx, this.client, address, amount, { asset: stableAsset });
1542
+ const normalized = normalizeAsset(asset);
1543
+ return addWithdrawToTx(tx, this.client, address, amount, { asset: normalized });
1531
1544
  }
1532
1545
  async addSaveToTx(tx, address, coin, asset, options) {
1533
- const stableAsset = normalizeAsset(asset);
1534
- return addSaveToTx(tx, this.client, address, coin, { ...options, asset: stableAsset });
1546
+ const normalized = normalizeAsset(asset);
1547
+ return addSaveToTx(tx, this.client, address, coin, { ...options, asset: normalized });
1535
1548
  }
1536
1549
  async addRepayToTx(tx, address, coin, asset) {
1537
- const stableAsset = normalizeAsset(asset);
1538
- return addRepayToTx(tx, this.client, address, coin, { asset: stableAsset });
1550
+ const normalized = normalizeAsset(asset);
1551
+ return addRepayToTx(tx, this.client, address, coin, { asset: normalized });
1539
1552
  }
1540
1553
  };
1541
1554
  var DEFAULT_SLIPPAGE_BPS = 300;
@@ -1847,7 +1860,7 @@ var SuilendAdapter = class {
1847
1860
  name = "Suilend";
1848
1861
  version = "2.0.0";
1849
1862
  capabilities = ["save", "withdraw", "borrow", "repay"];
1850
- supportedAssets = [...STABLE_ASSETS];
1863
+ supportedAssets = [...STABLE_ASSETS, "SUI", "ETH", "BTC"];
1851
1864
  supportsSameAssetBorrow = false;
1852
1865
  client;
1853
1866
  publishedAt = null;
@@ -2869,6 +2882,38 @@ var PortfolioManager = class {
2869
2882
  this.load();
2870
2883
  return Object.entries(this.data.positions).filter(([, pos]) => pos.totalAmount > 0).map(([asset, pos]) => ({ asset, ...pos }));
2871
2884
  }
2885
+ recordEarn(asset, protocol, apy) {
2886
+ this.load();
2887
+ const pos = this.data.positions[asset];
2888
+ if (!pos || pos.totalAmount <= 0) {
2889
+ throw new T2000Error("INSUFFICIENT_INVESTMENT", `No ${asset} position to earn on`);
2890
+ }
2891
+ if (pos.earning) {
2892
+ throw new T2000Error("INVEST_ALREADY_EARNING", `${asset} is already earning via ${pos.earningProtocol}`);
2893
+ }
2894
+ pos.earning = true;
2895
+ pos.earningProtocol = protocol;
2896
+ pos.earningApy = apy;
2897
+ this.data.positions[asset] = pos;
2898
+ this.save();
2899
+ }
2900
+ recordUnearn(asset) {
2901
+ this.load();
2902
+ const pos = this.data.positions[asset];
2903
+ if (!pos || !pos.earning) {
2904
+ throw new T2000Error("INVEST_NOT_EARNING", `${asset} is not currently earning`);
2905
+ }
2906
+ pos.earning = false;
2907
+ pos.earningProtocol = void 0;
2908
+ pos.earningApy = void 0;
2909
+ this.data.positions[asset] = pos;
2910
+ this.save();
2911
+ }
2912
+ isEarning(asset) {
2913
+ this.load();
2914
+ const pos = this.data.positions[asset];
2915
+ return pos?.earning === true;
2916
+ }
2872
2917
  getRealizedPnL() {
2873
2918
  this.load();
2874
2919
  return this.data.realizedPnL;
@@ -3428,16 +3473,49 @@ To access invested funds: t2000 invest sell ${params.amount} ${asset}`,
3428
3473
  return adapter.maxWithdraw(this._address, "USDC");
3429
3474
  }
3430
3475
  // -- Borrowing --
3476
+ async adjustMaxBorrowForInvestments(adapter, maxResult) {
3477
+ const earningPositions = this.portfolio.getPositions().filter((p) => p.earning);
3478
+ if (earningPositions.length === 0) return maxResult;
3479
+ let investmentCollateralUsd = 0;
3480
+ const swapAdapter = this.registry.listSwap()[0];
3481
+ for (const pos of earningPositions) {
3482
+ if (pos.earningProtocol !== adapter.id) continue;
3483
+ try {
3484
+ let price = 0;
3485
+ if (pos.asset === "SUI" && swapAdapter) {
3486
+ price = await swapAdapter.getPoolPrice();
3487
+ } else if (swapAdapter) {
3488
+ const quote = await swapAdapter.getQuote("USDC", pos.asset, 1);
3489
+ price = quote.expectedOutput > 0 ? 1 / quote.expectedOutput : 0;
3490
+ }
3491
+ investmentCollateralUsd += pos.totalAmount * price;
3492
+ } catch {
3493
+ }
3494
+ }
3495
+ if (investmentCollateralUsd <= 0) return maxResult;
3496
+ const CONSERVATIVE_LTV = 0.6;
3497
+ const investmentBorrowCapacity = investmentCollateralUsd * CONSERVATIVE_LTV;
3498
+ const adjustedMax = Math.max(0, maxResult.maxAmount - investmentBorrowCapacity);
3499
+ return { ...maxResult, maxAmount: adjustedMax };
3500
+ }
3431
3501
  async borrow(params) {
3432
3502
  this.enforcer.assertNotLocked();
3433
3503
  const asset = "USDC";
3434
3504
  const adapter = await this.resolveLending(params.protocol, asset, "borrow");
3435
- const maxResult = await adapter.maxBorrow(this._address, asset);
3505
+ const rawMax = await adapter.maxBorrow(this._address, asset);
3506
+ const maxResult = await this.adjustMaxBorrowForInvestments(adapter, rawMax);
3436
3507
  if (maxResult.maxAmount <= 0) {
3508
+ const hasInvestmentEarning = this.portfolio.getPositions().some((p) => p.earning && p.earningProtocol === adapter.id);
3509
+ if (hasInvestmentEarning) {
3510
+ throw new T2000Error(
3511
+ "BORROW_GUARD_INVESTMENT",
3512
+ "Max safe borrow: $0.00. Only savings deposits (stablecoins) count as borrowable collateral. Investment collateral (SUI, ETH, BTC) is excluded."
3513
+ );
3514
+ }
3437
3515
  throw new T2000Error("NO_COLLATERAL", "No collateral deposited. Save first with `t2000 save <amount>`.");
3438
3516
  }
3439
3517
  if (params.amount > maxResult.maxAmount) {
3440
- throw new T2000Error("HEALTH_FACTOR_TOO_LOW", `Max safe borrow: $${maxResult.maxAmount.toFixed(2)}`, {
3518
+ throw new T2000Error("HEALTH_FACTOR_TOO_LOW", `Max safe borrow: $${maxResult.maxAmount.toFixed(2)}. Only savings deposits count as borrowable collateral.`, {
3441
3519
  maxBorrow: maxResult.maxAmount,
3442
3520
  currentHF: maxResult.currentHF
3443
3521
  });
@@ -3602,7 +3680,8 @@ To access invested funds: t2000 invest sell ${params.amount} ${asset}`,
3602
3680
  }
3603
3681
  async maxBorrow() {
3604
3682
  const adapter = await this.resolveLending(void 0, "USDC", "borrow");
3605
- return adapter.maxBorrow(this._address, "USDC");
3683
+ const rawMax = await adapter.maxBorrow(this._address, "USDC");
3684
+ return this.adjustMaxBorrowForInvestments(adapter, rawMax);
3606
3685
  }
3607
3686
  async healthFactor() {
3608
3687
  const adapter = await this.resolveLending(void 0, "USDC", "save");
@@ -3766,6 +3845,9 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
3766
3845
  if (!pos || pos.totalAmount <= 0) {
3767
3846
  throw new T2000Error("INSUFFICIENT_INVESTMENT", `No ${params.asset} position to sell`);
3768
3847
  }
3848
+ if (pos.earning && pos.earningProtocol) {
3849
+ await this.investUnearn({ asset: params.asset });
3850
+ }
3769
3851
  const assetInfo = SUPPORTED_ASSETS[params.asset];
3770
3852
  const assetBalance = await this.client.getBalance({
3771
3853
  owner: this._address,
@@ -3840,6 +3922,91 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
3840
3922
  position
3841
3923
  };
3842
3924
  }
3925
+ async investEarn(params) {
3926
+ this.enforcer.assertNotLocked();
3927
+ if (!(params.asset in INVESTMENT_ASSETS)) {
3928
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `${params.asset} is not available for investment`);
3929
+ }
3930
+ const pos = this.portfolio.getPosition(params.asset);
3931
+ if (!pos || pos.totalAmount <= 0) {
3932
+ throw new T2000Error("INSUFFICIENT_INVESTMENT", `No ${params.asset} position to earn on`);
3933
+ }
3934
+ if (pos.earning) {
3935
+ throw new T2000Error("INVEST_ALREADY_EARNING", `${params.asset} is already earning via ${pos.earningProtocol}`);
3936
+ }
3937
+ const { adapter, rate } = await this.registry.bestSaveRate(params.asset);
3938
+ const assetInfo = SUPPORTED_ASSETS[params.asset];
3939
+ const assetBalance = await this.client.getBalance({
3940
+ owner: this._address,
3941
+ coinType: assetInfo.type
3942
+ });
3943
+ const walletAmount = Number(assetBalance.totalBalance) / 10 ** assetInfo.decimals;
3944
+ const gasReserve = params.asset === "SUI" ? GAS_RESERVE_MIN : 0;
3945
+ const depositAmount = Math.max(0, walletAmount - gasReserve);
3946
+ if (depositAmount <= 0) {
3947
+ throw new T2000Error("INSUFFICIENT_BALANCE", `No ${params.asset} available to deposit (wallet: ${walletAmount}, gas reserve: ${gasReserve})`);
3948
+ }
3949
+ const { tx } = await adapter.buildSaveTx(this._address, depositAmount, params.asset);
3950
+ const result = await this.client.signAndExecuteTransaction({
3951
+ signer: this.keypair,
3952
+ transaction: tx,
3953
+ options: { showEffects: true }
3954
+ });
3955
+ await this.client.waitForTransaction({ digest: result.digest });
3956
+ const gasCost = result.effects?.gasUsed ? Math.abs(
3957
+ (Number(result.effects.gasUsed.computationCost) + Number(result.effects.gasUsed.storageCost) - Number(result.effects.gasUsed.storageRebate)) / 1e9
3958
+ ) : 0;
3959
+ this.portfolio.recordEarn(params.asset, adapter.id, rate.saveApy);
3960
+ return {
3961
+ success: true,
3962
+ tx: result.digest,
3963
+ asset: params.asset,
3964
+ amount: depositAmount,
3965
+ protocol: adapter.name,
3966
+ apy: rate.saveApy,
3967
+ gasCost,
3968
+ gasMethod: "self-funded"
3969
+ };
3970
+ }
3971
+ async investUnearn(params) {
3972
+ this.enforcer.assertNotLocked();
3973
+ if (!(params.asset in INVESTMENT_ASSETS)) {
3974
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `${params.asset} is not available for investment`);
3975
+ }
3976
+ const pos = this.portfolio.getPosition(params.asset);
3977
+ if (!pos || !pos.earning || !pos.earningProtocol) {
3978
+ throw new T2000Error("INVEST_NOT_EARNING", `${params.asset} is not currently earning`);
3979
+ }
3980
+ const adapter = this.registry.getLending(pos.earningProtocol);
3981
+ if (!adapter) {
3982
+ throw new T2000Error("PROTOCOL_UNAVAILABLE", `Lending protocol ${pos.earningProtocol} not found`);
3983
+ }
3984
+ const positions = await adapter.getPositions(this._address);
3985
+ const supply = positions.supplies.find((s) => s.asset === params.asset);
3986
+ const withdrawAmount = supply?.amount ?? pos.totalAmount;
3987
+ const { tx, effectiveAmount } = await adapter.buildWithdrawTx(this._address, withdrawAmount, params.asset);
3988
+ const result = await this.client.signAndExecuteTransaction({
3989
+ signer: this.keypair,
3990
+ transaction: tx,
3991
+ options: { showEffects: true }
3992
+ });
3993
+ await this.client.waitForTransaction({ digest: result.digest });
3994
+ const gasCost = result.effects?.gasUsed ? Math.abs(
3995
+ (Number(result.effects.gasUsed.computationCost) + Number(result.effects.gasUsed.storageCost) - Number(result.effects.gasUsed.storageRebate)) / 1e9
3996
+ ) : 0;
3997
+ const protocolName = adapter.name;
3998
+ this.portfolio.recordUnearn(params.asset);
3999
+ return {
4000
+ success: true,
4001
+ tx: result.digest,
4002
+ asset: params.asset,
4003
+ amount: effectiveAmount,
4004
+ protocol: protocolName,
4005
+ apy: 0,
4006
+ gasCost,
4007
+ gasMethod: "self-funded"
4008
+ };
4009
+ }
3843
4010
  async getPortfolio() {
3844
4011
  const positions = this.portfolio.getPositions();
3845
4012
  const realizedPnL = this.portfolio.getRealizedPnL();
@@ -3889,7 +4056,10 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
3889
4056
  currentValue,
3890
4057
  unrealizedPnL,
3891
4058
  unrealizedPnLPct,
3892
- trades: pos.trades
4059
+ trades: pos.trades,
4060
+ earning: pos.earning,
4061
+ earningProtocol: pos.earningProtocol,
4062
+ earningApy: pos.earningApy
3893
4063
  });
3894
4064
  }
3895
4065
  const totalInvested = enriched.reduce((sum, p) => sum + p.costBasis, 0);
@@ -3954,8 +4124,11 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
3954
4124
  this.registry.allPositions(this._address),
3955
4125
  this.registry.allRatesAcrossAssets()
3956
4126
  ]);
4127
+ const earningAssets = new Set(
4128
+ this.portfolio.getPositions().filter((p) => p.earning).map((p) => p.asset)
4129
+ );
3957
4130
  const savePositions = allPositions.flatMap(
3958
- (p) => p.positions.supplies.filter((s) => s.amount > 0.01).map((s) => ({
4131
+ (p) => p.positions.supplies.filter((s) => s.amount > 0.01).filter((s) => !earningAssets.has(s.asset)).map((s) => ({
3959
4132
  protocolId: p.protocolId,
3960
4133
  protocol: p.protocol,
3961
4134
  asset: s.asset,