@t2000/sdk 0.15.2 → 0.16.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.js CHANGED
@@ -4,7 +4,7 @@ import { SuiJsonRpcClient } from '@mysten/sui/jsonRpc';
4
4
  import { normalizeSuiAddress, isValidSuiAddress, normalizeStructTag } from '@mysten/sui/utils';
5
5
  import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
6
6
  import { decodeSuiPrivateKey } from '@mysten/sui/cryptography';
7
- import { createHash, randomBytes, createCipheriv, createDecipheriv, scryptSync } from 'crypto';
7
+ import { createHash, randomUUID, randomBytes, createCipheriv, createDecipheriv, scryptSync } from 'crypto';
8
8
  import { access, mkdir, writeFile, readFile } from 'fs/promises';
9
9
  import { join, dirname, resolve } from 'path';
10
10
  import { homedir } from 'os';
@@ -85,6 +85,26 @@ var INVESTMENT_ASSETS = {
85
85
  BTC: SUPPORTED_ASSETS.BTC,
86
86
  ETH: SUPPORTED_ASSETS.ETH
87
87
  };
88
+ var DEFAULT_STRATEGIES = {
89
+ bluechip: {
90
+ name: "Bluechip / Large-Cap",
91
+ allocations: { BTC: 50, ETH: 30, SUI: 20 },
92
+ description: "Large-cap crypto index",
93
+ custom: false
94
+ },
95
+ layer1: {
96
+ name: "Smart Contract Platforms",
97
+ allocations: { ETH: 50, SUI: 50 },
98
+ description: "Smart contract platforms",
99
+ custom: false
100
+ },
101
+ "sui-heavy": {
102
+ name: "Sui-Weighted Portfolio",
103
+ allocations: { BTC: 20, ETH: 20, SUI: 60 },
104
+ description: "Sui-weighted portfolio",
105
+ custom: false
106
+ }
107
+ };
88
108
  var PERPS_MARKETS = ["SUI-PERP"];
89
109
  var DEFAULT_MAX_LEVERAGE = 5;
90
110
  var DEFAULT_MAX_POSITION_SIZE = 1e3;
@@ -2816,7 +2836,7 @@ var ContactManager = class {
2816
2836
  }
2817
2837
  };
2818
2838
  function emptyData() {
2819
- return { positions: {}, realizedPnL: 0 };
2839
+ return { positions: {}, strategies: {}, realizedPnL: 0 };
2820
2840
  }
