@t2000/sdk 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -56,6 +56,18 @@ var SUPPORTED_ASSETS = {
56
56
  decimals: 9,
57
57
  symbol: "SUI",
58
58
  displayName: "SUI"
59
+ },
60
+ BTC: {
61
+ type: "0xaafb102dd0902f5055cadecd687fb5b71ca82ef0e0285d90afde828ec58ca96b::btc::BTC",
62
+ decimals: 8,
63
+ symbol: "BTC",
64
+ displayName: "Bitcoin"
65
+ },
66
+ ETH: {
67
+ type: "0xd0e89b2af5e4910726fbcd8b8dd37bb79b29e5f83f7491bca830e94f7f226d29::eth::ETH",
68
+ decimals: 8,
69
+ symbol: "ETH",
70
+ displayName: "Ethereum"
59
71
  }
60
72
  };
61
73
  var STABLE_ASSETS = ["USDC", "USDT", "USDe", "USDsui"];
@@ -68,6 +80,15 @@ var DEFAULT_KEY_PATH = "~/.t2000/wallet.key";
68
80
  var API_BASE_URL = process.env.T2000_API_URL ?? "https://api.t2000.ai";
69
81
  var CETUS_USDC_SUI_POOL = "0x51e883ba7c0b566a26cbc8a94cd33eb0abd418a77cc1e60ad22fd9b1f29cd2ab";
70
82
  var CETUS_PACKAGE = "0x1eabed72c53feb3805120a081dc15963c204dc8d091542592abaf7a35689b2fb";
