@t2000/sdk 0.13.0 → 0.14.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.
- package/README.md +37 -9
- package/dist/adapters/index.cjs +21 -4
- package/dist/adapters/index.cjs.map +1 -1
- package/dist/adapters/index.d.cts +1 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.js +21 -4
- package/dist/adapters/index.js.map +1 -1
- package/dist/{index-Dd4EvUGr.d.cts → index-B14ZyQZt.d.cts} +226 -129
- package/dist/{index-Dd4EvUGr.d.ts → index-B14ZyQZt.d.ts} +226 -129
- package/dist/index.cjs +408 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +134 -57
- package/dist/index.d.ts +134 -57
- package/dist/index.js +402 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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",
|
|
@@ -302,6 +323,11 @@ function formatSui(amount) {
|
|
|
302
323
|
if (amount < 1e-3) return `${amount.toFixed(6)} SUI`;
|
|
303
324
|
return `${amount.toFixed(3)} SUI`;
|
|
304
325
|
}
|
|
326
|
+
function formatAssetAmount(amount, asset) {
|
|
327
|
+
if (asset === "BTC") return amount.toFixed(8);
|
|
328
|
+
if (asset === "ETH") return amount.toFixed(6);
|
|
329
|
+
return amount.toFixed(4);
|
|
330
|
+
}
|
|
305
331
|
var ASSET_LOOKUP = /* @__PURE__ */ new Map();
|
|
306
332
|
for (const [key, info] of Object.entries(SUPPORTED_ASSETS)) {
|
|
307
333
|
ASSET_LOOKUP.set(key.toUpperCase(), key);
|
|
@@ -406,6 +432,8 @@ async function queryBalance(client, address) {
|
|
|
406
432
|
available: totalStables,
|
|
407
433
|
savings,
|
|
408
434
|
debt: 0,
|
|
435
|
+
investment: 0,
|
|
436
|
+
investmentPnL: 0,
|
|
409
437
|
gasReserve: {
|
|
410
438
|
sui: suiAmount,
|
|
411
439
|
usdEquiv
|
|
@@ -1684,10 +1712,10 @@ var CetusAdapter = class {
|
|
|
1684
1712
|
};
|
|
1685
1713
|
}
|
|
1686
1714
|
getSupportedPairs() {
|
|
1687
|
-
const pairs = [
|
|
1688
|
-
|
|
1689
|
-
{ from: "
|
|
1690
|
-
|
|
1715
|
+
const pairs = [];
|
|
1716
|
+
for (const asset of Object.keys(INVESTMENT_ASSETS)) {
|
|
1717
|
+
pairs.push({ from: "USDC", to: asset }, { from: asset, to: "USDC" });
|
|
1718
|
+
}
|
|
1691
1719
|
for (const a of STABLE_ASSETS) {
|
|
1692
1720
|
for (const b of STABLE_ASSETS) {
|
|
1693
1721
|
if (a !== b) pairs.push({ from: a, to: b });
|
|
@@ -2776,6 +2804,76 @@ var ContactManager = class {
|
|
|
2776
2804
|
}
|
|
2777
2805
|
}
|
|
2778
2806
|
};
|
|
2807
|
+
function emptyData() {
|
|
2808
|
+
return { positions: {}, realizedPnL: 0 };
|
|
2809
|
+
}
|
|
2810
|
+
var PortfolioManager = class {
|
|
2811
|
+
data = emptyData();
|
|
2812
|
+
filePath;
|
|
2813
|
+
dir;
|
|
2814
|
+
constructor(configDir) {
|
|
2815
|
+
this.dir = configDir ?? path.join(os.homedir(), ".t2000");
|
|
2816
|
+
this.filePath = path.join(this.dir, "portfolio.json");
|
|
2817
|
+
this.load();
|
|
2818
|
+
}
|
|
2819
|
+
load() {
|
|
2820
|
+
try {
|
|
2821
|
+
if (fs.existsSync(this.filePath)) {
|
|
2822
|
+
this.data = JSON.parse(fs.readFileSync(this.filePath, "utf-8"));
|
|
2823
|
+
}
|
|
2824
|
+
} catch {
|
|
2825
|
+
this.data = emptyData();
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
save() {
|
|
2829
|
+
if (!fs.existsSync(this.dir)) fs.mkdirSync(this.dir, { recursive: true });
|
|
2830
|
+
fs.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
|
|
2831
|
+
}
|
|
2832
|
+
recordBuy(trade) {
|
|
2833
|
+
this.load();
|
|
2834
|
+
const pos = this.data.positions[trade.asset] ?? { totalAmount: 0, costBasis: 0, avgPrice: 0, trades: [] };
|
|
2835
|
+
pos.totalAmount += trade.amount;
|
|
2836
|
+
pos.costBasis += trade.usdValue;
|
|
2837
|
+
pos.avgPrice = pos.costBasis / pos.totalAmount;
|
|
2838
|
+
pos.trades.push(trade);
|
|
2839
|
+
this.data.positions[trade.asset] = pos;
|
|
2840
|
+
this.save();
|
|
2841
|
+
}
|
|
2842
|
+
recordSell(trade) {
|
|
2843
|
+
this.load();
|
|
2844
|
+
const pos = this.data.positions[trade.asset];
|
|
2845
|
+
if (!pos || pos.totalAmount <= 0) {
|
|
2846
|
+
throw new T2000Error("INSUFFICIENT_INVESTMENT", `No ${trade.asset} position to sell`);
|
|
2847
|
+
}
|
|
2848
|
+
const sellAmount = Math.min(trade.amount, pos.totalAmount);
|
|
2849
|
+
const costOfSold = pos.avgPrice * sellAmount;
|
|
2850
|
+
const realizedPnL = trade.usdValue - costOfSold;
|
|
2851
|
+
pos.totalAmount -= sellAmount;
|
|
2852
|
+
pos.costBasis -= costOfSold;
|
|
2853
|
+
if (pos.totalAmount < 1e-6) {
|
|
2854
|
+
pos.totalAmount = 0;
|
|
2855
|
+
pos.costBasis = 0;
|
|
2856
|
+
pos.avgPrice = 0;
|
|
2857
|
+
}
|
|
2858
|
+
pos.trades.push(trade);
|
|
2859
|
+
this.data.realizedPnL += realizedPnL;
|
|
2860
|
+
this.data.positions[trade.asset] = pos;
|
|
2861
|
+
this.save();
|
|
2862
|
+
return realizedPnL;
|
|
2863
|
+
}
|
|
2864
|
+
getPosition(asset) {
|
|
2865
|
+
this.load();
|
|
2866
|
+
return this.data.positions[asset];
|
|
2867
|
+
}
|
|
2868
|
+
getPositions() {
|
|
2869
|
+
this.load();
|
|
2870
|
+
return Object.entries(this.data.positions).filter(([, pos]) => pos.totalAmount > 0).map(([asset, pos]) => ({ asset, ...pos }));
|
|
2871
|
+
}
|
|
2872
|
+
getRealizedPnL() {
|
|
2873
|
+
this.load();
|
|
2874
|
+
return this.data.realizedPnL;
|
|
2875
|
+
}
|
|
2876
|
+
};
|
|
2779
2877
|
var DEFAULT_CONFIG_DIR = path.join(os.homedir(), ".t2000");
|
|
2780
2878
|
var T2000 = class _T2000 extends eventemitter3.EventEmitter {
|
|
2781
2879
|
keypair;
|
|
@@ -2784,6 +2882,7 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
|
|
|
2784
2882
|
registry;
|
|
2785
2883
|
enforcer;
|
|
2786
2884
|
contacts;
|
|
2885
|
+
portfolio;
|
|
2787
2886
|
constructor(keypair, client, registry, configDir) {
|
|
2788
2887
|
super();
|
|
2789
2888
|
this.keypair = keypair;
|
|
@@ -2793,6 +2892,7 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
|
|
|
2793
2892
|
this.enforcer = new SafeguardEnforcer(configDir);
|
|
2794
2893
|
this.enforcer.load();
|
|
2795
2894
|
this.contacts = new ContactManager(configDir);
|
|
2895
|
+
this.portfolio = new PortfolioManager(configDir);
|
|
2796
2896
|
}
|
|
2797
2897
|
static createDefaultRegistry(client) {
|
|
2798
2898
|
const registry = new ProtocolRegistry();
|
|
@@ -2872,6 +2972,19 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
|
|
|
2872
2972
|
if (!(asset in SUPPORTED_ASSETS)) {
|
|
2873
2973
|
throw new T2000Error("ASSET_NOT_SUPPORTED", `Asset ${asset} is not supported`);
|
|
2874
2974
|
}
|
|
2975
|
+
if (asset in INVESTMENT_ASSETS) {
|
|
2976
|
+
const free = await this.getFreeBalance(asset);
|
|
2977
|
+
if (params.amount > free) {
|
|
2978
|
+
const pos = this.portfolio.getPosition(asset);
|
|
2979
|
+
const invested = pos?.totalAmount ?? 0;
|
|
2980
|
+
throw new T2000Error(
|
|
2981
|
+
"INVESTMENT_LOCKED",
|
|
2982
|
+
`Cannot send ${params.amount} ${asset} \u2014 ${invested.toFixed(4)} ${asset} is invested. Free ${asset}: ${free.toFixed(4)}
|
|
2983
|
+
To access invested funds: t2000 invest sell ${params.amount} ${asset}`,
|
|
2984
|
+
{ free, invested, requested: params.amount }
|
|
2985
|
+
);
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2875
2988
|
const resolved = this.contacts.resolve(params.to);
|
|
2876
2989
|
const sendAmount = params.amount;
|
|
2877
2990
|
const sendTo = resolved.address;
|
|
@@ -2904,9 +3017,52 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
|
|
|
2904
3017
|
const debt = positions.positions.filter((p) => p.type === "borrow").reduce((sum, p) => sum + p.amount, 0);
|
|
2905
3018
|
bal.savings = savings;
|
|
2906
3019
|
bal.debt = debt;
|
|
2907
|
-
bal.total = bal.available + savings - debt + bal.gasReserve.usdEquiv;
|
|
2908
3020
|
} catch {
|
|
2909
3021
|
}
|
|
3022
|
+
try {
|
|
3023
|
+
const portfolioPositions = this.portfolio.getPositions();
|
|
3024
|
+
const suiPrice = bal.gasReserve.sui > 0 ? bal.gasReserve.usdEquiv / bal.gasReserve.sui : 0;
|
|
3025
|
+
const assetPrices = { SUI: suiPrice };
|
|
3026
|
+
const swapAdapter = this.registry.listSwap()[0];
|
|
3027
|
+
for (const pos of portfolioPositions) {
|
|
3028
|
+
if (pos.asset !== "SUI" && pos.asset in INVESTMENT_ASSETS && !(pos.asset in assetPrices)) {
|
|
3029
|
+
try {
|
|
3030
|
+
if (swapAdapter) {
|
|
3031
|
+
const quote = await swapAdapter.getQuote("USDC", pos.asset, 1);
|
|
3032
|
+
assetPrices[pos.asset] = quote.expectedOutput > 0 ? 1 / quote.expectedOutput : 0;
|
|
3033
|
+
}
|
|
3034
|
+
} catch {
|
|
3035
|
+
assetPrices[pos.asset] = 0;
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
let investmentValue = 0;
|
|
3040
|
+
let investmentCostBasis = 0;
|
|
3041
|
+
for (const pos of portfolioPositions) {
|
|
3042
|
+
if (!(pos.asset in INVESTMENT_ASSETS)) continue;
|
|
3043
|
+
const price = assetPrices[pos.asset] ?? 0;
|
|
3044
|
+
if (pos.asset === "SUI") {
|
|
3045
|
+
const actualHeld = Math.min(pos.totalAmount, bal.gasReserve.sui);
|
|
3046
|
+
investmentValue += actualHeld * price;
|
|
3047
|
+
if (actualHeld < pos.totalAmount && pos.totalAmount > 0) {
|
|
3048
|
+
investmentCostBasis += pos.costBasis * (actualHeld / pos.totalAmount);
|
|
3049
|
+
} else {
|
|
3050
|
+
investmentCostBasis += pos.costBasis;
|
|
3051
|
+
}
|
|
3052
|
+
const gasSui = Math.max(0, bal.gasReserve.sui - pos.totalAmount);
|
|
3053
|
+
bal.gasReserve = { sui: gasSui, usdEquiv: gasSui * price };
|
|
3054
|
+
} else {
|
|
3055
|
+
investmentValue += pos.totalAmount * price;
|
|
3056
|
+
investmentCostBasis += pos.costBasis;
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
bal.investment = investmentValue;
|
|
3060
|
+
bal.investmentPnL = investmentValue - investmentCostBasis;
|
|
3061
|
+
} catch {
|
|
3062
|
+
bal.investment = 0;
|
|
3063
|
+
bal.investmentPnL = 0;
|
|
3064
|
+
}
|
|
3065
|
+
bal.total = bal.available + bal.savings - bal.debt + bal.investment + bal.gasReserve.usdEquiv;
|
|
2910
3066
|
return bal;
|
|
2911
3067
|
}
|
|
2912
3068
|
async history(params) {
|
|
@@ -3469,6 +3625,19 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
|
|
|
3469
3625
|
if (fromAsset === toAsset) {
|
|
3470
3626
|
throw new T2000Error("INVALID_AMOUNT", "Cannot swap same asset");
|
|
3471
3627
|
}
|
|
3628
|
+
if (!params._bypassInvestmentGuard && fromAsset in INVESTMENT_ASSETS) {
|
|
3629
|
+
const free = await this.getFreeBalance(fromAsset);
|
|
3630
|
+
if (params.amount > free) {
|
|
3631
|
+
const pos = this.portfolio.getPosition(fromAsset);
|
|
3632
|
+
const invested = pos?.totalAmount ?? 0;
|
|
3633
|
+
throw new T2000Error(
|
|
3634
|
+
"INVESTMENT_LOCKED",
|
|
3635
|
+
`Cannot exchange ${params.amount} ${fromAsset} \u2014 ${invested.toFixed(4)} ${fromAsset} is invested. Free ${fromAsset}: ${free.toFixed(4)}
|
|
3636
|
+
To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
|
|
3637
|
+
{ free, invested, requested: params.amount }
|
|
3638
|
+
);
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3472
3641
|
const best = await this.registry.bestSwapQuote(fromAsset, toAsset, params.amount);
|
|
3473
3642
|
const adapter = best.adapter;
|
|
3474
3643
|
const fee = calculateFee("swap", params.amount);
|
|
@@ -3520,6 +3689,222 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
|
|
|
3520
3689
|
const fee = calculateFee("swap", params.amount);
|
|
3521
3690
|
return { ...best.quote, fee: { amount: fee.amount, rate: fee.rate } };
|
|
3522
3691
|
}
|
|
3692
|
+
// -- Investment --
|
|
3693
|
+
async investBuy(params) {
|
|
3694
|
+
this.enforcer.assertNotLocked();
|
|
3695
|
+
if (!params.usdAmount || params.usdAmount <= 0 || !isFinite(params.usdAmount)) {
|
|
3696
|
+
throw new T2000Error("INVALID_AMOUNT", "Investment amount must be greater than $0");
|
|
3697
|
+
}
|
|
3698
|
+
this.enforcer.check({ operation: "invest", amount: params.usdAmount });
|
|
3699
|
+
if (!(params.asset in INVESTMENT_ASSETS)) {
|
|
3700
|
+
throw new T2000Error("ASSET_NOT_SUPPORTED", `${params.asset} is not available for investment`);
|
|
3701
|
+
}
|
|
3702
|
+
const bal = await queryBalance(this.client, this._address);
|
|
3703
|
+
if (bal.available < params.usdAmount) {
|
|
3704
|
+
throw new T2000Error("INSUFFICIENT_BALANCE", `Insufficient checking balance. Available: $${bal.available.toFixed(2)}, requested: $${params.usdAmount.toFixed(2)}`);
|
|
3705
|
+
}
|
|
3706
|
+
const swapResult = await this.exchange({
|
|
3707
|
+
from: "USDC",
|
|
3708
|
+
to: params.asset,
|
|
3709
|
+
amount: params.usdAmount,
|
|
3710
|
+
maxSlippage: params.maxSlippage ?? 0.03,
|
|
3711
|
+
_bypassInvestmentGuard: true
|
|
3712
|
+
});
|
|
3713
|
+
if (swapResult.toAmount === 0) {
|
|
3714
|
+
throw new T2000Error("SWAP_FAILED", "Swap returned zero tokens \u2014 try a different amount or check liquidity");
|
|
3715
|
+
}
|
|
3716
|
+
const price = params.usdAmount / swapResult.toAmount;
|
|
3717
|
+
this.portfolio.recordBuy({
|
|
3718
|
+
id: `inv_${Date.now()}`,
|
|
3719
|
+
type: "buy",
|
|
3720
|
+
asset: params.asset,
|
|
3721
|
+
amount: swapResult.toAmount,
|
|
3722
|
+
price,
|
|
3723
|
+
usdValue: params.usdAmount,
|
|
3724
|
+
fee: swapResult.fee,
|
|
3725
|
+
tx: swapResult.tx,
|
|
3726
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3727
|
+
});
|
|
3728
|
+
const pos = this.portfolio.getPosition(params.asset);
|
|
3729
|
+
const currentPrice = price;
|
|
3730
|
+
const position = {
|
|
3731
|
+
asset: params.asset,
|
|
3732
|
+
totalAmount: pos?.totalAmount ?? swapResult.toAmount,
|
|
3733
|
+
costBasis: pos?.costBasis ?? params.usdAmount,
|
|
3734
|
+
avgPrice: pos?.avgPrice ?? price,
|
|
3735
|
+
currentPrice,
|
|
3736
|
+
currentValue: (pos?.totalAmount ?? swapResult.toAmount) * currentPrice,
|
|
3737
|
+
unrealizedPnL: 0,
|
|
3738
|
+
unrealizedPnLPct: 0,
|
|
3739
|
+
trades: pos?.trades ?? []
|
|
3740
|
+
};
|
|
3741
|
+
return {
|
|
3742
|
+
success: true,
|
|
3743
|
+
tx: swapResult.tx,
|
|
3744
|
+
type: "buy",
|
|
3745
|
+
asset: params.asset,
|
|
3746
|
+
amount: swapResult.toAmount,
|
|
3747
|
+
price,
|
|
3748
|
+
usdValue: params.usdAmount,
|
|
3749
|
+
fee: swapResult.fee,
|
|
3750
|
+
gasCost: swapResult.gasCost,
|
|
3751
|
+
gasMethod: swapResult.gasMethod,
|
|
3752
|
+
position
|
|
3753
|
+
};
|
|
3754
|
+
}
|
|
3755
|
+
async investSell(params) {
|
|
3756
|
+
this.enforcer.assertNotLocked();
|
|
3757
|
+
if (params.usdAmount !== "all") {
|
|
3758
|
+
if (!params.usdAmount || params.usdAmount <= 0 || !isFinite(params.usdAmount)) {
|
|
3759
|
+
throw new T2000Error("INVALID_AMOUNT", "Sell amount must be greater than $0");
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
if (!(params.asset in INVESTMENT_ASSETS)) {
|
|
3763
|
+
throw new T2000Error("ASSET_NOT_SUPPORTED", `${params.asset} is not available for investment`);
|
|
3764
|
+
}
|
|
3765
|
+
const pos = this.portfolio.getPosition(params.asset);
|
|
3766
|
+
if (!pos || pos.totalAmount <= 0) {
|
|
3767
|
+
throw new T2000Error("INSUFFICIENT_INVESTMENT", `No ${params.asset} position to sell`);
|
|
3768
|
+
}
|
|
3769
|
+
const assetInfo = SUPPORTED_ASSETS[params.asset];
|
|
3770
|
+
const assetBalance = await this.client.getBalance({
|
|
3771
|
+
owner: this._address,
|
|
3772
|
+
coinType: assetInfo.type
|
|
3773
|
+
});
|
|
3774
|
+
const walletAmount = Number(assetBalance.totalBalance) / 10 ** assetInfo.decimals;
|
|
3775
|
+
const gasReserve = params.asset === "SUI" ? GAS_RESERVE_MIN : 0;
|
|
3776
|
+
const maxSellable = Math.max(0, walletAmount - gasReserve);
|
|
3777
|
+
let sellAmountAsset;
|
|
3778
|
+
if (params.usdAmount === "all") {
|
|
3779
|
+
sellAmountAsset = Math.min(pos.totalAmount, maxSellable);
|
|
3780
|
+
} else {
|
|
3781
|
+
const swapAdapter = this.registry.listSwap()[0];
|
|
3782
|
+
if (!swapAdapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", "No swap adapter available");
|
|
3783
|
+
const quote = await swapAdapter.getQuote("USDC", params.asset, 1);
|
|
3784
|
+
const assetPrice = 1 / quote.expectedOutput;
|
|
3785
|
+
sellAmountAsset = params.usdAmount / assetPrice;
|
|
3786
|
+
sellAmountAsset = Math.min(sellAmountAsset, pos.totalAmount);
|
|
3787
|
+
if (sellAmountAsset > maxSellable) {
|
|
3788
|
+
throw new T2000Error(
|
|
3789
|
+
"INSUFFICIENT_INVESTMENT",
|
|
3790
|
+
`Cannot sell $${params.usdAmount.toFixed(2)} \u2014 max sellable: $${(maxSellable * assetPrice).toFixed(2)} (gas reserve: ${gasReserve} ${params.asset})`
|
|
3791
|
+
);
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
if (sellAmountAsset <= 0) {
|
|
3795
|
+
throw new T2000Error("INSUFFICIENT_INVESTMENT", "Nothing to sell after gas reserve");
|
|
3796
|
+
}
|
|
3797
|
+
const swapResult = await this.exchange({
|
|
3798
|
+
from: params.asset,
|
|
3799
|
+
to: "USDC",
|
|
3800
|
+
amount: sellAmountAsset,
|
|
3801
|
+
maxSlippage: params.maxSlippage ?? 0.03,
|
|
3802
|
+
_bypassInvestmentGuard: true
|
|
3803
|
+
});
|
|
3804
|
+
const price = swapResult.toAmount / sellAmountAsset;
|
|
3805
|
+
const realizedPnL = this.portfolio.recordSell({
|
|
3806
|
+
id: `inv_${Date.now()}`,
|
|
3807
|
+
type: "sell",
|
|
3808
|
+
asset: params.asset,
|
|
3809
|
+
amount: sellAmountAsset,
|
|
3810
|
+
price,
|
|
3811
|
+
usdValue: swapResult.toAmount,
|
|
3812
|
+
fee: swapResult.fee,
|
|
3813
|
+
tx: swapResult.tx,
|
|
3814
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3815
|
+
});
|
|
3816
|
+
const updatedPos = this.portfolio.getPosition(params.asset);
|
|
3817
|
+
const position = {
|
|
3818
|
+
asset: params.asset,
|
|
3819
|
+
totalAmount: updatedPos?.totalAmount ?? 0,
|
|
3820
|
+
costBasis: updatedPos?.costBasis ?? 0,
|
|
3821
|
+
avgPrice: updatedPos?.avgPrice ?? 0,
|
|
3822
|
+
currentPrice: price,
|
|
3823
|
+
currentValue: (updatedPos?.totalAmount ?? 0) * price,
|
|
3824
|
+
unrealizedPnL: 0,
|
|
3825
|
+
unrealizedPnLPct: 0,
|
|
3826
|
+
trades: updatedPos?.trades ?? []
|
|
3827
|
+
};
|
|
3828
|
+
return {
|
|
3829
|
+
success: true,
|
|
3830
|
+
tx: swapResult.tx,
|
|
3831
|
+
type: "sell",
|
|
3832
|
+
asset: params.asset,
|
|
3833
|
+
amount: sellAmountAsset,
|
|
3834
|
+
price,
|
|
3835
|
+
usdValue: swapResult.toAmount,
|
|
3836
|
+
fee: swapResult.fee,
|
|
3837
|
+
gasCost: swapResult.gasCost,
|
|
3838
|
+
gasMethod: swapResult.gasMethod,
|
|
3839
|
+
realizedPnL,
|
|
3840
|
+
position
|
|
3841
|
+
};
|
|
3842
|
+
}
|
|
3843
|
+
async getPortfolio() {
|
|
3844
|
+
const positions = this.portfolio.getPositions();
|
|
3845
|
+
const realizedPnL = this.portfolio.getRealizedPnL();
|
|
3846
|
+
const prices = {};
|
|
3847
|
+
const swapAdapter = this.registry.listSwap()[0];
|
|
3848
|
+
for (const asset of Object.keys(INVESTMENT_ASSETS)) {
|
|
3849
|
+
try {
|
|
3850
|
+
if (asset === "SUI" && swapAdapter) {
|
|
3851
|
+
prices[asset] = await swapAdapter.getPoolPrice();
|
|
3852
|
+
} else if (swapAdapter) {
|
|
3853
|
+
const quote = await swapAdapter.getQuote("USDC", asset, 1);
|
|
3854
|
+
prices[asset] = quote.expectedOutput > 0 ? 1 / quote.expectedOutput : 0;
|
|
3855
|
+
}
|
|
3856
|
+
} catch {
|
|
3857
|
+
prices[asset] = 0;
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
const enriched = [];
|
|
3861
|
+
for (const pos of positions) {
|
|
3862
|
+
const currentPrice = prices[pos.asset] ?? 0;
|
|
3863
|
+
let totalAmount = pos.totalAmount;
|
|
3864
|
+
let costBasis = pos.costBasis;
|
|
3865
|
+
if (pos.asset in INVESTMENT_ASSETS) {
|
|
3866
|
+
try {
|
|
3867
|
+
const assetInfo = SUPPORTED_ASSETS[pos.asset];
|
|
3868
|
+
const bal = await this.client.getBalance({ owner: this._address, coinType: assetInfo.type });
|
|
3869
|
+
const walletAmount = Number(bal.totalBalance) / 10 ** assetInfo.decimals;
|
|
3870
|
+
const gasReserve = pos.asset === "SUI" ? GAS_RESERVE_MIN : 0;
|
|
3871
|
+
const actualHeld = Math.max(0, walletAmount - gasReserve);
|
|
3872
|
+
if (actualHeld < totalAmount) {
|
|
3873
|
+
const ratio = totalAmount > 0 ? actualHeld / totalAmount : 0;
|
|
3874
|
+
costBasis *= ratio;
|
|
3875
|
+
totalAmount = actualHeld;
|
|
3876
|
+
}
|
|
3877
|
+
} catch {
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
const currentValue = totalAmount * currentPrice;
|
|
3881
|
+
const unrealizedPnL = currentPrice > 0 ? currentValue - costBasis : 0;
|
|
3882
|
+
const unrealizedPnLPct = currentPrice > 0 && costBasis > 0 ? unrealizedPnL / costBasis * 100 : 0;
|
|
3883
|
+
enriched.push({
|
|
3884
|
+
asset: pos.asset,
|
|
3885
|
+
totalAmount,
|
|
3886
|
+
costBasis,
|
|
3887
|
+
avgPrice: pos.avgPrice,
|
|
3888
|
+
currentPrice,
|
|
3889
|
+
currentValue,
|
|
3890
|
+
unrealizedPnL,
|
|
3891
|
+
unrealizedPnLPct,
|
|
3892
|
+
trades: pos.trades
|
|
3893
|
+
});
|
|
3894
|
+
}
|
|
3895
|
+
const totalInvested = enriched.reduce((sum, p) => sum + p.costBasis, 0);
|
|
3896
|
+
const totalValue = enriched.reduce((sum, p) => sum + p.currentValue, 0);
|
|
3897
|
+
const totalUnrealizedPnL = totalValue - totalInvested;
|
|
3898
|
+
const totalUnrealizedPnLPct = totalInvested > 0 ? totalUnrealizedPnL / totalInvested * 100 : 0;
|
|
3899
|
+
return {
|
|
3900
|
+
positions: enriched,
|
|
3901
|
+
totalInvested,
|
|
3902
|
+
totalValue,
|
|
3903
|
+
unrealizedPnL: totalUnrealizedPnL,
|
|
3904
|
+
unrealizedPnLPct: totalUnrealizedPnLPct,
|
|
3905
|
+
realizedPnL
|
|
3906
|
+
};
|
|
3907
|
+
}
|
|
3523
3908
|
// -- Info --
|
|
3524
3909
|
async positions() {
|
|
3525
3910
|
const allPositions = await this.registry.allPositions(this._address);
|
|
@@ -3847,6 +4232,17 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
|
|
|
3847
4232
|
return attack(this.client, this.keypair, id, prompt, fee);
|
|
3848
4233
|
}
|
|
3849
4234
|
// -- Helpers --
|
|
4235
|
+
async getFreeBalance(asset) {
|
|
4236
|
+
if (!(asset in INVESTMENT_ASSETS)) return Infinity;
|
|
4237
|
+
const pos = this.portfolio.getPosition(asset);
|
|
4238
|
+
const invested = pos?.totalAmount ?? 0;
|
|
4239
|
+
if (invested <= 0) return Infinity;
|
|
4240
|
+
const assetInfo = SUPPORTED_ASSETS[asset];
|
|
4241
|
+
const balance = await this.client.getBalance({ owner: this._address, coinType: assetInfo.type });
|
|
4242
|
+
const walletAmount = Number(balance.totalBalance) / 10 ** assetInfo.decimals;
|
|
4243
|
+
const gasReserve = asset === "SUI" ? GAS_RESERVE_MIN : 0;
|
|
4244
|
+
return Math.max(0, walletAmount - invested - gasReserve);
|
|
4245
|
+
}
|
|
3850
4246
|
async resolveLending(protocol, asset, capability) {
|
|
3851
4247
|
if (protocol) {
|
|
3852
4248
|
const adapter = this.registry.getLending(protocol);
|
|
@@ -3999,11 +4395,17 @@ exports.BPS_DENOMINATOR = BPS_DENOMINATOR;
|
|
|
3999
4395
|
exports.CLOCK_ID = CLOCK_ID;
|
|
4000
4396
|
exports.CetusAdapter = CetusAdapter;
|
|
4001
4397
|
exports.ContactManager = ContactManager;
|
|
4398
|
+
exports.DEFAULT_MAX_LEVERAGE = DEFAULT_MAX_LEVERAGE;
|
|
4399
|
+
exports.DEFAULT_MAX_POSITION_SIZE = DEFAULT_MAX_POSITION_SIZE;
|
|
4002
4400
|
exports.DEFAULT_NETWORK = DEFAULT_NETWORK;
|
|
4003
4401
|
exports.DEFAULT_SAFEGUARD_CONFIG = DEFAULT_SAFEGUARD_CONFIG;
|
|
4402
|
+
exports.GAS_RESERVE_MIN = GAS_RESERVE_MIN;
|
|
4403
|
+
exports.INVESTMENT_ASSETS = INVESTMENT_ASSETS;
|
|
4004
4404
|
exports.MIST_PER_SUI = MIST_PER_SUI;
|
|
4005
4405
|
exports.NaviAdapter = NaviAdapter;
|
|
4006
4406
|
exports.OUTBOUND_OPS = OUTBOUND_OPS;
|
|
4407
|
+
exports.PERPS_MARKETS = PERPS_MARKETS;
|
|
4408
|
+
exports.PortfolioManager = PortfolioManager;
|
|
4007
4409
|
exports.ProtocolRegistry = ProtocolRegistry;
|
|
4008
4410
|
exports.SENTINEL = SENTINEL;
|
|
4009
4411
|
exports.SUI_DECIMALS = SUI_DECIMALS;
|
|
@@ -4021,6 +4423,7 @@ exports.cetusDescriptor = descriptor3;
|
|
|
4021
4423
|
exports.executeAutoTopUp = executeAutoTopUp;
|
|
4022
4424
|
exports.executeWithGas = executeWithGas;
|
|
4023
4425
|
exports.exportPrivateKey = exportPrivateKey;
|
|
4426
|
+
exports.formatAssetAmount = formatAssetAmount;
|
|
4024
4427
|
exports.formatSui = formatSui;
|
|
4025
4428
|
exports.formatUsd = formatUsd;
|
|
4026
4429
|
exports.generateKeypair = generateKeypair;
|