@t2000/sdk 0.14.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs DELETED
@@ -1,4463 +0,0 @@
1
- 'use strict';
2
-
3
- var eventemitter3 = require('eventemitter3');
4
- var transactions = require('@mysten/sui/transactions');
5
- var jsonRpc = require('@mysten/sui/jsonRpc');
6
- var utils = require('@mysten/sui/utils');
7
- var ed25519 = require('@mysten/sui/keypairs/ed25519');
8
- var cryptography = require('@mysten/sui/cryptography');
9
- var crypto = require('crypto');
10
- var promises = require('fs/promises');
11
- var path = require('path');
12
- var os = require('os');
13
- var bcs = require('@mysten/sui/bcs');
14
- var aggregatorSdk = require('@cetusprotocol/aggregator-sdk');
15
- var fs = require('fs');
16
-
17
- // src/t2000.ts
18
-
19
- // src/constants.ts
20
- var MIST_PER_SUI = 1000000000n;
21
- var SUI_DECIMALS = 9;
22
- var USDC_DECIMALS = 6;
23
- var BPS_DENOMINATOR = 10000n;
24
- var AUTO_TOPUP_THRESHOLD = 50000000n;
25
- var AUTO_TOPUP_AMOUNT = 1000000n;
26
- var AUTO_TOPUP_MIN_USDC = 2000000n;
27
- var SAVE_FEE_BPS = 10n;
28
- var SWAP_FEE_BPS = 0n;
29
- var BORROW_FEE_BPS = 5n;
30
- var CLOCK_ID = "0x6";
31
- var SUPPORTED_ASSETS = {
32
- USDC: {
33
- type: "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC",
34
- decimals: 6,
35
- symbol: "USDC",
36
- displayName: "USDC"
37
- },
38
- USDT: {
39
- type: "0x375f70cf2ae4c00bf37117d0c85a2c71545e6ee05c4a5c7d282cd66a4504b068::usdt::USDT",
40
- decimals: 6,
41
- symbol: "USDT",
42
- displayName: "suiUSDT"
43
- },
44
- USDe: {
45
- type: "0x41d587e5336f1c86cad50d38a7136db99333bb9bda91cea4ba69115defeb1402::sui_usde::SUI_USDE",
46
- decimals: 6,
47
- symbol: "USDe",
48
- displayName: "suiUSDe"
49
- },
50
- USDsui: {
51
- type: "0x44f838219cf67b058f3b37907b655f226153c18e33dfcd0da559a844fea9b1c1::usdsui::USDSUI",
52
- decimals: 6,
53
- symbol: "USDsui",
54
- displayName: "USDsui"
55
- },
56
- SUI: {
57
- type: "0x2::sui::SUI",
58
- decimals: 9,
59
- symbol: "SUI",
60
- displayName: "SUI"
61
- },
62
- BTC: {
63
- type: "0xaafb102dd0902f5055cadecd687fb5b71ca82ef0e0285d90afde828ec58ca96b::btc::BTC",
64
- decimals: 8,
65
- symbol: "BTC",
66
- displayName: "Bitcoin"
67
- },
68
- ETH: {
69
- type: "0xd0e89b2af5e4910726fbcd8b8dd37bb79b29e5f83f7491bca830e94f7f226d29::eth::ETH",
70
- decimals: 8,
71
- symbol: "ETH",
72
- displayName: "Ethereum"
73
- }
74
- };
75
- var STABLE_ASSETS = ["USDC", "USDT", "USDe", "USDsui"];
76
- var T2000_PACKAGE_ID = process.env.T2000_PACKAGE_ID ?? "0xab92e9f1fe549ad3d6a52924a73181b45791e76120b975138fac9ec9b75db9f3";
77
- var T2000_CONFIG_ID = process.env.T2000_CONFIG_ID ?? "0x408add9aa9322f93cfd87523d8f603006eb8713894f4c460283c58a6888dae8a";
78
- var T2000_TREASURY_ID = process.env.T2000_TREASURY_ID ?? "0x3bb501b8300125dca59019247941a42af6b292a150ce3cfcce9449456be2ec91";
79
- var DEFAULT_NETWORK = "mainnet";
80
- var DEFAULT_RPC_URL = "https://fullnode.mainnet.sui.io:443";
81
- var DEFAULT_KEY_PATH = "~/.t2000/wallet.key";
82
- var API_BASE_URL = process.env.T2000_API_URL ?? "https://api.t2000.ai";
83
- var CETUS_USDC_SUI_POOL = "0x51e883ba7c0b566a26cbc8a94cd33eb0abd418a77cc1e60ad22fd9b1f29cd2ab";
84
- var CETUS_PACKAGE = "0x1eabed72c53feb3805120a081dc15963c204dc8d091542592abaf7a35689b2fb";
85
- var INVESTMENT_ASSETS = {
86
- SUI: SUPPORTED_ASSETS.SUI,
87
- BTC: SUPPORTED_ASSETS.BTC,
88
- ETH: SUPPORTED_ASSETS.ETH
89
- };
90
- var PERPS_MARKETS = ["SUI-PERP"];
91
- var DEFAULT_MAX_LEVERAGE = 5;
92
- var DEFAULT_MAX_POSITION_SIZE = 1e3;
93
- var GAS_RESERVE_MIN = 0.05;
94
- var SENTINEL = {
95
- PACKAGE: "0x88b83f36dafcd5f6dcdcf1d2cb5889b03f61264ab3cee9cae35db7aa940a21b7",
96
- AGENT_REGISTRY: "0xc47564f5f14c12b31e0dfa1a3dc99a6380a1edf8929c28cb0eaa3359c8db36ac",
97
- ENCLAVE: "0xfb1261aeb9583514cb1341a548a5ec12d1231bd96af22215f1792617a93e1213",
98
- PROTOCOL_CONFIG: "0x2fa4fa4a1dd0498612304635ff9334e1b922e78af325000e9d9c0e88adea459f",
99
- TEE_API: "https://app.suisentinel.xyz/api/consume-prompt",
100
- SENTINELS_API: "https://api.suisentinel.xyz/agents/mainnet",
101
- RANDOM: "0x8",
102
- MIN_FEE_MIST: 100000000n,
103
- // 0.1 SUI
104
- MAX_PROMPT_TOKENS: 600
105
- };
106
-
107
- // src/errors.ts
108
- var T2000Error = class extends Error {
109
- code;
110
- data;
111
- retryable;
112
- constructor(code, message, data, retryable = false) {
113
- super(message);
114
- this.name = "T2000Error";
115
- this.code = code;
116
- this.data = data;
117
- this.retryable = retryable;
118
- }
119
- toJSON() {
120
- return {
121
- error: this.code,
122
- message: this.message,
123
- ...this.data && { data: this.data },
124
- retryable: this.retryable
125
- };
126
- }
127
- };
128
- function mapWalletError(error) {
129
- const msg = error instanceof Error ? error.message : String(error);
130
- if (msg.includes("rejected") || msg.includes("cancelled")) {
131
- return new T2000Error("TRANSACTION_FAILED", "Transaction cancelled");
132
- }
133
- if (msg.includes("Insufficient") || msg.includes("insufficient")) {
134
- return new T2000Error("INSUFFICIENT_BALANCE", "Insufficient balance");
135
- }
136
- return new T2000Error("UNKNOWN", msg, void 0, true);
137
- }
138
- function mapMoveAbortCode(code) {
139
- const abortMessages = {
140
- 1: "Protocol is temporarily paused",
141
- 2: "Amount must be greater than zero",
142
- 3: "Invalid operation type",
143
- 4: "Fee rate exceeds maximum",
144
- 5: "Insufficient treasury balance",
145
- 6: "Not authorized",
146
- 7: "Package version mismatch \u2014 upgrade required",
147
- 8: "Timelock is active \u2014 wait for expiry",
148
- 9: "No pending change to execute",
149
- 10: "Already at current version",
150
- // NAVI Protocol abort codes
151
- 1502: "Oracle price is stale \u2014 try again in a moment",
152
- 1503: 'Withdrawal amount is invalid (zero or dust) \u2014 try a specific amount instead of "all"',
153
- 1600: "Health factor too low \u2014 withdrawal would risk liquidation",
154
- 1605: "Asset borrowing is disabled or at capacity on this protocol",
155
- // Cetus DEX abort codes
156
- 46001: "Swap failed \u2014 the DEX pool rejected the trade (liquidity or routing issue). Try again."
157
- };
158
- return abortMessages[code] ?? `Move abort code: ${code}`;
159
- }
160
- function isMoveAbort(msg) {
161
- return msg.includes("MoveAbort") || msg.includes("MovePrimitiveRuntimeError");
162
- }
163
- function parseMoveAbortMessage(msg) {
164
- const abortMatch = msg.match(/abort code:\s*(\d+)/i) ?? msg.match(/MoveAbort[^,]*,\s*(\d+)/);
165
- if (abortMatch) {
166
- const code = parseInt(abortMatch[1], 10);
167
- const mapped = mapMoveAbortCode(code);
168
- if (mapped.startsWith("Move abort code:")) {
169
- const moduleMatch = msg.match(/in '([^']+)'/);
170
- if (moduleMatch) return `${mapped} (in ${moduleMatch[1]})`;
171
- }
172
- return mapped;
173
- }
174
- return msg;
175
- }
176
-
177
- // src/utils/sui.ts
178
- var cachedClient = null;
179
- function getSuiClient(rpcUrl) {
180
- const url = rpcUrl ?? DEFAULT_RPC_URL;
181
- if (cachedClient) return cachedClient;
182
- cachedClient = new jsonRpc.SuiJsonRpcClient({ url, network: "mainnet" });
183
- return cachedClient;
184
- }
185
- function validateAddress(address) {
186
- const normalized = utils.normalizeSuiAddress(address);
187
- if (!utils.isValidSuiAddress(normalized)) {
188
- throw new T2000Error("INVALID_ADDRESS", `Invalid Sui address: ${address}`);
189
- }
190
- return normalized;
191
- }
192
- function truncateAddress(address) {
193
- if (address.length <= 10) return address;
194
- return `${address.slice(0, 6)}...${address.slice(-4)}`;
195
- }
196
- var ALGORITHM = "aes-256-gcm";
197
- var SCRYPT_N = 2 ** 14;
198
- var SCRYPT_R = 8;
199
- var SCRYPT_P = 1;
200
- var SALT_LENGTH = 32;
201
- var IV_LENGTH = 16;
202
- function expandPath(p) {
203
- if (p.startsWith("~")) return path.resolve(os.homedir(), p.slice(2));
204
- return path.resolve(p);
205
- }
206
- function deriveKey(passphrase, salt) {
207
- return crypto.scryptSync(passphrase, salt, 32, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P });
208
- }
209
- function encrypt(data, passphrase) {
210
- const salt = crypto.randomBytes(SALT_LENGTH);
211
- const key = deriveKey(passphrase, salt);
212
- const iv = crypto.randomBytes(IV_LENGTH);
213
- const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
214
- const ciphertext = Buffer.concat([cipher.update(data), cipher.final()]);
215
- const tag = cipher.getAuthTag();
216
- return {
217
- version: 1,
218
- algorithm: ALGORITHM,
219
- salt: salt.toString("hex"),
220
- iv: iv.toString("hex"),
221
- tag: tag.toString("hex"),
222
- ciphertext: ciphertext.toString("hex")
223
- };
224
- }
225
- function decrypt(encrypted, passphrase) {
226
- const salt = Buffer.from(encrypted.salt, "hex");
227
- const key = deriveKey(passphrase, salt);
228
- const iv = Buffer.from(encrypted.iv, "hex");
229
- const tag = Buffer.from(encrypted.tag, "hex");
230
- const ciphertext = Buffer.from(encrypted.ciphertext, "hex");
231
- const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
232
- decipher.setAuthTag(tag);
233
- try {
234
- return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
235
- } catch {
236
- throw new T2000Error("WALLET_LOCKED", "Invalid PIN");
237
- }
238
- }
239
- function generateKeypair() {
240
- return ed25519.Ed25519Keypair.generate();
241
- }
242
- function keypairFromPrivateKey(privateKey) {
243
- if (privateKey.startsWith("suiprivkey")) {
244
- const decoded = cryptography.decodeSuiPrivateKey(privateKey);
245
- return ed25519.Ed25519Keypair.fromSecretKey(decoded.secretKey);
246
- }
247
- const bytes = Buffer.from(privateKey.replace(/^0x/, ""), "hex");
248
- return ed25519.Ed25519Keypair.fromSecretKey(bytes);
249
- }
250
- async function saveKey(keypair, passphrase, keyPath) {
251
- const filePath = expandPath(keyPath ?? DEFAULT_KEY_PATH);
252
- try {
253
- await promises.access(filePath);
254
- throw new T2000Error("WALLET_EXISTS", `Wallet already exists at ${filePath}`);
255
- } catch (error) {
256
- if (error instanceof T2000Error) throw error;
257
- }
258
- await promises.mkdir(path.dirname(filePath), { recursive: true });
259
- const bech32Key = keypair.getSecretKey();
260
- const encrypted = encrypt(Buffer.from(bech32Key, "utf-8"), passphrase);
261
- await promises.writeFile(filePath, JSON.stringify(encrypted, null, 2), { mode: 384 });
262
- return filePath;
263
- }
264
- async function loadKey(passphrase, keyPath) {
265
- const filePath = expandPath(keyPath ?? DEFAULT_KEY_PATH);
266
- let content;
267
- try {
268
- content = await promises.readFile(filePath, "utf-8");
269
- } catch {
270
- throw new T2000Error("WALLET_NOT_FOUND", `No wallet found at ${filePath}`);
271
- }
272
- const encrypted = JSON.parse(content);
273
- const decrypted = decrypt(encrypted, passphrase);
274
- const bech32Key = decrypted.toString("utf-8");
275
- const decoded = cryptography.decodeSuiPrivateKey(bech32Key);
276
- return ed25519.Ed25519Keypair.fromSecretKey(decoded.secretKey);
277
- }
278
- async function walletExists(keyPath) {
279
- const filePath = expandPath(keyPath ?? DEFAULT_KEY_PATH);
280
- try {
281
- await promises.access(filePath);
282
- return true;
283
- } catch {
284
- return false;
285
- }
286
- }
287
- function exportPrivateKey(keypair) {
288
- return keypair.getSecretKey();
289
- }
290
- function getAddress(keypair) {
291
- return keypair.getPublicKey().toSuiAddress();
292
- }
293
-
294
- // src/utils/format.ts
295
- function mistToSui(mist) {
296
- return Number(mist) / Number(MIST_PER_SUI);
297
- }
298
- function suiToMist(sui) {
299
- return BigInt(Math.round(sui * Number(MIST_PER_SUI)));
300
- }
301
- function usdcToRaw(amount) {
302
- return BigInt(Math.round(amount * 10 ** USDC_DECIMALS));
303
- }
304
- function rawToUsdc(raw) {
305
- return Number(raw) / 10 ** USDC_DECIMALS;
306
- }
307
- function stableToRaw(amount, decimals) {
308
- return BigInt(Math.round(amount * 10 ** decimals));
309
- }
310
- function rawToStable(raw, decimals) {
311
- return Number(raw) / 10 ** decimals;
312
- }
313
- function getDecimals(asset) {
314
- return SUPPORTED_ASSETS[asset].decimals;
315
- }
316
- function displayToRaw(amount, decimals) {
317
- return BigInt(Math.round(amount * 10 ** decimals));
318
- }
319
- function formatUsd(amount) {
320
- return `$${amount.toFixed(2)}`;
321
- }
322
- function formatSui(amount) {
323
- if (amount < 1e-3) return `${amount.toFixed(6)} SUI`;
324
- return `${amount.toFixed(3)} SUI`;
325
- }
326
- function formatAssetAmount(amount, asset) {
327
- if (asset === "BTC") return amount.toFixed(8);
328
- if (asset === "ETH") return amount.toFixed(6);
329
- return amount.toFixed(4);
330
- }
331
- var ASSET_LOOKUP = /* @__PURE__ */ new Map();
332
- for (const [key, info] of Object.entries(SUPPORTED_ASSETS)) {
333
- ASSET_LOOKUP.set(key.toUpperCase(), key);
334
- if (info.displayName && info.displayName.toUpperCase() !== key.toUpperCase()) {
335
- ASSET_LOOKUP.set(info.displayName.toUpperCase(), key);
336
- }
337
- }
338
- function normalizeAsset(input) {
339
- return ASSET_LOOKUP.get(input.toUpperCase()) ?? input;
340
- }
341
-
342
- // src/wallet/send.ts
343
- async function buildSendTx({
344
- client,
345
- address,
346
- to,
347
- amount,
348
- asset = "USDC"
349
- }) {
350
- const recipient = validateAddress(to);
351
- const assetInfo = SUPPORTED_ASSETS[asset];
352
- if (!assetInfo) throw new T2000Error("ASSET_NOT_SUPPORTED", `Asset ${asset} is not supported`);
353
- if (amount <= 0) throw new T2000Error("INVALID_AMOUNT", "Amount must be greater than zero");
354
- const rawAmount = displayToRaw(amount, assetInfo.decimals);
355
- const tx = new transactions.Transaction();
356
- tx.setSender(address);
357
- if (asset === "SUI") {
358
- const [coin] = tx.splitCoins(tx.gas, [rawAmount]);
359
- tx.transferObjects([coin], recipient);
360
- } else {
361
- const coins = await client.getCoins({ owner: address, coinType: assetInfo.type });
362
- if (coins.data.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${asset} coins found`);
363
- const totalBalance = coins.data.reduce((sum, c) => sum + BigInt(c.balance), 0n);
364
- if (totalBalance < rawAmount) {
365
- throw new T2000Error("INSUFFICIENT_BALANCE", `Insufficient ${asset} balance`, {
366
- available: Number(totalBalance) / 10 ** assetInfo.decimals,
367
- required: amount
368
- });
369
- }
370
- const primaryCoin = tx.object(coins.data[0].coinObjectId);
371
- if (coins.data.length > 1) {
372
- tx.mergeCoins(primaryCoin, coins.data.slice(1).map((c) => tx.object(c.coinObjectId)));
373
- }
374
- const [sendCoin] = tx.splitCoins(primaryCoin, [rawAmount]);
375
- tx.transferObjects([sendCoin], recipient);
376
- }
377
- return tx;
378
- }
379
-
380
- // src/wallet/balance.ts
381
- var _cachedSuiPrice = 0;
382
- var _priceLastFetched = 0;
383
- var PRICE_CACHE_TTL_MS = 6e4;
384
- async function fetchSuiPrice(client) {
385
- const now = Date.now();
386
- if (_cachedSuiPrice > 0 && now - _priceLastFetched < PRICE_CACHE_TTL_MS) {
387
- return _cachedSuiPrice;
388
- }
389
- try {
390
- const pool = await client.getObject({
391
- id: CETUS_USDC_SUI_POOL,
392
- options: { showContent: true }
393
- });
394
- if (pool.data?.content?.dataType === "moveObject") {
395
- const fields = pool.data.content.fields;
396
- const currentSqrtPrice = BigInt(String(fields.current_sqrt_price ?? "0"));
397
- if (currentSqrtPrice > 0n) {
398
- const Q64 = 2n ** 64n;
399
- const sqrtPriceFloat = Number(currentSqrtPrice) / Number(Q64);
400
- const rawPrice = sqrtPriceFloat * sqrtPriceFloat;
401
- const price = 1e3 / rawPrice;
402
- if (price > 0.01 && price < 1e3) {
403
- _cachedSuiPrice = price;
404
- _priceLastFetched = now;
405
- }
406
- }
407
- }
408
- } catch {
409
- }
410
- return _cachedSuiPrice;
411
- }
412
- async function queryBalance(client, address) {
413
- const stableBalancePromises = STABLE_ASSETS.map(
414
- (asset) => client.getBalance({ owner: address, coinType: SUPPORTED_ASSETS[asset].type }).then((b) => ({ asset, amount: Number(b.totalBalance) / 10 ** SUPPORTED_ASSETS[asset].decimals }))
415
- );
416
- const [suiBalance, suiPriceUsd, ...stableResults] = await Promise.all([
417
- client.getBalance({ owner: address, coinType: SUPPORTED_ASSETS.SUI.type }),
418
- fetchSuiPrice(client),
419
- ...stableBalancePromises
420
- ]);
421
- const stables = {};
422
- let totalStables = 0;
423
- for (const { asset, amount } of stableResults) {
424
- stables[asset] = amount;
425
- totalStables += amount;
426
- }
427
- const suiAmount = Number(suiBalance.totalBalance) / Number(MIST_PER_SUI);
428
- const savings = 0;
429
- const usdEquiv = suiAmount * suiPriceUsd;
430
- const total = totalStables + savings + usdEquiv;
431
- return {
432
- available: totalStables,
433
- savings,
434
- debt: 0,
435
- investment: 0,
436
- investmentPnL: 0,
437
- gasReserve: {
438
- sui: suiAmount,
439
- usdEquiv
440
- },
441
- total,
442
- stables,
443
- assets: {
444
- USDC: stables.USDC ?? 0,
445
- SUI: suiAmount
446
- }
447
- };
448
- }
449
-
450
- // src/wallet/history.ts
451
- async function queryHistory(client, address, limit = 20) {
452
- const txns = await client.queryTransactionBlocks({
453
- filter: { FromAddress: address },
454
- options: { showEffects: true, showInput: true },
455
- limit,
456
- order: "descending"
457
- });
458
- return txns.data.map((tx) => {
459
- const gasUsed = tx.effects?.gasUsed;
460
- const gasCost = gasUsed ? (Number(gasUsed.computationCost) + Number(gasUsed.storageCost) - Number(gasUsed.storageRebate)) / 1e9 : void 0;
461
- return {
462
- digest: tx.digest,
463
- action: inferAction(tx.transaction),
464
- timestamp: Number(tx.timestampMs ?? 0),
465
- gasCost
466
- };
467
- });
468
- }
469
- function inferAction(txBlock) {
470
- if (!txBlock || typeof txBlock !== "object") return "unknown";
471
- const data = "data" in txBlock ? txBlock.data : void 0;
472
- if (!data || typeof data !== "object") return "unknown";
473
- const inner = "transaction" in data ? data.transaction : void 0;
474
- if (!inner || typeof inner !== "object") return "unknown";
475
- const kind = "kind" in inner ? inner.kind : void 0;
476
- if (kind === "ProgrammableTransaction") return "transaction";
477
- return kind ?? "unknown";
478
- }
479
-
480
- // src/protocols/protocolFee.ts
481
- var FEE_RATES = {
482
- save: SAVE_FEE_BPS,
483
- swap: SWAP_FEE_BPS,
484
- borrow: BORROW_FEE_BPS
485
- };
486
- var OP_CODES = {
487
- save: 0,
488
- swap: 1,
489
- borrow: 2
490
- };
491
- function calculateFee(operation, amount) {
492
- const bps = FEE_RATES[operation];
493
- const feeAmount = amount * Number(bps) / Number(BPS_DENOMINATOR);
494
- const rawAmount = usdcToRaw(feeAmount);
495
- return {
496
- amount: feeAmount,
497
- asset: "USDC",
498
- rate: Number(bps) / Number(BPS_DENOMINATOR),
499
- rawAmount
500
- };
501
- }
502
- function addCollectFeeToTx(tx, paymentCoin, operation) {
503
- const bps = FEE_RATES[operation];
504
- if (bps <= 0n) return;
505
- tx.moveCall({
506
- target: `${T2000_PACKAGE_ID}::treasury::collect_fee`,
507
- typeArguments: [SUPPORTED_ASSETS.USDC.type],
508
- arguments: [
509
- tx.object(T2000_TREASURY_ID),
510
- tx.object(T2000_CONFIG_ID),
511
- paymentCoin,
512
- tx.pure.u8(OP_CODES[operation])
513
- ]
514
- });
515
- }
516
- async function reportFee(agentAddress, operation, feeAmount, feeRate, txDigest) {
517
- try {
518
- await fetch(`${API_BASE_URL}/api/fees`, {
519
- method: "POST",
520
- headers: { "Content-Type": "application/json" },
521
- body: JSON.stringify({
522
- agentAddress,
523
- operation,
524
- feeAmount: feeAmount.toString(),
525
- feeRate: feeRate.toString(),
526
- txDigest
527
- })
528
- });
529
- } catch {
530
- }
531
- }
532
- SUPPORTED_ASSETS.USDC.type;
533
- var RATE_DECIMALS = 27;
534
- var LTV_DECIMALS = 27;
535
- var MIN_HEALTH_FACTOR = 1.5;
536
- var WITHDRAW_DUST_BUFFER = 1e-3;
537
- var CLOCK = "0x06";
538
- var SUI_SYSTEM_STATE = "0x05";
539
- var NAVI_BALANCE_DECIMALS = 9;
540
- var CONFIG_API = "https://open-api.naviprotocol.io/api/navi/config?env=prod";
541
- var POOLS_API = "https://open-api.naviprotocol.io/api/navi/pools?env=prod";
542
- var PACKAGE_API = "https://open-api.naviprotocol.io/api/package";
543
- var packageCache = null;
544
- function toBigInt(v) {
545
- if (typeof v === "bigint") return v;
546
- return BigInt(String(v));
547
- }
548
- var UserStateInfo = bcs.bcs.struct("UserStateInfo", {
549
- asset_id: bcs.bcs.u8(),
550
- borrow_balance: bcs.bcs.u256(),
551
- supply_balance: bcs.bcs.u256()
552
- });
553
- function decodeDevInspect(result, schema) {
554
- const rv = result.results?.[0]?.returnValues?.[0];
555
- if (result.error || !rv) return void 0;
556
- const bytes = Uint8Array.from(rv[0]);
557
- return schema.parse(bytes);
558
- }
559
- var configCache = null;
560
- var poolsCache = null;
561
- var CACHE_TTL = 5 * 6e4;
562
- async function fetchJson(url) {
563
- const res = await fetch(url);
564
- if (!res.ok) throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI API error: ${res.status}`);
565
- const json = await res.json();
566
- return json.data ?? json;
567
- }
568
- async function getLatestPackageId() {
569
- if (packageCache && Date.now() - packageCache.ts < CACHE_TTL) return packageCache.id;
570
- const res = await fetch(PACKAGE_API);
571
- if (!res.ok) throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI package API error: ${res.status}`);
572
- const json = await res.json();
573
- if (!json.packageId) throw new T2000Error("PROTOCOL_UNAVAILABLE", "NAVI package API returned no packageId");
574
- packageCache = { id: json.packageId, ts: Date.now() };
575
- return json.packageId;
576
- }
577
- async function getConfig(fresh = false) {
578
- if (configCache && !fresh && Date.now() - configCache.ts < CACHE_TTL) return configCache.data;
579
- const [data, latestPkg] = await Promise.all([
580
- fetchJson(CONFIG_API),
581
- getLatestPackageId()
582
- ]);
583
- data.package = latestPkg;
584
- configCache = { data, ts: Date.now() };
585
- return data;
586
- }
587
- async function getPools(fresh = false) {
588
- if (poolsCache && !fresh && Date.now() - poolsCache.ts < CACHE_TTL) return poolsCache.data;
589
- const data = await fetchJson(POOLS_API);
590
- poolsCache = { data, ts: Date.now() };
591
- return data;
592
- }
593
- function matchesCoinType(poolType, targetType) {
594
- const poolSuffix = poolType.split("::").slice(1).join("::").toLowerCase();
595
- const targetSuffix = targetType.split("::").slice(1).join("::").toLowerCase();
596
- return poolSuffix === targetSuffix;
597
- }
598
- function resolvePoolSymbol(pool) {
599
- const coinType = pool.suiCoinType || pool.coinType || "";
600
- for (const [key, info] of Object.entries(SUPPORTED_ASSETS)) {
601
- if (matchesCoinType(coinType, info.type)) return key;
602
- }
603
- return pool.token?.symbol ?? "UNKNOWN";
604
- }
605
- async function getPool(asset = "USDC") {
606
- const pools = await getPools();
607
- const targetType = SUPPORTED_ASSETS[asset].type;
608
- const pool = pools.find(
609
- (p) => matchesCoinType(p.suiCoinType || p.coinType || "", targetType)
610
- );
611
- if (!pool) {
612
- throw new T2000Error(
613
- "ASSET_NOT_SUPPORTED",
614
- `${SUPPORTED_ASSETS[asset].displayName} pool not found on NAVI. Try: ${STABLE_ASSETS.filter((a) => a !== asset).join(", ")}`
615
- );
616
- }
617
- return pool;
618
- }
619
- function addOracleUpdate(tx, config, pool) {
620
- const feed = config.oracle.feeds?.find((f2) => f2.assetId === pool.id);
621
- if (!feed) {
622
- throw new T2000Error("PROTOCOL_UNAVAILABLE", `Oracle feed not found for asset ${pool.token?.symbol ?? pool.id}`);
623
- }
624
- tx.moveCall({
625
- target: `${config.oracle.packageId}::oracle_pro::update_single_price_v2`,
626
- arguments: [
627
- tx.object(CLOCK),
628
- tx.object(config.oracle.oracleConfig),
629
- tx.object(config.oracle.priceOracle),
630
- tx.object(config.oracle.supraOracleHolder),
631
- tx.object(feed.pythPriceInfoObject),
632
- tx.object(config.oracle.switchboardAggregator),
633
- tx.pure.address(feed.feedId)
634
- ]
635
- });
636
- }
637
- function refreshStableOracles(tx, config, pools) {
638
- const stableTypes = STABLE_ASSETS.map((a) => SUPPORTED_ASSETS[a].type);
639
- const stablePools = pools.filter((p) => {
640
- const ct = p.suiCoinType || p.coinType || "";
641
- return stableTypes.some((t) => matchesCoinType(ct, t));
642
- });
643
- for (const pool of stablePools) {
644
- addOracleUpdate(tx, config, pool);
645
- }
646
- }
647
- function rateToApy(rawRate) {
648
- if (!rawRate || rawRate === "0") return 0;
649
- return Number(BigInt(rawRate)) / 10 ** RATE_DECIMALS * 100;
650
- }
651
- function parseLtv(rawLtv) {
652
- if (!rawLtv || rawLtv === "0") return 0.75;
653
- return Number(BigInt(rawLtv)) / 10 ** LTV_DECIMALS;
654
- }
655
- function parseLiqThreshold(val) {
656
- if (typeof val === "number") return val;
657
- const n = Number(val);
658
- if (n > 1) return Number(BigInt(val)) / 10 ** LTV_DECIMALS;
659
- return n;
660
- }
661
- function normalizeHealthFactor(raw) {
662
- const v = raw / 10 ** RATE_DECIMALS;
663
- return v > 1e5 ? Infinity : v;
664
- }
665
- function compoundBalance(rawBalance, currentIndex) {
666
- if (!rawBalance || !currentIndex || currentIndex === "0") return 0;
667
- const scale = BigInt("1" + "0".repeat(RATE_DECIMALS));
668
- const half = scale / 2n;
669
- const result = (rawBalance * BigInt(currentIndex) + half) / scale;
670
- return Number(result) / 10 ** NAVI_BALANCE_DECIMALS;
671
- }
672
- async function getUserState(client, address) {
673
- const config = await getConfig();
674
- const tx = new transactions.Transaction();
675
- tx.moveCall({
676
- target: `${config.uiGetter}::getter_unchecked::get_user_state`,
677
- arguments: [tx.object(config.storage), tx.pure.address(address)]
678
- });
679
- const result = await client.devInspectTransactionBlock({
680
- transactionBlock: tx,
681
- sender: address
682
- });
683
- const decoded = decodeDevInspect(result, bcs.bcs.vector(UserStateInfo));
684
- if (!decoded) return [];
685
- const mapped = decoded.map((s) => ({
686
- assetId: s.asset_id,
687
- supplyBalance: toBigInt(s.supply_balance),
688
- borrowBalance: toBigInt(s.borrow_balance)
689
- }));
690
- return mapped.filter((s) => s.supplyBalance !== 0n || s.borrowBalance !== 0n);
691
- }
692
- async function fetchCoins(client, owner, coinType) {
693
- const all = [];
694
- let cursor;
695
- let hasNext = true;
696
- while (hasNext) {
697
- const page = await client.getCoins({ owner, coinType, cursor: cursor ?? void 0 });
698
- all.push(...page.data.map((c) => ({ coinObjectId: c.coinObjectId, balance: c.balance })));
699
- cursor = page.nextCursor;
700
- hasNext = page.hasNextPage;
701
- }
702
- return all;
703
- }
704
- function mergeCoins(tx, coins) {
705
- if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No coins to merge");
706
- const primary = tx.object(coins[0].coinObjectId);
707
- if (coins.length > 1) {
708
- tx.mergeCoins(primary, coins.slice(1).map((c) => tx.object(c.coinObjectId)));
709
- }
710
- return primary;
711
- }
712
- async function buildSaveTx(client, address, amount, options = {}) {
713
- if (!amount || amount <= 0 || !Number.isFinite(amount)) {
714
- throw new T2000Error("INVALID_AMOUNT", "Save amount must be a positive number");
715
- }
716
- const asset = options.asset ?? "USDC";
717
- const assetInfo = SUPPORTED_ASSETS[asset];
718
- const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
719
- const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
720
- const coins = await fetchCoins(client, address, assetInfo.type);
721
- if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins found`);
722
- const tx = new transactions.Transaction();
723
- tx.setSender(address);
724
- const coinObj = mergeCoins(tx, coins);
725
- if (options.collectFee) {
726
- addCollectFeeToTx(tx, coinObj, "save");
727
- }
728
- tx.moveCall({
729
- target: `${config.package}::incentive_v3::entry_deposit`,
730
- arguments: [
731
- tx.object(CLOCK),
732
- tx.object(config.storage),
733
- tx.object(pool.contract.pool),
734
- tx.pure.u8(pool.id),
735
- coinObj,
736
- tx.pure.u64(rawAmount),
737
- tx.object(config.incentiveV2),
738
- tx.object(config.incentiveV3)
739
- ],
740
- typeArguments: [pool.suiCoinType]
741
- });
742
- return tx;
743
- }
744
- async function buildWithdrawTx(client, address, amount, options = {}) {
745
- const asset = options.asset ?? "USDC";
746
- const assetInfo = SUPPORTED_ASSETS[asset];
747
- const [config, pool, pools, states] = await Promise.all([
748
- getConfig(),
749
- getPool(asset),
750
- getPools(),
751
- getUserState(client, address)
752
- ]);
753
- const assetState = states.find((s) => s.assetId === pool.id);
754
- const deposited = assetState ? compoundBalance(assetState.supplyBalance, pool.currentSupplyIndex) : 0;
755
- const effectiveAmount = Math.min(amount, Math.max(0, deposited - WITHDRAW_DUST_BUFFER));
756
- if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on NAVI`);
757
- const rawAmount = Number(stableToRaw(effectiveAmount, assetInfo.decimals));
758
- if (rawAmount <= 0) {
759
- throw new T2000Error("INVALID_AMOUNT", `Withdrawal amount rounds to zero \u2014 balance is dust`);
760
- }
761
- const tx = new transactions.Transaction();
762
- tx.setSender(address);
763
- refreshStableOracles(tx, config, pools);
764
- const [balance] = tx.moveCall({
765
- target: `${config.package}::incentive_v3::withdraw_v2`,
766
- arguments: [
767
- tx.object(CLOCK),
768
- tx.object(config.oracle.priceOracle),
769
- tx.object(config.storage),
770
- tx.object(pool.contract.pool),
771
- tx.pure.u8(pool.id),
772
- tx.pure.u64(rawAmount),
773
- tx.object(config.incentiveV2),
774
- tx.object(config.incentiveV3),
775
- tx.object(SUI_SYSTEM_STATE)
776
- ],
777
- typeArguments: [pool.suiCoinType]
778
- });
779
- const [coin] = tx.moveCall({
780
- target: "0x2::coin::from_balance",
781
- arguments: [balance],
782
- typeArguments: [pool.suiCoinType]
783
- });
784
- tx.transferObjects([coin], address);
785
- return { tx, effectiveAmount };
786
- }
787
- async function addWithdrawToTx(tx, client, address, amount, options = {}) {
788
- const asset = options.asset ?? "USDC";
789
- const assetInfo = SUPPORTED_ASSETS[asset];
790
- const [config, pool, pools, states] = await Promise.all([
791
- getConfig(),
792
- getPool(asset),
793
- getPools(),
794
- getUserState(client, address)
795
- ]);
796
- const assetState = states.find((s) => s.assetId === pool.id);
797
- const deposited = assetState ? compoundBalance(assetState.supplyBalance, pool.currentSupplyIndex) : 0;
798
- const effectiveAmount = Math.min(amount, Math.max(0, deposited - WITHDRAW_DUST_BUFFER));
799
- if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on NAVI`);
800
- const rawAmount = Number(stableToRaw(effectiveAmount, assetInfo.decimals));
801
- if (rawAmount <= 0) {
802
- const [coin2] = tx.moveCall({
803
- target: "0x2::coin::zero",
804
- typeArguments: [pool.suiCoinType]
805
- });
806
- return { coin: coin2, effectiveAmount: 0 };
807
- }
808
- refreshStableOracles(tx, config, pools);
809
- const [balance] = tx.moveCall({
810
- target: `${config.package}::incentive_v3::withdraw_v2`,
811
- arguments: [
812
- tx.object(CLOCK),
813
- tx.object(config.oracle.priceOracle),
814
- tx.object(config.storage),
815
- tx.object(pool.contract.pool),
816
- tx.pure.u8(pool.id),
817
- tx.pure.u64(rawAmount),
818
- tx.object(config.incentiveV2),
819
- tx.object(config.incentiveV3),
820
- tx.object(SUI_SYSTEM_STATE)
821
- ],
822
- typeArguments: [pool.suiCoinType]
823
- });
824
- const [coin] = tx.moveCall({
825
- target: "0x2::coin::from_balance",
826
- arguments: [balance],
827
- typeArguments: [pool.suiCoinType]
828
- });
829
- return { coin, effectiveAmount };
830
- }
831
- async function addSaveToTx(tx, _client, _address, coin, options = {}) {
832
- const asset = options.asset ?? "USDC";
833
- const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
834
- if (options.collectFee) {
835
- addCollectFeeToTx(tx, coin, "save");
836
- }
837
- const [coinValue] = tx.moveCall({
838
- target: "0x2::coin::value",
839
- typeArguments: [pool.suiCoinType],
840
- arguments: [coin]
841
- });
842
- tx.moveCall({
843
- target: `${config.package}::incentive_v3::entry_deposit`,
844
- arguments: [
845
- tx.object(CLOCK),
846
- tx.object(config.storage),
847
- tx.object(pool.contract.pool),
848
- tx.pure.u8(pool.id),
849
- coin,
850
- coinValue,
851
- tx.object(config.incentiveV2),
852
- tx.object(config.incentiveV3)
853
- ],
854
- typeArguments: [pool.suiCoinType]
855
- });
856
- }
857
- async function addRepayToTx(tx, client, _address, coin, options = {}) {
858
- const asset = options.asset ?? "USDC";
859
- const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
860
- addOracleUpdate(tx, config, pool);
861
- const [coinValue] = tx.moveCall({
862
- target: "0x2::coin::value",
863
- typeArguments: [pool.suiCoinType],
864
- arguments: [coin]
865
- });
866
- tx.moveCall({
867
- target: `${config.package}::incentive_v3::entry_repay`,
868
- arguments: [
869
- tx.object(CLOCK),
870
- tx.object(config.oracle.priceOracle),
871
- tx.object(config.storage),
872
- tx.object(pool.contract.pool),
873
- tx.pure.u8(pool.id),
874
- coin,
875
- coinValue,
876
- tx.object(config.incentiveV2),
877
- tx.object(config.incentiveV3)
878
- ],
879
- typeArguments: [pool.suiCoinType]
880
- });
881
- }
882
- async function buildBorrowTx(client, address, amount, options = {}) {
883
- if (!amount || amount <= 0 || !Number.isFinite(amount)) {
884
- throw new T2000Error("INVALID_AMOUNT", "Borrow amount must be a positive number");
885
- }
886
- const asset = options.asset ?? "USDC";
887
- const assetInfo = SUPPORTED_ASSETS[asset];
888
- const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
889
- const [config, pool, pools] = await Promise.all([
890
- getConfig(),
891
- getPool(asset),
892
- getPools()
893
- ]);
894
- const tx = new transactions.Transaction();
895
- tx.setSender(address);
896
- refreshStableOracles(tx, config, pools);
897
- const [balance] = tx.moveCall({
898
- target: `${config.package}::incentive_v3::borrow_v2`,
899
- arguments: [
900
- tx.object(CLOCK),
901
- tx.object(config.oracle.priceOracle),
902
- tx.object(config.storage),
903
- tx.object(pool.contract.pool),
904
- tx.pure.u8(pool.id),
905
- tx.pure.u64(rawAmount),
906
- tx.object(config.incentiveV2),
907
- tx.object(config.incentiveV3),
908
- tx.object(SUI_SYSTEM_STATE)
909
- ],
910
- typeArguments: [pool.suiCoinType]
911
- });
912
- const [borrowedCoin] = tx.moveCall({
913
- target: "0x2::coin::from_balance",
914
- arguments: [balance],
915
- typeArguments: [pool.suiCoinType]
916
- });
917
- if (options.collectFee) {
918
- addCollectFeeToTx(tx, borrowedCoin, "borrow");
919
- }
920
- tx.transferObjects([borrowedCoin], address);
921
- return tx;
922
- }
923
- async function buildRepayTx(client, address, amount, options = {}) {
924
- if (!amount || amount <= 0 || !Number.isFinite(amount)) {
925
- throw new T2000Error("INVALID_AMOUNT", "Repay amount must be a positive number");
926
- }
927
- const asset = options.asset ?? "USDC";
928
- const assetInfo = SUPPORTED_ASSETS[asset];
929
- const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
930
- const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
931
- const coins = await fetchCoins(client, address, assetInfo.type);
932
- if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins to repay with`);
933
- const tx = new transactions.Transaction();
934
- tx.setSender(address);
935
- addOracleUpdate(tx, config, pool);
936
- const coinObj = mergeCoins(tx, coins);
937
- tx.moveCall({
938
- target: `${config.package}::incentive_v3::entry_repay`,
939
- arguments: [
940
- tx.object(CLOCK),
941
- tx.object(config.oracle.priceOracle),
942
- tx.object(config.storage),
943
- tx.object(pool.contract.pool),
944
- tx.pure.u8(pool.id),
945
- coinObj,
946
- tx.pure.u64(rawAmount),
947
- tx.object(config.incentiveV2),
948
- tx.object(config.incentiveV3)
949
- ],
950
- typeArguments: [pool.suiCoinType]
951
- });
952
- return tx;
953
- }
954
- async function getHealthFactor(client, addressOrKeypair) {
955
- const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
956
- const [config, pools, states] = await Promise.all([
957
- getConfig(),
958
- getPools(),
959
- getUserState(client, address)
960
- ]);
961
- let supplied = 0;
962
- let borrowed = 0;
963
- let weightedLtv = 0;
964
- let weightedLiqThreshold = 0;
965
- for (const state of states) {
966
- const pool = pools.find((p) => p.id === state.assetId);
967
- if (!pool) continue;
968
- const supplyBal = compoundBalance(state.supplyBalance, pool.currentSupplyIndex);
969
- const borrowBal = compoundBalance(state.borrowBalance, pool.currentBorrowIndex);
970
- const price = pool.token?.price ?? 1;
971
- supplied += supplyBal * price;
972
- borrowed += borrowBal * price;
973
- if (supplyBal > 0) {
974
- weightedLtv += supplyBal * price * parseLtv(pool.ltv);
975
- weightedLiqThreshold += supplyBal * price * parseLiqThreshold(pool.liquidationFactor.threshold);
976
- }
977
- }
978
- const ltv = supplied > 0 ? weightedLtv / supplied : 0.75;
979
- const liqThreshold = supplied > 0 ? weightedLiqThreshold / supplied : 0.75;
980
- const maxBorrowVal = Math.max(0, supplied * ltv - borrowed);
981
- const usdcPool = pools.find((p) => matchesCoinType(p.suiCoinType || p.coinType || "", SUPPORTED_ASSETS.USDC.type));
982
- let healthFactor;
983
- if (borrowed <= 0) {
984
- healthFactor = Infinity;
985
- } else if (usdcPool) {
986
- try {
987
- const tx = new transactions.Transaction();
988
- tx.moveCall({
989
- target: `${config.uiGetter}::calculator_unchecked::dynamic_health_factor`,
990
- arguments: [
991
- tx.object(CLOCK),
992
- tx.object(config.storage),
993
- tx.object(config.oracle.priceOracle),
994
- tx.pure.u8(usdcPool.id),
995
- tx.pure.address(address),
996
- tx.pure.u8(usdcPool.id),
997
- tx.pure.u64(0),
998
- tx.pure.u64(0),
999
- tx.pure.bool(false)
1000
- ],
1001
- typeArguments: [usdcPool.suiCoinType]
1002
- });
1003
- const result = await client.devInspectTransactionBlock({
1004
- transactionBlock: tx,
1005
- sender: address
1006
- });
1007
- const decoded = decodeDevInspect(result, bcs.bcs.u256());
1008
- if (decoded !== void 0) {
1009
- healthFactor = normalizeHealthFactor(Number(decoded));
1010
- } else {
1011
- healthFactor = supplied * liqThreshold / borrowed;
1012
- }
1013
- } catch {
1014
- healthFactor = supplied * liqThreshold / borrowed;
1015
- }
1016
- } else {
1017
- healthFactor = supplied * liqThreshold / borrowed;
1018
- }
1019
- return {
1020
- healthFactor,
1021
- supplied,
1022
- borrowed,
1023
- maxBorrow: maxBorrowVal,
1024
- liquidationThreshold: liqThreshold
1025
- };
1026
- }
1027
- async function getRates(client) {
1028
- try {
1029
- const pools = await getPools();
1030
- const result = {};
1031
- for (const asset of STABLE_ASSETS) {
1032
- const targetType = SUPPORTED_ASSETS[asset].type;
1033
- const pool = pools.find((p) => matchesCoinType(p.suiCoinType || p.coinType || "", targetType));
1034
- if (!pool) continue;
1035
- let saveApy = rateToApy(pool.currentSupplyRate);
1036
- let borrowApy = rateToApy(pool.currentBorrowRate);
1037
- if (saveApy <= 0 || saveApy > 100) saveApy = 0;
1038
- if (borrowApy <= 0 || borrowApy > 100) borrowApy = 0;
1039
- result[asset] = { saveApy, borrowApy };
1040
- }
1041
- if (!result.USDC) result.USDC = { saveApy: 4, borrowApy: 6 };
1042
- return result;
1043
- } catch {
1044
- return { USDC: { saveApy: 4, borrowApy: 6 } };
1045
- }
1046
- }
1047
- async function getPositions(client, addressOrKeypair) {
1048
- const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
1049
- const [states, pools] = await Promise.all([getUserState(client, address), getPools()]);
1050
- const positions = [];
1051
- for (const state of states) {
1052
- const pool = pools.find((p) => p.id === state.assetId);
1053
- if (!pool) continue;
1054
- const symbol = resolvePoolSymbol(pool);
1055
- const supplyBal = compoundBalance(state.supplyBalance, pool.currentSupplyIndex);
1056
- const borrowBal = compoundBalance(state.borrowBalance, pool.currentBorrowIndex);
1057
- if (supplyBal > 1e-4) {
1058
- positions.push({
1059
- protocol: "navi",
1060
- asset: symbol,
1061
- type: "save",
1062
- amount: supplyBal,
1063
- apy: rateToApy(pool.currentSupplyRate)
1064
- });
1065
- }
1066
- if (borrowBal > 1e-4) {
1067
- positions.push({
1068
- protocol: "navi",
1069
- asset: symbol,
1070
- type: "borrow",
1071
- amount: borrowBal,
1072
- apy: rateToApy(pool.currentBorrowRate)
1073
- });
1074
- }
1075
- }
1076
- return { positions };
1077
- }
1078
- async function maxWithdrawAmount(client, addressOrKeypair) {
1079
- const hf = await getHealthFactor(client, addressOrKeypair);
1080
- const ltv = hf.liquidationThreshold > 0 ? hf.liquidationThreshold : 0.75;
1081
- let maxAmount;
1082
- if (hf.borrowed === 0) {
1083
- maxAmount = hf.supplied;
1084
- } else {
1085
- maxAmount = Math.max(0, hf.supplied - hf.borrowed * MIN_HEALTH_FACTOR / ltv);
1086
- }
1087
- const remainingSupply = hf.supplied - maxAmount;
1088
- const hfAfter = hf.borrowed > 0 ? remainingSupply / hf.borrowed : Infinity;
1089
- return { maxAmount, healthFactorAfter: hfAfter, currentHF: hf.healthFactor };
1090
- }
1091
- async function maxBorrowAmount(client, addressOrKeypair) {
1092
- const hf = await getHealthFactor(client, addressOrKeypair);
1093
- const ltv = hf.liquidationThreshold > 0 ? hf.liquidationThreshold : 0.75;
1094
- const maxAmount = Math.max(0, hf.supplied * ltv / MIN_HEALTH_FACTOR - hf.borrowed);
1095
- return { maxAmount, healthFactorAfter: MIN_HEALTH_FACTOR, currentHF: hf.healthFactor };
1096
- }
1097
-
1098
- // src/protocols/yieldTracker.ts
1099
- async function getEarnings(client, keypair) {
1100
- const hf = await getHealthFactor(client, keypair);
1101
- const rates = await getRates();
1102
- const supplied = hf.supplied;
1103
- const apy = rates.USDC.saveApy / 100;
1104
- const dailyRate = apy / 365;
1105
- const dailyEarning = supplied * dailyRate;
1106
- const totalYieldEarned = dailyEarning * 30;
1107
- return {
1108
- totalYieldEarned,
1109
- currentApy: rates.USDC.saveApy,
1110
- dailyEarning,
1111
- supplied
1112
- };
1113
- }
1114
- async function getFundStatus(client, keypair) {
1115
- const earnings = await getEarnings(client, keypair);
1116
- return {
1117
- supplied: earnings.supplied,
1118
- apy: earnings.currentApy,
1119
- earnedToday: earnings.dailyEarning,
1120
- earnedAllTime: earnings.totalYieldEarned,
1121
- projectedMonthly: earnings.dailyEarning * 30
1122
- };
1123
- }
1124
- var descriptor = {
1125
- id: "sentinel",
1126
- name: "Sui Sentinel",
1127
- packages: [SENTINEL.PACKAGE],
1128
- actionMap: {
1129
- "sentinel::request_attack": "sentinel_attack",
1130
- "sentinel::consume_prompt": "sentinel_settle"
1131
- }
1132
- };
1133
- function mapAgent(raw) {
1134
- return {
1135
- id: raw.agent_id,
1136
- objectId: raw.agent_object_id,
1137
- name: raw.agent_name,
1138
- model: raw.model ?? "unknown",
1139
- systemPrompt: raw.prompt,
1140
- attackFee: BigInt(raw.cost_per_message),
1141
- prizePool: BigInt(raw.total_balance),
1142
- totalAttacks: raw.total_attacks,
1143
- successfulBreaches: raw.successful_breaches ?? 0,
1144
- state: raw.state
1145
- };
1146
- }
1147
- async function listSentinels() {
1148
- const res = await fetch(SENTINEL.SENTINELS_API);
1149
- if (!res.ok) {
1150
- throw new T2000Error("SENTINEL_API_ERROR", `Sentinel API returned ${res.status}`);
1151
- }
1152
- const data = await res.json();
1153
- if (!Array.isArray(data.agents)) {
1154
- throw new T2000Error("SENTINEL_API_ERROR", "Unexpected API response shape");
1155
- }
1156
- return data.agents.filter((a) => a.state === "active").map(mapAgent);
1157
- }
1158
- async function getSentinelInfo(client, sentinelObjectId) {
1159
- const agents = await listSentinels();
1160
- const match = agents.find((a) => a.objectId === sentinelObjectId || a.id === sentinelObjectId);
1161
- if (match) return match;
1162
- const obj = await client.getObject({
1163
- id: sentinelObjectId,
1164
- options: { showContent: true, showType: true }
1165
- });
1166
- if (!obj.data) {
1167
- throw new T2000Error("SENTINEL_NOT_FOUND", `Sentinel ${sentinelObjectId} not found on-chain`);
1168
- }
1169
- const content = obj.data.content;
1170
- if (!content || content.dataType !== "moveObject") {
1171
- throw new T2000Error("SENTINEL_NOT_FOUND", `Object ${sentinelObjectId} is not a Move object`);
1172
- }
1173
- const fields = content.fields;
1174
- return {
1175
- id: fields.id?.id ?? sentinelObjectId,
1176
- objectId: sentinelObjectId,
1177
- name: fields.name ?? "Unknown",
1178
- model: fields.model ?? "unknown",
1179
- systemPrompt: fields.system_prompt ?? "",
1180
- attackFee: BigInt(fields.cost_per_message ?? "0"),
1181
- prizePool: BigInt(fields.balance ?? "0"),
1182
- totalAttacks: Number(fields.total_attacks ?? "0"),
1183
- successfulBreaches: Number(fields.successful_breaches ?? "0"),
1184
- state: fields.state ?? "unknown"
1185
- };
1186
- }
1187
- async function requestAttack(client, signer, sentinelObjectId, feeMist) {
1188
- if (feeMist < SENTINEL.MIN_FEE_MIST) {
1189
- throw new T2000Error("INVALID_AMOUNT", `Attack fee must be at least 0.1 SUI (${SENTINEL.MIN_FEE_MIST} MIST)`);
1190
- }
1191
- const tx = new transactions.Transaction();
1192
- const [coin] = tx.splitCoins(tx.gas, [Number(feeMist)]);
1193
- const [attack2] = tx.moveCall({
1194
- target: `${SENTINEL.PACKAGE}::sentinel::request_attack`,
1195
- arguments: [
1196
- tx.object(SENTINEL.AGENT_REGISTRY),
1197
- tx.object(sentinelObjectId),
1198
- tx.object(SENTINEL.PROTOCOL_CONFIG),
1199
- coin,
1200
- tx.object(SENTINEL.RANDOM),
1201
- tx.object(CLOCK_ID)
1202
- ]
1203
- });
1204
- const address = signer.toSuiAddress();
1205
- tx.transferObjects([attack2], address);
1206
- const result = await client.signAndExecuteTransaction({
1207
- signer,
1208
- transaction: tx,
1209
- options: { showObjectChanges: true, showEffects: true }
1210
- });
1211
- await client.waitForTransaction({ digest: result.digest });
1212
- const attackObj = result.objectChanges?.find(
1213
- (c) => c.type === "created" && c.objectType?.includes("::sentinel::Attack")
1214
- );
1215
- const attackObjectId = attackObj && "objectId" in attackObj ? attackObj.objectId : void 0;
1216
- if (!attackObjectId) {
1217
- throw new T2000Error("SENTINEL_TX_FAILED", "Attack object was not created \u2014 transaction may have failed");
1218
- }
1219
- return { attackObjectId, digest: result.digest };
1220
- }
1221
- async function submitPrompt(agentId, attackObjectId, prompt) {
1222
- const res = await fetch(SENTINEL.TEE_API, {
1223
- method: "POST",
1224
- headers: { "Content-Type": "application/json" },
1225
- body: JSON.stringify({
1226
- agent_id: agentId,
1227
- attack_object_id: attackObjectId,
1228
- message: prompt
1229
- })
1230
- });
1231
- if (!res.ok) {
1232
- const body = await res.text().catch(() => "");
1233
- throw new T2000Error("SENTINEL_TEE_ERROR", `TEE returned ${res.status}: ${body.slice(0, 200)}`);
1234
- }
1235
- const raw = await res.json();
1236
- const envelope = raw.response ?? raw;
1237
- const data = envelope.data ?? envelope;
1238
- const signature = raw.signature ?? data.signature;
1239
- const timestampMs = envelope.timestamp_ms ?? data.timestamp_ms;
1240
- if (typeof signature !== "string") {
1241
- throw new T2000Error("SENTINEL_TEE_ERROR", "TEE response missing signature");
1242
- }
1243
- return {
1244
- success: data.success ?? data.is_success,
1245
- score: data.score,
1246
- agentResponse: data.agent_response,
1247
- juryResponse: data.jury_response,
1248
- funResponse: data.fun_response ?? "",
1249
- signature,
1250
- timestampMs
1251
- };
1252
- }
1253
- async function settleAttack(client, signer, sentinelObjectId, attackObjectId, prompt, verdict) {
1254
- const sigBytes = Array.from(Buffer.from(verdict.signature.replace(/^0x/, ""), "hex"));
1255
- const tx = new transactions.Transaction();
1256
- tx.moveCall({
1257
- target: `${SENTINEL.PACKAGE}::sentinel::consume_prompt`,
1258
- arguments: [
1259
- tx.object(SENTINEL.AGENT_REGISTRY),
1260
- tx.object(SENTINEL.PROTOCOL_CONFIG),
1261
- tx.object(sentinelObjectId),
1262
- tx.pure.bool(verdict.success),
1263
- tx.pure.string(verdict.agentResponse),
1264
- tx.pure.string(verdict.juryResponse),
1265
- tx.pure.string(verdict.funResponse),
1266
- tx.pure.string(prompt),
1267
- tx.pure.u8(verdict.score),
1268
- tx.pure.u64(verdict.timestampMs),
1269
- tx.pure(bcs.bcs.vector(bcs.bcs.u8()).serialize(sigBytes)),
1270
- tx.object(SENTINEL.ENCLAVE),
1271
- tx.object(attackObjectId),
1272
- tx.object(CLOCK_ID)
1273
- ]
1274
- });
1275
- const result = await client.signAndExecuteTransaction({
1276
- signer,
1277
- transaction: tx,
1278
- options: { showEffects: true }
1279
- });
1280
- await client.waitForTransaction({ digest: result.digest });
1281
- const txSuccess = result.effects?.status?.status === "success";
1282
- return { digest: result.digest, success: txSuccess };
1283
- }
1284
- async function attack(client, signer, sentinelId, prompt, feeMist) {
1285
- const sentinel = await getSentinelInfo(client, sentinelId);
1286
- const fee = feeMist ?? sentinel.attackFee;
1287
- if (fee < SENTINEL.MIN_FEE_MIST) {
1288
- throw new T2000Error("INVALID_AMOUNT", `Attack fee must be at least 0.1 SUI`);
1289
- }
1290
- const { attackObjectId, digest: requestTx } = await requestAttack(
1291
- client,
1292
- signer,
1293
- sentinel.objectId,
1294
- fee
1295
- );
1296
- const verdict = await submitPrompt(sentinel.id, attackObjectId, prompt);
1297
- const { digest: settleTx } = await settleAttack(
1298
- client,
1299
- signer,
1300
- sentinel.objectId,
1301
- attackObjectId,
1302
- prompt,
1303
- verdict
1304
- );
1305
- const won = verdict.success && verdict.score >= 70;
1306
- return {
1307
- attackObjectId,
1308
- sentinelId: sentinel.id,
1309
- prompt,
1310
- verdict,
1311
- requestTx,
1312
- settleTx,
1313
- won,
1314
- feePaid: Number(fee) / Number(MIST_PER_SUI)
1315
- };
1316
- }
1317
-
1318
- // src/adapters/registry.ts
1319
- var ProtocolRegistry = class {
1320
- lending = /* @__PURE__ */ new Map();
1321
- swap = /* @__PURE__ */ new Map();
1322
- registerLending(adapter) {
1323
- this.lending.set(adapter.id, adapter);
1324
- }
1325
- registerSwap(adapter) {
1326
- this.swap.set(adapter.id, adapter);
1327
- }
1328
- async bestSaveRate(asset) {
1329
- const candidates = [];
1330
- for (const adapter of this.lending.values()) {
1331
- if (!adapter.supportedAssets.includes(asset)) continue;
1332
- if (!adapter.capabilities.includes("save")) continue;
1333
- try {
1334
- const rate = await adapter.getRates(asset);
1335
- candidates.push({ adapter, rate });
1336
- } catch {
1337
- }
1338
- }
1339
- if (candidates.length === 0) {
1340
- throw new T2000Error("ASSET_NOT_SUPPORTED", `No lending adapter supports saving ${asset}`);
1341
- }
1342
- candidates.sort((a, b) => b.rate.saveApy - a.rate.saveApy);
1343
- return candidates[0];
1344
- }
1345
- async bestBorrowRate(asset, opts) {
1346
- const candidates = [];
1347
- for (const adapter of this.lending.values()) {
1348
- if (!adapter.supportedAssets.includes(asset)) continue;
1349
- if (!adapter.capabilities.includes("borrow")) continue;
1350
- if (opts?.requireSameAssetBorrow && !adapter.supportsSameAssetBorrow) continue;
1351
- try {
1352
- const rate = await adapter.getRates(asset);
1353
- candidates.push({ adapter, rate });
1354
- } catch {
1355
- }
1356
- }
1357
- if (candidates.length === 0) {
1358
- throw new T2000Error("ASSET_NOT_SUPPORTED", `No lending adapter supports borrowing ${asset}`);
1359
- }
1360
- candidates.sort((a, b) => a.rate.borrowApy - b.rate.borrowApy);
1361
- return candidates[0];
1362
- }
1363
- async bestSwapQuote(from, to, amount) {
1364
- const candidates = [];
1365
- for (const adapter of this.swap.values()) {
1366
- const pairs = adapter.getSupportedPairs();
1367
- if (!pairs.some((p) => p.from === from && p.to === to)) continue;
1368
- try {
1369
- const quote = await adapter.getQuote(from, to, amount);
1370
- candidates.push({ adapter, quote });
1371
- } catch {
1372
- }
1373
- }
1374
- if (candidates.length === 0) {
1375
- throw new T2000Error("ASSET_NOT_SUPPORTED", `No swap adapter supports ${from} \u2192 ${to}`);
1376
- }
1377
- candidates.sort((a, b) => b.quote.expectedOutput - a.quote.expectedOutput);
1378
- return candidates[0];
1379
- }
1380
- async bestSaveRateAcrossAssets() {
1381
- const candidates = [];
1382
- for (const asset of STABLE_ASSETS) {
1383
- for (const adapter of this.lending.values()) {
1384
- if (!adapter.supportedAssets.includes(asset)) continue;
1385
- if (!adapter.capabilities.includes("save")) continue;
1386
- try {
1387
- const rate = await adapter.getRates(asset);
1388
- candidates.push({ adapter, rate, asset });
1389
- } catch {
1390
- }
1391
- }
1392
- }
1393
- if (candidates.length === 0) {
1394
- throw new T2000Error("ASSET_NOT_SUPPORTED", "No lending adapter found for any stablecoin");
1395
- }
1396
- candidates.sort((a, b) => b.rate.saveApy - a.rate.saveApy);
1397
- return candidates[0];
1398
- }
1399
- async allRatesAcrossAssets() {
1400
- const results = [];
1401
- for (const asset of STABLE_ASSETS) {
1402
- for (const adapter of this.lending.values()) {
1403
- if (!adapter.supportedAssets.includes(asset)) continue;
1404
- try {
1405
- const rates = await adapter.getRates(asset);
1406
- if (rates.saveApy > 0 || rates.borrowApy > 0) {
1407
- results.push({ protocol: adapter.name, protocolId: adapter.id, asset, rates });
1408
- }
1409
- } catch {
1410
- }
1411
- }
1412
- }
1413
- return results;
1414
- }
1415
- async allRates(asset) {
1416
- const results = [];
1417
- for (const adapter of this.lending.values()) {
1418
- if (!adapter.supportedAssets.includes(asset)) continue;
1419
- try {
1420
- const rates = await adapter.getRates(asset);
1421
- results.push({ protocol: adapter.name, protocolId: adapter.id, rates });
1422
- } catch {
1423
- }
1424
- }
1425
- return results;
1426
- }
1427
- async allPositions(address) {
1428
- const results = [];
1429
- for (const adapter of this.lending.values()) {
1430
- try {
1431
- const positions = await adapter.getPositions(address);
1432
- if (positions.supplies.length > 0 || positions.borrows.length > 0) {
1433
- results.push({ protocol: adapter.name, protocolId: adapter.id, positions });
1434
- }
1435
- } catch {
1436
- }
1437
- }
1438
- return results;
1439
- }
1440
- getLending(id) {
1441
- return this.lending.get(id);
1442
- }
1443
- getSwap(id) {
1444
- return this.swap.get(id);
1445
- }
1446
- listLending() {
1447
- return [...this.lending.values()];
1448
- }
1449
- listSwap() {
1450
- return [...this.swap.values()];
1451
- }
1452
- };
1453
-
1454
- // src/adapters/navi.ts
1455
- var descriptor2 = {
1456
- id: "navi",
1457
- name: "NAVI Protocol",
1458
- packages: [],
1459
- dynamicPackageId: true,
1460
- actionMap: {
1461
- "incentive_v3::entry_deposit": "save",
1462
- "incentive_v3::deposit": "save",
1463
- "incentive_v3::withdraw_v2": "withdraw",
1464
- "incentive_v3::entry_withdraw": "withdraw",
1465
- "incentive_v3::borrow_v2": "borrow",
1466
- "incentive_v3::entry_borrow": "borrow",
1467
- "incentive_v3::entry_repay": "repay",
1468
- "incentive_v3::repay": "repay"
1469
- }
1470
- };
1471
- var NaviAdapter = class {
1472
- id = "navi";
1473
- name = "NAVI Protocol";
1474
- version = "1.0.0";
1475
- capabilities = ["save", "withdraw", "borrow", "repay"];
1476
- supportedAssets = [...STABLE_ASSETS];
1477
- supportsSameAssetBorrow = true;
1478
- client;
1479
- async init(client) {
1480
- this.client = client;
1481
- }
1482
- initSync(client) {
1483
- this.client = client;
1484
- }
1485
- async getRates(asset) {
1486
- const rates = await getRates(this.client);
1487
- const normalized = normalizeAsset(asset);
1488
- const r = rates[normalized];
1489
- if (!r) throw new T2000Error("ASSET_NOT_SUPPORTED", `NAVI does not support ${asset}`);
1490
- return { asset: normalized, saveApy: r.saveApy, borrowApy: r.borrowApy };
1491
- }
1492
- async getPositions(address) {
1493
- const result = await getPositions(this.client, address);
1494
- return {
1495
- supplies: result.positions.filter((p) => p.type === "save").map((p) => ({ asset: p.asset, amount: p.amount, apy: p.apy })),
1496
- borrows: result.positions.filter((p) => p.type === "borrow").map((p) => ({ asset: p.asset, amount: p.amount, apy: p.apy }))
1497
- };
1498
- }
1499
- async getHealth(address) {
1500
- return getHealthFactor(this.client, address);
1501
- }
1502
- async buildSaveTx(address, amount, asset, options) {
1503
- const stableAsset = normalizeAsset(asset);
1504
- const tx = await buildSaveTx(this.client, address, amount, { ...options, asset: stableAsset });
1505
- return { tx };
1506
- }
1507
- async buildWithdrawTx(address, amount, asset) {
1508
- const stableAsset = normalizeAsset(asset);
1509
- const result = await buildWithdrawTx(this.client, address, amount, { asset: stableAsset });
1510
- return { tx: result.tx, effectiveAmount: result.effectiveAmount };
1511
- }
1512
- async buildBorrowTx(address, amount, asset, options) {
1513
- const stableAsset = normalizeAsset(asset);
1514
- const tx = await buildBorrowTx(this.client, address, amount, { ...options, asset: stableAsset });
1515
- return { tx };
1516
- }
1517
- async buildRepayTx(address, amount, asset) {
1518
- const stableAsset = normalizeAsset(asset);
1519
- const tx = await buildRepayTx(this.client, address, amount, { asset: stableAsset });
1520
- return { tx };
1521
- }
1522
- async maxWithdraw(address, _asset) {
1523
- return maxWithdrawAmount(this.client, address);
1524
- }
1525
- async maxBorrow(address, _asset) {
1526
- return maxBorrowAmount(this.client, address);
1527
- }
1528
- async addWithdrawToTx(tx, address, amount, asset) {
1529
- const stableAsset = normalizeAsset(asset);
1530
- return addWithdrawToTx(tx, this.client, address, amount, { asset: stableAsset });
1531
- }
1532
- async addSaveToTx(tx, address, coin, asset, options) {
1533
- const stableAsset = normalizeAsset(asset);
1534
- return addSaveToTx(tx, this.client, address, coin, { ...options, asset: stableAsset });
1535
- }
1536
- async addRepayToTx(tx, address, coin, asset) {
1537
- const stableAsset = normalizeAsset(asset);
1538
- return addRepayToTx(tx, this.client, address, coin, { asset: stableAsset });
1539
- }
1540
- };
1541
- var DEFAULT_SLIPPAGE_BPS = 300;
1542
- function createAggregatorClient(client, signer) {
1543
- return new aggregatorSdk.AggregatorClient({
1544
- client,
1545
- signer,
1546
- env: aggregatorSdk.Env.Mainnet
1547
- });
1548
- }
1549
- async function buildSwapTx(params) {
1550
- const { client, address, fromAsset, toAsset, amount, maxSlippageBps = DEFAULT_SLIPPAGE_BPS } = params;
1551
- const fromInfo = SUPPORTED_ASSETS[fromAsset];
1552
- const toInfo = SUPPORTED_ASSETS[toAsset];
1553
- if (!fromInfo || !toInfo) {
1554
- throw new T2000Error("ASSET_NOT_SUPPORTED", `Swap pair ${fromAsset}/${toAsset} is not supported`);
1555
- }
1556
- const rawAmount = BigInt(Math.floor(amount * 10 ** fromInfo.decimals));
1557
- const aggClient = createAggregatorClient(client, address);
1558
- const result = await aggClient.findRouters({
1559
- from: fromInfo.type,
1560
- target: toInfo.type,
1561
- amount: rawAmount,
1562
- byAmountIn: true
1563
- });
1564
- if (!result || result.insufficientLiquidity) {
1565
- throw new T2000Error(
1566
- "ASSET_NOT_SUPPORTED",
1567
- `No swap route found for ${fromAsset} \u2192 ${toAsset}`
1568
- );
1569
- }
1570
- const tx = new transactions.Transaction();
1571
- const slippage = maxSlippageBps / 1e4;
1572
- await aggClient.fastRouterSwap({
1573
- router: result,
1574
- txb: tx,
1575
- slippage
1576
- });
1577
- const estimatedOut = Number(result.amountOut.toString());
1578
- return {
1579
- tx,
1580
- estimatedOut,
1581
- toDecimals: toInfo.decimals
1582
- };
1583
- }
1584
- async function addSwapToTx(params) {
1585
- const { tx, client, address, inputCoin, fromAsset, toAsset, amount, maxSlippageBps = DEFAULT_SLIPPAGE_BPS } = params;
1586
- const fromInfo = SUPPORTED_ASSETS[fromAsset];
1587
- const toInfo = SUPPORTED_ASSETS[toAsset];
1588
- if (!fromInfo || !toInfo) {
1589
- throw new T2000Error("ASSET_NOT_SUPPORTED", `Swap pair ${fromAsset}/${toAsset} is not supported`);
1590
- }
1591
- const rawAmount = BigInt(Math.floor(amount * 10 ** fromInfo.decimals));
1592
- const aggClient = createAggregatorClient(client, address);
1593
- const result = await aggClient.findRouters({
1594
- from: fromInfo.type,
1595
- target: toInfo.type,
1596
- amount: rawAmount,
1597
- byAmountIn: true
1598
- });
1599
- if (!result || result.insufficientLiquidity) {
1600
- throw new T2000Error(
1601
- "ASSET_NOT_SUPPORTED",
1602
- `No swap route found for ${fromAsset} \u2192 ${toAsset}`
1603
- );
1604
- }
1605
- const slippage = maxSlippageBps / 1e4;
1606
- const outputCoin = await aggClient.routerSwap({
1607
- router: result,
1608
- txb: tx,
1609
- inputCoin,
1610
- slippage
1611
- });
1612
- const estimatedOut = Number(result.amountOut.toString());
1613
- return {
1614
- outputCoin,
1615
- estimatedOut,
1616
- toDecimals: toInfo.decimals
1617
- };
1618
- }
1619
- async function getPoolPrice(client) {
1620
- try {
1621
- const pool = await client.getObject({
1622
- id: CETUS_USDC_SUI_POOL,
1623
- options: { showContent: true }
1624
- });
1625
- if (pool.data?.content?.dataType === "moveObject") {
1626
- const fields = pool.data.content.fields;
1627
- const currentSqrtPrice = BigInt(String(fields.current_sqrt_price ?? "0"));
1628
- if (currentSqrtPrice > 0n) {
1629
- const Q64 = 2n ** 64n;
1630
- const sqrtPriceFloat = Number(currentSqrtPrice) / Number(Q64);
1631
- const rawPrice = sqrtPriceFloat * sqrtPriceFloat;
1632
- const suiPriceUsd = 1e3 / rawPrice;
1633
- if (suiPriceUsd > 0.01 && suiPriceUsd < 1e3) return suiPriceUsd;
1634
- }
1635
- }
1636
- } catch {
1637
- }
1638
- return 3.5;
1639
- }
1640
- async function getSwapQuote(client, fromAsset, toAsset, amount) {
1641
- const fromInfo = SUPPORTED_ASSETS[fromAsset];
1642
- const toInfo = SUPPORTED_ASSETS[toAsset];
1643
- if (!fromInfo || !toInfo) {
1644
- throw new T2000Error("ASSET_NOT_SUPPORTED", `Swap pair ${fromAsset}/${toAsset} is not supported`);
1645
- }
1646
- const rawAmount = BigInt(Math.floor(amount * 10 ** fromInfo.decimals));
1647
- const poolPrice = await getPoolPrice(client);
1648
- try {
1649
- const aggClient = createAggregatorClient(client);
1650
- const result = await aggClient.findRouters({
1651
- from: fromInfo.type,
1652
- target: toInfo.type,
1653
- amount: rawAmount,
1654
- byAmountIn: true
1655
- });
1656
- if (!result || result.insufficientLiquidity) {
1657
- return fallbackQuote(fromAsset, amount, poolPrice);
1658
- }
1659
- const expectedOutput = Number(result.amountOut.toString()) / 10 ** toInfo.decimals;
1660
- const priceImpact = result.deviationRatio ?? 0;
1661
- return { expectedOutput, priceImpact, poolPrice };
1662
- } catch {
1663
- return fallbackQuote(fromAsset, amount, poolPrice);
1664
- }
1665
- }
1666
- function fallbackQuote(fromAsset, amount, poolPrice) {
1667
- const expectedOutput = fromAsset === "USDC" ? amount / poolPrice : amount * poolPrice;
1668
- return { expectedOutput, priceImpact: 0, poolPrice };
1669
- }
1670
-
1671
- // src/adapters/cetus.ts
1672
- var descriptor3 = {
1673
- id: "cetus",
1674
- name: "Cetus DEX",
1675
- packages: [CETUS_PACKAGE],
1676
- actionMap: {
1677
- "router::swap": "swap",
1678
- "router::swap_ab_bc": "swap",
1679
- "router::swap_ab_cb": "swap",
1680
- "router::swap_ba_bc": "swap",
1681
- "router::swap_ba_cb": "swap"
1682
- }
1683
- };
1684
- var CetusAdapter = class {
1685
- id = "cetus";
1686
- name = "Cetus";
1687
- version = "1.0.0";
1688
- capabilities = ["swap"];
1689
- client;
1690
- async init(client) {
1691
- this.client = client;
1692
- }
1693
- initSync(client) {
1694
- this.client = client;
1695
- }
1696
- async getQuote(from, to, amount) {
1697
- return getSwapQuote(this.client, from, to, amount);
1698
- }
1699
- async buildSwapTx(address, from, to, amount, maxSlippageBps) {
1700
- const result = await buildSwapTx({
1701
- client: this.client,
1702
- address,
1703
- fromAsset: from,
1704
- toAsset: to,
1705
- amount,
1706
- maxSlippageBps
1707
- });
1708
- return {
1709
- tx: result.tx,
1710
- estimatedOut: result.estimatedOut,
1711
- toDecimals: result.toDecimals
1712
- };
1713
- }
1714
- getSupportedPairs() {
1715
- const pairs = [];
1716
- for (const asset of Object.keys(INVESTMENT_ASSETS)) {
1717
- pairs.push({ from: "USDC", to: asset }, { from: asset, to: "USDC" });
1718
- }
1719
- for (const a of STABLE_ASSETS) {
1720
- for (const b of STABLE_ASSETS) {
1721
- if (a !== b) pairs.push({ from: a, to: b });
1722
- }
1723
- }
1724
- return pairs;
1725
- }
1726
- async getPoolPrice() {
1727
- return getPoolPrice(this.client);
1728
- }
1729
- async addSwapToTx(tx, address, inputCoin, from, to, amount, maxSlippageBps) {
1730
- return addSwapToTx({
1731
- tx,
1732
- client: this.client,
1733
- address,
1734
- inputCoin,
1735
- fromAsset: from,
1736
- toAsset: to,
1737
- amount,
1738
- maxSlippageBps
1739
- });
1740
- }
1741
- };
1742
- SUPPORTED_ASSETS.USDC.type;
1743
- var WAD = 1e18;
1744
- var MIN_HEALTH_FACTOR2 = 1.5;
1745
- var CLOCK2 = "0x6";
1746
- var LENDING_MARKET_ID = "0x84030d26d85eaa7035084a057f2f11f701b7e2e4eda87551becbc7c97505ece1";
1747
- var LENDING_MARKET_TYPE = "0xf95b06141ed4a174f239417323bde3f209b972f5930d8521ea38a52aff3a6ddf::suilend::MAIN_POOL";
1748
- var SUILEND_PACKAGE = "0xf95b06141ed4a174f239417323bde3f209b972f5930d8521ea38a52aff3a6ddf";
1749
- var UPGRADE_CAP_ID = "0x3d4ef1859c3ee9fc72858f588b56a09da5466e64f8cc4e90a7b3b909fba8a7ae";
1750
- var FALLBACK_PUBLISHED_AT = "0x3d4353f3bd3565329655e6b77bc2abfd31e558b86662ebd078ae453d416bc10f";
1751
- var descriptor4 = {
1752
- id: "suilend",
1753
- name: "Suilend",
1754
- packages: [SUILEND_PACKAGE],
1755
- actionMap: {
1756
- "lending_market::deposit_liquidity_and_mint_ctokens": "save",
1757
- "lending_market::deposit_ctokens_into_obligation": "save",
1758
- "lending_market::create_obligation": "save",
1759
- "lending_market::withdraw_ctokens": "withdraw",
1760
- "lending_market::redeem_ctokens_and_withdraw_liquidity": "withdraw",
1761
- "lending_market::borrow": "borrow",
1762
- "lending_market::repay": "repay"
1763
- }
1764
- };
1765
- function interpolateRate(utilBreakpoints, aprBreakpoints, utilizationPct) {
1766
- if (utilBreakpoints.length === 0) return 0;
1767
- if (utilizationPct <= utilBreakpoints[0]) return aprBreakpoints[0];
1768
- if (utilizationPct >= utilBreakpoints[utilBreakpoints.length - 1]) {
1769
- return aprBreakpoints[aprBreakpoints.length - 1];
1770
- }
1771
- for (let i = 1; i < utilBreakpoints.length; i++) {
1772
- if (utilizationPct <= utilBreakpoints[i]) {
1773
- const t = (utilizationPct - utilBreakpoints[i - 1]) / (utilBreakpoints[i] - utilBreakpoints[i - 1]);
1774
- return aprBreakpoints[i - 1] + t * (aprBreakpoints[i] - aprBreakpoints[i - 1]);
1775
- }
1776
- }
1777
- return aprBreakpoints[aprBreakpoints.length - 1];
1778
- }
1779
- function computeRates(reserve) {
1780
- const available = reserve.availableAmount / 10 ** reserve.mintDecimals;
1781
- const borrowed = reserve.borrowedAmountWad / WAD / 10 ** reserve.mintDecimals;
1782
- const totalDeposited = available + borrowed;
1783
- const utilizationPct = totalDeposited > 0 ? borrowed / totalDeposited * 100 : 0;
1784
- if (reserve.interestRateUtils.length === 0) return { borrowAprPct: 0, depositAprPct: 0 };
1785
- const aprs = reserve.interestRateAprs.map((a) => a / 100);
1786
- const borrowAprPct = interpolateRate(reserve.interestRateUtils, aprs, utilizationPct);
1787
- const depositAprPct = utilizationPct / 100 * (borrowAprPct / 100) * (1 - reserve.spreadFeeBps / 1e4) * 100;
1788
- return { borrowAprPct, depositAprPct };
1789
- }
1790
- function cTokenRatio(reserve) {
1791
- if (reserve.ctokenSupply === 0) return 1;
1792
- const totalSupply = reserve.availableAmount + reserve.borrowedAmountWad / WAD - reserve.unclaimedSpreadFeesWad / WAD;
1793
- return totalSupply / reserve.ctokenSupply;
1794
- }
1795
- function f(obj) {
1796
- if (obj && typeof obj === "object" && "fields" in obj) return obj.fields;
1797
- return obj;
1798
- }
1799
- function str(v) {
1800
- return String(v ?? "0");
1801
- }
1802
- function num(v) {
1803
- return Number(str(v));
1804
- }
1805
- function parseReserve(raw, index) {
1806
- const r = f(raw);
1807
- const coinTypeField = f(r.coin_type);
1808
- const config = f(f(r.config)?.element);
1809
- return {
1810
- coinType: str(coinTypeField?.name),
1811
- mintDecimals: num(r.mint_decimals),
1812
- availableAmount: num(r.available_amount),
1813
- borrowedAmountWad: num(f(r.borrowed_amount)?.value),
1814
- ctokenSupply: num(r.ctoken_supply),
1815
- unclaimedSpreadFeesWad: num(f(r.unclaimed_spread_fees)?.value),
1816
- cumulativeBorrowRateWad: num(f(r.cumulative_borrow_rate)?.value),
1817
- openLtvPct: num(config?.open_ltv_pct),
1818
- closeLtvPct: num(config?.close_ltv_pct),
1819
- spreadFeeBps: num(config?.spread_fee_bps),
1820
- interestRateUtils: Array.isArray(config?.interest_rate_utils) ? config.interest_rate_utils.map(num) : [],
1821
- interestRateAprs: Array.isArray(config?.interest_rate_aprs) ? config.interest_rate_aprs.map(num) : [],
1822
- arrayIndex: index
1823
- };
1824
- }
1825
- function parseObligation(raw) {
1826
- const deposits = Array.isArray(raw.deposits) ? raw.deposits.map((d) => {
1827
- const df = f(d);
1828
- return {
1829
- coinType: str(f(df.coin_type)?.name),
1830
- ctokenAmount: num(df.deposited_ctoken_amount),
1831
- reserveIdx: num(df.reserve_array_index)
1832
- };
1833
- }) : [];
1834
- const borrows = Array.isArray(raw.borrows) ? raw.borrows.map((b) => {
1835
- const bf = f(b);
1836
- return {
1837
- coinType: str(f(bf.coin_type)?.name),
1838
- borrowedWad: num(f(bf.borrowed_amount)?.value),
1839
- cumBorrowRateWad: num(f(bf.cumulative_borrow_rate)?.value),
1840
- reserveIdx: num(bf.reserve_array_index)
1841
- };
1842
- }) : [];
1843
- return { deposits, borrows };
1844
- }
1845
- var SuilendAdapter = class {
1846
- id = "suilend";
1847
- name = "Suilend";
1848
- version = "2.0.0";
1849
- capabilities = ["save", "withdraw", "borrow", "repay"];
1850
- supportedAssets = [...STABLE_ASSETS];
1851
- supportsSameAssetBorrow = false;
1852
- client;
1853
- publishedAt = null;
1854
- reserveCache = null;
1855
- async init(client) {
1856
- this.client = client;
1857
- }
1858
- initSync(client) {
1859
- this.client = client;
1860
- }
1861
- // -- On-chain reads -------------------------------------------------------
1862
- async resolvePackage() {
1863
- if (this.publishedAt) return this.publishedAt;
1864
- try {
1865
- const cap = await this.client.getObject({ id: UPGRADE_CAP_ID, options: { showContent: true } });
1866
- if (cap.data?.content?.dataType === "moveObject") {
1867
- const fields = cap.data.content.fields;
1868
- this.publishedAt = str(fields.package);
1869
- return this.publishedAt;
1870
- }
1871
- } catch {
1872
- }
1873
- this.publishedAt = FALLBACK_PUBLISHED_AT;
1874
- return this.publishedAt;
1875
- }
1876
- async loadReserves(fresh = false) {
1877
- if (this.reserveCache && !fresh) return this.reserveCache;
1878
- const market = await this.client.getObject({
1879
- id: LENDING_MARKET_ID,
1880
- options: { showContent: true }
1881
- });
1882
- if (market.data?.content?.dataType !== "moveObject") {
1883
- throw new T2000Error("PROTOCOL_UNAVAILABLE", "Failed to read Suilend lending market");
1884
- }
1885
- const fields = market.data.content.fields;
1886
- const reservesRaw = fields.reserves;
1887
- if (!Array.isArray(reservesRaw)) {
1888
- throw new T2000Error("PROTOCOL_UNAVAILABLE", "Failed to parse Suilend reserves");
1889
- }
1890
- this.reserveCache = reservesRaw.map((r, i) => parseReserve(r, i));
1891
- return this.reserveCache;
1892
- }
1893
- findReserve(reserves, asset) {
1894
- let coinType;
1895
- if (asset in SUPPORTED_ASSETS) {
1896
- coinType = SUPPORTED_ASSETS[asset].type;
1897
- } else if (asset.includes("::")) {
1898
- coinType = asset;
1899
- } else {
1900
- return void 0;
1901
- }
1902
- try {
1903
- const normalized = utils.normalizeStructTag(coinType);
1904
- return reserves.find((r) => {
1905
- try {
1906
- return utils.normalizeStructTag(r.coinType) === normalized;
1907
- } catch {
1908
- return false;
1909
- }
1910
- });
1911
- } catch {
1912
- return void 0;
1913
- }
1914
- }
1915
- async fetchObligationCaps(address) {
1916
- const capType = `${SUILEND_PACKAGE}::lending_market::ObligationOwnerCap<${LENDING_MARKET_TYPE}>`;
1917
- const caps = [];
1918
- let cursor;
1919
- let hasNext = true;
1920
- while (hasNext) {
1921
- const page = await this.client.getOwnedObjects({
1922
- owner: address,
1923
- filter: { StructType: capType },
1924
- options: { showContent: true },
1925
- cursor: cursor ?? void 0
1926
- });
1927
- for (const item of page.data) {
1928
- if (item.data?.content?.dataType !== "moveObject") continue;
1929
- const fields = item.data.content.fields;
1930
- caps.push({
1931
- id: item.data.objectId,
1932
- obligationId: str(fields.obligation_id)
1933
- });
1934
- }
1935
- cursor = page.nextCursor;
1936
- hasNext = page.hasNextPage;
1937
- }
1938
- return caps;
1939
- }
1940
- async fetchObligation(obligationId) {
1941
- const obj = await this.client.getObject({ id: obligationId, options: { showContent: true } });
1942
- if (obj.data?.content?.dataType !== "moveObject") {
1943
- throw new T2000Error("PROTOCOL_UNAVAILABLE", "Failed to read Suilend obligation");
1944
- }
1945
- return parseObligation(obj.data.content.fields);
1946
- }
1947
- resolveSymbol(coinType) {
1948
- try {
1949
- const normalized = utils.normalizeStructTag(coinType);
1950
- for (const [key, info] of Object.entries(SUPPORTED_ASSETS)) {
1951
- try {
1952
- if (utils.normalizeStructTag(info.type) === normalized) return key;
1953
- } catch {
1954
- }
1955
- }
1956
- } catch {
1957
- }
1958
- const parts = coinType.split("::");
1959
- return parts[parts.length - 1] || "UNKNOWN";
1960
- }
1961
- // -- Adapter interface ----------------------------------------------------
1962
- async getRates(asset) {
1963
- const reserves = await this.loadReserves();
1964
- const reserve = this.findReserve(reserves, asset);
1965
- if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `Suilend does not support ${asset}`);
1966
- const { borrowAprPct, depositAprPct } = computeRates(reserve);
1967
- return { asset, saveApy: depositAprPct, borrowApy: borrowAprPct };
1968
- }
1969
- async getPositions(address) {
1970
- const supplies = [];
1971
- const borrows = [];
1972
- const caps = await this.fetchObligationCaps(address);
1973
- if (caps.length === 0) return { supplies, borrows };
1974
- const [reserves, obligation] = await Promise.all([
1975
- this.loadReserves(),
1976
- this.fetchObligation(caps[0].obligationId)
1977
- ]);
1978
- for (const dep of obligation.deposits) {
1979
- const reserve = reserves[dep.reserveIdx];
1980
- if (!reserve) continue;
1981
- const ratio = cTokenRatio(reserve);
1982
- const amount = dep.ctokenAmount * ratio / 10 ** reserve.mintDecimals;
1983
- const { depositAprPct } = computeRates(reserve);
1984
- supplies.push({ asset: this.resolveSymbol(dep.coinType), amount, apy: depositAprPct });
1985
- }
1986
- for (const bor of obligation.borrows) {
1987
- const reserve = reserves[bor.reserveIdx];
1988
- if (!reserve) continue;
1989
- const rawAmount = bor.borrowedWad / WAD / 10 ** reserve.mintDecimals;
1990
- const reserveRate = reserve.cumulativeBorrowRateWad / WAD;
1991
- const posRate = bor.cumBorrowRateWad / WAD;
1992
- const compounded = posRate > 0 ? rawAmount * (reserveRate / posRate) : rawAmount;
1993
- const { borrowAprPct } = computeRates(reserve);
1994
- borrows.push({ asset: this.resolveSymbol(bor.coinType), amount: compounded, apy: borrowAprPct });
1995
- }
1996
- return { supplies, borrows };
1997
- }
1998
- async getHealth(address) {
1999
- const caps = await this.fetchObligationCaps(address);
2000
- if (caps.length === 0) {
2001
- return { healthFactor: Infinity, supplied: 0, borrowed: 0, maxBorrow: 0, liquidationThreshold: 0 };
2002
- }
2003
- const [reserves, obligation] = await Promise.all([
2004
- this.loadReserves(),
2005
- this.fetchObligation(caps[0].obligationId)
2006
- ]);
2007
- let supplied = 0;
2008
- let borrowed = 0;
2009
- let weightedCloseLtv = 0;
2010
- let weightedOpenLtv = 0;
2011
- for (const dep of obligation.deposits) {
2012
- const reserve = reserves[dep.reserveIdx];
2013
- if (!reserve) continue;
2014
- const ratio = cTokenRatio(reserve);
2015
- const amount = dep.ctokenAmount * ratio / 10 ** reserve.mintDecimals;
2016
- supplied += amount;
2017
- weightedCloseLtv += amount * (reserve.closeLtvPct / 100);
2018
- weightedOpenLtv += amount * (reserve.openLtvPct / 100);
2019
- }
2020
- for (const bor of obligation.borrows) {
2021
- const reserve = reserves[bor.reserveIdx];
2022
- if (!reserve) continue;
2023
- const rawAmount = bor.borrowedWad / WAD / 10 ** reserve.mintDecimals;
2024
- const reserveRate = reserve.cumulativeBorrowRateWad / WAD;
2025
- const posRate = bor.cumBorrowRateWad / WAD;
2026
- borrowed += posRate > 0 ? rawAmount * (reserveRate / posRate) : rawAmount;
2027
- }
2028
- const liqThreshold = supplied > 0 ? weightedCloseLtv / supplied : 0.75;
2029
- const openLtv = supplied > 0 ? weightedOpenLtv / supplied : 0.7;
2030
- const healthFactor = borrowed > 0 ? supplied * liqThreshold / borrowed : Infinity;
2031
- const maxBorrow = Math.max(0, supplied * openLtv - borrowed);
2032
- return { healthFactor, supplied, borrowed, maxBorrow, liquidationThreshold: liqThreshold };
2033
- }
2034
- async buildSaveTx(address, amount, asset, options) {
2035
- const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
2036
- const assetInfo = SUPPORTED_ASSETS[assetKey];
2037
- const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
2038
- const reserve = this.findReserve(reserves, assetKey);
2039
- if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend. Try: NAVI or a different asset.`);
2040
- const caps = await this.fetchObligationCaps(address);
2041
- const tx = new transactions.Transaction();
2042
- tx.setSender(address);
2043
- let capRef;
2044
- if (caps.length === 0) {
2045
- const [newCap] = tx.moveCall({
2046
- target: `${pkg}::lending_market::create_obligation`,
2047
- typeArguments: [LENDING_MARKET_TYPE],
2048
- arguments: [tx.object(LENDING_MARKET_ID)]
2049
- });
2050
- capRef = newCap;
2051
- } else {
2052
- capRef = caps[0].id;
2053
- }
2054
- const allCoins = await this.fetchAllCoins(address, assetInfo.type);
2055
- if (allCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins found`);
2056
- const primaryCoinId = allCoins[0].coinObjectId;
2057
- if (allCoins.length > 1) {
2058
- tx.mergeCoins(tx.object(primaryCoinId), allCoins.slice(1).map((c) => tx.object(c.coinObjectId)));
2059
- }
2060
- const rawAmount = stableToRaw(amount, assetInfo.decimals).toString();
2061
- const [depositCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawAmount]);
2062
- if (options?.collectFee) {
2063
- addCollectFeeToTx(tx, depositCoin, "save");
2064
- }
2065
- const [ctokens] = tx.moveCall({
2066
- target: `${pkg}::lending_market::deposit_liquidity_and_mint_ctokens`,
2067
- typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
2068
- arguments: [
2069
- tx.object(LENDING_MARKET_ID),
2070
- tx.pure.u64(reserve.arrayIndex),
2071
- tx.object(CLOCK2),
2072
- depositCoin
2073
- ]
2074
- });
2075
- tx.moveCall({
2076
- target: `${pkg}::lending_market::deposit_ctokens_into_obligation`,
2077
- typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
2078
- arguments: [
2079
- tx.object(LENDING_MARKET_ID),
2080
- tx.pure.u64(reserve.arrayIndex),
2081
- typeof capRef === "string" ? tx.object(capRef) : capRef,
2082
- tx.object(CLOCK2),
2083
- ctokens
2084
- ]
2085
- });
2086
- if (typeof capRef !== "string") {
2087
- tx.transferObjects([capRef], address);
2088
- }
2089
- return { tx };
2090
- }
2091
- async buildWithdrawTx(address, amount, asset) {
2092
- const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
2093
- const assetInfo = SUPPORTED_ASSETS[assetKey];
2094
- const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves(true)]);
2095
- const reserve = this.findReserve(reserves, assetKey);
2096
- if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
2097
- const caps = await this.fetchObligationCaps(address);
2098
- if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found");
2099
- const positions = await this.getPositions(address);
2100
- const deposited = positions.supplies.find((s) => s.asset === assetKey)?.amount ?? 0;
2101
- const effectiveAmount = Math.min(amount, deposited);
2102
- if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on Suilend`);
2103
- const ratio = cTokenRatio(reserve);
2104
- const ctokenAmount = Math.ceil(effectiveAmount * 10 ** reserve.mintDecimals / ratio);
2105
- const tx = new transactions.Transaction();
2106
- tx.setSender(address);
2107
- const [ctokens] = tx.moveCall({
2108
- target: `${pkg}::lending_market::withdraw_ctokens`,
2109
- typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
2110
- arguments: [
2111
- tx.object(LENDING_MARKET_ID),
2112
- tx.pure.u64(reserve.arrayIndex),
2113
- tx.object(caps[0].id),
2114
- tx.object(CLOCK2),
2115
- tx.pure.u64(ctokenAmount)
2116
- ]
2117
- });
2118
- const exemptionType = `${SUILEND_PACKAGE}::lending_market::RateLimiterExemption<${LENDING_MARKET_TYPE}, ${assetInfo.type}>`;
2119
- const [none] = tx.moveCall({
2120
- target: "0x1::option::none",
2121
- typeArguments: [exemptionType]
2122
- });
2123
- const [coin] = tx.moveCall({
2124
- target: `${pkg}::lending_market::redeem_ctokens_and_withdraw_liquidity`,
2125
- typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
2126
- arguments: [
2127
- tx.object(LENDING_MARKET_ID),
2128
- tx.pure.u64(reserve.arrayIndex),
2129
- tx.object(CLOCK2),
2130
- ctokens,
2131
- none
2132
- ]
2133
- });
2134
- tx.transferObjects([coin], address);
2135
- return { tx, effectiveAmount };
2136
- }
2137
- async addWithdrawToTx(tx, address, amount, asset) {
2138
- const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
2139
- const assetInfo = SUPPORTED_ASSETS[assetKey];
2140
- const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves(true)]);
2141
- const reserve = this.findReserve(reserves, assetKey);
2142
- if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
2143
- const caps = await this.fetchObligationCaps(address);
2144
- if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found");
2145
- const positions = await this.getPositions(address);
2146
- const deposited = positions.supplies.find((s) => s.asset === assetKey)?.amount ?? 0;
2147
- const effectiveAmount = Math.min(amount, deposited);
2148
- if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on Suilend`);
2149
- const ratio = cTokenRatio(reserve);
2150
- const ctokenAmount = Math.ceil(effectiveAmount * 10 ** reserve.mintDecimals / ratio);
2151
- const [ctokens] = tx.moveCall({
2152
- target: `${pkg}::lending_market::withdraw_ctokens`,
2153
- typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
2154
- arguments: [
2155
- tx.object(LENDING_MARKET_ID),
2156
- tx.pure.u64(reserve.arrayIndex),
2157
- tx.object(caps[0].id),
2158
- tx.object(CLOCK2),
2159
- tx.pure.u64(ctokenAmount)
2160
- ]
2161
- });
2162
- const exemptionType = `${SUILEND_PACKAGE}::lending_market::RateLimiterExemption<${LENDING_MARKET_TYPE}, ${assetInfo.type}>`;
2163
- const [none] = tx.moveCall({
2164
- target: "0x1::option::none",
2165
- typeArguments: [exemptionType]
2166
- });
2167
- const [coin] = tx.moveCall({
2168
- target: `${pkg}::lending_market::redeem_ctokens_and_withdraw_liquidity`,
2169
- typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
2170
- arguments: [
2171
- tx.object(LENDING_MARKET_ID),
2172
- tx.pure.u64(reserve.arrayIndex),
2173
- tx.object(CLOCK2),
2174
- ctokens,
2175
- none
2176
- ]
2177
- });
2178
- return { coin, effectiveAmount };
2179
- }
2180
- async addSaveToTx(tx, address, coin, asset, options) {
2181
- const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
2182
- const assetInfo = SUPPORTED_ASSETS[assetKey];
2183
- const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
2184
- const reserve = this.findReserve(reserves, assetKey);
2185
- if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
2186
- const caps = await this.fetchObligationCaps(address);
2187
- let capRef;
2188
- if (caps.length === 0) {
2189
- const [newCap] = tx.moveCall({
2190
- target: `${pkg}::lending_market::create_obligation`,
2191
- typeArguments: [LENDING_MARKET_TYPE],
2192
- arguments: [tx.object(LENDING_MARKET_ID)]
2193
- });
2194
- capRef = newCap;
2195
- } else {
2196
- capRef = caps[0].id;
2197
- }
2198
- if (options?.collectFee) {
2199
- addCollectFeeToTx(tx, coin, "save");
2200
- }
2201
- const [ctokens] = tx.moveCall({
2202
- target: `${pkg}::lending_market::deposit_liquidity_and_mint_ctokens`,
2203
- typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
2204
- arguments: [
2205
- tx.object(LENDING_MARKET_ID),
2206
- tx.pure.u64(reserve.arrayIndex),
2207
- tx.object(CLOCK2),
2208
- coin
2209
- ]
2210
- });
2211
- tx.moveCall({
2212
- target: `${pkg}::lending_market::deposit_ctokens_into_obligation`,
2213
- typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
2214
- arguments: [
2215
- tx.object(LENDING_MARKET_ID),
2216
- tx.pure.u64(reserve.arrayIndex),
2217
- typeof capRef === "string" ? tx.object(capRef) : capRef,
2218
- tx.object(CLOCK2),
2219
- ctokens
2220
- ]
2221
- });
2222
- if (typeof capRef !== "string") {
2223
- tx.transferObjects([capRef], address);
2224
- }
2225
- }
2226
- async buildBorrowTx(address, amount, asset, options) {
2227
- const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
2228
- const assetInfo = SUPPORTED_ASSETS[assetKey];
2229
- const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
2230
- const reserve = this.findReserve(reserves, assetKey);
2231
- if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend. Try: NAVI or a different asset.`);
2232
- const caps = await this.fetchObligationCaps(address);
2233
- if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found. Deposit collateral first with: t2000 save <amount>");
2234
- const rawAmount = stableToRaw(amount, assetInfo.decimals);
2235
- const tx = new transactions.Transaction();
2236
- tx.setSender(address);
2237
- const [coin] = tx.moveCall({
2238
- target: `${pkg}::lending_market::borrow`,
2239
- typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
2240
- arguments: [
2241
- tx.object(LENDING_MARKET_ID),
2242
- tx.pure.u64(reserve.arrayIndex),
2243
- tx.object(caps[0].id),
2244
- tx.object(CLOCK2),
2245
- tx.pure.u64(rawAmount)
2246
- ]
2247
- });
2248
- if (options?.collectFee) {
2249
- addCollectFeeToTx(tx, coin, "borrow");
2250
- }
2251
- tx.transferObjects([coin], address);
2252
- return { tx };
2253
- }
2254
- async buildRepayTx(address, amount, asset) {
2255
- const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
2256
- const assetInfo = SUPPORTED_ASSETS[assetKey];
2257
- const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
2258
- const reserve = this.findReserve(reserves, assetKey);
2259
- if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
2260
- const caps = await this.fetchObligationCaps(address);
2261
- if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend obligation found");
2262
- const allCoins = await this.fetchAllCoins(address, assetInfo.type);
2263
- if (allCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins to repay with`);
2264
- const rawAmount = stableToRaw(amount, assetInfo.decimals);
2265
- const tx = new transactions.Transaction();
2266
- tx.setSender(address);
2267
- const primaryCoinId = allCoins[0].coinObjectId;
2268
- if (allCoins.length > 1) {
2269
- tx.mergeCoins(tx.object(primaryCoinId), allCoins.slice(1).map((c) => tx.object(c.coinObjectId)));
2270
- }
2271
- const [repayCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawAmount.toString()]);
2272
- tx.moveCall({
2273
- target: `${pkg}::lending_market::repay`,
2274
- typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
2275
- arguments: [
2276
- tx.object(LENDING_MARKET_ID),
2277
- tx.pure.u64(reserve.arrayIndex),
2278
- tx.object(caps[0].id),
2279
- tx.object(CLOCK2),
2280
- repayCoin
2281
- ]
2282
- });
2283
- return { tx };
2284
- }
2285
- async addRepayToTx(tx, address, coin, asset) {
2286
- const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
2287
- const assetInfo = SUPPORTED_ASSETS[assetKey];
2288
- const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
2289
- const reserve = this.findReserve(reserves, assetKey);
2290
- if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
2291
- const caps = await this.fetchObligationCaps(address);
2292
- if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend obligation found");
2293
- tx.moveCall({
2294
- target: `${pkg}::lending_market::repay`,
2295
- typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
2296
- arguments: [
2297
- tx.object(LENDING_MARKET_ID),
2298
- tx.pure.u64(reserve.arrayIndex),
2299
- tx.object(caps[0].id),
2300
- tx.object(CLOCK2),
2301
- coin
2302
- ]
2303
- });
2304
- }
2305
- async maxWithdraw(address, _asset) {
2306
- const health = await this.getHealth(address);
2307
- let maxAmount;
2308
- if (health.borrowed === 0) {
2309
- maxAmount = health.supplied;
2310
- } else {
2311
- maxAmount = Math.max(0, health.supplied - health.borrowed * MIN_HEALTH_FACTOR2 / health.liquidationThreshold);
2312
- }
2313
- const remainingSupply = health.supplied - maxAmount;
2314
- const hfAfter = health.borrowed > 0 ? remainingSupply * health.liquidationThreshold / health.borrowed : Infinity;
2315
- return { maxAmount, healthFactorAfter: hfAfter, currentHF: health.healthFactor };
2316
- }
2317
- async maxBorrow(address, _asset) {
2318
- const health = await this.getHealth(address);
2319
- const maxAmount = health.maxBorrow;
2320
- return { maxAmount, healthFactorAfter: MIN_HEALTH_FACTOR2, currentHF: health.healthFactor };
2321
- }
2322
- async fetchAllCoins(owner, coinType) {
2323
- const all = [];
2324
- let cursor = null;
2325
- let hasNext = true;
2326
- while (hasNext) {
2327
- const page = await this.client.getCoins({ owner, coinType, cursor: cursor ?? void 0 });
2328
- all.push(...page.data.map((c) => ({ coinObjectId: c.coinObjectId, balance: c.balance })));
2329
- cursor = page.nextCursor;
2330
- hasNext = page.hasNextPage;
2331
- }
2332
- return all;
2333
- }
2334
- };
2335
- function hasLeadingZeroBits(hash, bits) {
2336
- const fullBytes = Math.floor(bits / 8);
2337
- const remainingBits = bits % 8;
2338
- for (let i = 0; i < fullBytes; i++) {
2339
- if (hash[i] !== 0) return false;
2340
- }
2341
- if (remainingBits > 0) {
2342
- const mask = 255 << 8 - remainingBits;
2343
- if ((hash[fullBytes] & mask) !== 0) return false;
2344
- }
2345
- return true;
2346
- }
2347
- function solveHashcash(challenge) {
2348
- const bits = parseInt(challenge.split(":")[1], 10);
2349
- let counter = 0;
2350
- while (true) {
2351
- const stamp = `${challenge}${counter.toString(16)}`;
2352
- const hash = crypto.createHash("sha256").update(stamp).digest();
2353
- if (hasLeadingZeroBits(hash, bits)) return stamp;
2354
- counter++;
2355
- }
2356
- }
2357
-
2358
- // src/gas/gasStation.ts
2359
- async function requestGasSponsorship(txJson, sender, type) {
2360
- const txBytes = Buffer.from(txJson).toString("base64");
2361
- const res = await fetch(`${API_BASE_URL}/api/gas`, {
2362
- method: "POST",
2363
- headers: { "Content-Type": "application/json" },
2364
- body: JSON.stringify({ txJson, txBytes, sender, type })
2365
- });
2366
- const data = await res.json();
2367
- if (!res.ok) {
2368
- const errorCode = data.error;
2369
- if (errorCode === "CIRCUIT_BREAKER" || errorCode === "POOL_DEPLETED") {
2370
- throw new T2000Error(
2371
- "GAS_STATION_UNAVAILABLE",
2372
- data.message ?? "Gas station temporarily unavailable",
2373
- { retryAfter: data.retryAfter },
2374
- true
2375
- );
2376
- }
2377
- if (errorCode === "GAS_FEE_EXCEEDED") {
2378
- throw new T2000Error(
2379
- "GAS_FEE_EXCEEDED",
2380
- data.message ?? "Gas fee exceeds ceiling",
2381
- { retryAfter: data.retryAfter },
2382
- true
2383
- );
2384
- }
2385
- throw new T2000Error(
2386
- "GAS_STATION_UNAVAILABLE",
2387
- data.message ?? "Gas sponsorship request failed",
2388
- void 0,
2389
- true
2390
- );
2391
- }
2392
- return data;
2393
- }
2394
- async function reportGasUsage(sender, txDigest, gasCostSui, usdcCharged, type) {
2395
- try {
2396
- await fetch(`${API_BASE_URL}/api/gas/report`, {
2397
- method: "POST",
2398
- headers: { "Content-Type": "application/json" },
2399
- body: JSON.stringify({ sender, txDigest, gasCostSui, usdcCharged, type })
2400
- });
2401
- } catch {
2402
- }
2403
- }
2404
- async function getGasStatus(address) {
2405
- const url = new URL(`${API_BASE_URL}/api/gas/status`);
2406
- if (address) url.searchParams.set("address", address);
2407
- const res = await fetch(url.toString());
2408
- if (!res.ok) {
2409
- throw new T2000Error("GAS_STATION_UNAVAILABLE", "Failed to fetch gas status", void 0, true);
2410
- }
2411
- return await res.json();
2412
- }
2413
-
2414
- // src/gas/autoTopUp.ts
2415
- async function shouldAutoTopUp(client, address) {
2416
- const [suiBalance, usdcBalance] = await Promise.all([
2417
- client.getBalance({ owner: address, coinType: SUPPORTED_ASSETS.SUI.type }),
2418
- client.getBalance({ owner: address, coinType: SUPPORTED_ASSETS.USDC.type })
2419
- ]);
2420
- const suiRaw = BigInt(suiBalance.totalBalance);
2421
- const usdcRaw = BigInt(usdcBalance.totalBalance);
2422
- return suiRaw < AUTO_TOPUP_THRESHOLD && usdcRaw >= AUTO_TOPUP_MIN_USDC;
2423
- }
2424
- async function executeAutoTopUp(client, keypair) {
2425
- const address = keypair.getPublicKey().toSuiAddress();
2426
- const topupAmountHuman = Number(AUTO_TOPUP_AMOUNT) / 1e6;
2427
- const { tx } = await buildSwapTx({
2428
- client,
2429
- address,
2430
- fromAsset: "USDC",
2431
- toAsset: "SUI",
2432
- amount: topupAmountHuman
2433
- });
2434
- const txJson = tx.serialize();
2435
- const sponsoredResult = await requestGasSponsorship(txJson, address, "auto-topup");
2436
- const sponsoredTxBytes = Buffer.from(sponsoredResult.txBytes, "base64");
2437
- const { signature: agentSig } = await keypair.signTransaction(sponsoredTxBytes);
2438
- const result = await client.executeTransactionBlock({
2439
- transactionBlock: sponsoredResult.txBytes,
2440
- signature: [agentSig, sponsoredResult.sponsorSignature],
2441
- options: { showEffects: true, showBalanceChanges: true }
2442
- });
2443
- await client.waitForTransaction({ digest: result.digest });
2444
- let suiReceived = 0;
2445
- if (result.balanceChanges) {
2446
- for (const change of result.balanceChanges) {
2447
- if (change.coinType === SUPPORTED_ASSETS.SUI.type && change.owner && typeof change.owner === "object" && "AddressOwner" in change.owner && change.owner.AddressOwner === address) {
2448
- suiReceived += Number(change.amount) / Number(MIST_PER_SUI);
2449
- }
2450
- }
2451
- }
2452
- reportGasUsage(address, result.digest, 0, 0, "auto-topup");
2453
- return {
2454
- success: true,
2455
- tx: result.digest,
2456
- usdcSpent: topupAmountHuman,
2457
- suiReceived: Math.abs(suiReceived)
2458
- };
2459
- }
2460
-
2461
- // src/gas/manager.ts
2462
- function extractGasCost(effects) {
2463
- if (!effects?.gasUsed) return 0;
2464
- return (Number(effects.gasUsed.computationCost) + Number(effects.gasUsed.storageCost) - Number(effects.gasUsed.storageRebate)) / 1e9;
2465
- }
2466
- async function getSuiBalance(client, address) {
2467
- const bal = await client.getBalance({ owner: address, coinType: SUPPORTED_ASSETS.SUI.type });
2468
- return BigInt(bal.totalBalance);
2469
- }
2470
- async function trySelfFunded(client, keypair, tx) {
2471
- const address = keypair.getPublicKey().toSuiAddress();
2472
- const suiBalance = await getSuiBalance(client, address);
2473
- if (suiBalance < AUTO_TOPUP_THRESHOLD) return null;
2474
- tx.setSender(address);
2475
- const result = await client.signAndExecuteTransaction({
2476
- signer: keypair,
2477
- transaction: tx,
2478
- options: { showEffects: true }
2479
- });
2480
- await client.waitForTransaction({ digest: result.digest });
2481
- return {
2482
- digest: result.digest,
2483
- effects: result.effects,
2484
- gasMethod: "self-funded",
2485
- gasCostSui: extractGasCost(result.effects)
2486
- };
2487
- }
2488
- async function tryAutoTopUpThenSelfFund(client, keypair, tx) {
2489
- const address = keypair.getPublicKey().toSuiAddress();
2490
- const canTopUp = await shouldAutoTopUp(client, address);
2491
- if (!canTopUp) return null;
2492
- await executeAutoTopUp(client, keypair);
2493
- tx.setSender(address);
2494
- const result = await client.signAndExecuteTransaction({
2495
- signer: keypair,
2496
- transaction: tx,
2497
- options: { showEffects: true }
2498
- });
2499
- await client.waitForTransaction({ digest: result.digest });
2500
- return {
2501
- digest: result.digest,
2502
- effects: result.effects,
2503
- gasMethod: "auto-topup",
2504
- gasCostSui: extractGasCost(result.effects)
2505
- };
2506
- }
2507
- async function trySponsored(client, keypair, tx) {
2508
- const address = keypair.getPublicKey().toSuiAddress();
2509
- tx.setSender(address);
2510
- const txJson = tx.serialize();
2511
- const sponsoredResult = await requestGasSponsorship(txJson, address);
2512
- const sponsoredTxBytes = Buffer.from(sponsoredResult.txBytes, "base64");
2513
- const { signature: agentSig } = await keypair.signTransaction(sponsoredTxBytes);
2514
- const result = await client.executeTransactionBlock({
2515
- transactionBlock: sponsoredResult.txBytes,
2516
- signature: [agentSig, sponsoredResult.sponsorSignature],
2517
- options: { showEffects: true }
2518
- });
2519
- await client.waitForTransaction({ digest: result.digest });
2520
- const gasCost = extractGasCost(result.effects);
2521
- reportGasUsage(address, result.digest, gasCost, 0, sponsoredResult.type);
2522
- return {
2523
- digest: result.digest,
2524
- effects: result.effects,
2525
- gasMethod: "sponsored",
2526
- gasCostSui: gasCost
2527
- };
2528
- }
2529
- async function executeWithGas(client, keypair, buildTx, options) {
2530
- if (options?.enforcer && options?.metadata) {
2531
- options.enforcer.check(options.metadata);
2532
- }
2533
- const errors = [];
2534
- try {
2535
- const tx = await buildTx();
2536
- const result = await trySelfFunded(client, keypair, tx);
2537
- if (result) return result;
2538
- errors.push("self-funded: SUI below threshold");
2539
- } catch (err) {
2540
- const msg = err instanceof Error ? err.message : String(err);
2541
- if (isMoveAbort(msg)) {
2542
- throw new T2000Error("TRANSACTION_FAILED", parseMoveAbortMessage(msg));
2543
- }
2544
- errors.push(`self-funded: ${msg}`);
2545
- }
2546
- try {
2547
- const tx = await buildTx();
2548
- const result = await tryAutoTopUpThenSelfFund(client, keypair, tx);
2549
- if (result) return result;
2550
- errors.push("auto-topup: not eligible (low USDC or sufficient SUI)");
2551
- } catch (err) {
2552
- errors.push(`auto-topup: ${err instanceof Error ? err.message : String(err)}`);
2553
- }
2554
- try {
2555
- const tx = await buildTx();
2556
- const result = await trySponsored(client, keypair, tx);
2557
- if (result) return result;
2558
- errors.push("sponsored: returned null");
2559
- } catch (err) {
2560
- errors.push(`sponsored: ${err instanceof Error ? err.message : String(err)}`);
2561
- }
2562
- throw new T2000Error(
2563
- "INSUFFICIENT_GAS",
2564
- `No SUI for gas and Gas Station unavailable. Fund your wallet with SUI or USDC. [${errors.join(" | ")}]`,
2565
- { reason: "all_gas_methods_exhausted", errors }
2566
- );
2567
- }
2568
-
2569
- // src/safeguards/types.ts
2570
- var OUTBOUND_OPS = /* @__PURE__ */ new Set([
2571
- "send",
2572
- "pay",
2573
- "sentinel"
2574
- ]);
2575
- var DEFAULT_SAFEGUARD_CONFIG = {
2576
- locked: false,
2577
- maxPerTx: 0,
2578
- maxDailySend: 0,
2579
- dailyUsed: 0,
2580
- dailyResetDate: ""
2581
- };
2582
-
2583
- // src/safeguards/errors.ts
2584
- var SafeguardError = class extends T2000Error {
2585
- rule;
2586
- details;
2587
- constructor(rule, details, message) {
2588
- const msg = message ?? buildMessage(rule, details);
2589
- super("SAFEGUARD_BLOCKED", msg, { rule, ...details });
2590
- this.name = "SafeguardError";
2591
- this.rule = rule;
2592
- this.details = details;
2593
- }
2594
- toJSON() {
2595
- return {
2596
- error: "SAFEGUARD_BLOCKED",
2597
- message: this.message,
2598
- retryable: this.retryable,
2599
- data: { rule: this.rule, ...this.details }
2600
- };
2601
- }
2602
- };
2603
- function buildMessage(rule, details) {
2604
- switch (rule) {
2605
- case "locked":
2606
- return "Agent is locked. All operations are frozen.";
2607
- case "maxPerTx":
2608
- return `Amount $${(details.attempted ?? 0).toFixed(2)} exceeds per-transaction limit ($${(details.limit ?? 0).toFixed(2)})`;
2609
- case "maxDailySend":
2610
- return `Daily send limit reached ($${(details.current ?? 0).toFixed(2)}/$${(details.limit ?? 0).toFixed(2)} used today)`;
2611
- }
2612
- }
2613
-
2614
- // src/safeguards/enforcer.ts
2615
- var SafeguardEnforcer = class {
2616
- config;
2617
- configPath;
2618
- constructor(configDir) {
2619
- this.config = { ...DEFAULT_SAFEGUARD_CONFIG };
2620
- this.configPath = configDir ? path.join(configDir, "config.json") : null;
2621
- }
2622
- load() {
2623
- if (!this.configPath) return;
2624
- try {
2625
- const raw = JSON.parse(fs.readFileSync(this.configPath, "utf-8"));
2626
- this.config = {
2627
- ...DEFAULT_SAFEGUARD_CONFIG,
2628
- locked: raw.locked ?? false,
2629
- maxPerTx: raw.maxPerTx ?? 0,
2630
- maxDailySend: raw.maxDailySend ?? 0,
2631
- dailyUsed: raw.dailyUsed ?? 0,
2632
- dailyResetDate: raw.dailyResetDate ?? ""
2633
- };
2634
- } catch {
2635
- this.config = { ...DEFAULT_SAFEGUARD_CONFIG };
2636
- }
2637
- }
2638
- assertNotLocked() {
2639
- this.load();
2640
- if (this.config.locked) {
2641
- throw new SafeguardError("locked", {});
2642
- }
2643
- }
2644
- check(metadata) {
2645
- this.load();
2646
- if (this.config.locked) {
2647
- throw new SafeguardError("locked", {});
2648
- }
2649
- if (!OUTBOUND_OPS.has(metadata.operation)) return;
2650
- const amount = metadata.amount ?? 0;
2651
- if (this.config.maxPerTx > 0 && amount > this.config.maxPerTx) {
2652
- throw new SafeguardError("maxPerTx", {
2653
- attempted: amount,
2654
- limit: this.config.maxPerTx
2655
- });
2656
- }
2657
- this.resetDailyIfNewDay();
2658
- if (this.config.maxDailySend > 0 && this.config.dailyUsed + amount > this.config.maxDailySend) {
2659
- throw new SafeguardError("maxDailySend", {
2660
- attempted: amount,
2661
- limit: this.config.maxDailySend,
2662
- current: this.config.dailyUsed
2663
- });
2664
- }
2665
- }
2666
- recordUsage(amount) {
2667
- this.resetDailyIfNewDay();
2668
- this.config.dailyUsed += amount;
2669
- this.save();
2670
- }
2671
- lock() {
2672
- this.config.locked = true;
2673
- this.save();
2674
- }
2675
- unlock() {
2676
- this.config.locked = false;
2677
- this.save();
2678
- }
2679
- set(key, value) {
2680
- if (key === "locked" && typeof value === "boolean") {
2681
- this.config.locked = value;
2682
- } else if (key === "maxPerTx" && typeof value === "number") {
2683
- this.config.maxPerTx = value;
2684
- } else if (key === "maxDailySend" && typeof value === "number") {
2685
- this.config.maxDailySend = value;
2686
- }
2687
- this.save();
2688
- }
2689
- getConfig() {
2690
- this.load();
2691
- this.resetDailyIfNewDay();
2692
- return { ...this.config };
2693
- }
2694
- isConfigured() {
2695
- return this.config.maxPerTx > 0 || this.config.maxDailySend > 0;
2696
- }
2697
- resetDailyIfNewDay() {
2698
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2699
- if (this.config.dailyResetDate !== today) {
2700
- this.config.dailyUsed = 0;
2701
- this.config.dailyResetDate = today;
2702
- this.save();
2703
- }
2704
- }
2705
- save() {
2706
- if (!this.configPath) return;
2707
- try {
2708
- let existing = {};
2709
- try {
2710
- existing = JSON.parse(fs.readFileSync(this.configPath, "utf-8"));
2711
- } catch {
2712
- }
2713
- const merged = {
2714
- ...existing,
2715
- locked: this.config.locked,
2716
- maxPerTx: this.config.maxPerTx,
2717
- maxDailySend: this.config.maxDailySend,
2718
- dailyUsed: this.config.dailyUsed,
2719
- dailyResetDate: this.config.dailyResetDate
2720
- };
2721
- const dir = this.configPath.replace(/[/\\][^/\\]+$/, "");
2722
- if (!fs.existsSync(dir)) {
2723
- fs.mkdirSync(dir, { recursive: true });
2724
- }
2725
- fs.writeFileSync(this.configPath, JSON.stringify(merged, null, 2) + "\n");
2726
- } catch {
2727
- }
2728
- }
2729
- };
2730
- var RESERVED_NAMES = /* @__PURE__ */ new Set(["to", "all", "address"]);
2731
- var ContactManager = class {
2732
- contacts = {};
2733
- filePath;
2734
- dir;
2735
- constructor(configDir) {
2736
- this.dir = configDir ?? path.join(os.homedir(), ".t2000");
2737
- this.filePath = path.join(this.dir, "contacts.json");
2738
- this.load();
2739
- }
2740
- load() {
2741
- try {
2742
- if (fs.existsSync(this.filePath)) {
2743
- this.contacts = JSON.parse(fs.readFileSync(this.filePath, "utf-8"));
2744
- }
2745
- } catch {
2746
- this.contacts = {};
2747
- }
2748
- }
2749
- save() {
2750
- if (!fs.existsSync(this.dir)) fs.mkdirSync(this.dir, { recursive: true });
2751
- fs.writeFileSync(this.filePath, JSON.stringify(this.contacts, null, 2));
2752
- }
2753
- add(name, address) {
2754
- this.validateName(name);
2755
- const normalized = validateAddress(address);
2756
- const key = name.toLowerCase();
2757
- const existed = key in this.contacts;
2758
- this.contacts[key] = { name, address: normalized };
2759
- this.save();
2760
- return { action: existed ? "updated" : "added" };
2761
- }
2762
- remove(name) {
2763
- const key = name.toLowerCase();
2764
- if (!(key in this.contacts)) return false;
2765
- delete this.contacts[key];
2766
- this.save();
2767
- return true;
2768
- }
2769
- get(name) {
2770
- this.load();
2771
- return this.contacts[name.toLowerCase()];
2772
- }
2773
- list() {
2774
- this.load();
2775
- return Object.values(this.contacts);
2776
- }
2777
- resolve(nameOrAddress) {
2778
- this.load();
2779
- if (nameOrAddress.startsWith("0x") && nameOrAddress.length >= 42) {
2780
- return { address: validateAddress(nameOrAddress) };
2781
- }
2782
- const contact = this.contacts[nameOrAddress.toLowerCase()];
2783
- if (contact) {
2784
- return { address: contact.address, contactName: contact.name };
2785
- }
2786
- throw new T2000Error(
2787
- "CONTACT_NOT_FOUND",
2788
- `"${nameOrAddress}" is not a valid Sui address or saved contact.
2789
- Add it: t2000 contacts add ${nameOrAddress} 0x...`
2790
- );
2791
- }
2792
- validateName(name) {
2793
- if (name.startsWith("0x")) {
2794
- throw new T2000Error("INVALID_CONTACT_NAME", "Contact names cannot start with 0x");
2795
- }
2796
- if (!/^[a-zA-Z0-9_]+$/.test(name)) {
2797
- throw new T2000Error("INVALID_CONTACT_NAME", "Contact names can only contain letters, numbers, and underscores");
2798
- }
2799
- if (name.length > 32) {
2800
- throw new T2000Error("INVALID_CONTACT_NAME", "Contact names must be 32 characters or fewer");
2801
- }
2802
- if (RESERVED_NAMES.has(name.toLowerCase())) {
2803
- throw new T2000Error("INVALID_CONTACT_NAME", `"${name}" is a reserved name and cannot be used as a contact`);
2804
- }
2805
- }
2806
- };
2807
- function emptyData() {
2808
- return { positions: {}, realizedPnL: 0 };
2809
- }
2810
- var PortfolioManager = class {
2811
- data = emptyData();
2812
- filePath;
2813
- dir;
2814
- constructor(configDir) {
2815
- this.dir = configDir ?? path.join(os.homedir(), ".t2000");
2816
- this.filePath = path.join(this.dir, "portfolio.json");
2817
- this.load();
2818
- }
2819
- load() {
2820
- try {
2821
- if (fs.existsSync(this.filePath)) {
2822
- this.data = JSON.parse(fs.readFileSync(this.filePath, "utf-8"));
2823
- }
2824
- } catch {
2825
- this.data = emptyData();
2826
- }
2827
- }
2828
- save() {
2829
- if (!fs.existsSync(this.dir)) fs.mkdirSync(this.dir, { recursive: true });
2830
- fs.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
2831
- }
2832
- recordBuy(trade) {
2833
- this.load();
2834
- const pos = this.data.positions[trade.asset] ?? { totalAmount: 0, costBasis: 0, avgPrice: 0, trades: [] };
2835
- pos.totalAmount += trade.amount;
2836
- pos.costBasis += trade.usdValue;
2837
- pos.avgPrice = pos.costBasis / pos.totalAmount;
2838
- pos.trades.push(trade);
2839
- this.data.positions[trade.asset] = pos;
2840
- this.save();
2841
- }
2842
- recordSell(trade) {
2843
- this.load();
2844
- const pos = this.data.positions[trade.asset];
2845
- if (!pos || pos.totalAmount <= 0) {
2846
- throw new T2000Error("INSUFFICIENT_INVESTMENT", `No ${trade.asset} position to sell`);
2847
- }
2848
- const sellAmount = Math.min(trade.amount, pos.totalAmount);
2849
- const costOfSold = pos.avgPrice * sellAmount;
2850
- const realizedPnL = trade.usdValue - costOfSold;
2851
- pos.totalAmount -= sellAmount;
2852
- pos.costBasis -= costOfSold;
2853
- if (pos.totalAmount < 1e-6) {
2854
- pos.totalAmount = 0;
2855
- pos.costBasis = 0;
2856
- pos.avgPrice = 0;
2857
- }
2858
- pos.trades.push(trade);
2859
- this.data.realizedPnL += realizedPnL;
2860
- this.data.positions[trade.asset] = pos;
2861
- this.save();
2862
- return realizedPnL;
2863
- }
2864
- getPosition(asset) {
2865
- this.load();
2866
- return this.data.positions[asset];
2867
- }
2868
- getPositions() {
2869
- this.load();
2870
- return Object.entries(this.data.positions).filter(([, pos]) => pos.totalAmount > 0).map(([asset, pos]) => ({ asset, ...pos }));
2871
- }
2872
- getRealizedPnL() {
2873
- this.load();
2874
- return this.data.realizedPnL;
2875
- }
2876
- };
2877
- var DEFAULT_CONFIG_DIR = path.join(os.homedir(), ".t2000");
2878
- var T2000 = class _T2000 extends eventemitter3.EventEmitter {
2879
- keypair;
2880
- client;
2881
- _address;
2882
- registry;
2883
- enforcer;
2884
- contacts;
2885
- portfolio;
2886
- constructor(keypair, client, registry, configDir) {
2887
- super();
2888
- this.keypair = keypair;
2889
- this.client = client;
2890
- this._address = getAddress(keypair);
2891
- this.registry = registry ?? _T2000.createDefaultRegistry(client);
2892
- this.enforcer = new SafeguardEnforcer(configDir);
2893
- this.enforcer.load();
2894
- this.contacts = new ContactManager(configDir);
2895
- this.portfolio = new PortfolioManager(configDir);
2896
- }
2897
- static createDefaultRegistry(client) {
2898
- const registry = new ProtocolRegistry();
2899
- const naviAdapter = new NaviAdapter();
2900
- naviAdapter.initSync(client);
2901
- registry.registerLending(naviAdapter);
2902
- const cetusAdapter = new CetusAdapter();
2903
- cetusAdapter.initSync(client);
2904
- registry.registerSwap(cetusAdapter);
2905
- const suilendAdapter = new SuilendAdapter();
2906
- suilendAdapter.initSync(client);
2907
- registry.registerLending(suilendAdapter);
2908
- return registry;
2909
- }
2910
- static async create(options = {}) {
2911
- const { keyPath, pin, passphrase, network = DEFAULT_NETWORK, rpcUrl, sponsored, name } = options;
2912
- const secret = pin ?? passphrase;
2913
- const client = getSuiClient(rpcUrl);
2914
- if (sponsored) {
2915
- const keypair2 = generateKeypair();
2916
- if (secret) {
2917
- await saveKey(keypair2, secret, keyPath);
2918
- }
2919
- return new _T2000(keypair2, client, void 0, DEFAULT_CONFIG_DIR);
2920
- }
2921
- const exists = await walletExists(keyPath);
2922
- if (!exists) {
2923
- throw new T2000Error(
2924
- "WALLET_NOT_FOUND",
2925
- "No wallet found. Run `t2000 init` to create one."
2926
- );
2927
- }
2928
- if (!secret) {
2929
- throw new T2000Error("WALLET_LOCKED", "PIN required to unlock wallet");
2930
- }
2931
- const keypair = await loadKey(secret, keyPath);
2932
- return new _T2000(keypair, client, void 0, DEFAULT_CONFIG_DIR);
2933
- }
2934
- static fromPrivateKey(privateKey, options = {}) {
2935
- const keypair = keypairFromPrivateKey(privateKey);
2936
- const client = getSuiClient(options.rpcUrl);
2937
- return new _T2000(keypair, client);
2938
- }
2939
- static async init(options) {
2940
- const secret = options.pin ?? options.passphrase ?? "";
2941
- const keypair = generateKeypair();
2942
- await saveKey(keypair, secret, options.keyPath);
2943
- const client = getSuiClient();
2944
- const agent = new _T2000(keypair, client, void 0, DEFAULT_CONFIG_DIR);
2945
- const address = agent.address();
2946
- let sponsored = false;
2947
- if (options.sponsored !== false) {
2948
- try {
2949
- await callSponsorApi(address, options.name);
2950
- sponsored = true;
2951
- } catch {
2952
- }
2953
- }
2954
- return { agent, address, sponsored };
2955
- }
2956
- // -- Gas --
2957
- /** SuiJsonRpcClient used by this agent — exposed for x402 and other integrations. */
2958
- get suiClient() {
2959
- return this.client;
2960
- }
2961
- /** Ed25519Keypair used by this agent — exposed for x402 and other integrations. */
2962
- get signer() {
2963
- return this.keypair;
2964
- }
2965
- // -- Wallet --
2966
- address() {
2967
- return this._address;
2968
- }
2969
- async send(params) {
2970
- this.enforcer.assertNotLocked();
2971
- const asset = params.asset ?? "USDC";
2972
- if (!(asset in SUPPORTED_ASSETS)) {
2973
- throw new T2000Error("ASSET_NOT_SUPPORTED", `Asset ${asset} is not supported`);
2974
- }
2975
- if (asset in INVESTMENT_ASSETS) {
2976
- const free = await this.getFreeBalance(asset);
2977
- if (params.amount > free) {
2978
- const pos = this.portfolio.getPosition(asset);
2979
- const invested = pos?.totalAmount ?? 0;
2980
- throw new T2000Error(
2981
- "INVESTMENT_LOCKED",
2982
- `Cannot send ${params.amount} ${asset} \u2014 ${invested.toFixed(4)} ${asset} is invested. Free ${asset}: ${free.toFixed(4)}
2983
- To access invested funds: t2000 invest sell ${params.amount} ${asset}`,
2984
- { free, invested, requested: params.amount }
2985
- );
2986
- }
2987
- }
2988
- const resolved = this.contacts.resolve(params.to);
2989
- const sendAmount = params.amount;
2990
- const sendTo = resolved.address;
2991
- const gasResult = await executeWithGas(
2992
- this.client,
2993
- this.keypair,
2994
- () => buildSendTx({ client: this.client, address: this._address, to: sendTo, amount: sendAmount, asset }),
2995
- { metadata: { operation: "send", amount: sendAmount }, enforcer: this.enforcer }
2996
- );
2997
- this.enforcer.recordUsage(sendAmount);
2998
- const balance = await this.balance();
2999
- this.emitBalanceChange(asset, sendAmount, "send", gasResult.digest);
3000
- return {
3001
- success: true,
3002
- tx: gasResult.digest,
3003
- amount: sendAmount,
3004
- to: resolved.address,
3005
- contactName: resolved.contactName,
3006
- gasCost: gasResult.gasCostSui,
3007
- gasCostUnit: "SUI",
3008
- gasMethod: gasResult.gasMethod,
3009
- balance
3010
- };
3011
- }
3012
- async balance() {
3013
- const bal = await queryBalance(this.client, this._address);
3014
- try {
3015
- const positions = await this.positions();
3016
- const savings = positions.positions.filter((p) => p.type === "save").reduce((sum, p) => sum + p.amount, 0);
3017
- const debt = positions.positions.filter((p) => p.type === "borrow").reduce((sum, p) => sum + p.amount, 0);
3018
- bal.savings = savings;
3019
- bal.debt = debt;
3020
- } catch {
3021
- }
3022
- try {
3023
- const portfolioPositions = this.portfolio.getPositions();
3024
- const suiPrice = bal.gasReserve.sui > 0 ? bal.gasReserve.usdEquiv / bal.gasReserve.sui : 0;
3025
- const assetPrices = { SUI: suiPrice };
3026
- const swapAdapter = this.registry.listSwap()[0];
3027
- for (const pos of portfolioPositions) {
3028
- if (pos.asset !== "SUI" && pos.asset in INVESTMENT_ASSETS && !(pos.asset in assetPrices)) {
3029
- try {
3030
- if (swapAdapter) {
3031
- const quote = await swapAdapter.getQuote("USDC", pos.asset, 1);
3032
- assetPrices[pos.asset] = quote.expectedOutput > 0 ? 1 / quote.expectedOutput : 0;
3033
- }
3034
- } catch {
3035
- assetPrices[pos.asset] = 0;
3036
- }
3037
- }
3038
- }
3039
- let investmentValue = 0;
3040
- let investmentCostBasis = 0;
3041
- for (const pos of portfolioPositions) {
3042
- if (!(pos.asset in INVESTMENT_ASSETS)) continue;
3043
- const price = assetPrices[pos.asset] ?? 0;
3044
- if (pos.asset === "SUI") {
3045
- const actualHeld = Math.min(pos.totalAmount, bal.gasReserve.sui);
3046
- investmentValue += actualHeld * price;
3047
- if (actualHeld < pos.totalAmount && pos.totalAmount > 0) {
3048
- investmentCostBasis += pos.costBasis * (actualHeld / pos.totalAmount);
3049
- } else {
3050
- investmentCostBasis += pos.costBasis;
3051
- }
3052
- const gasSui = Math.max(0, bal.gasReserve.sui - pos.totalAmount);
3053
- bal.gasReserve = { sui: gasSui, usdEquiv: gasSui * price };
3054
- } else {
3055
- investmentValue += pos.totalAmount * price;
3056
- investmentCostBasis += pos.costBasis;
3057
- }
3058
- }
3059
- bal.investment = investmentValue;
3060
- bal.investmentPnL = investmentValue - investmentCostBasis;
3061
- } catch {
3062
- bal.investment = 0;
3063
- bal.investmentPnL = 0;
3064
- }
3065
- bal.total = bal.available + bal.savings - bal.debt + bal.investment + bal.gasReserve.usdEquiv;
3066
- return bal;
3067
- }
3068
- async history(params) {
3069
- return queryHistory(this.client, this._address, params?.limit);
3070
- }
3071
- async deposit() {
3072
- return {
3073
- address: this._address,
3074
- network: "Sui (mainnet)",
3075
- supportedAssets: ["USDC"],
3076
- instructions: [
3077
- `Send USDC on Sui to: ${this._address}`,
3078
- "",
3079
- "From a CEX (Coinbase, Binance):",
3080
- ` 1. Withdraw USDC`,
3081
- ` 2. Select "Sui" network`,
3082
- ` 3. Paste address: ${truncateAddress(this._address)}`,
3083
- "",
3084
- "From another Sui wallet:",
3085
- ` Transfer USDC to ${truncateAddress(this._address)}`
3086
- ].join("\n")
3087
- };
3088
- }
3089
- exportKey() {
3090
- return exportPrivateKey(this.keypair);
3091
- }
3092
- async registerAdapter(adapter) {
3093
- await adapter.init(this.client);
3094
- if ("buildSaveTx" in adapter) this.registry.registerLending(adapter);
3095
- if ("buildSwapTx" in adapter) this.registry.registerSwap(adapter);
3096
- }
3097
- // -- Savings --
3098
- async save(params) {
3099
- this.enforcer.assertNotLocked();
3100
- const asset = "USDC";
3101
- const bal = await queryBalance(this.client, this._address);
3102
- const usdcBalance = bal.stables.USDC ?? 0;
3103
- const needsAutoConvert = params.amount === "all" ? Object.entries(bal.stables).some(([k, v]) => k !== "USDC" && v > 0.01) : typeof params.amount === "number" && params.amount > usdcBalance;
3104
- let amount;
3105
- if (params.amount === "all") {
3106
- amount = (bal.available ?? 0) - 1;
3107
- if (amount <= 0) {
3108
- throw new T2000Error("INSUFFICIENT_BALANCE", "Balance too low to save after $1 gas reserve", {
3109
- reason: "gas_reserve_required",
3110
- available: bal.available ?? 0
3111
- });
3112
- }
3113
- } else {
3114
- amount = params.amount;
3115
- if (amount > (bal.available ?? 0)) {
3116
- throw new T2000Error("INSUFFICIENT_BALANCE", `Insufficient balance. Available: $${(bal.available ?? 0).toFixed(2)}, requested: $${amount.toFixed(2)}`);
3117
- }
3118
- }
3119
- const fee = calculateFee("save", amount);
3120
- const saveAmount = amount;
3121
- const adapter = await this.resolveLending(params.protocol, asset, "save");
3122
- const swapAdapter = this.registry.listSwap()[0];
3123
- const canPTB = adapter.addSaveToTx && (!needsAutoConvert || swapAdapter?.addSwapToTx);
3124
- const gasResult = await executeWithGas(this.client, this.keypair, async () => {
3125
- if (canPTB && needsAutoConvert) {
3126
- const tx2 = new transactions.Transaction();
3127
- tx2.setSender(this._address);
3128
- const usdcCoins = [];
3129
- for (const [stableAsset, stableAmount] of Object.entries(bal.stables)) {
3130
- if (stableAsset === "USDC" || stableAmount <= 0.01) continue;
3131
- const assetInfo = SUPPORTED_ASSETS[stableAsset];
3132
- if (!assetInfo) continue;
3133
- const coins = await this._fetchCoins(assetInfo.type);
3134
- if (coins.length === 0) continue;
3135
- const merged = this._mergeCoinsInTx(tx2, coins);
3136
- const { outputCoin } = await swapAdapter.addSwapToTx(
3137
- tx2,
3138
- this._address,
3139
- merged,
3140
- stableAsset,
3141
- "USDC",
3142
- stableAmount
3143
- );
3144
- usdcCoins.push(outputCoin);
3145
- }
3146
- const existingUsdc = await this._fetchCoins(SUPPORTED_ASSETS.USDC.type);
3147
- if (existingUsdc.length > 0) {
3148
- usdcCoins.push(this._mergeCoinsInTx(tx2, existingUsdc));
3149
- }
3150
- if (usdcCoins.length > 1) {
3151
- tx2.mergeCoins(usdcCoins[0], usdcCoins.slice(1));
3152
- }
3153
- await adapter.addSaveToTx(tx2, this._address, usdcCoins[0], asset, { collectFee: true });
3154
- return tx2;
3155
- }
3156
- if (canPTB && !needsAutoConvert) {
3157
- const tx2 = new transactions.Transaction();
3158
- tx2.setSender(this._address);
3159
- const existingUsdc = await this._fetchCoins(SUPPORTED_ASSETS.USDC.type);
3160
- if (existingUsdc.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins found");
3161
- const merged = this._mergeCoinsInTx(tx2, existingUsdc);
3162
- const rawAmount = BigInt(Math.floor(saveAmount * 10 ** SUPPORTED_ASSETS.USDC.decimals));
3163
- const [depositCoin] = tx2.splitCoins(merged, [rawAmount]);
3164
- await adapter.addSaveToTx(tx2, this._address, depositCoin, asset, { collectFee: true });
3165
- return tx2;
3166
- }
3167
- if (needsAutoConvert) {
3168
- await this._convertWalletStablesToUsdc(bal, params.amount === "all" ? void 0 : amount - usdcBalance);
3169
- }
3170
- const { tx } = await adapter.buildSaveTx(this._address, saveAmount, asset, { collectFee: true });
3171
- return tx;
3172
- });
3173
- const rates = await adapter.getRates(asset);
3174
- reportFee(this._address, "save", fee.amount, fee.rate, gasResult.digest);
3175
- this.emitBalanceChange(asset, saveAmount, "save", gasResult.digest);
3176
- let savingsBalance = saveAmount;
3177
- try {
3178
- const positions = await this.positions();
3179
- savingsBalance = positions.positions.filter((p) => p.type === "save" && p.asset === asset).reduce((sum, p) => sum + p.amount, 0);
3180
- } catch {
3181
- }
3182
- return {
3183
- success: true,
3184
- tx: gasResult.digest,
3185
- amount: saveAmount,
3186
- apy: rates.saveApy,
3187
- fee: fee.amount,
3188
- gasCost: gasResult.gasCostSui,
3189
- gasMethod: gasResult.gasMethod,
3190
- savingsBalance
3191
- };
3192
- }
3193
- async withdraw(params) {
3194
- this.enforcer.assertNotLocked();
3195
- if (params.amount === "all" && !params.protocol) {
3196
- return this.withdrawAllProtocols();
3197
- }
3198
- const allPositions = await this.registry.allPositions(this._address);
3199
- const supplies = [];
3200
- for (const pos of allPositions) {
3201
- if (params.protocol && pos.protocolId !== params.protocol) continue;
3202
- for (const s of pos.positions.supplies) {
3203
- if (s.amount > 1e-3) supplies.push({ protocolId: pos.protocolId, asset: s.asset, amount: s.amount, apy: s.apy });
3204
- }
3205
- }
3206
- if (supplies.length === 0) {
3207
- throw new T2000Error("NO_COLLATERAL", "No savings to withdraw");
3208
- }
3209
- supplies.sort((a, b) => a.apy - b.apy);
3210
- const target = supplies[0];
3211
- const adapter = this.registry.getLending(target.protocolId);
3212
- if (!adapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", `Protocol ${target.protocolId} not found`);
3213
- let amount;
3214
- if (params.amount === "all") {
3215
- const maxResult = await adapter.maxWithdraw(this._address, target.asset);
3216
- amount = maxResult.maxAmount;
3217
- if (amount <= 0) {
3218
- throw new T2000Error("NO_COLLATERAL", "No savings to withdraw");
3219
- }
3220
- } else {
3221
- amount = params.amount;
3222
- const hf = await adapter.getHealth(this._address);
3223
- if (hf.borrowed > 0) {
3224
- const maxResult = await adapter.maxWithdraw(this._address, target.asset);
3225
- if (amount > maxResult.maxAmount) {
3226
- throw new T2000Error(
3227
- "WITHDRAW_WOULD_LIQUIDATE",
3228
- `Withdrawing $${amount.toFixed(2)} would drop health factor below 1.5`,
3229
- {
3230
- safeWithdrawAmount: maxResult.maxAmount,
3231
- currentHF: maxResult.currentHF,
3232
- projectedHF: maxResult.healthFactorAfter
3233
- }
3234
- );
3235
- }
3236
- }
3237
- }
3238
- const withdrawAmount = amount;
3239
- let finalAmount = withdrawAmount;
3240
- const swapAdapter = target.asset !== "USDC" ? this.registry.listSwap()[0] : void 0;
3241
- const canPTB = adapter.addWithdrawToTx && (!swapAdapter || swapAdapter.addSwapToTx);
3242
- const gasResult = await executeWithGas(this.client, this.keypair, async () => {
3243
- if (canPTB) {
3244
- const tx = new transactions.Transaction();
3245
- tx.setSender(this._address);
3246
- const { coin, effectiveAmount } = await adapter.addWithdrawToTx(tx, this._address, withdrawAmount, target.asset);
3247
- finalAmount = effectiveAmount;
3248
- if (target.asset !== "USDC" && swapAdapter?.addSwapToTx) {
3249
- const { outputCoin, estimatedOut, toDecimals } = await swapAdapter.addSwapToTx(
3250
- tx,
3251
- this._address,
3252
- coin,
3253
- target.asset,
3254
- "USDC",
3255
- effectiveAmount
3256
- );
3257
- finalAmount = estimatedOut / 10 ** toDecimals;
3258
- tx.transferObjects([outputCoin], this._address);
3259
- } else {
3260
- tx.transferObjects([coin], this._address);
3261
- }
3262
- return tx;
3263
- }
3264
- const built = await adapter.buildWithdrawTx(this._address, withdrawAmount, target.asset);
3265
- finalAmount = built.effectiveAmount;
3266
- return built.tx;
3267
- });
3268
- this.emitBalanceChange("USDC", finalAmount, "withdraw", gasResult.digest);
3269
- return {
3270
- success: true,
3271
- tx: gasResult.digest,
3272
- amount: finalAmount,
3273
- gasCost: gasResult.gasCostSui,
3274
- gasMethod: gasResult.gasMethod
3275
- };
3276
- }
3277
- async withdrawAllProtocols() {
3278
- const allPositions = await this.registry.allPositions(this._address);
3279
- const withdrawable = [];
3280
- for (const pos of allPositions) {
3281
- for (const supply of pos.positions.supplies) {
3282
- if (supply.amount > 0.01) {
3283
- withdrawable.push({ protocolId: pos.protocolId, asset: supply.asset, amount: supply.amount });
3284
- }
3285
- }
3286
- }
3287
- if (withdrawable.length === 0) {
3288
- throw new T2000Error("NO_COLLATERAL", "No savings to withdraw across any protocol");
3289
- }
3290
- const entries = [];
3291
- for (const entry of withdrawable) {
3292
- const adapter = this.registry.getLending(entry.protocolId);
3293
- if (!adapter) continue;
3294
- const maxResult = await adapter.maxWithdraw(this._address, entry.asset);
3295
- const perAssetMax = Math.min(entry.amount, maxResult.maxAmount);
3296
- if (perAssetMax > 0.01) {
3297
- entries.push({ ...entry, maxAmount: perAssetMax, adapter });
3298
- }
3299
- }
3300
- if (entries.length === 0) {
3301
- throw new T2000Error("NO_COLLATERAL", "No savings to withdraw across any protocol");
3302
- }
3303
- const swapAdapter = this.registry.listSwap()[0];
3304
- const canPTB = entries.every((e) => e.adapter.addWithdrawToTx) && (!swapAdapter || swapAdapter.addSwapToTx);
3305
- let totalUsdcReceived = 0;
3306
- const gasResult = await executeWithGas(this.client, this.keypair, async () => {
3307
- if (canPTB) {
3308
- const tx = new transactions.Transaction();
3309
- tx.setSender(this._address);
3310
- const usdcCoins = [];
3311
- for (const entry of entries) {
3312
- const { coin, effectiveAmount } = await entry.adapter.addWithdrawToTx(
3313
- tx,
3314
- this._address,
3315
- entry.maxAmount,
3316
- entry.asset
3317
- );
3318
- if (entry.asset !== "USDC" && swapAdapter?.addSwapToTx) {
3319
- const { outputCoin, estimatedOut, toDecimals } = await swapAdapter.addSwapToTx(
3320
- tx,
3321
- this._address,
3322
- coin,
3323
- entry.asset,
3324
- "USDC",
3325
- effectiveAmount
3326
- );
3327
- totalUsdcReceived += estimatedOut / 10 ** toDecimals;
3328
- usdcCoins.push(outputCoin);
3329
- } else {
3330
- totalUsdcReceived += effectiveAmount;
3331
- usdcCoins.push(coin);
3332
- }
3333
- }
3334
- if (usdcCoins.length > 1) {
3335
- tx.mergeCoins(usdcCoins[0], usdcCoins.slice(1));
3336
- }
3337
- tx.transferObjects([usdcCoins[0]], this._address);
3338
- return tx;
3339
- }
3340
- let lastTx;
3341
- for (const entry of entries) {
3342
- const built = await entry.adapter.buildWithdrawTx(this._address, entry.maxAmount, entry.asset);
3343
- totalUsdcReceived += built.effectiveAmount;
3344
- lastTx = built.tx;
3345
- }
3346
- return lastTx;
3347
- });
3348
- if (totalUsdcReceived <= 0) {
3349
- throw new T2000Error("NO_COLLATERAL", "No savings to withdraw across any protocol");
3350
- }
3351
- return {
3352
- success: true,
3353
- tx: gasResult.digest,
3354
- amount: totalUsdcReceived,
3355
- gasCost: gasResult.gasCostSui,
3356
- gasMethod: gasResult.gasMethod
3357
- };
3358
- }
3359
- async _fetchCoins(coinType) {
3360
- const all = [];
3361
- let cursor;
3362
- let hasNext = true;
3363
- while (hasNext) {
3364
- const page = await this.client.getCoins({ owner: this._address, coinType, cursor: cursor ?? void 0 });
3365
- all.push(...page.data.map((c) => ({ coinObjectId: c.coinObjectId, balance: c.balance })));
3366
- cursor = page.nextCursor;
3367
- hasNext = page.hasNextPage;
3368
- }
3369
- return all;
3370
- }
3371
- _mergeCoinsInTx(tx, coins) {
3372
- if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No coins to merge");
3373
- const primary = tx.object(coins[0].coinObjectId);
3374
- if (coins.length > 1) {
3375
- tx.mergeCoins(primary, coins.slice(1).map((c) => tx.object(c.coinObjectId)));
3376
- }
3377
- return primary;
3378
- }
3379
- async _swapToUsdc(asset, amount) {
3380
- const swapAdapter = this.registry.listSwap()[0];
3381
- if (!swapAdapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", "No swap adapter available");
3382
- let estimatedOut = 0;
3383
- let toDecimals = 6;
3384
- const gasResult = await executeWithGas(this.client, this.keypair, async () => {
3385
- const built = await swapAdapter.buildSwapTx(this._address, asset, "USDC", amount);
3386
- estimatedOut = built.estimatedOut;
3387
- toDecimals = built.toDecimals;
3388
- return built.tx;
3389
- });
3390
- const usdcReceived = estimatedOut / 10 ** toDecimals;
3391
- return { usdcReceived, digest: gasResult.digest, gasCost: gasResult.gasCostSui };
3392
- }
3393
- async _swapFromUsdc(toAsset, amount) {
3394
- const swapAdapter = this.registry.listSwap()[0];
3395
- if (!swapAdapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", "No swap adapter available");
3396
- let estimatedOut = 0;
3397
- let toDecimals = 6;
3398
- const gasResult = await executeWithGas(this.client, this.keypair, async () => {
3399
- const built = await swapAdapter.buildSwapTx(this._address, "USDC", toAsset, amount);
3400
- estimatedOut = built.estimatedOut;
3401
- toDecimals = built.toDecimals;
3402
- return built.tx;
3403
- });
3404
- const received = estimatedOut / 10 ** toDecimals;
3405
- return { received, digest: gasResult.digest, gasCost: gasResult.gasCostSui };
3406
- }
3407
- async _convertWalletStablesToUsdc(bal, amountNeeded) {
3408
- const nonUsdcStables = [];
3409
- for (const [asset, amount] of Object.entries(bal.stables)) {
3410
- if (asset !== "USDC" && amount > 0.01) {
3411
- nonUsdcStables.push({ asset, amount });
3412
- }
3413
- }
3414
- if (nonUsdcStables.length === 0) return;
3415
- nonUsdcStables.sort((a, b) => b.amount - a.amount);
3416
- let converted = 0;
3417
- for (const entry of nonUsdcStables) {
3418
- if (amountNeeded !== void 0 && converted >= amountNeeded) break;
3419
- try {
3420
- await this._swapToUsdc(entry.asset, entry.amount);
3421
- converted += entry.amount;
3422
- } catch {
3423
- }
3424
- }
3425
- }
3426
- async maxWithdraw() {
3427
- const adapter = await this.resolveLending(void 0, "USDC", "withdraw");
3428
- return adapter.maxWithdraw(this._address, "USDC");
3429
- }
3430
- // -- Borrowing --
3431
- async borrow(params) {
3432
- this.enforcer.assertNotLocked();
3433
- const asset = "USDC";
3434
- const adapter = await this.resolveLending(params.protocol, asset, "borrow");
3435
- const maxResult = await adapter.maxBorrow(this._address, asset);
3436
- if (maxResult.maxAmount <= 0) {
3437
- throw new T2000Error("NO_COLLATERAL", "No collateral deposited. Save first with `t2000 save <amount>`.");
3438
- }
3439
- if (params.amount > maxResult.maxAmount) {
3440
- throw new T2000Error("HEALTH_FACTOR_TOO_LOW", `Max safe borrow: $${maxResult.maxAmount.toFixed(2)}`, {
3441
- maxBorrow: maxResult.maxAmount,
3442
- currentHF: maxResult.currentHF
3443
- });
3444
- }
3445
- const fee = calculateFee("borrow", params.amount);
3446
- const borrowAmount = params.amount;
3447
- const gasResult = await executeWithGas(this.client, this.keypair, async () => {
3448
- const { tx } = await adapter.buildBorrowTx(this._address, borrowAmount, asset, { collectFee: true });
3449
- return tx;
3450
- });
3451
- const hf = await adapter.getHealth(this._address);
3452
- reportFee(this._address, "borrow", fee.amount, fee.rate, gasResult.digest);
3453
- this.emitBalanceChange(asset, borrowAmount, "borrow", gasResult.digest);
3454
- return {
3455
- success: true,
3456
- tx: gasResult.digest,
3457
- amount: borrowAmount,
3458
- fee: fee.amount,
3459
- healthFactor: hf.healthFactor,
3460
- gasCost: gasResult.gasCostSui,
3461
- gasMethod: gasResult.gasMethod
3462
- };
3463
- }
3464
- async repay(params) {
3465
- this.enforcer.assertNotLocked();
3466
- const allPositions = await this.registry.allPositions(this._address);
3467
- const borrows = [];
3468
- for (const pos of allPositions) {
3469
- if (params.protocol && pos.protocolId !== params.protocol) continue;
3470
- for (const b of pos.positions.borrows) {
3471
- if (b.amount > 1e-3) borrows.push({ protocolId: pos.protocolId, asset: b.asset, amount: b.amount, apy: b.apy });
3472
- }
3473
- }
3474
- if (borrows.length === 0) {
3475
- throw new T2000Error("NO_COLLATERAL", "No outstanding borrow to repay");
3476
- }
3477
- if (params.amount === "all") {
3478
- return this._repayAllBorrows(borrows);
3479
- }
3480
- borrows.sort((a, b) => b.apy - a.apy);
3481
- const target = borrows[0];
3482
- const adapter = this.registry.getLending(target.protocolId);
3483
- if (!adapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", `Protocol ${target.protocolId} not found`);
3484
- const repayAmount = Math.min(params.amount, target.amount);
3485
- const swapAdapter = target.asset !== "USDC" ? this.registry.listSwap()[0] : void 0;
3486
- const canPTB = adapter.addRepayToTx && (!swapAdapter || swapAdapter.addSwapToTx);
3487
- const gasResult = await executeWithGas(this.client, this.keypair, async () => {
3488
- if (canPTB && target.asset !== "USDC" && swapAdapter?.addSwapToTx) {
3489
- const tx2 = new transactions.Transaction();
3490
- tx2.setSender(this._address);
3491
- const buffer = repayAmount * 1.005;
3492
- const usdcCoins = await this._fetchCoins(SUPPORTED_ASSETS.USDC.type);
3493
- if (usdcCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins for swap");
3494
- const merged = this._mergeCoinsInTx(tx2, usdcCoins);
3495
- const rawSwap = BigInt(Math.floor(buffer * 10 ** SUPPORTED_ASSETS.USDC.decimals));
3496
- const [splitCoin] = tx2.splitCoins(merged, [rawSwap]);
3497
- const { outputCoin } = await swapAdapter.addSwapToTx(
3498
- tx2,
3499
- this._address,
3500
- splitCoin,
3501
- "USDC",
3502
- target.asset,
3503
- buffer
3504
- );
3505
- await adapter.addRepayToTx(tx2, this._address, outputCoin, target.asset);
3506
- return tx2;
3507
- }
3508
- if (canPTB && target.asset === "USDC") {
3509
- const tx2 = new transactions.Transaction();
3510
- tx2.setSender(this._address);
3511
- const usdcCoins = await this._fetchCoins(SUPPORTED_ASSETS.USDC.type);
3512
- if (usdcCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins");
3513
- const merged = this._mergeCoinsInTx(tx2, usdcCoins);
3514
- const raw = BigInt(Math.floor(repayAmount * 10 ** SUPPORTED_ASSETS.USDC.decimals));
3515
- const [repayCoin] = tx2.splitCoins(merged, [raw]);
3516
- await adapter.addRepayToTx(tx2, this._address, repayCoin, target.asset);
3517
- return tx2;
3518
- }
3519
- if (target.asset !== "USDC") {
3520
- await this._swapFromUsdc(target.asset, repayAmount * 1.005);
3521
- }
3522
- const { tx } = await adapter.buildRepayTx(this._address, repayAmount, target.asset);
3523
- return tx;
3524
- });
3525
- const hf = await adapter.getHealth(this._address);
3526
- this.emitBalanceChange("USDC", repayAmount, "repay", gasResult.digest);
3527
- return {
3528
- success: true,
3529
- tx: gasResult.digest,
3530
- amount: repayAmount,
3531
- remainingDebt: hf.borrowed,
3532
- gasCost: gasResult.gasCostSui,
3533
- gasMethod: gasResult.gasMethod
3534
- };
3535
- }
3536
- async _repayAllBorrows(borrows) {
3537
- borrows.sort((a, b) => b.apy - a.apy);
3538
- const entries = [];
3539
- for (const borrow of borrows) {
3540
- const adapter = this.registry.getLending(borrow.protocolId);
3541
- if (adapter) entries.push({ borrow, adapter });
3542
- }
3543
- const swapAdapter = this.registry.listSwap()[0];
3544
- const canPTB = entries.every((e) => e.adapter.addRepayToTx) && (entries.every((e) => e.borrow.asset === "USDC") || swapAdapter?.addSwapToTx);
3545
- let totalRepaid = 0;
3546
- const gasResult = await executeWithGas(this.client, this.keypair, async () => {
3547
- if (canPTB) {
3548
- const tx = new transactions.Transaction();
3549
- tx.setSender(this._address);
3550
- const usdcCoins = await this._fetchCoins(SUPPORTED_ASSETS.USDC.type);
3551
- let usdcMerged;
3552
- if (usdcCoins.length > 0) {
3553
- usdcMerged = this._mergeCoinsInTx(tx, usdcCoins);
3554
- }
3555
- for (const { borrow, adapter } of entries) {
3556
- if (borrow.asset !== "USDC" && swapAdapter?.addSwapToTx) {
3557
- const buffer = borrow.amount * 1.005;
3558
- const rawSwap = BigInt(Math.floor(buffer * 10 ** SUPPORTED_ASSETS.USDC.decimals));
3559
- if (!usdcMerged) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC for swap");
3560
- const [splitCoin] = tx.splitCoins(usdcMerged, [rawSwap]);
3561
- const { outputCoin } = await swapAdapter.addSwapToTx(
3562
- tx,
3563
- this._address,
3564
- splitCoin,
3565
- "USDC",
3566
- borrow.asset,
3567
- buffer
3568
- );
3569
- await adapter.addRepayToTx(tx, this._address, outputCoin, borrow.asset);
3570
- } else {
3571
- const raw = BigInt(Math.floor(borrow.amount * 10 ** SUPPORTED_ASSETS.USDC.decimals));
3572
- if (!usdcMerged) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC for repayment");
3573
- const [repayCoin] = tx.splitCoins(usdcMerged, [raw]);
3574
- await adapter.addRepayToTx(tx, this._address, repayCoin, borrow.asset);
3575
- }
3576
- totalRepaid += borrow.amount;
3577
- }
3578
- return tx;
3579
- }
3580
- let lastTx;
3581
- for (const { borrow, adapter } of entries) {
3582
- if (borrow.asset !== "USDC") {
3583
- await this._swapFromUsdc(borrow.asset, borrow.amount * 1.005);
3584
- }
3585
- const { tx } = await adapter.buildRepayTx(this._address, borrow.amount, borrow.asset);
3586
- lastTx = tx;
3587
- totalRepaid += borrow.amount;
3588
- }
3589
- return lastTx;
3590
- });
3591
- const firstAdapter = entries[0]?.adapter;
3592
- const hf = firstAdapter ? await firstAdapter.getHealth(this._address) : { borrowed: 0 };
3593
- this.emitBalanceChange("USDC", totalRepaid, "repay", gasResult.digest);
3594
- return {
3595
- success: true,
3596
- tx: gasResult.digest,
3597
- amount: totalRepaid,
3598
- remainingDebt: hf.borrowed,
3599
- gasCost: gasResult.gasCostSui,
3600
- gasMethod: gasResult.gasMethod
3601
- };
3602
- }
3603
- async maxBorrow() {
3604
- const adapter = await this.resolveLending(void 0, "USDC", "borrow");
3605
- return adapter.maxBorrow(this._address, "USDC");
3606
- }
3607
- async healthFactor() {
3608
- const adapter = await this.resolveLending(void 0, "USDC", "save");
3609
- const hf = await adapter.getHealth(this._address);
3610
- if (hf.healthFactor < 1.2) {
3611
- this.emit("healthCritical", { healthFactor: hf.healthFactor, threshold: 1.5, severity: "critical" });
3612
- } else if (hf.healthFactor < 2) {
3613
- this.emit("healthWarning", { healthFactor: hf.healthFactor, threshold: 2, severity: "warning" });
3614
- }
3615
- return hf;
3616
- }
3617
- // -- Exchange --
3618
- async exchange(params) {
3619
- this.enforcer.assertNotLocked();
3620
- const fromAsset = params.from;
3621
- const toAsset = params.to;
3622
- if (!(fromAsset in SUPPORTED_ASSETS) || !(toAsset in SUPPORTED_ASSETS)) {
3623
- throw new T2000Error("ASSET_NOT_SUPPORTED", `Swap pair ${fromAsset}/${toAsset} is not supported`);
3624
- }
3625
- if (fromAsset === toAsset) {
3626
- throw new T2000Error("INVALID_AMOUNT", "Cannot swap same asset");
3627
- }
3628
- if (!params._bypassInvestmentGuard && fromAsset in INVESTMENT_ASSETS) {
3629
- const free = await this.getFreeBalance(fromAsset);
3630
- if (params.amount > free) {
3631
- const pos = this.portfolio.getPosition(fromAsset);
3632
- const invested = pos?.totalAmount ?? 0;
3633
- throw new T2000Error(
3634
- "INVESTMENT_LOCKED",
3635
- `Cannot exchange ${params.amount} ${fromAsset} \u2014 ${invested.toFixed(4)} ${fromAsset} is invested. Free ${fromAsset}: ${free.toFixed(4)}
3636
- To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
3637
- { free, invested, requested: params.amount }
3638
- );
3639
- }
3640
- }
3641
- const best = await this.registry.bestSwapQuote(fromAsset, toAsset, params.amount);
3642
- const adapter = best.adapter;
3643
- const fee = calculateFee("swap", params.amount);
3644
- const swapAmount = params.amount;
3645
- const slippageBps = params.maxSlippage ? params.maxSlippage * 100 : void 0;
3646
- let swapMeta = { estimatedOut: 0, toDecimals: 0 };
3647
- const gasResult = await executeWithGas(this.client, this.keypair, async () => {
3648
- const built = await adapter.buildSwapTx(this._address, fromAsset, toAsset, swapAmount, slippageBps);
3649
- swapMeta = { estimatedOut: built.estimatedOut, toDecimals: built.toDecimals };
3650
- return built.tx;
3651
- });
3652
- const toInfo = SUPPORTED_ASSETS[toAsset];
3653
- await this.client.waitForTransaction({ digest: gasResult.digest });
3654
- const txDetail = await this.client.getTransactionBlock({
3655
- digest: gasResult.digest,
3656
- options: { showBalanceChanges: true }
3657
- });
3658
- let actualReceived = 0;
3659
- if (txDetail.balanceChanges) {
3660
- for (const change of txDetail.balanceChanges) {
3661
- if (change.coinType === toInfo.type && change.owner && typeof change.owner === "object" && "AddressOwner" in change.owner && change.owner.AddressOwner === this._address) {
3662
- const amt = Number(change.amount) / 10 ** toInfo.decimals;
3663
- if (amt > 0) actualReceived += amt;
3664
- }
3665
- }
3666
- }
3667
- const expectedOutput = swapMeta.estimatedOut / 10 ** swapMeta.toDecimals;
3668
- if (actualReceived === 0) actualReceived = expectedOutput;
3669
- const priceImpact = expectedOutput > 0 ? Math.abs(actualReceived - expectedOutput) / expectedOutput : 0;
3670
- reportFee(this._address, "swap", fee.amount, fee.rate, gasResult.digest);
3671
- this.emitBalanceChange(fromAsset, swapAmount, "swap", gasResult.digest);
3672
- return {
3673
- success: true,
3674
- tx: gasResult.digest,
3675
- fromAmount: swapAmount,
3676
- fromAsset,
3677
- toAmount: actualReceived,
3678
- toAsset,
3679
- priceImpact,
3680
- fee: fee.amount,
3681
- gasCost: gasResult.gasCostSui,
3682
- gasMethod: gasResult.gasMethod
3683
- };
3684
- }
3685
- async exchangeQuote(params) {
3686
- const fromAsset = params.from;
3687
- const toAsset = params.to;
3688
- const best = await this.registry.bestSwapQuote(fromAsset, toAsset, params.amount);
3689
- const fee = calculateFee("swap", params.amount);
3690
- return { ...best.quote, fee: { amount: fee.amount, rate: fee.rate } };
3691
- }
3692
- // -- Investment --
3693
- async investBuy(params) {
3694
- this.enforcer.assertNotLocked();
3695
- if (!params.usdAmount || params.usdAmount <= 0 || !isFinite(params.usdAmount)) {
3696
- throw new T2000Error("INVALID_AMOUNT", "Investment amount must be greater than $0");
3697
- }
3698
- this.enforcer.check({ operation: "invest", amount: params.usdAmount });
3699
- if (!(params.asset in INVESTMENT_ASSETS)) {
3700
- throw new T2000Error("ASSET_NOT_SUPPORTED", `${params.asset} is not available for investment`);
3701
- }
3702
- const bal = await queryBalance(this.client, this._address);
3703
- if (bal.available < params.usdAmount) {
3704
- throw new T2000Error("INSUFFICIENT_BALANCE", `Insufficient checking balance. Available: $${bal.available.toFixed(2)}, requested: $${params.usdAmount.toFixed(2)}`);
3705
- }
3706
- const swapResult = await this.exchange({
3707
- from: "USDC",
3708
- to: params.asset,
3709
- amount: params.usdAmount,
3710
- maxSlippage: params.maxSlippage ?? 0.03,
3711
- _bypassInvestmentGuard: true
3712
- });
3713
- if (swapResult.toAmount === 0) {
3714
- throw new T2000Error("SWAP_FAILED", "Swap returned zero tokens \u2014 try a different amount or check liquidity");
3715
- }
3716
- const price = params.usdAmount / swapResult.toAmount;
3717
- this.portfolio.recordBuy({
3718
- id: `inv_${Date.now()}`,
3719
- type: "buy",
3720
- asset: params.asset,
3721
- amount: swapResult.toAmount,
3722
- price,
3723
- usdValue: params.usdAmount,
3724
- fee: swapResult.fee,
3725
- tx: swapResult.tx,
3726
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
3727
- });
3728
- const pos = this.portfolio.getPosition(params.asset);
3729
- const currentPrice = price;
3730
- const position = {
3731
- asset: params.asset,
3732
- totalAmount: pos?.totalAmount ?? swapResult.toAmount,
3733
- costBasis: pos?.costBasis ?? params.usdAmount,
3734
- avgPrice: pos?.avgPrice ?? price,
3735
- currentPrice,
3736
- currentValue: (pos?.totalAmount ?? swapResult.toAmount) * currentPrice,
3737
- unrealizedPnL: 0,
3738
- unrealizedPnLPct: 0,
3739
- trades: pos?.trades ?? []
3740
- };
3741
- return {
3742
- success: true,
3743
- tx: swapResult.tx,
3744
- type: "buy",
3745
- asset: params.asset,
3746
- amount: swapResult.toAmount,
3747
- price,
3748
- usdValue: params.usdAmount,
3749
- fee: swapResult.fee,
3750
- gasCost: swapResult.gasCost,
3751
- gasMethod: swapResult.gasMethod,
3752
- position
3753
- };
3754
- }
3755
- async investSell(params) {
3756
- this.enforcer.assertNotLocked();
3757
- if (params.usdAmount !== "all") {
3758
- if (!params.usdAmount || params.usdAmount <= 0 || !isFinite(params.usdAmount)) {
3759
- throw new T2000Error("INVALID_AMOUNT", "Sell amount must be greater than $0");
3760
- }
3761
- }
3762
- if (!(params.asset in INVESTMENT_ASSETS)) {
3763
- throw new T2000Error("ASSET_NOT_SUPPORTED", `${params.asset} is not available for investment`);
3764
- }
3765
- const pos = this.portfolio.getPosition(params.asset);
3766
- if (!pos || pos.totalAmount <= 0) {
3767
- throw new T2000Error("INSUFFICIENT_INVESTMENT", `No ${params.asset} position to sell`);
3768
- }
3769
- const assetInfo = SUPPORTED_ASSETS[params.asset];
3770
- const assetBalance = await this.client.getBalance({
3771
- owner: this._address,
3772
- coinType: assetInfo.type
3773
- });
3774
- const walletAmount = Number(assetBalance.totalBalance) / 10 ** assetInfo.decimals;
3775
- const gasReserve = params.asset === "SUI" ? GAS_RESERVE_MIN : 0;
3776
- const maxSellable = Math.max(0, walletAmount - gasReserve);
3777
- let sellAmountAsset;
3778
- if (params.usdAmount === "all") {
3779
- sellAmountAsset = Math.min(pos.totalAmount, maxSellable);
3780
- } else {
3781
- const swapAdapter = this.registry.listSwap()[0];
3782
- if (!swapAdapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", "No swap adapter available");
3783
- const quote = await swapAdapter.getQuote("USDC", params.asset, 1);
3784
- const assetPrice = 1 / quote.expectedOutput;
3785
- sellAmountAsset = params.usdAmount / assetPrice;
3786
- sellAmountAsset = Math.min(sellAmountAsset, pos.totalAmount);
3787
- if (sellAmountAsset > maxSellable) {
3788
- throw new T2000Error(
3789
- "INSUFFICIENT_INVESTMENT",
3790
- `Cannot sell $${params.usdAmount.toFixed(2)} \u2014 max sellable: $${(maxSellable * assetPrice).toFixed(2)} (gas reserve: ${gasReserve} ${params.asset})`
3791
- );
3792
- }
3793
- }
3794
- if (sellAmountAsset <= 0) {
3795
- throw new T2000Error("INSUFFICIENT_INVESTMENT", "Nothing to sell after gas reserve");
3796
- }
3797
- const swapResult = await this.exchange({
3798
- from: params.asset,
3799
- to: "USDC",
3800
- amount: sellAmountAsset,
3801
- maxSlippage: params.maxSlippage ?? 0.03,
3802
- _bypassInvestmentGuard: true
3803
- });
3804
- const price = swapResult.toAmount / sellAmountAsset;
3805
- const realizedPnL = this.portfolio.recordSell({
3806
- id: `inv_${Date.now()}`,
3807
- type: "sell",
3808
- asset: params.asset,
3809
- amount: sellAmountAsset,
3810
- price,
3811
- usdValue: swapResult.toAmount,
3812
- fee: swapResult.fee,
3813
- tx: swapResult.tx,
3814
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
3815
- });
3816
- const updatedPos = this.portfolio.getPosition(params.asset);
3817
- const position = {
3818
- asset: params.asset,
3819
- totalAmount: updatedPos?.totalAmount ?? 0,
3820
- costBasis: updatedPos?.costBasis ?? 0,
3821
- avgPrice: updatedPos?.avgPrice ?? 0,
3822
- currentPrice: price,
3823
- currentValue: (updatedPos?.totalAmount ?? 0) * price,
3824
- unrealizedPnL: 0,
3825
- unrealizedPnLPct: 0,
3826
- trades: updatedPos?.trades ?? []
3827
- };
3828
- return {
3829
- success: true,
3830
- tx: swapResult.tx,
3831
- type: "sell",
3832
- asset: params.asset,
3833
- amount: sellAmountAsset,
3834
- price,
3835
- usdValue: swapResult.toAmount,
3836
- fee: swapResult.fee,
3837
- gasCost: swapResult.gasCost,
3838
- gasMethod: swapResult.gasMethod,
3839
- realizedPnL,
3840
- position
3841
- };
3842
- }
3843
- async getPortfolio() {
3844
- const positions = this.portfolio.getPositions();
3845
- const realizedPnL = this.portfolio.getRealizedPnL();
3846
- const prices = {};
3847
- const swapAdapter = this.registry.listSwap()[0];
3848
- for (const asset of Object.keys(INVESTMENT_ASSETS)) {
3849
- try {
3850
- if (asset === "SUI" && swapAdapter) {
3851
- prices[asset] = await swapAdapter.getPoolPrice();
3852
- } else if (swapAdapter) {
3853
- const quote = await swapAdapter.getQuote("USDC", asset, 1);
3854
- prices[asset] = quote.expectedOutput > 0 ? 1 / quote.expectedOutput : 0;
3855
- }
3856
- } catch {
3857
- prices[asset] = 0;
3858
- }
3859
- }
3860
- const enriched = [];
3861
- for (const pos of positions) {
3862
- const currentPrice = prices[pos.asset] ?? 0;
3863
- let totalAmount = pos.totalAmount;
3864
- let costBasis = pos.costBasis;
3865
- if (pos.asset in INVESTMENT_ASSETS) {
3866
- try {
3867
- const assetInfo = SUPPORTED_ASSETS[pos.asset];
3868
- const bal = await this.client.getBalance({ owner: this._address, coinType: assetInfo.type });
3869
- const walletAmount = Number(bal.totalBalance) / 10 ** assetInfo.decimals;
3870
- const gasReserve = pos.asset === "SUI" ? GAS_RESERVE_MIN : 0;
3871
- const actualHeld = Math.max(0, walletAmount - gasReserve);
3872
- if (actualHeld < totalAmount) {
3873
- const ratio = totalAmount > 0 ? actualHeld / totalAmount : 0;
3874
- costBasis *= ratio;
3875
- totalAmount = actualHeld;
3876
- }
3877
- } catch {
3878
- }
3879
- }
3880
- const currentValue = totalAmount * currentPrice;
3881
- const unrealizedPnL = currentPrice > 0 ? currentValue - costBasis : 0;
3882
- const unrealizedPnLPct = currentPrice > 0 && costBasis > 0 ? unrealizedPnL / costBasis * 100 : 0;
3883
- enriched.push({
3884
- asset: pos.asset,
3885
- totalAmount,
3886
- costBasis,
3887
- avgPrice: pos.avgPrice,
3888
- currentPrice,
3889
- currentValue,
3890
- unrealizedPnL,
3891
- unrealizedPnLPct,
3892
- trades: pos.trades
3893
- });
3894
- }
3895
- const totalInvested = enriched.reduce((sum, p) => sum + p.costBasis, 0);
3896
- const totalValue = enriched.reduce((sum, p) => sum + p.currentValue, 0);
3897
- const totalUnrealizedPnL = totalValue - totalInvested;
3898
- const totalUnrealizedPnLPct = totalInvested > 0 ? totalUnrealizedPnL / totalInvested * 100 : 0;
3899
- return {
3900
- positions: enriched,
3901
- totalInvested,
3902
- totalValue,
3903
- unrealizedPnL: totalUnrealizedPnL,
3904
- unrealizedPnLPct: totalUnrealizedPnLPct,
3905
- realizedPnL
3906
- };
3907
- }
3908
- // -- Info --
3909
- async positions() {
3910
- const allPositions = await this.registry.allPositions(this._address);
3911
- const positions = allPositions.flatMap(
3912
- (p) => [
3913
- ...p.positions.supplies.filter((s) => s.amount > 5e-3).map((s) => ({
3914
- protocol: p.protocolId,
3915
- asset: s.asset,
3916
- type: "save",
3917
- amount: s.amount,
3918
- apy: s.apy
3919
- })),
3920
- ...p.positions.borrows.filter((b) => b.amount > 5e-3).map((b) => ({
3921
- protocol: p.protocolId,
3922
- asset: b.asset,
3923
- type: "borrow",
3924
- amount: b.amount,
3925
- apy: b.apy
3926
- }))
3927
- ]
3928
- );
3929
- return { positions };
3930
- }
3931
- async rates() {
3932
- const allRatesResult = await this.registry.allRatesAcrossAssets();
3933
- const result = {};
3934
- for (const entry of allRatesResult) {
3935
- if (!result[entry.asset] || entry.rates.saveApy > result[entry.asset].saveApy) {
3936
- result[entry.asset] = { saveApy: entry.rates.saveApy, borrowApy: entry.rates.borrowApy };
3937
- }
3938
- }
3939
- if (!result.USDC) result.USDC = { saveApy: 0, borrowApy: 0 };
3940
- return result;
3941
- }
3942
- async allRates(asset = "USDC") {
3943
- return this.registry.allRates(asset);
3944
- }
3945
- async allRatesAcrossAssets() {
3946
- return this.registry.allRatesAcrossAssets();
3947
- }
3948
- async rebalance(opts = {}) {
3949
- this.enforcer.assertNotLocked();
3950
- const dryRun = opts.dryRun ?? false;
3951
- const minYieldDiff = opts.minYieldDiff ?? 0.5;
3952
- const maxBreakEven = opts.maxBreakEven ?? 30;
3953
- const [allPositions, allRates] = await Promise.all([
3954
- this.registry.allPositions(this._address),
3955
- this.registry.allRatesAcrossAssets()
3956
- ]);
3957
- const savePositions = allPositions.flatMap(
3958
- (p) => p.positions.supplies.filter((s) => s.amount > 0.01).map((s) => ({
3959
- protocolId: p.protocolId,
3960
- protocol: p.protocol,
3961
- asset: s.asset,
3962
- amount: s.amount,
3963
- apy: s.apy
3964
- }))
3965
- );
3966
- if (savePositions.length === 0) {
3967
- throw new T2000Error("NO_COLLATERAL", "No savings positions to rebalance. Use `t2000 save <amount>` first.");
3968
- }
3969
- const borrowPositions = allPositions.flatMap(
3970
- (p) => p.positions.borrows.filter((b) => b.amount > 0.01)
3971
- );
3972
- if (borrowPositions.length > 0) {
3973
- const healthResults = await Promise.all(
3974
- allPositions.filter((p) => p.positions.borrows.some((b) => b.amount > 0.01)).map(async (p) => {
3975
- const adapter = this.registry.getLending(p.protocolId);
3976
- if (!adapter) return null;
3977
- return adapter.getHealth(this._address);
3978
- })
3979
- );
3980
- for (const hf of healthResults) {
3981
- if (hf && hf.healthFactor < 1.5) {
3982
- throw new T2000Error(
3983
- "HEALTH_FACTOR_TOO_LOW",
3984
- `Cannot rebalance \u2014 health factor is ${hf.healthFactor.toFixed(2)} (minimum 1.5). Repay some debt first.`,
3985
- { healthFactor: hf.healthFactor }
3986
- );
3987
- }
3988
- }
3989
- }
3990
- const bestRate = allRates.reduce(
3991
- (best, r) => r.rates.saveApy > best.rates.saveApy ? r : best
3992
- );
3993
- const current = savePositions.reduce(
3994
- (worst, p) => p.apy < worst.apy ? p : worst
3995
- );
3996
- const withdrawAdapter = this.registry.getLending(current.protocolId);
3997
- if (withdrawAdapter) {
3998
- try {
3999
- const maxResult = await withdrawAdapter.maxWithdraw(this._address, current.asset);
4000
- if (maxResult.maxAmount < current.amount) {
4001
- current.amount = Math.max(0, maxResult.maxAmount - 0.01);
4002
- }
4003
- } catch {
4004
- }
4005
- }
4006
- if (current.amount <= 0.01) {
4007
- throw new T2000Error(
4008
- "HEALTH_FACTOR_TOO_LOW",
4009
- "Cannot rebalance \u2014 active borrows prevent safe withdrawal. Repay some debt first."
4010
- );
4011
- }
4012
- const apyDiff = bestRate.rates.saveApy - current.apy;
4013
- const isSameProtocol = current.protocolId === bestRate.protocolId;
4014
- const isSameAsset = current.asset === bestRate.asset;
4015
- if (apyDiff < minYieldDiff) {
4016
- return {
4017
- executed: false,
4018
- steps: [],
4019
- fromProtocol: current.protocol,
4020
- fromAsset: current.asset,
4021
- toProtocol: bestRate.protocol,
4022
- toAsset: bestRate.asset,
4023
- amount: current.amount,
4024
- currentApy: current.apy,
4025
- newApy: bestRate.rates.saveApy,
4026
- annualGain: current.amount * apyDiff / 100,
4027
- estimatedSwapCost: 0,
4028
- breakEvenDays: Infinity,
4029
- txDigests: [],
4030
- totalGasCost: 0
4031
- };
4032
- }
4033
- if (isSameProtocol && isSameAsset) {
4034
- return {
4035
- executed: false,
4036
- steps: [],
4037
- fromProtocol: current.protocol,
4038
- fromAsset: current.asset,
4039
- toProtocol: bestRate.protocol,
4040
- toAsset: bestRate.asset,
4041
- amount: current.amount,
4042
- currentApy: current.apy,
4043
- newApy: bestRate.rates.saveApy,
4044
- annualGain: 0,
4045
- estimatedSwapCost: 0,
4046
- breakEvenDays: Infinity,
4047
- txDigests: [],
4048
- totalGasCost: 0
4049
- };
4050
- }
4051
- const steps = [];
4052
- let estimatedSwapCost = 0;
4053
- steps.push({
4054
- action: "withdraw",
4055
- protocol: current.protocolId,
4056
- fromAsset: current.asset,
4057
- amount: current.amount
4058
- });
4059
- let amountToDeposit = current.amount;
4060
- if (!isSameAsset) {
4061
- try {
4062
- const quote = await this.registry.bestSwapQuote(current.asset, bestRate.asset, current.amount);
4063
- amountToDeposit = quote.quote.expectedOutput;
4064
- estimatedSwapCost = Math.abs(current.amount - amountToDeposit);
4065
- } catch {
4066
- estimatedSwapCost = current.amount * 3e-3;
4067
- amountToDeposit = current.amount - estimatedSwapCost;
4068
- }
4069
- steps.push({
4070
- action: "swap",
4071
- fromAsset: current.asset,
4072
- toAsset: bestRate.asset,
4073
- amount: current.amount,
4074
- estimatedOutput: amountToDeposit
4075
- });
4076
- }
4077
- steps.push({
4078
- action: "deposit",
4079
- protocol: bestRate.protocolId,
4080
- toAsset: bestRate.asset,
4081
- amount: amountToDeposit
4082
- });
4083
- const annualGain = amountToDeposit * apyDiff / 100;
4084
- const breakEvenDays = estimatedSwapCost > 0 ? Math.ceil(estimatedSwapCost / annualGain * 365) : 0;
4085
- if (breakEvenDays > maxBreakEven && estimatedSwapCost > 0) {
4086
- return {
4087
- executed: false,
4088
- steps,
4089
- fromProtocol: current.protocol,
4090
- fromAsset: current.asset,
4091
- toProtocol: bestRate.protocol,
4092
- toAsset: bestRate.asset,
4093
- amount: current.amount,
4094
- currentApy: current.apy,
4095
- newApy: bestRate.rates.saveApy,
4096
- annualGain,
4097
- estimatedSwapCost,
4098
- breakEvenDays,
4099
- txDigests: [],
4100
- totalGasCost: 0
4101
- };
4102
- }
4103
- if (dryRun) {
4104
- return {
4105
- executed: false,
4106
- steps,
4107
- fromProtocol: current.protocol,
4108
- fromAsset: current.asset,
4109
- toProtocol: bestRate.protocol,
4110
- toAsset: bestRate.asset,
4111
- amount: current.amount,
4112
- currentApy: current.apy,
4113
- newApy: bestRate.rates.saveApy,
4114
- annualGain,
4115
- estimatedSwapCost,
4116
- breakEvenDays,
4117
- txDigests: [],
4118
- totalGasCost: 0
4119
- };
4120
- }
4121
- if (!withdrawAdapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", `Protocol ${current.protocolId} not found`);
4122
- const depositAdapter = this.registry.getLending(bestRate.protocolId);
4123
- if (!depositAdapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", `Protocol ${bestRate.protocolId} not found`);
4124
- const canComposePTB = withdrawAdapter.addWithdrawToTx && depositAdapter.addSaveToTx && (isSameAsset || this.registry.listSwap()[0]?.addSwapToTx);
4125
- let txDigests;
4126
- let totalGasCost;
4127
- if (canComposePTB) {
4128
- const result = await executeWithGas(this.client, this.keypair, async () => {
4129
- const tx = new transactions.Transaction();
4130
- tx.setSender(this._address);
4131
- const { coin: withdrawnCoin, effectiveAmount } = await withdrawAdapter.addWithdrawToTx(
4132
- tx,
4133
- this._address,
4134
- current.amount,
4135
- current.asset
4136
- );
4137
- amountToDeposit = effectiveAmount;
4138
- let depositCoin = withdrawnCoin;
4139
- if (!isSameAsset) {
4140
- const swapAdapter = this.registry.listSwap()[0];
4141
- const { outputCoin, estimatedOut, toDecimals } = await swapAdapter.addSwapToTx(
4142
- tx,
4143
- this._address,
4144
- withdrawnCoin,
4145
- current.asset,
4146
- bestRate.asset,
4147
- amountToDeposit
4148
- );
4149
- depositCoin = outputCoin;
4150
- amountToDeposit = estimatedOut / 10 ** toDecimals;
4151
- }
4152
- await depositAdapter.addSaveToTx(
4153
- tx,
4154
- this._address,
4155
- depositCoin,
4156
- bestRate.asset,
4157
- { collectFee: bestRate.asset === "USDC" }
4158
- );
4159
- return tx;
4160
- });
4161
- txDigests = [result.digest];
4162
- totalGasCost = result.gasCostSui;
4163
- } else {
4164
- txDigests = [];
4165
- totalGasCost = 0;
4166
- const withdrawResult = await executeWithGas(this.client, this.keypair, async () => {
4167
- const built = await withdrawAdapter.buildWithdrawTx(this._address, current.amount, current.asset);
4168
- amountToDeposit = built.effectiveAmount;
4169
- return built.tx;
4170
- });
4171
- txDigests.push(withdrawResult.digest);
4172
- totalGasCost += withdrawResult.gasCostSui;
4173
- if (!isSameAsset) {
4174
- const swapAdapter = this.registry.listSwap()[0];
4175
- if (!swapAdapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", "No swap adapter available");
4176
- const swapResult = await executeWithGas(this.client, this.keypair, async () => {
4177
- const built = await swapAdapter.buildSwapTx(this._address, current.asset, bestRate.asset, amountToDeposit);
4178
- amountToDeposit = built.estimatedOut / 10 ** built.toDecimals;
4179
- return built.tx;
4180
- });
4181
- txDigests.push(swapResult.digest);
4182
- totalGasCost += swapResult.gasCostSui;
4183
- }
4184
- const depositResult = await executeWithGas(this.client, this.keypair, async () => {
4185
- const { tx } = await depositAdapter.buildSaveTx(this._address, amountToDeposit, bestRate.asset, { collectFee: bestRate.asset === "USDC" });
4186
- return tx;
4187
- });
4188
- txDigests.push(depositResult.digest);
4189
- totalGasCost += depositResult.gasCostSui;
4190
- }
4191
- return {
4192
- executed: true,
4193
- steps,
4194
- fromProtocol: current.protocol,
4195
- fromAsset: current.asset,
4196
- toProtocol: bestRate.protocol,
4197
- toAsset: bestRate.asset,
4198
- amount: current.amount,
4199
- currentApy: current.apy,
4200
- newApy: bestRate.rates.saveApy,
4201
- annualGain,
4202
- estimatedSwapCost,
4203
- breakEvenDays,
4204
- txDigests,
4205
- totalGasCost
4206
- };
4207
- }
4208
- async earnings() {
4209
- const result = await getEarnings(this.client, this.keypair);
4210
- if (result.totalYieldEarned > 0) {
4211
- this.emit("yield", {
4212
- earned: result.dailyEarning,
4213
- total: result.totalYieldEarned,
4214
- apy: result.currentApy / 100,
4215
- timestamp: Date.now()
4216
- });
4217
- }
4218
- return result;
4219
- }
4220
- async fundStatus() {
4221
- return getFundStatus(this.client, this.keypair);
4222
- }
4223
- // -- Sentinel --
4224
- async sentinelList() {
4225
- return listSentinels();
4226
- }
4227
- async sentinelInfo(id) {
4228
- return getSentinelInfo(this.client, id);
4229
- }
4230
- async sentinelAttack(id, prompt, fee) {
4231
- this.enforcer.check({ operation: "sentinel", amount: fee ? Number(fee) / 1e9 : 0.1 });
4232
- return attack(this.client, this.keypair, id, prompt, fee);
4233
- }
4234
- // -- Helpers --
4235
- async getFreeBalance(asset) {
4236
- if (!(asset in INVESTMENT_ASSETS)) return Infinity;
4237
- const pos = this.portfolio.getPosition(asset);
4238
- const invested = pos?.totalAmount ?? 0;
4239
- if (invested <= 0) return Infinity;
4240
- const assetInfo = SUPPORTED_ASSETS[asset];
4241
- const balance = await this.client.getBalance({ owner: this._address, coinType: assetInfo.type });
4242
- const walletAmount = Number(balance.totalBalance) / 10 ** assetInfo.decimals;
4243
- const gasReserve = asset === "SUI" ? GAS_RESERVE_MIN : 0;
4244
- return Math.max(0, walletAmount - invested - gasReserve);
4245
- }
4246
- async resolveLending(protocol, asset, capability) {
4247
- if (protocol) {
4248
- const adapter = this.registry.getLending(protocol);
4249
- if (!adapter) throw new T2000Error("ASSET_NOT_SUPPORTED", `Lending adapter '${protocol}' not found`);
4250
- return adapter;
4251
- }
4252
- if (capability === "save") {
4253
- const { adapter } = await this.registry.bestSaveRate(asset);
4254
- return adapter;
4255
- }
4256
- if (capability === "borrow" || capability === "repay") {
4257
- const adapters2 = this.registry.listLending().filter(
4258
- (a) => a.supportedAssets.includes(asset) && a.capabilities.includes(capability) && (capability !== "borrow" || a.supportsSameAssetBorrow)
4259
- );
4260
- if (adapters2.length === 0) {
4261
- const alternatives = this.registry.listLending().filter(
4262
- (a) => a.capabilities.includes(capability) && (capability !== "borrow" || a.supportsSameAssetBorrow)
4263
- );
4264
- if (alternatives.length > 0) {
4265
- const altList = alternatives.map((a) => a.name).join(", ");
4266
- const altAssets = [...new Set(alternatives.flatMap((a) => [...a.supportedAssets]))].join(", ");
4267
- throw new T2000Error("ASSET_NOT_SUPPORTED", `No protocol supports ${capability} for ${asset}. Available for ${capability}: ${altList} (assets: ${altAssets})`);
4268
- }
4269
- throw new T2000Error("ASSET_NOT_SUPPORTED", `No adapter supports ${capability} ${asset}`);
4270
- }
4271
- return adapters2[0];
4272
- }
4273
- const adapters = this.registry.listLending().filter(
4274
- (a) => a.supportedAssets.includes(asset) && a.capabilities.includes(capability)
4275
- );
4276
- if (adapters.length === 0) {
4277
- const alternatives = this.registry.listLending().filter(
4278
- (a) => a.capabilities.includes(capability)
4279
- );
4280
- if (alternatives.length > 0) {
4281
- const altList = alternatives.map((a) => `${a.name} (${[...a.supportedAssets].join(", ")})`).join("; ");
4282
- throw new T2000Error("ASSET_NOT_SUPPORTED", `No protocol supports ${capability} for ${asset}. Try: ${altList}`);
4283
- }
4284
- throw new T2000Error("ASSET_NOT_SUPPORTED", `No adapter supports ${capability} ${asset}`);
4285
- }
4286
- return adapters[0];
4287
- }
4288
- emitBalanceChange(asset, amount, cause, tx) {
4289
- this.emit("balanceChange", { asset, previous: 0, current: 0, cause, tx });
4290
- }
4291
- };
4292
- async function callSponsorApi(address, name) {
4293
- const res = await fetch(`${API_BASE_URL}/api/sponsor`, {
4294
- method: "POST",
4295
- headers: { "Content-Type": "application/json" },
4296
- body: JSON.stringify({ address, name })
4297
- });
4298
- if (res.status === 429) {
4299
- const data = await res.json();
4300
- if (data.challenge) {
4301
- const proof = solveHashcash(data.challenge);
4302
- const retry = await fetch(`${API_BASE_URL}/api/sponsor`, {
4303
- method: "POST",
4304
- headers: { "Content-Type": "application/json" },
4305
- body: JSON.stringify({ address, name, proof })
4306
- });
4307
- if (!retry.ok) throw new T2000Error("SPONSOR_RATE_LIMITED", "Sponsor rate limited");
4308
- return;
4309
- }
4310
- }
4311
- if (!res.ok) {
4312
- throw new T2000Error("SPONSOR_FAILED", "Sponsor API unavailable");
4313
- }
4314
- }
4315
-
4316
- // src/utils/simulate.ts
4317
- async function simulateTransaction(client, tx, sender) {
4318
- tx.setSender(sender);
4319
- try {
4320
- const txBytes = await tx.build({ client });
4321
- const dryRun = await client.dryRunTransactionBlock({
4322
- transactionBlock: Buffer.from(txBytes).toString("base64")
4323
- });
4324
- const status = dryRun.effects?.status;
4325
- const gasUsed = dryRun.effects?.gasUsed;
4326
- const gasEstimateSui = gasUsed ? (Number(gasUsed.computationCost) + Number(gasUsed.storageCost) - Number(gasUsed.storageRebate)) / 1e9 : 0;
4327
- if (status?.status === "failure") {
4328
- const rawError = status.error ?? "Unknown simulation error";
4329
- const parsed = parseMoveAbort(rawError);
4330
- return {
4331
- success: false,
4332
- gasEstimateSui,
4333
- error: {
4334
- moveAbortCode: parsed.abortCode,
4335
- moveModule: parsed.module,
4336
- reason: parsed.reason,
4337
- rawError
4338
- }
4339
- };
4340
- }
4341
- return { success: true, gasEstimateSui };
4342
- } catch (err) {
4343
- const rawError = err instanceof Error ? err.message : String(err);
4344
- return {
4345
- success: false,
4346
- gasEstimateSui: 0,
4347
- error: {
4348
- reason: "Simulation failed: " + rawError,
4349
- rawError
4350
- }
4351
- };
4352
- }
4353
- }
4354
- function throwIfSimulationFailed(sim) {
4355
- if (sim.success) return;
4356
- throw new T2000Error(
4357
- "SIMULATION_FAILED",
4358
- sim.error?.reason ?? "Transaction simulation failed",
4359
- {
4360
- moveAbortCode: sim.error?.moveAbortCode,
4361
- moveModule: sim.error?.moveModule,
4362
- reason: sim.error?.reason,
4363
- rawError: sim.error?.rawError
4364
- }
4365
- );
4366
- }
4367
- function parseMoveAbort(errorStr) {
4368
- const abortMatch = errorStr.match(/MoveAbort\([^,]*,\s*(\d+)\)/);
4369
- const moduleMatch = errorStr.match(/name:\s*Identifier\("([^"]+)"\)/);
4370
- if (abortMatch) {
4371
- const code = parseInt(abortMatch[1], 10);
4372
- const module = moduleMatch?.[1];
4373
- const reason = mapMoveAbortCode(code);
4374
- return { abortCode: code, module, reason };
4375
- }
4376
- if (errorStr.includes("MovePrimitiveRuntimeError")) {
4377
- const module = moduleMatch?.[1];
4378
- return {
4379
- module,
4380
- reason: `Move runtime error in ${module ?? "unknown"} module`
4381
- };
4382
- }
4383
- return { reason: errorStr };
4384
- }
4385
-
4386
- // src/adapters/index.ts
4387
- var allDescriptors = [
4388
- descriptor2,
4389
- descriptor4,
4390
- descriptor3,
4391
- descriptor
4392
- ];
4393
-
4394
- exports.BPS_DENOMINATOR = BPS_DENOMINATOR;
4395
- exports.CLOCK_ID = CLOCK_ID;
4396
- exports.CetusAdapter = CetusAdapter;
4397
- exports.ContactManager = ContactManager;
4398
- exports.DEFAULT_MAX_LEVERAGE = DEFAULT_MAX_LEVERAGE;
4399
- exports.DEFAULT_MAX_POSITION_SIZE = DEFAULT_MAX_POSITION_SIZE;
4400
- exports.DEFAULT_NETWORK = DEFAULT_NETWORK;
4401
- exports.DEFAULT_SAFEGUARD_CONFIG = DEFAULT_SAFEGUARD_CONFIG;
4402
- exports.GAS_RESERVE_MIN = GAS_RESERVE_MIN;
4403
- exports.INVESTMENT_ASSETS = INVESTMENT_ASSETS;
4404
- exports.MIST_PER_SUI = MIST_PER_SUI;
4405
- exports.NaviAdapter = NaviAdapter;
4406
- exports.OUTBOUND_OPS = OUTBOUND_OPS;
4407
- exports.PERPS_MARKETS = PERPS_MARKETS;
4408
- exports.PortfolioManager = PortfolioManager;
4409
- exports.ProtocolRegistry = ProtocolRegistry;
4410
- exports.SENTINEL = SENTINEL;
4411
- exports.SUI_DECIMALS = SUI_DECIMALS;
4412
- exports.SUPPORTED_ASSETS = SUPPORTED_ASSETS;
4413
- exports.SafeguardEnforcer = SafeguardEnforcer;
4414
- exports.SafeguardError = SafeguardError;
4415
- exports.SuilendAdapter = SuilendAdapter;
4416
- exports.T2000 = T2000;
4417
- exports.T2000Error = T2000Error;
4418
- exports.USDC_DECIMALS = USDC_DECIMALS;
4419
- exports.addCollectFeeToTx = addCollectFeeToTx;
4420
- exports.allDescriptors = allDescriptors;
4421
- exports.calculateFee = calculateFee;
4422
- exports.cetusDescriptor = descriptor3;
4423
- exports.executeAutoTopUp = executeAutoTopUp;
4424
- exports.executeWithGas = executeWithGas;
4425
- exports.exportPrivateKey = exportPrivateKey;
4426
- exports.formatAssetAmount = formatAssetAmount;
4427
- exports.formatSui = formatSui;
4428
- exports.formatUsd = formatUsd;
4429
- exports.generateKeypair = generateKeypair;
4430
- exports.getAddress = getAddress;
4431
- exports.getDecimals = getDecimals;
4432
- exports.getGasStatus = getGasStatus;
4433
- exports.getPoolPrice = getPoolPrice;
4434
- exports.getRates = getRates;
4435
- exports.getSentinelInfo = getSentinelInfo;
4436
- exports.keypairFromPrivateKey = keypairFromPrivateKey;
4437
- exports.listSentinels = listSentinels;
4438
- exports.loadKey = loadKey;
4439
- exports.mapMoveAbortCode = mapMoveAbortCode;
4440
- exports.mapWalletError = mapWalletError;
4441
- exports.mistToSui = mistToSui;
4442
- exports.naviDescriptor = descriptor2;
4443
- exports.rawToStable = rawToStable;
4444
- exports.rawToUsdc = rawToUsdc;
4445
- exports.requestAttack = requestAttack;
4446
- exports.saveKey = saveKey;
4447
- exports.sentinelAttack = attack;
4448
- exports.sentinelDescriptor = descriptor;
4449
- exports.settleAttack = settleAttack;
4450
- exports.shouldAutoTopUp = shouldAutoTopUp;
4451
- exports.simulateTransaction = simulateTransaction;
4452
- exports.solveHashcash = solveHashcash;
4453
- exports.stableToRaw = stableToRaw;
4454
- exports.submitPrompt = submitPrompt;
4455
- exports.suiToMist = suiToMist;
4456
- exports.suilendDescriptor = descriptor4;
4457
- exports.throwIfSimulationFailed = throwIfSimulationFailed;
4458
- exports.truncateAddress = truncateAddress;
4459
- exports.usdcToRaw = usdcToRaw;
4460
- exports.validateAddress = validateAddress;
4461
- exports.walletExists = walletExists;
4462
- //# sourceMappingURL=index.cjs.map
4463
- //# sourceMappingURL=index.cjs.map