83
+ var INVESTMENT_ASSETS = {
84
+ SUI: SUPPORTED_ASSETS.SUI,
85
+ BTC: SUPPORTED_ASSETS.BTC,
86
+ ETH: SUPPORTED_ASSETS.ETH
87
+ };
88
+ var PERPS_MARKETS = ["SUI-PERP"];
89
+ var DEFAULT_MAX_LEVERAGE = 5;
90
+ var DEFAULT_MAX_POSITION_SIZE = 1e3;
91
+ var GAS_RESERVE_MIN = 0.05;
71
92
  var SENTINEL = {
72
93
  PACKAGE: "0x88b83f36dafcd5f6dcdcf1d2cb5889b03f61264ab3cee9cae35db7aa940a21b7",
73
94
  AGENT_REGISTRY: "0xc47564f5f14c12b31e0dfa1a3dc99a6380a1edf8929c28cb0eaa3359c8db36ac",
@@ -404,6 +425,8 @@ async function queryBalance(client, address) {
404
425
  available: totalStables,
405
426
  savings,
406
427
  debt: 0,
428
+ investment: 0,
429
+ investmentPnL: 0,
407
430
  gasReserve: {
408
431
  sui: suiAmount,
409
432
  usdEquiv
@@ -1682,10 +1705,10 @@ var CetusAdapter = class {
1682
1705
  };
1683
1706
  }
1684
1707
  getSupportedPairs() {
1685
- const pairs = [
1686
- { from: "USDC", to: "SUI" },
1687
- { from: "SUI", to: "USDC" }
1688
- ];
1708
+ const pairs = [];
1709
+ for (const asset of Object.keys(INVESTMENT_ASSETS)) {
1710
+ pairs.push({ from: "USDC", to: asset }, { from: asset, to: "USDC" });
1711
+ }
1689
1712
  for (const a of STABLE_ASSETS) {
1690
1713
  for (const b of STABLE_ASSETS) {
1691
1714
  if (a !== b) pairs.push({ from: a, to: b });
@@ -2774,6 +2797,76 @@ var ContactManager = class {
2774
2797
  }
2775
2798
  }
2776
2799
  };
2800
+ function emptyData() {
2801
+ return { positions: {}, realizedPnL: 0 };
2802
+ }
2803
+ var PortfolioManager = class {
2804
+ data = emptyData();
2805
+ filePath;
2806
+ dir;
2807
+ constructor(configDir) {
2808
+ this.dir = configDir ?? join(homedir(), ".t2000");
2809
+ this.filePath = join(this.dir, "portfolio.json");
2810
+ this.load();
2811
+ }
2812
+ load() {
2813
+ try {
2814
+ if (existsSync(this.filePath)) {
2815
+ this.data = JSON.parse(readFileSync(this.filePath, "utf-8"));
2816
+ }
2817
+ } catch {
2818
+ this.data = emptyData();
2819
+ }
2820
+ }
2821
+ save() {
2822
+ if (!existsSync(this.dir)) mkdirSync(this.dir, { recursive: true });
2823
+ writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
2824
+ }
2825
+ recordBuy(trade) {
2826
+ this.load();
2827
+ const pos = this.data.positions[trade.asset] ?? { totalAmount: 0, costBasis: 0, avgPrice: 0, trades: [] };
2828
+ pos.totalAmount += trade.amount;
2829
+ pos.costBasis += trade.usdValue;
2830
+ pos.avgPrice = pos.costBasis / pos.totalAmount;
2831
+ pos.trades.push(trade);
2832
+ this.data.positions[trade.asset] = pos;
2833
+ this.save();
2834
+ }
2835
+ recordSell(trade) {
2836
+ this.load();
2837
+ const pos = this.data.positions[trade.asset];
2838
+ if (!pos || pos.totalAmount <= 0) {
2839
+ throw new T2000Error("INSUFFICIENT_INVESTMENT", `No ${trade.asset} position to sell`);
2840
+ }
2841
+ const sellAmount = Math.min(trade.amount, pos.totalAmount);
2842
+ const costOfSold = pos.avgPrice * sellAmount;
2843
+ const realizedPnL = trade.usdValue - costOfSold;
2844
+ pos.totalAmount -= sellAmount;
2845
+ pos.costBasis -= costOfSold;
2846
+ if (pos.totalAmount < 1e-6) {
2847
+ pos.totalAmount = 0;
2848
+ pos.costBasis = 0;
2849
+ pos.avgPrice = 0;
2850
+ }
2851
+ pos.trades.push(trade);
2852
+ this.data.realizedPnL += realizedPnL;
2853
+ this.data.positions[trade.asset] = pos;
2854
+ this.save();
2855
+ return realizedPnL;
2856
+ }
2857
+ getPosition(asset) {
2858
+ this.load();
2859
+ return this.data.positions[asset];
2860
+ }
2861
+ getPositions() {
2862
+ this.load();
2863
+ return Object.entries(this.data.positions).filter(([, pos]) => pos.totalAmount > 0).map(([asset, pos]) => ({ asset, ...pos }));
2864
+ }
2865
+ getRealizedPnL() {
2866
+ this.load();
2867
+ return this.data.realizedPnL;
2868
+ }
2869
+ };
2777
2870
  var DEFAULT_CONFIG_DIR = join(homedir(), ".t2000");
2778
2871
  var T2000 = class _T2000 extends EventEmitter {
2779
2872
  keypair;
@@ -2782,6 +2875,7 @@ var T2000 = class _T2000 extends EventEmitter {
2782
2875
  registry;
2783
2876
  enforcer;
2784
2877
  contacts;
2878
+ portfolio;
2785
2879
  constructor(keypair, client, registry, configDir) {
2786
2880
  super();
2787
2881
  this.keypair = keypair;
@@ -2791,6 +2885,7 @@ var T2000 = class _T2000 extends EventEmitter {
2791
2885
  this.enforcer = new SafeguardEnforcer(configDir);
2792
2886
  this.enforcer.load();
2793
2887
  this.contacts = new ContactManager(configDir);
2888
+ this.portfolio = new PortfolioManager(configDir);
2794
2889
  }
2795
2890
  static createDefaultRegistry(client) {
2796
2891
  const registry = new ProtocolRegistry();
@@ -2870,6 +2965,19 @@ var T2000 = class _T2000 extends EventEmitter {
2870
2965
  if (!(asset in SUPPORTED_ASSETS)) {
2871
2966
  throw new T2000Error("ASSET_NOT_SUPPORTED", `Asset ${asset} is not supported`);
2872
2967
  }
2968
+ if (asset in INVESTMENT_ASSETS) {
2969
+ const free = await this.getFreeBalance(asset);
2970
+ if (params.amount > free) {
2971
+ const pos = this.portfolio.getPosition(asset);
2972
+ const invested = pos?.totalAmount ?? 0;
2973
+ throw new T2000Error(
2974
+ "INVESTMENT_LOCKED",
2975
+ `Cannot send ${params.amount} ${asset} \u2014 ${invested.toFixed(4)} ${asset} is invested. Free ${asset}: ${free.toFixed(4)}
2976
+ To access invested funds: t2000 invest sell ${params.amount} ${asset}`,
2977
+ { free, invested, requested: params.amount }
2978
+ );
2979
+ }
2980
+ }
2873
2981
  const resolved = this.contacts.resolve(params.to);
2874
2982
  const sendAmount = params.amount;
2875
2983
  const sendTo = resolved.address;
@@ -2902,9 +3010,52 @@ var T2000 = class _T2000 extends EventEmitter {
2902
3010
  const debt = positions.positions.filter((p) => p.type === "borrow").reduce((sum, p) => sum + p.amount, 0);
2903
3011
  bal.savings = savings;
2904
3012
  bal.debt = debt;
2905
- bal.total = bal.available + savings - debt + bal.gasReserve.usdEquiv;
2906
3013
  } catch {
2907
3014
  }
3015
+ try {
3016
+ const portfolioPositions = this.portfolio.getPositions();
3017
+ const suiPrice = bal.gasReserve.sui > 0 ? bal.gasReserve.usdEquiv / bal.gasReserve.sui : 0;
3018
+ const assetPrices = { SUI: suiPrice };
3019
+ const swapAdapter = this.registry.listSwap()[0];
3020
+ for (const pos of portfolioPositions) {
3021
+ if (pos.asset !== "SUI" && pos.asset in INVESTMENT_ASSETS && !(pos.asset in assetPrices)) {
3022
+ try {
3023
+ if (swapAdapter) {
3024
+ const quote = await swapAdapter.getQuote("USDC", pos.asset, 1);
3025
+ assetPrices[pos.asset] = quote.expectedOutput > 0 ? 1 / quote.expectedOutput : 0;
3026
+ }
3027
+ } catch {
3028
+ assetPrices[pos.asset] = 0;
3029
+ }
3030
+ }
3031
+ }
3032
+ let investmentValue = 0;
3033
+ let investmentCostBasis = 0;
3034
+ for (const pos of portfolioPositions) {
3035
+ if (!(pos.asset in INVESTMENT_ASSETS)) continue;
3036
+ const price = assetPrices[pos.asset] ?? 0;
3037
+ if (pos.asset === "SUI") {
3038
+ const actualHeld = Math.min(pos.totalAmount, bal.gasReserve.sui);
3039
+ investmentValue += actualHeld * price;
3040
+ if (actualHeld < pos.totalAmount && pos.totalAmount > 0) {
3041
+ investmentCostBasis += pos.costBasis * (actualHeld / pos.totalAmount);
3042
+ } else {
3043
+ investmentCostBasis += pos.costBasis;
3044
+ }
3045
+ const gasSui = Math.max(0, bal.gasReserve.sui - pos.totalAmount);
3046
+ bal.gasReserve = { sui: gasSui, usdEquiv: gasSui * price };
3047
+ } else {
3048
+ investmentValue += pos.totalAmount * price;
3049
+ investmentCostBasis += pos.costBasis;
3050
+ }
3051
+ }
3052
+ bal.investment = investmentValue;
3053
+ bal.investmentPnL = investmentValue - investmentCostBasis;
3054
+ } catch {
3055
+ bal.investment = 0;
3056
+ bal.investmentPnL = 0;
3057
+ }
3058
+ bal.total = bal.available + bal.savings - bal.debt + bal.investment + bal.gasReserve.usdEquiv;
2908
3059
  return bal;
2909
3060
  }
2910
3061
  async history(params) {
@@ -3467,6 +3618,19 @@ var T2000 = class _T2000 extends EventEmitter {
3467
3618
  if (fromAsset === toAsset) {
3468
3619
  throw new T2000Error("INVALID_AMOUNT", "Cannot swap same asset");
3469
3620
  }
3621
+ if (!params._bypassInvestmentGuard && fromAsset in INVESTMENT_ASSETS) {
3622
+ const free = await this.getFreeBalance(fromAsset);
3623
+ if (params.amount > free) {
3624
+ const pos = this.portfolio.getPosition(fromAsset);
3625
+ const invested = pos?.totalAmount ?? 0;
3626
+ throw new T2000Error(
3627
+ "INVESTMENT_LOCKED",
3628
+ `Cannot exchange ${params.amount} ${fromAsset} \u2014 ${invested.toFixed(4)} ${fromAsset} is invested. Free ${fromAsset}: ${free.toFixed(4)}
3629
+ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
3630
+ { free, invested, requested: params.amount }
3631
+ );
3632
+ }
3633
+ }
3470
3634
  const best = await this.registry.bestSwapQuote(fromAsset, toAsset, params.amount);
3471
3635
  const adapter = best.adapter;
3472
3636
  const fee = calculateFee("swap", params.amount);
@@ -3518,6 +3682,222 @@ var T2000 = class _T2000 extends EventEmitter {
3518
3682
  const fee = calculateFee("swap", params.amount);
3519
3683
  return { ...best.quote, fee: { amount: fee.amount, rate: fee.rate } };
3520
3684
  }
3685
+ // -- Investment --
3686
+ async investBuy(params) {
3687
+ this.enforcer.assertNotLocked();
3688
+ if (!params.usdAmount || params.usdAmount <= 0 || !isFinite(params.usdAmount)) {
3689
+ throw new T2000Error("INVALID_AMOUNT", "Investment amount must be greater than $0");
3690
+ }
3691
+ this.enforcer.check({ operation: "invest", amount: params.usdAmount });
3692
+ if (!(params.asset in INVESTMENT_ASSETS)) {
3693
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `${params.asset} is not available for investment`);
3694
+ }
3695
+ const bal = await queryBalance(this.client, this._address);
3696
+ if (bal.available < params.usdAmount) {
3697
+ throw new T2000Error("INSUFFICIENT_BALANCE", `Insufficient checking balance. Available: $${bal.available.toFixed(2)}, requested: $${params.usdAmount.toFixed(2)}`);
3698
+ }
3699
+ const swapResult = await this.exchange({
3700
+ from: "USDC",
3701
+ to: params.asset,
3702
+ amount: params.usdAmount,
3703
+ maxSlippage: params.maxSlippage ?? 0.03,
3704
+ _bypassInvestmentGuard: true
3705
+ });
3706
+ if (swapResult.toAmount === 0) {
3707
+ throw new T2000Error("SWAP_FAILED", "Swap returned zero tokens \u2014 try a different amount or check liquidity");
3708
+ }
3709
+ const price = params.usdAmount / swapResult.toAmount;
3710
+ this.portfolio.recordBuy({
3711
+ id: `inv_${Date.now()}`,
3712
+ type: "buy",
3713
+ asset: params.asset,
3714
+ amount: swapResult.toAmount,
3715
+ price,
3716
+ usdValue: params.usdAmount,
3717
+ fee: swapResult.fee,
3718
+ tx: swapResult.tx,
3719
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3720
+ });
3721
+ const pos = this.portfolio.getPosition(params.asset);
3722
+ const currentPrice = price;
3723
+ const position = {
3724
+ asset: params.asset,
3725
+ totalAmount: pos?.totalAmount ?? swapResult.toAmount,
3726
+ costBasis: pos?.costBasis ?? params.usdAmount,
3727
+ avgPrice: pos?.avgPrice ?? price,
3728
+ currentPrice,
3729
+ currentValue: (pos?.totalAmount ?? swapResult.toAmount) * currentPrice,
3730
+ unrealizedPnL: 0,
3731
+ unrealizedPnLPct: 0,
3732
+ trades: pos?.trades ?? []
3733
+ };
3734
+ return {
3735
+ success: true,
3736
+ tx: swapResult.tx,
3737
+ type: "buy",
3738
+ asset: params.asset,
3739
+ amount: swapResult.toAmount,
3740
+ price,
3741
+ usdValue: params.usdAmount,
3742
+ fee: swapResult.fee,
3743
+ gasCost: swapResult.gasCost,
3744
+ gasMethod: swapResult.gasMethod,
3745
+ position
3746
+ };
3747
+ }
3748
+ async investSell(params) {
3749
+ this.enforcer.assertNotLocked();
3750
+ if (params.usdAmount !== "all") {
3751
+ if (!params.usdAmount || params.usdAmount <= 0 || !isFinite(params.usdAmount)) {
3752
+ throw new T2000Error("INVALID_AMOUNT", "Sell amount must be greater than $0");
3753
+ }
3754
+ }
3755
+ if (!(params.asset in INVESTMENT_ASSETS)) {
3756
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `${params.asset} is not available for investment`);
3757
+ }
3758
+ const pos = this.portfolio.getPosition(params.asset);
3759
+ if (!pos || pos.totalAmount <= 0) {
3760
+ throw new T2000Error("INSUFFICIENT_INVESTMENT", `No ${params.asset} position to sell`);
3761
+ }
3762
+ const assetInfo = SUPPORTED_ASSETS[params.asset];
3763
+ const assetBalance = await this.client.getBalance({
3764
+ owner: this._address,
3765
+ coinType: assetInfo.type
3766
+ });
3767
+ const walletAmount = Number(assetBalance.totalBalance) / 10 ** assetInfo.decimals;
3768
+ const gasReserve = params.asset === "SUI" ? GAS_RESERVE_MIN : 0;
3769
+ const maxSellable = Math.max(0, walletAmount - gasReserve);
3770
+ let sellAmountAsset;
3771
+ if (params.usdAmount === "all") {
3772
+ sellAmountAsset = Math.min(pos.totalAmount, maxSellable);
3773
+ } else {
3774
+ const swapAdapter = this.registry.listSwap()[0];
3775
+ if (!swapAdapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", "No swap adapter available");
3776
+ const quote = await swapAdapter.getQuote("USDC", params.asset, 1);
3777
+ const assetPrice = 1 / quote.expectedOutput;
3778
+ sellAmountAsset = params.usdAmount / assetPrice;
3779
+ sellAmountAsset = Math.min(sellAmountAsset, pos.totalAmount);
3780
+ if (sellAmountAsset > maxSellable) {
3781
+ throw new T2000Error(
3782
+ "INSUFFICIENT_INVESTMENT",
3783
+ `Cannot sell $${params.usdAmount.toFixed(2)} \u2014 max sellable: $${(maxSellable * assetPrice).toFixed(2)} (gas reserve: ${gasReserve} ${params.asset})`
3784
+ );
3785
+ }
3786
+ }
3787
+ if (sellAmountAsset <= 0) {
3788
+ throw new T2000Error("INSUFFICIENT_INVESTMENT", "Nothing to sell after gas reserve");
3789
+ }
3790
+ const swapResult = await this.exchange({
3791
+ from: params.asset,
3792
+ to: "USDC",
3793
+ amount: sellAmountAsset,
3794
+ maxSlippage: params.maxSlippage ?? 0.03,
3795
+ _bypassInvestmentGuard: true
3796
+ });
3797
+ const price = swapResult.toAmount / sellAmountAsset;
3798
+ const realizedPnL = this.portfolio.recordSell({
3799
+ id: `inv_${Date.now()}`,
3800
+ type: "sell",
3801
+ asset: params.asset,
3802
+ amount: sellAmountAsset,
3803
+ price,
3804
+ usdValue: swapResult.toAmount,
3805
+ fee: swapResult.fee,
3806
+ tx: swapResult.tx,
3807
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3808
+ });
3809
+ const updatedPos = this.portfolio.getPosition(params.asset);
3810
+ const position = {
3811
+ asset: params.asset,
3812
+ totalAmount: updatedPos?.totalAmount ?? 0,
3813
+ costBasis: updatedPos?.costBasis ?? 0,
3814
+ avgPrice: updatedPos?.avgPrice ?? 0,
3815
+ currentPrice: price,
3816
+ currentValue: (updatedPos?.totalAmount ?? 0) * price,
3817
+ unrealizedPnL: 0,
3818
+ unrealizedPnLPct: 0,
3819
+ trades: updatedPos?.trades ?? []
3820
+ };
3821
+ return {
3822
+ success: true,
3823
+ tx: swapResult.tx,
3824
+ type: "sell",
3825
+ asset: params.asset,
3826
+ amount: sellAmountAsset,
3827
+ price,
3828
+ usdValue: swapResult.toAmount,
3829
+ fee: swapResult.fee,
3830
+ gasCost: swapResult.gasCost,
3831
+ gasMethod: swapResult.gasMethod,
3832
+ realizedPnL,
3833
+ position
3834
+ };
3835
+ }
3836
+ async getPortfolio() {
3837
+ const positions = this.portfolio.getPositions();
3838
+ const realizedPnL = this.portfolio.getRealizedPnL();
3839
+ const prices = {};
3840
+ const swapAdapter = this.registry.listSwap()[0];
3841
+ for (const asset of Object.keys(INVESTMENT_ASSETS)) {
3842
+ try {
3843
+ if (asset === "SUI" && swapAdapter) {
3844
+ prices[asset] = await swapAdapter.getPoolPrice();
3845
+ } else if (swapAdapter) {
3846
+ const quote = await swapAdapter.getQuote("USDC", asset, 1);
3847
+ prices[asset] = quote.expectedOutput > 0 ? 1 / quote.expectedOutput : 0;
3848
+ }
3849
+ } catch {
3850
+ prices[asset] = 0;
3851
+ }
3852
+ }
3853
+ const enriched = [];
3854
+ for (const pos of positions) {
3855
+ const currentPrice = prices[pos.asset] ?? 0;
3856
+ let totalAmount = pos.totalAmount;
3857
+ let costBasis = pos.costBasis;
3858
+ if (pos.asset in INVESTMENT_ASSETS) {
3859
+ try {
3860
+ const assetInfo = SUPPORTED_ASSETS[pos.asset];
3861
+ const bal = await this.client.getBalance({ owner: this._address, coinType: assetInfo.type });
3862
+ const walletAmount = Number(bal.totalBalance) / 10 ** assetInfo.decimals;
3863
+ const gasReserve = pos.asset === "SUI" ? GAS_RESERVE_MIN : 0;
3864
+ const actualHeld = Math.max(0, walletAmount - gasReserve);
3865
+ if (actualHeld < totalAmount) {
3866
+ const ratio = totalAmount > 0 ? actualHeld / totalAmount : 0;
3867
+ costBasis *= ratio;
3868
+ totalAmount = actualHeld;
3869
+ }
3870
+ } catch {
3871
+ }
3872
+ }
3873
+ const currentValue = totalAmount * currentPrice;
3874
+ const unrealizedPnL = currentPrice > 0 ? currentValue - costBasis : 0;
3875
+ const unrealizedPnLPct = currentPrice > 0 && costBasis > 0 ? unrealizedPnL / costBasis * 100 : 0;
3876
+ enriched.push({
3877
+ asset: pos.asset,
3878
+ totalAmount,
3879
+ costBasis,
3880
+ avgPrice: pos.avgPrice,
3881
+ currentPrice,
3882
+ currentValue,
3883
+ unrealizedPnL,
3884
+ unrealizedPnLPct,
3885
+ trades: pos.trades
3886
+ });
3887
+ }
3888
+ const totalInvested = enriched.reduce((sum, p) => sum + p.costBasis, 0);
3889
+ const totalValue = enriched.reduce((sum, p) => sum + p.currentValue, 0);
3890
+ const totalUnrealizedPnL = totalValue - totalInvested;
3891
+ const totalUnrealizedPnLPct = totalInvested > 0 ? totalUnrealizedPnL / totalInvested * 100 : 0;
3892
+ return {
3893
+ positions: enriched,
3894
+ totalInvested,
3895
+ totalValue,
3896
+ unrealizedPnL: totalUnrealizedPnL,
3897
+ unrealizedPnLPct: totalUnrealizedPnLPct,
3898
+ realizedPnL
3899
+ };
3900
+ }
3521
3901
  // -- Info --
3522
3902
  async positions() {
3523
3903
  const allPositions = await this.registry.allPositions(this._address);
@@ -3845,6 +4225,17 @@ var T2000 = class _T2000 extends EventEmitter {
3845
4225
  return attack(this.client, this.keypair, id, prompt, fee);
3846
4226
  }
3847
4227
  // -- Helpers --
4228
+ async getFreeBalance(asset) {
4229
+ if (!(asset in INVESTMENT_ASSETS)) return Infinity;
4230
+ const pos = this.portfolio.getPosition(asset);
4231
+ const invested = pos?.totalAmount ?? 0;
4232
+ if (invested <= 0) return Infinity;
4233
+ const assetInfo = SUPPORTED_ASSETS[asset];
4234
+ const balance = await this.client.getBalance({ owner: this._address, coinType: assetInfo.type });
4235
+ const walletAmount = Number(balance.totalBalance) / 10 ** assetInfo.decimals;
4236
+ const gasReserve = asset === "SUI" ? GAS_RESERVE_MIN : 0;
4237
+ return Math.max(0, walletAmount - invested - gasReserve);
4238
+ }
3848
4239
  async resolveLending(protocol, asset, capability) {
3849
4240
  if (protocol) {
3850
4241
  const adapter = this.registry.getLending(protocol);
@@ -3993,6 +4384,6 @@ var allDescriptors = [
3993
4384
  descriptor
3994
4385
  ];
3995
4386
 
3996
- export { BPS_DENOMINATOR, CLOCK_ID, CetusAdapter, ContactManager, DEFAULT_NETWORK, DEFAULT_SAFEGUARD_CONFIG, MIST_PER_SUI, NaviAdapter, OUTBOUND_OPS, ProtocolRegistry, SENTINEL, SUI_DECIMALS, SUPPORTED_ASSETS, SafeguardEnforcer, SafeguardError, SuilendAdapter, T2000, T2000Error, USDC_DECIMALS, addCollectFeeToTx, allDescriptors, calculateFee, descriptor3 as cetusDescriptor, executeAutoTopUp, executeWithGas, exportPrivateKey, formatSui, formatUsd, generateKeypair, getAddress, getDecimals, getGasStatus, getPoolPrice, getRates, getSentinelInfo, keypairFromPrivateKey, listSentinels, loadKey, mapMoveAbortCode, mapWalletError, mistToSui, descriptor2 as naviDescriptor, rawToStable, rawToUsdc, requestAttack, saveKey, attack as sentinelAttack, descriptor as sentinelDescriptor, settleAttack, shouldAutoTopUp, simulateTransaction, solveHashcash, stableToRaw, submitPrompt, suiToMist, descriptor4 as suilendDescriptor, throwIfSimulationFailed, truncateAddress, usdcToRaw, validateAddress, walletExists };
4387
+ export { BPS_DENOMINATOR, CLOCK_ID, CetusAdapter, ContactManager, DEFAULT_MAX_LEVERAGE, DEFAULT_MAX_POSITION_SIZE, DEFAULT_NETWORK, DEFAULT_SAFEGUARD_CONFIG, GAS_RESERVE_MIN, INVESTMENT_ASSETS, MIST_PER_SUI, NaviAdapter, OUTBOUND_OPS, PERPS_MARKETS, PortfolioManager, ProtocolRegistry, SENTINEL, SUI_DECIMALS, SUPPORTED_ASSETS, SafeguardEnforcer, SafeguardError, SuilendAdapter, T2000, T2000Error, USDC_DECIMALS, addCollectFeeToTx, allDescriptors, calculateFee, descriptor3 as cetusDescriptor, executeAutoTopUp, executeWithGas, exportPrivateKey, formatSui, formatUsd, generateKeypair, getAddress, getDecimals, getGasStatus, getPoolPrice, getRates, getSentinelInfo, keypairFromPrivateKey, listSentinels, loadKey, mapMoveAbortCode, mapWalletError, mistToSui, descriptor2 as naviDescriptor, rawToStable, rawToUsdc, requestAttack, saveKey, attack as sentinelAttack, descriptor as sentinelDescriptor, settleAttack, shouldAutoTopUp, simulateTransaction, solveHashcash, stableToRaw, submitPrompt, suiToMist, descriptor4 as suilendDescriptor, throwIfSimulationFailed, truncateAddress, usdcToRaw, validateAddress, walletExists };
3997
4388
  //# sourceMappingURL=index.js.map
3998
4389
  //# sourceMappingURL=index.js.map