@t2000/sdk 0.14.1 → 0.15.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 +10 -2
- package/dist/adapters/index.cjs +48 -35
- 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 +48 -35
- package/dist/adapters/index.js.map +1 -1
- package/dist/{index-B14ZyQZt.d.cts → index-BykavuDO.d.cts} +14 -1
- package/dist/{index-B14ZyQZt.d.ts → index-BykavuDO.d.ts} +14 -1
- package/dist/index.cjs +213 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -6
- package/dist/index.d.ts +19 -6
- package/dist/index.js +213 -40
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -223,6 +223,9 @@ interface InvestmentPosition {
|
|
|
223
223
|
unrealizedPnL: number;
|
|
224
224
|
unrealizedPnLPct: number;
|
|
225
225
|
trades: InvestmentTrade[];
|
|
226
|
+
earning?: boolean;
|
|
227
|
+
earningProtocol?: string;
|
|
228
|
+
earningApy?: number;
|
|
226
229
|
}
|
|
227
230
|
interface PortfolioResult {
|
|
228
231
|
positions: InvestmentPosition[];
|
|
@@ -246,6 +249,16 @@ interface InvestResult {
|
|
|
246
249
|
realizedPnL?: number;
|
|
247
250
|
position: InvestmentPosition;
|
|
248
251
|
}
|
|
252
|
+
interface InvestEarnResult {
|
|
253
|
+
success: boolean;
|
|
254
|
+
tx: string;
|
|
255
|
+
asset: string;
|
|
256
|
+
amount: number;
|
|
257
|
+
protocol: string;
|
|
258
|
+
apy: number;
|
|
259
|
+
gasCost: number;
|
|
260
|
+
gasMethod: GasMethod;
|
|
261
|
+
}
|
|
249
262
|
type PositionSide = 'long' | 'short';
|
|
250
263
|
interface PerpsPosition {
|
|
251
264
|
market: string;
|
|
@@ -608,4 +621,4 @@ declare function attack(client: SuiJsonRpcClient, signer: Ed25519Keypair, sentin
|
|
|
608
621
|
/** All registered protocol descriptors — used by the indexer for event classification */
|
|
609
622
|
declare const allDescriptors: ProtocolDescriptor[];
|
|
610
623
|
|
|
611
|
-
export {
|
|
624
|
+
export { descriptor$3 as $, type AdapterCapability as A, type BalanceResponse as B, CetusAdapter as C, type DepositInfo as D, type EarningsResult as E, type FundStatusResult as F, type GasMethod as G, type HealthFactorResult as H, type InvestmentTrade as I, type RebalanceStep as J, type SentinelVerdict as K, type LendingAdapter as L, type MaxWithdrawResult as M, NaviAdapter as N, SuilendAdapter as O, type PortfolioResult as P, type SwapQuote as Q, type RepayResult as R, type SendResult as S, type T2000Options as T, type TradePositionsResult as U, type TradeResult as V, type WithdrawResult as W, allDescriptors as X, descriptor$2 as Y, getSentinelInfo as Z, listSentinels as _, type TransactionRecord as a, requestAttack as a0, attack as a1, descriptor as a2, settleAttack as a3, submitPrompt as a4, descriptor$1 as a5, type SwapAdapter as b, type SaveResult as c, type BorrowResult as d, type MaxBorrowResult as e, type SwapResult as f, type InvestResult as g, type InvestEarnResult as h, type PositionsResult as i, type RatesResult as j, type LendingRates as k, type RebalanceResult as l, type SentinelAgent as m, type SentinelAttackResult as n, type AdapterPositions as o, type AdapterTxResult as p, type AssetRates as q, type GasReserve as r, type HealthInfo as s, type InvestmentPosition as t, type PerpsAdapter as u, type PerpsPosition as v, type PositionEntry as w, type PositionSide as x, type ProtocolDescriptor as y, ProtocolRegistry as z };
|
|
@@ -223,6 +223,9 @@ interface InvestmentPosition {
|
|
|
223
223
|
unrealizedPnL: number;
|
|
224
224
|
unrealizedPnLPct: number;
|
|
225
225
|
trades: InvestmentTrade[];
|
|
226
|
+
earning?: boolean;
|
|
227
|
+
earningProtocol?: string;
|
|
228
|
+
earningApy?: number;
|
|
226
229
|
}
|
|
227
230
|
interface PortfolioResult {
|
|
228
231
|
positions: InvestmentPosition[];
|
|
@@ -246,6 +249,16 @@ interface InvestResult {
|
|
|
246
249
|
realizedPnL?: number;
|
|
247
250
|
position: InvestmentPosition;
|
|
248
251
|
}
|
|
252
|
+
interface InvestEarnResult {
|
|
253
|
+
success: boolean;
|
|
254
|
+
tx: string;
|
|
255
|
+
asset: string;
|
|
256
|
+
amount: number;
|
|
257
|
+
protocol: string;
|
|
258
|
+
apy: number;
|
|
259
|
+
gasCost: number;
|
|
260
|
+
gasMethod: GasMethod;
|
|
261
|
+
}
|
|
249
262
|
type PositionSide = 'long' | 'short';
|
|
250
263
|
interface PerpsPosition {
|
|
251
264
|
market: string;
|
|
@@ -608,4 +621,4 @@ declare function attack(client: SuiJsonRpcClient, signer: Ed25519Keypair, sentin
|
|
|
608
621
|
/** All registered protocol descriptors — used by the indexer for event classification */
|
|
609
622
|
declare const allDescriptors: ProtocolDescriptor[];
|
|
610
623
|
|
|
611
|
-
export {
|
|
624
|
+
export { descriptor$3 as $, type AdapterCapability as A, type BalanceResponse as B, CetusAdapter as C, type DepositInfo as D, type EarningsResult as E, type FundStatusResult as F, type GasMethod as G, type HealthFactorResult as H, type InvestmentTrade as I, type RebalanceStep as J, type SentinelVerdict as K, type LendingAdapter as L, type MaxWithdrawResult as M, NaviAdapter as N, SuilendAdapter as O, type PortfolioResult as P, type SwapQuote as Q, type RepayResult as R, type SendResult as S, type T2000Options as T, type TradePositionsResult as U, type TradeResult as V, type WithdrawResult as W, allDescriptors as X, descriptor$2 as Y, getSentinelInfo as Z, listSentinels as _, type TransactionRecord as a, requestAttack as a0, attack as a1, descriptor as a2, settleAttack as a3, submitPrompt as a4, descriptor$1 as a5, type SwapAdapter as b, type SaveResult as c, type BorrowResult as d, type MaxBorrowResult as e, type SwapResult as f, type InvestResult as g, type InvestEarnResult as h, type PositionsResult as i, type RatesResult as j, type LendingRates as k, type RebalanceResult as l, type SentinelAgent as m, type SentinelAttackResult as n, type AdapterPositions as o, type AdapterTxResult as p, type AssetRates as q, type GasReserve as r, type HealthInfo as s, type InvestmentPosition as t, type PerpsAdapter as u, type PerpsPosition as v, type PositionEntry as w, type PositionSide as x, type ProtocolDescriptor as y, ProtocolRegistry as z };
|
package/dist/index.cjs
CHANGED
|
@@ -60,7 +60,7 @@ var SUPPORTED_ASSETS = {
|
|
|
60
60
|
displayName: "SUI"
|
|
61
61
|
},
|
|
62
62
|
BTC: {
|
|
63
|
-
type: "
|
|
63
|
+
type: "0x0041f9f9344cac094454cd574e333c4fdb132d7bcc9379bcd4aab485b2a63942::wbtc::WBTC",
|
|
64
64
|
decimals: 8,
|
|
65
65
|
symbol: "BTC",
|
|
66
66
|
displayName: "Bitcoin"
|
|
@@ -602,16 +602,23 @@ function resolvePoolSymbol(pool) {
|
|
|
602
602
|
}
|
|
603
603
|
return pool.token?.symbol ?? "UNKNOWN";
|
|
604
604
|
}
|
|
605
|
+
function resolveAssetInfo(asset) {
|
|
606
|
+
if (asset in SUPPORTED_ASSETS) {
|
|
607
|
+
const info = SUPPORTED_ASSETS[asset];
|
|
608
|
+
return { type: info.type, decimals: info.decimals, displayName: info.displayName };
|
|
609
|
+
}
|
|
610
|
+
throw new T2000Error("ASSET_NOT_SUPPORTED", `Unknown asset: ${asset}`);
|
|
611
|
+
}
|
|
605
612
|
async function getPool(asset = "USDC") {
|
|
606
613
|
const pools = await getPools();
|
|
607
|
-
const targetType =
|
|
614
|
+
const { type: targetType, displayName } = resolveAssetInfo(asset);
|
|
608
615
|
const pool = pools.find(
|
|
609
616
|
(p) => matchesCoinType(p.suiCoinType || p.coinType || "", targetType)
|
|
610
617
|
);
|
|
611
618
|
if (!pool) {
|
|
612
619
|
throw new T2000Error(
|
|
613
620
|
"ASSET_NOT_SUPPORTED",
|
|
614
|
-
`${
|
|
621
|
+
`${displayName} pool not found on NAVI`
|
|
615
622
|
);
|
|
616
623
|
}
|
|
617
624
|
return pool;
|
|
@@ -634,13 +641,14 @@ function addOracleUpdate(tx, config, pool) {
|
|
|
634
641
|
]
|
|
635
642
|
});
|
|
636
643
|
}
|
|
637
|
-
function
|
|
638
|
-
const
|
|
639
|
-
const
|
|
644
|
+
function refreshOracles(tx, config, pools, opts) {
|
|
645
|
+
const assetsToRefresh = NAVI_SUPPORTED_ASSETS;
|
|
646
|
+
const targetTypes = assetsToRefresh.map((a) => SUPPORTED_ASSETS[a].type);
|
|
647
|
+
const matchedPools = pools.filter((p) => {
|
|
640
648
|
const ct = p.suiCoinType || p.coinType || "";
|
|
641
|
-
return
|
|
649
|
+
return targetTypes.some((t) => matchesCoinType(ct, t));
|
|
642
650
|
});
|
|
643
|
-
for (const pool of
|
|
651
|
+
for (const pool of matchedPools) {
|
|
644
652
|
addOracleUpdate(tx, config, pool);
|
|
645
653
|
}
|
|
646
654
|
}
|
|
@@ -714,7 +722,7 @@ async function buildSaveTx(client, address, amount, options = {}) {
|
|
|
714
722
|
throw new T2000Error("INVALID_AMOUNT", "Save amount must be a positive number");
|
|
715
723
|
}
|
|
716
724
|
const asset = options.asset ?? "USDC";
|
|
717
|
-
const assetInfo =
|
|
725
|
+
const assetInfo = resolveAssetInfo(asset);
|
|
718
726
|
const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
|
|
719
727
|
const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
|
|
720
728
|
const coins = await fetchCoins(client, address, assetInfo.type);
|
|
@@ -743,7 +751,7 @@ async function buildSaveTx(client, address, amount, options = {}) {
|
|
|
743
751
|
}
|
|
744
752
|
async function buildWithdrawTx(client, address, amount, options = {}) {
|
|
745
753
|
const asset = options.asset ?? "USDC";
|
|
746
|
-
const assetInfo =
|
|
754
|
+
const assetInfo = resolveAssetInfo(asset);
|
|
747
755
|
const [config, pool, pools, states] = await Promise.all([
|
|
748
756
|
getConfig(),
|
|
749
757
|
getPool(asset),
|
|
@@ -760,7 +768,7 @@ async function buildWithdrawTx(client, address, amount, options = {}) {
|
|
|
760
768
|
}
|
|
761
769
|
const tx = new transactions.Transaction();
|
|
762
770
|
tx.setSender(address);
|
|
763
|
-
|
|
771
|
+
refreshOracles(tx, config, pools);
|
|
764
772
|
const [balance] = tx.moveCall({
|
|
765
773
|
target: `${config.package}::incentive_v3::withdraw_v2`,
|
|
766
774
|
arguments: [
|
|
@@ -786,7 +794,7 @@ async function buildWithdrawTx(client, address, amount, options = {}) {
|
|
|
786
794
|
}
|
|
787
795
|
async function addWithdrawToTx(tx, client, address, amount, options = {}) {
|
|
788
796
|
const asset = options.asset ?? "USDC";
|
|
789
|
-
const assetInfo =
|
|
797
|
+
const assetInfo = resolveAssetInfo(asset);
|
|
790
798
|
const [config, pool, pools, states] = await Promise.all([
|
|
791
799
|
getConfig(),
|
|
792
800
|
getPool(asset),
|
|
@@ -805,7 +813,7 @@ async function addWithdrawToTx(tx, client, address, amount, options = {}) {
|
|
|
805
813
|
});
|
|
806
814
|
return { coin: coin2, effectiveAmount: 0 };
|
|
807
815
|
}
|
|
808
|
-
|
|
816
|
+
refreshOracles(tx, config, pools);
|
|
809
817
|
const [balance] = tx.moveCall({
|
|
810
818
|
target: `${config.package}::incentive_v3::withdraw_v2`,
|
|
811
819
|
arguments: [
|
|
@@ -854,7 +862,7 @@ async function addSaveToTx(tx, _client, _address, coin, options = {}) {
|
|
|
854
862
|
typeArguments: [pool.suiCoinType]
|
|
855
863
|
});
|
|
856
864
|
}
|
|
857
|
-
async function addRepayToTx(tx,
|
|
865
|
+
async function addRepayToTx(tx, _client, _address, coin, options = {}) {
|
|
858
866
|
const asset = options.asset ?? "USDC";
|
|
859
867
|
const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
|
|
860
868
|
addOracleUpdate(tx, config, pool);
|
|
@@ -884,7 +892,7 @@ async function buildBorrowTx(client, address, amount, options = {}) {
|
|
|
884
892
|
throw new T2000Error("INVALID_AMOUNT", "Borrow amount must be a positive number");
|
|
885
893
|
}
|
|
886
894
|
const asset = options.asset ?? "USDC";
|
|
887
|
-
const assetInfo =
|
|
895
|
+
const assetInfo = resolveAssetInfo(asset);
|
|
888
896
|
const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
|
|
889
897
|
const [config, pool, pools] = await Promise.all([
|
|
890
898
|
getConfig(),
|
|
@@ -893,7 +901,7 @@ async function buildBorrowTx(client, address, amount, options = {}) {
|
|
|
893
901
|
]);
|
|
894
902
|
const tx = new transactions.Transaction();
|
|
895
903
|
tx.setSender(address);
|
|
896
|
-
|
|
904
|
+
refreshOracles(tx, config, pools);
|
|
897
905
|
const [balance] = tx.moveCall({
|
|
898
906
|
target: `${config.package}::incentive_v3::borrow_v2`,
|
|
899
907
|
arguments: [
|
|
@@ -925,7 +933,7 @@ async function buildRepayTx(client, address, amount, options = {}) {
|
|
|
925
933
|
throw new T2000Error("INVALID_AMOUNT", "Repay amount must be a positive number");
|
|
926
934
|
}
|
|
927
935
|
const asset = options.asset ?? "USDC";
|
|
928
|
-
const assetInfo =
|
|
936
|
+
const assetInfo = resolveAssetInfo(asset);
|
|
929
937
|
const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
|
|
930
938
|
const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
|
|
931
939
|
const coins = await fetchCoins(client, address, assetInfo.type);
|
|
@@ -1024,11 +1032,12 @@ async function getHealthFactor(client, addressOrKeypair) {
|
|
|
1024
1032
|
liquidationThreshold: liqThreshold
|
|
1025
1033
|
};
|
|
1026
1034
|
}
|
|
1035
|
+
var NAVI_SUPPORTED_ASSETS = [...STABLE_ASSETS, "SUI", "ETH"];
|
|
1027
1036
|
async function getRates(client) {
|
|
1028
1037
|
try {
|
|
1029
1038
|
const pools = await getPools();
|
|
1030
1039
|
const result = {};
|
|
1031
|
-
for (const asset of
|
|
1040
|
+
for (const asset of NAVI_SUPPORTED_ASSETS) {
|
|
1032
1041
|
const targetType = SUPPORTED_ASSETS[asset].type;
|
|
1033
1042
|
const pool = pools.find((p) => matchesCoinType(p.suiCoinType || p.coinType || "", targetType));
|
|
1034
1043
|
if (!pool) continue;
|
|
@@ -1398,7 +1407,11 @@ var ProtocolRegistry = class {
|
|
|
1398
1407
|
}
|
|
1399
1408
|
async allRatesAcrossAssets() {
|
|
1400
1409
|
const results = [];
|
|
1401
|
-
|
|
1410
|
+
const allAssets = [...STABLE_ASSETS, ...Object.keys(INVESTMENT_ASSETS)];
|
|
1411
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1412
|
+
for (const asset of allAssets) {
|
|
1413
|
+
if (seen.has(asset)) continue;
|
|
1414
|
+
seen.add(asset);
|
|
1402
1415
|
for (const adapter of this.lending.values()) {
|
|
1403
1416
|
if (!adapter.supportedAssets.includes(asset)) continue;
|
|
1404
1417
|
try {
|
|
@@ -1473,7 +1486,7 @@ var NaviAdapter = class {
|
|
|
1473
1486
|
name = "NAVI Protocol";
|
|
1474
1487
|
version = "1.0.0";
|
|
1475
1488
|
capabilities = ["save", "withdraw", "borrow", "repay"];
|
|
1476
|
-
supportedAssets = [...STABLE_ASSETS];
|
|
1489
|
+
supportedAssets = [...STABLE_ASSETS, "SUI", "ETH"];
|
|
1477
1490
|
supportsSameAssetBorrow = true;
|
|
1478
1491
|
client;
|
|
1479
1492
|
async init(client) {
|
|
@@ -1500,23 +1513,23 @@ var NaviAdapter = class {
|
|
|
1500
1513
|
return getHealthFactor(this.client, address);
|
|
1501
1514
|
}
|
|
1502
1515
|
async buildSaveTx(address, amount, asset, options) {
|
|
1503
|
-
const
|
|
1504
|
-
const tx = await buildSaveTx(this.client, address, amount, { ...options, asset:
|
|
1516
|
+
const normalized = normalizeAsset(asset);
|
|
1517
|
+
const tx = await buildSaveTx(this.client, address, amount, { ...options, asset: normalized });
|
|
1505
1518
|
return { tx };
|
|
1506
1519
|
}
|
|
1507
1520
|
async buildWithdrawTx(address, amount, asset) {
|
|
1508
|
-
const
|
|
1509
|
-
const result = await buildWithdrawTx(this.client, address, amount, { asset:
|
|
1521
|
+
const normalized = normalizeAsset(asset);
|
|
1522
|
+
const result = await buildWithdrawTx(this.client, address, amount, { asset: normalized });
|
|
1510
1523
|
return { tx: result.tx, effectiveAmount: result.effectiveAmount };
|
|
1511
1524
|
}
|
|
1512
1525
|
async buildBorrowTx(address, amount, asset, options) {
|
|
1513
|
-
const
|
|
1514
|
-
const tx = await buildBorrowTx(this.client, address, amount, { ...options, asset:
|
|
1526
|
+
const normalized = normalizeAsset(asset);
|
|
1527
|
+
const tx = await buildBorrowTx(this.client, address, amount, { ...options, asset: normalized });
|
|
1515
1528
|
return { tx };
|
|
1516
1529
|
}
|
|
1517
1530
|
async buildRepayTx(address, amount, asset) {
|
|
1518
|
-
const
|
|
1519
|
-
const tx = await buildRepayTx(this.client, address, amount, { asset:
|
|
1531
|
+
const normalized = normalizeAsset(asset);
|
|
1532
|
+
const tx = await buildRepayTx(this.client, address, amount, { asset: normalized });
|
|
1520
1533
|
return { tx };
|
|
1521
1534
|
}
|
|
1522
1535
|
async maxWithdraw(address, _asset) {
|
|
@@ -1526,16 +1539,16 @@ var NaviAdapter = class {
|
|
|
1526
1539
|
return maxBorrowAmount(this.client, address);
|
|
1527
1540
|
}
|
|
1528
1541
|
async addWithdrawToTx(tx, address, amount, asset) {
|
|
1529
|
-
const
|
|
1530
|
-
return addWithdrawToTx(tx, this.client, address, amount, { asset:
|
|
1542
|
+
const normalized = normalizeAsset(asset);
|
|
1543
|
+
return addWithdrawToTx(tx, this.client, address, amount, { asset: normalized });
|
|
1531
1544
|
}
|
|
1532
1545
|
async addSaveToTx(tx, address, coin, asset, options) {
|
|
1533
|
-
const
|
|
1534
|
-
return addSaveToTx(tx, this.client, address, coin, { ...options, asset:
|
|
1546
|
+
const normalized = normalizeAsset(asset);
|
|
1547
|
+
return addSaveToTx(tx, this.client, address, coin, { ...options, asset: normalized });
|
|
1535
1548
|
}
|
|
1536
1549
|
async addRepayToTx(tx, address, coin, asset) {
|
|
1537
|
-
const
|
|
1538
|
-
return addRepayToTx(tx, this.client, address, coin, { asset:
|
|
1550
|
+
const normalized = normalizeAsset(asset);
|
|
1551
|
+
return addRepayToTx(tx, this.client, address, coin, { asset: normalized });
|
|
1539
1552
|
}
|
|
1540
1553
|
};
|
|
1541
1554
|
var DEFAULT_SLIPPAGE_BPS = 300;
|
|
@@ -1847,7 +1860,7 @@ var SuilendAdapter = class {
|
|
|
1847
1860
|
name = "Suilend";
|
|
1848
1861
|
version = "2.0.0";
|
|
1849
1862
|
capabilities = ["save", "withdraw", "borrow", "repay"];
|
|
1850
|
-
supportedAssets = [...STABLE_ASSETS];
|
|
1863
|
+
supportedAssets = [...STABLE_ASSETS, "SUI", "ETH", "BTC"];
|
|
1851
1864
|
supportsSameAssetBorrow = false;
|
|
1852
1865
|
client;
|
|
1853
1866
|
publishedAt = null;
|
|
@@ -2869,6 +2882,38 @@ var PortfolioManager = class {
|
|
|
2869
2882
|
this.load();
|
|
2870
2883
|
return Object.entries(this.data.positions).filter(([, pos]) => pos.totalAmount > 0).map(([asset, pos]) => ({ asset, ...pos }));
|
|
2871
2884
|
}
|
|
2885
|
+
recordEarn(asset, protocol, apy) {
|
|
2886
|
+
this.load();
|
|
2887
|
+
const pos = this.data.positions[asset];
|
|
2888
|
+
if (!pos || pos.totalAmount <= 0) {
|
|
2889
|
+
throw new T2000Error("INSUFFICIENT_INVESTMENT", `No ${asset} position to earn on`);
|
|
2890
|
+
}
|
|
2891
|
+
if (pos.earning) {
|
|
2892
|
+
throw new T2000Error("INVEST_ALREADY_EARNING", `${asset} is already earning via ${pos.earningProtocol}`);
|
|
2893
|
+
}
|
|
2894
|
+
pos.earning = true;
|
|
2895
|
+
pos.earningProtocol = protocol;
|
|
2896
|
+
pos.earningApy = apy;
|
|
2897
|
+
this.data.positions[asset] = pos;
|
|
2898
|
+
this.save();
|
|
2899
|
+
}
|
|
2900
|
+
recordUnearn(asset) {
|
|
2901
|
+
this.load();
|
|
2902
|
+
const pos = this.data.positions[asset];
|
|
2903
|
+
if (!pos || !pos.earning) {
|
|
2904
|
+
throw new T2000Error("INVEST_NOT_EARNING", `${asset} is not currently earning`);
|
|
2905
|
+
}
|
|
2906
|
+
pos.earning = false;
|
|
2907
|
+
pos.earningProtocol = void 0;
|
|
2908
|
+
pos.earningApy = void 0;
|
|
2909
|
+
this.data.positions[asset] = pos;
|
|
2910
|
+
this.save();
|
|
2911
|
+
}
|
|
2912
|
+
isEarning(asset) {
|
|
2913
|
+
this.load();
|
|
2914
|
+
const pos = this.data.positions[asset];
|
|
2915
|
+
return pos?.earning === true;
|
|
2916
|
+
}
|
|
2872
2917
|
getRealizedPnL() {
|
|
2873
2918
|
this.load();
|
|
2874
2919
|
return this.data.realizedPnL;
|
|
@@ -3428,16 +3473,49 @@ To access invested funds: t2000 invest sell ${params.amount} ${asset}`,
|
|
|
3428
3473
|
return adapter.maxWithdraw(this._address, "USDC");
|
|
3429
3474
|
}
|
|
3430
3475
|
// -- Borrowing --
|
|
3476
|
+
async adjustMaxBorrowForInvestments(adapter, maxResult) {
|
|
3477
|
+
const earningPositions = this.portfolio.getPositions().filter((p) => p.earning);
|
|
3478
|
+
if (earningPositions.length === 0) return maxResult;
|
|
3479
|
+
let investmentCollateralUsd = 0;
|
|
3480
|
+
const swapAdapter = this.registry.listSwap()[0];
|
|
3481
|
+
for (const pos of earningPositions) {
|
|
3482
|
+
if (pos.earningProtocol !== adapter.id) continue;
|
|
3483
|
+
try {
|
|
3484
|
+
let price = 0;
|
|
3485
|
+
if (pos.asset === "SUI" && swapAdapter) {
|
|
3486
|
+
price = await swapAdapter.getPoolPrice();
|
|
3487
|
+
} else if (swapAdapter) {
|
|
3488
|
+
const quote = await swapAdapter.getQuote("USDC", pos.asset, 1);
|
|
3489
|
+
price = quote.expectedOutput > 0 ? 1 / quote.expectedOutput : 0;
|
|
3490
|
+
}
|
|
3491
|
+
investmentCollateralUsd += pos.totalAmount * price;
|
|
3492
|
+
} catch {
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
if (investmentCollateralUsd <= 0) return maxResult;
|
|
3496
|
+
const CONSERVATIVE_LTV = 0.6;
|
|
3497
|
+
const investmentBorrowCapacity = investmentCollateralUsd * CONSERVATIVE_LTV;
|
|
3498
|
+
const adjustedMax = Math.max(0, maxResult.maxAmount - investmentBorrowCapacity);
|
|
3499
|
+
return { ...maxResult, maxAmount: adjustedMax };
|
|
3500
|
+
}
|
|
3431
3501
|
async borrow(params) {
|
|
3432
3502
|
this.enforcer.assertNotLocked();
|
|
3433
3503
|
const asset = "USDC";
|
|
3434
3504
|
const adapter = await this.resolveLending(params.protocol, asset, "borrow");
|
|
3435
|
-
const
|
|
3505
|
+
const rawMax = await adapter.maxBorrow(this._address, asset);
|
|
3506
|
+
const maxResult = await this.adjustMaxBorrowForInvestments(adapter, rawMax);
|
|
3436
3507
|
if (maxResult.maxAmount <= 0) {
|
|
3508
|
+
const hasInvestmentEarning = this.portfolio.getPositions().some((p) => p.earning && p.earningProtocol === adapter.id);
|
|
3509
|
+
if (hasInvestmentEarning) {
|
|
3510
|
+
throw new T2000Error(
|
|
3511
|
+
"BORROW_GUARD_INVESTMENT",
|
|
3512
|
+
"Max safe borrow: $0.00. Only savings deposits (stablecoins) count as borrowable collateral. Investment collateral (SUI, ETH, BTC) is excluded."
|
|
3513
|
+
);
|
|
3514
|
+
}
|
|
3437
3515
|
throw new T2000Error("NO_COLLATERAL", "No collateral deposited. Save first with `t2000 save <amount>`.");
|
|
3438
3516
|
}
|
|
3439
3517
|
if (params.amount > maxResult.maxAmount) {
|
|
3440
|
-
throw new T2000Error("HEALTH_FACTOR_TOO_LOW", `Max safe borrow: $${maxResult.maxAmount.toFixed(2)}
|
|
3518
|
+
throw new T2000Error("HEALTH_FACTOR_TOO_LOW", `Max safe borrow: $${maxResult.maxAmount.toFixed(2)}. Only savings deposits count as borrowable collateral.`, {
|
|
3441
3519
|
maxBorrow: maxResult.maxAmount,
|
|
3442
3520
|
currentHF: maxResult.currentHF
|
|
3443
3521
|
});
|
|
@@ -3602,7 +3680,8 @@ To access invested funds: t2000 invest sell ${params.amount} ${asset}`,
|
|
|
3602
3680
|
}
|
|
3603
3681
|
async maxBorrow() {
|
|
3604
3682
|
const adapter = await this.resolveLending(void 0, "USDC", "borrow");
|
|
3605
|
-
|
|
3683
|
+
const rawMax = await adapter.maxBorrow(this._address, "USDC");
|
|
3684
|
+
return this.adjustMaxBorrowForInvestments(adapter, rawMax);
|
|
3606
3685
|
}
|
|
3607
3686
|
async healthFactor() {
|
|
3608
3687
|
const adapter = await this.resolveLending(void 0, "USDC", "save");
|
|
@@ -3766,6 +3845,9 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
|
|
|
3766
3845
|
if (!pos || pos.totalAmount <= 0) {
|
|
3767
3846
|
throw new T2000Error("INSUFFICIENT_INVESTMENT", `No ${params.asset} position to sell`);
|
|
3768
3847
|
}
|
|
3848
|
+
if (pos.earning && pos.earningProtocol) {
|
|
3849
|
+
await this.investUnearn({ asset: params.asset });
|
|
3850
|
+
}
|
|
3769
3851
|
const assetInfo = SUPPORTED_ASSETS[params.asset];
|
|
3770
3852
|
const assetBalance = await this.client.getBalance({
|
|
3771
3853
|
owner: this._address,
|
|
@@ -3840,6 +3922,91 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
|
|
|
3840
3922
|
position
|
|
3841
3923
|
};
|
|
3842
3924
|
}
|
|
3925
|
+
async investEarn(params) {
|
|
3926
|
+
this.enforcer.assertNotLocked();
|
|
3927
|
+
if (!(params.asset in INVESTMENT_ASSETS)) {
|
|
3928
|
+
throw new T2000Error("ASSET_NOT_SUPPORTED", `${params.asset} is not available for investment`);
|
|
3929
|
+
}
|
|
3930
|
+
const pos = this.portfolio.getPosition(params.asset);
|
|
3931
|
+
if (!pos || pos.totalAmount <= 0) {
|
|
3932
|
+
throw new T2000Error("INSUFFICIENT_INVESTMENT", `No ${params.asset} position to earn on`);
|
|
3933
|
+
}
|
|
3934
|
+
if (pos.earning) {
|
|
3935
|
+
throw new T2000Error("INVEST_ALREADY_EARNING", `${params.asset} is already earning via ${pos.earningProtocol}`);
|
|
3936
|
+
}
|
|
3937
|
+
const { adapter, rate } = await this.registry.bestSaveRate(params.asset);
|
|
3938
|
+
const assetInfo = SUPPORTED_ASSETS[params.asset];
|
|
3939
|
+
const assetBalance = await this.client.getBalance({
|
|
3940
|
+
owner: this._address,
|
|
3941
|
+
coinType: assetInfo.type
|
|
3942
|
+
});
|
|
3943
|
+
const walletAmount = Number(assetBalance.totalBalance) / 10 ** assetInfo.decimals;
|
|
3944
|
+
const gasReserve = params.asset === "SUI" ? GAS_RESERVE_MIN : 0;
|
|
3945
|
+
const depositAmount = Math.max(0, walletAmount - gasReserve);
|
|
3946
|
+
if (depositAmount <= 0) {
|
|
3947
|
+
throw new T2000Error("INSUFFICIENT_BALANCE", `No ${params.asset} available to deposit (wallet: ${walletAmount}, gas reserve: ${gasReserve})`);
|
|
3948
|
+
}
|
|
3949
|
+
const { tx } = await adapter.buildSaveTx(this._address, depositAmount, params.asset);
|
|
3950
|
+
const result = await this.client.signAndExecuteTransaction({
|
|
3951
|
+
signer: this.keypair,
|
|
3952
|
+
transaction: tx,
|
|
3953
|
+
options: { showEffects: true }
|
|
3954
|
+
});
|
|
3955
|
+
await this.client.waitForTransaction({ digest: result.digest });
|
|
3956
|
+
const gasCost = result.effects?.gasUsed ? Math.abs(
|
|
3957
|
+
(Number(result.effects.gasUsed.computationCost) + Number(result.effects.gasUsed.storageCost) - Number(result.effects.gasUsed.storageRebate)) / 1e9
|
|
3958
|
+
) : 0;
|
|
3959
|
+
this.portfolio.recordEarn(params.asset, adapter.id, rate.saveApy);
|
|
3960
|
+
return {
|
|
3961
|
+
success: true,
|
|
3962
|
+
tx: result.digest,
|
|
3963
|
+
asset: params.asset,
|
|
3964
|
+
amount: depositAmount,
|
|
3965
|
+
protocol: adapter.name,
|
|
3966
|
+
apy: rate.saveApy,
|
|
3967
|
+
gasCost,
|
|
3968
|
+
gasMethod: "self-funded"
|
|
3969
|
+
};
|
|
3970
|
+
}
|
|
3971
|
+
async investUnearn(params) {
|
|
3972
|
+
this.enforcer.assertNotLocked();
|
|
3973
|
+
if (!(params.asset in INVESTMENT_ASSETS)) {
|
|
3974
|
+
throw new T2000Error("ASSET_NOT_SUPPORTED", `${params.asset} is not available for investment`);
|
|
3975
|
+
}
|
|
3976
|
+
const pos = this.portfolio.getPosition(params.asset);
|
|
3977
|
+
if (!pos || !pos.earning || !pos.earningProtocol) {
|
|
3978
|
+
throw new T2000Error("INVEST_NOT_EARNING", `${params.asset} is not currently earning`);
|
|
3979
|
+
}
|
|
3980
|
+
const adapter = this.registry.getLending(pos.earningProtocol);
|
|
3981
|
+
if (!adapter) {
|
|
3982
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", `Lending protocol ${pos.earningProtocol} not found`);
|
|
3983
|
+
}
|
|
3984
|
+
const positions = await adapter.getPositions(this._address);
|
|
3985
|
+
const supply = positions.supplies.find((s) => s.asset === params.asset);
|
|
3986
|
+
const withdrawAmount = supply?.amount ?? pos.totalAmount;
|
|
3987
|
+
const { tx, effectiveAmount } = await adapter.buildWithdrawTx(this._address, withdrawAmount, params.asset);
|
|
3988
|
+
const result = await this.client.signAndExecuteTransaction({
|
|
3989
|
+
signer: this.keypair,
|
|
3990
|
+
transaction: tx,
|
|
3991
|
+
options: { showEffects: true }
|
|
3992
|
+
});
|
|
3993
|
+
await this.client.waitForTransaction({ digest: result.digest });
|
|
3994
|
+
const gasCost = result.effects?.gasUsed ? Math.abs(
|
|
3995
|
+
(Number(result.effects.gasUsed.computationCost) + Number(result.effects.gasUsed.storageCost) - Number(result.effects.gasUsed.storageRebate)) / 1e9
|
|
3996
|
+
) : 0;
|
|
3997
|
+
const protocolName = adapter.name;
|
|
3998
|
+
this.portfolio.recordUnearn(params.asset);
|
|
3999
|
+
return {
|
|
4000
|
+
success: true,
|
|
4001
|
+
tx: result.digest,
|
|
4002
|
+
asset: params.asset,
|
|
4003
|
+
amount: effectiveAmount,
|
|
4004
|
+
protocol: protocolName,
|
|
4005
|
+
apy: 0,
|
|
4006
|
+
gasCost,
|
|
4007
|
+
gasMethod: "self-funded"
|
|
4008
|
+
};
|
|
4009
|
+
}
|
|
3843
4010
|
async getPortfolio() {
|
|
3844
4011
|
const positions = this.portfolio.getPositions();
|
|
3845
4012
|
const realizedPnL = this.portfolio.getRealizedPnL();
|
|
@@ -3889,7 +4056,10 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
|
|
|
3889
4056
|
currentValue,
|
|
3890
4057
|
unrealizedPnL,
|
|
3891
4058
|
unrealizedPnLPct,
|
|
3892
|
-
trades: pos.trades
|
|
4059
|
+
trades: pos.trades,
|
|
4060
|
+
earning: pos.earning,
|
|
4061
|
+
earningProtocol: pos.earningProtocol,
|
|
4062
|
+
earningApy: pos.earningApy
|
|
3893
4063
|
});
|
|
3894
4064
|
}
|
|
3895
4065
|
const totalInvested = enriched.reduce((sum, p) => sum + p.costBasis, 0);
|
|
@@ -3954,8 +4124,11 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
|
|
|
3954
4124
|
this.registry.allPositions(this._address),
|
|
3955
4125
|
this.registry.allRatesAcrossAssets()
|
|
3956
4126
|
]);
|
|
4127
|
+
const earningAssets = new Set(
|
|
4128
|
+
this.portfolio.getPositions().filter((p) => p.earning).map((p) => p.asset)
|
|
4129
|
+
);
|
|
3957
4130
|
const savePositions = allPositions.flatMap(
|
|
3958
|
-
(p) => p.positions.supplies.filter((s) => s.amount > 0.01).map((s) => ({
|
|
4131
|
+
(p) => p.positions.supplies.filter((s) => s.amount > 0.01).filter((s) => !earningAssets.has(s.asset)).map((s) => ({
|
|
3959
4132
|
protocolId: p.protocolId,
|
|
3960
4133
|
protocol: p.protocol,
|
|
3961
4134
|
asset: s.asset,
|