@t2000/sdk 0.11.2 → 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 });
@@ -2699,6 +2722,153 @@ var SafeguardEnforcer = class {
2699
2722
  }
2700
2723
  }
2701
2724
  };
2725
+ var RESERVED_NAMES = /* @__PURE__ */ new Set(["to", "all", "address"]);
2726
+ var ContactManager = class {
2727
+ contacts = {};
2728
+ filePath;
2729
+ dir;
2730
+ constructor(configDir) {
2731
+ this.dir = configDir ?? path.join(os.homedir(), ".t2000");
2732
+ this.filePath = path.join(this.dir, "contacts.json");
2733
+ this.load();
2734
+ }
2735
+ load() {
2736
+ try {
2737
+ if (fs.existsSync(this.filePath)) {
2738
+ this.contacts = JSON.parse(fs.readFileSync(this.filePath, "utf-8"));
2739
+ }
2740
+ } catch {
2741
+ this.contacts = {};
2742
+ }
2743
+ }
2744
+ save() {
2745
+ if (!fs.existsSync(this.dir)) fs.mkdirSync(this.dir, { recursive: true });
2746
+ fs.writeFileSync(this.filePath, JSON.stringify(this.contacts, null, 2));
2747
+ }
2748
+ add(name, address) {
2749
+ this.validateName(name);
2750
+ const normalized = validateAddress(address);
2751
+ const key = name.toLowerCase();
2752
+ const existed = key in this.contacts;
2753
+ this.contacts[key] = { name, address: normalized };
2754
+ this.save();
2755
+ return { action: existed ? "updated" : "added" };
2756
+ }
2757
+ remove(name) {
2758
+ const key = name.toLowerCase();
2759
+ if (!(key in this.contacts)) return false;
2760
+ delete this.contacts[key];
2761
+ this.save();
2762
+ return true;
2763
+ }
2764
+ get(name) {
2765
+ this.load();
2766
+ return this.contacts[name.toLowerCase()];
2767
+ }
2768
+ list() {
2769
+ this.load();
2770
+ return Object.values(this.contacts);
2771
+ }
2772
+ resolve(nameOrAddress) {
2773
+ this.load();
2774
+ if (nameOrAddress.startsWith("0x") && nameOrAddress.length >= 42) {
2775
+ return { address: validateAddress(nameOrAddress) };
2776
+ }
2777
+ const contact = this.contacts[nameOrAddress.toLowerCase()];
2778
+ if (contact) {
2779
+ return { address: contact.address, contactName: contact.name };
2780
+ }
2781
+ throw new T2000Error(
2782
+ "CONTACT_NOT_FOUND",
2783
+ `"${nameOrAddress}" is not a valid Sui address or saved contact.
2784
+ Add it: t2000 contacts add ${nameOrAddress} 0x...`
2785
+ );
2786
+ }
2787
+ validateName(name) {
2788
+ if (name.startsWith("0x")) {
2789
+ throw new T2000Error("INVALID_CONTACT_NAME", "Contact names cannot start with 0x");
2790
+ }
2791
+ if (!/^[a-zA-Z0-9_]+$/.test(name)) {
2792
+ throw new T2000Error("INVALID_CONTACT_NAME", "Contact names can only contain letters, numbers, and underscores");
2793
+ }
2794
+ if (name.length > 32) {
2795
+ throw new T2000Error("INVALID_CONTACT_NAME", "Contact names must be 32 characters or fewer");
2796
+ }
2797
+ if (RESERVED_NAMES.has(name.toLowerCase())) {
2798
+ throw new T2000Error("INVALID_CONTACT_NAME", `"${name}" is a reserved name and cannot be used as a contact`);
2799
+ }
2800
+ }
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
+ };
2702
2872
  var DEFAULT_CONFIG_DIR = path.join(os.homedir(), ".t2000");
