@t2000/sdk 0.19.24 → 0.20.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 +34 -150
- package/dist/adapters/descriptors.cjs +1 -42
- package/dist/adapters/descriptors.cjs.map +1 -1
- package/dist/adapters/descriptors.d.cts +1 -1
- package/dist/adapters/descriptors.d.ts +1 -1
- package/dist/adapters/descriptors.js +2 -41
- package/dist/adapters/descriptors.js.map +1 -1
- package/dist/adapters/index.cjs +3 -745
- package/dist/adapters/index.cjs.map +1 -1
- package/dist/adapters/index.d.cts +4 -171
- package/dist/adapters/index.d.ts +4 -171
- package/dist/adapters/index.js +4 -742
- package/dist/adapters/index.js.map +1 -1
- package/dist/browser.js +20 -178
- package/dist/browser.js.map +1 -1
- package/dist/descriptors-Be4FAgN5.d.cts +127 -0
- package/dist/descriptors-Be4FAgN5.d.ts +127 -0
- package/dist/index-D1DxZ1DK.d.ts +292 -0
- package/dist/index-MP_J_nSO.d.cts +292 -0
- package/dist/index.cjs +680 -3461
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -313
- package/dist/index.d.ts +34 -313
- package/dist/index.js +592 -3360
- package/dist/index.js.map +1 -1
- package/package.json +5 -8
- package/dist/descriptors-B6qt_mwi.d.cts +0 -584
- package/dist/descriptors-B6qt_mwi.d.ts +0 -584
package/dist/index.js
CHANGED
|
@@ -1,159 +1,36 @@
|
|
|
1
|
-
import { EventEmitter } from 'eventemitter3';
|
|
2
1
|
import { Transaction } from '@mysten/sui/transactions';
|
|
2
|
+
import { AggregatorClient, Env } from '@cetusprotocol/aggregator-sdk';
|
|
3
|
+
import { EventEmitter } from 'eventemitter3';
|
|
3
4
|
import { SuiJsonRpcClient } from '@mysten/sui/jsonRpc';
|
|
4
|
-
import { normalizeSuiAddress, isValidSuiAddress
|
|
5
|
+
import { normalizeSuiAddress, isValidSuiAddress } from '@mysten/sui/utils';
|
|
5
6
|
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
|
|
6
7
|
import { decodeSuiPrivateKey } from '@mysten/sui/cryptography';
|
|
7
|
-
import { createHash,
|
|
8
|
+
import { createHash, randomBytes, createCipheriv, createDecipheriv, scryptSync } from 'crypto';
|
|
8
9
|
import { access, mkdir, writeFile, readFile } from 'fs/promises';
|
|
9
10
|
import { join, dirname, resolve } from 'path';
|
|
10
11
|
import { homedir } from 'os';
|
|
11
12
|
import { getPools, getLendingPositions, getHealthFactor as getHealthFactor$1, depositCoinPTB, withdrawCoinPTB, borrowCoinPTB, repayCoinPTB, getUserAvailableLendingRewards, summaryLendingRewards, claimLendingRewardsPTB, updateOraclePriceBeforeUserOperationPTB } from '@naviprotocol/lending';
|
|
12
|
-
import { AggregatorClient, Env } from '@cetusprotocol/aggregator-sdk';
|
|
13
|
-
import { SuilendClient, LENDING_MARKET_ID, LENDING_MARKET_TYPE } from '@suilend/sdk/client';
|
|
14
|
-
import { initializeSuilend, initializeObligations } from '@suilend/sdk/lib/initialize';
|
|
15
|
-
import { Side } from '@suilend/sdk/lib/types';
|
|
16
13
|
import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
var SUI_DECIMALS = 9;
|
|
23
|
-
var USDC_DECIMALS = 6;
|
|
24
|
-
var BPS_DENOMINATOR = 10000n;
|
|
25
|
-
var AUTO_TOPUP_THRESHOLD = 50000000n;
|
|
26
|
-
var GAS_RESERVE_TARGET = 150000000n;
|
|
27
|
-
var AUTO_TOPUP_AMOUNT = 1000000n;
|
|
28
|
-
var AUTO_TOPUP_MIN_USDC = 2000000n;
|
|
29
|
-
var SAVE_FEE_BPS = 10n;
|
|
30
|
-
var SWAP_FEE_BPS = 0n;
|
|
31
|
-
var BORROW_FEE_BPS = 5n;
|
|
32
|
-
var CLOCK_ID = "0x6";
|
|
33
|
-
var SUPPORTED_ASSETS = {
|
|
34
|
-
USDC: {
|
|
35
|
-
type: "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC",
|
|
36
|
-
decimals: 6,
|
|
37
|
-
symbol: "USDC",
|
|
38
|
-
displayName: "USDC"
|
|
39
|
-
},
|
|
40
|
-
USDT: {
|
|
41
|
-
type: "0x375f70cf2ae4c00bf37117d0c85a2c71545e6ee05c4a5c7d282cd66a4504b068::usdt::USDT",
|
|
42
|
-
decimals: 6,
|
|
43
|
-
symbol: "USDT",
|
|
44
|
-
displayName: "suiUSDT"
|
|
45
|
-
},
|
|
46
|
-
USDe: {
|
|
47
|
-
type: "0x41d587e5336f1c86cad50d38a7136db99333bb9bda91cea4ba69115defeb1402::sui_usde::SUI_USDE",
|
|
48
|
-
decimals: 6,
|
|
49
|
-
symbol: "USDe",
|
|
50
|
-
displayName: "suiUSDe"
|
|
51
|
-
},
|
|
52
|
-
USDsui: {
|
|
53
|
-
type: "0x44f838219cf67b058f3b37907b655f226153c18e33dfcd0da559a844fea9b1c1::usdsui::USDSUI",
|
|
54
|
-
decimals: 6,
|
|
55
|
-
symbol: "USDsui",
|
|
56
|
-
displayName: "USDsui"
|
|
57
|
-
},
|
|
58
|
-
SUI: {
|
|
59
|
-
type: "0x2::sui::SUI",
|
|
60
|
-
decimals: 9,
|
|
61
|
-
symbol: "SUI",
|
|
62
|
-
displayName: "SUI"
|
|
63
|
-
},
|
|
64
|
-
BTC: {
|
|
65
|
-
type: "0x0041f9f9344cac094454cd574e333c4fdb132d7bcc9379bcd4aab485b2a63942::wbtc::WBTC",
|
|
66
|
-
decimals: 8,
|
|
67
|
-
symbol: "BTC",
|
|
68
|
-
displayName: "Bitcoin"
|
|
69
|
-
},
|
|
70
|
-
ETH: {
|
|
71
|
-
type: "0xd0e89b2af5e4910726fbcd8b8dd37bb79b29e5f83f7491bca830e94f7f226d29::eth::ETH",
|
|
72
|
-
decimals: 8,
|
|
73
|
-
symbol: "ETH",
|
|
74
|
-
displayName: "Ethereum"
|
|
75
|
-
},
|
|
76
|
-
GOLD: {
|
|
77
|
-
type: "0x9d297676e7a4b771ab023291377b2adfaa4938fb9080b8d12430e4b108b836a9::xaum::XAUM",
|
|
78
|
-
decimals: 9,
|
|
79
|
-
symbol: "GOLD",
|
|
80
|
-
displayName: "Gold"
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
var STABLE_ASSETS = ["USDC", "USDT", "USDe", "USDsui"];
|
|
84
|
-
var T2000_PACKAGE_ID = process.env.T2000_PACKAGE_ID ?? "0xab92e9f1fe549ad3d6a52924a73181b45791e76120b975138fac9ec9b75db9f3";
|
|
85
|
-
var T2000_CONFIG_ID = process.env.T2000_CONFIG_ID ?? "0x408add9aa9322f93cfd87523d8f603006eb8713894f4c460283c58a6888dae8a";
|
|
86
|
-
var T2000_TREASURY_ID = process.env.T2000_TREASURY_ID ?? "0x3bb501b8300125dca59019247941a42af6b292a150ce3cfcce9449456be2ec91";
|
|
87
|
-
var DEFAULT_NETWORK = "mainnet";
|
|
88
|
-
var DEFAULT_RPC_URL = "https://fullnode.mainnet.sui.io:443";
|
|
89
|
-
var DEFAULT_KEY_PATH = "~/.t2000/wallet.key";
|
|
90
|
-
var API_BASE_URL = process.env.T2000_API_URL ?? "https://api.t2000.ai";
|
|
91
|
-
var CETUS_USDC_SUI_POOL = "0x51e883ba7c0b566a26cbc8a94cd33eb0abd418a77cc1e60ad22fd9b1f29cd2ab";
|
|
92
|
-
var CETUS_PACKAGE = "0x1eabed72c53feb3805120a081dc15963c204dc8d091542592abaf7a35689b2fb";
|
|
93
|
-
var INVESTMENT_ASSETS = {
|
|
94
|
-
SUI: SUPPORTED_ASSETS.SUI,
|
|
95
|
-
BTC: SUPPORTED_ASSETS.BTC,
|
|
96
|
-
ETH: SUPPORTED_ASSETS.ETH,
|
|
97
|
-
GOLD: SUPPORTED_ASSETS.GOLD
|
|
15
|
+
var __defProp = Object.defineProperty;
|
|
16
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
17
|
+
var __esm = (fn, res) => function __init() {
|
|
18
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
98
19
|
};
|
|
99
|
-
var
|
|
100
|
-
|
|
101
|
-
name:
|
|
102
|
-
allocations: { BTC: 50, ETH: 30, SUI: 20 },
|
|
103
|
-
description: "Large-cap crypto index",
|
|
104
|
-
custom: false
|
|
105
|
-
},
|
|
106
|
-
layer1: {
|
|
107
|
-
name: "Smart Contract Platforms",
|
|
108
|
-
allocations: { ETH: 50, SUI: 50 },
|
|
109
|
-
description: "Smart contract platforms",
|
|
110
|
-
custom: false
|
|
111
|
-
},
|
|
112
|
-
"sui-heavy": {
|
|
113
|
-
name: "Sui-Weighted Portfolio",
|
|
114
|
-
allocations: { BTC: 20, ETH: 20, SUI: 60 },
|
|
115
|
-
description: "Sui-weighted portfolio",
|
|
116
|
-
custom: false
|
|
117
|
-
},
|
|
118
|
-
"all-weather": {
|
|
119
|
-
name: "All-Weather Portfolio",
|
|
120
|
-
allocations: { BTC: 30, ETH: 20, SUI: 20, GOLD: 30 },
|
|
121
|
-
description: "Crypto and commodities",
|
|
122
|
-
custom: false
|
|
123
|
-
},
|
|
124
|
-
"safe-haven": {
|
|
125
|
-
name: "Safe Haven",
|
|
126
|
-
allocations: { BTC: 50, GOLD: 50 },
|
|
127
|
-
description: "Store-of-value assets",
|
|
128
|
-
custom: false
|
|
129
|
-
}
|
|
20
|
+
var __export = (target, all) => {
|
|
21
|
+
for (var name in all)
|
|
22
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
130
23
|
};
|
|
131
|
-
var PERPS_MARKETS = ["SUI-PERP"];
|
|
132
|
-
var DEFAULT_MAX_LEVERAGE = 5;
|
|
133
|
-
var DEFAULT_MAX_POSITION_SIZE = 1e3;
|
|
134
|
-
var GAS_RESERVE_MIN = 0.05;
|
|
135
24
|
|
|
136
25
|
// src/errors.ts
|
|
137
|
-
var
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
this.data = data;
|
|
146
|
-
this.retryable = retryable;
|
|
147
|
-
}
|
|
148
|
-
toJSON() {
|
|
149
|
-
return {
|
|
150
|
-
error: this.code,
|
|
151
|
-
message: this.message,
|
|
152
|
-
...this.data && { data: this.data },
|
|
153
|
-
retryable: this.retryable
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
};
|
|
26
|
+
var errors_exports = {};
|
|
27
|
+
__export(errors_exports, {
|
|
28
|
+
T2000Error: () => T2000Error,
|
|
29
|
+
isMoveAbort: () => isMoveAbort,
|
|
30
|
+
mapMoveAbortCode: () => mapMoveAbortCode,
|
|
31
|
+
mapWalletError: () => mapWalletError,
|
|
32
|
+
parseMoveAbortMessage: () => parseMoveAbortMessage
|
|
33
|
+
});
|
|
157
34
|
function mapWalletError(error) {
|
|
158
35
|
const msg = error instanceof Error ? error.message : String(error);
|
|
159
36
|
if (msg.includes("rejected") || msg.includes("cancelled")) {
|
|
@@ -182,9 +59,7 @@ function mapMoveAbortCode(code) {
|
|
|
182
59
|
1600: "Health factor too low \u2014 withdrawal would risk liquidation",
|
|
183
60
|
1605: "Asset borrowing is disabled or at capacity on this protocol",
|
|
184
61
|
// NAVI utils abort codes
|
|
185
|
-
46e3: "Insufficient balance to repay \u2014 withdraw some savings first to get cash"
|
|
186
|
-
// Cetus DEX abort codes
|
|
187
|
-
46001: "Swap failed \u2014 the DEX pool rejected the trade (liquidity or routing issue). Try again."
|
|
62
|
+
46e3: "Insufficient balance to repay \u2014 withdraw some savings first to get cash"
|
|
188
63
|
};
|
|
189
64
|
return abortMessages[code] ?? `Move abort code: ${code}`;
|
|
190
65
|
}
|
|
@@ -200,7 +75,7 @@ function parseMoveAbortMessage(msg) {
|
|
|
200
75
|
const context = `${moduleMatch?.[1] ?? ""}${fnMatch ? `::${fnMatch[1]}` : ""}`.toLowerCase();
|
|
201
76
|
const suffix = moduleMatch ? ` [${moduleMatch[1]}${fnMatch ? `::${fnMatch[1]}` : ""}]` : "";
|
|
202
77
|
if (context.includes("slippage")) {
|
|
203
|
-
return `
|
|
78
|
+
return `Slippage too high \u2014 price moved during execution${suffix}`;
|
|
204
79
|
}
|
|
205
80
|
if (context.includes("balance::split") || context.includes("balance::ENotEnough")) {
|
|
206
81
|
return `Insufficient on-chain balance${suffix}`;
|
|
@@ -210,8 +85,296 @@ function parseMoveAbortMessage(msg) {
|
|
|
210
85
|
}
|
|
211
86
|
return msg;
|
|
212
87
|
}
|
|
88
|
+
var T2000Error;
|
|
89
|
+
var init_errors = __esm({
|
|
90
|
+
"src/errors.ts"() {
|
|
91
|
+
T2000Error = class extends Error {
|
|
92
|
+
code;
|
|
93
|
+
data;
|
|
94
|
+
retryable;
|
|
95
|
+
constructor(code, message, data, retryable = false) {
|
|
96
|
+
super(message);
|
|
97
|
+
this.name = "T2000Error";
|
|
98
|
+
this.code = code;
|
|
99
|
+
this.data = data;
|
|
100
|
+
this.retryable = retryable;
|
|
101
|
+
}
|
|
102
|
+
toJSON() {
|
|
103
|
+
return {
|
|
104
|
+
error: this.code,
|
|
105
|
+
message: this.message,
|
|
106
|
+
...this.data && { data: this.data },
|
|
107
|
+
retryable: this.retryable
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// src/protocols/volo.ts
|
|
115
|
+
var volo_exports = {};
|
|
116
|
+
__export(volo_exports, {
|
|
117
|
+
MIN_STAKE_MIST: () => MIN_STAKE_MIST,
|
|
118
|
+
SUI_SYSTEM_STATE: () => SUI_SYSTEM_STATE,
|
|
119
|
+
VOLO_METADATA: () => VOLO_METADATA,
|
|
120
|
+
VOLO_PKG: () => VOLO_PKG,
|
|
121
|
+
VOLO_POOL: () => VOLO_POOL,
|
|
122
|
+
VSUI_TYPE: () => VSUI_TYPE,
|
|
123
|
+
buildStakeVSuiTx: () => buildStakeVSuiTx,
|
|
124
|
+
buildUnstakeVSuiTx: () => buildUnstakeVSuiTx,
|
|
125
|
+
getVoloStats: () => getVoloStats
|
|
126
|
+
});
|
|
127
|
+
async function getVoloStats() {
|
|
128
|
+
const res = await fetch(VOLO_STATS_URL, {
|
|
129
|
+
signal: AbortSignal.timeout(8e3)
|
|
130
|
+
});
|
|
131
|
+
if (!res.ok) {
|
|
132
|
+
throw new Error(`VOLO stats API error: HTTP ${res.status}`);
|
|
133
|
+
}
|
|
134
|
+
const data = await res.json();
|
|
135
|
+
const d = data.data ?? data;
|
|
136
|
+
return {
|
|
137
|
+
apy: d.apy ?? 0,
|
|
138
|
+
exchangeRate: d.exchange_rate ?? d.exchangeRate ?? 1,
|
|
139
|
+
tvl: d.tvl ?? 0
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
async function buildStakeVSuiTx(_client, address, amountMist) {
|
|
143
|
+
if (amountMist < MIN_STAKE_MIST) {
|
|
144
|
+
throw new Error(`Minimum stake is 1 SUI (${MIN_STAKE_MIST} MIST). Got: ${amountMist}`);
|
|
145
|
+
}
|
|
146
|
+
const tx = new Transaction();
|
|
147
|
+
tx.setSender(address);
|
|
148
|
+
const [suiCoin] = tx.splitCoins(tx.gas, [amountMist]);
|
|
149
|
+
const [vSuiCoin] = tx.moveCall({
|
|
150
|
+
target: `${VOLO_PKG}::stake_pool::stake`,
|
|
151
|
+
arguments: [
|
|
152
|
+
tx.object(VOLO_POOL),
|
|
153
|
+
tx.object(VOLO_METADATA),
|
|
154
|
+
tx.object(SUI_SYSTEM_STATE),
|
|
155
|
+
suiCoin
|
|
156
|
+
]
|
|
157
|
+
});
|
|
158
|
+
tx.transferObjects([vSuiCoin], address);
|
|
159
|
+
return tx;
|
|
160
|
+
}
|
|
161
|
+
async function buildUnstakeVSuiTx(client, address, amountMist) {
|
|
162
|
+
const coins = await fetchVSuiCoins(client, address);
|
|
163
|
+
if (coins.length === 0) {
|
|
164
|
+
throw new Error("No vSUI found in wallet.");
|
|
165
|
+
}
|
|
166
|
+
const tx = new Transaction();
|
|
167
|
+
tx.setSender(address);
|
|
168
|
+
const primary = tx.object(coins[0].coinObjectId);
|
|
169
|
+
if (coins.length > 1) {
|
|
170
|
+
tx.mergeCoins(primary, coins.slice(1).map((c) => tx.object(c.coinObjectId)));
|
|
171
|
+
}
|
|
172
|
+
let vSuiCoin;
|
|
173
|
+
if (amountMist === "all") {
|
|
174
|
+
vSuiCoin = primary;
|
|
175
|
+
} else {
|
|
176
|
+
[vSuiCoin] = tx.splitCoins(primary, [amountMist]);
|
|
177
|
+
}
|
|
178
|
+
const [suiCoin] = tx.moveCall({
|
|
179
|
+
target: `${VOLO_PKG}::stake_pool::unstake`,
|
|
180
|
+
arguments: [
|
|
181
|
+
tx.object(VOLO_POOL),
|
|
182
|
+
tx.object(VOLO_METADATA),
|
|
183
|
+
tx.object(SUI_SYSTEM_STATE),
|
|
184
|
+
vSuiCoin
|
|
185
|
+
]
|
|
186
|
+
});
|
|
187
|
+
tx.transferObjects([suiCoin], address);
|
|
188
|
+
return tx;
|
|
189
|
+
}
|
|
190
|
+
async function fetchVSuiCoins(client, address) {
|
|
191
|
+
const all = [];
|
|
192
|
+
let cursor;
|
|
193
|
+
let hasNext = true;
|
|
194
|
+
while (hasNext) {
|
|
195
|
+
const page = await client.getCoins({
|
|
196
|
+
owner: address,
|
|
197
|
+
coinType: VSUI_TYPE,
|
|
198
|
+
cursor: cursor ?? void 0
|
|
199
|
+
});
|
|
200
|
+
all.push(...page.data.map((c) => ({ coinObjectId: c.coinObjectId, balance: c.balance })));
|
|
201
|
+
cursor = page.nextCursor;
|
|
202
|
+
hasNext = page.hasNextPage;
|
|
203
|
+
}
|
|
204
|
+
return all;
|
|
205
|
+
}
|
|
206
|
+
var VOLO_PKG, VOLO_POOL, VOLO_METADATA, VSUI_TYPE, SUI_SYSTEM_STATE, MIN_STAKE_MIST, VOLO_STATS_URL;
|
|
207
|
+
var init_volo = __esm({
|
|
208
|
+
"src/protocols/volo.ts"() {
|
|
209
|
+
VOLO_PKG = "0x68d22cf8bdbcd11ecba1e094922873e4080d4d11133e2443fddda0bfd11dae20";
|
|
210
|
+
VOLO_POOL = "0x2d914e23d82fedef1b5f56a32d5c64bdcc3087ccfea2b4d6ea51a71f587840e5";
|
|
211
|
+
VOLO_METADATA = "0x680cd26af32b2bde8d3361e804c53ec1d1cfe24c7f039eb7f549e8dfde389a60";
|
|
212
|
+
VSUI_TYPE = "0x549e8b69270defbfafd4f94e17ec44cdbdd99820b33bda2278dea3b9a32d3f55::cert::CERT";
|
|
213
|
+
SUI_SYSTEM_STATE = "0x05";
|
|
214
|
+
MIN_STAKE_MIST = 1000000000n;
|
|
215
|
+
VOLO_STATS_URL = "https://open-api.naviprotocol.io/api/volo/stats";
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// src/protocols/cetus-swap.ts
|
|
220
|
+
var cetus_swap_exports = {};
|
|
221
|
+
__export(cetus_swap_exports, {
|
|
222
|
+
TOKEN_MAP: () => TOKEN_MAP,
|
|
223
|
+
buildSwapTx: () => buildSwapTx,
|
|
224
|
+
findSwapRoute: () => findSwapRoute,
|
|
225
|
+
resolveTokenType: () => resolveTokenType,
|
|
226
|
+
simulateSwap: () => simulateSwap
|
|
227
|
+
});
|
|
228
|
+
function getClient(walletAddress) {
|
|
229
|
+
if (clientInstance) return clientInstance;
|
|
230
|
+
clientInstance = new AggregatorClient({
|
|
231
|
+
signer: walletAddress,
|
|
232
|
+
env: Env.Mainnet
|
|
233
|
+
});
|
|
234
|
+
return clientInstance;
|
|
235
|
+
}
|
|
236
|
+
async function findSwapRoute(params) {
|
|
237
|
+
const client = getClient(params.walletAddress);
|
|
238
|
+
const findParams = {
|
|
239
|
+
from: params.from,
|
|
240
|
+
target: params.to,
|
|
241
|
+
amount: params.amount.toString(),
|
|
242
|
+
byAmountIn: params.byAmountIn
|
|
243
|
+
};
|
|
244
|
+
const routerData = await client.findRouters(findParams);
|
|
245
|
+
if (!routerData) return null;
|
|
246
|
+
if (routerData.insufficientLiquidity) {
|
|
247
|
+
return {
|
|
248
|
+
routerData,
|
|
249
|
+
amountIn: routerData.amountIn.toString(),
|
|
250
|
+
amountOut: routerData.amountOut.toString(),
|
|
251
|
+
byAmountIn: params.byAmountIn,
|
|
252
|
+
priceImpact: routerData.deviationRatio,
|
|
253
|
+
insufficientLiquidity: true
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (routerData.error) {
|
|
257
|
+
const { T2000Error: T2000Error2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
|
|
258
|
+
throw new T2000Error2("SWAP_FAILED", `Cetus routing error: ${routerData.error.msg} (code ${routerData.error.code})`);
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
routerData,
|
|
262
|
+
amountIn: routerData.amountIn.toString(),
|
|
263
|
+
amountOut: routerData.amountOut.toString(),
|
|
264
|
+
byAmountIn: params.byAmountIn,
|
|
265
|
+
priceImpact: routerData.deviationRatio,
|
|
266
|
+
insufficientLiquidity: false
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
async function buildSwapTx(params) {
|
|
270
|
+
const client = getClient(params.walletAddress);
|
|
271
|
+
const clampedSlippage = Math.max(1e-3, Math.min(params.slippage, 0.05));
|
|
272
|
+
const outputCoin = await client.routerSwap({
|
|
273
|
+
router: params.route.routerData,
|
|
274
|
+
inputCoin: params.inputCoin,
|
|
275
|
+
slippage: clampedSlippage,
|
|
276
|
+
txb: params.tx
|
|
277
|
+
});
|
|
278
|
+
return outputCoin;
|
|
279
|
+
}
|
|
280
|
+
async function simulateSwap(params) {
|
|
281
|
+
const client = getClient(params.walletAddress);
|
|
282
|
+
try {
|
|
283
|
+
await client.devInspectTransactionBlock(params.tx);
|
|
284
|
+
return { success: true };
|
|
285
|
+
} catch (err) {
|
|
286
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function resolveTokenType(nameOrType) {
|
|
290
|
+
if (nameOrType.includes("::")) return nameOrType;
|
|
291
|
+
return TOKEN_MAP[nameOrType.toUpperCase()] ?? null;
|
|
292
|
+
}
|
|
293
|
+
var clientInstance, TOKEN_MAP;
|
|
294
|
+
var init_cetus_swap = __esm({
|
|
295
|
+
"src/protocols/cetus-swap.ts"() {
|
|
296
|
+
clientInstance = null;
|
|
297
|
+
TOKEN_MAP = {
|
|
298
|
+
SUI: "0x2::sui::SUI",
|
|
299
|
+
USDC: "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC",
|
|
300
|
+
USDT: "0x375f70cf2ae4c00bf37117d0c85a2c71545e6ee05c4a5c7d282cd66a4504b068::usdt::USDT",
|
|
301
|
+
CETUS: "0x06864a6f921804860930db6ddbe2e16acdf8504495ea7481637a1c8b9a8fe54b::cetus::CETUS",
|
|
302
|
+
DEEP: "0xdeeb7a4662eec9f2f3def03fb937a663dddaa2e215b8078a284d026b7946c270::deep::DEEP",
|
|
303
|
+
NAVX: "0xa99b8952d4f7d947ea77fe0ecdcc9e5fc0bcab2841d6e2a5aa00c3044e5544b5::navx::NAVX",
|
|
304
|
+
vSUI: "0x549e8b69270defbfafd4f94e17ec44cdbdd99820b33bda2278dea3b9a32d3f55::cert::CERT",
|
|
305
|
+
haSUI: "0xbde4ba4c2e274a60ce15c1cfff9e5c42e136a8bc::hasui::HASUI",
|
|
306
|
+
afSUI: "0xf325ce1300e8dac124071d3152c5c5ee6174914f8bc2161e88329cf579246efc::afsui::AFSUI",
|
|
307
|
+
WAL: "0x356a26eb9e012a68958082340d4c4116e7f55615cf27affcff209cf0ae544f59::wal::WAL",
|
|
308
|
+
ETH: "0xd0e89b2af5e4910726fbcd8b8dd37bb79b29e5f83f7491bca830e94f7f226d29::eth::ETH",
|
|
309
|
+
wBTC: "0x0041f9f9344cac094454cd574e333c4fdb132d7bcc9379bcd4aab485b2a63942::wbtc::WBTC",
|
|
310
|
+
FDUSD: "0xf16e6b723f242ec745dfd7634ad072c42d5c1d9ac9d62a39c381303eaa57693a::fdusd::FDUSD",
|
|
311
|
+
AUSD: "0x2053d08c1e2bd02791056171aab0fd12bd7cd7efad2ab8f6b9c8902f14df2ff2::ausd::AUSD",
|
|
312
|
+
BUCK: "0xce7ff77a83ea0cb6fd39bd8748e2ec89a3f41e8efdc3f4eb123e0ca37b184db2::buck::BUCK",
|
|
313
|
+
USDe: "0x41d587e5336f1c86cad50d38a7136db99333bb9bda91cea4ba69115defeb1402::sui_usde::SUI_USDE",
|
|
314
|
+
NS: "0x5145494a5f5100e645e4b0aa950fa6b68f614e8c59e17bc5ded3495123a79178::ns::NS",
|
|
315
|
+
BLUB: "0xfa7ac3951fdca12c1b6d18eb19e1aa2fbc31e4d45773c8e45b4ded3ef8d83f8a::blub::BLUB",
|
|
316
|
+
SCA: "0x7016aae72cfc67f2fadf55769c0a7dd54291a583b63051a5ed71081cce836ac6::sca::SCA",
|
|
317
|
+
TURBOS: "0x5d1f47ea69bb0de31c313d7acf89b890dbb8991ea8e03c6c355171f84bb1ba4a::turbos::TURBOS"
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// src/constants.ts
|
|
323
|
+
var MIST_PER_SUI = 1000000000n;
|
|
324
|
+
var SUI_DECIMALS = 9;
|
|
325
|
+
var USDC_DECIMALS = 6;
|
|
326
|
+
var BPS_DENOMINATOR = 10000n;
|
|
327
|
+
var AUTO_TOPUP_THRESHOLD = 50000000n;
|
|
328
|
+
var GAS_RESERVE_TARGET = 150000000n;
|
|
329
|
+
var AUTO_TOPUP_MIN_USDC = 2000000n;
|
|
330
|
+
var SAVE_FEE_BPS = 10n;
|
|
331
|
+
var BORROW_FEE_BPS = 5n;
|
|
332
|
+
var CLOCK_ID = "0x6";
|
|
333
|
+
var SUPPORTED_ASSETS = {
|
|
334
|
+
USDC: {
|
|
335
|
+
type: "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC",
|
|
336
|
+
decimals: 6,
|
|
337
|
+
symbol: "USDC",
|
|
338
|
+
displayName: "USDC"
|
|
339
|
+
},
|
|
340
|
+
USDT: {
|
|
341
|
+
type: "0x375f70cf2ae4c00bf37117d0c85a2c71545e6ee05c4a5c7d282cd66a4504b068::usdt::USDT",
|
|
342
|
+
decimals: 6,
|
|
343
|
+
symbol: "USDT",
|
|
344
|
+
displayName: "suiUSDT"
|
|
345
|
+
},
|
|
346
|
+
USDe: {
|
|
347
|
+
type: "0x41d587e5336f1c86cad50d38a7136db99333bb9bda91cea4ba69115defeb1402::sui_usde::SUI_USDE",
|
|
348
|
+
decimals: 6,
|
|
349
|
+
symbol: "USDe",
|
|
350
|
+
displayName: "suiUSDe"
|
|
351
|
+
},
|
|
352
|
+
USDsui: {
|
|
353
|
+
type: "0x44f838219cf67b058f3b37907b655f226153c18e33dfcd0da559a844fea9b1c1::usdsui::USDSUI",
|
|
354
|
+
decimals: 6,
|
|
355
|
+
symbol: "USDsui",
|
|
356
|
+
displayName: "USDsui"
|
|
357
|
+
},
|
|
358
|
+
SUI: {
|
|
359
|
+
type: "0x2::sui::SUI",
|
|
360
|
+
decimals: 9,
|
|
361
|
+
symbol: "SUI",
|
|
362
|
+
displayName: "SUI"
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
var STABLE_ASSETS = ["USDC"];
|
|
366
|
+
var T2000_PACKAGE_ID = process.env.T2000_PACKAGE_ID ?? "0xab92e9f1fe549ad3d6a52924a73181b45791e76120b975138fac9ec9b75db9f3";
|
|
367
|
+
var T2000_CONFIG_ID = process.env.T2000_CONFIG_ID ?? "0x408add9aa9322f93cfd87523d8f603006eb8713894f4c460283c58a6888dae8a";
|
|
368
|
+
var T2000_TREASURY_ID = process.env.T2000_TREASURY_ID ?? "0x3bb501b8300125dca59019247941a42af6b292a150ce3cfcce9449456be2ec91";
|
|
369
|
+
var DEFAULT_NETWORK = "mainnet";
|
|
370
|
+
var DEFAULT_RPC_URL = "https://fullnode.mainnet.sui.io:443";
|
|
371
|
+
var DEFAULT_KEY_PATH = "~/.t2000/wallet.key";
|
|
372
|
+
var API_BASE_URL = process.env.T2000_API_URL ?? "https://api.t2000.ai";
|
|
373
|
+
var CETUS_USDC_SUI_POOL = "0x51e883ba7c0b566a26cbc8a94cd33eb0abd418a77cc1e60ad22fd9b1f29cd2ab";
|
|
374
|
+
var GAS_RESERVE_MIN = 0.05;
|
|
213
375
|
|
|
214
376
|
// src/utils/sui.ts
|
|
377
|
+
init_errors();
|
|
215
378
|
var cachedClient = null;
|
|
216
379
|
function getSuiClient(rpcUrl) {
|
|
217
380
|
const url = rpcUrl ?? DEFAULT_RPC_URL;
|
|
@@ -230,6 +393,9 @@ function truncateAddress(address) {
|
|
|
230
393
|
if (address.length <= 10) return address;
|
|
231
394
|
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
232
395
|
}
|
|
396
|
+
|
|
397
|
+
// src/wallet/keyManager.ts
|
|
398
|
+
init_errors();
|
|
233
399
|
var ALGORITHM = "aes-256-gcm";
|
|
234
400
|
var SCRYPT_N = 2 ** 14;
|
|
235
401
|
var SCRYPT_R = 8;
|
|
@@ -371,6 +537,7 @@ var ZkLoginSigner = class {
|
|
|
371
537
|
return currentEpoch >= this.maxEpoch;
|
|
372
538
|
}
|
|
373
539
|
};
|
|
540
|
+
init_errors();
|
|
374
541
|
|
|
375
542
|
// src/utils/format.ts
|
|
376
543
|
function mistToSui(mist) {
|
|
@@ -496,18 +663,11 @@ async function queryBalance(client, address) {
|
|
|
496
663
|
const stableBalancePromises = STABLE_ASSETS.map(
|
|
497
664
|
(asset) => client.getBalance({ owner: address, coinType: SUPPORTED_ASSETS[asset].type }).then((b) => ({ asset, amount: Number(b.totalBalance) / 10 ** SUPPORTED_ASSETS[asset].decimals })).catch(() => ({ asset, amount: 0 }))
|
|
498
665
|
);
|
|
499
|
-
const
|
|
500
|
-
const investBalancePromises = nonSuiInvestmentAssets.map(
|
|
501
|
-
(asset) => client.getBalance({ owner: address, coinType: INVESTMENT_ASSETS[asset].type }).then((b) => ({ asset, amount: Number(b.totalBalance) / 10 ** INVESTMENT_ASSETS[asset].decimals })).catch(() => ({ asset, amount: 0 }))
|
|
502
|
-
);
|
|
503
|
-
const [suiBalance, suiPriceUsd, ...rest] = await Promise.all([
|
|
666
|
+
const [suiBalance, suiPriceUsd, ...stableResults] = await Promise.all([
|
|
504
667
|
client.getBalance({ owner: address, coinType: SUPPORTED_ASSETS.SUI.type }),
|
|
505
668
|
fetchSuiPrice(client),
|
|
506
|
-
...stableBalancePromises
|
|
507
|
-
...investBalancePromises
|
|
669
|
+
...stableBalancePromises
|
|
508
670
|
]);
|
|
509
|
-
const stableResults = rest.slice(0, STABLE_ASSETS.length);
|
|
510
|
-
const investResults = rest.slice(STABLE_ASSETS.length);
|
|
511
671
|
const stables = {};
|
|
512
672
|
let totalStables = 0;
|
|
513
673
|
for (const { asset, amount } of stableResults) {
|
|
@@ -518,27 +678,17 @@ async function queryBalance(client, address) {
|
|
|
518
678
|
const savings = 0;
|
|
519
679
|
const usdEquiv = suiAmount * suiPriceUsd;
|
|
520
680
|
const total = totalStables + savings + usdEquiv;
|
|
521
|
-
const assets = {
|
|
522
|
-
USDC: stables.USDC ?? 0,
|
|
523
|
-
SUI: suiAmount
|
|
524
|
-
};
|
|
525
|
-
for (const { asset, amount } of investResults) {
|
|
526
|
-
assets[asset] = amount;
|
|
527
|
-
}
|
|
528
681
|
return {
|
|
529
682
|
available: totalStables,
|
|
530
683
|
savings,
|
|
531
684
|
debt: 0,
|
|
532
|
-
investment: 0,
|
|
533
|
-
investmentPnL: 0,
|
|
534
685
|
pendingRewards: 0,
|
|
535
686
|
gasReserve: {
|
|
536
687
|
sui: suiAmount,
|
|
537
688
|
usdEquiv
|
|
538
689
|
},
|
|
539
690
|
total,
|
|
540
|
-
stables
|
|
541
|
-
assets
|
|
691
|
+
stables
|
|
542
692
|
};
|
|
543
693
|
}
|
|
544
694
|
|
|
@@ -645,12 +795,10 @@ function classifyAction(targets, commandTypes) {
|
|
|
645
795
|
// src/protocols/protocolFee.ts
|
|
646
796
|
var FEE_RATES = {
|
|
647
797
|
save: SAVE_FEE_BPS,
|
|
648
|
-
swap: SWAP_FEE_BPS,
|
|
649
798
|
borrow: BORROW_FEE_BPS
|
|
650
799
|
};
|
|
651
800
|
var OP_CODES = {
|
|
652
801
|
save: 0,
|
|
653
|
-
swap: 1,
|
|
654
802
|
borrow: 2
|
|
655
803
|
};
|
|
656
804
|
function calculateFee(operation, amount) {
|
|
@@ -694,6 +842,7 @@ async function reportFee(agentAddress, operation, feeAmount, feeRate, txDigest)
|
|
|
694
842
|
} catch {
|
|
695
843
|
}
|
|
696
844
|
}
|
|
845
|
+
init_errors();
|
|
697
846
|
var MIN_HEALTH_FACTOR = 1.5;
|
|
698
847
|
var NAVI_SUPPORTED_ASSETS = [...STABLE_ASSETS, "SUI", "ETH", "GOLD"];
|
|
699
848
|
function sdkOptions(client) {
|
|
@@ -1127,15 +1276,12 @@ async function getFundStatus(client, address) {
|
|
|
1127
1276
|
}
|
|
1128
1277
|
|
|
1129
1278
|
// src/adapters/registry.ts
|
|
1279
|
+
init_errors();
|
|
1130
1280
|
var ProtocolRegistry = class {
|
|
1131
1281
|
lending = /* @__PURE__ */ new Map();
|
|
1132
|
-
swap = /* @__PURE__ */ new Map();
|
|
1133
1282
|
registerLending(adapter) {
|
|
1134
1283
|
this.lending.set(adapter.id, adapter);
|
|
1135
1284
|
}
|
|
1136
|
-
registerSwap(adapter) {
|
|
1137
|
-
this.swap.set(adapter.id, adapter);
|
|
1138
|
-
}
|
|
1139
1285
|
async bestSaveRate(asset) {
|
|
1140
1286
|
const candidates = [];
|
|
1141
1287
|
for (const adapter of this.lending.values()) {
|
|
@@ -1171,23 +1317,6 @@ var ProtocolRegistry = class {
|
|
|
1171
1317
|
candidates.sort((a, b) => a.rate.borrowApy - b.rate.borrowApy);
|
|
1172
1318
|
return candidates[0];
|
|
1173
1319
|
}
|
|
1174
|
-
async bestSwapQuote(from, to, amount) {
|
|
1175
|
-
const candidates = [];
|
|
1176
|
-
for (const adapter of this.swap.values()) {
|
|
1177
|
-
const pairs = adapter.getSupportedPairs();
|
|
1178
|
-
if (!pairs.some((p) => p.from === from && p.to === to)) continue;
|
|
1179
|
-
try {
|
|
1180
|
-
const quote = await adapter.getQuote(from, to, amount);
|
|
1181
|
-
candidates.push({ adapter, quote });
|
|
1182
|
-
} catch {
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
if (candidates.length === 0) {
|
|
1186
|
-
throw new T2000Error("ASSET_NOT_SUPPORTED", `No swap adapter supports ${from} \u2192 ${to}`);
|
|
1187
|
-
}
|
|
1188
|
-
candidates.sort((a, b) => b.quote.expectedOutput - a.quote.expectedOutput);
|
|
1189
|
-
return candidates[0];
|
|
1190
|
-
}
|
|
1191
1320
|
async bestSaveRateAcrossAssets() {
|
|
1192
1321
|
const candidates = [];
|
|
1193
1322
|
for (const asset of STABLE_ASSETS) {
|
|
@@ -1209,9 +1338,8 @@ var ProtocolRegistry = class {
|
|
|
1209
1338
|
}
|
|
1210
1339
|
async allRatesAcrossAssets() {
|
|
1211
1340
|
const results = [];
|
|
1212
|
-
const allAssets = [...STABLE_ASSETS, ...Object.keys(INVESTMENT_ASSETS)];
|
|
1213
1341
|
const seen = /* @__PURE__ */ new Set();
|
|
1214
|
-
for (const asset of
|
|
1342
|
+
for (const asset of STABLE_ASSETS) {
|
|
1215
1343
|
if (seen.has(asset)) continue;
|
|
1216
1344
|
seen.add(asset);
|
|
1217
1345
|
for (const adapter of this.lending.values()) {
|
|
@@ -1260,19 +1388,15 @@ var ProtocolRegistry = class {
|
|
|
1260
1388
|
getLending(id) {
|
|
1261
1389
|
return this.lending.get(id);
|
|
1262
1390
|
}
|
|
1263
|
-
getSwap(id) {
|
|
1264
|
-
return this.swap.get(id);
|
|
1265
|
-
}
|
|
1266
1391
|
listLending() {
|
|
1267
1392
|
return [...this.lending.values()];
|
|
1268
1393
|
}
|
|
1269
|
-
listSwap() {
|
|
1270
|
-
return [...this.swap.values()];
|
|
1271
|
-
}
|
|
1272
1394
|
};
|
|
1273
1395
|
|
|
1396
|
+
// src/adapters/navi.ts
|
|
1397
|
+
init_errors();
|
|
1398
|
+
|
|
1274
1399
|
// src/adapters/descriptors.ts
|
|
1275
|
-
var SUILEND_PACKAGE = "0xf95b06141ed4a174f239417323bde3f209b972f5930d8521ea38a52aff3a6ddf";
|
|
1276
1400
|
var naviDescriptor = {
|
|
1277
1401
|
id: "navi",
|
|
1278
1402
|
name: "NAVI Protocol",
|
|
@@ -1289,39 +1413,8 @@ var naviDescriptor = {
|
|
|
1289
1413
|
"incentive_v3::repay": "repay"
|
|
1290
1414
|
}
|
|
1291
1415
|
};
|
|
1292
|
-
var suilendDescriptor = {
|
|
1293
|
-
id: "suilend",
|
|
1294
|
-
name: "Suilend",
|
|
1295
|
-
packages: [SUILEND_PACKAGE],
|
|
1296
|
-
actionMap: {
|
|
1297
|
-
"lending_market::deposit_liquidity_and_mint_ctokens": "save",
|
|
1298
|
-
"lending_market::deposit_ctokens_into_obligation": "save",
|
|
1299
|
-
"lending_market::create_obligation": "save",
|
|
1300
|
-
"lending_market::withdraw_ctokens": "withdraw",
|
|
1301
|
-
"lending_market::redeem_ctokens_and_withdraw_liquidity": "withdraw",
|
|
1302
|
-
"lending_market::redeem_ctokens_and_withdraw_liquidity_request": "withdraw",
|
|
1303
|
-
"lending_market::fulfill_liquidity_request": "withdraw",
|
|
1304
|
-
"lending_market::unstake_sui_from_staker": "withdraw",
|
|
1305
|
-
"lending_market::borrow": "borrow",
|
|
1306
|
-
"lending_market::repay": "repay"
|
|
1307
|
-
}
|
|
1308
|
-
};
|
|
1309
|
-
var cetusDescriptor = {
|
|
1310
|
-
id: "cetus",
|
|
1311
|
-
name: "Cetus DEX",
|
|
1312
|
-
packages: [CETUS_PACKAGE],
|
|
1313
|
-
actionMap: {
|
|
1314
|
-
"router::swap": "swap",
|
|
1315
|
-
"router::swap_ab_bc": "swap",
|
|
1316
|
-
"router::swap_ab_cb": "swap",
|
|
1317
|
-
"router::swap_ba_bc": "swap",
|
|
1318
|
-
"router::swap_ba_cb": "swap"
|
|
1319
|
-
}
|
|
1320
|
-
};
|
|
1321
1416
|
var allDescriptors = [
|
|
1322
|
-
naviDescriptor
|
|
1323
|
-
suilendDescriptor,
|
|
1324
|
-
cetusDescriptor
|
|
1417
|
+
naviDescriptor
|
|
1325
1418
|
];
|
|
1326
1419
|
|
|
1327
1420
|
// src/adapters/navi.ts
|
|
@@ -1405,712 +1498,52 @@ var NaviAdapter = class {
|
|
|
1405
1498
|
return addClaimRewardsToTx(tx, this.client, address);
|
|
1406
1499
|
}
|
|
1407
1500
|
};
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
env: Env.Mainnet
|
|
1414
|
-
});
|
|
1415
|
-
}
|
|
1416
|
-
async function buildSwapTx(params) {
|
|
1417
|
-
const { client, address, fromAsset, toAsset, amount, maxSlippageBps = DEFAULT_SLIPPAGE_BPS } = params;
|
|
1418
|
-
const fromInfo = SUPPORTED_ASSETS[fromAsset];
|
|
1419
|
-
const toInfo = SUPPORTED_ASSETS[toAsset];
|
|
1420
|
-
if (!fromInfo || !toInfo) {
|
|
1421
|
-
throw new T2000Error("ASSET_NOT_SUPPORTED", `Swap pair ${fromAsset}/${toAsset} is not supported`);
|
|
1422
|
-
}
|
|
1423
|
-
const rawAmount = BigInt(Math.floor(amount * 10 ** fromInfo.decimals));
|
|
1424
|
-
const aggClient = createAggregatorClient(client, address);
|
|
1425
|
-
const _origLog = console.log;
|
|
1426
|
-
console.log = () => {
|
|
1427
|
-
};
|
|
1428
|
-
let result;
|
|
1429
|
-
try {
|
|
1430
|
-
result = await aggClient.findRouters({
|
|
1431
|
-
from: fromInfo.type,
|
|
1432
|
-
target: toInfo.type,
|
|
1433
|
-
amount: rawAmount,
|
|
1434
|
-
byAmountIn: true
|
|
1435
|
-
});
|
|
1436
|
-
} finally {
|
|
1437
|
-
console.log = _origLog;
|
|
1438
|
-
}
|
|
1439
|
-
if (!result || result.insufficientLiquidity) {
|
|
1440
|
-
throw new T2000Error(
|
|
1441
|
-
"ASSET_NOT_SUPPORTED",
|
|
1442
|
-
`No swap route found for ${fromAsset} \u2192 ${toAsset}`
|
|
1443
|
-
);
|
|
1501
|
+
function hasLeadingZeroBits(hash, bits) {
|
|
1502
|
+
const fullBytes = Math.floor(bits / 8);
|
|
1503
|
+
const remainingBits = bits % 8;
|
|
1504
|
+
for (let i = 0; i < fullBytes; i++) {
|
|
1505
|
+
if (hash[i] !== 0) return false;
|
|
1444
1506
|
}
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
};
|
|
1449
|
-
try {
|
|
1450
|
-
await aggClient.fastRouterSwap({
|
|
1451
|
-
router: result,
|
|
1452
|
-
txb: tx,
|
|
1453
|
-
slippage
|
|
1454
|
-
});
|
|
1455
|
-
} finally {
|
|
1456
|
-
console.log = _origLog;
|
|
1507
|
+
if (remainingBits > 0) {
|
|
1508
|
+
const mask = 255 << 8 - remainingBits;
|
|
1509
|
+
if ((hash[fullBytes] & mask) !== 0) return false;
|
|
1457
1510
|
}
|
|
1458
|
-
|
|
1459
|
-
return {
|
|
1460
|
-
tx,
|
|
1461
|
-
estimatedOut,
|
|
1462
|
-
toDecimals: toInfo.decimals
|
|
1463
|
-
};
|
|
1511
|
+
return true;
|
|
1464
1512
|
}
|
|
1465
|
-
|
|
1466
|
-
const
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
const aggClient = createAggregatorClient(client, address);
|
|
1474
|
-
const _origLog = console.log;
|
|
1475
|
-
console.log = () => {
|
|
1476
|
-
};
|
|
1477
|
-
let result;
|
|
1478
|
-
try {
|
|
1479
|
-
result = await aggClient.findRouters({
|
|
1480
|
-
from: fromInfo.type,
|
|
1481
|
-
target: toInfo.type,
|
|
1482
|
-
amount: rawAmount,
|
|
1483
|
-
byAmountIn: true
|
|
1484
|
-
});
|
|
1485
|
-
} finally {
|
|
1486
|
-
console.log = _origLog;
|
|
1513
|
+
function solveHashcash(challenge) {
|
|
1514
|
+
const bits = parseInt(challenge.split(":")[1], 10);
|
|
1515
|
+
let counter = 0;
|
|
1516
|
+
while (true) {
|
|
1517
|
+
const stamp = `${challenge}${counter.toString(16)}`;
|
|
1518
|
+
const hash = createHash("sha256").update(stamp).digest();
|
|
1519
|
+
if (hasLeadingZeroBits(hash, bits)) return stamp;
|
|
1520
|
+
counter++;
|
|
1487
1521
|
}
|
|
1488
|
-
if (!result || result.insufficientLiquidity) {
|
|
1489
|
-
throw new T2000Error(
|
|
1490
|
-
"ASSET_NOT_SUPPORTED",
|
|
1491
|
-
`No swap route found for ${fromAsset} \u2192 ${toAsset}`
|
|
1492
|
-
);
|
|
1493
|
-
}
|
|
1494
|
-
const slippage = maxSlippageBps / 1e4;
|
|
1495
|
-
console.log = () => {
|
|
1496
|
-
};
|
|
1497
|
-
let outputCoin;
|
|
1498
|
-
try {
|
|
1499
|
-
outputCoin = await aggClient.routerSwap({
|
|
1500
|
-
router: result,
|
|
1501
|
-
txb: tx,
|
|
1502
|
-
inputCoin,
|
|
1503
|
-
slippage
|
|
1504
|
-
});
|
|
1505
|
-
} finally {
|
|
1506
|
-
console.log = _origLog;
|
|
1507
|
-
}
|
|
1508
|
-
const estimatedOut = Number(result.amountOut.toString());
|
|
1509
|
-
return {
|
|
1510
|
-
outputCoin,
|
|
1511
|
-
estimatedOut,
|
|
1512
|
-
toDecimals: toInfo.decimals
|
|
1513
|
-
};
|
|
1514
|
-
}
|
|
1515
|
-
async function buildRawSwapTx(params) {
|
|
1516
|
-
const { client, address, fromCoinType, toCoinType, amount, toDecimals, maxSlippageBps = 500 } = params;
|
|
1517
|
-
const aggClient = createAggregatorClient(client, address);
|
|
1518
|
-
const _origLog = console.log;
|
|
1519
|
-
console.log = () => {
|
|
1520
|
-
};
|
|
1521
|
-
let result;
|
|
1522
|
-
try {
|
|
1523
|
-
result = await aggClient.findRouters({
|
|
1524
|
-
from: fromCoinType,
|
|
1525
|
-
target: toCoinType,
|
|
1526
|
-
amount,
|
|
1527
|
-
byAmountIn: true
|
|
1528
|
-
});
|
|
1529
|
-
} finally {
|
|
1530
|
-
console.log = _origLog;
|
|
1531
|
-
}
|
|
1532
|
-
if (!result || result.insufficientLiquidity) {
|
|
1533
|
-
throw new T2000Error("ASSET_NOT_SUPPORTED", `No swap route for reward token \u2192 USDC`);
|
|
1534
|
-
}
|
|
1535
|
-
const tx = new Transaction();
|
|
1536
|
-
const slippage = maxSlippageBps / 1e4;
|
|
1537
|
-
console.log = () => {
|
|
1538
|
-
};
|
|
1539
|
-
try {
|
|
1540
|
-
await aggClient.fastRouterSwap({
|
|
1541
|
-
router: result,
|
|
1542
|
-
txb: tx,
|
|
1543
|
-
slippage
|
|
1544
|
-
});
|
|
1545
|
-
} finally {
|
|
1546
|
-
console.log = _origLog;
|
|
1547
|
-
}
|
|
1548
|
-
return {
|
|
1549
|
-
tx,
|
|
1550
|
-
estimatedOut: Number(result.amountOut.toString()),
|
|
1551
|
-
toDecimals
|
|
1552
|
-
};
|
|
1553
|
-
}
|
|
1554
|
-
async function getPoolPrice(client) {
|
|
1555
|
-
try {
|
|
1556
|
-
const pool = await client.getObject({
|
|
1557
|
-
id: CETUS_USDC_SUI_POOL,
|
|
1558
|
-
options: { showContent: true }
|
|
1559
|
-
});
|
|
1560
|
-
if (pool.data?.content?.dataType === "moveObject") {
|
|
1561
|
-
const fields = pool.data.content.fields;
|
|
1562
|
-
const currentSqrtPrice = BigInt(String(fields.current_sqrt_price ?? "0"));
|
|
1563
|
-
if (currentSqrtPrice > 0n) {
|
|
1564
|
-
const Q64 = 2n ** 64n;
|
|
1565
|
-
const sqrtPriceFloat = Number(currentSqrtPrice) / Number(Q64);
|
|
1566
|
-
const rawPrice = sqrtPriceFloat * sqrtPriceFloat;
|
|
1567
|
-
const suiPriceUsd = 1e3 / rawPrice;
|
|
1568
|
-
if (suiPriceUsd > 0.01 && suiPriceUsd < 1e3) return suiPriceUsd;
|
|
1569
|
-
}
|
|
1570
|
-
}
|
|
1571
|
-
} catch {
|
|
1572
|
-
}
|
|
1573
|
-
return 3.5;
|
|
1574
|
-
}
|
|
1575
|
-
async function getSwapQuote(client, fromAsset, toAsset, amount) {
|
|
1576
|
-
const fromInfo = SUPPORTED_ASSETS[fromAsset];
|
|
1577
|
-
const toInfo = SUPPORTED_ASSETS[toAsset];
|
|
1578
|
-
if (!fromInfo || !toInfo) {
|
|
1579
|
-
throw new T2000Error("ASSET_NOT_SUPPORTED", `Swap pair ${fromAsset}/${toAsset} is not supported`);
|
|
1580
|
-
}
|
|
1581
|
-
const rawAmount = BigInt(Math.floor(amount * 10 ** fromInfo.decimals));
|
|
1582
|
-
const poolPrice = await getPoolPrice(client);
|
|
1583
|
-
try {
|
|
1584
|
-
const aggClient = createAggregatorClient(client);
|
|
1585
|
-
const result = await aggClient.findRouters({
|
|
1586
|
-
from: fromInfo.type,
|
|
1587
|
-
target: toInfo.type,
|
|
1588
|
-
amount: rawAmount,
|
|
1589
|
-
byAmountIn: true
|
|
1590
|
-
});
|
|
1591
|
-
if (!result || result.insufficientLiquidity) {
|
|
1592
|
-
return fallbackQuote(fromAsset, amount, poolPrice);
|
|
1593
|
-
}
|
|
1594
|
-
const expectedOutput = Number(result.amountOut.toString()) / 10 ** toInfo.decimals;
|
|
1595
|
-
const priceImpact = result.deviationRatio ?? 0;
|
|
1596
|
-
return { expectedOutput, priceImpact, poolPrice };
|
|
1597
|
-
} catch {
|
|
1598
|
-
return fallbackQuote(fromAsset, amount, poolPrice);
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
function fallbackQuote(fromAsset, amount, poolPrice) {
|
|
1602
|
-
const expectedOutput = fromAsset === "USDC" ? amount / poolPrice : amount * poolPrice;
|
|
1603
|
-
return { expectedOutput, priceImpact: 0, poolPrice };
|
|
1604
1522
|
}
|
|
1605
1523
|
|
|
1606
|
-
// src/
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
async getQuote(from, to, amount) {
|
|
1620
|
-
return getSwapQuote(this.client, from, to, amount);
|
|
1621
|
-
}
|
|
1622
|
-
async buildSwapTx(address, from, to, amount, maxSlippageBps) {
|
|
1623
|
-
const result = await buildSwapTx({
|
|
1624
|
-
client: this.client,
|
|
1625
|
-
address,
|
|
1626
|
-
fromAsset: from,
|
|
1627
|
-
toAsset: to,
|
|
1628
|
-
amount,
|
|
1629
|
-
maxSlippageBps
|
|
1630
|
-
});
|
|
1631
|
-
return {
|
|
1632
|
-
tx: result.tx,
|
|
1633
|
-
estimatedOut: result.estimatedOut,
|
|
1634
|
-
toDecimals: result.toDecimals
|
|
1635
|
-
};
|
|
1636
|
-
}
|
|
1637
|
-
getSupportedPairs() {
|
|
1638
|
-
const pairs = [];
|
|
1639
|
-
for (const asset of Object.keys(INVESTMENT_ASSETS)) {
|
|
1640
|
-
pairs.push({ from: "USDC", to: asset }, { from: asset, to: "USDC" });
|
|
1641
|
-
}
|
|
1642
|
-
for (const a of STABLE_ASSETS) {
|
|
1643
|
-
for (const b of STABLE_ASSETS) {
|
|
1644
|
-
if (a !== b) pairs.push({ from: a, to: b });
|
|
1645
|
-
}
|
|
1646
|
-
}
|
|
1647
|
-
return pairs;
|
|
1648
|
-
}
|
|
1649
|
-
async getPoolPrice() {
|
|
1650
|
-
return getPoolPrice(this.client);
|
|
1651
|
-
}
|
|
1652
|
-
async addSwapToTx(tx, address, inputCoin, from, to, amount, maxSlippageBps) {
|
|
1653
|
-
return addSwapToTx({
|
|
1654
|
-
tx,
|
|
1655
|
-
client: this.client,
|
|
1656
|
-
address,
|
|
1657
|
-
inputCoin,
|
|
1658
|
-
fromAsset: from,
|
|
1659
|
-
toAsset: to,
|
|
1660
|
-
amount,
|
|
1661
|
-
maxSlippageBps
|
|
1662
|
-
});
|
|
1663
|
-
}
|
|
1664
|
-
};
|
|
1665
|
-
var MIN_HEALTH_FACTOR2 = 1.5;
|
|
1666
|
-
async function quietSuilend(fn) {
|
|
1667
|
-
const origLog = console.log;
|
|
1668
|
-
const origWarn = console.warn;
|
|
1669
|
-
const filter = (...args) => typeof args[0] === "string" && (args[0].includes("PythEndpoint") || args[0].includes("PythConnection"));
|
|
1670
|
-
console.log = (...args) => {
|
|
1671
|
-
if (!filter(...args)) origLog.apply(console, args);
|
|
1672
|
-
};
|
|
1673
|
-
console.warn = (...args) => {
|
|
1674
|
-
if (!filter(...args)) origWarn.apply(console, args);
|
|
1675
|
-
};
|
|
1676
|
-
return fn().finally(() => {
|
|
1677
|
-
console.log = origLog;
|
|
1678
|
-
console.warn = origWarn;
|
|
1679
|
-
});
|
|
1680
|
-
}
|
|
1681
|
-
var SuilendAdapter = class {
|
|
1682
|
-
id = "suilend";
|
|
1683
|
-
name = "Suilend";
|
|
1684
|
-
version = "3.0.0";
|
|
1685
|
-
capabilities = ["save", "withdraw", "borrow", "repay"];
|
|
1686
|
-
supportedAssets = [...STABLE_ASSETS, "SUI", "ETH", "BTC", "GOLD"];
|
|
1687
|
-
supportsSameAssetBorrow = false;
|
|
1688
|
-
client;
|
|
1689
|
-
sdkClient = null;
|
|
1690
|
-
async init(client) {
|
|
1691
|
-
this.client = client;
|
|
1692
|
-
}
|
|
1693
|
-
initSync(client) {
|
|
1694
|
-
this.client = client;
|
|
1695
|
-
}
|
|
1696
|
-
async getSdkClient() {
|
|
1697
|
-
if (!this.sdkClient) {
|
|
1698
|
-
this.sdkClient = await SuilendClient.initialize(
|
|
1699
|
-
LENDING_MARKET_ID,
|
|
1700
|
-
LENDING_MARKET_TYPE,
|
|
1701
|
-
this.client,
|
|
1702
|
-
false
|
|
1703
|
-
);
|
|
1704
|
-
}
|
|
1705
|
-
return this.sdkClient;
|
|
1706
|
-
}
|
|
1707
|
-
resolveSymbol(coinType) {
|
|
1708
|
-
try {
|
|
1709
|
-
const normalized = normalizeStructTag(coinType);
|
|
1710
|
-
for (const [key, info] of Object.entries(SUPPORTED_ASSETS)) {
|
|
1711
|
-
try {
|
|
1712
|
-
if (normalizeStructTag(info.type) === normalized) return key;
|
|
1713
|
-
} catch {
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
} catch {
|
|
1717
|
-
}
|
|
1718
|
-
const parts = coinType.split("::");
|
|
1719
|
-
return parts[parts.length - 1] || "UNKNOWN";
|
|
1720
|
-
}
|
|
1721
|
-
async getRates(asset) {
|
|
1722
|
-
try {
|
|
1723
|
-
const sdk = await this.getSdkClient();
|
|
1724
|
-
const { reserveMap } = await quietSuilend(() => initializeSuilend(this.client, sdk));
|
|
1725
|
-
const assetInfo = SUPPORTED_ASSETS[asset];
|
|
1726
|
-
if (!assetInfo) throw new T2000Error("ASSET_NOT_SUPPORTED", `Suilend does not support ${asset}`);
|
|
1727
|
-
const normalized = normalizeStructTag(assetInfo.type);
|
|
1728
|
-
const reserve = Object.values(reserveMap).find((r) => {
|
|
1729
|
-
try {
|
|
1730
|
-
return normalizeStructTag(r.coinType) === normalized;
|
|
1731
|
-
} catch {
|
|
1732
|
-
return false;
|
|
1733
|
-
}
|
|
1734
|
-
});
|
|
1735
|
-
if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `Suilend does not support ${asset}`);
|
|
1736
|
-
return {
|
|
1737
|
-
asset,
|
|
1738
|
-
saveApy: reserve.depositAprPercent.toNumber(),
|
|
1739
|
-
borrowApy: reserve.borrowAprPercent.toNumber()
|
|
1740
|
-
};
|
|
1741
|
-
} catch (err) {
|
|
1742
|
-
if (err instanceof T2000Error) throw err;
|
|
1743
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1744
|
-
throw new T2000Error("PROTOCOL_UNAVAILABLE", `Suilend getRates failed: ${msg}`);
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
async getPositions(address) {
|
|
1748
|
-
const supplies = [];
|
|
1749
|
-
const borrows = [];
|
|
1750
|
-
try {
|
|
1751
|
-
const sdk = await this.getSdkClient();
|
|
1752
|
-
const { reserveMap, refreshedRawReserves } = await quietSuilend(() => initializeSuilend(this.client, sdk));
|
|
1753
|
-
const { obligations, obligationOwnerCaps } = await initializeObligations(
|
|
1754
|
-
this.client,
|
|
1755
|
-
sdk,
|
|
1756
|
-
refreshedRawReserves,
|
|
1757
|
-
reserveMap,
|
|
1758
|
-
address
|
|
1759
|
-
);
|
|
1760
|
-
if (obligationOwnerCaps.length === 0 || obligations.length === 0) {
|
|
1761
|
-
return { supplies, borrows };
|
|
1762
|
-
}
|
|
1763
|
-
const obligation = obligations[0];
|
|
1764
|
-
for (const dep of obligation.deposits) {
|
|
1765
|
-
const symbol = this.resolveSymbol(dep.coinType);
|
|
1766
|
-
const amount = dep.depositedAmount.toNumber();
|
|
1767
|
-
const amountUsd = dep.depositedAmountUsd.toNumber();
|
|
1768
|
-
const apy = dep.reserve.depositAprPercent.toNumber();
|
|
1769
|
-
if (amountUsd > 0.01) {
|
|
1770
|
-
supplies.push({ asset: symbol, amount, amountUsd, apy });
|
|
1771
|
-
}
|
|
1772
|
-
}
|
|
1773
|
-
for (const bor of obligation.borrows) {
|
|
1774
|
-
const symbol = this.resolveSymbol(bor.coinType);
|
|
1775
|
-
const amount = bor.borrowedAmount.toNumber();
|
|
1776
|
-
const amountUsd = bor.borrowedAmountUsd.toNumber();
|
|
1777
|
-
const apy = bor.reserve.borrowAprPercent.toNumber();
|
|
1778
|
-
if (amountUsd > 0.01) {
|
|
1779
|
-
borrows.push({ asset: symbol, amount, amountUsd, apy });
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1782
|
-
} catch (err) {
|
|
1783
|
-
if (err instanceof T2000Error) throw err;
|
|
1784
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1785
|
-
throw new T2000Error("PROTOCOL_UNAVAILABLE", `Suilend getPositions failed: ${msg}`);
|
|
1786
|
-
}
|
|
1787
|
-
return { supplies, borrows };
|
|
1788
|
-
}
|
|
1789
|
-
async getHealth(address) {
|
|
1790
|
-
try {
|
|
1791
|
-
const sdk = await this.getSdkClient();
|
|
1792
|
-
const { reserveMap, refreshedRawReserves } = await quietSuilend(() => initializeSuilend(this.client, sdk));
|
|
1793
|
-
const { obligations, obligationOwnerCaps } = await initializeObligations(
|
|
1794
|
-
this.client,
|
|
1795
|
-
sdk,
|
|
1796
|
-
refreshedRawReserves,
|
|
1797
|
-
reserveMap,
|
|
1798
|
-
address
|
|
1799
|
-
);
|
|
1800
|
-
if (obligationOwnerCaps.length === 0 || obligations.length === 0) {
|
|
1801
|
-
return { healthFactor: Infinity, supplied: 0, borrowed: 0, maxBorrow: 0, liquidationThreshold: 0 };
|
|
1802
|
-
}
|
|
1803
|
-
const ob = obligations[0];
|
|
1804
|
-
const supplied = ob.depositedAmountUsd.toNumber();
|
|
1805
|
-
const borrowed = ob.borrowedAmountUsd.toNumber();
|
|
1806
|
-
const borrowLimit = ob.borrowLimitUsd.toNumber();
|
|
1807
|
-
const unhealthy = ob.unhealthyBorrowValueUsd.toNumber();
|
|
1808
|
-
const liqThreshold = supplied > 0 ? unhealthy / supplied : 0.75;
|
|
1809
|
-
const healthFactor = borrowed > 0 ? unhealthy / borrowed : Infinity;
|
|
1810
|
-
const maxBorrow = Math.max(0, borrowLimit - borrowed);
|
|
1811
|
-
return { healthFactor, supplied, borrowed, maxBorrow, liquidationThreshold: liqThreshold };
|
|
1812
|
-
} catch {
|
|
1813
|
-
return { healthFactor: Infinity, supplied: 0, borrowed: 0, maxBorrow: 0, liquidationThreshold: 0 };
|
|
1814
|
-
}
|
|
1815
|
-
}
|
|
1816
|
-
async buildSaveTx(address, amount, asset, options) {
|
|
1817
|
-
const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
|
|
1818
|
-
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
1819
|
-
const sdk = await this.getSdkClient();
|
|
1820
|
-
const caps = await SuilendClient.getObligationOwnerCaps(address, [LENDING_MARKET_TYPE], this.client);
|
|
1821
|
-
const tx = new Transaction();
|
|
1822
|
-
tx.setSender(address);
|
|
1823
|
-
const rawValue = stableToRaw(amount, assetInfo.decimals).toString();
|
|
1824
|
-
if (caps.length > 0) {
|
|
1825
|
-
if (options?.collectFee) {
|
|
1826
|
-
const allCoins = await this.fetchAllCoins(address, assetInfo.type);
|
|
1827
|
-
if (allCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins found`);
|
|
1828
|
-
const primaryCoinId = allCoins[0].coinObjectId;
|
|
1829
|
-
if (allCoins.length > 1) {
|
|
1830
|
-
tx.mergeCoins(tx.object(primaryCoinId), allCoins.slice(1).map((c) => tx.object(c.coinObjectId)));
|
|
1831
|
-
}
|
|
1832
|
-
const [depositCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawValue]);
|
|
1833
|
-
addCollectFeeToTx(tx, depositCoin, "save");
|
|
1834
|
-
}
|
|
1835
|
-
await sdk.depositIntoObligation(address, assetInfo.type, rawValue, tx, caps[0].id);
|
|
1836
|
-
} else {
|
|
1837
|
-
const newCap = sdk.createObligation(tx);
|
|
1838
|
-
let depositCoin;
|
|
1839
|
-
if (assetKey === "SUI") {
|
|
1840
|
-
[depositCoin] = tx.splitCoins(tx.gas, [rawValue]);
|
|
1841
|
-
} else {
|
|
1842
|
-
const allCoins = await this.fetchAllCoins(address, assetInfo.type);
|
|
1843
|
-
if (allCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins found`);
|
|
1844
|
-
const primaryCoin = tx.object(allCoins[0].coinObjectId);
|
|
1845
|
-
if (allCoins.length > 1) {
|
|
1846
|
-
tx.mergeCoins(primaryCoin, allCoins.slice(1).map((c) => tx.object(c.coinObjectId)));
|
|
1847
|
-
}
|
|
1848
|
-
[depositCoin] = tx.splitCoins(primaryCoin, [rawValue]);
|
|
1849
|
-
}
|
|
1850
|
-
if (options?.collectFee) {
|
|
1851
|
-
addCollectFeeToTx(tx, depositCoin, "save");
|
|
1852
|
-
}
|
|
1853
|
-
sdk.deposit(depositCoin, assetInfo.type, newCap, tx);
|
|
1854
|
-
tx.transferObjects([newCap], address);
|
|
1855
|
-
}
|
|
1856
|
-
return { tx };
|
|
1857
|
-
}
|
|
1858
|
-
async buildWithdrawTx(address, amount, asset) {
|
|
1859
|
-
const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
|
|
1860
|
-
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
1861
|
-
const sdk = await this.getSdkClient();
|
|
1862
|
-
const caps = await SuilendClient.getObligationOwnerCaps(address, [LENDING_MARKET_TYPE], this.client);
|
|
1863
|
-
if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found");
|
|
1864
|
-
const { ctokenRaw, effectiveAmount, obligationIndex } = await this.resolveWithdrawCTokens(sdk, address, assetKey, assetInfo, amount);
|
|
1865
|
-
const cap = caps[obligationIndex] ?? caps[0];
|
|
1866
|
-
const tx = new Transaction();
|
|
1867
|
-
tx.setSender(address);
|
|
1868
|
-
await sdk.withdrawAndSendToUser(address, cap.id, cap.obligationId, assetInfo.type, ctokenRaw, tx);
|
|
1869
|
-
return { tx, effectiveAmount };
|
|
1870
|
-
}
|
|
1871
|
-
async addWithdrawToTx(tx, address, amount, asset) {
|
|
1872
|
-
const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
|
|
1873
|
-
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
1874
|
-
const sdk = await this.getSdkClient();
|
|
1875
|
-
const caps = await SuilendClient.getObligationOwnerCaps(address, [LENDING_MARKET_TYPE], this.client);
|
|
1876
|
-
if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found");
|
|
1877
|
-
const { ctokenRaw, effectiveAmount, obligationIndex } = await this.resolveWithdrawCTokens(sdk, address, assetKey, assetInfo, amount);
|
|
1878
|
-
const cap = caps[obligationIndex] ?? caps[0];
|
|
1879
|
-
const coin = await sdk.withdraw(cap.id, cap.obligationId, assetInfo.type, ctokenRaw, tx);
|
|
1880
|
-
return { coin, effectiveAmount };
|
|
1881
|
-
}
|
|
1882
|
-
async addSaveToTx(tx, address, coin, asset, options) {
|
|
1883
|
-
const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
|
|
1884
|
-
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
1885
|
-
const sdk = await this.getSdkClient();
|
|
1886
|
-
const caps = await SuilendClient.getObligationOwnerCaps(address, [LENDING_MARKET_TYPE], this.client);
|
|
1887
|
-
let capRef;
|
|
1888
|
-
if (caps.length === 0) {
|
|
1889
|
-
const newCap = sdk.createObligation(tx);
|
|
1890
|
-
capRef = newCap;
|
|
1891
|
-
tx.transferObjects([newCap], address);
|
|
1892
|
-
} else {
|
|
1893
|
-
capRef = caps[0].id;
|
|
1894
|
-
}
|
|
1895
|
-
if (options?.collectFee) {
|
|
1896
|
-
addCollectFeeToTx(tx, coin, "save");
|
|
1897
|
-
}
|
|
1898
|
-
sdk.deposit(coin, assetInfo.type, capRef, tx);
|
|
1899
|
-
}
|
|
1900
|
-
async buildBorrowTx(address, amount, asset, options) {
|
|
1901
|
-
const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
|
|
1902
|
-
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
1903
|
-
const sdk = await this.getSdkClient();
|
|
1904
|
-
const caps = await SuilendClient.getObligationOwnerCaps(address, [LENDING_MARKET_TYPE], this.client);
|
|
1905
|
-
if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found. Deposit collateral first with: t2000 save <amount>");
|
|
1906
|
-
const rawValue = stableToRaw(amount, assetInfo.decimals).toString();
|
|
1907
|
-
const tx = new Transaction();
|
|
1908
|
-
tx.setSender(address);
|
|
1909
|
-
if (options?.collectFee) {
|
|
1910
|
-
const coin = await sdk.borrow(caps[0].id, caps[0].obligationId, assetInfo.type, rawValue, tx);
|
|
1911
|
-
addCollectFeeToTx(tx, coin, "borrow");
|
|
1912
|
-
tx.transferObjects([coin], address);
|
|
1913
|
-
} else {
|
|
1914
|
-
await sdk.borrowAndSendToUser(address, caps[0].id, caps[0].obligationId, assetInfo.type, rawValue, tx);
|
|
1915
|
-
}
|
|
1916
|
-
return { tx };
|
|
1917
|
-
}
|
|
1918
|
-
async buildRepayTx(address, amount, asset) {
|
|
1919
|
-
const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
|
|
1920
|
-
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
1921
|
-
const sdk = await this.getSdkClient();
|
|
1922
|
-
const caps = await SuilendClient.getObligationOwnerCaps(address, [LENDING_MARKET_TYPE], this.client);
|
|
1923
|
-
if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend obligation found");
|
|
1924
|
-
const rawValue = stableToRaw(amount, assetInfo.decimals).toString();
|
|
1925
|
-
const tx = new Transaction();
|
|
1926
|
-
tx.setSender(address);
|
|
1927
|
-
await sdk.repayIntoObligation(address, caps[0].obligationId, assetInfo.type, rawValue, tx);
|
|
1928
|
-
return { tx };
|
|
1929
|
-
}
|
|
1930
|
-
async addRepayToTx(tx, address, coin, asset) {
|
|
1931
|
-
const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
|
|
1932
|
-
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
1933
|
-
const sdk = await this.getSdkClient();
|
|
1934
|
-
const caps = await SuilendClient.getObligationOwnerCaps(address, [LENDING_MARKET_TYPE], this.client);
|
|
1935
|
-
if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend obligation found");
|
|
1936
|
-
sdk.repay(caps[0].obligationId, assetInfo.type, coin, tx);
|
|
1937
|
-
}
|
|
1938
|
-
async resolveWithdrawCTokens(sdk, address, assetKey, assetInfo, amount) {
|
|
1939
|
-
const { reserveMap, refreshedRawReserves } = await quietSuilend(() => initializeSuilend(this.client, sdk));
|
|
1940
|
-
const { obligations } = await initializeObligations(
|
|
1941
|
-
this.client,
|
|
1942
|
-
sdk,
|
|
1943
|
-
refreshedRawReserves,
|
|
1944
|
-
reserveMap,
|
|
1945
|
-
address
|
|
1946
|
-
);
|
|
1947
|
-
if (obligations.length === 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on Suilend \u2014 no obligations found`);
|
|
1948
|
-
let dep;
|
|
1949
|
-
let matchedObligation = 0;
|
|
1950
|
-
for (let oi = 0; oi < obligations.length; oi++) {
|
|
1951
|
-
dep = obligations[oi].deposits.find((d) => {
|
|
1952
|
-
const resolved = this.resolveSymbol(d.coinType);
|
|
1953
|
-
if (resolved === assetKey) return true;
|
|
1954
|
-
try {
|
|
1955
|
-
return normalizeStructTag(d.coinType) === normalizeStructTag(assetInfo.type);
|
|
1956
|
-
} catch {
|
|
1957
|
-
return false;
|
|
1958
|
-
}
|
|
1959
|
-
});
|
|
1960
|
-
if (dep && dep.depositedAmount.toNumber() > 1e-10) {
|
|
1961
|
-
matchedObligation = oi;
|
|
1962
|
-
break;
|
|
1963
|
-
}
|
|
1964
|
-
dep = void 0;
|
|
1965
|
-
}
|
|
1966
|
-
if (!dep) {
|
|
1967
|
-
const foundDeposits = obligations.flatMap(
|
|
1968
|
-
(o, i) => o.deposits.map((d) => `ob${i}:${this.resolveSymbol(d.coinType)}=${d.depositedAmount.toFixed(8)}`)
|
|
1969
|
-
);
|
|
1970
|
-
throw new T2000Error(
|
|
1971
|
-
"NO_COLLATERAL",
|
|
1972
|
-
`Nothing to withdraw for ${assetInfo.displayName} (${assetKey}) on Suilend. Found deposits: [${foundDeposits.join(", ")}]`
|
|
1973
|
-
);
|
|
1974
|
-
}
|
|
1975
|
-
const deposited = dep.depositedAmount.toNumber();
|
|
1976
|
-
const effectiveAmount = Math.min(amount, deposited);
|
|
1977
|
-
const proportion = effectiveAmount / deposited;
|
|
1978
|
-
const ctokenRaw = proportion >= 0.999 ? dep.depositedCtokenAmount.toFixed(0) : dep.depositedCtokenAmount.times(proportion).integerValue(1).toFixed(0);
|
|
1979
|
-
return { ctokenRaw, effectiveAmount, obligationIndex: matchedObligation };
|
|
1980
|
-
}
|
|
1981
|
-
async maxWithdraw(address, _asset) {
|
|
1982
|
-
const health = await this.getHealth(address);
|
|
1983
|
-
let maxAmount;
|
|
1984
|
-
if (health.borrowed === 0) {
|
|
1985
|
-
maxAmount = health.supplied;
|
|
1986
|
-
} else {
|
|
1987
|
-
maxAmount = Math.max(0, health.supplied - health.borrowed * MIN_HEALTH_FACTOR2 / health.liquidationThreshold);
|
|
1988
|
-
}
|
|
1989
|
-
const remainingSupply = health.supplied - maxAmount;
|
|
1990
|
-
const hfAfter = health.borrowed > 0 ? remainingSupply * health.liquidationThreshold / health.borrowed : Infinity;
|
|
1991
|
-
return { maxAmount, healthFactorAfter: hfAfter, currentHF: health.healthFactor };
|
|
1992
|
-
}
|
|
1993
|
-
async maxBorrow(address, _asset) {
|
|
1994
|
-
const health = await this.getHealth(address);
|
|
1995
|
-
return { maxAmount: health.maxBorrow, healthFactorAfter: MIN_HEALTH_FACTOR2, currentHF: health.healthFactor };
|
|
1996
|
-
}
|
|
1997
|
-
async fetchAllCoins(owner, coinType) {
|
|
1998
|
-
const all = [];
|
|
1999
|
-
let cursor = null;
|
|
2000
|
-
let hasNext = true;
|
|
2001
|
-
while (hasNext) {
|
|
2002
|
-
const page = await this.client.getCoins({ owner, coinType, cursor: cursor ?? void 0 });
|
|
2003
|
-
all.push(...page.data.map((c) => ({ coinObjectId: c.coinObjectId, balance: c.balance })));
|
|
2004
|
-
cursor = page.nextCursor;
|
|
2005
|
-
hasNext = page.hasNextPage;
|
|
2006
|
-
}
|
|
2007
|
-
return all;
|
|
2008
|
-
}
|
|
2009
|
-
async getPendingRewards(address) {
|
|
2010
|
-
try {
|
|
2011
|
-
const sdk = await this.getSdkClient();
|
|
2012
|
-
const { reserveMap, refreshedRawReserves } = await quietSuilend(() => initializeSuilend(this.client, sdk));
|
|
2013
|
-
const { obligations, obligationOwnerCaps } = await initializeObligations(
|
|
2014
|
-
this.client,
|
|
2015
|
-
sdk,
|
|
2016
|
-
refreshedRawReserves,
|
|
2017
|
-
reserveMap,
|
|
2018
|
-
address
|
|
2019
|
-
);
|
|
2020
|
-
if (obligationOwnerCaps.length === 0 || obligations.length === 0) return [];
|
|
2021
|
-
const ob = obligations[0];
|
|
2022
|
-
const rewards = [];
|
|
2023
|
-
const WAD = 1e18;
|
|
2024
|
-
for (const dep of ob.deposits) {
|
|
2025
|
-
const urm = dep.userRewardManager;
|
|
2026
|
-
for (const rw of dep.reserve.depositsPoolRewardManager?.poolRewards ?? []) {
|
|
2027
|
-
if (rw.endTimeMs <= Date.now()) continue;
|
|
2028
|
-
let claimableAmount = 0;
|
|
2029
|
-
const userReward = urm?.rewards?.[rw.rewardIndex];
|
|
2030
|
-
if (userReward?.earnedRewards) {
|
|
2031
|
-
claimableAmount = Number(BigInt(userReward.earnedRewards.value.toString())) / WAD / 10 ** rw.mintDecimals;
|
|
2032
|
-
}
|
|
2033
|
-
const symbol = rw.symbol || rw.coinType.split("::").pop() || "UNKNOWN";
|
|
2034
|
-
rewards.push({
|
|
2035
|
-
protocol: "suilend",
|
|
2036
|
-
asset: this.resolveSymbol(dep.coinType),
|
|
2037
|
-
coinType: rw.coinType,
|
|
2038
|
-
symbol,
|
|
2039
|
-
amount: claimableAmount,
|
|
2040
|
-
estimatedValueUsd: 0
|
|
2041
|
-
});
|
|
2042
|
-
}
|
|
2043
|
-
}
|
|
2044
|
-
return rewards;
|
|
2045
|
-
} catch {
|
|
2046
|
-
return [];
|
|
2047
|
-
}
|
|
2048
|
-
}
|
|
2049
|
-
async addClaimRewardsToTx(tx, address) {
|
|
2050
|
-
try {
|
|
2051
|
-
const sdk = await this.getSdkClient();
|
|
2052
|
-
const caps = await SuilendClient.getObligationOwnerCaps(address, [LENDING_MARKET_TYPE], this.client);
|
|
2053
|
-
if (caps.length === 0) return [];
|
|
2054
|
-
const { reserveMap, refreshedRawReserves } = await quietSuilend(() => initializeSuilend(this.client, sdk));
|
|
2055
|
-
const { obligations } = await initializeObligations(
|
|
2056
|
-
this.client,
|
|
2057
|
-
sdk,
|
|
2058
|
-
refreshedRawReserves,
|
|
2059
|
-
reserveMap,
|
|
2060
|
-
address
|
|
2061
|
-
);
|
|
2062
|
-
if (obligations.length === 0) return [];
|
|
2063
|
-
const ob = obligations[0];
|
|
2064
|
-
const claimRewards = [];
|
|
2065
|
-
for (const dep of ob.deposits) {
|
|
2066
|
-
for (const rw of dep.reserve.depositsPoolRewardManager.poolRewards) {
|
|
2067
|
-
if (rw.endTimeMs <= Date.now()) continue;
|
|
2068
|
-
claimRewards.push({
|
|
2069
|
-
reserveArrayIndex: dep.reserveArrayIndex,
|
|
2070
|
-
rewardIndex: BigInt(rw.rewardIndex),
|
|
2071
|
-
rewardCoinType: rw.coinType,
|
|
2072
|
-
side: Side.DEPOSIT
|
|
2073
|
-
});
|
|
2074
|
-
}
|
|
2075
|
-
}
|
|
2076
|
-
if (claimRewards.length === 0) return [];
|
|
2077
|
-
sdk.claimRewardsAndSendToUser(address, caps[0].id, claimRewards, tx);
|
|
2078
|
-
return claimRewards.map((r) => ({
|
|
2079
|
-
protocol: "suilend",
|
|
2080
|
-
asset: "",
|
|
2081
|
-
coinType: r.rewardCoinType,
|
|
2082
|
-
symbol: r.rewardCoinType.split("::").pop() ?? "UNKNOWN",
|
|
2083
|
-
amount: 0,
|
|
2084
|
-
estimatedValueUsd: 0
|
|
2085
|
-
}));
|
|
2086
|
-
} catch {
|
|
2087
|
-
return [];
|
|
2088
|
-
}
|
|
2089
|
-
}
|
|
2090
|
-
};
|
|
2091
|
-
function hasLeadingZeroBits(hash, bits) {
|
|
2092
|
-
const fullBytes = Math.floor(bits / 8);
|
|
2093
|
-
const remainingBits = bits % 8;
|
|
2094
|
-
for (let i = 0; i < fullBytes; i++) {
|
|
2095
|
-
if (hash[i] !== 0) return false;
|
|
2096
|
-
}
|
|
2097
|
-
if (remainingBits > 0) {
|
|
2098
|
-
const mask = 255 << 8 - remainingBits;
|
|
2099
|
-
if ((hash[fullBytes] & mask) !== 0) return false;
|
|
1524
|
+
// src/gas/manager.ts
|
|
1525
|
+
init_errors();
|
|
1526
|
+
|
|
1527
|
+
// src/gas/autoTopUp.ts
|
|
1528
|
+
async function shouldAutoTopUp(client, address) {
|
|
1529
|
+
const [suiBalance, usdcBalance] = await Promise.all([
|
|
1530
|
+
client.getBalance({ owner: address, coinType: SUPPORTED_ASSETS.SUI.type }),
|
|
1531
|
+
client.getBalance({ owner: address, coinType: SUPPORTED_ASSETS.USDC.type })
|
|
1532
|
+
]);
|
|
1533
|
+
const suiRaw = BigInt(suiBalance.totalBalance);
|
|
1534
|
+
const usdcRaw = BigInt(usdcBalance.totalBalance);
|
|
1535
|
+
if (suiRaw < GAS_RESERVE_TARGET && usdcRaw >= AUTO_TOPUP_MIN_USDC) {
|
|
1536
|
+
return false;
|
|
2100
1537
|
}
|
|
2101
|
-
return
|
|
1538
|
+
return false;
|
|
2102
1539
|
}
|
|
2103
|
-
function
|
|
2104
|
-
|
|
2105
|
-
let counter = 0;
|
|
2106
|
-
while (true) {
|
|
2107
|
-
const stamp = `${challenge}${counter.toString(16)}`;
|
|
2108
|
-
const hash = createHash("sha256").update(stamp).digest();
|
|
2109
|
-
if (hasLeadingZeroBits(hash, bits)) return stamp;
|
|
2110
|
-
counter++;
|
|
2111
|
-
}
|
|
1540
|
+
async function executeAutoTopUp(_client, _signer) {
|
|
1541
|
+
return { success: false, tx: "", usdcSpent: 0, suiReceived: 0 };
|
|
2112
1542
|
}
|
|
2113
1543
|
|
|
1544
|
+
// src/gas/gasStation.ts
|
|
1545
|
+
init_errors();
|
|
1546
|
+
|
|
2114
1547
|
// src/utils/base64.ts
|
|
2115
1548
|
function toBase64(bytes) {
|
|
2116
1549
|
let binary = "";
|
|
@@ -2186,92 +1619,6 @@ async function getGasStatus(address) {
|
|
|
2186
1619
|
return await res.json();
|
|
2187
1620
|
}
|
|
2188
1621
|
|
|
2189
|
-
// src/gas/autoTopUp.ts
|
|
2190
|
-
async function shouldAutoTopUp(client, address) {
|
|
2191
|
-
const [suiBalance, usdcBalance] = await Promise.all([
|
|
2192
|
-
client.getBalance({ owner: address, coinType: SUPPORTED_ASSETS.SUI.type }),
|
|
2193
|
-
client.getBalance({ owner: address, coinType: SUPPORTED_ASSETS.USDC.type })
|
|
2194
|
-
]);
|
|
2195
|
-
const suiRaw = BigInt(suiBalance.totalBalance);
|
|
2196
|
-
const usdcRaw = BigInt(usdcBalance.totalBalance);
|
|
2197
|
-
return suiRaw < GAS_RESERVE_TARGET && usdcRaw >= AUTO_TOPUP_MIN_USDC;
|
|
2198
|
-
}
|
|
2199
|
-
async function executeAutoTopUp(client, signer) {
|
|
2200
|
-
const address = signer.getAddress();
|
|
2201
|
-
const topupAmountHuman = Number(AUTO_TOPUP_AMOUNT) / 1e6;
|
|
2202
|
-
const { tx } = await buildSwapTx({
|
|
2203
|
-
client,
|
|
2204
|
-
address,
|
|
2205
|
-
fromAsset: "USDC",
|
|
2206
|
-
toAsset: "SUI",
|
|
2207
|
-
amount: topupAmountHuman
|
|
2208
|
-
});
|
|
2209
|
-
tx.setSender(address);
|
|
2210
|
-
let result;
|
|
2211
|
-
try {
|
|
2212
|
-
const builtBytes = await tx.build({ client });
|
|
2213
|
-
const { signature } = await signer.signTransaction(builtBytes);
|
|
2214
|
-
result = await client.executeTransactionBlock({
|
|
2215
|
-
transactionBlock: toBase64(builtBytes),
|
|
2216
|
-
signature: [signature],
|
|
2217
|
-
options: { showEffects: true, showBalanceChanges: true }
|
|
2218
|
-
});
|
|
2219
|
-
} catch {
|
|
2220
|
-
const { tx: freshTx } = await buildSwapTx({
|
|
2221
|
-
client,
|
|
2222
|
-
address,
|
|
2223
|
-
fromAsset: "USDC",
|
|
2224
|
-
toAsset: "SUI",
|
|
2225
|
-
amount: topupAmountHuman
|
|
2226
|
-
});
|
|
2227
|
-
freshTx.setSender(address);
|
|
2228
|
-
let txJson;
|
|
2229
|
-
let txBcsBase64;
|
|
2230
|
-
try {
|
|
2231
|
-
txJson = freshTx.serialize();
|
|
2232
|
-
} catch {
|
|
2233
|
-
const bcsBytes = await freshTx.build({ client });
|
|
2234
|
-
txBcsBase64 = toBase64(bcsBytes);
|
|
2235
|
-
}
|
|
2236
|
-
const sponsored = await requestGasSponsorship(
|
|
2237
|
-
txJson ?? "",
|
|
2238
|
-
address,
|
|
2239
|
-
"auto-topup",
|
|
2240
|
-
txBcsBase64
|
|
2241
|
-
);
|
|
2242
|
-
const sponsoredTxBytes = fromBase64(sponsored.txBytes);
|
|
2243
|
-
const { signature: agentSig } = await signer.signTransaction(sponsoredTxBytes);
|
|
2244
|
-
result = await client.executeTransactionBlock({
|
|
2245
|
-
transactionBlock: sponsored.txBytes,
|
|
2246
|
-
signature: [agentSig, sponsored.sponsorSignature],
|
|
2247
|
-
options: { showEffects: true, showBalanceChanges: true }
|
|
2248
|
-
});
|
|
2249
|
-
reportGasUsage(address, result.digest, 0, 0, "auto-topup");
|
|
2250
|
-
}
|
|
2251
|
-
await client.waitForTransaction({ digest: result.digest });
|
|
2252
|
-
const eff = result.effects;
|
|
2253
|
-
if (eff?.status?.status === "failure") {
|
|
2254
|
-
throw new T2000Error(
|
|
2255
|
-
"TRANSACTION_FAILED",
|
|
2256
|
-
`Auto-topup swap failed on-chain: ${eff.status.error ?? "unknown"}`
|
|
2257
|
-
);
|
|
2258
|
-
}
|
|
2259
|
-
let suiReceived = 0;
|
|
2260
|
-
if (result.balanceChanges) {
|
|
2261
|
-
for (const change of result.balanceChanges) {
|
|
2262
|
-
if (change.coinType === SUPPORTED_ASSETS.SUI.type && change.owner && typeof change.owner === "object" && "AddressOwner" in change.owner && change.owner.AddressOwner === address) {
|
|
2263
|
-
suiReceived += Number(change.amount) / Number(MIST_PER_SUI);
|
|
2264
|
-
}
|
|
2265
|
-
}
|
|
2266
|
-
}
|
|
2267
|
-
return {
|
|
2268
|
-
success: true,
|
|
2269
|
-
tx: result.digest,
|
|
2270
|
-
usdcSpent: topupAmountHuman,
|
|
2271
|
-
suiReceived: Math.abs(suiReceived)
|
|
2272
|
-
};
|
|
2273
|
-
}
|
|
2274
|
-
|
|
2275
1622
|
// src/gas/manager.ts
|
|
2276
1623
|
function extractGasCost(effects) {
|
|
2277
1624
|
if (!effects?.gasUsed) return 0;
|
|
@@ -2317,7 +1664,7 @@ async function tryAutoTopUpThenSelfFund(client, signer, buildTx) {
|
|
|
2317
1664
|
const address = signer.getAddress();
|
|
2318
1665
|
const canTopUp = await shouldAutoTopUp(client, address);
|
|
2319
1666
|
if (!canTopUp) return null;
|
|
2320
|
-
await executeAutoTopUp(
|
|
1667
|
+
await executeAutoTopUp();
|
|
2321
1668
|
const tx = await buildTx();
|
|
2322
1669
|
tx.setSender(address);
|
|
2323
1670
|
const suiAfterTopUp = await getSuiBalance(client, address);
|
|
@@ -2482,6 +1829,9 @@ async function resolveGas(client, signer, buildTx) {
|
|
|
2482
1829
|
);
|
|
2483
1830
|
}
|
|
2484
1831
|
|
|
1832
|
+
// src/t2000.ts
|
|
1833
|
+
init_errors();
|
|
1834
|
+
|
|
2485
1835
|
// src/safeguards/types.ts
|
|
2486
1836
|
var OUTBOUND_OPS = /* @__PURE__ */ new Set([
|
|
2487
1837
|
"send",
|
|
@@ -2496,6 +1846,7 @@ var DEFAULT_SAFEGUARD_CONFIG = {
|
|
|
2496
1846
|
};
|
|
2497
1847
|
|
|
2498
1848
|
// src/safeguards/errors.ts
|
|
1849
|
+
init_errors();
|
|
2499
1850
|
var SafeguardError = class extends T2000Error {
|
|
2500
1851
|
rule;
|
|
2501
1852
|
details;
|
|
@@ -2642,6 +1993,7 @@ var SafeguardEnforcer = class {
|
|
|
2642
1993
|
}
|
|
2643
1994
|
}
|
|
2644
1995
|
};
|
|
1996
|
+
init_errors();
|
|
2645
1997
|
var RESERVED_NAMES = /* @__PURE__ */ new Set(["to", "all", "address"]);
|
|
2646
1998
|
var ContactManager = class {
|
|
2647
1999
|
contacts = {};
|
|
@@ -2719,489 +2071,6 @@ var ContactManager = class {
|
|
|
2719
2071
|
}
|
|
2720
2072
|
}
|
|
2721
2073
|
};
|
|
2722
|
-
var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
2723
|
-
function emptyData() {
|
|
2724
|
-
return { positions: {}, strategies: {}, realizedPnL: 0 };
|
|
2725
|
-
}
|
|
2726
|
-
var PortfolioManager = class {
|
|
2727
|
-
data = emptyData();
|
|
2728
|
-
filePath;
|
|
2729
|
-
dir;
|
|
2730
|
-
constructor(configDir) {
|
|
2731
|
-
this.dir = configDir ?? join(homedir(), ".t2000");
|
|
2732
|
-
this.filePath = join(this.dir, "portfolio.json");
|
|
2733
|
-
this.load();
|
|
2734
|
-
}
|
|
2735
|
-
load() {
|
|
2736
|
-
try {
|
|
2737
|
-
if (existsSync(this.filePath)) {
|
|
2738
|
-
this.data = JSON.parse(readFileSync(this.filePath, "utf-8"));
|
|
2739
|
-
if (!this.data.strategies) this.data.strategies = {};
|
|
2740
|
-
}
|
|
2741
|
-
} catch {
|
|
2742
|
-
this.data = emptyData();
|
|
2743
|
-
}
|
|
2744
|
-
}
|
|
2745
|
-
save() {
|
|
2746
|
-
if (!existsSync(this.dir)) mkdirSync(this.dir, { recursive: true });
|
|
2747
|
-
writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
|
|
2748
|
-
}
|
|
2749
|
-
recordBuy(trade) {
|
|
2750
|
-
this.load();
|
|
2751
|
-
const pos = this.data.positions[trade.asset] ?? { totalAmount: 0, costBasis: 0, avgPrice: 0, trades: [] };
|
|
2752
|
-
pos.totalAmount += trade.amount;
|
|
2753
|
-
pos.costBasis += trade.usdValue;
|
|
2754
|
-
pos.avgPrice = pos.costBasis / pos.totalAmount;
|
|
2755
|
-
pos.trades.push(trade);
|
|
2756
|
-
this.data.positions[trade.asset] = pos;
|
|
2757
|
-
this.save();
|
|
2758
|
-
}
|
|
2759
|
-
recordSell(trade) {
|
|
2760
|
-
this.load();
|
|
2761
|
-
const pos = this.data.positions[trade.asset];
|
|
2762
|
-
if (!pos || pos.totalAmount <= 0) {
|
|
2763
|
-
throw new T2000Error("INSUFFICIENT_INVESTMENT", `No ${trade.asset} position to sell`);
|
|
2764
|
-
}
|
|
2765
|
-
const sellAmount = Math.min(trade.amount, pos.totalAmount);
|
|
2766
|
-
const effectiveUsdValue = trade.amount > 0 && sellAmount < trade.amount ? trade.usdValue * (sellAmount / trade.amount) : trade.usdValue;
|
|
2767
|
-
const costOfSold = pos.avgPrice * sellAmount;
|
|
2768
|
-
const realizedPnL = effectiveUsdValue - costOfSold;
|
|
2769
|
-
pos.totalAmount -= sellAmount;
|
|
2770
|
-
pos.costBasis -= costOfSold;
|
|
2771
|
-
if (pos.totalAmount < 1e-6) {
|
|
2772
|
-
pos.totalAmount = 0;
|
|
2773
|
-
pos.costBasis = 0;
|
|
2774
|
-
pos.avgPrice = 0;
|
|
2775
|
-
}
|
|
2776
|
-
pos.trades.push(trade);
|
|
2777
|
-
this.data.realizedPnL += realizedPnL;
|
|
2778
|
-
this.data.positions[trade.asset] = pos;
|
|
2779
|
-
this.save();
|
|
2780
|
-
return realizedPnL;
|
|
2781
|
-
}
|
|
2782
|
-
getPosition(asset) {
|
|
2783
|
-
this.load();
|
|
2784
|
-
return this.data.positions[asset];
|
|
2785
|
-
}
|
|
2786
|
-
getPositions() {
|
|
2787
|
-
this.load();
|
|
2788
|
-
return Object.entries(this.data.positions).filter(([, pos]) => pos.totalAmount > 0).map(([asset, pos]) => ({ asset, ...pos }));
|
|
2789
|
-
}
|
|
2790
|
-
recordEarn(asset, protocol, apy) {
|
|
2791
|
-
this.load();
|
|
2792
|
-
const pos = this.data.positions[asset];
|
|
2793
|
-
if (!pos || pos.totalAmount <= 0) {
|
|
2794
|
-
throw new T2000Error("INSUFFICIENT_INVESTMENT", `No ${asset} position to earn on`);
|
|
2795
|
-
}
|
|
2796
|
-
pos.earning = true;
|
|
2797
|
-
pos.earningProtocol = protocol;
|
|
2798
|
-
pos.earningApy = apy;
|
|
2799
|
-
this.data.positions[asset] = pos;
|
|
2800
|
-
this.save();
|
|
2801
|
-
}
|
|
2802
|
-
recordUnearn(asset) {
|
|
2803
|
-
this.load();
|
|
2804
|
-
const pos = this.data.positions[asset];
|
|
2805
|
-
if (!pos || !pos.earning) {
|
|
2806
|
-
throw new T2000Error("INVEST_NOT_EARNING", `${asset} is not currently earning`);
|
|
2807
|
-
}
|
|
2808
|
-
pos.earning = false;
|
|
2809
|
-
pos.earningProtocol = void 0;
|
|
2810
|
-
pos.earningApy = void 0;
|
|
2811
|
-
this.data.positions[asset] = pos;
|
|
2812
|
-
this.save();
|
|
2813
|
-
}
|
|
2814
|
-
getStrategyAmountForAsset(asset) {
|
|
2815
|
-
this.load();
|
|
2816
|
-
let total = 0;
|
|
2817
|
-
for (const bucket of Object.values(this.data.strategies)) {
|
|
2818
|
-
const pos = bucket[asset];
|
|
2819
|
-
if (pos && pos.totalAmount > 0) total += pos.totalAmount;
|
|
2820
|
-
}
|
|
2821
|
-
return total;
|
|
2822
|
-
}
|
|
2823
|
-
getDirectAmount(asset) {
|
|
2824
|
-
this.load();
|
|
2825
|
-
const aggregate = this.data.positions[asset]?.totalAmount ?? 0;
|
|
2826
|
-
const strategyAmount = this.getStrategyAmountForAsset(asset);
|
|
2827
|
-
return Math.max(0, aggregate - strategyAmount);
|
|
2828
|
-
}
|
|
2829
|
-
deductFromStrategies(asset, amount) {
|
|
2830
|
-
this.load();
|
|
2831
|
-
let remaining = amount;
|
|
2832
|
-
for (const [stratKey, bucket] of Object.entries(this.data.strategies)) {
|
|
2833
|
-
if (remaining <= 0) break;
|
|
2834
|
-
const pos = bucket[asset];
|
|
2835
|
-
if (!pos || pos.totalAmount <= 0) continue;
|
|
2836
|
-
const deduct = Math.min(pos.totalAmount, remaining);
|
|
2837
|
-
const costDeduct = pos.avgPrice * deduct;
|
|
2838
|
-
pos.totalAmount -= deduct;
|
|
2839
|
-
pos.costBasis -= costDeduct;
|
|
2840
|
-
if (pos.totalAmount < 1e-6) {
|
|
2841
|
-
pos.totalAmount = 0;
|
|
2842
|
-
pos.costBasis = 0;
|
|
2843
|
-
pos.avgPrice = 0;
|
|
2844
|
-
}
|
|
2845
|
-
remaining -= deduct;
|
|
2846
|
-
const hasPositions = Object.values(bucket).some((p) => p.totalAmount > 0);
|
|
2847
|
-
if (!hasPositions) {
|
|
2848
|
-
delete this.data.strategies[stratKey];
|
|
2849
|
-
}
|
|
2850
|
-
}
|
|
2851
|
-
this.save();
|
|
2852
|
-
}
|
|
2853
|
-
closePosition(asset) {
|
|
2854
|
-
this.load();
|
|
2855
|
-
const pos = this.data.positions[asset];
|
|
2856
|
-
if (pos) {
|
|
2857
|
-
pos.totalAmount = 0;
|
|
2858
|
-
pos.costBasis = 0;
|
|
2859
|
-
pos.avgPrice = 0;
|
|
2860
|
-
pos.earning = false;
|
|
2861
|
-
pos.earningProtocol = void 0;
|
|
2862
|
-
pos.earningApy = void 0;
|
|
2863
|
-
this.data.positions[asset] = pos;
|
|
2864
|
-
this.save();
|
|
2865
|
-
}
|
|
2866
|
-
}
|
|
2867
|
-
isEarning(asset) {
|
|
2868
|
-
this.load();
|
|
2869
|
-
const pos = this.data.positions[asset];
|
|
2870
|
-
return pos?.earning === true;
|
|
2871
|
-
}
|
|
2872
|
-
getRealizedPnL() {
|
|
2873
|
-
this.load();
|
|
2874
|
-
return this.data.realizedPnL;
|
|
2875
|
-
}
|
|
2876
|
-
// --- Strategy position tracking ---
|
|
2877
|
-
recordStrategyBuy(strategyKey, trade) {
|
|
2878
|
-
if (UNSAFE_KEYS.has(strategyKey) || UNSAFE_KEYS.has(trade.asset)) {
|
|
2879
|
-
throw new T2000Error("ASSET_NOT_SUPPORTED", "Invalid strategy key or asset name");
|
|
2880
|
-
}
|
|
2881
|
-
this.load();
|
|
2882
|
-
if (!Object.hasOwn(this.data.strategies, strategyKey)) {
|
|
2883
|
-
this.data.strategies[strategyKey] = /* @__PURE__ */ Object.create(null);
|
|
2884
|
-
}
|
|
2885
|
-
const bucket = this.data.strategies[strategyKey];
|
|
2886
|
-
const pos = Object.hasOwn(bucket, trade.asset) ? bucket[trade.asset] : { totalAmount: 0, costBasis: 0, avgPrice: 0, trades: [] };
|
|
2887
|
-
pos.totalAmount += trade.amount;
|
|
2888
|
-
pos.costBasis += trade.usdValue;
|
|
2889
|
-
pos.avgPrice = pos.costBasis / pos.totalAmount;
|
|
2890
|
-
pos.trades.push(trade);
|
|
2891
|
-
Object.defineProperty(bucket, trade.asset, { value: pos, writable: true, enumerable: true, configurable: true });
|
|
2892
|
-
this.save();
|
|
2893
|
-
}
|
|
2894
|
-
recordStrategySell(strategyKey, trade) {
|
|
2895
|
-
if (UNSAFE_KEYS.has(strategyKey) || UNSAFE_KEYS.has(trade.asset)) {
|
|
2896
|
-
throw new T2000Error("ASSET_NOT_SUPPORTED", "Invalid strategy key or asset name");
|
|
2897
|
-
}
|
|
2898
|
-
this.load();
|
|
2899
|
-
const bucket = this.data.strategies[strategyKey];
|
|
2900
|
-
if (!bucket) {
|
|
2901
|
-
throw new T2000Error("STRATEGY_NOT_FOUND", `No positions for strategy '${strategyKey}'`);
|
|
2902
|
-
}
|
|
2903
|
-
if (!Object.hasOwn(bucket, trade.asset)) {
|
|
2904
|
-
throw new T2000Error("INSUFFICIENT_INVESTMENT", `No ${trade.asset} position in strategy '${strategyKey}'`);
|
|
2905
|
-
}
|
|
2906
|
-
const pos = bucket[trade.asset];
|
|
2907
|
-
if (!pos || pos.totalAmount <= 0) {
|
|
2908
|
-
throw new T2000Error("INSUFFICIENT_INVESTMENT", `No ${trade.asset} position in strategy '${strategyKey}'`);
|
|
2909
|
-
}
|
|
2910
|
-
const sellAmount = Math.min(trade.amount, pos.totalAmount);
|
|
2911
|
-
const effectiveUsdValue = trade.amount > 0 && sellAmount < trade.amount ? trade.usdValue * (sellAmount / trade.amount) : trade.usdValue;
|
|
2912
|
-
const costOfSold = pos.avgPrice * sellAmount;
|
|
2913
|
-
const realizedPnL = effectiveUsdValue - costOfSold;
|
|
2914
|
-
pos.totalAmount -= sellAmount;
|
|
2915
|
-
pos.costBasis -= costOfSold;
|
|
2916
|
-
if (pos.totalAmount < 1e-6) {
|
|
2917
|
-
pos.totalAmount = 0;
|
|
2918
|
-
pos.costBasis = 0;
|
|
2919
|
-
pos.avgPrice = 0;
|
|
2920
|
-
}
|
|
2921
|
-
pos.trades.push(trade);
|
|
2922
|
-
Object.defineProperty(bucket, trade.asset, { value: pos, writable: true, enumerable: true, configurable: true });
|
|
2923
|
-
const hasPositions = Object.values(bucket).some((p) => p.totalAmount > 0);
|
|
2924
|
-
if (!hasPositions) {
|
|
2925
|
-
delete this.data.strategies[strategyKey];
|
|
2926
|
-
}
|
|
2927
|
-
this.save();
|
|
2928
|
-
return realizedPnL;
|
|
2929
|
-
}
|
|
2930
|
-
getStrategyPositions(strategyKey) {
|
|
2931
|
-
this.load();
|
|
2932
|
-
const bucket = this.data.strategies[strategyKey];
|
|
2933
|
-
if (!bucket) return [];
|
|
2934
|
-
return Object.entries(bucket).filter(([, pos]) => pos.totalAmount > 0).map(([asset, pos]) => ({ asset, ...pos }));
|
|
2935
|
-
}
|
|
2936
|
-
getAllStrategyKeys() {
|
|
2937
|
-
this.load();
|
|
2938
|
-
return Object.keys(this.data.strategies);
|
|
2939
|
-
}
|
|
2940
|
-
clearStrategy(strategyKey) {
|
|
2941
|
-
this.load();
|
|
2942
|
-
delete this.data.strategies[strategyKey];
|
|
2943
|
-
this.save();
|
|
2944
|
-
}
|
|
2945
|
-
hasStrategyPositions(strategyKey) {
|
|
2946
|
-
this.load();
|
|
2947
|
-
const bucket = this.data.strategies[strategyKey];
|
|
2948
|
-
if (!bucket) return false;
|
|
2949
|
-
return Object.values(bucket).some((p) => p.totalAmount > 0);
|
|
2950
|
-
}
|
|
2951
|
-
closeStrategyPosition(strategyKey, asset) {
|
|
2952
|
-
this.load();
|
|
2953
|
-
const bucket = this.data.strategies[strategyKey];
|
|
2954
|
-
if (!bucket?.[asset]) return;
|
|
2955
|
-
bucket[asset].totalAmount = 0;
|
|
2956
|
-
bucket[asset].costBasis = 0;
|
|
2957
|
-
bucket[asset].avgPrice = 0;
|
|
2958
|
-
const hasPositions = Object.values(bucket).some((p) => p.totalAmount > 0);
|
|
2959
|
-
if (!hasPositions) {
|
|
2960
|
-
delete this.data.strategies[strategyKey];
|
|
2961
|
-
}
|
|
2962
|
-
this.save();
|
|
2963
|
-
}
|
|
2964
|
-
};
|
|
2965
|
-
function emptyData2() {
|
|
2966
|
-
return { strategies: {} };
|
|
2967
|
-
}
|
|
2968
|
-
var StrategyManager = class {
|
|
2969
|
-
data = emptyData2();
|
|
2970
|
-
filePath;
|
|
2971
|
-
dir;
|
|
2972
|
-
seeded = false;
|
|
2973
|
-
constructor(configDir) {
|
|
2974
|
-
this.dir = configDir ?? join(homedir(), ".t2000");
|
|
2975
|
-
this.filePath = join(this.dir, "strategies.json");
|
|
2976
|
-
this.load();
|
|
2977
|
-
}
|
|
2978
|
-
load() {
|
|
2979
|
-
try {
|
|
2980
|
-
if (existsSync(this.filePath)) {
|
|
2981
|
-
this.data = JSON.parse(readFileSync(this.filePath, "utf-8"));
|
|
2982
|
-
}
|
|
2983
|
-
} catch {
|
|
2984
|
-
this.data = emptyData2();
|
|
2985
|
-
}
|
|
2986
|
-
if (!this.seeded) {
|
|
2987
|
-
this.seedDefaults();
|
|
2988
|
-
}
|
|
2989
|
-
}
|
|
2990
|
-
save() {
|
|
2991
|
-
if (!existsSync(this.dir)) mkdirSync(this.dir, { recursive: true });
|
|
2992
|
-
writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
|
|
2993
|
-
}
|
|
2994
|
-
seedDefaults() {
|
|
2995
|
-
this.seeded = true;
|
|
2996
|
-
let changed = false;
|
|
2997
|
-
for (const [key, def] of Object.entries(DEFAULT_STRATEGIES)) {
|
|
2998
|
-
if (!this.data.strategies[key]) {
|
|
2999
|
-
this.data.strategies[key] = { ...def, allocations: { ...def.allocations } };
|
|
3000
|
-
changed = true;
|
|
3001
|
-
}
|
|
3002
|
-
}
|
|
3003
|
-
if (changed) this.save();
|
|
3004
|
-
}
|
|
3005
|
-
getAll() {
|
|
3006
|
-
this.load();
|
|
3007
|
-
return { ...this.data.strategies };
|
|
3008
|
-
}
|
|
3009
|
-
get(name) {
|
|
3010
|
-
this.load();
|
|
3011
|
-
const strategy = this.data.strategies[name];
|
|
3012
|
-
if (!strategy) {
|
|
3013
|
-
throw new T2000Error("STRATEGY_NOT_FOUND", `Strategy '${name}' not found`);
|
|
3014
|
-
}
|
|
3015
|
-
return strategy;
|
|
3016
|
-
}
|
|
3017
|
-
create(params) {
|
|
3018
|
-
this.load();
|
|
3019
|
-
const key = params.name.toLowerCase().replace(/\s+/g, "-");
|
|
3020
|
-
if (this.data.strategies[key]) {
|
|
3021
|
-
throw new T2000Error("STRATEGY_INVALID_ALLOCATIONS", `Strategy '${key}' already exists`);
|
|
3022
|
-
}
|
|
3023
|
-
this.validateAllocations(params.allocations);
|
|
3024
|
-
const definition = {
|
|
3025
|
-
name: params.name,
|
|
3026
|
-
allocations: { ...params.allocations },
|
|
3027
|
-
description: params.description ?? `Custom strategy: ${params.name}`,
|
|
3028
|
-
custom: true
|
|
3029
|
-
};
|
|
3030
|
-
this.data.strategies[key] = definition;
|
|
3031
|
-
this.save();
|
|
3032
|
-
return definition;
|
|
3033
|
-
}
|
|
3034
|
-
delete(name) {
|
|
3035
|
-
this.load();
|
|
3036
|
-
const strategy = this.data.strategies[name];
|
|
3037
|
-
if (!strategy) {
|
|
3038
|
-
throw new T2000Error("STRATEGY_NOT_FOUND", `Strategy '${name}' not found`);
|
|
3039
|
-
}
|
|
3040
|
-
if (!strategy.custom) {
|
|
3041
|
-
throw new T2000Error("STRATEGY_BUILTIN", `Cannot delete built-in strategy '${name}'`);
|
|
3042
|
-
}
|
|
3043
|
-
delete this.data.strategies[name];
|
|
3044
|
-
this.save();
|
|
3045
|
-
}
|
|
3046
|
-
validateAllocations(allocations) {
|
|
3047
|
-
const total = Object.values(allocations).reduce((sum, pct) => sum + pct, 0);
|
|
3048
|
-
if (Math.abs(total - 100) > 0.01) {
|
|
3049
|
-
throw new T2000Error("STRATEGY_INVALID_ALLOCATIONS", `Allocations must sum to 100 (got ${total})`);
|
|
3050
|
-
}
|
|
3051
|
-
for (const asset of Object.keys(allocations)) {
|
|
3052
|
-
if (!(asset in INVESTMENT_ASSETS)) {
|
|
3053
|
-
throw new T2000Error("STRATEGY_INVALID_ALLOCATIONS", `${asset} is not an investment asset`);
|
|
3054
|
-
}
|
|
3055
|
-
if (allocations[asset] <= 0) {
|
|
3056
|
-
throw new T2000Error("STRATEGY_INVALID_ALLOCATIONS", `Allocation for ${asset} must be > 0`);
|
|
3057
|
-
}
|
|
3058
|
-
}
|
|
3059
|
-
}
|
|
3060
|
-
validateMinAmount(allocations, totalUsd) {
|
|
3061
|
-
const smallestPct = Math.min(...Object.values(allocations));
|
|
3062
|
-
const minRequired = Math.ceil(100 / smallestPct);
|
|
3063
|
-
if (totalUsd < minRequired) {
|
|
3064
|
-
const smallestAsset = Object.entries(allocations).find(([, p]) => p === smallestPct)?.[0] ?? "?";
|
|
3065
|
-
throw new T2000Error(
|
|
3066
|
-
"STRATEGY_MIN_AMOUNT",
|
|
3067
|
-
`Minimum $${minRequired} for this strategy (${smallestAsset} at ${smallestPct}% needs at least $1)`
|
|
3068
|
-
);
|
|
3069
|
-
}
|
|
3070
|
-
}
|
|
3071
|
-
};
|
|
3072
|
-
function emptyData3() {
|
|
3073
|
-
return { schedules: [] };
|
|
3074
|
-
}
|
|
3075
|
-
function computeNextRun(frequency, dayOfWeek, dayOfMonth, from) {
|
|
3076
|
-
const base = /* @__PURE__ */ new Date();
|
|
3077
|
-
const next = new Date(base);
|
|
3078
|
-
switch (frequency) {
|
|
3079
|
-
case "daily":
|
|
3080
|
-
next.setDate(next.getDate() + 1);
|
|
3081
|
-
next.setHours(0, 0, 0, 0);
|
|
3082
|
-
break;
|
|
3083
|
-
case "weekly": {
|
|
3084
|
-
const dow = dayOfWeek ?? 1;
|
|
3085
|
-
next.setDate(next.getDate() + ((7 - next.getDay() + dow) % 7 || 7));
|
|
3086
|
-
next.setHours(0, 0, 0, 0);
|
|
3087
|
-
break;
|
|
3088
|
-
}
|
|
3089
|
-
case "monthly": {
|
|
3090
|
-
const dom = dayOfMonth ?? 1;
|
|
3091
|
-
next.setMonth(next.getMonth() + 1, dom);
|
|
3092
|
-
next.setHours(0, 0, 0, 0);
|
|
3093
|
-
break;
|
|
3094
|
-
}
|
|
3095
|
-
}
|
|
3096
|
-
return next.toISOString();
|
|
3097
|
-
}
|
|
3098
|
-
var AutoInvestManager = class {
|
|
3099
|
-
data = emptyData3();
|
|
3100
|
-
filePath;
|
|
3101
|
-
dir;
|
|
3102
|
-
constructor(configDir) {
|
|
3103
|
-
this.dir = configDir ?? join(homedir(), ".t2000");
|
|
3104
|
-
this.filePath = join(this.dir, "auto-invest.json");
|
|
3105
|
-
this.load();
|
|
3106
|
-
}
|
|
3107
|
-
load() {
|
|
3108
|
-
try {
|
|
3109
|
-
if (existsSync(this.filePath)) {
|
|
3110
|
-
this.data = JSON.parse(readFileSync(this.filePath, "utf-8"));
|
|
3111
|
-
}
|
|
3112
|
-
} catch {
|
|
3113
|
-
this.data = emptyData3();
|
|
3114
|
-
}
|
|
3115
|
-
if (!this.data.schedules) {
|
|
3116
|
-
this.data.schedules = [];
|
|
3117
|
-
}
|
|
3118
|
-
}
|
|
3119
|
-
save() {
|
|
3120
|
-
if (!existsSync(this.dir)) mkdirSync(this.dir, { recursive: true });
|
|
3121
|
-
writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
|
|
3122
|
-
}
|
|
3123
|
-
setup(params) {
|
|
3124
|
-
this.load();
|
|
3125
|
-
if (!params.strategy && !params.asset) {
|
|
3126
|
-
throw new T2000Error("AUTO_INVEST_NOT_FOUND", "Either strategy or asset must be specified");
|
|
3127
|
-
}
|
|
3128
|
-
if (params.amount < 1) {
|
|
3129
|
-
throw new T2000Error("AUTO_INVEST_INSUFFICIENT", "Auto-invest amount must be at least $1");
|
|
3130
|
-
}
|
|
3131
|
-
const schedule = {
|
|
3132
|
-
id: randomUUID().slice(0, 8),
|
|
3133
|
-
strategy: params.strategy,
|
|
3134
|
-
asset: params.asset,
|
|
3135
|
-
amount: params.amount,
|
|
3136
|
-
frequency: params.frequency,
|
|
3137
|
-
dayOfWeek: params.dayOfWeek,
|
|
3138
|
-
dayOfMonth: params.dayOfMonth,
|
|
3139
|
-
nextRun: computeNextRun(params.frequency, params.dayOfWeek, params.dayOfMonth),
|
|
3140
|
-
enabled: true,
|
|
3141
|
-
totalInvested: 0,
|
|
3142
|
-
runCount: 0
|
|
3143
|
-
};
|
|
3144
|
-
this.data.schedules.push(schedule);
|
|
3145
|
-
this.save();
|
|
3146
|
-
return schedule;
|
|
3147
|
-
}
|
|
3148
|
-
getStatus() {
|
|
3149
|
-
this.load();
|
|
3150
|
-
const now = /* @__PURE__ */ new Date();
|
|
3151
|
-
const pending = this.data.schedules.filter(
|
|
3152
|
-
(s) => s.enabled && new Date(s.nextRun) <= now
|
|
3153
|
-
);
|
|
3154
|
-
return {
|
|
3155
|
-
schedules: [...this.data.schedules],
|
|
3156
|
-
pendingRuns: pending
|
|
3157
|
-
};
|
|
3158
|
-
}
|
|
3159
|
-
getSchedule(id) {
|
|
3160
|
-
this.load();
|
|
3161
|
-
const schedule = this.data.schedules.find((s) => s.id === id);
|
|
3162
|
-
if (!schedule) {
|
|
3163
|
-
throw new T2000Error("AUTO_INVEST_NOT_FOUND", `Schedule '${id}' not found`);
|
|
3164
|
-
}
|
|
3165
|
-
return schedule;
|
|
3166
|
-
}
|
|
3167
|
-
recordRun(id, amountInvested) {
|
|
3168
|
-
this.load();
|
|
3169
|
-
const schedule = this.data.schedules.find((s) => s.id === id);
|
|
3170
|
-
if (!schedule) return;
|
|
3171
|
-
schedule.lastRun = (/* @__PURE__ */ new Date()).toISOString();
|
|
3172
|
-
schedule.nextRun = computeNextRun(schedule.frequency, schedule.dayOfWeek, schedule.dayOfMonth);
|
|
3173
|
-
schedule.totalInvested += amountInvested;
|
|
3174
|
-
schedule.runCount += 1;
|
|
3175
|
-
this.save();
|
|
3176
|
-
}
|
|
3177
|
-
stop(id) {
|
|
3178
|
-
this.load();
|
|
3179
|
-
const schedule = this.data.schedules.find((s) => s.id === id);
|
|
3180
|
-
if (!schedule) {
|
|
3181
|
-
throw new T2000Error("AUTO_INVEST_NOT_FOUND", `Schedule '${id}' not found`);
|
|
3182
|
-
}
|
|
3183
|
-
schedule.enabled = false;
|
|
3184
|
-
this.save();
|
|
3185
|
-
}
|
|
3186
|
-
remove(id) {
|
|
3187
|
-
this.load();
|
|
3188
|
-
const idx = this.data.schedules.findIndex((s) => s.id === id);
|
|
3189
|
-
if (idx === -1) {
|
|
3190
|
-
throw new T2000Error("AUTO_INVEST_NOT_FOUND", `Schedule '${id}' not found`);
|
|
3191
|
-
}
|
|
3192
|
-
this.data.schedules.splice(idx, 1);
|
|
3193
|
-
this.save();
|
|
3194
|
-
}
|
|
3195
|
-
};
|
|
3196
|
-
var LOW_LIQUIDITY_ASSETS = /* @__PURE__ */ new Set(["GOLD"]);
|
|
3197
|
-
var REWARD_TOKEN_DECIMALS = {
|
|
3198
|
-
"0x549e8b69270defbfafd4f94e17ec44cdbdd99820b33bda2278dea3b9a32d3f55::cert::CERT": 9,
|
|
3199
|
-
"0xdeeb7a4662eec9f2f3def03fb937a663dddaa2e215b8078a284d026b7946c270::deep::DEEP": 6,
|
|
3200
|
-
"0x83556891f4a0f233ce7b05cfe7f957d4020492a34f5405b2cb9377d060bef4bf::spring_sui::SPRING_SUI": 9
|
|
3201
|
-
};
|
|
3202
|
-
function defaultSlippage(asset) {
|
|
3203
|
-
return LOW_LIQUIDITY_ASSETS.has(asset) ? 0.05 : 0.03;
|
|
3204
|
-
}
|
|
3205
2074
|
var DEFAULT_CONFIG_DIR = join(homedir(), ".t2000");
|
|
3206
2075
|
var T2000 = class _T2000 extends EventEmitter {
|
|
3207
2076
|
_signer;
|
|
@@ -3211,9 +2080,6 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
3211
2080
|
registry;
|
|
3212
2081
|
enforcer;
|
|
3213
2082
|
contacts;
|
|
3214
|
-
portfolio;
|
|
3215
|
-
strategies;
|
|
3216
|
-
autoInvest;
|
|
3217
2083
|
constructor(keypairOrSigner, client, registry, configDir, isSignerMode) {
|
|
3218
2084
|
super();
|
|
3219
2085
|
if (isSignerMode) {
|
|
@@ -3231,21 +2097,12 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
3231
2097
|
this.enforcer = new SafeguardEnforcer(configDir);
|
|
3232
2098
|
this.enforcer.load();
|
|
3233
2099
|
this.contacts = new ContactManager(configDir);
|
|
3234
|
-
this.portfolio = new PortfolioManager(configDir);
|
|
3235
|
-
this.strategies = new StrategyManager(configDir);
|
|
3236
|
-
this.autoInvest = new AutoInvestManager(configDir);
|
|
3237
2100
|
}
|
|
3238
2101
|
static createDefaultRegistry(client) {
|
|
3239
2102
|
const registry = new ProtocolRegistry();
|
|
3240
2103
|
const naviAdapter = new NaviAdapter();
|
|
3241
2104
|
naviAdapter.initSync(client);
|
|
3242
2105
|
registry.registerLending(naviAdapter);
|
|
3243
|
-
const cetusAdapter = new CetusAdapter();
|
|
3244
|
-
cetusAdapter.initSync(client);
|
|
3245
|
-
registry.registerSwap(cetusAdapter);
|
|
3246
|
-
const suilendAdapter = new SuilendAdapter();
|
|
3247
|
-
suilendAdapter.initSync(client);
|
|
3248
|
-
registry.registerLending(suilendAdapter);
|
|
3249
2106
|
return registry;
|
|
3250
2107
|
}
|
|
3251
2108
|
static async create(options = {}) {
|
|
@@ -3321,14 +2178,15 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
3321
2178
|
this.enforcer.assertNotLocked();
|
|
3322
2179
|
this.enforcer.check({ operation: "pay", amount: options.maxPrice ?? 1 });
|
|
3323
2180
|
const { Mppx } = await import('mppx/client');
|
|
3324
|
-
const { sui } = await import('@
|
|
2181
|
+
const { sui } = await import('@suimpp/mpp/client');
|
|
3325
2182
|
const client = this.client;
|
|
3326
2183
|
const signer = this._signer;
|
|
2184
|
+
const signerAddress = signer.getAddress();
|
|
3327
2185
|
const mppx = Mppx.create({
|
|
3328
2186
|
polyfill: false,
|
|
3329
2187
|
methods: [sui({
|
|
3330
2188
|
client,
|
|
3331
|
-
signer,
|
|
2189
|
+
signer: { toSuiAddress: () => signerAddress },
|
|
3332
2190
|
execute: async (tx) => {
|
|
3333
2191
|
const result = await executeWithGas(client, signer, () => tx);
|
|
3334
2192
|
return { digest: result.digest, effects: result.effects };
|
|
@@ -3355,11 +2213,156 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
3355
2213
|
this.enforcer.recordUsage(options.maxPrice ?? 1);
|
|
3356
2214
|
}
|
|
3357
2215
|
return {
|
|
3358
|
-
status: response.status,
|
|
3359
|
-
body,
|
|
3360
|
-
paid,
|
|
3361
|
-
cost: paid ? options.maxPrice ?? void 0 : void 0,
|
|
3362
|
-
receipt: receiptHeader ? { reference: receiptHeader, timestamp: (/* @__PURE__ */ new Date()).toISOString() } : void 0
|
|
2216
|
+
status: response.status,
|
|
2217
|
+
body,
|
|
2218
|
+
paid,
|
|
2219
|
+
cost: paid ? options.maxPrice ?? void 0 : void 0,
|
|
2220
|
+
receipt: receiptHeader ? { reference: receiptHeader, timestamp: (/* @__PURE__ */ new Date()).toISOString() } : void 0
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
// -- VOLO vSUI Staking --
|
|
2224
|
+
async stakeVSui(params) {
|
|
2225
|
+
this.enforcer.assertNotLocked();
|
|
2226
|
+
const { buildStakeVSuiTx: buildStakeVSuiTx2, getVoloStats: getVoloStats2 } = await Promise.resolve().then(() => (init_volo(), volo_exports));
|
|
2227
|
+
const amountMist = BigInt(Math.floor(params.amount * Number(MIST_PER_SUI)));
|
|
2228
|
+
const stats = await getVoloStats2();
|
|
2229
|
+
const gasResult = await executeWithGas(this.client, this._signer, async () => {
|
|
2230
|
+
return buildStakeVSuiTx2(this.client, this._address, amountMist);
|
|
2231
|
+
});
|
|
2232
|
+
const vSuiReceived = params.amount / stats.exchangeRate;
|
|
2233
|
+
return {
|
|
2234
|
+
success: true,
|
|
2235
|
+
tx: gasResult.digest,
|
|
2236
|
+
amountSui: params.amount,
|
|
2237
|
+
vSuiReceived,
|
|
2238
|
+
apy: stats.apy,
|
|
2239
|
+
gasCost: gasResult.gasCostSui,
|
|
2240
|
+
gasMethod: gasResult.gasMethod
|
|
2241
|
+
};
|
|
2242
|
+
}
|
|
2243
|
+
async unstakeVSui(params) {
|
|
2244
|
+
this.enforcer.assertNotLocked();
|
|
2245
|
+
const { buildUnstakeVSuiTx: buildUnstakeVSuiTx2, getVoloStats: getVoloStats2, VSUI_TYPE: VSUI_TYPE2 } = await Promise.resolve().then(() => (init_volo(), volo_exports));
|
|
2246
|
+
let amountMist;
|
|
2247
|
+
let vSuiAmount;
|
|
2248
|
+
if (params.amount === "all") {
|
|
2249
|
+
amountMist = "all";
|
|
2250
|
+
const coins = await this._fetchCoins(VSUI_TYPE2);
|
|
2251
|
+
vSuiAmount = coins.reduce((sum, c) => sum + Number(c.balance), 0) / 1e9;
|
|
2252
|
+
} else {
|
|
2253
|
+
amountMist = BigInt(Math.floor(params.amount * 1e9));
|
|
2254
|
+
vSuiAmount = params.amount;
|
|
2255
|
+
}
|
|
2256
|
+
const stats = await getVoloStats2();
|
|
2257
|
+
const gasResult = await executeWithGas(this.client, this._signer, async () => {
|
|
2258
|
+
return buildUnstakeVSuiTx2(this.client, this._address, amountMist);
|
|
2259
|
+
});
|
|
2260
|
+
const suiReceived = vSuiAmount * stats.exchangeRate;
|
|
2261
|
+
return {
|
|
2262
|
+
success: true,
|
|
2263
|
+
tx: gasResult.digest,
|
|
2264
|
+
vSuiAmount,
|
|
2265
|
+
suiReceived,
|
|
2266
|
+
gasCost: gasResult.gasCostSui,
|
|
2267
|
+
gasMethod: gasResult.gasMethod
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
// -- Swap --
|
|
2271
|
+
async swap(params) {
|
|
2272
|
+
this.enforcer.assertNotLocked();
|
|
2273
|
+
const { findSwapRoute: findSwapRoute2, buildSwapTx: buildSwapTx2, resolveTokenType: resolveTokenType2, TOKEN_MAP: TOKEN_MAP2 } = await Promise.resolve().then(() => (init_cetus_swap(), cetus_swap_exports));
|
|
2274
|
+
const fromType = resolveTokenType2(params.from);
|
|
2275
|
+
const toType = resolveTokenType2(params.to);
|
|
2276
|
+
if (!fromType) throw new T2000Error("ASSET_NOT_SUPPORTED", `Unknown token: ${params.from}. Provide the full coin type.`);
|
|
2277
|
+
if (!toType) throw new T2000Error("ASSET_NOT_SUPPORTED", `Unknown token: ${params.to}. Provide the full coin type.`);
|
|
2278
|
+
const byAmountIn = params.byAmountIn ?? true;
|
|
2279
|
+
const slippage = Math.min(params.slippage ?? 0.01, 0.05);
|
|
2280
|
+
const fromEntry = Object.values(TOKEN_MAP2).includes(fromType) ? Object.entries(SUPPORTED_ASSETS).find(([, v]) => v.type === fromType) : null;
|
|
2281
|
+
const fromDecimals = fromEntry ? fromEntry[1].decimals : fromType === "0x2::sui::SUI" ? 9 : 6;
|
|
2282
|
+
const rawAmount = BigInt(Math.floor(params.amount * 10 ** fromDecimals));
|
|
2283
|
+
const route = await findSwapRoute2({
|
|
2284
|
+
walletAddress: this._address,
|
|
2285
|
+
from: fromType,
|
|
2286
|
+
to: toType,
|
|
2287
|
+
amount: rawAmount,
|
|
2288
|
+
byAmountIn
|
|
2289
|
+
});
|
|
2290
|
+
if (!route) throw new T2000Error("SWAP_NO_ROUTE", `No swap route found for ${params.from} -> ${params.to}.`);
|
|
2291
|
+
if (route.insufficientLiquidity) throw new T2000Error("SWAP_NO_ROUTE", `Insufficient liquidity for ${params.from} -> ${params.to}.`);
|
|
2292
|
+
if (route.priceImpact > 0.05) {
|
|
2293
|
+
console.warn(`[swap] High price impact: ${(route.priceImpact * 100).toFixed(2)}%`);
|
|
2294
|
+
}
|
|
2295
|
+
const gasResult = await executeWithGas(this.client, this._signer, async () => {
|
|
2296
|
+
const tx = new Transaction();
|
|
2297
|
+
tx.setSender(this._address);
|
|
2298
|
+
let inputCoin;
|
|
2299
|
+
if (fromType === "0x2::sui::SUI") {
|
|
2300
|
+
[inputCoin] = tx.splitCoins(tx.gas, [rawAmount]);
|
|
2301
|
+
} else {
|
|
2302
|
+
const coins = await this._fetchCoins(fromType);
|
|
2303
|
+
if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${params.from} coins found.`);
|
|
2304
|
+
const merged = this._mergeCoinsInTx(tx, coins);
|
|
2305
|
+
[inputCoin] = tx.splitCoins(merged, [rawAmount]);
|
|
2306
|
+
}
|
|
2307
|
+
const outputCoin = await buildSwapTx2({
|
|
2308
|
+
walletAddress: this._address,
|
|
2309
|
+
route,
|
|
2310
|
+
tx,
|
|
2311
|
+
inputCoin,
|
|
2312
|
+
slippage
|
|
2313
|
+
});
|
|
2314
|
+
tx.transferObjects([outputCoin], this._address);
|
|
2315
|
+
return tx;
|
|
2316
|
+
});
|
|
2317
|
+
const toEntry = Object.entries(SUPPORTED_ASSETS).find(([, v]) => v.type === toType);
|
|
2318
|
+
const toDecimals = toEntry ? toEntry[1].decimals : toType === "0x2::sui::SUI" ? 9 : 6;
|
|
2319
|
+
const fromAmount = Number(route.amountIn) / 10 ** fromDecimals;
|
|
2320
|
+
const toAmount = Number(route.amountOut) / 10 ** toDecimals;
|
|
2321
|
+
const routeDesc = route.routerData.paths?.map((p) => p.provider).filter(Boolean).slice(0, 3).join(" + ") ?? "Cetus Aggregator";
|
|
2322
|
+
return {
|
|
2323
|
+
success: true,
|
|
2324
|
+
tx: gasResult.digest,
|
|
2325
|
+
fromToken: params.from,
|
|
2326
|
+
toToken: params.to,
|
|
2327
|
+
fromAmount,
|
|
2328
|
+
toAmount,
|
|
2329
|
+
priceImpact: route.priceImpact,
|
|
2330
|
+
route: routeDesc,
|
|
2331
|
+
gasCost: gasResult.gasCostSui,
|
|
2332
|
+
gasMethod: gasResult.gasMethod
|
|
2333
|
+
};
|
|
2334
|
+
}
|
|
2335
|
+
async swapQuote(params) {
|
|
2336
|
+
const { findSwapRoute: findSwapRoute2, resolveTokenType: resolveTokenType2, TOKEN_MAP: TOKEN_MAP2 } = await Promise.resolve().then(() => (init_cetus_swap(), cetus_swap_exports));
|
|
2337
|
+
const fromType = resolveTokenType2(params.from);
|
|
2338
|
+
const toType = resolveTokenType2(params.to);
|
|
2339
|
+
if (!fromType) throw new T2000Error("ASSET_NOT_SUPPORTED", `Unknown token: ${params.from}. Provide the full coin type.`);
|
|
2340
|
+
if (!toType) throw new T2000Error("ASSET_NOT_SUPPORTED", `Unknown token: ${params.to}. Provide the full coin type.`);
|
|
2341
|
+
const byAmountIn = params.byAmountIn ?? true;
|
|
2342
|
+
const fromEntry = Object.values(TOKEN_MAP2).includes(fromType) ? Object.entries(SUPPORTED_ASSETS).find(([, v]) => v.type === fromType) : null;
|
|
2343
|
+
const fromDecimals = fromEntry ? fromEntry[1].decimals : fromType === "0x2::sui::SUI" ? 9 : 6;
|
|
2344
|
+
const rawAmount = BigInt(Math.floor(params.amount * 10 ** fromDecimals));
|
|
2345
|
+
const route = await findSwapRoute2({
|
|
2346
|
+
walletAddress: this._address,
|
|
2347
|
+
from: fromType,
|
|
2348
|
+
to: toType,
|
|
2349
|
+
amount: rawAmount,
|
|
2350
|
+
byAmountIn
|
|
2351
|
+
});
|
|
2352
|
+
if (!route) throw new T2000Error("SWAP_NO_ROUTE", `No swap route found for ${params.from} -> ${params.to}.`);
|
|
2353
|
+
if (route.insufficientLiquidity) throw new T2000Error("SWAP_NO_ROUTE", `Insufficient liquidity for ${params.from} -> ${params.to}.`);
|
|
2354
|
+
const toEntry = Object.entries(SUPPORTED_ASSETS).find(([, v]) => v.type === toType);
|
|
2355
|
+
const toDecimals = toEntry ? toEntry[1].decimals : toType === "0x2::sui::SUI" ? 9 : 6;
|
|
2356
|
+
const fromAmount = Number(route.amountIn) / 10 ** fromDecimals;
|
|
2357
|
+
const toAmount = Number(route.amountOut) / 10 ** toDecimals;
|
|
2358
|
+
const routeDesc = route.routerData.paths?.map((p) => p.provider).filter(Boolean).slice(0, 3).join(" + ") ?? "Cetus Aggregator";
|
|
2359
|
+
return {
|
|
2360
|
+
fromToken: params.from,
|
|
2361
|
+
toToken: params.to,
|
|
2362
|
+
fromAmount,
|
|
2363
|
+
toAmount,
|
|
2364
|
+
priceImpact: route.priceImpact,
|
|
2365
|
+
route: routeDesc
|
|
3363
2366
|
};
|
|
3364
2367
|
}
|
|
3365
2368
|
// -- Wallet --
|
|
@@ -3398,38 +2401,14 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
3398
2401
|
}
|
|
3399
2402
|
async balance() {
|
|
3400
2403
|
const bal = await queryBalance(this.client, this._address);
|
|
3401
|
-
const portfolioPositions = this.portfolio.getPositions();
|
|
3402
|
-
const earningAssets = new Set(
|
|
3403
|
-
portfolioPositions.filter((p) => p.earning).map((p) => p.asset)
|
|
3404
|
-
);
|
|
3405
|
-
const suiPrice = bal.gasReserve.sui > 0 ? bal.gasReserve.usdEquiv / bal.gasReserve.sui : 0;
|
|
3406
|
-
const assetPrices = { SUI: suiPrice };
|
|
3407
|
-
const swapAdapter = this.registry.listSwap()[0];
|
|
3408
|
-
for (const asset of Object.keys(INVESTMENT_ASSETS)) {
|
|
3409
|
-
if (asset === "SUI") continue;
|
|
3410
|
-
try {
|
|
3411
|
-
if (swapAdapter) {
|
|
3412
|
-
const quote = await swapAdapter.getQuote("USDC", asset, 1);
|
|
3413
|
-
assetPrices[asset] = quote.expectedOutput > 0 ? 1 / quote.expectedOutput : 0;
|
|
3414
|
-
}
|
|
3415
|
-
} catch {
|
|
3416
|
-
assetPrices[asset] = 0;
|
|
3417
|
-
}
|
|
3418
|
-
}
|
|
3419
2404
|
let chainTotal = bal.available + bal.gasReserve.usdEquiv;
|
|
3420
|
-
for (const asset of Object.keys(INVESTMENT_ASSETS)) {
|
|
3421
|
-
if (asset === "SUI") continue;
|
|
3422
|
-
chainTotal += (bal.assets[asset] ?? 0) * (assetPrices[asset] ?? 0);
|
|
3423
|
-
}
|
|
3424
2405
|
try {
|
|
3425
2406
|
const positions = await this.positions();
|
|
3426
2407
|
for (const pos of positions.positions) {
|
|
3427
2408
|
const usdValue = pos.amountUsd ?? pos.amount;
|
|
3428
2409
|
if (pos.type === "save") {
|
|
3429
2410
|
chainTotal += usdValue;
|
|
3430
|
-
|
|
3431
|
-
bal.savings += usdValue;
|
|
3432
|
-
}
|
|
2411
|
+
bal.savings += usdValue;
|
|
3433
2412
|
} else if (pos.type === "borrow") {
|
|
3434
2413
|
chainTotal -= usdValue;
|
|
3435
2414
|
bal.debt += usdValue;
|
|
@@ -3437,50 +2416,6 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
3437
2416
|
}
|
|
3438
2417
|
} catch {
|
|
3439
2418
|
}
|
|
3440
|
-
try {
|
|
3441
|
-
const trackedAmounts = {};
|
|
3442
|
-
const trackedCostBasis = {};
|
|
3443
|
-
const earningAssetSet = /* @__PURE__ */ new Set();
|
|
3444
|
-
for (const pos of portfolioPositions) {
|
|
3445
|
-
if (!(pos.asset in INVESTMENT_ASSETS)) continue;
|
|
3446
|
-
trackedAmounts[pos.asset] = (trackedAmounts[pos.asset] ?? 0) + pos.totalAmount;
|
|
3447
|
-
trackedCostBasis[pos.asset] = (trackedCostBasis[pos.asset] ?? 0) + pos.costBasis;
|
|
3448
|
-
if (pos.earning) earningAssetSet.add(pos.asset);
|
|
3449
|
-
}
|
|
3450
|
-
let investmentValue = 0;
|
|
3451
|
-
let investmentCostBasis = 0;
|
|
3452
|
-
let trackedValue = 0;
|
|
3453
|
-
for (const asset of Object.keys(INVESTMENT_ASSETS)) {
|
|
3454
|
-
const price = assetPrices[asset] ?? 0;
|
|
3455
|
-
const tracked = trackedAmounts[asset] ?? 0;
|
|
3456
|
-
const costBasis = trackedCostBasis[asset] ?? 0;
|
|
3457
|
-
if (asset === "SUI") {
|
|
3458
|
-
const actualSui = earningAssetSet.has("SUI") ? tracked : Math.min(tracked, bal.gasReserve.sui);
|
|
3459
|
-
investmentValue += actualSui * price;
|
|
3460
|
-
trackedValue += actualSui * price;
|
|
3461
|
-
if (actualSui < tracked && tracked > 0) {
|
|
3462
|
-
investmentCostBasis += costBasis * (actualSui / tracked);
|
|
3463
|
-
} else {
|
|
3464
|
-
investmentCostBasis += costBasis;
|
|
3465
|
-
}
|
|
3466
|
-
if (!earningAssetSet.has("SUI")) {
|
|
3467
|
-
const gasSui = Math.max(0, bal.gasReserve.sui - tracked);
|
|
3468
|
-
bal.gasReserve = { sui: gasSui, usdEquiv: gasSui * price };
|
|
3469
|
-
}
|
|
3470
|
-
} else {
|
|
3471
|
-
const onChainAmount = bal.assets[asset] ?? 0;
|
|
3472
|
-
const effectiveAmount = Math.max(tracked, onChainAmount);
|
|
3473
|
-
investmentValue += effectiveAmount * price;
|
|
3474
|
-
trackedValue += tracked * price;
|
|
3475
|
-
investmentCostBasis += costBasis;
|
|
3476
|
-
}
|
|
3477
|
-
}
|
|
3478
|
-
bal.investment = investmentValue;
|
|
3479
|
-
bal.investmentPnL = trackedValue - investmentCostBasis;
|
|
3480
|
-
} catch {
|
|
3481
|
-
bal.investment = 0;
|
|
3482
|
-
bal.investmentPnL = 0;
|
|
3483
|
-
}
|
|
3484
2419
|
try {
|
|
3485
2420
|
const pendingRewards = await this.getPendingRewards();
|
|
3486
2421
|
bal.pendingRewards = pendingRewards.reduce((s, r) => s + r.estimatedValueUsd, 0);
|
|
@@ -3499,25 +2434,19 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
3499
2434
|
async deposit() {
|
|
3500
2435
|
return {
|
|
3501
2436
|
address: this._address,
|
|
3502
|
-
network: "
|
|
3503
|
-
supportedAssets: ["USDC"],
|
|
2437
|
+
network: "mainnet",
|
|
2438
|
+
supportedAssets: ["USDC", "USDT", "SUI"],
|
|
3504
2439
|
instructions: [
|
|
3505
|
-
`Send USDC
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
`
|
|
3509
|
-
` 2. Select "Sui" network`,
|
|
3510
|
-
` 3. Paste address: ${truncateAddress(this._address)}`,
|
|
3511
|
-
"",
|
|
3512
|
-
"From another Sui wallet:",
|
|
3513
|
-
` Transfer USDC to ${truncateAddress(this._address)}`
|
|
2440
|
+
`Send USDC to: ${this._address}`,
|
|
2441
|
+
`Network: Sui Mainnet`,
|
|
2442
|
+
`Or buy USDC on an exchange and withdraw to this address.`,
|
|
2443
|
+
`USDC contract: ${SUPPORTED_ASSETS.USDC.type}`
|
|
3514
2444
|
].join("\n")
|
|
3515
2445
|
};
|
|
3516
2446
|
}
|
|
3517
2447
|
exportKey() {
|
|
3518
2448
|
return exportPrivateKey(this.keypair);
|
|
3519
2449
|
}
|
|
3520
|
-
/** Create a T2000 instance from zkLogin credentials (for web app). */
|
|
3521
2450
|
static fromZkLogin(opts) {
|
|
3522
2451
|
const signer = new ZkLoginSigner(opts.ephemeralKeypair, opts.zkProof, opts.userAddress, opts.maxEpoch);
|
|
3523
2452
|
const client = getSuiClient(opts.rpcUrl);
|
|
@@ -3525,16 +2454,15 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
3525
2454
|
}
|
|
3526
2455
|
async registerAdapter(adapter) {
|
|
3527
2456
|
await adapter.init(this.client);
|
|
3528
|
-
|
|
3529
|
-
if ("buildSwapTx" in adapter) this.registry.registerSwap(adapter);
|
|
2457
|
+
this.registry.registerLending(adapter);
|
|
3530
2458
|
}
|
|
3531
2459
|
// -- Savings --
|
|
3532
2460
|
async save(params) {
|
|
3533
2461
|
this.enforcer.assertNotLocked();
|
|
3534
|
-
const asset = "USDC";
|
|
2462
|
+
const asset = params.asset ?? "USDC";
|
|
2463
|
+
const assetInfo = SUPPORTED_ASSETS[asset];
|
|
2464
|
+
if (!assetInfo) throw new T2000Error("ASSET_NOT_SUPPORTED", `Unsupported asset: ${asset}`);
|
|
3535
2465
|
const bal = await queryBalance(this.client, this._address);
|
|
3536
|
-
const usdcBalance = bal.stables.USDC ?? 0;
|
|
3537
|
-
const needsAutoConvert = params.amount === "all" ? Object.entries(bal.stables).some(([k, v]) => k !== "USDC" && v > 0.01) : typeof params.amount === "number" && params.amount > usdcBalance;
|
|
3538
2466
|
let amount;
|
|
3539
2467
|
if (params.amount === "all") {
|
|
3540
2468
|
amount = (bal.available ?? 0) - 1;
|
|
@@ -3553,54 +2481,25 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
3553
2481
|
const fee = calculateFee("save", amount);
|
|
3554
2482
|
const saveAmount = amount;
|
|
3555
2483
|
const adapter = await this.resolveLending(params.protocol, asset, "save");
|
|
3556
|
-
const
|
|
3557
|
-
const canPTB = adapter.addSaveToTx && (!needsAutoConvert || swapAdapter?.addSwapToTx);
|
|
2484
|
+
const canPTB = !!adapter.addSaveToTx;
|
|
3558
2485
|
const gasResult = await executeWithGas(this.client, this._signer, async () => {
|
|
3559
|
-
if (canPTB
|
|
2486
|
+
if (canPTB) {
|
|
3560
2487
|
const tx2 = new Transaction();
|
|
3561
2488
|
tx2.setSender(this._address);
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
2489
|
+
let inputCoin;
|
|
2490
|
+
if (asset === "SUI") {
|
|
2491
|
+
const rawAmount = BigInt(Math.floor(saveAmount * 10 ** assetInfo.decimals));
|
|
2492
|
+
[inputCoin] = tx2.splitCoins(tx2.gas, [rawAmount]);
|
|
2493
|
+
} else {
|
|
3567
2494
|
const coins = await this._fetchCoins(assetInfo.type);
|
|
3568
|
-
if (coins.length === 0)
|
|
2495
|
+
if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${asset} coins found`);
|
|
3569
2496
|
const merged = this._mergeCoinsInTx(tx2, coins);
|
|
3570
|
-
const
|
|
3571
|
-
|
|
3572
|
-
this._address,
|
|
3573
|
-
merged,
|
|
3574
|
-
stableAsset,
|
|
3575
|
-
"USDC",
|
|
3576
|
-
stableAmount
|
|
3577
|
-
);
|
|
3578
|
-
usdcCoins.push(outputCoin);
|
|
3579
|
-
}
|
|
3580
|
-
const existingUsdc = await this._fetchCoins(SUPPORTED_ASSETS.USDC.type);
|
|
3581
|
-
if (existingUsdc.length > 0) {
|
|
3582
|
-
usdcCoins.push(this._mergeCoinsInTx(tx2, existingUsdc));
|
|
2497
|
+
const rawAmount = BigInt(Math.floor(saveAmount * 10 ** assetInfo.decimals));
|
|
2498
|
+
[inputCoin] = tx2.splitCoins(merged, [rawAmount]);
|
|
3583
2499
|
}
|
|
3584
|
-
|
|
3585
|
-
tx2.mergeCoins(usdcCoins[0], usdcCoins.slice(1));
|
|
3586
|
-
}
|
|
3587
|
-
await adapter.addSaveToTx(tx2, this._address, usdcCoins[0], asset, { collectFee: true });
|
|
3588
|
-
return tx2;
|
|
3589
|
-
}
|
|
3590
|
-
if (canPTB && !needsAutoConvert) {
|
|
3591
|
-
const tx2 = new Transaction();
|
|
3592
|
-
tx2.setSender(this._address);
|
|
3593
|
-
const existingUsdc = await this._fetchCoins(SUPPORTED_ASSETS.USDC.type);
|
|
3594
|
-
if (existingUsdc.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins found");
|
|
3595
|
-
const merged = this._mergeCoinsInTx(tx2, existingUsdc);
|
|
3596
|
-
const rawAmount = BigInt(Math.floor(saveAmount * 10 ** SUPPORTED_ASSETS.USDC.decimals));
|
|
3597
|
-
const [depositCoin] = tx2.splitCoins(merged, [rawAmount]);
|
|
3598
|
-
await adapter.addSaveToTx(tx2, this._address, depositCoin, asset, { collectFee: true });
|
|
2500
|
+
await adapter.addSaveToTx(tx2, this._address, inputCoin, asset, { collectFee: true });
|
|
3599
2501
|
return tx2;
|
|
3600
2502
|
}
|
|
3601
|
-
if (needsAutoConvert) {
|
|
3602
|
-
await this._convertWalletStablesToUsdc(bal, params.amount === "all" ? void 0 : amount - usdcBalance);
|
|
3603
|
-
}
|
|
3604
2503
|
const { tx } = await adapter.buildSaveTx(this._address, saveAmount, asset, { collectFee: true });
|
|
3605
2504
|
return tx;
|
|
3606
2505
|
});
|
|
@@ -3633,24 +2532,22 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
3633
2532
|
}
|
|
3634
2533
|
async withdraw(params) {
|
|
3635
2534
|
this.enforcer.assertNotLocked();
|
|
3636
|
-
if (params.amount === "all" && !params.protocol) {
|
|
2535
|
+
if (params.amount === "all" && !params.protocol && !params.asset) {
|
|
3637
2536
|
return this.withdrawAllProtocols();
|
|
3638
2537
|
}
|
|
3639
2538
|
const allPositions = await this.registry.allPositions(this._address);
|
|
3640
|
-
const earningAssets = new Set(
|
|
3641
|
-
this.portfolio.getPositions().filter((p) => p.earning).map((p) => p.asset)
|
|
3642
|
-
);
|
|
3643
2539
|
const supplies = [];
|
|
3644
2540
|
for (const pos of allPositions) {
|
|
3645
2541
|
if (params.protocol && pos.protocolId !== params.protocol) continue;
|
|
3646
2542
|
for (const s of pos.positions.supplies) {
|
|
3647
|
-
if (s.amount > 1e-3
|
|
2543
|
+
if (s.amount > 1e-3) {
|
|
2544
|
+
if (params.asset && s.asset !== params.asset) continue;
|
|
3648
2545
|
supplies.push({ protocolId: pos.protocolId, asset: s.asset, amount: s.amount, apy: s.apy });
|
|
3649
2546
|
}
|
|
3650
2547
|
}
|
|
3651
2548
|
}
|
|
3652
2549
|
if (supplies.length === 0) {
|
|
3653
|
-
throw new T2000Error("NO_COLLATERAL", "No savings to withdraw");
|
|
2550
|
+
throw new T2000Error("NO_COLLATERAL", params.asset ? `No ${params.asset} savings to withdraw` : "No savings to withdraw");
|
|
3654
2551
|
}
|
|
3655
2552
|
supplies.sort((a, b) => {
|
|
3656
2553
|
const aIsUsdc = a.asset === "USDC" ? 0 : 1;
|
|
@@ -3688,42 +2585,19 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
3688
2585
|
}
|
|
3689
2586
|
const withdrawAmount = amount;
|
|
3690
2587
|
let finalAmount = withdrawAmount;
|
|
3691
|
-
const swapAdapter = target.asset !== "USDC" ? this.registry.listSwap()[0] : void 0;
|
|
3692
|
-
const canPTB = adapter.addWithdrawToTx && (!swapAdapter || swapAdapter.addSwapToTx);
|
|
3693
2588
|
const gasResult = await executeWithGas(this.client, this._signer, async () => {
|
|
3694
|
-
if (
|
|
2589
|
+
if (adapter.addWithdrawToTx) {
|
|
3695
2590
|
const tx = new Transaction();
|
|
3696
2591
|
tx.setSender(this._address);
|
|
3697
2592
|
const { coin, effectiveAmount } = await adapter.addWithdrawToTx(tx, this._address, withdrawAmount, target.asset);
|
|
3698
2593
|
finalAmount = effectiveAmount;
|
|
3699
|
-
|
|
3700
|
-
const { outputCoin, estimatedOut, toDecimals } = await swapAdapter.addSwapToTx(
|
|
3701
|
-
tx,
|
|
3702
|
-
this._address,
|
|
3703
|
-
coin,
|
|
3704
|
-
target.asset,
|
|
3705
|
-
"USDC",
|
|
3706
|
-
effectiveAmount,
|
|
3707
|
-
500
|
|
3708
|
-
);
|
|
3709
|
-
finalAmount = estimatedOut / 10 ** toDecimals;
|
|
3710
|
-
tx.transferObjects([outputCoin], this._address);
|
|
3711
|
-
} else {
|
|
3712
|
-
tx.transferObjects([coin], this._address);
|
|
3713
|
-
}
|
|
2594
|
+
tx.transferObjects([coin], this._address);
|
|
3714
2595
|
return tx;
|
|
3715
2596
|
}
|
|
3716
2597
|
const built = await adapter.buildWithdrawTx(this._address, withdrawAmount, target.asset);
|
|
3717
2598
|
finalAmount = built.effectiveAmount;
|
|
3718
2599
|
return built.tx;
|
|
3719
2600
|
});
|
|
3720
|
-
if (target.asset !== "USDC") {
|
|
3721
|
-
try {
|
|
3722
|
-
const postBal = await queryBalance(this.client, this._address);
|
|
3723
|
-
await this._convertWalletStablesToUsdc(postBal);
|
|
3724
|
-
} catch {
|
|
3725
|
-
}
|
|
3726
|
-
}
|
|
3727
2601
|
this.emitBalanceChange("USDC", finalAmount, "withdraw", gasResult.digest);
|
|
3728
2602
|
return {
|
|
3729
2603
|
success: true,
|
|
@@ -3735,13 +2609,10 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
3735
2609
|
}
|
|
3736
2610
|
async withdrawAllProtocols() {
|
|
3737
2611
|
const allPositions = await this.registry.allPositions(this._address);
|
|
3738
|
-
const earningAssets = new Set(
|
|
3739
|
-
this.portfolio.getPositions().filter((p) => p.earning).map((p) => p.asset)
|
|
3740
|
-
);
|
|
3741
2612
|
const withdrawable = [];
|
|
3742
2613
|
for (const pos of allPositions) {
|
|
3743
2614
|
for (const supply of pos.positions.supplies) {
|
|
3744
|
-
if (supply.amount > 0.01
|
|
2615
|
+
if (supply.amount > 0.01) {
|
|
3745
2616
|
withdrawable.push({ protocolId: pos.protocolId, asset: supply.asset, amount: supply.amount });
|
|
3746
2617
|
}
|
|
3747
2618
|
}
|
|
@@ -3768,87 +2639,39 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
3768
2639
|
if (entries.length === 0) {
|
|
3769
2640
|
throw new T2000Error("NO_COLLATERAL", "No savings to withdraw across any protocol");
|
|
3770
2641
|
}
|
|
3771
|
-
|
|
3772
|
-
const
|
|
3773
|
-
const dustEntries = entries.filter((e) => e.asset !== "USDC" && e.maxAmount < DUST_SWAP_THRESHOLD);
|
|
3774
|
-
const hasNonUsdc = swappableEntries.some((e) => e.asset !== "USDC");
|
|
3775
|
-
const swapAdapter = hasNonUsdc ? this.registry.listSwap()[0] : void 0;
|
|
3776
|
-
const canPTB = swappableEntries.every((e) => e.adapter.addWithdrawToTx) && (!swapAdapter || swapAdapter.addSwapToTx);
|
|
3777
|
-
let totalUsdcReceived = 0;
|
|
2642
|
+
let totalReceived = 0;
|
|
2643
|
+
const canPTB = entries.every((e) => e.adapter.addWithdrawToTx);
|
|
3778
2644
|
const gasResult = await executeWithGas(this.client, this._signer, async () => {
|
|
3779
2645
|
if (canPTB) {
|
|
3780
2646
|
const tx = new Transaction();
|
|
3781
2647
|
tx.setSender(this._address);
|
|
3782
|
-
const
|
|
3783
|
-
for (const entry of swappableEntries) {
|
|
2648
|
+
for (const entry of entries) {
|
|
3784
2649
|
const { coin, effectiveAmount } = await entry.adapter.addWithdrawToTx(
|
|
3785
2650
|
tx,
|
|
3786
2651
|
this._address,
|
|
3787
2652
|
entry.maxAmount,
|
|
3788
2653
|
entry.asset
|
|
3789
2654
|
);
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
usdcCoins.push(coin);
|
|
3793
|
-
} else if (swapAdapter?.addSwapToTx) {
|
|
3794
|
-
const { outputCoin, estimatedOut, toDecimals } = await swapAdapter.addSwapToTx(
|
|
3795
|
-
tx,
|
|
3796
|
-
this._address,
|
|
3797
|
-
coin,
|
|
3798
|
-
entry.asset,
|
|
3799
|
-
"USDC",
|
|
3800
|
-
effectiveAmount,
|
|
3801
|
-
500
|
|
3802
|
-
);
|
|
3803
|
-
totalUsdcReceived += estimatedOut / 10 ** toDecimals;
|
|
3804
|
-
usdcCoins.push(outputCoin);
|
|
3805
|
-
} else {
|
|
3806
|
-
totalUsdcReceived += effectiveAmount;
|
|
3807
|
-
tx.transferObjects([coin], this._address);
|
|
3808
|
-
}
|
|
3809
|
-
}
|
|
3810
|
-
for (const dust of dustEntries) {
|
|
3811
|
-
if (dust.adapter.addWithdrawToTx) {
|
|
3812
|
-
const { coin, effectiveAmount } = await dust.adapter.addWithdrawToTx(
|
|
3813
|
-
tx,
|
|
3814
|
-
this._address,
|
|
3815
|
-
dust.maxAmount,
|
|
3816
|
-
dust.asset
|
|
3817
|
-
);
|
|
3818
|
-
totalUsdcReceived += effectiveAmount;
|
|
3819
|
-
tx.transferObjects([coin], this._address);
|
|
3820
|
-
}
|
|
3821
|
-
}
|
|
3822
|
-
if (usdcCoins.length > 1) {
|
|
3823
|
-
tx.mergeCoins(usdcCoins[0], usdcCoins.slice(1));
|
|
3824
|
-
}
|
|
3825
|
-
if (usdcCoins.length > 0) {
|
|
3826
|
-
tx.transferObjects([usdcCoins[0]], this._address);
|
|
2655
|
+
totalReceived += effectiveAmount;
|
|
2656
|
+
tx.transferObjects([coin], this._address);
|
|
3827
2657
|
}
|
|
3828
2658
|
return tx;
|
|
3829
2659
|
}
|
|
3830
2660
|
let lastTx;
|
|
3831
2661
|
for (const entry of entries) {
|
|
3832
2662
|
const built = await entry.adapter.buildWithdrawTx(this._address, entry.maxAmount, entry.asset);
|
|
3833
|
-
|
|
2663
|
+
totalReceived += built.effectiveAmount;
|
|
3834
2664
|
lastTx = built.tx;
|
|
3835
2665
|
}
|
|
3836
2666
|
return lastTx;
|
|
3837
2667
|
});
|
|
3838
|
-
if (
|
|
3839
|
-
try {
|
|
3840
|
-
const postBal = await queryBalance(this.client, this._address);
|
|
3841
|
-
await this._convertWalletStablesToUsdc(postBal);
|
|
3842
|
-
} catch {
|
|
3843
|
-
}
|
|
3844
|
-
}
|
|
3845
|
-
if (totalUsdcReceived <= 0) {
|
|
2668
|
+
if (totalReceived <= 0) {
|
|
3846
2669
|
throw new T2000Error("NO_COLLATERAL", "No savings to withdraw across any protocol");
|
|
3847
2670
|
}
|
|
3848
2671
|
return {
|
|
3849
2672
|
success: true,
|
|
3850
2673
|
tx: gasResult.digest,
|
|
3851
|
-
amount:
|
|
2674
|
+
amount: totalReceived,
|
|
3852
2675
|
gasCost: gasResult.gasCostSui,
|
|
3853
2676
|
gasMethod: gasResult.gasMethod
|
|
3854
2677
|
};
|
|
@@ -3898,39 +2721,6 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
3898
2721
|
}
|
|
3899
2722
|
return primary;
|
|
3900
2723
|
}
|
|
3901
|
-
async _swapToUsdc(asset, amount) {
|
|
3902
|
-
const swapAdapter = this.registry.listSwap()[0];
|
|
3903
|
-
if (!swapAdapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", "No swap adapter available");
|
|
3904
|
-
let estimatedOut = 0;
|
|
3905
|
-
let toDecimals = 6;
|
|
3906
|
-
const gasResult = await executeWithGas(this.client, this._signer, async () => {
|
|
3907
|
-
const built = await swapAdapter.buildSwapTx(this._address, asset, "USDC", amount);
|
|
3908
|
-
estimatedOut = built.estimatedOut;
|
|
3909
|
-
toDecimals = built.toDecimals;
|
|
3910
|
-
return built.tx;
|
|
3911
|
-
});
|
|
3912
|
-
const usdcReceived = estimatedOut / 10 ** toDecimals;
|
|
3913
|
-
return { usdcReceived, digest: gasResult.digest, gasCost: gasResult.gasCostSui };
|
|
3914
|
-
}
|
|
3915
|
-
async _swapFromUsdc(toAsset, amount) {
|
|
3916
|
-
const swapAdapter = this.registry.listSwap()[0];
|
|
3917
|
-
if (!swapAdapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", "No swap adapter available");
|
|
3918
|
-
let estimatedOut = 0;
|
|
3919
|
-
let toDecimals = 6;
|
|
3920
|
-
const gasResult = await executeWithGas(this.client, this._signer, async () => {
|
|
3921
|
-
const built = await swapAdapter.buildSwapTx(this._address, "USDC", toAsset, amount);
|
|
3922
|
-
estimatedOut = built.estimatedOut;
|
|
3923
|
-
toDecimals = built.toDecimals;
|
|
3924
|
-
return built.tx;
|
|
3925
|
-
});
|
|
3926
|
-
const received = estimatedOut / 10 ** toDecimals;
|
|
3927
|
-
return { received, digest: gasResult.digest, gasCost: gasResult.gasCostSui };
|
|
3928
|
-
}
|
|
3929
|
-
/**
|
|
3930
|
-
* Auto-withdraw from savings when checking balance is insufficient for an
|
|
3931
|
-
* operation. Handles non-USDC savings (e.g. USDe) via the standard withdraw
|
|
3932
|
-
* path which swaps back to USDC. Throws if savings are also insufficient.
|
|
3933
|
-
*/
|
|
3934
2724
|
_lastFundDigest;
|
|
3935
2725
|
async _autoFundFromSavings(shortfall) {
|
|
3936
2726
|
const positions = await this.positions();
|
|
@@ -3961,69 +2751,17 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
3961
2751
|
}
|
|
3962
2752
|
this._lastFundDigest = result.tx;
|
|
3963
2753
|
}
|
|
3964
|
-
async _convertWalletStablesToUsdc(bal, amountNeeded) {
|
|
3965
|
-
const nonUsdcStables = [];
|
|
3966
|
-
for (const [asset, amount] of Object.entries(bal.stables)) {
|
|
3967
|
-
if (asset !== "USDC" && amount > 0.01) {
|
|
3968
|
-
nonUsdcStables.push({ asset, amount });
|
|
3969
|
-
}
|
|
3970
|
-
}
|
|
3971
|
-
if (nonUsdcStables.length === 0) return;
|
|
3972
|
-
nonUsdcStables.sort((a, b) => b.amount - a.amount);
|
|
3973
|
-
let converted = 0;
|
|
3974
|
-
for (const entry of nonUsdcStables) {
|
|
3975
|
-
if (amountNeeded !== void 0 && converted >= amountNeeded) break;
|
|
3976
|
-
try {
|
|
3977
|
-
await this._swapToUsdc(entry.asset, entry.amount);
|
|
3978
|
-
converted += entry.amount;
|
|
3979
|
-
} catch {
|
|
3980
|
-
}
|
|
3981
|
-
}
|
|
3982
|
-
}
|
|
3983
2754
|
async maxWithdraw() {
|
|
3984
2755
|
const adapter = await this.resolveLending(void 0, "USDC", "withdraw");
|
|
3985
2756
|
return adapter.maxWithdraw(this._address, "USDC");
|
|
3986
2757
|
}
|
|
3987
2758
|
// -- Borrowing --
|
|
3988
|
-
async adjustMaxBorrowForInvestments(adapter, maxResult) {
|
|
3989
|
-
const earningPositions = this.portfolio.getPositions().filter((p) => p.earning);
|
|
3990
|
-
if (earningPositions.length === 0) return maxResult;
|
|
3991
|
-
let investmentCollateralUsd = 0;
|
|
3992
|
-
const swapAdapter = this.registry.listSwap()[0];
|
|
3993
|
-
for (const pos of earningPositions) {
|
|
3994
|
-
if (pos.earningProtocol !== adapter.id) continue;
|
|
3995
|
-
try {
|
|
3996
|
-
let price = 0;
|
|
3997
|
-
if (pos.asset === "SUI" && swapAdapter) {
|
|
3998
|
-
price = await swapAdapter.getPoolPrice();
|
|
3999
|
-
} else if (swapAdapter) {
|
|
4000
|
-
const quote = await swapAdapter.getQuote("USDC", pos.asset, 1);
|
|
4001
|
-
price = quote.expectedOutput > 0 ? 1 / quote.expectedOutput : 0;
|
|
4002
|
-
}
|
|
4003
|
-
investmentCollateralUsd += pos.totalAmount * price;
|
|
4004
|
-
} catch {
|
|
4005
|
-
}
|
|
4006
|
-
}
|
|
4007
|
-
if (investmentCollateralUsd <= 0) return maxResult;
|
|
4008
|
-
const CONSERVATIVE_LTV = 0.6;
|
|
4009
|
-
const investmentBorrowCapacity = investmentCollateralUsd * CONSERVATIVE_LTV;
|
|
4010
|
-
const adjustedMax = Math.max(0, maxResult.maxAmount - investmentBorrowCapacity);
|
|
4011
|
-
return { ...maxResult, maxAmount: adjustedMax };
|
|
4012
|
-
}
|
|
4013
2759
|
async borrow(params) {
|
|
4014
2760
|
this.enforcer.assertNotLocked();
|
|
4015
2761
|
const asset = "USDC";
|
|
4016
2762
|
const adapter = await this.resolveLending(params.protocol, asset, "borrow");
|
|
4017
|
-
const
|
|
4018
|
-
const maxResult = await this.adjustMaxBorrowForInvestments(adapter, rawMax);
|
|
2763
|
+
const maxResult = await adapter.maxBorrow(this._address, asset);
|
|
4019
2764
|
if (maxResult.maxAmount <= 0) {
|
|
4020
|
-
const hasInvestmentEarning = this.portfolio.getPositions().some((p) => p.earning && p.earningProtocol === adapter.id);
|
|
4021
|
-
if (hasInvestmentEarning) {
|
|
4022
|
-
throw new T2000Error(
|
|
4023
|
-
"BORROW_GUARD_INVESTMENT",
|
|
4024
|
-
"Max safe borrow: $0.00. Only savings deposits (stablecoins) count as borrowable collateral. Investment collateral (SUI, ETH, BTC) is excluded."
|
|
4025
|
-
);
|
|
4026
|
-
}
|
|
4027
2765
|
throw new T2000Error("NO_COLLATERAL", "No collateral deposited. Save first with `t2000 save <amount>`.");
|
|
4028
2766
|
}
|
|
4029
2767
|
if (params.amount > maxResult.maxAmount) {
|
|
@@ -4072,30 +2810,8 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
4072
2810
|
const adapter = this.registry.getLending(target.protocolId);
|
|
4073
2811
|
if (!adapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", `Protocol ${target.protocolId} not found`);
|
|
4074
2812
|
const repayAmount = Math.min(params.amount, target.amount);
|
|
4075
|
-
const swapAdapter = target.asset !== "USDC" ? this.registry.listSwap()[0] : void 0;
|
|
4076
|
-
const canPTB = adapter.addRepayToTx && (!swapAdapter || swapAdapter.addSwapToTx);
|
|
4077
2813
|
const gasResult = await executeWithGas(this.client, this._signer, async () => {
|
|
4078
|
-
if (
|
|
4079
|
-
const tx2 = new Transaction();
|
|
4080
|
-
tx2.setSender(this._address);
|
|
4081
|
-
const buffer = repayAmount * 1.005;
|
|
4082
|
-
const usdcCoins = await this._fetchCoins(SUPPORTED_ASSETS.USDC.type);
|
|
4083
|
-
if (usdcCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins for swap");
|
|
4084
|
-
const merged = this._mergeCoinsInTx(tx2, usdcCoins);
|
|
4085
|
-
const rawSwap = BigInt(Math.floor(buffer * 10 ** SUPPORTED_ASSETS.USDC.decimals));
|
|
4086
|
-
const [splitCoin] = tx2.splitCoins(merged, [rawSwap]);
|
|
4087
|
-
const { outputCoin } = await swapAdapter.addSwapToTx(
|
|
4088
|
-
tx2,
|
|
4089
|
-
this._address,
|
|
4090
|
-
splitCoin,
|
|
4091
|
-
"USDC",
|
|
4092
|
-
target.asset,
|
|
4093
|
-
buffer
|
|
4094
|
-
);
|
|
4095
|
-
await adapter.addRepayToTx(tx2, this._address, outputCoin, target.asset);
|
|
4096
|
-
return tx2;
|
|
4097
|
-
}
|
|
4098
|
-
if (canPTB && target.asset === "USDC") {
|
|
2814
|
+
if (adapter.addRepayToTx) {
|
|
4099
2815
|
const tx2 = new Transaction();
|
|
4100
2816
|
tx2.setSender(this._address);
|
|
4101
2817
|
const usdcCoins = await this._fetchCoins(SUPPORTED_ASSETS.USDC.type);
|
|
@@ -4106,9 +2822,6 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
4106
2822
|
await adapter.addRepayToTx(tx2, this._address, repayCoin, target.asset);
|
|
4107
2823
|
return tx2;
|
|
4108
2824
|
}
|
|
4109
|
-
if (target.asset !== "USDC") {
|
|
4110
|
-
await this._swapFromUsdc(target.asset, repayAmount * 1.005);
|
|
4111
|
-
}
|
|
4112
2825
|
const { tx } = await adapter.buildRepayTx(this._address, repayAmount, target.asset);
|
|
4113
2826
|
return tx;
|
|
4114
2827
|
});
|
|
@@ -4130,8 +2843,7 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
4130
2843
|
const adapter = this.registry.getLending(borrow.protocolId);
|
|
4131
2844
|
if (adapter) entries.push({ borrow, adapter });
|
|
4132
2845
|
}
|
|
4133
|
-
const
|
|
4134
|
-
const canPTB = entries.every((e) => e.adapter.addRepayToTx) && (entries.every((e) => e.borrow.asset === "USDC") || swapAdapter?.addSwapToTx);
|
|
2846
|
+
const canPTB = entries.every((e) => e.adapter.addRepayToTx);
|
|
4135
2847
|
let totalRepaid = 0;
|
|
4136
2848
|
const gasResult = await executeWithGas(this.client, this._signer, async () => {
|
|
4137
2849
|
if (canPTB) {
|
|
@@ -4143,35 +2855,16 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
4143
2855
|
usdcMerged = this._mergeCoinsInTx(tx, usdcCoins);
|
|
4144
2856
|
}
|
|
4145
2857
|
for (const { borrow, adapter } of entries) {
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
const [splitCoin] = tx.splitCoins(usdcMerged, [rawSwap]);
|
|
4151
|
-
const { outputCoin } = await swapAdapter.addSwapToTx(
|
|
4152
|
-
tx,
|
|
4153
|
-
this._address,
|
|
4154
|
-
splitCoin,
|
|
4155
|
-
"USDC",
|
|
4156
|
-
borrow.asset,
|
|
4157
|
-
buffer
|
|
4158
|
-
);
|
|
4159
|
-
await adapter.addRepayToTx(tx, this._address, outputCoin, borrow.asset);
|
|
4160
|
-
} else {
|
|
4161
|
-
const raw = BigInt(Math.floor(borrow.amount * 10 ** SUPPORTED_ASSETS.USDC.decimals));
|
|
4162
|
-
if (!usdcMerged) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC for repayment");
|
|
4163
|
-
const [repayCoin] = tx.splitCoins(usdcMerged, [raw]);
|
|
4164
|
-
await adapter.addRepayToTx(tx, this._address, repayCoin, borrow.asset);
|
|
4165
|
-
}
|
|
2858
|
+
const raw = BigInt(Math.floor(borrow.amount * 10 ** SUPPORTED_ASSETS.USDC.decimals));
|
|
2859
|
+
if (!usdcMerged) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC for repayment");
|
|
2860
|
+
const [repayCoin] = tx.splitCoins(usdcMerged, [raw]);
|
|
2861
|
+
await adapter.addRepayToTx(tx, this._address, repayCoin, borrow.asset);
|
|
4166
2862
|
totalRepaid += borrow.amount;
|
|
4167
2863
|
}
|
|
4168
2864
|
return tx;
|
|
4169
2865
|
}
|
|
4170
2866
|
let lastTx;
|
|
4171
2867
|
for (const { borrow, adapter } of entries) {
|
|
4172
|
-
if (borrow.asset !== "USDC") {
|
|
4173
|
-
await this._swapFromUsdc(borrow.asset, borrow.amount * 1.005);
|
|
4174
|
-
}
|
|
4175
2868
|
const { tx } = await adapter.buildRepayTx(this._address, borrow.amount, borrow.asset);
|
|
4176
2869
|
lastTx = tx;
|
|
4177
2870
|
totalRepaid += borrow.amount;
|
|
@@ -4182,536 +2875,27 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
4182
2875
|
const hf = firstAdapter ? await firstAdapter.getHealth(this._address) : { borrowed: 0 };
|
|
4183
2876
|
this.emitBalanceChange("USDC", totalRepaid, "repay", gasResult.digest);
|
|
4184
2877
|
return {
|
|
4185
|
-
success: true,
|
|
4186
|
-
tx: gasResult.digest,
|
|
4187
|
-
amount: totalRepaid,
|
|
4188
|
-
remainingDebt: hf.borrowed,
|
|
4189
|
-
gasCost: gasResult.gasCostSui,
|
|
4190
|
-
gasMethod: gasResult.gasMethod
|
|
4191
|
-
};
|
|
4192
|
-
}
|
|
4193
|
-
async maxBorrow() {
|
|
4194
|
-
const adapter = await this.resolveLending(void 0, "USDC", "borrow");
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
const
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
this.emit("healthWarning", { healthFactor: hf.healthFactor, threshold: 2, severity: "warning" });
|
|
4205
|
-
}
|
|
4206
|
-
return hf;
|
|
4207
|
-
}
|
|
4208
|
-
// -- Swap (formerly Exchange) --
|
|
4209
|
-
async swap(params) {
|
|
4210
|
-
this.enforcer.assertNotLocked();
|
|
4211
|
-
const fromAsset = params.from;
|
|
4212
|
-
const toAsset = params.to;
|
|
4213
|
-
if (!(fromAsset in SUPPORTED_ASSETS) || !(toAsset in SUPPORTED_ASSETS)) {
|
|
4214
|
-
throw new T2000Error("ASSET_NOT_SUPPORTED", `Swap pair ${fromAsset}/${toAsset} is not supported`);
|
|
4215
|
-
}
|
|
4216
|
-
if (fromAsset === toAsset) {
|
|
4217
|
-
throw new T2000Error("INVALID_AMOUNT", "Cannot swap same asset");
|
|
4218
|
-
}
|
|
4219
|
-
const best = await this.registry.bestSwapQuote(fromAsset, toAsset, params.amount);
|
|
4220
|
-
const adapter = best.adapter;
|
|
4221
|
-
const fee = calculateFee("swap", params.amount);
|
|
4222
|
-
const swapAmount = params.amount;
|
|
4223
|
-
const slippageBps = params.maxSlippage ? Math.round(params.maxSlippage * 1e4) : void 0;
|
|
4224
|
-
let swapMeta = { estimatedOut: 0, toDecimals: 0 };
|
|
4225
|
-
const gasResult = await executeWithGas(this.client, this._signer, async () => {
|
|
4226
|
-
const built = await adapter.buildSwapTx(this._address, fromAsset, toAsset, swapAmount, slippageBps);
|
|
4227
|
-
swapMeta = { estimatedOut: built.estimatedOut, toDecimals: built.toDecimals };
|
|
4228
|
-
return built.tx;
|
|
4229
|
-
});
|
|
4230
|
-
const toInfo = SUPPORTED_ASSETS[toAsset];
|
|
4231
|
-
await this.client.waitForTransaction({ digest: gasResult.digest });
|
|
4232
|
-
const txDetail = await this.client.getTransactionBlock({
|
|
4233
|
-
digest: gasResult.digest,
|
|
4234
|
-
options: { showBalanceChanges: true }
|
|
4235
|
-
});
|
|
4236
|
-
let actualReceived = 0;
|
|
4237
|
-
if (txDetail.balanceChanges) {
|
|
4238
|
-
for (const change of txDetail.balanceChanges) {
|
|
4239
|
-
if (change.coinType === toInfo.type && change.owner && typeof change.owner === "object" && "AddressOwner" in change.owner && change.owner.AddressOwner === this._address) {
|
|
4240
|
-
const amt = Number(change.amount) / 10 ** toInfo.decimals;
|
|
4241
|
-
if (amt > 0) actualReceived += amt;
|
|
4242
|
-
}
|
|
4243
|
-
}
|
|
4244
|
-
}
|
|
4245
|
-
const expectedOutput = swapMeta.estimatedOut / 10 ** swapMeta.toDecimals;
|
|
4246
|
-
if (actualReceived === 0) actualReceived = expectedOutput;
|
|
4247
|
-
const priceImpact = expectedOutput > 0 ? Math.abs(actualReceived - expectedOutput) / expectedOutput : 0;
|
|
4248
|
-
reportFee(this._address, "swap", fee.amount, fee.rate, gasResult.digest);
|
|
4249
|
-
this.emitBalanceChange(fromAsset, swapAmount, "swap", gasResult.digest);
|
|
4250
|
-
const stableSet = new Set(STABLE_ASSETS);
|
|
4251
|
-
if (!params._skipPortfolioRecord && stableSet.has(fromAsset) && toAsset in INVESTMENT_ASSETS && actualReceived > 0) {
|
|
4252
|
-
const price = swapAmount / actualReceived;
|
|
4253
|
-
this.portfolio.recordBuy({
|
|
4254
|
-
id: `swap_${Date.now()}`,
|
|
4255
|
-
type: "buy",
|
|
4256
|
-
asset: toAsset,
|
|
4257
|
-
amount: actualReceived,
|
|
4258
|
-
price,
|
|
4259
|
-
usdValue: swapAmount,
|
|
4260
|
-
fee: fee.amount,
|
|
4261
|
-
tx: gasResult.digest,
|
|
4262
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4263
|
-
});
|
|
4264
|
-
} else if (!params._skipPortfolioRecord && fromAsset in INVESTMENT_ASSETS && stableSet.has(toAsset) && actualReceived > 0) {
|
|
4265
|
-
const price = actualReceived / swapAmount;
|
|
4266
|
-
this.portfolio.recordSell({
|
|
4267
|
-
id: `swap_${Date.now()}`,
|
|
4268
|
-
type: "sell",
|
|
4269
|
-
asset: fromAsset,
|
|
4270
|
-
amount: swapAmount,
|
|
4271
|
-
price,
|
|
4272
|
-
usdValue: actualReceived,
|
|
4273
|
-
fee: fee.amount,
|
|
4274
|
-
tx: gasResult.digest,
|
|
4275
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4276
|
-
});
|
|
4277
|
-
}
|
|
4278
|
-
return {
|
|
4279
|
-
success: true,
|
|
4280
|
-
tx: gasResult.digest,
|
|
4281
|
-
fromAmount: swapAmount,
|
|
4282
|
-
fromAsset,
|
|
4283
|
-
toAmount: actualReceived,
|
|
4284
|
-
toAsset,
|
|
4285
|
-
priceImpact,
|
|
4286
|
-
fee: fee.amount,
|
|
4287
|
-
gasCost: gasResult.gasCostSui,
|
|
4288
|
-
gasMethod: gasResult.gasMethod
|
|
4289
|
-
};
|
|
4290
|
-
}
|
|
4291
|
-
async swapQuote(params) {
|
|
4292
|
-
const fromAsset = params.from;
|
|
4293
|
-
const toAsset = params.to;
|
|
4294
|
-
const best = await this.registry.bestSwapQuote(fromAsset, toAsset, params.amount);
|
|
4295
|
-
const fee = calculateFee("swap", params.amount);
|
|
4296
|
-
return { ...best.quote, fee: { amount: fee.amount, rate: fee.rate } };
|
|
4297
|
-
}
|
|
4298
|
-
/** @deprecated Use swap() instead */
|
|
4299
|
-
async exchange(params) {
|
|
4300
|
-
return this.swap(params);
|
|
4301
|
-
}
|
|
4302
|
-
/** @deprecated Use swapQuote() instead */
|
|
4303
|
-
async exchangeQuote(params) {
|
|
4304
|
-
return this.swapQuote(params);
|
|
4305
|
-
}
|
|
4306
|
-
// -- Investment (strategies only — individual buy/sell deprecated, use swap()) --
|
|
4307
|
-
async investBuy(params) {
|
|
4308
|
-
this.enforcer.assertNotLocked();
|
|
4309
|
-
if (!params.usdAmount || params.usdAmount <= 0 || !isFinite(params.usdAmount)) {
|
|
4310
|
-
throw new T2000Error("INVALID_AMOUNT", "Investment amount must be greater than $0");
|
|
4311
|
-
}
|
|
4312
|
-
this.enforcer.check({ operation: "invest", amount: params.usdAmount });
|
|
4313
|
-
if (!(params.asset in INVESTMENT_ASSETS)) {
|
|
4314
|
-
throw new T2000Error("ASSET_NOT_SUPPORTED", `${params.asset} is not available for investment`);
|
|
4315
|
-
}
|
|
4316
|
-
const bal = await queryBalance(this.client, this._address);
|
|
4317
|
-
const totalFunds = bal.available + bal.savings;
|
|
4318
|
-
if (params.usdAmount > totalFunds * 1.05) {
|
|
4319
|
-
throw new T2000Error(
|
|
4320
|
-
"INSUFFICIENT_BALANCE",
|
|
4321
|
-
`Insufficient funds. You have $${totalFunds.toFixed(2)} total (checking: $${bal.available.toFixed(2)}, savings: $${bal.savings.toFixed(2)}) but need $${params.usdAmount.toFixed(2)}.`
|
|
4322
|
-
);
|
|
4323
|
-
}
|
|
4324
|
-
if (bal.available < params.usdAmount) {
|
|
4325
|
-
await this._autoFundFromSavings(params.usdAmount - bal.available);
|
|
4326
|
-
}
|
|
4327
|
-
let swapResult;
|
|
4328
|
-
const maxRetries = 3;
|
|
4329
|
-
for (let attempt = 0; ; attempt++) {
|
|
4330
|
-
try {
|
|
4331
|
-
swapResult = await this.swap({
|
|
4332
|
-
from: "USDC",
|
|
4333
|
-
to: params.asset,
|
|
4334
|
-
amount: params.usdAmount,
|
|
4335
|
-
maxSlippage: params.maxSlippage ?? defaultSlippage(params.asset),
|
|
4336
|
-
_skipPortfolioRecord: true
|
|
4337
|
-
});
|
|
4338
|
-
break;
|
|
4339
|
-
} catch (err) {
|
|
4340
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
4341
|
-
const isSlippage = msg.includes("slippage") || msg.includes("amount_out_slippage");
|
|
4342
|
-
if (isSlippage && attempt < maxRetries) {
|
|
4343
|
-
await new Promise((r) => setTimeout(r, 2e3 * (attempt + 1)));
|
|
4344
|
-
continue;
|
|
4345
|
-
}
|
|
4346
|
-
throw err;
|
|
4347
|
-
}
|
|
4348
|
-
}
|
|
4349
|
-
if (swapResult.toAmount === 0) {
|
|
4350
|
-
throw new T2000Error("SWAP_FAILED", "Swap returned zero tokens \u2014 try a different amount or check liquidity");
|
|
4351
|
-
}
|
|
4352
|
-
const price = params.usdAmount / swapResult.toAmount;
|
|
4353
|
-
this.portfolio.recordBuy({
|
|
4354
|
-
id: `inv_${Date.now()}`,
|
|
4355
|
-
type: "buy",
|
|
4356
|
-
asset: params.asset,
|
|
4357
|
-
amount: swapResult.toAmount,
|
|
4358
|
-
price,
|
|
4359
|
-
usdValue: params.usdAmount,
|
|
4360
|
-
fee: swapResult.fee,
|
|
4361
|
-
tx: swapResult.tx,
|
|
4362
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4363
|
-
});
|
|
4364
|
-
const pos = this.portfolio.getPosition(params.asset);
|
|
4365
|
-
const currentPrice = price;
|
|
4366
|
-
const position = {
|
|
4367
|
-
asset: params.asset,
|
|
4368
|
-
totalAmount: pos?.totalAmount ?? swapResult.toAmount,
|
|
4369
|
-
costBasis: pos?.costBasis ?? params.usdAmount,
|
|
4370
|
-
avgPrice: pos?.avgPrice ?? price,
|
|
4371
|
-
currentPrice,
|
|
4372
|
-
currentValue: (pos?.totalAmount ?? swapResult.toAmount) * currentPrice,
|
|
4373
|
-
unrealizedPnL: 0,
|
|
4374
|
-
unrealizedPnLPct: 0,
|
|
4375
|
-
trades: pos?.trades ?? []
|
|
4376
|
-
};
|
|
4377
|
-
return {
|
|
4378
|
-
success: true,
|
|
4379
|
-
tx: swapResult.tx,
|
|
4380
|
-
type: "buy",
|
|
4381
|
-
asset: params.asset,
|
|
4382
|
-
amount: swapResult.toAmount,
|
|
4383
|
-
price,
|
|
4384
|
-
usdValue: params.usdAmount,
|
|
4385
|
-
fee: swapResult.fee,
|
|
4386
|
-
gasCost: swapResult.gasCost,
|
|
4387
|
-
gasMethod: swapResult.gasMethod,
|
|
4388
|
-
position
|
|
4389
|
-
};
|
|
4390
|
-
}
|
|
4391
|
-
async investSell(params) {
|
|
4392
|
-
this.enforcer.assertNotLocked();
|
|
4393
|
-
if (params.usdAmount !== "all") {
|
|
4394
|
-
if (!params.usdAmount || params.usdAmount <= 0 || !isFinite(params.usdAmount)) {
|
|
4395
|
-
throw new T2000Error("INVALID_AMOUNT", "Sell amount must be greater than $0");
|
|
4396
|
-
}
|
|
4397
|
-
}
|
|
4398
|
-
if (!(params.asset in INVESTMENT_ASSETS)) {
|
|
4399
|
-
throw new T2000Error("ASSET_NOT_SUPPORTED", `${params.asset} is not available for investment`);
|
|
4400
|
-
}
|
|
4401
|
-
let pos = this.portfolio.getPosition(params.asset);
|
|
4402
|
-
const didAutoWithdraw = !!(pos?.earning && pos.earningProtocol);
|
|
4403
|
-
if (didAutoWithdraw) {
|
|
4404
|
-
const unearnResult = await this.investUnearn({ asset: params.asset });
|
|
4405
|
-
if (unearnResult.tx) {
|
|
4406
|
-
await this.client.waitForTransaction({ digest: unearnResult.tx, options: { showEffects: true } });
|
|
4407
|
-
}
|
|
4408
|
-
pos = this.portfolio.getPosition(params.asset);
|
|
4409
|
-
}
|
|
4410
|
-
const assetInfo = SUPPORTED_ASSETS[params.asset];
|
|
4411
|
-
const gasReserve = params.asset === "SUI" ? GAS_RESERVE_MIN : 0;
|
|
4412
|
-
let walletAmount = 0;
|
|
4413
|
-
for (let attempt = 0; ; attempt++) {
|
|
4414
|
-
const assetBalance = await this.client.getBalance({
|
|
4415
|
-
owner: this._address,
|
|
4416
|
-
coinType: assetInfo.type
|
|
4417
|
-
});
|
|
4418
|
-
walletAmount = Number(assetBalance.totalBalance) / 10 ** assetInfo.decimals;
|
|
4419
|
-
if (!didAutoWithdraw || walletAmount > gasReserve || attempt >= 5) break;
|
|
4420
|
-
await new Promise((r) => setTimeout(r, 1500));
|
|
4421
|
-
}
|
|
4422
|
-
const maxSellable = Math.max(0, walletAmount - gasReserve);
|
|
4423
|
-
const trackedAmount = pos ? pos.totalAmount : maxSellable;
|
|
4424
|
-
if (trackedAmount <= 0) {
|
|
4425
|
-
throw new T2000Error("INSUFFICIENT_INVESTMENT", `No ${params.asset} position to sell`);
|
|
4426
|
-
}
|
|
4427
|
-
let sellAmountAsset;
|
|
4428
|
-
if (params.usdAmount === "all") {
|
|
4429
|
-
sellAmountAsset = Math.min(trackedAmount, maxSellable);
|
|
4430
|
-
} else {
|
|
4431
|
-
const swapAdapter = this.registry.listSwap()[0];
|
|
4432
|
-
if (!swapAdapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", "No swap adapter available");
|
|
4433
|
-
const quote = await swapAdapter.getQuote("USDC", params.asset, 1);
|
|
4434
|
-
const assetPrice = 1 / quote.expectedOutput;
|
|
4435
|
-
sellAmountAsset = params.usdAmount / assetPrice;
|
|
4436
|
-
const maxPosition = params._strategyOnly ? maxSellable : trackedAmount;
|
|
4437
|
-
sellAmountAsset = Math.min(sellAmountAsset, maxPosition);
|
|
4438
|
-
if (sellAmountAsset > maxSellable) {
|
|
4439
|
-
throw new T2000Error(
|
|
4440
|
-
"INSUFFICIENT_INVESTMENT",
|
|
4441
|
-
`Cannot sell $${params.usdAmount.toFixed(2)} \u2014 max sellable: $${(maxSellable * assetPrice).toFixed(2)} (gas reserve: ${gasReserve} ${params.asset})`
|
|
4442
|
-
);
|
|
4443
|
-
}
|
|
4444
|
-
}
|
|
4445
|
-
if (sellAmountAsset <= 0) {
|
|
4446
|
-
throw new T2000Error("INSUFFICIENT_INVESTMENT", "Nothing to sell after gas reserve");
|
|
4447
|
-
}
|
|
4448
|
-
let swapResult;
|
|
4449
|
-
const maxRetries = 3;
|
|
4450
|
-
for (let attempt = 0; ; attempt++) {
|
|
4451
|
-
try {
|
|
4452
|
-
swapResult = await this.swap({
|
|
4453
|
-
from: params.asset,
|
|
4454
|
-
to: "USDC",
|
|
4455
|
-
amount: sellAmountAsset,
|
|
4456
|
-
maxSlippage: params.maxSlippage ?? defaultSlippage(params.asset),
|
|
4457
|
-
_skipPortfolioRecord: true
|
|
4458
|
-
});
|
|
4459
|
-
break;
|
|
4460
|
-
} catch (err) {
|
|
4461
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
4462
|
-
const isSlippage = msg.includes("slippage") || msg.includes("amount_out_slippage");
|
|
4463
|
-
if (isSlippage && attempt < maxRetries) {
|
|
4464
|
-
await new Promise((r) => setTimeout(r, 2e3 * (attempt + 1)));
|
|
4465
|
-
continue;
|
|
4466
|
-
}
|
|
4467
|
-
throw err;
|
|
4468
|
-
}
|
|
4469
|
-
}
|
|
4470
|
-
const price = swapResult.toAmount / sellAmountAsset;
|
|
4471
|
-
const directAmountBeforeSell = this.portfolio.getDirectAmount(params.asset);
|
|
4472
|
-
let realizedPnL = 0;
|
|
4473
|
-
try {
|
|
4474
|
-
realizedPnL = this.portfolio.recordSell({
|
|
4475
|
-
id: `inv_${Date.now()}`,
|
|
4476
|
-
type: "sell",
|
|
4477
|
-
asset: params.asset,
|
|
4478
|
-
amount: sellAmountAsset,
|
|
4479
|
-
price,
|
|
4480
|
-
usdValue: swapResult.toAmount,
|
|
4481
|
-
fee: swapResult.fee,
|
|
4482
|
-
tx: swapResult.tx,
|
|
4483
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4484
|
-
});
|
|
4485
|
-
} catch {
|
|
4486
|
-
}
|
|
4487
|
-
if (params.usdAmount === "all" && !params._strategyOnly) {
|
|
4488
|
-
this.portfolio.closePosition(params.asset);
|
|
4489
|
-
this.portfolio.deductFromStrategies(params.asset, sellAmountAsset);
|
|
4490
|
-
} else if (!params._strategyOnly && sellAmountAsset > 0) {
|
|
4491
|
-
const overflowIntoStrategy = sellAmountAsset - directAmountBeforeSell;
|
|
4492
|
-
if (overflowIntoStrategy > 0) {
|
|
4493
|
-
this.portfolio.deductFromStrategies(params.asset, overflowIntoStrategy);
|
|
4494
|
-
}
|
|
4495
|
-
}
|
|
4496
|
-
const updatedPos = this.portfolio.getPosition(params.asset);
|
|
4497
|
-
const position = {
|
|
4498
|
-
asset: params.asset,
|
|
4499
|
-
totalAmount: updatedPos?.totalAmount ?? 0,
|
|
4500
|
-
costBasis: updatedPos?.costBasis ?? 0,
|
|
4501
|
-
avgPrice: updatedPos?.avgPrice ?? 0,
|
|
4502
|
-
currentPrice: price,
|
|
4503
|
-
currentValue: (updatedPos?.totalAmount ?? 0) * price,
|
|
4504
|
-
unrealizedPnL: 0,
|
|
4505
|
-
unrealizedPnLPct: 0,
|
|
4506
|
-
trades: updatedPos?.trades ?? []
|
|
4507
|
-
};
|
|
4508
|
-
return {
|
|
4509
|
-
success: true,
|
|
4510
|
-
tx: swapResult.tx,
|
|
4511
|
-
type: "sell",
|
|
4512
|
-
asset: params.asset,
|
|
4513
|
-
amount: sellAmountAsset,
|
|
4514
|
-
price,
|
|
4515
|
-
usdValue: swapResult.toAmount,
|
|
4516
|
-
fee: swapResult.fee,
|
|
4517
|
-
gasCost: swapResult.gasCost,
|
|
4518
|
-
gasMethod: swapResult.gasMethod,
|
|
4519
|
-
realizedPnL,
|
|
4520
|
-
position
|
|
4521
|
-
};
|
|
4522
|
-
}
|
|
4523
|
-
async investEarn(params) {
|
|
4524
|
-
this.enforcer.assertNotLocked();
|
|
4525
|
-
if (!(params.asset in INVESTMENT_ASSETS)) {
|
|
4526
|
-
throw new T2000Error("ASSET_NOT_SUPPORTED", `${params.asset} is not available for investment`);
|
|
4527
|
-
}
|
|
4528
|
-
const pos = this.portfolio.getPosition(params.asset);
|
|
4529
|
-
if (!pos || pos.totalAmount <= 0) {
|
|
4530
|
-
throw new T2000Error("INSUFFICIENT_INVESTMENT", `No ${params.asset} position to earn on`);
|
|
4531
|
-
}
|
|
4532
|
-
const assetInfo = SUPPORTED_ASSETS[params.asset];
|
|
4533
|
-
const assetBalance = await this.client.getBalance({
|
|
4534
|
-
owner: this._address,
|
|
4535
|
-
coinType: assetInfo.type
|
|
4536
|
-
});
|
|
4537
|
-
const walletAmount = Number(assetBalance.totalBalance) / 10 ** assetInfo.decimals;
|
|
4538
|
-
const gasReserve = params.asset === "SUI" ? GAS_RESERVE_MIN : 0;
|
|
4539
|
-
const depositAmount = Math.min(pos.totalAmount, Math.max(0, walletAmount - gasReserve));
|
|
4540
|
-
if (pos.earning) {
|
|
4541
|
-
throw new T2000Error("INVEST_ALREADY_EARNING", `${params.asset} is already earning via ${pos.earningProtocol ?? "lending"}`);
|
|
4542
|
-
}
|
|
4543
|
-
if (depositAmount <= 0) {
|
|
4544
|
-
throw new T2000Error("INSUFFICIENT_BALANCE", `No ${params.asset} available to deposit (wallet: ${walletAmount}, gas reserve: ${gasReserve})`);
|
|
4545
|
-
}
|
|
4546
|
-
let adapter;
|
|
4547
|
-
let rate;
|
|
4548
|
-
if (params.protocol) {
|
|
4549
|
-
const specific = this.registry.getLending(params.protocol);
|
|
4550
|
-
if (!specific) throw new T2000Error("PROTOCOL_UNAVAILABLE", `Protocol ${params.protocol} not found`);
|
|
4551
|
-
adapter = specific;
|
|
4552
|
-
rate = await specific.getRates(params.asset);
|
|
4553
|
-
} else {
|
|
4554
|
-
({ adapter, rate } = await this.registry.bestSaveRate(params.asset));
|
|
4555
|
-
}
|
|
4556
|
-
const gasResult = await executeWithGas(this.client, this._signer, async () => {
|
|
4557
|
-
const { tx } = await adapter.buildSaveTx(this._address, depositAmount, params.asset);
|
|
4558
|
-
return tx;
|
|
4559
|
-
});
|
|
4560
|
-
this.portfolio.recordEarn(params.asset, adapter.id, rate.saveApy);
|
|
4561
|
-
return {
|
|
4562
|
-
success: true,
|
|
4563
|
-
tx: gasResult.digest,
|
|
4564
|
-
asset: params.asset,
|
|
4565
|
-
amount: depositAmount,
|
|
4566
|
-
protocol: adapter.name,
|
|
4567
|
-
apy: rate.saveApy,
|
|
4568
|
-
gasCost: gasResult.gasCostSui,
|
|
4569
|
-
gasMethod: gasResult.gasMethod
|
|
4570
|
-
};
|
|
4571
|
-
}
|
|
4572
|
-
async investUnearn(params) {
|
|
4573
|
-
this.enforcer.assertNotLocked();
|
|
4574
|
-
if (!(params.asset in INVESTMENT_ASSETS)) {
|
|
4575
|
-
throw new T2000Error("ASSET_NOT_SUPPORTED", `${params.asset} is not available for investment`);
|
|
4576
|
-
}
|
|
4577
|
-
let pos = this.portfolio.getPosition(params.asset);
|
|
4578
|
-
let adapter;
|
|
4579
|
-
let withdrawAmount;
|
|
4580
|
-
if (pos?.earning && pos.earningProtocol) {
|
|
4581
|
-
adapter = this.registry.getLending(pos.earningProtocol);
|
|
4582
|
-
withdrawAmount = pos.totalAmount;
|
|
4583
|
-
} else {
|
|
4584
|
-
const onChain = await this.registry.allPositions(this._address);
|
|
4585
|
-
let found;
|
|
4586
|
-
for (const entry of onChain) {
|
|
4587
|
-
const supply = entry.positions.supplies.find((s) => s.asset === params.asset);
|
|
4588
|
-
if (supply && supply.amount > 1e-10) {
|
|
4589
|
-
found = { protocolId: entry.protocolId, amount: supply.amount };
|
|
4590
|
-
break;
|
|
4591
|
-
}
|
|
4592
|
-
}
|
|
4593
|
-
if (!found) {
|
|
4594
|
-
throw new T2000Error("INVEST_NOT_EARNING", `${params.asset} is not currently earning (checked on-chain)`);
|
|
4595
|
-
}
|
|
4596
|
-
adapter = this.registry.getLending(found.protocolId);
|
|
4597
|
-
withdrawAmount = found.amount;
|
|
4598
|
-
}
|
|
4599
|
-
if (!adapter) {
|
|
4600
|
-
throw new T2000Error("PROTOCOL_UNAVAILABLE", `Lending protocol not found for ${params.asset}`);
|
|
4601
|
-
}
|
|
4602
|
-
const protocolName = adapter.name;
|
|
4603
|
-
let effectiveAmount = withdrawAmount;
|
|
4604
|
-
const gasResult = await executeWithGas(this.client, this._signer, async () => {
|
|
4605
|
-
const result = await adapter.buildWithdrawTx(this._address, withdrawAmount, params.asset);
|
|
4606
|
-
effectiveAmount = result.effectiveAmount;
|
|
4607
|
-
return result.tx;
|
|
4608
|
-
});
|
|
4609
|
-
try {
|
|
4610
|
-
this.portfolio.recordUnearn(params.asset);
|
|
4611
|
-
} catch {
|
|
4612
|
-
}
|
|
4613
|
-
return {
|
|
4614
|
-
success: true,
|
|
4615
|
-
tx: gasResult.digest,
|
|
4616
|
-
asset: params.asset,
|
|
4617
|
-
amount: effectiveAmount,
|
|
4618
|
-
protocol: protocolName,
|
|
4619
|
-
apy: 0,
|
|
4620
|
-
gasCost: gasResult.gasCostSui,
|
|
4621
|
-
gasMethod: gasResult.gasMethod
|
|
4622
|
-
};
|
|
4623
|
-
}
|
|
4624
|
-
// -- Invest Rebalance --
|
|
4625
|
-
async investRebalance(opts = {}) {
|
|
4626
|
-
this.enforcer.assertNotLocked();
|
|
4627
|
-
const minDiff = opts.minYieldDiff ?? 0.1;
|
|
4628
|
-
const positions = this.portfolio.getPositions().filter((p) => p.earning && p.earningProtocol);
|
|
4629
|
-
if (positions.length === 0) {
|
|
4630
|
-
return { executed: false, moves: [], totalGasCost: 0, skipped: [] };
|
|
4631
|
-
}
|
|
4632
|
-
const moves = [];
|
|
4633
|
-
const skipped = [];
|
|
4634
|
-
let totalGasCost = 0;
|
|
4635
|
-
for (const pos of positions) {
|
|
4636
|
-
const currentProtocol = pos.earningProtocol;
|
|
4637
|
-
let best;
|
|
4638
|
-
try {
|
|
4639
|
-
best = await this.registry.bestSaveRate(pos.asset);
|
|
4640
|
-
} catch {
|
|
4641
|
-
const currentApy2 = pos.earningApy ?? 0;
|
|
4642
|
-
skipped.push({ asset: pos.asset, protocol: currentProtocol, apy: currentApy2, bestApy: currentApy2, reason: "no_rates" });
|
|
4643
|
-
continue;
|
|
4644
|
-
}
|
|
4645
|
-
let currentApy = pos.earningApy ?? 0;
|
|
4646
|
-
try {
|
|
4647
|
-
const currentAdapter = this.registry.getLending(currentProtocol);
|
|
4648
|
-
if (currentAdapter) {
|
|
4649
|
-
const liveRate = await currentAdapter.getRates(pos.asset);
|
|
4650
|
-
currentApy = liveRate.saveApy;
|
|
4651
|
-
}
|
|
4652
|
-
} catch {
|
|
4653
|
-
}
|
|
4654
|
-
const apyGain = best.rate.saveApy - currentApy;
|
|
4655
|
-
if (best.adapter.id === currentProtocol || apyGain <= 0) {
|
|
4656
|
-
skipped.push({ asset: pos.asset, protocol: currentProtocol, apy: currentApy, bestApy: best.rate.saveApy, reason: "already_best" });
|
|
4657
|
-
continue;
|
|
4658
|
-
}
|
|
4659
|
-
if (apyGain < minDiff) {
|
|
4660
|
-
skipped.push({ asset: pos.asset, protocol: currentProtocol, apy: currentApy, bestApy: best.rate.saveApy, reason: "below_threshold" });
|
|
4661
|
-
continue;
|
|
4662
|
-
}
|
|
4663
|
-
if (opts.dryRun) {
|
|
4664
|
-
moves.push({
|
|
4665
|
-
asset: pos.asset,
|
|
4666
|
-
fromProtocol: this.registry.getLending(currentProtocol)?.name ?? currentProtocol,
|
|
4667
|
-
toProtocol: best.adapter.name,
|
|
4668
|
-
amount: pos.totalAmount,
|
|
4669
|
-
oldApy: currentApy,
|
|
4670
|
-
newApy: best.rate.saveApy,
|
|
4671
|
-
txDigests: [],
|
|
4672
|
-
gasCost: 0
|
|
4673
|
-
});
|
|
4674
|
-
continue;
|
|
4675
|
-
}
|
|
4676
|
-
const txDigests = [];
|
|
4677
|
-
let moveGasCost = 0;
|
|
4678
|
-
const fromAdapter = this.registry.getLending(currentProtocol);
|
|
4679
|
-
if (!fromAdapter) {
|
|
4680
|
-
skipped.push({ asset: pos.asset, protocol: currentProtocol, apy: currentApy, bestApy: best.rate.saveApy, reason: "protocol_unavailable" });
|
|
4681
|
-
continue;
|
|
4682
|
-
}
|
|
4683
|
-
const withdrawResult = await executeWithGas(this.client, this._signer, async () => {
|
|
4684
|
-
const result = await fromAdapter.buildWithdrawTx(this._address, pos.totalAmount, pos.asset);
|
|
4685
|
-
return result.tx;
|
|
4686
|
-
});
|
|
4687
|
-
txDigests.push(withdrawResult.digest);
|
|
4688
|
-
moveGasCost += withdrawResult.gasCostSui;
|
|
4689
|
-
const depositResult = await executeWithGas(this.client, this._signer, async () => {
|
|
4690
|
-
const assetInfo = SUPPORTED_ASSETS[pos.asset];
|
|
4691
|
-
const balance = await this.client.getBalance({ owner: this._address, coinType: assetInfo.type });
|
|
4692
|
-
const available = Number(balance.totalBalance) / 10 ** assetInfo.decimals;
|
|
4693
|
-
const gasReserve = pos.asset === "SUI" ? GAS_RESERVE_MIN : 0;
|
|
4694
|
-
const depositAmount = Math.max(0, available - gasReserve);
|
|
4695
|
-
const { tx } = await best.adapter.buildSaveTx(this._address, depositAmount, pos.asset);
|
|
4696
|
-
return tx;
|
|
4697
|
-
});
|
|
4698
|
-
txDigests.push(depositResult.digest);
|
|
4699
|
-
moveGasCost += depositResult.gasCostSui;
|
|
4700
|
-
this.portfolio.recordUnearn(pos.asset);
|
|
4701
|
-
this.portfolio.recordEarn(pos.asset, best.adapter.id, best.rate.saveApy);
|
|
4702
|
-
moves.push({
|
|
4703
|
-
asset: pos.asset,
|
|
4704
|
-
fromProtocol: fromAdapter.name,
|
|
4705
|
-
toProtocol: best.adapter.name,
|
|
4706
|
-
amount: pos.totalAmount,
|
|
4707
|
-
oldApy: currentApy,
|
|
4708
|
-
newApy: best.rate.saveApy,
|
|
4709
|
-
txDigests,
|
|
4710
|
-
gasCost: moveGasCost
|
|
4711
|
-
});
|
|
4712
|
-
totalGasCost += moveGasCost;
|
|
2878
|
+
success: true,
|
|
2879
|
+
tx: gasResult.digest,
|
|
2880
|
+
amount: totalRepaid,
|
|
2881
|
+
remainingDebt: hf.borrowed,
|
|
2882
|
+
gasCost: gasResult.gasCostSui,
|
|
2883
|
+
gasMethod: gasResult.gasMethod
|
|
2884
|
+
};
|
|
2885
|
+
}
|
|
2886
|
+
async maxBorrow() {
|
|
2887
|
+
const adapter = await this.resolveLending(void 0, "USDC", "borrow");
|
|
2888
|
+
return adapter.maxBorrow(this._address, "USDC");
|
|
2889
|
+
}
|
|
2890
|
+
async healthFactor() {
|
|
2891
|
+
const adapter = await this.resolveLending(void 0, "USDC", "save");
|
|
2892
|
+
const hf = await adapter.getHealth(this._address);
|
|
2893
|
+
if (hf.healthFactor < 1.2) {
|
|
2894
|
+
this.emit("healthCritical", { healthFactor: hf.healthFactor, threshold: 1.5, severity: "critical" });
|
|
2895
|
+
} else if (hf.healthFactor < 2) {
|
|
2896
|
+
this.emit("healthWarning", { healthFactor: hf.healthFactor, threshold: 2, severity: "warning" });
|
|
4713
2897
|
}
|
|
4714
|
-
return
|
|
2898
|
+
return hf;
|
|
4715
2899
|
}
|
|
4716
2900
|
// -- Claim Rewards --
|
|
4717
2901
|
async getPendingRewards() {
|
|
@@ -4729,7 +2913,7 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
4729
2913
|
this.enforcer.assertNotLocked();
|
|
4730
2914
|
const adapters = this.registry.listLending().filter((a) => a.addClaimRewardsToTx);
|
|
4731
2915
|
if (adapters.length === 0) {
|
|
4732
|
-
return { success: true, tx: "", rewards: [], totalValueUsd: 0,
|
|
2916
|
+
return { success: true, tx: "", rewards: [], totalValueUsd: 0, gasCost: 0, gasMethod: "none" };
|
|
4733
2917
|
}
|
|
4734
2918
|
const tx = new Transaction();
|
|
4735
2919
|
tx.setSender(this._address);
|
|
@@ -4742,698 +2926,20 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
4742
2926
|
}
|
|
4743
2927
|
}
|
|
4744
2928
|
if (allRewards.length === 0) {
|
|
4745
|
-
return { success: true, tx: "", rewards: [], totalValueUsd: 0,
|
|
2929
|
+
return { success: true, tx: "", rewards: [], totalValueUsd: 0, gasCost: 0, gasMethod: "none" };
|
|
4746
2930
|
}
|
|
4747
2931
|
const claimResult = await executeWithGas(this.client, this._signer, async () => tx);
|
|
4748
2932
|
await this.client.waitForTransaction({ digest: claimResult.digest });
|
|
4749
|
-
const
|
|
2933
|
+
const totalValueUsd = allRewards.reduce((s, r) => s + r.estimatedValueUsd, 0);
|
|
4750
2934
|
return {
|
|
4751
2935
|
success: true,
|
|
4752
2936
|
tx: claimResult.digest,
|
|
4753
2937
|
rewards: allRewards,
|
|
4754
|
-
totalValueUsd
|
|
4755
|
-
usdcReceived,
|
|
2938
|
+
totalValueUsd,
|
|
4756
2939
|
gasCost: claimResult.gasCostSui,
|
|
4757
2940
|
gasMethod: claimResult.gasMethod
|
|
4758
2941
|
};
|
|
4759
2942
|
}
|
|
4760
|
-
async swapRewardTokensToUsdc(rewards) {
|
|
4761
|
-
const uniqueTokens = [...new Set(rewards.map((r) => r.coinType))];
|
|
4762
|
-
const usdcType = SUPPORTED_ASSETS.USDC.type;
|
|
4763
|
-
const usdcDecimals = SUPPORTED_ASSETS.USDC.decimals;
|
|
4764
|
-
let totalUsdc = 0;
|
|
4765
|
-
for (const coinType of uniqueTokens) {
|
|
4766
|
-
try {
|
|
4767
|
-
const balResult = await this.client.getBalance({
|
|
4768
|
-
owner: this._address,
|
|
4769
|
-
coinType
|
|
4770
|
-
});
|
|
4771
|
-
const rawBalance = BigInt(balResult.totalBalance);
|
|
4772
|
-
if (rawBalance <= 0n) continue;
|
|
4773
|
-
const decimals = REWARD_TOKEN_DECIMALS[coinType] ?? 9;
|
|
4774
|
-
const swapResult = await buildRawSwapTx({
|
|
4775
|
-
client: this.client,
|
|
4776
|
-
address: this._address,
|
|
4777
|
-
fromCoinType: coinType,
|
|
4778
|
-
fromDecimals: decimals,
|
|
4779
|
-
toCoinType: usdcType,
|
|
4780
|
-
toDecimals: usdcDecimals,
|
|
4781
|
-
amount: rawBalance
|
|
4782
|
-
});
|
|
4783
|
-
const gasResult = await executeWithGas(this.client, this._signer, async () => swapResult.tx);
|
|
4784
|
-
await this.client.waitForTransaction({ digest: gasResult.digest });
|
|
4785
|
-
totalUsdc += swapResult.estimatedOut / 10 ** usdcDecimals;
|
|
4786
|
-
} catch {
|
|
4787
|
-
}
|
|
4788
|
-
}
|
|
4789
|
-
return totalUsdc;
|
|
4790
|
-
}
|
|
4791
|
-
// -- Strategies --
|
|
4792
|
-
async investStrategy(params) {
|
|
4793
|
-
this.enforcer.assertNotLocked();
|
|
4794
|
-
const definition = this.strategies.get(params.strategy);
|
|
4795
|
-
this.strategies.validateMinAmount(definition.allocations, params.usdAmount);
|
|
4796
|
-
if (!params.usdAmount || params.usdAmount <= 0) {
|
|
4797
|
-
throw new T2000Error("INVALID_AMOUNT", "Strategy investment must be > $0");
|
|
4798
|
-
}
|
|
4799
|
-
this.enforcer.check({ operation: "invest", amount: params.usdAmount });
|
|
4800
|
-
const bal = await queryBalance(this.client, this._address);
|
|
4801
|
-
const totalFunds = bal.available + bal.savings;
|
|
4802
|
-
if (params.usdAmount > totalFunds * 1.05) {
|
|
4803
|
-
throw new T2000Error(
|
|
4804
|
-
"INSUFFICIENT_BALANCE",
|
|
4805
|
-
`Insufficient funds. You have $${totalFunds.toFixed(2)} total (checking: $${bal.available.toFixed(2)}, savings: $${bal.savings.toFixed(2)}) but need $${params.usdAmount.toFixed(2)}.`
|
|
4806
|
-
);
|
|
4807
|
-
}
|
|
4808
|
-
if (bal.available < params.usdAmount && !params.dryRun) {
|
|
4809
|
-
await this._autoFundFromSavings(params.usdAmount - bal.available);
|
|
4810
|
-
}
|
|
4811
|
-
const buys = [];
|
|
4812
|
-
const allocEntries = Object.entries(definition.allocations);
|
|
4813
|
-
if (params.dryRun) {
|
|
4814
|
-
const swapAdapter2 = this.registry.listSwap()[0];
|
|
4815
|
-
for (const [asset, pct] of allocEntries) {
|
|
4816
|
-
const assetUsd = params.usdAmount * (pct / 100);
|
|
4817
|
-
let estAmount = 0;
|
|
4818
|
-
let estPrice = 0;
|
|
4819
|
-
try {
|
|
4820
|
-
if (swapAdapter2) {
|
|
4821
|
-
const quote = await swapAdapter2.getQuote("USDC", asset, assetUsd);
|
|
4822
|
-
estAmount = quote.expectedOutput;
|
|
4823
|
-
estPrice = assetUsd / estAmount;
|
|
4824
|
-
}
|
|
4825
|
-
} catch {
|
|
4826
|
-
}
|
|
4827
|
-
buys.push({ asset, usdAmount: assetUsd, amount: estAmount, price: estPrice, tx: "" });
|
|
4828
|
-
}
|
|
4829
|
-
return { success: true, strategy: params.strategy, totalInvested: params.usdAmount, buys, gasCost: 0, gasMethod: "self-funded" };
|
|
4830
|
-
}
|
|
4831
|
-
const swapAdapter = this.registry.listSwap()[0];
|
|
4832
|
-
if (!swapAdapter?.addSwapToTx) {
|
|
4833
|
-
throw new T2000Error("PROTOCOL_UNAVAILABLE", "Swap adapter does not support composable PTB");
|
|
4834
|
-
}
|
|
4835
|
-
let swapMetas = [];
|
|
4836
|
-
const gasResult = await executeWithGas(this.client, this._signer, async () => {
|
|
4837
|
-
swapMetas = [];
|
|
4838
|
-
const tx = new Transaction();
|
|
4839
|
-
tx.setSender(this._address);
|
|
4840
|
-
const usdcCoins = await this._fetchCoins(SUPPORTED_ASSETS.USDC.type);
|
|
4841
|
-
if (usdcCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins found");
|
|
4842
|
-
const mergedUsdc = this._mergeCoinsInTx(tx, usdcCoins);
|
|
4843
|
-
const splitAmounts = allocEntries.map(
|
|
4844
|
-
([, pct]) => BigInt(Math.floor(params.usdAmount * (pct / 100) * 10 ** SUPPORTED_ASSETS.USDC.decimals))
|
|
4845
|
-
);
|
|
4846
|
-
const splitCoins = tx.splitCoins(mergedUsdc, splitAmounts);
|
|
4847
|
-
const outputCoins = [];
|
|
4848
|
-
for (let i = 0; i < allocEntries.length; i++) {
|
|
4849
|
-
const [asset] = allocEntries[i];
|
|
4850
|
-
const assetUsd = params.usdAmount * (allocEntries[i][1] / 100);
|
|
4851
|
-
const { outputCoin, estimatedOut, toDecimals } = await swapAdapter.addSwapToTx(
|
|
4852
|
-
tx,
|
|
4853
|
-
this._address,
|
|
4854
|
-
splitCoins[i],
|
|
4855
|
-
"USDC",
|
|
4856
|
-
asset,
|
|
4857
|
-
assetUsd
|
|
4858
|
-
);
|
|
4859
|
-
outputCoins.push(outputCoin);
|
|
4860
|
-
swapMetas.push({ asset, usdAmount: assetUsd, estimatedOut, toDecimals });
|
|
4861
|
-
}
|
|
4862
|
-
tx.transferObjects(outputCoins, this._address);
|
|
4863
|
-
return tx;
|
|
4864
|
-
});
|
|
4865
|
-
const digest = gasResult.digest;
|
|
4866
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4867
|
-
for (const meta of swapMetas) {
|
|
4868
|
-
const amount = meta.estimatedOut / 10 ** meta.toDecimals;
|
|
4869
|
-
const price = meta.usdAmount / amount;
|
|
4870
|
-
this.portfolio.recordBuy({
|
|
4871
|
-
id: `inv_${Date.now()}_${meta.asset}`,
|
|
4872
|
-
type: "buy",
|
|
4873
|
-
asset: meta.asset,
|
|
4874
|
-
amount,
|
|
4875
|
-
price,
|
|
4876
|
-
usdValue: meta.usdAmount,
|
|
4877
|
-
fee: 0,
|
|
4878
|
-
tx: digest,
|
|
4879
|
-
timestamp: now
|
|
4880
|
-
});
|
|
4881
|
-
this.portfolio.recordStrategyBuy(params.strategy, {
|
|
4882
|
-
id: `strat_${Date.now()}_${meta.asset}`,
|
|
4883
|
-
type: "buy",
|
|
4884
|
-
asset: meta.asset,
|
|
4885
|
-
amount,
|
|
4886
|
-
price,
|
|
4887
|
-
usdValue: meta.usdAmount,
|
|
4888
|
-
fee: 0,
|
|
4889
|
-
tx: digest,
|
|
4890
|
-
timestamp: now
|
|
4891
|
-
});
|
|
4892
|
-
buys.push({ asset: meta.asset, usdAmount: meta.usdAmount, amount, price, tx: digest });
|
|
4893
|
-
}
|
|
4894
|
-
return {
|
|
4895
|
-
success: true,
|
|
4896
|
-
strategy: params.strategy,
|
|
4897
|
-
totalInvested: params.usdAmount,
|
|
4898
|
-
buys,
|
|
4899
|
-
gasCost: gasResult.gasCostSui,
|
|
4900
|
-
gasMethod: gasResult.gasMethod
|
|
4901
|
-
};
|
|
4902
|
-
}
|
|
4903
|
-
async sellStrategy(params) {
|
|
4904
|
-
this.enforcer.assertNotLocked();
|
|
4905
|
-
this.strategies.get(params.strategy);
|
|
4906
|
-
const stratPositions = this.portfolio.getStrategyPositions(params.strategy);
|
|
4907
|
-
if (stratPositions.length === 0) {
|
|
4908
|
-
throw new T2000Error("INSUFFICIENT_INVESTMENT", `No positions in strategy '${params.strategy}'`);
|
|
4909
|
-
}
|
|
4910
|
-
const swapAdapter = this.registry.listSwap()[0];
|
|
4911
|
-
if (!swapAdapter?.addSwapToTx) {
|
|
4912
|
-
throw new T2000Error("PROTOCOL_UNAVAILABLE", "Swap adapter does not support composable PTB");
|
|
4913
|
-
}
|
|
4914
|
-
const unearnFailures = [];
|
|
4915
|
-
for (const pos of stratPositions) {
|
|
4916
|
-
try {
|
|
4917
|
-
await this.investUnearn({ asset: pos.asset });
|
|
4918
|
-
await new Promise((r) => setTimeout(r, 1500));
|
|
4919
|
-
} catch (err) {
|
|
4920
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
4921
|
-
if (!msg.includes("not currently earning")) {
|
|
4922
|
-
unearnFailures.push({ asset: pos.asset, error: msg });
|
|
4923
|
-
}
|
|
4924
|
-
}
|
|
4925
|
-
}
|
|
4926
|
-
let swapMetas = [];
|
|
4927
|
-
const buildSellPtb = async () => {
|
|
4928
|
-
swapMetas = [];
|
|
4929
|
-
const tx = new Transaction();
|
|
4930
|
-
tx.setSender(this._address);
|
|
4931
|
-
const usdcOutputs = [];
|
|
4932
|
-
for (const pos of stratPositions) {
|
|
4933
|
-
const assetInfo = SUPPORTED_ASSETS[pos.asset];
|
|
4934
|
-
const bal = await this.client.getBalance({ owner: this._address, coinType: assetInfo.type });
|
|
4935
|
-
const walletAmount = Number(bal.totalBalance) / 10 ** assetInfo.decimals;
|
|
4936
|
-
const gasReserve = pos.asset === "SUI" ? GAS_RESERVE_MIN : 0;
|
|
4937
|
-
const sellAmount = Math.max(0, Math.min(pos.totalAmount, walletAmount) - gasReserve);
|
|
4938
|
-
if (sellAmount <= 0) continue;
|
|
4939
|
-
const rawAmount = BigInt(Math.floor(sellAmount * 10 ** assetInfo.decimals));
|
|
4940
|
-
let splitCoin;
|
|
4941
|
-
if (pos.asset === "SUI") {
|
|
4942
|
-
[splitCoin] = tx.splitCoins(tx.gas, [rawAmount]);
|
|
4943
|
-
} else {
|
|
4944
|
-
const coins = await this._fetchCoins(assetInfo.type);
|
|
4945
|
-
if (coins.length === 0) continue;
|
|
4946
|
-
const merged = this._mergeCoinsInTx(tx, coins);
|
|
4947
|
-
[splitCoin] = tx.splitCoins(merged, [rawAmount]);
|
|
4948
|
-
}
|
|
4949
|
-
const slippageBps = LOW_LIQUIDITY_ASSETS.has(pos.asset) ? 500 : 300;
|
|
4950
|
-
const { outputCoin, estimatedOut, toDecimals } = await swapAdapter.addSwapToTx(
|
|
4951
|
-
tx,
|
|
4952
|
-
this._address,
|
|
4953
|
-
splitCoin,
|
|
4954
|
-
pos.asset,
|
|
4955
|
-
"USDC",
|
|
4956
|
-
sellAmount,
|
|
4957
|
-
slippageBps
|
|
4958
|
-
);
|
|
4959
|
-
usdcOutputs.push(outputCoin);
|
|
4960
|
-
swapMetas.push({ asset: pos.asset, amount: sellAmount, estimatedOut, toDecimals });
|
|
4961
|
-
}
|
|
4962
|
-
if (usdcOutputs.length === 0) {
|
|
4963
|
-
throw new T2000Error("INSUFFICIENT_BALANCE", "No assets available to sell");
|
|
4964
|
-
}
|
|
4965
|
-
if (usdcOutputs.length > 1) {
|
|
4966
|
-
tx.mergeCoins(usdcOutputs[0], usdcOutputs.slice(1));
|
|
4967
|
-
}
|
|
4968
|
-
tx.transferObjects([usdcOutputs[0]], this._address);
|
|
4969
|
-
return tx;
|
|
4970
|
-
};
|
|
4971
|
-
let gasResult;
|
|
4972
|
-
const MAX_RETRIES = 3;
|
|
4973
|
-
for (let attempt = 0; ; attempt++) {
|
|
4974
|
-
try {
|
|
4975
|
-
gasResult = await executeWithGas(this.client, this._signer, buildSellPtb);
|
|
4976
|
-
break;
|
|
4977
|
-
} catch (err) {
|
|
4978
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
4979
|
-
const isSlippage = msg.includes("slippage") || msg.includes("amount_out_slippage");
|
|
4980
|
-
if (isSlippage && attempt < MAX_RETRIES) {
|
|
4981
|
-
await new Promise((r) => setTimeout(r, 2e3 * (attempt + 1)));
|
|
4982
|
-
continue;
|
|
4983
|
-
}
|
|
4984
|
-
throw err;
|
|
4985
|
-
}
|
|
4986
|
-
}
|
|
4987
|
-
const digest = gasResult.digest;
|
|
4988
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4989
|
-
const sells = [];
|
|
4990
|
-
let totalProceeds = 0;
|
|
4991
|
-
let totalPnL = 0;
|
|
4992
|
-
for (const meta of swapMetas) {
|
|
4993
|
-
const usdValue = meta.estimatedOut / 10 ** meta.toDecimals;
|
|
4994
|
-
const price = meta.amount > 0 ? usdValue / meta.amount : 0;
|
|
4995
|
-
let pnl = 0;
|
|
4996
|
-
try {
|
|
4997
|
-
pnl = this.portfolio.recordStrategySell(params.strategy, {
|
|
4998
|
-
id: `strat_sell_${Date.now()}_${meta.asset}`,
|
|
4999
|
-
type: "sell",
|
|
5000
|
-
asset: meta.asset,
|
|
5001
|
-
amount: meta.amount,
|
|
5002
|
-
price,
|
|
5003
|
-
usdValue,
|
|
5004
|
-
fee: 0,
|
|
5005
|
-
tx: digest,
|
|
5006
|
-
timestamp: now
|
|
5007
|
-
});
|
|
5008
|
-
} catch {
|
|
5009
|
-
}
|
|
5010
|
-
try {
|
|
5011
|
-
this.portfolio.recordSell({
|
|
5012
|
-
id: `inv_sell_${Date.now()}_${meta.asset}`,
|
|
5013
|
-
type: "sell",
|
|
5014
|
-
asset: meta.asset,
|
|
5015
|
-
amount: meta.amount,
|
|
5016
|
-
price,
|
|
5017
|
-
usdValue,
|
|
5018
|
-
fee: 0,
|
|
5019
|
-
tx: digest,
|
|
5020
|
-
timestamp: now
|
|
5021
|
-
});
|
|
5022
|
-
} catch {
|
|
5023
|
-
}
|
|
5024
|
-
sells.push({ asset: meta.asset, amount: meta.amount, usdValue, realizedPnL: pnl, tx: digest });
|
|
5025
|
-
totalProceeds += usdValue;
|
|
5026
|
-
totalPnL += pnl;
|
|
5027
|
-
}
|
|
5028
|
-
if (this.portfolio.hasStrategyPositions(params.strategy)) {
|
|
5029
|
-
if (unearnFailures.length === 0) {
|
|
5030
|
-
this.portfolio.clearStrategy(params.strategy);
|
|
5031
|
-
} else {
|
|
5032
|
-
for (const s of sells) {
|
|
5033
|
-
this.portfolio.closeStrategyPosition(params.strategy, s.asset);
|
|
5034
|
-
}
|
|
5035
|
-
}
|
|
5036
|
-
}
|
|
5037
|
-
const failed = unearnFailures.map((f) => ({ asset: f.asset, reason: f.error }));
|
|
5038
|
-
return {
|
|
5039
|
-
success: true,
|
|
5040
|
-
strategy: params.strategy,
|
|
5041
|
-
totalProceeds,
|
|
5042
|
-
realizedPnL: totalPnL,
|
|
5043
|
-
sells,
|
|
5044
|
-
failed: failed.length > 0 ? failed : void 0,
|
|
5045
|
-
gasCost: gasResult.gasCostSui,
|
|
5046
|
-
gasMethod: gasResult.gasMethod
|
|
5047
|
-
};
|
|
5048
|
-
}
|
|
5049
|
-
async rebalanceStrategy(params) {
|
|
5050
|
-
this.enforcer.assertNotLocked();
|
|
5051
|
-
const definition = this.strategies.get(params.strategy);
|
|
5052
|
-
const stratPositions = this.portfolio.getStrategyPositions(params.strategy);
|
|
5053
|
-
if (stratPositions.length === 0) {
|
|
5054
|
-
throw new T2000Error("INSUFFICIENT_INVESTMENT", `No positions in strategy '${params.strategy}'`);
|
|
5055
|
-
}
|
|
5056
|
-
const swapAdapter = this.registry.listSwap()[0];
|
|
5057
|
-
const prices = {};
|
|
5058
|
-
for (const pos of stratPositions) {
|
|
5059
|
-
try {
|
|
5060
|
-
if (pos.asset === "SUI" && swapAdapter) {
|
|
5061
|
-
prices[pos.asset] = await swapAdapter.getPoolPrice();
|
|
5062
|
-
} else if (swapAdapter) {
|
|
5063
|
-
const q = await swapAdapter.getQuote("USDC", pos.asset, 1);
|
|
5064
|
-
prices[pos.asset] = q.expectedOutput > 0 ? 1 / q.expectedOutput : 0;
|
|
5065
|
-
}
|
|
5066
|
-
} catch {
|
|
5067
|
-
prices[pos.asset] = 0;
|
|
5068
|
-
}
|
|
5069
|
-
}
|
|
5070
|
-
const totalValue = stratPositions.reduce((s, p) => s + p.totalAmount * (prices[p.asset] ?? 0), 0);
|
|
5071
|
-
if (totalValue <= 0) {
|
|
5072
|
-
throw new T2000Error("INSUFFICIENT_INVESTMENT", "Strategy has no value to rebalance");
|
|
5073
|
-
}
|
|
5074
|
-
const currentWeights = {};
|
|
5075
|
-
const beforeWeights = {};
|
|
5076
|
-
for (const pos of stratPositions) {
|
|
5077
|
-
const w = pos.totalAmount * (prices[pos.asset] ?? 0) / totalValue * 100;
|
|
5078
|
-
currentWeights[pos.asset] = w;
|
|
5079
|
-
beforeWeights[pos.asset] = w;
|
|
5080
|
-
}
|
|
5081
|
-
const threshold = 3;
|
|
5082
|
-
const sellOps = [];
|
|
5083
|
-
const buyOps = [];
|
|
5084
|
-
for (const [asset, targetPct] of Object.entries(definition.allocations)) {
|
|
5085
|
-
const currentPct = currentWeights[asset] ?? 0;
|
|
5086
|
-
const diff = targetPct - currentPct;
|
|
5087
|
-
if (Math.abs(diff) < threshold) continue;
|
|
5088
|
-
const usdDiff = totalValue * (Math.abs(diff) / 100);
|
|
5089
|
-
if (usdDiff < 1) continue;
|
|
5090
|
-
if (diff > 0) {
|
|
5091
|
-
buyOps.push({ asset, usdAmount: usdDiff });
|
|
5092
|
-
} else {
|
|
5093
|
-
const price = prices[asset] ?? 1;
|
|
5094
|
-
const assetAmount = price > 0 ? usdDiff / price : 0;
|
|
5095
|
-
sellOps.push({ asset, usdAmount: usdDiff, assetAmount });
|
|
5096
|
-
}
|
|
5097
|
-
}
|
|
5098
|
-
if (sellOps.length === 0 && buyOps.length === 0) {
|
|
5099
|
-
return { success: true, strategy: params.strategy, trades: [], beforeWeights, afterWeights: { ...beforeWeights }, targetWeights: { ...definition.allocations } };
|
|
5100
|
-
}
|
|
5101
|
-
if (!swapAdapter?.addSwapToTx) {
|
|
5102
|
-
throw new T2000Error("PROTOCOL_UNAVAILABLE", "Swap adapter does not support composable PTB");
|
|
5103
|
-
}
|
|
5104
|
-
const tradeMetas = [];
|
|
5105
|
-
const gasResult = await executeWithGas(this.client, this._signer, async () => {
|
|
5106
|
-
tradeMetas.length = 0;
|
|
5107
|
-
const tx = new Transaction();
|
|
5108
|
-
tx.setSender(this._address);
|
|
5109
|
-
const usdcCoins = [];
|
|
5110
|
-
for (const sell of sellOps) {
|
|
5111
|
-
const assetInfo = SUPPORTED_ASSETS[sell.asset];
|
|
5112
|
-
const bal = await this.client.getBalance({ owner: this._address, coinType: assetInfo.type });
|
|
5113
|
-
const walletAmount = Number(bal.totalBalance) / 10 ** assetInfo.decimals;
|
|
5114
|
-
const gasReserve = sell.asset === "SUI" ? GAS_RESERVE_MIN : 0;
|
|
5115
|
-
const sellAmount = Math.max(0, Math.min(sell.assetAmount, walletAmount) - gasReserve);
|
|
5116
|
-
if (sellAmount <= 0) continue;
|
|
5117
|
-
const rawAmount = BigInt(Math.floor(sellAmount * 10 ** assetInfo.decimals));
|
|
5118
|
-
let splitCoin;
|
|
5119
|
-
if (sell.asset === "SUI") {
|
|
5120
|
-
[splitCoin] = tx.splitCoins(tx.gas, [rawAmount]);
|
|
5121
|
-
} else {
|
|
5122
|
-
const coins = await this._fetchCoins(assetInfo.type);
|
|
5123
|
-
if (coins.length === 0) continue;
|
|
5124
|
-
const merged = this._mergeCoinsInTx(tx, coins);
|
|
5125
|
-
[splitCoin] = tx.splitCoins(merged, [rawAmount]);
|
|
5126
|
-
}
|
|
5127
|
-
const slippageBps = LOW_LIQUIDITY_ASSETS.has(sell.asset) ? 500 : 300;
|
|
5128
|
-
const { outputCoin, estimatedOut, toDecimals } = await swapAdapter.addSwapToTx(
|
|
5129
|
-
tx,
|
|
5130
|
-
this._address,
|
|
5131
|
-
splitCoin,
|
|
5132
|
-
sell.asset,
|
|
5133
|
-
"USDC",
|
|
5134
|
-
sellAmount,
|
|
5135
|
-
slippageBps
|
|
5136
|
-
);
|
|
5137
|
-
usdcCoins.push(outputCoin);
|
|
5138
|
-
tradeMetas.push({ action: "sell", asset: sell.asset, usdAmount: sell.usdAmount, estimatedOut, toDecimals });
|
|
5139
|
-
}
|
|
5140
|
-
if (buyOps.length > 0) {
|
|
5141
|
-
const walletUsdc = await this._fetchCoins(SUPPORTED_ASSETS.USDC.type);
|
|
5142
|
-
if (walletUsdc.length > 0) {
|
|
5143
|
-
usdcCoins.push(this._mergeCoinsInTx(tx, walletUsdc));
|
|
5144
|
-
}
|
|
5145
|
-
if (usdcCoins.length === 0) {
|
|
5146
|
-
throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC available for rebalance buys");
|
|
5147
|
-
}
|
|
5148
|
-
if (usdcCoins.length > 1) {
|
|
5149
|
-
tx.mergeCoins(usdcCoins[0], usdcCoins.slice(1));
|
|
5150
|
-
}
|
|
5151
|
-
const mergedUsdc = usdcCoins[0];
|
|
5152
|
-
const splitAmounts = buyOps.map(
|
|
5153
|
-
(b) => BigInt(Math.floor(b.usdAmount * 10 ** SUPPORTED_ASSETS.USDC.decimals))
|
|
5154
|
-
);
|
|
5155
|
-
const splitCoins = tx.splitCoins(mergedUsdc, splitAmounts);
|
|
5156
|
-
const outputCoins = [];
|
|
5157
|
-
for (let i = 0; i < buyOps.length; i++) {
|
|
5158
|
-
const buy = buyOps[i];
|
|
5159
|
-
const slippageBps = LOW_LIQUIDITY_ASSETS.has(buy.asset) ? 500 : 300;
|
|
5160
|
-
const { outputCoin, estimatedOut, toDecimals } = await swapAdapter.addSwapToTx(
|
|
5161
|
-
tx,
|
|
5162
|
-
this._address,
|
|
5163
|
-
splitCoins[i],
|
|
5164
|
-
"USDC",
|
|
5165
|
-
buy.asset,
|
|
5166
|
-
buy.usdAmount,
|
|
5167
|
-
slippageBps
|
|
5168
|
-
);
|
|
5169
|
-
outputCoins.push(outputCoin);
|
|
5170
|
-
tradeMetas.push({ action: "buy", asset: buy.asset, usdAmount: buy.usdAmount, estimatedOut, toDecimals });
|
|
5171
|
-
}
|
|
5172
|
-
tx.transferObjects(outputCoins, this._address);
|
|
5173
|
-
}
|
|
5174
|
-
return tx;
|
|
5175
|
-
});
|
|
5176
|
-
const digest = gasResult.digest;
|
|
5177
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5178
|
-
const trades = [];
|
|
5179
|
-
for (const meta of tradeMetas) {
|
|
5180
|
-
const rawAmount = meta.estimatedOut / 10 ** meta.toDecimals;
|
|
5181
|
-
if (meta.action === "sell") {
|
|
5182
|
-
const price = meta.usdAmount > 0 && rawAmount > 0 ? meta.usdAmount / rawAmount : prices[meta.asset] ?? 0;
|
|
5183
|
-
const assetAmount = prices[meta.asset] > 0 ? meta.usdAmount / prices[meta.asset] : 0;
|
|
5184
|
-
this.portfolio.recordStrategySell(params.strategy, {
|
|
5185
|
-
id: `strat_rebal_${Date.now()}_${meta.asset}`,
|
|
5186
|
-
type: "sell",
|
|
5187
|
-
asset: meta.asset,
|
|
5188
|
-
amount: assetAmount,
|
|
5189
|
-
price,
|
|
5190
|
-
usdValue: meta.usdAmount,
|
|
5191
|
-
fee: 0,
|
|
5192
|
-
tx: digest,
|
|
5193
|
-
timestamp: now
|
|
5194
|
-
});
|
|
5195
|
-
this.portfolio.recordSell({
|
|
5196
|
-
id: `inv_rebal_${Date.now()}_${meta.asset}`,
|
|
5197
|
-
type: "sell",
|
|
5198
|
-
asset: meta.asset,
|
|
5199
|
-
amount: assetAmount,
|
|
5200
|
-
price,
|
|
5201
|
-
usdValue: meta.usdAmount,
|
|
5202
|
-
fee: 0,
|
|
5203
|
-
tx: digest,
|
|
5204
|
-
timestamp: now
|
|
5205
|
-
});
|
|
5206
|
-
trades.push({ action: "sell", asset: meta.asset, usdAmount: meta.usdAmount, amount: assetAmount, tx: digest });
|
|
5207
|
-
} else {
|
|
5208
|
-
const amount = rawAmount;
|
|
5209
|
-
const price = meta.usdAmount / amount;
|
|
5210
|
-
this.portfolio.recordBuy({
|
|
5211
|
-
id: `inv_rebal_${Date.now()}_${meta.asset}`,
|
|
5212
|
-
type: "buy",
|
|
5213
|
-
asset: meta.asset,
|
|
5214
|
-
amount,
|
|
5215
|
-
price,
|
|
5216
|
-
usdValue: meta.usdAmount,
|
|
5217
|
-
fee: 0,
|
|
5218
|
-
tx: digest,
|
|
5219
|
-
timestamp: now
|
|
5220
|
-
});
|
|
5221
|
-
this.portfolio.recordStrategyBuy(params.strategy, {
|
|
5222
|
-
id: `strat_rebal_${Date.now()}_${meta.asset}`,
|
|
5223
|
-
type: "buy",
|
|
5224
|
-
asset: meta.asset,
|
|
5225
|
-
amount,
|
|
5226
|
-
price,
|
|
5227
|
-
usdValue: meta.usdAmount,
|
|
5228
|
-
fee: 0,
|
|
5229
|
-
tx: digest,
|
|
5230
|
-
timestamp: now
|
|
5231
|
-
});
|
|
5232
|
-
trades.push({ action: "buy", asset: meta.asset, usdAmount: meta.usdAmount, amount, tx: digest });
|
|
5233
|
-
}
|
|
5234
|
-
}
|
|
5235
|
-
const afterWeights = {};
|
|
5236
|
-
const updatedPositions = this.portfolio.getStrategyPositions(params.strategy);
|
|
5237
|
-
const newTotal = updatedPositions.reduce((s, p) => s + p.totalAmount * (prices[p.asset] ?? 0), 0);
|
|
5238
|
-
for (const p of updatedPositions) {
|
|
5239
|
-
afterWeights[p.asset] = newTotal > 0 ? p.totalAmount * (prices[p.asset] ?? 0) / newTotal * 100 : 0;
|
|
5240
|
-
}
|
|
5241
|
-
return { success: true, strategy: params.strategy, trades, beforeWeights, afterWeights, targetWeights: { ...definition.allocations } };
|
|
5242
|
-
}
|
|
5243
|
-
async getStrategyStatus(name) {
|
|
5244
|
-
const definition = this.strategies.get(name);
|
|
5245
|
-
const stratPositions = this.portfolio.getStrategyPositions(name);
|
|
5246
|
-
const swapAdapter = this.registry.listSwap()[0];
|
|
5247
|
-
const prices = {};
|
|
5248
|
-
for (const asset of Object.keys(definition.allocations)) {
|
|
5249
|
-
try {
|
|
5250
|
-
if (asset === "SUI" && swapAdapter) {
|
|
5251
|
-
prices[asset] = await swapAdapter.getPoolPrice();
|
|
5252
|
-
} else if (swapAdapter) {
|
|
5253
|
-
const q = await swapAdapter.getQuote("USDC", asset, 1);
|
|
5254
|
-
prices[asset] = q.expectedOutput > 0 ? 1 / q.expectedOutput : 0;
|
|
5255
|
-
}
|
|
5256
|
-
} catch {
|
|
5257
|
-
prices[asset] = 0;
|
|
5258
|
-
}
|
|
5259
|
-
}
|
|
5260
|
-
const positions = stratPositions.map((sp) => {
|
|
5261
|
-
const price = prices[sp.asset] ?? 0;
|
|
5262
|
-
const currentValue = sp.totalAmount * price;
|
|
5263
|
-
const pnl = currentValue - sp.costBasis;
|
|
5264
|
-
return {
|
|
5265
|
-
asset: sp.asset,
|
|
5266
|
-
totalAmount: sp.totalAmount,
|
|
5267
|
-
costBasis: sp.costBasis,
|
|
5268
|
-
avgPrice: sp.avgPrice,
|
|
5269
|
-
currentPrice: price,
|
|
5270
|
-
currentValue,
|
|
5271
|
-
unrealizedPnL: pnl,
|
|
5272
|
-
unrealizedPnLPct: sp.costBasis > 0 ? pnl / sp.costBasis * 100 : 0,
|
|
5273
|
-
trades: sp.trades
|
|
5274
|
-
};
|
|
5275
|
-
});
|
|
5276
|
-
const totalValue = positions.reduce((s, p) => s + p.currentValue, 0);
|
|
5277
|
-
const currentWeights = {};
|
|
5278
|
-
for (const p of positions) {
|
|
5279
|
-
currentWeights[p.asset] = totalValue > 0 ? p.currentValue / totalValue * 100 : 0;
|
|
5280
|
-
}
|
|
5281
|
-
return { definition, positions, currentWeights, totalValue };
|
|
5282
|
-
}
|
|
5283
|
-
// -- Auto-Invest --
|
|
5284
|
-
setupAutoInvest(params) {
|
|
5285
|
-
if (params.strategy) this.strategies.get(params.strategy);
|
|
5286
|
-
if (params.asset && !(params.asset in INVESTMENT_ASSETS)) {
|
|
5287
|
-
throw new T2000Error("ASSET_NOT_SUPPORTED", `${params.asset} is not an investment asset`);
|
|
5288
|
-
}
|
|
5289
|
-
return this.autoInvest.setup(params);
|
|
5290
|
-
}
|
|
5291
|
-
getAutoInvestStatus() {
|
|
5292
|
-
return this.autoInvest.getStatus();
|
|
5293
|
-
}
|
|
5294
|
-
async runAutoInvest() {
|
|
5295
|
-
this.enforcer.assertNotLocked();
|
|
5296
|
-
const status = this.autoInvest.getStatus();
|
|
5297
|
-
const executed = [];
|
|
5298
|
-
const skipped = [];
|
|
5299
|
-
for (const schedule of status.pendingRuns) {
|
|
5300
|
-
try {
|
|
5301
|
-
const bal = await queryBalance(this.client, this._address);
|
|
5302
|
-
if (bal.available < schedule.amount) {
|
|
5303
|
-
skipped.push({ scheduleId: schedule.id, reason: `Insufficient balance ($${bal.available.toFixed(2)} < $${schedule.amount})` });
|
|
5304
|
-
continue;
|
|
5305
|
-
}
|
|
5306
|
-
if (schedule.strategy) {
|
|
5307
|
-
const result = await this.investStrategy({ strategy: schedule.strategy, usdAmount: schedule.amount });
|
|
5308
|
-
this.autoInvest.recordRun(schedule.id, schedule.amount);
|
|
5309
|
-
executed.push({ scheduleId: schedule.id, strategy: schedule.strategy, amount: schedule.amount, result });
|
|
5310
|
-
} else if (schedule.asset) {
|
|
5311
|
-
const result = await this.investBuy({ asset: schedule.asset, usdAmount: schedule.amount });
|
|
5312
|
-
this.autoInvest.recordRun(schedule.id, schedule.amount);
|
|
5313
|
-
executed.push({ scheduleId: schedule.id, asset: schedule.asset, amount: schedule.amount, result });
|
|
5314
|
-
}
|
|
5315
|
-
} catch (err) {
|
|
5316
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
5317
|
-
skipped.push({ scheduleId: schedule.id, reason: msg });
|
|
5318
|
-
}
|
|
5319
|
-
}
|
|
5320
|
-
return { executed, skipped };
|
|
5321
|
-
}
|
|
5322
|
-
stopAutoInvest(id) {
|
|
5323
|
-
this.autoInvest.stop(id);
|
|
5324
|
-
}
|
|
5325
|
-
async getPortfolio() {
|
|
5326
|
-
const positions = this.portfolio.getPositions();
|
|
5327
|
-
const realizedPnL = this.portfolio.getRealizedPnL();
|
|
5328
|
-
const prices = {};
|
|
5329
|
-
const swapAdapter = this.registry.listSwap()[0];
|
|
5330
|
-
for (const asset of Object.keys(INVESTMENT_ASSETS)) {
|
|
5331
|
-
try {
|
|
5332
|
-
if (asset === "SUI" && swapAdapter) {
|
|
5333
|
-
prices[asset] = await swapAdapter.getPoolPrice();
|
|
5334
|
-
} else if (swapAdapter) {
|
|
5335
|
-
const quote = await swapAdapter.getQuote("USDC", asset, 1);
|
|
5336
|
-
prices[asset] = quote.expectedOutput > 0 ? 1 / quote.expectedOutput : 0;
|
|
5337
|
-
}
|
|
5338
|
-
} catch {
|
|
5339
|
-
prices[asset] = 0;
|
|
5340
|
-
}
|
|
5341
|
-
}
|
|
5342
|
-
const enrichPosition = async (pos, adjustWallet) => {
|
|
5343
|
-
const currentPrice = prices[pos.asset] ?? 0;
|
|
5344
|
-
let totalAmount = pos.totalAmount;
|
|
5345
|
-
let costBasis = pos.costBasis;
|
|
5346
|
-
if (adjustWallet && pos.asset in INVESTMENT_ASSETS && !pos.earning) {
|
|
5347
|
-
try {
|
|
5348
|
-
const assetInfo = SUPPORTED_ASSETS[pos.asset];
|
|
5349
|
-
const bal = await this.client.getBalance({ owner: this._address, coinType: assetInfo.type });
|
|
5350
|
-
const walletAmount = Number(bal.totalBalance) / 10 ** assetInfo.decimals;
|
|
5351
|
-
const gasReserve = pos.asset === "SUI" ? GAS_RESERVE_MIN : 0;
|
|
5352
|
-
const actualHeld = Math.max(0, walletAmount - gasReserve);
|
|
5353
|
-
if (actualHeld < totalAmount) {
|
|
5354
|
-
const ratio = totalAmount > 0 ? actualHeld / totalAmount : 0;
|
|
5355
|
-
costBasis *= ratio;
|
|
5356
|
-
totalAmount = actualHeld;
|
|
5357
|
-
}
|
|
5358
|
-
} catch {
|
|
5359
|
-
}
|
|
5360
|
-
}
|
|
5361
|
-
const currentValue = totalAmount * currentPrice;
|
|
5362
|
-
const unrealizedPnL = currentPrice > 0 ? currentValue - costBasis : 0;
|
|
5363
|
-
const unrealizedPnLPct = currentPrice > 0 && costBasis > 0 ? unrealizedPnL / costBasis * 100 : 0;
|
|
5364
|
-
return {
|
|
5365
|
-
asset: pos.asset,
|
|
5366
|
-
totalAmount,
|
|
5367
|
-
costBasis,
|
|
5368
|
-
avgPrice: pos.avgPrice,
|
|
5369
|
-
currentPrice,
|
|
5370
|
-
currentValue,
|
|
5371
|
-
unrealizedPnL,
|
|
5372
|
-
unrealizedPnLPct,
|
|
5373
|
-
trades: pos.trades,
|
|
5374
|
-
earning: pos.earning,
|
|
5375
|
-
earningProtocol: pos.earningProtocol,
|
|
5376
|
-
earningApy: pos.earningApy
|
|
5377
|
-
};
|
|
5378
|
-
};
|
|
5379
|
-
const enriched = [];
|
|
5380
|
-
for (const pos of positions) {
|
|
5381
|
-
enriched.push(await enrichPosition(pos, true));
|
|
5382
|
-
}
|
|
5383
|
-
const strategyPositions = {};
|
|
5384
|
-
for (const key of this.portfolio.getAllStrategyKeys()) {
|
|
5385
|
-
const sps = this.portfolio.getStrategyPositions(key);
|
|
5386
|
-
const enrichedStrat = [];
|
|
5387
|
-
for (const sp of sps) {
|
|
5388
|
-
enrichedStrat.push(await enrichPosition(sp, false));
|
|
5389
|
-
}
|
|
5390
|
-
if (enrichedStrat.length > 0) {
|
|
5391
|
-
strategyPositions[key] = enrichedStrat;
|
|
5392
|
-
}
|
|
5393
|
-
}
|
|
5394
|
-
const strategyAmountByAsset = {};
|
|
5395
|
-
for (const strats of Object.values(strategyPositions)) {
|
|
5396
|
-
for (const sp of strats) {
|
|
5397
|
-
const prev = strategyAmountByAsset[sp.asset] ?? { amount: 0, costBasis: 0 };
|
|
5398
|
-
strategyAmountByAsset[sp.asset] = {
|
|
5399
|
-
amount: prev.amount + sp.totalAmount,
|
|
5400
|
-
costBasis: prev.costBasis + sp.costBasis
|
|
5401
|
-
};
|
|
5402
|
-
}
|
|
5403
|
-
}
|
|
5404
|
-
const directOnly = enriched.map((pos) => {
|
|
5405
|
-
const strat = strategyAmountByAsset[pos.asset];
|
|
5406
|
-
if (!strat) return pos;
|
|
5407
|
-
const directAmt = pos.totalAmount - strat.amount;
|
|
5408
|
-
if (directAmt <= 1e-6) return null;
|
|
5409
|
-
const directCost = pos.costBasis - strat.costBasis;
|
|
5410
|
-
const currentValue = directAmt * (pos.currentPrice ?? 0);
|
|
5411
|
-
return {
|
|
5412
|
-
...pos,
|
|
5413
|
-
totalAmount: directAmt,
|
|
5414
|
-
costBasis: directCost,
|
|
5415
|
-
currentValue,
|
|
5416
|
-
unrealizedPnL: currentValue - directCost,
|
|
5417
|
-
unrealizedPnLPct: directCost > 0 ? (currentValue - directCost) / directCost * 100 : 0
|
|
5418
|
-
};
|
|
5419
|
-
}).filter((p) => p !== null);
|
|
5420
|
-
const totalInvested = enriched.reduce((sum, p) => sum + p.costBasis, 0);
|
|
5421
|
-
const totalValue = enriched.reduce((sum, p) => sum + p.currentValue, 0);
|
|
5422
|
-
const totalUnrealizedPnL = totalValue - totalInvested;
|
|
5423
|
-
const totalUnrealizedPnLPct = totalInvested > 0 ? totalUnrealizedPnL / totalInvested * 100 : 0;
|
|
5424
|
-
const result = {
|
|
5425
|
-
positions: directOnly,
|
|
5426
|
-
totalInvested,
|
|
5427
|
-
totalValue,
|
|
5428
|
-
unrealizedPnL: totalUnrealizedPnL,
|
|
5429
|
-
unrealizedPnLPct: totalUnrealizedPnLPct,
|
|
5430
|
-
realizedPnL
|
|
5431
|
-
};
|
|
5432
|
-
if (Object.keys(strategyPositions).length > 0) {
|
|
5433
|
-
result.strategyPositions = strategyPositions;
|
|
5434
|
-
}
|
|
5435
|
-
return result;
|
|
5436
|
-
}
|
|
5437
2943
|
// -- Info --
|
|
5438
2944
|
async positions() {
|
|
5439
2945
|
const allPositions = await this.registry.allPositions(this._address);
|
|
@@ -5476,274 +2982,6 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
5476
2982
|
async allRatesAcrossAssets() {
|
|
5477
2983
|
return this.registry.allRatesAcrossAssets();
|
|
5478
2984
|
}
|
|
5479
|
-
async rebalance(opts = {}) {
|
|
5480
|
-
this.enforcer.assertNotLocked();
|
|
5481
|
-
const dryRun = opts.dryRun ?? false;
|
|
5482
|
-
const minYieldDiff = opts.minYieldDiff ?? 0.5;
|
|
5483
|
-
const maxBreakEven = opts.maxBreakEven ?? 30;
|
|
5484
|
-
const [allPositions, allRates] = await Promise.all([
|
|
5485
|
-
this.registry.allPositions(this._address),
|
|
5486
|
-
this.registry.allRatesAcrossAssets()
|
|
5487
|
-
]);
|
|
5488
|
-
const earningAssets = new Set(
|
|
5489
|
-
this.portfolio.getPositions().filter((p) => p.earning).map((p) => p.asset)
|
|
5490
|
-
);
|
|
5491
|
-
const savePositions = allPositions.flatMap(
|
|
5492
|
-
(p) => p.positions.supplies.filter((s) => s.amount > 0.01).filter((s) => !earningAssets.has(s.asset)).filter((s) => !(s.asset in INVESTMENT_ASSETS)).map((s) => ({
|
|
5493
|
-
protocolId: p.protocolId,
|
|
5494
|
-
protocol: p.protocol,
|
|
5495
|
-
asset: s.asset,
|
|
5496
|
-
amount: s.amount,
|
|
5497
|
-
apy: s.apy
|
|
5498
|
-
}))
|
|
5499
|
-
);
|
|
5500
|
-
if (savePositions.length === 0) {
|
|
5501
|
-
throw new T2000Error("NO_COLLATERAL", "No savings positions to rebalance. Use `t2000 save <amount>` first.");
|
|
5502
|
-
}
|
|
5503
|
-
const borrowPositions = allPositions.flatMap(
|
|
5504
|
-
(p) => p.positions.borrows.filter((b) => b.amount > 0.01)
|
|
5505
|
-
);
|
|
5506
|
-
if (borrowPositions.length > 0) {
|
|
5507
|
-
const healthResults = await Promise.all(
|
|
5508
|
-
allPositions.filter((p) => p.positions.borrows.some((b) => b.amount > 0.01)).map(async (p) => {
|
|
5509
|
-
const adapter = this.registry.getLending(p.protocolId);
|
|
5510
|
-
if (!adapter) return null;
|
|
5511
|
-
return adapter.getHealth(this._address);
|
|
5512
|
-
})
|
|
5513
|
-
);
|
|
5514
|
-
for (const hf of healthResults) {
|
|
5515
|
-
if (hf && hf.healthFactor < 1.5) {
|
|
5516
|
-
throw new T2000Error(
|
|
5517
|
-
"HEALTH_FACTOR_TOO_LOW",
|
|
5518
|
-
`Cannot rebalance \u2014 health factor is ${hf.healthFactor.toFixed(2)} (minimum 1.5). Repay some debt first.`,
|
|
5519
|
-
{ healthFactor: hf.healthFactor }
|
|
5520
|
-
);
|
|
5521
|
-
}
|
|
5522
|
-
}
|
|
5523
|
-
}
|
|
5524
|
-
const stableSet = new Set(STABLE_ASSETS);
|
|
5525
|
-
const stableRates = allRates.filter((r) => stableSet.has(r.asset));
|
|
5526
|
-
if (stableRates.length === 0) {
|
|
5527
|
-
throw new T2000Error("PROTOCOL_UNAVAILABLE", "No stablecoin lending rates available for rebalance");
|
|
5528
|
-
}
|
|
5529
|
-
const bestRate = stableRates.reduce(
|
|
5530
|
-
(best, r) => r.rates.saveApy > best.rates.saveApy ? r : best
|
|
5531
|
-
);
|
|
5532
|
-
const current = savePositions.reduce(
|
|
5533
|
-
(worst, p) => p.apy < worst.apy ? p : worst
|
|
5534
|
-
);
|
|
5535
|
-
const withdrawAdapter = this.registry.getLending(current.protocolId);
|
|
5536
|
-
if (withdrawAdapter) {
|
|
5537
|
-
try {
|
|
5538
|
-
const maxResult = await withdrawAdapter.maxWithdraw(this._address, current.asset);
|
|
5539
|
-
if (maxResult.maxAmount < current.amount) {
|
|
5540
|
-
current.amount = Math.max(0, maxResult.maxAmount - 0.01);
|
|
5541
|
-
}
|
|
5542
|
-
} catch {
|
|
5543
|
-
}
|
|
5544
|
-
}
|
|
5545
|
-
if (current.amount <= 0.01) {
|
|
5546
|
-
throw new T2000Error(
|
|
5547
|
-
"HEALTH_FACTOR_TOO_LOW",
|
|
5548
|
-
"Cannot rebalance \u2014 active borrows prevent safe withdrawal. Repay some debt first."
|
|
5549
|
-
);
|
|
5550
|
-
}
|
|
5551
|
-
const apyDiff = bestRate.rates.saveApy - current.apy;
|
|
5552
|
-
const isSameProtocol = current.protocolId === bestRate.protocolId;
|
|
5553
|
-
const isSameAsset = current.asset === bestRate.asset;
|
|
5554
|
-
if (apyDiff < minYieldDiff) {
|
|
5555
|
-
return {
|
|
5556
|
-
executed: false,
|
|
5557
|
-
steps: [],
|
|
5558
|
-
fromProtocol: current.protocol,
|
|
5559
|
-
fromAsset: current.asset,
|
|
5560
|
-
toProtocol: bestRate.protocol,
|
|
5561
|
-
toAsset: bestRate.asset,
|
|
5562
|
-
amount: current.amount,
|
|
5563
|
-
currentApy: current.apy,
|
|
5564
|
-
newApy: bestRate.rates.saveApy,
|
|
5565
|
-
annualGain: current.amount * apyDiff / 100,
|
|
5566
|
-
estimatedSwapCost: 0,
|
|
5567
|
-
breakEvenDays: Infinity,
|
|
5568
|
-
txDigests: [],
|
|
5569
|
-
totalGasCost: 0
|
|
5570
|
-
};
|
|
5571
|
-
}
|
|
5572
|
-
if (isSameProtocol && isSameAsset) {
|
|
5573
|
-
return {
|
|
5574
|
-
executed: false,
|
|
5575
|
-
steps: [],
|
|
5576
|
-
fromProtocol: current.protocol,
|
|
5577
|
-
fromAsset: current.asset,
|
|
5578
|
-
toProtocol: bestRate.protocol,
|
|
5579
|
-
toAsset: bestRate.asset,
|
|
5580
|
-
amount: current.amount,
|
|
5581
|
-
currentApy: current.apy,
|
|
5582
|
-
newApy: bestRate.rates.saveApy,
|
|
5583
|
-
annualGain: 0,
|
|
5584
|
-
estimatedSwapCost: 0,
|
|
5585
|
-
breakEvenDays: Infinity,
|
|
5586
|
-
txDigests: [],
|
|
5587
|
-
totalGasCost: 0
|
|
5588
|
-
};
|
|
5589
|
-
}
|
|
5590
|
-
const steps = [];
|
|
5591
|
-
let estimatedSwapCost = 0;
|
|
5592
|
-
steps.push({
|
|
5593
|
-
action: "withdraw",
|
|
5594
|
-
protocol: current.protocolId,
|
|
5595
|
-
fromAsset: current.asset,
|
|
5596
|
-
amount: current.amount
|
|
5597
|
-
});
|
|
5598
|
-
let amountToDeposit = current.amount;
|
|
5599
|
-
if (!isSameAsset) {
|
|
5600
|
-
try {
|
|
5601
|
-
const quote = await this.registry.bestSwapQuote(current.asset, bestRate.asset, current.amount);
|
|
5602
|
-
amountToDeposit = quote.quote.expectedOutput;
|
|
5603
|
-
estimatedSwapCost = Math.abs(current.amount - amountToDeposit);
|
|
5604
|
-
} catch {
|
|
5605
|
-
estimatedSwapCost = current.amount * 3e-3;
|
|
5606
|
-
amountToDeposit = current.amount - estimatedSwapCost;
|
|
5607
|
-
}
|
|
5608
|
-
steps.push({
|
|
5609
|
-
action: "swap",
|
|
5610
|
-
fromAsset: current.asset,
|
|
5611
|
-
toAsset: bestRate.asset,
|
|
5612
|
-
amount: current.amount,
|
|
5613
|
-
estimatedOutput: amountToDeposit
|
|
5614
|
-
});
|
|
5615
|
-
}
|
|
5616
|
-
steps.push({
|
|
5617
|
-
action: "deposit",
|
|
5618
|
-
protocol: bestRate.protocolId,
|
|
5619
|
-
toAsset: bestRate.asset,
|
|
5620
|
-
amount: amountToDeposit
|
|
5621
|
-
});
|
|
5622
|
-
const annualGain = amountToDeposit * apyDiff / 100;
|
|
5623
|
-
const breakEvenDays = estimatedSwapCost > 0 ? Math.ceil(estimatedSwapCost / annualGain * 365) : 0;
|
|
5624
|
-
if (breakEvenDays > maxBreakEven && estimatedSwapCost > 0) {
|
|
5625
|
-
return {
|
|
5626
|
-
executed: false,
|
|
5627
|
-
steps,
|
|
5628
|
-
fromProtocol: current.protocol,
|
|
5629
|
-
fromAsset: current.asset,
|
|
5630
|
-
toProtocol: bestRate.protocol,
|
|
5631
|
-
toAsset: bestRate.asset,
|
|
5632
|
-
amount: current.amount,
|
|
5633
|
-
currentApy: current.apy,
|
|
5634
|
-
newApy: bestRate.rates.saveApy,
|
|
5635
|
-
annualGain,
|
|
5636
|
-
estimatedSwapCost,
|
|
5637
|
-
breakEvenDays,
|
|
5638
|
-
txDigests: [],
|
|
5639
|
-
totalGasCost: 0
|
|
5640
|
-
};
|
|
5641
|
-
}
|
|
5642
|
-
if (dryRun) {
|
|
5643
|
-
return {
|
|
5644
|
-
executed: false,
|
|
5645
|
-
steps,
|
|
5646
|
-
fromProtocol: current.protocol,
|
|
5647
|
-
fromAsset: current.asset,
|
|
5648
|
-
toProtocol: bestRate.protocol,
|
|
5649
|
-
toAsset: bestRate.asset,
|
|
5650
|
-
amount: current.amount,
|
|
5651
|
-
currentApy: current.apy,
|
|
5652
|
-
newApy: bestRate.rates.saveApy,
|
|
5653
|
-
annualGain,
|
|
5654
|
-
estimatedSwapCost,
|
|
5655
|
-
breakEvenDays,
|
|
5656
|
-
txDigests: [],
|
|
5657
|
-
totalGasCost: 0
|
|
5658
|
-
};
|
|
5659
|
-
}
|
|
5660
|
-
if (!withdrawAdapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", `Protocol ${current.protocolId} not found`);
|
|
5661
|
-
const depositAdapter = this.registry.getLending(bestRate.protocolId);
|
|
5662
|
-
if (!depositAdapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", `Protocol ${bestRate.protocolId} not found`);
|
|
5663
|
-
const canComposePTB = withdrawAdapter.addWithdrawToTx && depositAdapter.addSaveToTx && (isSameAsset || this.registry.listSwap()[0]?.addSwapToTx);
|
|
5664
|
-
let txDigests;
|
|
5665
|
-
let totalGasCost;
|
|
5666
|
-
if (canComposePTB) {
|
|
5667
|
-
const result = await executeWithGas(this.client, this._signer, async () => {
|
|
5668
|
-
const tx = new Transaction();
|
|
5669
|
-
tx.setSender(this._address);
|
|
5670
|
-
const { coin: withdrawnCoin, effectiveAmount } = await withdrawAdapter.addWithdrawToTx(
|
|
5671
|
-
tx,
|
|
5672
|
-
this._address,
|
|
5673
|
-
current.amount,
|
|
5674
|
-
current.asset
|
|
5675
|
-
);
|
|
5676
|
-
amountToDeposit = effectiveAmount;
|
|
5677
|
-
let depositCoin = withdrawnCoin;
|
|
5678
|
-
if (!isSameAsset) {
|
|
5679
|
-
const swapAdapter = this.registry.listSwap()[0];
|
|
5680
|
-
const { outputCoin, estimatedOut, toDecimals } = await swapAdapter.addSwapToTx(
|
|
5681
|
-
tx,
|
|
5682
|
-
this._address,
|
|
5683
|
-
withdrawnCoin,
|
|
5684
|
-
current.asset,
|
|
5685
|
-
bestRate.asset,
|
|
5686
|
-
amountToDeposit
|
|
5687
|
-
);
|
|
5688
|
-
depositCoin = outputCoin;
|
|
5689
|
-
amountToDeposit = estimatedOut / 10 ** toDecimals;
|
|
5690
|
-
}
|
|
5691
|
-
await depositAdapter.addSaveToTx(
|
|
5692
|
-
tx,
|
|
5693
|
-
this._address,
|
|
5694
|
-
depositCoin,
|
|
5695
|
-
bestRate.asset,
|
|
5696
|
-
{ collectFee: bestRate.asset === "USDC" }
|
|
5697
|
-
);
|
|
5698
|
-
return tx;
|
|
5699
|
-
});
|
|
5700
|
-
txDigests = [result.digest];
|
|
5701
|
-
totalGasCost = result.gasCostSui;
|
|
5702
|
-
} else {
|
|
5703
|
-
txDigests = [];
|
|
5704
|
-
totalGasCost = 0;
|
|
5705
|
-
const withdrawResult = await executeWithGas(this.client, this._signer, async () => {
|
|
5706
|
-
const built = await withdrawAdapter.buildWithdrawTx(this._address, current.amount, current.asset);
|
|
5707
|
-
amountToDeposit = built.effectiveAmount;
|
|
5708
|
-
return built.tx;
|
|
5709
|
-
});
|
|
5710
|
-
txDigests.push(withdrawResult.digest);
|
|
5711
|
-
totalGasCost += withdrawResult.gasCostSui;
|
|
5712
|
-
if (!isSameAsset) {
|
|
5713
|
-
const swapAdapter = this.registry.listSwap()[0];
|
|
5714
|
-
if (!swapAdapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", "No swap adapter available");
|
|
5715
|
-
const swapResult = await executeWithGas(this.client, this._signer, async () => {
|
|
5716
|
-
const built = await swapAdapter.buildSwapTx(this._address, current.asset, bestRate.asset, amountToDeposit);
|
|
5717
|
-
amountToDeposit = built.estimatedOut / 10 ** built.toDecimals;
|
|
5718
|
-
return built.tx;
|
|
5719
|
-
});
|
|
5720
|
-
txDigests.push(swapResult.digest);
|
|
5721
|
-
totalGasCost += swapResult.gasCostSui;
|
|
5722
|
-
}
|
|
5723
|
-
const depositResult = await executeWithGas(this.client, this._signer, async () => {
|
|
5724
|
-
const { tx } = await depositAdapter.buildSaveTx(this._address, amountToDeposit, bestRate.asset, { collectFee: bestRate.asset === "USDC" });
|
|
5725
|
-
return tx;
|
|
5726
|
-
});
|
|
5727
|
-
txDigests.push(depositResult.digest);
|
|
5728
|
-
totalGasCost += depositResult.gasCostSui;
|
|
5729
|
-
}
|
|
5730
|
-
return {
|
|
5731
|
-
executed: true,
|
|
5732
|
-
steps,
|
|
5733
|
-
fromProtocol: current.protocol,
|
|
5734
|
-
fromAsset: current.asset,
|
|
5735
|
-
toProtocol: bestRate.protocol,
|
|
5736
|
-
toAsset: bestRate.asset,
|
|
5737
|
-
amount: current.amount,
|
|
5738
|
-
currentApy: current.apy,
|
|
5739
|
-
newApy: bestRate.rates.saveApy,
|
|
5740
|
-
annualGain,
|
|
5741
|
-
estimatedSwapCost,
|
|
5742
|
-
breakEvenDays,
|
|
5743
|
-
txDigests,
|
|
5744
|
-
totalGasCost
|
|
5745
|
-
};
|
|
5746
|
-
}
|
|
5747
2985
|
async earnings() {
|
|
5748
2986
|
const result = await getEarnings(this.client, this._address);
|
|
5749
2987
|
if (result.totalYieldEarned > 0) {
|
|
@@ -5760,17 +2998,6 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
5760
2998
|
return getFundStatus(this.client, this._address);
|
|
5761
2999
|
}
|
|
5762
3000
|
// -- Helpers --
|
|
5763
|
-
async getFreeBalance(asset) {
|
|
5764
|
-
if (!(asset in INVESTMENT_ASSETS)) return Infinity;
|
|
5765
|
-
const pos = this.portfolio.getPosition(asset);
|
|
5766
|
-
const walletInvested = pos && pos.totalAmount > 0 && !pos.earning ? pos.totalAmount : 0;
|
|
5767
|
-
if (walletInvested <= 0) return Infinity;
|
|
5768
|
-
const assetInfo = SUPPORTED_ASSETS[asset];
|
|
5769
|
-
const balance = await this.client.getBalance({ owner: this._address, coinType: assetInfo.type });
|
|
5770
|
-
const walletAmount = Number(balance.totalBalance) / 10 ** assetInfo.decimals;
|
|
5771
|
-
const gasReserve = asset === "SUI" ? GAS_RESERVE_MIN : 0;
|
|
5772
|
-
return Math.max(0, walletAmount - walletInvested - gasReserve);
|
|
5773
|
-
}
|
|
5774
3001
|
async resolveLending(protocol, asset, capability) {
|
|
5775
3002
|
if (protocol) {
|
|
5776
3003
|
const adapter = this.registry.getLending(protocol);
|
|
@@ -5865,7 +3092,12 @@ async function callUsdcSponsorApi(address) {
|
|
|
5865
3092
|
}
|
|
5866
3093
|
}
|
|
5867
3094
|
|
|
3095
|
+
// src/index.ts
|
|
3096
|
+
init_errors();
|
|
3097
|
+
|
|
5868
3098
|
// src/utils/simulate.ts
|
|
3099
|
+
init_errors();
|
|
3100
|
+
init_errors();
|
|
5869
3101
|
async function simulateTransaction(client, tx, sender) {
|
|
5870
3102
|
tx.setSender(sender);
|
|
5871
3103
|
try {
|
|
@@ -5935,6 +3167,6 @@ function parseMoveAbort(errorStr) {
|
|
|
5935
3167
|
return { reason: errorStr };
|
|
5936
3168
|
}
|
|
5937
3169
|
|
|
5938
|
-
export {
|
|
3170
|
+
export { BPS_DENOMINATOR, CETUS_USDC_SUI_POOL, CLOCK_ID, ContactManager, DEFAULT_NETWORK, DEFAULT_SAFEGUARD_CONFIG, GAS_RESERVE_MIN, KeypairSigner, MIST_PER_SUI, NaviAdapter, OUTBOUND_OPS, ProtocolRegistry, STABLE_ASSETS, SUI_DECIMALS, SUPPORTED_ASSETS, SafeguardEnforcer, SafeguardError, T2000, T2000Error, USDC_DECIMALS, ZkLoginSigner, addCollectFeeToTx, allDescriptors, calculateFee, executeAutoTopUp, executeWithGas, exportPrivateKey, formatAssetAmount, formatSui, formatUsd, generateKeypair, getAddress, getDecimals, getGasStatus, getRates, keypairFromPrivateKey, loadKey, mapMoveAbortCode, mapWalletError, mistToSui, naviDescriptor, rawToStable, rawToUsdc, saveKey, shouldAutoTopUp, simulateTransaction, solveHashcash, stableToRaw, suiToMist, throwIfSimulationFailed, truncateAddress, usdcToRaw, validateAddress, walletExists };
|
|
5939
3171
|
//# sourceMappingURL=index.js.map
|
|
5940
3172
|
//# sourceMappingURL=index.js.map
|