2821
2841
  var PortfolioManager = class {
2822
2842
  data = emptyData();
@@ -2831,6 +2851,7 @@ var PortfolioManager = class {
2831
2851
  try {
2832
2852
  if (existsSync(this.filePath)) {
2833
2853
  this.data = JSON.parse(readFileSync(this.filePath, "utf-8"));
2854
+ if (!this.data.strategies) this.data.strategies = {};
2834
2855
  }
2835
2856
  } catch {
2836
2857
  this.data = emptyData();
@@ -2916,6 +2937,299 @@ var PortfolioManager = class {
2916
2937
  this.load();
2917
2938
  return this.data.realizedPnL;
2918
2939
  }
2940
+ // --- Strategy position tracking ---
2941
+ recordStrategyBuy(strategyKey, trade) {
2942
+ this.load();
2943
+ if (!this.data.strategies[strategyKey]) {
2944
+ this.data.strategies[strategyKey] = {};
2945
+ }
2946
+ const bucket = this.data.strategies[strategyKey];
2947
+ const pos = bucket[trade.asset] ?? { totalAmount: 0, costBasis: 0, avgPrice: 0, trades: [] };
2948
+ pos.totalAmount += trade.amount;
2949
+ pos.costBasis += trade.usdValue;
2950
+ pos.avgPrice = pos.costBasis / pos.totalAmount;
2951
+ pos.trades.push(trade);
2952
+ bucket[trade.asset] = pos;
2953
+ this.save();
2954
+ }
2955
+ recordStrategySell(strategyKey, trade) {
2956
+ this.load();
2957
+ const bucket = this.data.strategies[strategyKey];
2958
+ if (!bucket) {
2959
+ throw new T2000Error("STRATEGY_NOT_FOUND", `No positions for strategy '${strategyKey}'`);
2960
+ }
2961
+ const pos = bucket[trade.asset];
2962
+ if (!pos || pos.totalAmount <= 0) {
2963
+ throw new T2000Error("INSUFFICIENT_INVESTMENT", `No ${trade.asset} position in strategy '${strategyKey}'`);
2964
+ }
2965
+ const sellAmount = Math.min(trade.amount, pos.totalAmount);
2966
+ const costOfSold = pos.avgPrice * sellAmount;
2967
+ const realizedPnL = trade.usdValue - costOfSold;
2968
+ pos.totalAmount -= sellAmount;
2969
+ pos.costBasis -= costOfSold;
2970
+ if (pos.totalAmount < 1e-6) {
2971
+ pos.totalAmount = 0;
2972
+ pos.costBasis = 0;
2973
+ pos.avgPrice = 0;
2974
+ }
2975
+ pos.trades.push(trade);
2976
+ this.data.realizedPnL += realizedPnL;
2977
+ bucket[trade.asset] = pos;
2978
+ const hasPositions = Object.values(bucket).some((p) => p.totalAmount > 0);
2979
+ if (!hasPositions) {
2980
+ delete this.data.strategies[strategyKey];
2981
+ }
2982
+ this.save();
2983
+ return realizedPnL;
2984
+ }
2985
+ getStrategyPositions(strategyKey) {
2986
+ this.load();
2987
+ const bucket = this.data.strategies[strategyKey];
2988
+ if (!bucket) return [];
2989
+ return Object.entries(bucket).filter(([, pos]) => pos.totalAmount > 0).map(([asset, pos]) => ({ asset, ...pos }));
2990
+ }
2991
+ getAllStrategyKeys() {
2992
+ this.load();
2993
+ return Object.keys(this.data.strategies);
2994
+ }
2995
+ hasStrategyPositions(strategyKey) {
2996
+ this.load();
2997
+ const bucket = this.data.strategies[strategyKey];
2998
+ if (!bucket) return false;
2999
+ return Object.values(bucket).some((p) => p.totalAmount > 0);
3000
+ }
3001
+ };
3002
+ function emptyData2() {
3003
+ return { strategies: {} };
3004
+ }
3005
+ var StrategyManager = class {
3006
+ data = emptyData2();
3007
+ filePath;
3008
+ dir;
3009
+ seeded = false;
3010
+ constructor(configDir) {
3011
+ this.dir = configDir ?? join(homedir(), ".t2000");
3012
+ this.filePath = join(this.dir, "strategies.json");
3013
+ this.load();
3014
+ }
3015
+ load() {
3016
+ try {
3017
+ if (existsSync(this.filePath)) {
3018
+ this.data = JSON.parse(readFileSync(this.filePath, "utf-8"));
3019
+ }
3020
+ } catch {
3021
+ this.data = emptyData2();
3022
+ }
3023
+ if (!this.seeded) {
3024
+ this.seedDefaults();
3025
+ }
3026
+ }
3027
+ save() {
3028
+ if (!existsSync(this.dir)) mkdirSync(this.dir, { recursive: true });
3029
+ writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
3030
+ }
3031
+ seedDefaults() {
3032
+ this.seeded = true;
3033
+ let changed = false;
3034
+ for (const [key, def] of Object.entries(DEFAULT_STRATEGIES)) {
3035
+ if (!this.data.strategies[key]) {
3036
+ this.data.strategies[key] = { ...def, allocations: { ...def.allocations } };
3037
+ changed = true;
3038
+ }
3039
+ }
3040
+ if (changed) this.save();
3041
+ }
3042
+ getAll() {
3043
+ this.load();
3044
+ return { ...this.data.strategies };
3045
+ }
3046
+ get(name) {
3047
+ this.load();
3048
+ const strategy = this.data.strategies[name];
3049
+ if (!strategy) {
3050
+ throw new T2000Error("STRATEGY_NOT_FOUND", `Strategy '${name}' not found`);
3051
+ }
3052
+ return strategy;
3053
+ }
3054
+ create(params) {
3055
+ this.load();
3056
+ const key = params.name.toLowerCase().replace(/\s+/g, "-");
3057
+ if (this.data.strategies[key]) {
3058
+ throw new T2000Error("STRATEGY_INVALID_ALLOCATIONS", `Strategy '${key}' already exists`);
3059
+ }
3060
+ this.validateAllocations(params.allocations);
3061
+ const definition = {
3062
+ name: params.name,
3063
+ allocations: { ...params.allocations },
3064
+ description: params.description ?? `Custom strategy: ${params.name}`,
3065
+ custom: true
3066
+ };
3067
+ this.data.strategies[key] = definition;
3068
+ this.save();
3069
+ return definition;
3070
+ }
3071
+ delete(name) {
3072
+ this.load();
3073
+ const strategy = this.data.strategies[name];
3074
+ if (!strategy) {
3075
+ throw new T2000Error("STRATEGY_NOT_FOUND", `Strategy '${name}' not found`);
3076
+ }
3077
+ if (!strategy.custom) {
3078
+ throw new T2000Error("STRATEGY_BUILTIN", `Cannot delete built-in strategy '${name}'`);
3079
+ }
3080
+ delete this.data.strategies[name];
3081
+ this.save();
3082
+ }
3083
+ validateAllocations(allocations) {
3084
+ const total = Object.values(allocations).reduce((sum, pct) => sum + pct, 0);
3085
+ if (Math.abs(total - 100) > 0.01) {
3086
+ throw new T2000Error("STRATEGY_INVALID_ALLOCATIONS", `Allocations must sum to 100 (got ${total})`);
3087
+ }
3088
+ for (const asset of Object.keys(allocations)) {
3089
+ if (!(asset in INVESTMENT_ASSETS)) {
3090
+ throw new T2000Error("STRATEGY_INVALID_ALLOCATIONS", `${asset} is not an investment asset`);
3091
+ }
3092
+ if (allocations[asset] <= 0) {
3093
+ throw new T2000Error("STRATEGY_INVALID_ALLOCATIONS", `Allocation for ${asset} must be > 0`);
3094
+ }
3095
+ }
3096
+ }
3097
+ validateMinAmount(allocations, totalUsd) {
3098
+ const assetCount = Object.keys(allocations).length;
3099
+ for (const [asset, pct] of Object.entries(allocations)) {
3100
+ const assetUsd = totalUsd * (pct / 100);
3101
+ if (assetUsd < 1) {
3102
+ throw new T2000Error(
3103
+ "STRATEGY_MIN_AMOUNT",
3104
+ `Minimum $1 per asset in strategy. Need at least $${assetCount} for ${assetCount}-asset strategy.`
3105
+ );
3106
+ }
3107
+ }
3108
+ }
3109
+ };
3110
+ function emptyData3() {
3111
+ return { schedules: [] };
3112
+ }
3113
+ function computeNextRun(frequency, dayOfWeek, dayOfMonth, from) {
3114
+ const base = /* @__PURE__ */ new Date();
3115
+ const next = new Date(base);
3116
+ switch (frequency) {
3117
+ case "daily":
3118
+ next.setDate(next.getDate() + 1);
3119
+ next.setHours(0, 0, 0, 0);
3120
+ break;
3121
+ case "weekly": {
3122
+ const dow = dayOfWeek ?? 1;
3123
+ next.setDate(next.getDate() + ((7 - next.getDay() + dow) % 7 || 7));
3124
+ next.setHours(0, 0, 0, 0);
3125
+ break;
3126
+ }
3127
+ case "monthly": {
3128
+ const dom = dayOfMonth ?? 1;
3129
+ next.setMonth(next.getMonth() + 1, dom);
3130
+ next.setHours(0, 0, 0, 0);
3131
+ break;
3132
+ }
3133
+ }
3134
+ return next.toISOString();
3135
+ }
3136
+ var AutoInvestManager = class {
3137
+ data = emptyData3();
3138
+ filePath;
3139
+ dir;
3140
+ constructor(configDir) {
3141
+ this.dir = configDir ?? join(homedir(), ".t2000");
3142
+ this.filePath = join(this.dir, "auto-invest.json");
3143
+ this.load();
3144
+ }
3145
+ load() {
3146
+ try {
3147
+ if (existsSync(this.filePath)) {
3148
+ this.data = JSON.parse(readFileSync(this.filePath, "utf-8"));
3149
+ }
3150
+ } catch {
3151
+ this.data = emptyData3();
3152
+ }
3153
+ if (!this.data.schedules) {
3154
+ this.data.schedules = [];
3155
+ }
3156
+ }
3157
+ save() {
3158
+ if (!existsSync(this.dir)) mkdirSync(this.dir, { recursive: true });
3159
+ writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
3160
+ }
3161
+ setup(params) {
3162
+ this.load();
3163
+ if (!params.strategy && !params.asset) {
3164
+ throw new T2000Error("AUTO_INVEST_NOT_FOUND", "Either strategy or asset must be specified");
3165
+ }
3166
+ if (params.amount < 1) {
3167
+ throw new T2000Error("AUTO_INVEST_INSUFFICIENT", "Auto-invest amount must be at least $1");
3168
+ }
3169
+ const schedule = {
3170
+ id: randomUUID().slice(0, 8),
3171
+ strategy: params.strategy,
3172
+ asset: params.asset,
3173
+ amount: params.amount,
3174
+ frequency: params.frequency,
3175
+ dayOfWeek: params.dayOfWeek,
3176
+ dayOfMonth: params.dayOfMonth,
3177
+ nextRun: computeNextRun(params.frequency, params.dayOfWeek, params.dayOfMonth),
3178
+ enabled: true,
3179
+ totalInvested: 0,
3180
+ runCount: 0
3181
+ };
3182
+ this.data.schedules.push(schedule);
3183
+ this.save();
3184
+ return schedule;
3185
+ }
3186
+ getStatus() {
3187
+ this.load();
3188
+ const now = /* @__PURE__ */ new Date();
3189
+ const pending = this.data.schedules.filter(
3190
+ (s) => s.enabled && new Date(s.nextRun) <= now
3191
+ );
3192
+ return {
3193
+ schedules: [...this.data.schedules],
3194
+ pendingRuns: pending
3195
+ };
3196
+ }
3197
+ getSchedule(id) {
3198
+ this.load();
3199
+ const schedule = this.data.schedules.find((s) => s.id === id);
3200
+ if (!schedule) {
3201
+ throw new T2000Error("AUTO_INVEST_NOT_FOUND", `Schedule '${id}' not found`);
3202
+ }
3203
+ return schedule;
3204
+ }
3205
+ recordRun(id, amountInvested) {
3206
+ this.load();
3207
+ const schedule = this.data.schedules.find((s) => s.id === id);
3208
+ if (!schedule) return;
3209
+ schedule.lastRun = (/* @__PURE__ */ new Date()).toISOString();
3210
+ schedule.nextRun = computeNextRun(schedule.frequency, schedule.dayOfWeek, schedule.dayOfMonth);
3211
+ schedule.totalInvested += amountInvested;
3212
+ schedule.runCount += 1;
3213
+ this.save();
3214
+ }
3215
+ stop(id) {
3216
+ this.load();
3217
+ const schedule = this.data.schedules.find((s) => s.id === id);
3218
+ if (!schedule) {
3219
+ throw new T2000Error("AUTO_INVEST_NOT_FOUND", `Schedule '${id}' not found`);
3220
+ }
3221
+ schedule.enabled = false;
3222
+ this.save();
3223
+ }
3224
+ remove(id) {
3225
+ this.load();
3226
+ const idx = this.data.schedules.findIndex((s) => s.id === id);
3227
+ if (idx === -1) {
3228
+ throw new T2000Error("AUTO_INVEST_NOT_FOUND", `Schedule '${id}' not found`);
3229
+ }
3230
+ this.data.schedules.splice(idx, 1);
3231
+ this.save();
3232
+ }
2919
3233
  };
2920
3234
  var DEFAULT_CONFIG_DIR = join(homedir(), ".t2000");
2921
3235
  var T2000 = class _T2000 extends EventEmitter {
@@ -2926,6 +3240,8 @@ var T2000 = class _T2000 extends EventEmitter {
2926
3240
  enforcer;
2927
3241
  contacts;
2928
3242
  portfolio;
3243
+ strategies;
3244
+ autoInvest;
2929
3245
  constructor(keypair, client, registry, configDir) {
2930
3246
  super();
2931
3247
  this.keypair = keypair;
@@ -2936,6 +3252,8 @@ var T2000 = class _T2000 extends EventEmitter {
2936
3252
  this.enforcer.load();
2937
3253
  this.contacts = new ContactManager(configDir);
2938
3254
  this.portfolio = new PortfolioManager(configDir);
3255
+ this.strategies = new StrategyManager(configDir);
3256
+ this.autoInvest = new AutoInvestManager(configDir);
2939
3257
  }
2940
3258
  static createDefaultRegistry(client) {
2941
3259
  const registry = new ProtocolRegistry();
@@ -3054,9 +3372,12 @@ To access invested funds: t2000 invest sell ${params.amount} ${asset}`,
3054
3372
  }
3055
3373
  async balance() {
3056
3374
  const bal = await queryBalance(this.client, this._address);
3375
+ const earningAssets = new Set(
3376
+ this.portfolio.getPositions().filter((p) => p.earning).map((p) => p.asset)
3377
+ );
3057
3378
  try {
3058
3379
  const positions = await this.positions();
3059
- const savings = positions.positions.filter((p) => p.type === "save").reduce((sum, p) => sum + p.amount, 0);
3380
+ const savings = positions.positions.filter((p) => p.type === "save").filter((p) => !earningAssets.has(p.asset)).reduce((sum, p) => sum + p.amount, 0);
3060
3381
  const debt = positions.positions.filter((p) => p.type === "borrow").reduce((sum, p) => sum + p.amount, 0);
3061
3382
  bal.savings = savings;
3062
3383
  bal.debt = debt;
@@ -3084,7 +3405,14 @@ To access invested funds: t2000 invest sell ${params.amount} ${asset}`,
3084
3405
  for (const pos of portfolioPositions) {
3085
3406
  if (!(pos.asset in INVESTMENT_ASSETS)) continue;
3086
3407
  const price = assetPrices[pos.asset] ?? 0;
3087
- if (pos.asset === "SUI") {
3408
+ if (pos.earning) {
3409
+ investmentValue += pos.totalAmount * price;
3410
+ investmentCostBasis += pos.costBasis;
3411
+ if (pos.asset === "SUI") {
3412
+ const gasSui = Math.max(0, bal.gasReserve.sui);
3413
+ bal.gasReserve = { sui: gasSui, usdEquiv: gasSui * price };
3414
+ }
3415
+ } else if (pos.asset === "SUI") {
3088
3416
  const actualHeld = Math.min(pos.totalAmount, bal.gasReserve.sui);
3089
3417
  investmentValue += actualHeld * price;
3090
3418
  if (actualHeld < pos.totalAmount && pos.totalAmount > 0) {
@@ -3099,6 +3427,14 @@ To access invested funds: t2000 invest sell ${params.amount} ${asset}`,
3099
3427
  investmentCostBasis += pos.costBasis;
3100
3428
  }
3101
3429
  }
3430
+ for (const key of this.portfolio.getAllStrategyKeys()) {
3431
+ for (const sp of this.portfolio.getStrategyPositions(key)) {
3432
+ if (!(sp.asset in INVESTMENT_ASSETS)) continue;
3433
+ const price = assetPrices[sp.asset] ?? 0;
3434
+ investmentValue += sp.totalAmount * price;
3435
+ investmentCostBasis += sp.costBasis;
3436
+ }
3437
+ }
3102
3438
  bal.investment = investmentValue;
3103
3439
  bal.investmentPnL = investmentValue - investmentCostBasis;
3104
3440
  } catch {
@@ -3995,6 +4331,252 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
3995
4331
  gasMethod: gasResult.gasMethod
3996
4332
  };
3997
4333
  }
4334
+ // -- Strategies --
4335
+ async investStrategy(params) {
4336
+ this.enforcer.assertNotLocked();
4337
+ const definition = this.strategies.get(params.strategy);
4338
+ this.strategies.validateMinAmount(definition.allocations, params.usdAmount);
4339
+ if (!params.usdAmount || params.usdAmount <= 0) {
4340
+ throw new T2000Error("INVALID_AMOUNT", "Strategy investment must be > $0");
4341
+ }
4342
+ const bal = await queryBalance(this.client, this._address);
4343
+ if (bal.available < params.usdAmount) {
4344
+ throw new T2000Error("INSUFFICIENT_BALANCE", `Insufficient balance. Available: $${bal.available.toFixed(2)}, requested: $${params.usdAmount.toFixed(2)}`);
4345
+ }
4346
+ const buys = [];
4347
+ if (params.dryRun) {
4348
+ const swapAdapter = this.registry.listSwap()[0];
4349
+ for (const [asset, pct] of Object.entries(definition.allocations)) {
4350
+ const assetUsd = params.usdAmount * (pct / 100);
4351
+ let estAmount = 0;
4352
+ let estPrice = 0;
4353
+ try {
4354
+ if (swapAdapter) {
4355
+ const quote = await swapAdapter.getQuote("USDC", asset, assetUsd);
4356
+ estAmount = quote.expectedOutput;
4357
+ estPrice = assetUsd / estAmount;
4358
+ }
4359
+ } catch {
4360
+ }
4361
+ buys.push({ asset, usdAmount: assetUsd, amount: estAmount, price: estPrice, tx: "" });
4362
+ }
4363
+ return { success: true, strategy: params.strategy, totalInvested: params.usdAmount, buys, gasCost: 0, gasMethod: "self-funded" };
4364
+ }
4365
+ let totalGas = 0;
4366
+ let gasMethod = "self-funded";
4367
+ for (const [asset, pct] of Object.entries(definition.allocations)) {
4368
+ const assetUsd = params.usdAmount * (pct / 100);
4369
+ const result = await this.investBuy({ asset, usdAmount: assetUsd });
4370
+ this.portfolio.recordStrategyBuy(params.strategy, {
4371
+ id: `strat_${Date.now()}_${asset}`,
4372
+ type: "buy",
4373
+ asset,
4374
+ amount: result.amount,
4375
+ price: result.price,
4376
+ usdValue: assetUsd,
4377
+ fee: result.fee,
4378
+ tx: result.tx,
4379
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4380
+ });
4381
+ buys.push({ asset, usdAmount: assetUsd, amount: result.amount, price: result.price, tx: result.tx });
4382
+ totalGas += result.gasCost;
4383
+ gasMethod = result.gasMethod;
4384
+ }
4385
+ return { success: true, strategy: params.strategy, totalInvested: params.usdAmount, buys, gasCost: totalGas, gasMethod };
4386
+ }
4387
+ async sellStrategy(params) {
4388
+ this.enforcer.assertNotLocked();
4389
+ this.strategies.get(params.strategy);
4390
+ const stratPositions = this.portfolio.getStrategyPositions(params.strategy);
4391
+ if (stratPositions.length === 0) {
4392
+ throw new T2000Error("INSUFFICIENT_INVESTMENT", `No positions in strategy '${params.strategy}'`);
4393
+ }
4394
+ const sells = [];
4395
+ let totalProceeds = 0;
4396
+ let totalPnL = 0;
4397
+ let totalGas = 0;
4398
+ let gasMethod = "self-funded";
4399
+ for (const pos of stratPositions) {
4400
+ const result = await this.investSell({ asset: pos.asset, usdAmount: "all" });
4401
+ const pnl = this.portfolio.recordStrategySell(params.strategy, {
4402
+ id: `strat_sell_${Date.now()}_${pos.asset}`,
4403
+ type: "sell",
4404
+ asset: pos.asset,
4405
+ amount: result.amount,
4406
+ price: result.price,
4407
+ usdValue: result.usdValue,
4408
+ fee: result.fee,
4409
+ tx: result.tx,
4410
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4411
+ });
4412
+ sells.push({ asset: pos.asset, amount: result.amount, usdValue: result.usdValue, realizedPnL: pnl, tx: result.tx });
4413
+ totalProceeds += result.usdValue;
4414
+ totalPnL += pnl;
4415
+ totalGas += result.gasCost;
4416
+ gasMethod = result.gasMethod;
4417
+ }
4418
+ return { success: true, strategy: params.strategy, totalProceeds, realizedPnL: totalPnL, sells, gasCost: totalGas, gasMethod };
4419
+ }
4420
+ async rebalanceStrategy(params) {
4421
+ this.enforcer.assertNotLocked();
4422
+ const definition = this.strategies.get(params.strategy);
4423
+ const stratPositions = this.portfolio.getStrategyPositions(params.strategy);
4424
+ if (stratPositions.length === 0) {
4425
+ throw new T2000Error("INSUFFICIENT_INVESTMENT", `No positions in strategy '${params.strategy}'`);
4426
+ }
4427
+ const swapAdapter = this.registry.listSwap()[0];
4428
+ const prices = {};
4429
+ for (const pos of stratPositions) {
4430
+ try {
4431
+ if (pos.asset === "SUI" && swapAdapter) {
4432
+ prices[pos.asset] = await swapAdapter.getPoolPrice();
4433
+ } else if (swapAdapter) {
4434
+ const q = await swapAdapter.getQuote("USDC", pos.asset, 1);
4435
+ prices[pos.asset] = q.expectedOutput > 0 ? 1 / q.expectedOutput : 0;
4436
+ }
4437
+ } catch {
4438
+ prices[pos.asset] = 0;
4439
+ }
4440
+ }
4441
+ const totalValue = stratPositions.reduce((s, p) => s + p.totalAmount * (prices[p.asset] ?? 0), 0);
4442
+ if (totalValue <= 0) {
4443
+ throw new T2000Error("INSUFFICIENT_INVESTMENT", "Strategy has no value to rebalance");
4444
+ }
4445
+ const currentWeights = {};
4446
+ const beforeWeights = {};
4447
+ for (const pos of stratPositions) {
4448
+ const w = pos.totalAmount * (prices[pos.asset] ?? 0) / totalValue * 100;
4449
+ currentWeights[pos.asset] = w;
4450
+ beforeWeights[pos.asset] = w;
4451
+ }
4452
+ const trades = [];
4453
+ const threshold = 3;
4454
+ for (const [asset, targetPct] of Object.entries(definition.allocations)) {
4455
+ const currentPct = currentWeights[asset] ?? 0;
4456
+ const diff = targetPct - currentPct;
4457
+ if (Math.abs(diff) < threshold) continue;
4458
+ const usdDiff = totalValue * (Math.abs(diff) / 100);
4459
+ if (usdDiff < 1) continue;
4460
+ if (diff > 0) {
4461
+ const result = await this.investBuy({ asset, usdAmount: usdDiff });
4462
+ this.portfolio.recordStrategyBuy(params.strategy, {
4463
+ id: `strat_rebal_${Date.now()}_${asset}`,
4464
+ type: "buy",
4465
+ asset,
4466
+ amount: result.amount,
4467
+ price: result.price,
4468
+ usdValue: usdDiff,
4469
+ fee: result.fee,
4470
+ tx: result.tx,
4471
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4472
+ });
4473
+ trades.push({ action: "buy", asset, usdAmount: usdDiff, amount: result.amount, tx: result.tx });
4474
+ } else {
4475
+ const result = await this.investSell({ asset, usdAmount: usdDiff });
4476
+ this.portfolio.recordStrategySell(params.strategy, {
4477
+ id: `strat_rebal_${Date.now()}_${asset}`,
4478
+ type: "sell",
4479
+ asset,
4480
+ amount: result.amount,
4481
+ price: result.price,
4482
+ usdValue: result.usdValue,
4483
+ fee: result.fee,
4484
+ tx: result.tx,
4485
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4486
+ });
4487
+ trades.push({ action: "sell", asset, usdAmount: result.usdValue, amount: result.amount, tx: result.tx });
4488
+ }
4489
+ }
4490
+ const afterWeights = {};
4491
+ const updatedPositions = this.portfolio.getStrategyPositions(params.strategy);
4492
+ const newTotal = updatedPositions.reduce((s, p) => s + p.totalAmount * (prices[p.asset] ?? 0), 0);
4493
+ for (const p of updatedPositions) {
4494
+ afterWeights[p.asset] = newTotal > 0 ? p.totalAmount * (prices[p.asset] ?? 0) / newTotal * 100 : 0;
4495
+ }
4496
+ return { success: true, strategy: params.strategy, trades, beforeWeights, afterWeights, targetWeights: { ...definition.allocations } };
4497
+ }
4498
+ async getStrategyStatus(name) {
4499
+ const definition = this.strategies.get(name);
4500
+ const stratPositions = this.portfolio.getStrategyPositions(name);
4501
+ const swapAdapter = this.registry.listSwap()[0];
4502
+ const prices = {};
4503
+ for (const asset of Object.keys(definition.allocations)) {
4504
+ try {
4505
+ if (asset === "SUI" && swapAdapter) {
4506
+ prices[asset] = await swapAdapter.getPoolPrice();
4507
+ } else if (swapAdapter) {
4508
+ const q = await swapAdapter.getQuote("USDC", asset, 1);
4509
+ prices[asset] = q.expectedOutput > 0 ? 1 / q.expectedOutput : 0;
4510
+ }
4511
+ } catch {
4512
+ prices[asset] = 0;
4513
+ }
4514
+ }
4515
+ const positions = stratPositions.map((sp) => {
4516
+ const price = prices[sp.asset] ?? 0;
4517
+ const currentValue = sp.totalAmount * price;
4518
+ const pnl = currentValue - sp.costBasis;
4519
+ return {
4520
+ asset: sp.asset,
4521
+ totalAmount: sp.totalAmount,
4522
+ costBasis: sp.costBasis,
4523
+ avgPrice: sp.avgPrice,
4524
+ currentPrice: price,
4525
+ currentValue,
4526
+ unrealizedPnL: pnl,
4527
+ unrealizedPnLPct: sp.costBasis > 0 ? pnl / sp.costBasis * 100 : 0,
4528
+ trades: sp.trades
4529
+ };
4530
+ });
4531
+ const totalValue = positions.reduce((s, p) => s + p.currentValue, 0);
4532
+ const currentWeights = {};
4533
+ for (const p of positions) {
4534
+ currentWeights[p.asset] = totalValue > 0 ? p.currentValue / totalValue * 100 : 0;
4535
+ }
4536
+ return { definition, positions, currentWeights, totalValue };
4537
+ }
4538
+ // -- Auto-Invest --
4539
+ setupAutoInvest(params) {
4540
+ if (params.strategy) this.strategies.get(params.strategy);
4541
+ if (params.asset && !(params.asset in INVESTMENT_ASSETS)) {
4542
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `${params.asset} is not an investment asset`);
4543
+ }
4544
+ return this.autoInvest.setup(params);
4545
+ }
4546
+ getAutoInvestStatus() {
4547
+ return this.autoInvest.getStatus();
4548
+ }
4549
+ async runAutoInvest() {
4550
+ this.enforcer.assertNotLocked();
4551
+ const status = this.autoInvest.getStatus();
4552
+ const executed = [];
4553
+ const skipped = [];
4554
+ for (const schedule of status.pendingRuns) {
4555
+ try {
4556
+ const bal = await queryBalance(this.client, this._address);
4557
+ if (bal.available < schedule.amount) {
4558
+ skipped.push({ scheduleId: schedule.id, reason: `Insufficient balance ($${bal.available.toFixed(2)} < $${schedule.amount})` });
4559
+ continue;
4560
+ }
4561
+ if (schedule.strategy) {
4562
+ const result = await this.investStrategy({ strategy: schedule.strategy, usdAmount: schedule.amount });
4563
+ this.autoInvest.recordRun(schedule.id, schedule.amount);
4564
+ executed.push({ scheduleId: schedule.id, strategy: schedule.strategy, amount: schedule.amount, result });
4565
+ } else if (schedule.asset) {
4566
+ const result = await this.investBuy({ asset: schedule.asset, usdAmount: schedule.amount });
4567
+ this.autoInvest.recordRun(schedule.id, schedule.amount);
4568
+ executed.push({ scheduleId: schedule.id, asset: schedule.asset, amount: schedule.amount, result });
4569
+ }
4570
+ } catch (err) {
4571
+ const msg = err instanceof Error ? err.message : String(err);
4572
+ skipped.push({ scheduleId: schedule.id, reason: msg });
4573
+ }
4574
+ }
4575
+ return { executed, skipped };
4576
+ }
4577
+ stopAutoInvest(id) {
4578
+ this.autoInvest.stop(id);
4579
+ }
3998
4580
  async getPortfolio() {
3999
4581
  const positions = this.portfolio.getPositions();
4000
4582
  const realizedPnL = this.portfolio.getRealizedPnL();
@@ -4012,12 +4594,11 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
4012
4594
  prices[asset] = 0;
4013
4595
  }
4014
4596
  }
4015
- const enriched = [];
4016
- for (const pos of positions) {
4597
+ const enrichPosition = async (pos, adjustWallet) => {
4017
4598
  const currentPrice = prices[pos.asset] ?? 0;
4018
4599
  let totalAmount = pos.totalAmount;
4019
4600
  let costBasis = pos.costBasis;
4020
- if (pos.asset in INVESTMENT_ASSETS) {
4601
+ if (adjustWallet && pos.asset in INVESTMENT_ASSETS && !pos.earning) {
4021
4602
  try {
4022
4603
  const assetInfo = SUPPORTED_ASSETS[pos.asset];
4023
4604
  const bal = await this.client.getBalance({ owner: this._address, coinType: assetInfo.type });
@@ -4035,7 +4616,7 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
4035
4616
  const currentValue = totalAmount * currentPrice;
4036
4617
  const unrealizedPnL = currentPrice > 0 ? currentValue - costBasis : 0;
4037
4618
  const unrealizedPnLPct = currentPrice > 0 && costBasis > 0 ? unrealizedPnL / costBasis * 100 : 0;
4038
- enriched.push({
4619
+ return {
4039
4620
  asset: pos.asset,
4040
4621
  totalAmount,
4041
4622
  costBasis,
@@ -4048,13 +4629,29 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
4048
4629
  earning: pos.earning,
4049
4630
  earningProtocol: pos.earningProtocol,
4050
4631
  earningApy: pos.earningApy
4051
- });
4632
+ };
4633
+ };
4634
+ const enriched = [];
4635
+ for (const pos of positions) {
4636
+ enriched.push(await enrichPosition(pos, true));
4637
+ }
4638
+ const strategyPositions = {};
4639
+ for (const key of this.portfolio.getAllStrategyKeys()) {
4640
+ const sps = this.portfolio.getStrategyPositions(key);
4641
+ const enrichedStrat = [];
4642
+ for (const sp of sps) {
4643
+ enrichedStrat.push(await enrichPosition(sp, false));
4644
+ }
4645
+ if (enrichedStrat.length > 0) {
4646
+ strategyPositions[key] = enrichedStrat;
4647
+ }
4052
4648
  }
4053
- const totalInvested = enriched.reduce((sum, p) => sum + p.costBasis, 0);
4054
- const totalValue = enriched.reduce((sum, p) => sum + p.currentValue, 0);
4649
+ const allPositions = [...enriched, ...Object.values(strategyPositions).flat()];
4650
+ const totalInvested = allPositions.reduce((sum, p) => sum + p.costBasis, 0);
4651
+ const totalValue = allPositions.reduce((sum, p) => sum + p.currentValue, 0);
4055
4652
  const totalUnrealizedPnL = totalValue - totalInvested;
4056
4653
  const totalUnrealizedPnLPct = totalInvested > 0 ? totalUnrealizedPnL / totalInvested * 100 : 0;
4057
- return {
4654
+ const result = {
4058
4655
  positions: enriched,
4059
4656
  totalInvested,
4060
4657
  totalValue,
@@ -4062,6 +4659,10 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
4062
4659
  unrealizedPnLPct: totalUnrealizedPnLPct,
4063
4660
  realizedPnL
4064
4661
  };
4662
+ if (Object.keys(strategyPositions).length > 0) {
4663
+ result.strategyPositions = strategyPositions;
4664
+ }
4665
+ return result;
4065
4666
  }
4066
4667
  // -- Info --
4067
4668
  async positions() {
@@ -4552,6 +5153,6 @@ var allDescriptors = [
4552
5153
  descriptor
4553
5154
  ];
4554
5155
 
4555
- 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, formatAssetAmount, 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 };
5156
+ export { AutoInvestManager, BPS_DENOMINATOR, CLOCK_ID, CetusAdapter, ContactManager, DEFAULT_MAX_LEVERAGE, DEFAULT_MAX_POSITION_SIZE, DEFAULT_NETWORK, DEFAULT_SAFEGUARD_CONFIG, DEFAULT_STRATEGIES, GAS_RESERVE_MIN, INVESTMENT_ASSETS, MIST_PER_SUI, NaviAdapter, OUTBOUND_OPS, PERPS_MARKETS, PortfolioManager, ProtocolRegistry, SENTINEL, SUI_DECIMALS, SUPPORTED_ASSETS, SafeguardEnforcer, SafeguardError, StrategyManager, SuilendAdapter, T2000, T2000Error, USDC_DECIMALS, addCollectFeeToTx, allDescriptors, calculateFee, descriptor3 as cetusDescriptor, executeAutoTopUp, executeWithGas, exportPrivateKey, formatAssetAmount, 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 };
4556
5157
  //# sourceMappingURL=index.js.map
4557
5158
  //# sourceMappingURL=index.js.map