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