@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/README.md +39 -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-BwHcYli9.d.cts → index-B14ZyQZt.d.cts} +227 -129
- package/dist/{index-BwHcYli9.d.ts → index-B14ZyQZt.d.ts} +227 -129
- package/dist/index.cjs +486 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +159 -57
- package/dist/index.d.ts +159 -57
- package/dist/index.js +480 -8
- 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",
|
|
@@ -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
|
-
|
|
1689
|
-
{ from: "
|
|
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 =
|
|
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:
|
|
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;
|