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