2703
2873
  var T2000 = class _T2000 extends eventemitter3.EventEmitter {
2704
2874
  keypair;
@@ -2706,6 +2876,8 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
2706
2876
  _address;
2707
2877
  registry;
2708
2878
  enforcer;
2879
+ contacts;
2880
+ portfolio;
2709
2881
  constructor(keypair, client, registry, configDir) {
2710
2882
  super();
2711
2883
  this.keypair = keypair;
@@ -2714,6 +2886,8 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
2714
2886
  this.registry = registry ?? _T2000.createDefaultRegistry(client);
2715
2887
  this.enforcer = new SafeguardEnforcer(configDir);
2716
2888
  this.enforcer.load();
2889
+ this.contacts = new ContactManager(configDir);
2890
+ this.portfolio = new PortfolioManager(configDir);
2717
2891
  }
2718
2892
  static createDefaultRegistry(client) {
2719
2893
  const registry = new ProtocolRegistry();
@@ -2793,8 +2967,22 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
2793
2967
  if (!(asset in SUPPORTED_ASSETS)) {
2794
2968
  throw new T2000Error("ASSET_NOT_SUPPORTED", `Asset ${asset} is not supported`);
2795
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
+ }
2983
+ const resolved = this.contacts.resolve(params.to);
2796
2984
  const sendAmount = params.amount;
2797
- const sendTo = params.to;
2985
+ const sendTo = resolved.address;
2798
2986
  const gasResult = await executeWithGas(
2799
2987
  this.client,
2800
2988
  this.keypair,
@@ -2808,7 +2996,8 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
2808
2996
  success: true,
2809
2997
  tx: gasResult.digest,
2810
2998
  amount: sendAmount,
2811
- to: params.to,
2999
+ to: resolved.address,
3000
+ contactName: resolved.contactName,
2812
3001
  gasCost: gasResult.gasCostSui,
2813
3002
  gasCostUnit: "SUI",
2814
3003
  gasMethod: gasResult.gasMethod,
@@ -2823,9 +3012,52 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
2823
3012
  const debt = positions.positions.filter((p) => p.type === "borrow").reduce((sum, p) => sum + p.amount, 0);
2824
3013
  bal.savings = savings;
2825
3014
  bal.debt = debt;
2826
- bal.total = bal.available + savings - debt + bal.gasReserve.usdEquiv;
2827
3015
  } catch {
2828
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;
2829
3061
  return bal;
2830
3062
  }
2831
3063
  async history(params) {
@@ -3388,6 +3620,19 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
3388
3620
  if (fromAsset === toAsset) {
3389
3621
  throw new T2000Error("INVALID_AMOUNT", "Cannot swap same asset");
3390
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
+ }
3391
3636
  const best = await this.registry.bestSwapQuote(fromAsset, toAsset, params.amount);
3392
3637
  const adapter = best.adapter;
3393
3638
  const fee = calculateFee("swap", params.amount);
@@ -3439,6 +3684,222 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
3439
3684
  const fee = calculateFee("swap", params.amount);
3440
3685
  return { ...best.quote, fee: { amount: fee.amount, rate: fee.rate } };
3441
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
+ }
3442
3903
  // -- Info --
3443
3904
  async positions() {
3444
3905
  const allPositions = await this.registry.allPositions(this._address);
@@ -3766,6 +4227,17 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
3766
4227
  return attack(this.client, this.keypair, id, prompt, fee);
3767
4228
  }
3768
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
+ }
3769
4241
  async resolveLending(protocol, asset, capability) {
3770
4242
  if (protocol) {
3771
4243
  const adapter = this.registry.getLending(protocol);
@@ -3917,11 +4389,18 @@ var allDescriptors = [
3917
4389
  exports.BPS_DENOMINATOR = BPS_DENOMINATOR;
3918
4390
  exports.CLOCK_ID = CLOCK_ID;
3919
4391
  exports.CetusAdapter = CetusAdapter;
4392
+ exports.ContactManager = ContactManager;
4393
+ exports.DEFAULT_MAX_LEVERAGE = DEFAULT_MAX_LEVERAGE;
4394
+ exports.DEFAULT_MAX_POSITION_SIZE = DEFAULT_MAX_POSITION_SIZE;
3920
4395
  exports.DEFAULT_NETWORK = DEFAULT_NETWORK;
3921
4396
  exports.DEFAULT_SAFEGUARD_CONFIG = DEFAULT_SAFEGUARD_CONFIG;
4397
+ exports.GAS_RESERVE_MIN = GAS_RESERVE_MIN;
4398
+ exports.INVESTMENT_ASSETS = INVESTMENT_ASSETS;
3922
4399
  exports.MIST_PER_SUI = MIST_PER_SUI;
3923
4400
  exports.NaviAdapter = NaviAdapter;
3924
4401
  exports.OUTBOUND_OPS = OUTBOUND_OPS;
4402
+ exports.PERPS_MARKETS = PERPS_MARKETS;
4403
+ exports.PortfolioManager = PortfolioManager;
3925
4404
  exports.ProtocolRegistry = ProtocolRegistry;
3926
4405
  exports.SENTINEL = SENTINEL;
3927
4406
  exports.SUI_DECIMALS = SUI_DECIMALS;