@t2000/sdk 0.15.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1745 @@
1
+ import { Transaction } from '@mysten/sui/transactions';
2
+ import { bcs } from '@mysten/sui/bcs';
3
+ import { AggregatorClient, Env } from '@cetusprotocol/aggregator-sdk';
4
+ import { normalizeStructTag } from '@mysten/sui/utils';
5
+
6
+ // src/constants.ts
7
+ var SAVE_FEE_BPS = 10n;
8
+ var SWAP_FEE_BPS = 0n;
9
+ var BORROW_FEE_BPS = 5n;
10
+ var SUPPORTED_ASSETS = {
11
+ USDC: {
12
+ type: "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC",
13
+ decimals: 6,
14
+ symbol: "USDC",
15
+ displayName: "USDC"
16
+ },
17
+ USDT: {
18
+ type: "0x375f70cf2ae4c00bf37117d0c85a2c71545e6ee05c4a5c7d282cd66a4504b068::usdt::USDT",
19
+ decimals: 6,
20
+ symbol: "USDT",
21
+ displayName: "suiUSDT"
22
+ },
23
+ USDe: {
24
+ type: "0x41d587e5336f1c86cad50d38a7136db99333bb9bda91cea4ba69115defeb1402::sui_usde::SUI_USDE",
25
+ decimals: 6,
26
+ symbol: "USDe",
27
+ displayName: "suiUSDe"
28
+ },
29
+ USDsui: {
30
+ type: "0x44f838219cf67b058f3b37907b655f226153c18e33dfcd0da559a844fea9b1c1::usdsui::USDSUI",
31
+ decimals: 6,
32
+ symbol: "USDsui",
33
+ displayName: "USDsui"
34
+ },
35
+ SUI: {
36
+ type: "0x2::sui::SUI",
37
+ decimals: 9,
38
+ symbol: "SUI",
39
+ displayName: "SUI"
40
+ },
41
+ BTC: {
42
+ type: "0x0041f9f9344cac094454cd574e333c4fdb132d7bcc9379bcd4aab485b2a63942::wbtc::WBTC",
43
+ decimals: 8,
44
+ symbol: "BTC",
45
+ displayName: "Bitcoin"
46
+ },
47
+ ETH: {
48
+ type: "0xd0e89b2af5e4910726fbcd8b8dd37bb79b29e5f83f7491bca830e94f7f226d29::eth::ETH",
49
+ decimals: 8,
50
+ symbol: "ETH",
51
+ displayName: "Ethereum"
52
+ }
53
+ };
54
+ var STABLE_ASSETS = ["USDC", "USDT", "USDe", "USDsui"];
55
+ var T2000_PACKAGE_ID = process.env.T2000_PACKAGE_ID ?? "0xab92e9f1fe549ad3d6a52924a73181b45791e76120b975138fac9ec9b75db9f3";
56
+ var T2000_CONFIG_ID = process.env.T2000_CONFIG_ID ?? "0x408add9aa9322f93cfd87523d8f603006eb8713894f4c460283c58a6888dae8a";
57
+ var T2000_TREASURY_ID = process.env.T2000_TREASURY_ID ?? "0x3bb501b8300125dca59019247941a42af6b292a150ce3cfcce9449456be2ec91";
58
+ process.env.T2000_API_URL ?? "https://api.t2000.ai";
59
+ var CETUS_USDC_SUI_POOL = "0x51e883ba7c0b566a26cbc8a94cd33eb0abd418a77cc1e60ad22fd9b1f29cd2ab";
60
+ var CETUS_PACKAGE = "0x1eabed72c53feb3805120a081dc15963c204dc8d091542592abaf7a35689b2fb";
61
+ var INVESTMENT_ASSETS = {
62
+ SUI: SUPPORTED_ASSETS.SUI,
63
+ BTC: SUPPORTED_ASSETS.BTC,
64
+ ETH: SUPPORTED_ASSETS.ETH
65
+ };
66
+ var SENTINEL = {
67
+ PACKAGE: "0x88b83f36dafcd5f6dcdcf1d2cb5889b03f61264ab3cee9cae35db7aa940a21b7"};
68
+
69
+ // src/errors.ts
70
+ var T2000Error = class extends Error {
71
+ code;
72
+ data;
73
+ retryable;
74
+ constructor(code, message, data, retryable = false) {
75
+ super(message);
76
+ this.name = "T2000Error";
77
+ this.code = code;
78
+ this.data = data;
79
+ this.retryable = retryable;
80
+ }
81
+ toJSON() {
82
+ return {
83
+ error: this.code,
84
+ message: this.message,
85
+ ...this.data && { data: this.data },
86
+ retryable: this.retryable
87
+ };
88
+ }
89
+ };
90
+
91
+ // src/adapters/registry.ts
92
+ var ProtocolRegistry = class {
93
+ lending = /* @__PURE__ */ new Map();
94
+ swap = /* @__PURE__ */ new Map();
95
+ registerLending(adapter) {
96
+ this.lending.set(adapter.id, adapter);
97
+ }
98
+ registerSwap(adapter) {
99
+ this.swap.set(adapter.id, adapter);
100
+ }
101
+ async bestSaveRate(asset) {
102
+ const candidates = [];
103
+ for (const adapter of this.lending.values()) {
104
+ if (!adapter.supportedAssets.includes(asset)) continue;
105
+ if (!adapter.capabilities.includes("save")) continue;
106
+ try {
107
+ const rate = await adapter.getRates(asset);
108
+ candidates.push({ adapter, rate });
109
+ } catch {
110
+ }
111
+ }
112
+ if (candidates.length === 0) {
113
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `No lending adapter supports saving ${asset}`);
114
+ }
115
+ candidates.sort((a, b) => b.rate.saveApy - a.rate.saveApy);
116
+ return candidates[0];
117
+ }
118
+ async bestBorrowRate(asset, opts) {
119
+ const candidates = [];
120
+ for (const adapter of this.lending.values()) {
121
+ if (!adapter.supportedAssets.includes(asset)) continue;
122
+ if (!adapter.capabilities.includes("borrow")) continue;
123
+ if (opts?.requireSameAssetBorrow && !adapter.supportsSameAssetBorrow) continue;
124
+ try {
125
+ const rate = await adapter.getRates(asset);
126
+ candidates.push({ adapter, rate });
127
+ } catch {
128
+ }
129
+ }
130
+ if (candidates.length === 0) {
131
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `No lending adapter supports borrowing ${asset}`);
132
+ }
133
+ candidates.sort((a, b) => a.rate.borrowApy - b.rate.borrowApy);
134
+ return candidates[0];
135
+ }
136
+ async bestSwapQuote(from, to, amount) {
137
+ const candidates = [];
138
+ for (const adapter of this.swap.values()) {
139
+ const pairs = adapter.getSupportedPairs();
140
+ if (!pairs.some((p) => p.from === from && p.to === to)) continue;
141
+ try {
142
+ const quote = await adapter.getQuote(from, to, amount);
143
+ candidates.push({ adapter, quote });
144
+ } catch {
145
+ }
146
+ }
147
+ if (candidates.length === 0) {
148
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `No swap adapter supports ${from} \u2192 ${to}`);
149
+ }
150
+ candidates.sort((a, b) => b.quote.expectedOutput - a.quote.expectedOutput);
151
+ return candidates[0];
152
+ }
153
+ async bestSaveRateAcrossAssets() {
154
+ const candidates = [];
155
+ for (const asset of STABLE_ASSETS) {
156
+ for (const adapter of this.lending.values()) {
157
+ if (!adapter.supportedAssets.includes(asset)) continue;
158
+ if (!adapter.capabilities.includes("save")) continue;
159
+ try {
160
+ const rate = await adapter.getRates(asset);
161
+ candidates.push({ adapter, rate, asset });
162
+ } catch {
163
+ }
164
+ }
165
+ }
166
+ if (candidates.length === 0) {
167
+ throw new T2000Error("ASSET_NOT_SUPPORTED", "No lending adapter found for any stablecoin");
168
+ }
169
+ candidates.sort((a, b) => b.rate.saveApy - a.rate.saveApy);
170
+ return candidates[0];
171
+ }
172
+ async allRatesAcrossAssets() {
173
+ const results = [];
174
+ const allAssets = [...STABLE_ASSETS, ...Object.keys(INVESTMENT_ASSETS)];
175
+ const seen = /* @__PURE__ */ new Set();
176
+ for (const asset of allAssets) {
177
+ if (seen.has(asset)) continue;
178
+ seen.add(asset);
179
+ for (const adapter of this.lending.values()) {
180
+ if (!adapter.supportedAssets.includes(asset)) continue;
181
+ try {
182
+ const rates = await adapter.getRates(asset);
183
+ if (rates.saveApy > 0 || rates.borrowApy > 0) {
184
+ results.push({ protocol: adapter.name, protocolId: adapter.id, asset, rates });
185
+ }
186
+ } catch {
187
+ }
188
+ }
189
+ }
190
+ return results;
191
+ }
192
+ async allRates(asset) {
193
+ const results = [];
194
+ for (const adapter of this.lending.values()) {
195
+ if (!adapter.supportedAssets.includes(asset)) continue;
196
+ try {
197
+ const rates = await adapter.getRates(asset);
198
+ results.push({ protocol: adapter.name, protocolId: adapter.id, rates });
199
+ } catch {
200
+ }
201
+ }
202
+ return results;
203
+ }
204
+ async allPositions(address) {
205
+ const results = [];
206
+ for (const adapter of this.lending.values()) {
207
+ try {
208
+ const positions = await adapter.getPositions(address);
209
+ if (positions.supplies.length > 0 || positions.borrows.length > 0) {
210
+ results.push({ protocol: adapter.name, protocolId: adapter.id, positions });
211
+ }
212
+ } catch {
213
+ }
214
+ }
215
+ return results;
216
+ }
217
+ getLending(id) {
218
+ return this.lending.get(id);
219
+ }
220
+ getSwap(id) {
221
+ return this.swap.get(id);
222
+ }
223
+ listLending() {
224
+ return [...this.lending.values()];
225
+ }
226
+ listSwap() {
227
+ return [...this.swap.values()];
228
+ }
229
+ };
230
+
231
+ // src/utils/format.ts
232
+ function stableToRaw(amount, decimals) {
233
+ return BigInt(Math.round(amount * 10 ** decimals));
234
+ }
235
+ var ASSET_LOOKUP = /* @__PURE__ */ new Map();
236
+ for (const [key, info] of Object.entries(SUPPORTED_ASSETS)) {
237
+ ASSET_LOOKUP.set(key.toUpperCase(), key);
238
+ if (info.displayName && info.displayName.toUpperCase() !== key.toUpperCase()) {
239
+ ASSET_LOOKUP.set(info.displayName.toUpperCase(), key);
240
+ }
241
+ }
242
+ function normalizeAsset(input) {
243
+ return ASSET_LOOKUP.get(input.toUpperCase()) ?? input;
244
+ }
245
+
246
+ // src/protocols/protocolFee.ts
247
+ var FEE_RATES = {
248
+ save: SAVE_FEE_BPS,
249
+ swap: SWAP_FEE_BPS,
250
+ borrow: BORROW_FEE_BPS
251
+ };
252
+ var OP_CODES = {
253
+ save: 0,
254
+ swap: 1,
255
+ borrow: 2
256
+ };
257
+ function addCollectFeeToTx(tx, paymentCoin, operation) {
258
+ const bps = FEE_RATES[operation];
259
+ if (bps <= 0n) return;
260
+ tx.moveCall({
261
+ target: `${T2000_PACKAGE_ID}::treasury::collect_fee`,
262
+ typeArguments: [SUPPORTED_ASSETS.USDC.type],
263
+ arguments: [
264
+ tx.object(T2000_TREASURY_ID),
265
+ tx.object(T2000_CONFIG_ID),
266
+ paymentCoin,
267
+ tx.pure.u8(OP_CODES[operation])
268
+ ]
269
+ });
270
+ }
271
+ var RATE_DECIMALS = 27;
272
+ var LTV_DECIMALS = 27;
273
+ var MIN_HEALTH_FACTOR = 1.5;
274
+ var WITHDRAW_DUST_BUFFER = 1e-3;
275
+ var CLOCK = "0x06";
276
+ var SUI_SYSTEM_STATE = "0x05";
277
+ var NAVI_BALANCE_DECIMALS = 9;
278
+ var CONFIG_API = "https://open-api.naviprotocol.io/api/navi/config?env=prod";
279
+ var POOLS_API = "https://open-api.naviprotocol.io/api/navi/pools?env=prod";
280
+ var PACKAGE_API = "https://open-api.naviprotocol.io/api/package";
281
+ var packageCache = null;
282
+ function toBigInt(v) {
283
+ if (typeof v === "bigint") return v;
284
+ return BigInt(String(v));
285
+ }
286
+ var UserStateInfo = bcs.struct("UserStateInfo", {
287
+ asset_id: bcs.u8(),
288
+ borrow_balance: bcs.u256(),
289
+ supply_balance: bcs.u256()
290
+ });
291
+ function decodeDevInspect(result, schema) {
292
+ const rv = result.results?.[0]?.returnValues?.[0];
293
+ if (result.error || !rv) return void 0;
294
+ const bytes = Uint8Array.from(rv[0]);
295
+ return schema.parse(bytes);
296
+ }
297
+ var configCache = null;
298
+ var poolsCache = null;
299
+ var CACHE_TTL = 5 * 6e4;
300
+ async function fetchJson(url) {
301
+ const res = await fetch(url);
302
+ if (!res.ok) throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI API error: ${res.status}`);
303
+ const json = await res.json();
304
+ return json.data ?? json;
305
+ }
306
+ async function getLatestPackageId() {
307
+ if (packageCache && Date.now() - packageCache.ts < CACHE_TTL) return packageCache.id;
308
+ const res = await fetch(PACKAGE_API);
309
+ if (!res.ok) throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI package API error: ${res.status}`);
310
+ const json = await res.json();
311
+ if (!json.packageId) throw new T2000Error("PROTOCOL_UNAVAILABLE", "NAVI package API returned no packageId");
312
+ packageCache = { id: json.packageId, ts: Date.now() };
313
+ return json.packageId;
314
+ }
315
+ async function getConfig(fresh = false) {
316
+ if (configCache && !fresh && Date.now() - configCache.ts < CACHE_TTL) return configCache.data;
317
+ const [data, latestPkg] = await Promise.all([
318
+ fetchJson(CONFIG_API),
319
+ getLatestPackageId()
320
+ ]);
321
+ data.package = latestPkg;
322
+ configCache = { data, ts: Date.now() };
323
+ return data;
324
+ }
325
+ async function getPools(fresh = false) {
326
+ if (poolsCache && !fresh && Date.now() - poolsCache.ts < CACHE_TTL) return poolsCache.data;
327
+ const data = await fetchJson(POOLS_API);
328
+ poolsCache = { data, ts: Date.now() };
329
+ return data;
330
+ }
331
+ function matchesCoinType(poolType, targetType) {
332
+ const poolSuffix = poolType.split("::").slice(1).join("::").toLowerCase();
333
+ const targetSuffix = targetType.split("::").slice(1).join("::").toLowerCase();
334
+ return poolSuffix === targetSuffix;
335
+ }
336
+ function resolvePoolSymbol(pool) {
337
+ const coinType = pool.suiCoinType || pool.coinType || "";
338
+ for (const [key, info] of Object.entries(SUPPORTED_ASSETS)) {
339
+ if (matchesCoinType(coinType, info.type)) return key;
340
+ }
341
+ return pool.token?.symbol ?? "UNKNOWN";
342
+ }
343
+ function resolveAssetInfo(asset) {
344
+ if (asset in SUPPORTED_ASSETS) {
345
+ const info = SUPPORTED_ASSETS[asset];
346
+ return { type: info.type, decimals: info.decimals, displayName: info.displayName };
347
+ }
348
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `Unknown asset: ${asset}`);
349
+ }
350
+ async function getPool(asset = "USDC") {
351
+ const pools = await getPools();
352
+ const { type: targetType, displayName } = resolveAssetInfo(asset);
353
+ const pool = pools.find(
354
+ (p) => matchesCoinType(p.suiCoinType || p.coinType || "", targetType)
355
+ );
356
+ if (!pool) {
357
+ throw new T2000Error(
358
+ "ASSET_NOT_SUPPORTED",
359
+ `${displayName} pool not found on NAVI`
360
+ );
361
+ }
362
+ return pool;
363
+ }
364
+ function addOracleUpdate(tx, config, pool) {
365
+ const feed = config.oracle.feeds?.find((f2) => f2.assetId === pool.id);
366
+ if (!feed) {
367
+ throw new T2000Error("PROTOCOL_UNAVAILABLE", `Oracle feed not found for asset ${pool.token?.symbol ?? pool.id}`);
368
+ }
369
+ tx.moveCall({
370
+ target: `${config.oracle.packageId}::oracle_pro::update_single_price_v2`,
371
+ arguments: [
372
+ tx.object(CLOCK),
373
+ tx.object(config.oracle.oracleConfig),
374
+ tx.object(config.oracle.priceOracle),
375
+ tx.object(config.oracle.supraOracleHolder),
376
+ tx.object(feed.pythPriceInfoObject),
377
+ tx.object(config.oracle.switchboardAggregator),
378
+ tx.pure.address(feed.feedId)
379
+ ]
380
+ });
381
+ }
382
+ function refreshOracles(tx, config, pools, opts) {
383
+ const assetsToRefresh = NAVI_SUPPORTED_ASSETS;
384
+ const targetTypes = assetsToRefresh.map((a) => SUPPORTED_ASSETS[a].type);
385
+ const matchedPools = pools.filter((p) => {
386
+ const ct = p.suiCoinType || p.coinType || "";
387
+ return targetTypes.some((t) => matchesCoinType(ct, t));
388
+ });
389
+ for (const pool of matchedPools) {
390
+ addOracleUpdate(tx, config, pool);
391
+ }
392
+ }
393
+ function rateToApy(rawRate) {
394
+ if (!rawRate || rawRate === "0") return 0;
395
+ return Number(BigInt(rawRate)) / 10 ** RATE_DECIMALS * 100;
396
+ }
397
+ function parseLtv(rawLtv) {
398
+ if (!rawLtv || rawLtv === "0") return 0.75;
399
+ return Number(BigInt(rawLtv)) / 10 ** LTV_DECIMALS;
400
+ }
401
+ function parseLiqThreshold(val) {
402
+ if (typeof val === "number") return val;
403
+ const n = Number(val);
404
+ if (n > 1) return Number(BigInt(val)) / 10 ** LTV_DECIMALS;
405
+ return n;
406
+ }
407
+ function normalizeHealthFactor(raw) {
408
+ const v = raw / 10 ** RATE_DECIMALS;
409
+ return v > 1e5 ? Infinity : v;
410
+ }
411
+ function compoundBalance(rawBalance, currentIndex) {
412
+ if (!rawBalance || !currentIndex || currentIndex === "0") return 0;
413
+ const scale = BigInt("1" + "0".repeat(RATE_DECIMALS));
414
+ const half = scale / 2n;
415
+ const result = (rawBalance * BigInt(currentIndex) + half) / scale;
416
+ return Number(result) / 10 ** NAVI_BALANCE_DECIMALS;
417
+ }
418
+ async function getUserState(client, address) {
419
+ const config = await getConfig();
420
+ const tx = new Transaction();
421
+ tx.moveCall({
422
+ target: `${config.uiGetter}::getter_unchecked::get_user_state`,
423
+ arguments: [tx.object(config.storage), tx.pure.address(address)]
424
+ });
425
+ const result = await client.devInspectTransactionBlock({
426
+ transactionBlock: tx,
427
+ sender: address
428
+ });
429
+ const decoded = decodeDevInspect(result, bcs.vector(UserStateInfo));
430
+ if (!decoded) return [];
431
+ const mapped = decoded.map((s) => ({
432
+ assetId: s.asset_id,
433
+ supplyBalance: toBigInt(s.supply_balance),
434
+ borrowBalance: toBigInt(s.borrow_balance)
435
+ }));
436
+ return mapped.filter((s) => s.supplyBalance !== 0n || s.borrowBalance !== 0n);
437
+ }
438
+ async function fetchCoins(client, owner, coinType) {
439
+ const all = [];
440
+ let cursor;
441
+ let hasNext = true;
442
+ while (hasNext) {
443
+ const page = await client.getCoins({ owner, coinType, cursor: cursor ?? void 0 });
444
+ all.push(...page.data.map((c) => ({ coinObjectId: c.coinObjectId, balance: c.balance })));
445
+ cursor = page.nextCursor;
446
+ hasNext = page.hasNextPage;
447
+ }
448
+ return all;
449
+ }
450
+ function mergeCoins(tx, coins) {
451
+ if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No coins to merge");
452
+ const primary = tx.object(coins[0].coinObjectId);
453
+ if (coins.length > 1) {
454
+ tx.mergeCoins(primary, coins.slice(1).map((c) => tx.object(c.coinObjectId)));
455
+ }
456
+ return primary;
457
+ }
458
+ async function buildSaveTx(client, address, amount, options = {}) {
459
+ if (!amount || amount <= 0 || !Number.isFinite(amount)) {
460
+ throw new T2000Error("INVALID_AMOUNT", "Save amount must be a positive number");
461
+ }
462
+ const asset = options.asset ?? "USDC";
463
+ const assetInfo = resolveAssetInfo(asset);
464
+ const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
465
+ const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
466
+ const coins = await fetchCoins(client, address, assetInfo.type);
467
+ if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins found`);
468
+ const tx = new Transaction();
469
+ tx.setSender(address);
470
+ const coinObj = mergeCoins(tx, coins);
471
+ if (options.collectFee) {
472
+ addCollectFeeToTx(tx, coinObj, "save");
473
+ }
474
+ tx.moveCall({
475
+ target: `${config.package}::incentive_v3::entry_deposit`,
476
+ arguments: [
477
+ tx.object(CLOCK),
478
+ tx.object(config.storage),
479
+ tx.object(pool.contract.pool),
480
+ tx.pure.u8(pool.id),
481
+ coinObj,
482
+ tx.pure.u64(rawAmount),
483
+ tx.object(config.incentiveV2),
484
+ tx.object(config.incentiveV3)
485
+ ],
486
+ typeArguments: [pool.suiCoinType]
487
+ });
488
+ return tx;
489
+ }
490
+ async function buildWithdrawTx(client, address, amount, options = {}) {
491
+ const asset = options.asset ?? "USDC";
492
+ const assetInfo = resolveAssetInfo(asset);
493
+ const [config, pool, pools, states] = await Promise.all([
494
+ getConfig(),
495
+ getPool(asset),
496
+ getPools(),
497
+ getUserState(client, address)
498
+ ]);
499
+ const assetState = states.find((s) => s.assetId === pool.id);
500
+ const deposited = assetState ? compoundBalance(assetState.supplyBalance, pool.currentSupplyIndex) : 0;
501
+ const effectiveAmount = Math.min(amount, Math.max(0, deposited - WITHDRAW_DUST_BUFFER));
502
+ if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on NAVI`);
503
+ const rawAmount = Number(stableToRaw(effectiveAmount, assetInfo.decimals));
504
+ if (rawAmount <= 0) {
505
+ throw new T2000Error("INVALID_AMOUNT", `Withdrawal amount rounds to zero \u2014 balance is dust`);
506
+ }
507
+ const tx = new Transaction();
508
+ tx.setSender(address);
509
+ refreshOracles(tx, config, pools);
510
+ const [balance] = tx.moveCall({
511
+ target: `${config.package}::incentive_v3::withdraw_v2`,
512
+ arguments: [
513
+ tx.object(CLOCK),
514
+ tx.object(config.oracle.priceOracle),
515
+ tx.object(config.storage),
516
+ tx.object(pool.contract.pool),
517
+ tx.pure.u8(pool.id),
518
+ tx.pure.u64(rawAmount),
519
+ tx.object(config.incentiveV2),
520
+ tx.object(config.incentiveV3),
521
+ tx.object(SUI_SYSTEM_STATE)
522
+ ],
523
+ typeArguments: [pool.suiCoinType]
524
+ });
525
+ const [coin] = tx.moveCall({
526
+ target: "0x2::coin::from_balance",
527
+ arguments: [balance],
528
+ typeArguments: [pool.suiCoinType]
529
+ });
530
+ tx.transferObjects([coin], address);
531
+ return { tx, effectiveAmount };
532
+ }
533
+ async function addWithdrawToTx(tx, client, address, amount, options = {}) {
534
+ const asset = options.asset ?? "USDC";
535
+ const assetInfo = resolveAssetInfo(asset);
536
+ const [config, pool, pools, states] = await Promise.all([
537
+ getConfig(),
538
+ getPool(asset),
539
+ getPools(),
540
+ getUserState(client, address)
541
+ ]);
542
+ const assetState = states.find((s) => s.assetId === pool.id);
543
+ const deposited = assetState ? compoundBalance(assetState.supplyBalance, pool.currentSupplyIndex) : 0;
544
+ const effectiveAmount = Math.min(amount, Math.max(0, deposited - WITHDRAW_DUST_BUFFER));
545
+ if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on NAVI`);
546
+ const rawAmount = Number(stableToRaw(effectiveAmount, assetInfo.decimals));
547
+ if (rawAmount <= 0) {
548
+ const [coin2] = tx.moveCall({
549
+ target: "0x2::coin::zero",
550
+ typeArguments: [pool.suiCoinType]
551
+ });
552
+ return { coin: coin2, effectiveAmount: 0 };
553
+ }
554
+ refreshOracles(tx, config, pools);
555
+ const [balance] = tx.moveCall({
556
+ target: `${config.package}::incentive_v3::withdraw_v2`,
557
+ arguments: [
558
+ tx.object(CLOCK),
559
+ tx.object(config.oracle.priceOracle),
560
+ tx.object(config.storage),
561
+ tx.object(pool.contract.pool),
562
+ tx.pure.u8(pool.id),
563
+ tx.pure.u64(rawAmount),
564
+ tx.object(config.incentiveV2),
565
+ tx.object(config.incentiveV3),
566
+ tx.object(SUI_SYSTEM_STATE)
567
+ ],
568
+ typeArguments: [pool.suiCoinType]
569
+ });
570
+ const [coin] = tx.moveCall({
571
+ target: "0x2::coin::from_balance",
572
+ arguments: [balance],
573
+ typeArguments: [pool.suiCoinType]
574
+ });
575
+ return { coin, effectiveAmount };
576
+ }
577
+ async function addSaveToTx(tx, _client, _address, coin, options = {}) {
578
+ const asset = options.asset ?? "USDC";
579
+ const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
580
+ if (options.collectFee) {
581
+ addCollectFeeToTx(tx, coin, "save");
582
+ }
583
+ const [coinValue] = tx.moveCall({
584
+ target: "0x2::coin::value",
585
+ typeArguments: [pool.suiCoinType],
586
+ arguments: [coin]
587
+ });
588
+ tx.moveCall({
589
+ target: `${config.package}::incentive_v3::entry_deposit`,
590
+ arguments: [
591
+ tx.object(CLOCK),
592
+ tx.object(config.storage),
593
+ tx.object(pool.contract.pool),
594
+ tx.pure.u8(pool.id),
595
+ coin,
596
+ coinValue,
597
+ tx.object(config.incentiveV2),
598
+ tx.object(config.incentiveV3)
599
+ ],
600
+ typeArguments: [pool.suiCoinType]
601
+ });
602
+ }
603
+ async function addRepayToTx(tx, _client, _address, coin, options = {}) {
604
+ const asset = options.asset ?? "USDC";
605
+ const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
606
+ addOracleUpdate(tx, config, pool);
607
+ const [coinValue] = tx.moveCall({
608
+ target: "0x2::coin::value",
609
+ typeArguments: [pool.suiCoinType],
610
+ arguments: [coin]
611
+ });
612
+ tx.moveCall({
613
+ target: `${config.package}::incentive_v3::entry_repay`,
614
+ arguments: [
615
+ tx.object(CLOCK),
616
+ tx.object(config.oracle.priceOracle),
617
+ tx.object(config.storage),
618
+ tx.object(pool.contract.pool),
619
+ tx.pure.u8(pool.id),
620
+ coin,
621
+ coinValue,
622
+ tx.object(config.incentiveV2),
623
+ tx.object(config.incentiveV3)
624
+ ],
625
+ typeArguments: [pool.suiCoinType]
626
+ });
627
+ }
628
+ async function buildBorrowTx(client, address, amount, options = {}) {
629
+ if (!amount || amount <= 0 || !Number.isFinite(amount)) {
630
+ throw new T2000Error("INVALID_AMOUNT", "Borrow amount must be a positive number");
631
+ }
632
+ const asset = options.asset ?? "USDC";
633
+ const assetInfo = resolveAssetInfo(asset);
634
+ const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
635
+ const [config, pool, pools] = await Promise.all([
636
+ getConfig(),
637
+ getPool(asset),
638
+ getPools()
639
+ ]);
640
+ const tx = new Transaction();
641
+ tx.setSender(address);
642
+ refreshOracles(tx, config, pools);
643
+ const [balance] = tx.moveCall({
644
+ target: `${config.package}::incentive_v3::borrow_v2`,
645
+ arguments: [
646
+ tx.object(CLOCK),
647
+ tx.object(config.oracle.priceOracle),
648
+ tx.object(config.storage),
649
+ tx.object(pool.contract.pool),
650
+ tx.pure.u8(pool.id),
651
+ tx.pure.u64(rawAmount),
652
+ tx.object(config.incentiveV2),
653
+ tx.object(config.incentiveV3),
654
+ tx.object(SUI_SYSTEM_STATE)
655
+ ],
656
+ typeArguments: [pool.suiCoinType]
657
+ });
658
+ const [borrowedCoin] = tx.moveCall({
659
+ target: "0x2::coin::from_balance",
660
+ arguments: [balance],
661
+ typeArguments: [pool.suiCoinType]
662
+ });
663
+ if (options.collectFee) {
664
+ addCollectFeeToTx(tx, borrowedCoin, "borrow");
665
+ }
666
+ tx.transferObjects([borrowedCoin], address);
667
+ return tx;
668
+ }
669
+ async function buildRepayTx(client, address, amount, options = {}) {
670
+ if (!amount || amount <= 0 || !Number.isFinite(amount)) {
671
+ throw new T2000Error("INVALID_AMOUNT", "Repay amount must be a positive number");
672
+ }
673
+ const asset = options.asset ?? "USDC";
674
+ const assetInfo = resolveAssetInfo(asset);
675
+ const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
676
+ const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
677
+ const coins = await fetchCoins(client, address, assetInfo.type);
678
+ if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins to repay with`);
679
+ const tx = new Transaction();
680
+ tx.setSender(address);
681
+ addOracleUpdate(tx, config, pool);
682
+ const coinObj = mergeCoins(tx, coins);
683
+ tx.moveCall({
684
+ target: `${config.package}::incentive_v3::entry_repay`,
685
+ arguments: [
686
+ tx.object(CLOCK),
687
+ tx.object(config.oracle.priceOracle),
688
+ tx.object(config.storage),
689
+ tx.object(pool.contract.pool),
690
+ tx.pure.u8(pool.id),
691
+ coinObj,
692
+ tx.pure.u64(rawAmount),
693
+ tx.object(config.incentiveV2),
694
+ tx.object(config.incentiveV3)
695
+ ],
696
+ typeArguments: [pool.suiCoinType]
697
+ });
698
+ return tx;
699
+ }
700
+ async function getHealthFactor(client, addressOrKeypair) {
701
+ const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
702
+ const [config, pools, states] = await Promise.all([
703
+ getConfig(),
704
+ getPools(),
705
+ getUserState(client, address)
706
+ ]);
707
+ let supplied = 0;
708
+ let borrowed = 0;
709
+ let weightedLtv = 0;
710
+ let weightedLiqThreshold = 0;
711
+ for (const state of states) {
712
+ const pool = pools.find((p) => p.id === state.assetId);
713
+ if (!pool) continue;
714
+ const supplyBal = compoundBalance(state.supplyBalance, pool.currentSupplyIndex);
715
+ const borrowBal = compoundBalance(state.borrowBalance, pool.currentBorrowIndex);
716
+ const price = pool.token?.price ?? 1;
717
+ supplied += supplyBal * price;
718
+ borrowed += borrowBal * price;
719
+ if (supplyBal > 0) {
720
+ weightedLtv += supplyBal * price * parseLtv(pool.ltv);
721
+ weightedLiqThreshold += supplyBal * price * parseLiqThreshold(pool.liquidationFactor.threshold);
722
+ }
723
+ }
724
+ const ltv = supplied > 0 ? weightedLtv / supplied : 0.75;
725
+ const liqThreshold = supplied > 0 ? weightedLiqThreshold / supplied : 0.75;
726
+ const maxBorrowVal = Math.max(0, supplied * ltv - borrowed);
727
+ const usdcPool = pools.find((p) => matchesCoinType(p.suiCoinType || p.coinType || "", SUPPORTED_ASSETS.USDC.type));
728
+ let healthFactor;
729
+ if (borrowed <= 0) {
730
+ healthFactor = Infinity;
731
+ } else if (usdcPool) {
732
+ try {
733
+ const tx = new Transaction();
734
+ tx.moveCall({
735
+ target: `${config.uiGetter}::calculator_unchecked::dynamic_health_factor`,
736
+ arguments: [
737
+ tx.object(CLOCK),
738
+ tx.object(config.storage),
739
+ tx.object(config.oracle.priceOracle),
740
+ tx.pure.u8(usdcPool.id),
741
+ tx.pure.address(address),
742
+ tx.pure.u8(usdcPool.id),
743
+ tx.pure.u64(0),
744
+ tx.pure.u64(0),
745
+ tx.pure.bool(false)
746
+ ],
747
+ typeArguments: [usdcPool.suiCoinType]
748
+ });
749
+ const result = await client.devInspectTransactionBlock({
750
+ transactionBlock: tx,
751
+ sender: address
752
+ });
753
+ const decoded = decodeDevInspect(result, bcs.u256());
754
+ if (decoded !== void 0) {
755
+ healthFactor = normalizeHealthFactor(Number(decoded));
756
+ } else {
757
+ healthFactor = supplied * liqThreshold / borrowed;
758
+ }
759
+ } catch {
760
+ healthFactor = supplied * liqThreshold / borrowed;
761
+ }
762
+ } else {
763
+ healthFactor = supplied * liqThreshold / borrowed;
764
+ }
765
+ return {
766
+ healthFactor,
767
+ supplied,
768
+ borrowed,
769
+ maxBorrow: maxBorrowVal,
770
+ liquidationThreshold: liqThreshold
771
+ };
772
+ }
773
+ var NAVI_SUPPORTED_ASSETS = [...STABLE_ASSETS, "SUI", "ETH"];
774
+ async function getRates(client) {
775
+ try {
776
+ const pools = await getPools();
777
+ const result = {};
778
+ for (const asset of NAVI_SUPPORTED_ASSETS) {
779
+ const targetType = SUPPORTED_ASSETS[asset].type;
780
+ const pool = pools.find((p) => matchesCoinType(p.suiCoinType || p.coinType || "", targetType));
781
+ if (!pool) continue;
782
+ let saveApy = rateToApy(pool.currentSupplyRate);
783
+ let borrowApy = rateToApy(pool.currentBorrowRate);
784
+ if (saveApy <= 0 || saveApy > 100) saveApy = 0;
785
+ if (borrowApy <= 0 || borrowApy > 100) borrowApy = 0;
786
+ result[asset] = { saveApy, borrowApy };
787
+ }
788
+ if (!result.USDC) result.USDC = { saveApy: 4, borrowApy: 6 };
789
+ return result;
790
+ } catch {
791
+ return { USDC: { saveApy: 4, borrowApy: 6 } };
792
+ }
793
+ }
794
+ async function getPositions(client, addressOrKeypair) {
795
+ const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
796
+ const [states, pools] = await Promise.all([getUserState(client, address), getPools()]);
797
+ const positions = [];
798
+ for (const state of states) {
799
+ const pool = pools.find((p) => p.id === state.assetId);
800
+ if (!pool) continue;
801
+ const symbol = resolvePoolSymbol(pool);
802
+ const supplyBal = compoundBalance(state.supplyBalance, pool.currentSupplyIndex);
803
+ const borrowBal = compoundBalance(state.borrowBalance, pool.currentBorrowIndex);
804
+ if (supplyBal > 1e-4) {
805
+ positions.push({
806
+ protocol: "navi",
807
+ asset: symbol,
808
+ type: "save",
809
+ amount: supplyBal,
810
+ apy: rateToApy(pool.currentSupplyRate)
811
+ });
812
+ }
813
+ if (borrowBal > 1e-4) {
814
+ positions.push({
815
+ protocol: "navi",
816
+ asset: symbol,
817
+ type: "borrow",
818
+ amount: borrowBal,
819
+ apy: rateToApy(pool.currentBorrowRate)
820
+ });
821
+ }
822
+ }
823
+ return { positions };
824
+ }
825
+ async function maxWithdrawAmount(client, addressOrKeypair) {
826
+ const hf = await getHealthFactor(client, addressOrKeypair);
827
+ const ltv = hf.liquidationThreshold > 0 ? hf.liquidationThreshold : 0.75;
828
+ let maxAmount;
829
+ if (hf.borrowed === 0) {
830
+ maxAmount = hf.supplied;
831
+ } else {
832
+ maxAmount = Math.max(0, hf.supplied - hf.borrowed * MIN_HEALTH_FACTOR / ltv);
833
+ }
834
+ const remainingSupply = hf.supplied - maxAmount;
835
+ const hfAfter = hf.borrowed > 0 ? remainingSupply / hf.borrowed : Infinity;
836
+ return { maxAmount, healthFactorAfter: hfAfter, currentHF: hf.healthFactor };
837
+ }
838
+ async function maxBorrowAmount(client, addressOrKeypair) {
839
+ const hf = await getHealthFactor(client, addressOrKeypair);
840
+ const ltv = hf.liquidationThreshold > 0 ? hf.liquidationThreshold : 0.75;
841
+ const maxAmount = Math.max(0, hf.supplied * ltv / MIN_HEALTH_FACTOR - hf.borrowed);
842
+ return { maxAmount, healthFactorAfter: MIN_HEALTH_FACTOR, currentHF: hf.healthFactor };
843
+ }
844
+
845
+ // src/adapters/navi.ts
846
+ var descriptor = {
847
+ id: "navi",
848
+ name: "NAVI Protocol",
849
+ packages: [],
850
+ dynamicPackageId: true,
851
+ actionMap: {
852
+ "incentive_v3::entry_deposit": "save",
853
+ "incentive_v3::deposit": "save",
854
+ "incentive_v3::withdraw_v2": "withdraw",
855
+ "incentive_v3::entry_withdraw": "withdraw",
856
+ "incentive_v3::borrow_v2": "borrow",
857
+ "incentive_v3::entry_borrow": "borrow",
858
+ "incentive_v3::entry_repay": "repay",
859
+ "incentive_v3::repay": "repay"
860
+ }
861
+ };
862
+ var NaviAdapter = class {
863
+ id = "navi";
864
+ name = "NAVI Protocol";
865
+ version = "1.0.0";
866
+ capabilities = ["save", "withdraw", "borrow", "repay"];
867
+ supportedAssets = [...STABLE_ASSETS, "SUI", "ETH"];
868
+ supportsSameAssetBorrow = true;
869
+ client;
870
+ async init(client) {
871
+ this.client = client;
872
+ }
873
+ initSync(client) {
874
+ this.client = client;
875
+ }
876
+ async getRates(asset) {
877
+ const rates = await getRates(this.client);
878
+ const normalized = normalizeAsset(asset);
879
+ const r = rates[normalized];
880
+ if (!r) throw new T2000Error("ASSET_NOT_SUPPORTED", `NAVI does not support ${asset}`);
881
+ return { asset: normalized, saveApy: r.saveApy, borrowApy: r.borrowApy };
882
+ }
883
+ async getPositions(address) {
884
+ const result = await getPositions(this.client, address);
885
+ return {
886
+ supplies: result.positions.filter((p) => p.type === "save").map((p) => ({ asset: p.asset, amount: p.amount, apy: p.apy })),
887
+ borrows: result.positions.filter((p) => p.type === "borrow").map((p) => ({ asset: p.asset, amount: p.amount, apy: p.apy }))
888
+ };
889
+ }
890
+ async getHealth(address) {
891
+ return getHealthFactor(this.client, address);
892
+ }
893
+ async buildSaveTx(address, amount, asset, options) {
894
+ const normalized = normalizeAsset(asset);
895
+ const tx = await buildSaveTx(this.client, address, amount, { ...options, asset: normalized });
896
+ return { tx };
897
+ }
898
+ async buildWithdrawTx(address, amount, asset) {
899
+ const normalized = normalizeAsset(asset);
900
+ const result = await buildWithdrawTx(this.client, address, amount, { asset: normalized });
901
+ return { tx: result.tx, effectiveAmount: result.effectiveAmount };
902
+ }
903
+ async buildBorrowTx(address, amount, asset, options) {
904
+ const normalized = normalizeAsset(asset);
905
+ const tx = await buildBorrowTx(this.client, address, amount, { ...options, asset: normalized });
906
+ return { tx };
907
+ }
908
+ async buildRepayTx(address, amount, asset) {
909
+ const normalized = normalizeAsset(asset);
910
+ const tx = await buildRepayTx(this.client, address, amount, { asset: normalized });
911
+ return { tx };
912
+ }
913
+ async maxWithdraw(address, _asset) {
914
+ return maxWithdrawAmount(this.client, address);
915
+ }
916
+ async maxBorrow(address, _asset) {
917
+ return maxBorrowAmount(this.client, address);
918
+ }
919
+ async addWithdrawToTx(tx, address, amount, asset) {
920
+ const normalized = normalizeAsset(asset);
921
+ return addWithdrawToTx(tx, this.client, address, amount, { asset: normalized });
922
+ }
923
+ async addSaveToTx(tx, address, coin, asset, options) {
924
+ const normalized = normalizeAsset(asset);
925
+ return addSaveToTx(tx, this.client, address, coin, { ...options, asset: normalized });
926
+ }
927
+ async addRepayToTx(tx, address, coin, asset) {
928
+ const normalized = normalizeAsset(asset);
929
+ return addRepayToTx(tx, this.client, address, coin, { asset: normalized });
930
+ }
931
+ };
932
+ var DEFAULT_SLIPPAGE_BPS = 300;
933
+ function createAggregatorClient(client, signer) {
934
+ return new AggregatorClient({
935
+ client,
936
+ signer,
937
+ env: Env.Mainnet
938
+ });
939
+ }
940
+ async function buildSwapTx(params) {
941
+ const { client, address, fromAsset, toAsset, amount, maxSlippageBps = DEFAULT_SLIPPAGE_BPS } = params;
942
+ const fromInfo = SUPPORTED_ASSETS[fromAsset];
943
+ const toInfo = SUPPORTED_ASSETS[toAsset];
944
+ if (!fromInfo || !toInfo) {
945
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `Swap pair ${fromAsset}/${toAsset} is not supported`);
946
+ }
947
+ const rawAmount = BigInt(Math.floor(amount * 10 ** fromInfo.decimals));
948
+ const aggClient = createAggregatorClient(client, address);
949
+ const result = await aggClient.findRouters({
950
+ from: fromInfo.type,
951
+ target: toInfo.type,
952
+ amount: rawAmount,
953
+ byAmountIn: true
954
+ });
955
+ if (!result || result.insufficientLiquidity) {
956
+ throw new T2000Error(
957
+ "ASSET_NOT_SUPPORTED",
958
+ `No swap route found for ${fromAsset} \u2192 ${toAsset}`
959
+ );
960
+ }
961
+ const tx = new Transaction();
962
+ const slippage = maxSlippageBps / 1e4;
963
+ await aggClient.fastRouterSwap({
964
+ router: result,
965
+ txb: tx,
966
+ slippage
967
+ });
968
+ const estimatedOut = Number(result.amountOut.toString());
969
+ return {
970
+ tx,
971
+ estimatedOut,
972
+ toDecimals: toInfo.decimals
973
+ };
974
+ }
975
+ async function addSwapToTx(params) {
976
+ const { tx, client, address, inputCoin, fromAsset, toAsset, amount, maxSlippageBps = DEFAULT_SLIPPAGE_BPS } = params;
977
+ const fromInfo = SUPPORTED_ASSETS[fromAsset];
978
+ const toInfo = SUPPORTED_ASSETS[toAsset];
979
+ if (!fromInfo || !toInfo) {
980
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `Swap pair ${fromAsset}/${toAsset} is not supported`);
981
+ }
982
+ const rawAmount = BigInt(Math.floor(amount * 10 ** fromInfo.decimals));
983
+ const aggClient = createAggregatorClient(client, address);
984
+ const result = await aggClient.findRouters({
985
+ from: fromInfo.type,
986
+ target: toInfo.type,
987
+ amount: rawAmount,
988
+ byAmountIn: true
989
+ });
990
+ if (!result || result.insufficientLiquidity) {
991
+ throw new T2000Error(
992
+ "ASSET_NOT_SUPPORTED",
993
+ `No swap route found for ${fromAsset} \u2192 ${toAsset}`
994
+ );
995
+ }
996
+ const slippage = maxSlippageBps / 1e4;
997
+ const outputCoin = await aggClient.routerSwap({
998
+ router: result,
999
+ txb: tx,
1000
+ inputCoin,
1001
+ slippage
1002
+ });
1003
+ const estimatedOut = Number(result.amountOut.toString());
1004
+ return {
1005
+ outputCoin,
1006
+ estimatedOut,
1007
+ toDecimals: toInfo.decimals
1008
+ };
1009
+ }
1010
+ async function getPoolPrice(client) {
1011
+ try {
1012
+ const pool = await client.getObject({
1013
+ id: CETUS_USDC_SUI_POOL,
1014
+ options: { showContent: true }
1015
+ });
1016
+ if (pool.data?.content?.dataType === "moveObject") {
1017
+ const fields = pool.data.content.fields;
1018
+ const currentSqrtPrice = BigInt(String(fields.current_sqrt_price ?? "0"));
1019
+ if (currentSqrtPrice > 0n) {
1020
+ const Q64 = 2n ** 64n;
1021
+ const sqrtPriceFloat = Number(currentSqrtPrice) / Number(Q64);
1022
+ const rawPrice = sqrtPriceFloat * sqrtPriceFloat;
1023
+ const suiPriceUsd = 1e3 / rawPrice;
1024
+ if (suiPriceUsd > 0.01 && suiPriceUsd < 1e3) return suiPriceUsd;
1025
+ }
1026
+ }
1027
+ } catch {
1028
+ }
1029
+ return 3.5;
1030
+ }
1031
+ async function getSwapQuote(client, fromAsset, toAsset, amount) {
1032
+ const fromInfo = SUPPORTED_ASSETS[fromAsset];
1033
+ const toInfo = SUPPORTED_ASSETS[toAsset];
1034
+ if (!fromInfo || !toInfo) {
1035
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `Swap pair ${fromAsset}/${toAsset} is not supported`);
1036
+ }
1037
+ const rawAmount = BigInt(Math.floor(amount * 10 ** fromInfo.decimals));
1038
+ const poolPrice = await getPoolPrice(client);
1039
+ try {
1040
+ const aggClient = createAggregatorClient(client);
1041
+ const result = await aggClient.findRouters({
1042
+ from: fromInfo.type,
1043
+ target: toInfo.type,
1044
+ amount: rawAmount,
1045
+ byAmountIn: true
1046
+ });
1047
+ if (!result || result.insufficientLiquidity) {
1048
+ return fallbackQuote(fromAsset, amount, poolPrice);
1049
+ }
1050
+ const expectedOutput = Number(result.amountOut.toString()) / 10 ** toInfo.decimals;
1051
+ const priceImpact = result.deviationRatio ?? 0;
1052
+ return { expectedOutput, priceImpact, poolPrice };
1053
+ } catch {
1054
+ return fallbackQuote(fromAsset, amount, poolPrice);
1055
+ }
1056
+ }
1057
+ function fallbackQuote(fromAsset, amount, poolPrice) {
1058
+ const expectedOutput = fromAsset === "USDC" ? amount / poolPrice : amount * poolPrice;
1059
+ return { expectedOutput, priceImpact: 0, poolPrice };
1060
+ }
1061
+
1062
+ // src/adapters/cetus.ts
1063
+ var descriptor2 = {
1064
+ id: "cetus",
1065
+ name: "Cetus DEX",
1066
+ packages: [CETUS_PACKAGE],
1067
+ actionMap: {
1068
+ "router::swap": "swap",
1069
+ "router::swap_ab_bc": "swap",
1070
+ "router::swap_ab_cb": "swap",
1071
+ "router::swap_ba_bc": "swap",
1072
+ "router::swap_ba_cb": "swap"
1073
+ }
1074
+ };
1075
+ var CetusAdapter = class {
1076
+ id = "cetus";
1077
+ name = "Cetus";
1078
+ version = "1.0.0";
1079
+ capabilities = ["swap"];
1080
+ client;
1081
+ async init(client) {
1082
+ this.client = client;
1083
+ }
1084
+ initSync(client) {
1085
+ this.client = client;
1086
+ }
1087
+ async getQuote(from, to, amount) {
1088
+ return getSwapQuote(this.client, from, to, amount);
1089
+ }
1090
+ async buildSwapTx(address, from, to, amount, maxSlippageBps) {
1091
+ const result = await buildSwapTx({
1092
+ client: this.client,
1093
+ address,
1094
+ fromAsset: from,
1095
+ toAsset: to,
1096
+ amount,
1097
+ maxSlippageBps
1098
+ });
1099
+ return {
1100
+ tx: result.tx,
1101
+ estimatedOut: result.estimatedOut,
1102
+ toDecimals: result.toDecimals
1103
+ };
1104
+ }
1105
+ getSupportedPairs() {
1106
+ const pairs = [];
1107
+ for (const asset of Object.keys(INVESTMENT_ASSETS)) {
1108
+ pairs.push({ from: "USDC", to: asset }, { from: asset, to: "USDC" });
1109
+ }
1110
+ for (const a of STABLE_ASSETS) {
1111
+ for (const b of STABLE_ASSETS) {
1112
+ if (a !== b) pairs.push({ from: a, to: b });
1113
+ }
1114
+ }
1115
+ return pairs;
1116
+ }
1117
+ async getPoolPrice() {
1118
+ return getPoolPrice(this.client);
1119
+ }
1120
+ async addSwapToTx(tx, address, inputCoin, from, to, amount, maxSlippageBps) {
1121
+ return addSwapToTx({
1122
+ tx,
1123
+ client: this.client,
1124
+ address,
1125
+ inputCoin,
1126
+ fromAsset: from,
1127
+ toAsset: to,
1128
+ amount,
1129
+ maxSlippageBps
1130
+ });
1131
+ }
1132
+ };
1133
+ var WAD = 1e18;
1134
+ var MIN_HEALTH_FACTOR2 = 1.5;
1135
+ var CLOCK2 = "0x6";
1136
+ var LENDING_MARKET_ID = "0x84030d26d85eaa7035084a057f2f11f701b7e2e4eda87551becbc7c97505ece1";
1137
+ var LENDING_MARKET_TYPE = "0xf95b06141ed4a174f239417323bde3f209b972f5930d8521ea38a52aff3a6ddf::suilend::MAIN_POOL";
1138
+ var SUILEND_PACKAGE = "0xf95b06141ed4a174f239417323bde3f209b972f5930d8521ea38a52aff3a6ddf";
1139
+ var UPGRADE_CAP_ID = "0x3d4ef1859c3ee9fc72858f588b56a09da5466e64f8cc4e90a7b3b909fba8a7ae";
1140
+ var FALLBACK_PUBLISHED_AT = "0x3d4353f3bd3565329655e6b77bc2abfd31e558b86662ebd078ae453d416bc10f";
1141
+ var descriptor3 = {
1142
+ id: "suilend",
1143
+ name: "Suilend",
1144
+ packages: [SUILEND_PACKAGE],
1145
+ actionMap: {
1146
+ "lending_market::deposit_liquidity_and_mint_ctokens": "save",
1147
+ "lending_market::deposit_ctokens_into_obligation": "save",
1148
+ "lending_market::create_obligation": "save",
1149
+ "lending_market::withdraw_ctokens": "withdraw",
1150
+ "lending_market::redeem_ctokens_and_withdraw_liquidity": "withdraw",
1151
+ "lending_market::borrow": "borrow",
1152
+ "lending_market::repay": "repay"
1153
+ }
1154
+ };
1155
+ function interpolateRate(utilBreakpoints, aprBreakpoints, utilizationPct) {
1156
+ if (utilBreakpoints.length === 0) return 0;
1157
+ if (utilizationPct <= utilBreakpoints[0]) return aprBreakpoints[0];
1158
+ if (utilizationPct >= utilBreakpoints[utilBreakpoints.length - 1]) {
1159
+ return aprBreakpoints[aprBreakpoints.length - 1];
1160
+ }
1161
+ for (let i = 1; i < utilBreakpoints.length; i++) {
1162
+ if (utilizationPct <= utilBreakpoints[i]) {
1163
+ const t = (utilizationPct - utilBreakpoints[i - 1]) / (utilBreakpoints[i] - utilBreakpoints[i - 1]);
1164
+ return aprBreakpoints[i - 1] + t * (aprBreakpoints[i] - aprBreakpoints[i - 1]);
1165
+ }
1166
+ }
1167
+ return aprBreakpoints[aprBreakpoints.length - 1];
1168
+ }
1169
+ function computeRates(reserve) {
1170
+ const available = reserve.availableAmount / 10 ** reserve.mintDecimals;
1171
+ const borrowed = reserve.borrowedAmountWad / WAD / 10 ** reserve.mintDecimals;
1172
+ const totalDeposited = available + borrowed;
1173
+ const utilizationPct = totalDeposited > 0 ? borrowed / totalDeposited * 100 : 0;
1174
+ if (reserve.interestRateUtils.length === 0) return { borrowAprPct: 0, depositAprPct: 0 };
1175
+ const aprs = reserve.interestRateAprs.map((a) => a / 100);
1176
+ const borrowAprPct = interpolateRate(reserve.interestRateUtils, aprs, utilizationPct);
1177
+ const depositAprPct = utilizationPct / 100 * (borrowAprPct / 100) * (1 - reserve.spreadFeeBps / 1e4) * 100;
1178
+ return { borrowAprPct, depositAprPct };
1179
+ }
1180
+ function cTokenRatio(reserve) {
1181
+ if (reserve.ctokenSupply === 0) return 1;
1182
+ const totalSupply = reserve.availableAmount + reserve.borrowedAmountWad / WAD - reserve.unclaimedSpreadFeesWad / WAD;
1183
+ return totalSupply / reserve.ctokenSupply;
1184
+ }
1185
+ function f(obj) {
1186
+ if (obj && typeof obj === "object" && "fields" in obj) return obj.fields;
1187
+ return obj;
1188
+ }
1189
+ function str(v) {
1190
+ return String(v ?? "0");
1191
+ }
1192
+ function num(v) {
1193
+ return Number(str(v));
1194
+ }
1195
+ function parseReserve(raw, index) {
1196
+ const r = f(raw);
1197
+ const coinTypeField = f(r.coin_type);
1198
+ const config = f(f(r.config)?.element);
1199
+ return {
1200
+ coinType: str(coinTypeField?.name),
1201
+ mintDecimals: num(r.mint_decimals),
1202
+ availableAmount: num(r.available_amount),
1203
+ borrowedAmountWad: num(f(r.borrowed_amount)?.value),
1204
+ ctokenSupply: num(r.ctoken_supply),
1205
+ unclaimedSpreadFeesWad: num(f(r.unclaimed_spread_fees)?.value),
1206
+ cumulativeBorrowRateWad: num(f(r.cumulative_borrow_rate)?.value),
1207
+ openLtvPct: num(config?.open_ltv_pct),
1208
+ closeLtvPct: num(config?.close_ltv_pct),
1209
+ spreadFeeBps: num(config?.spread_fee_bps),
1210
+ interestRateUtils: Array.isArray(config?.interest_rate_utils) ? config.interest_rate_utils.map(num) : [],
1211
+ interestRateAprs: Array.isArray(config?.interest_rate_aprs) ? config.interest_rate_aprs.map(num) : [],
1212
+ arrayIndex: index
1213
+ };
1214
+ }
1215
+ function parseObligation(raw) {
1216
+ const deposits = Array.isArray(raw.deposits) ? raw.deposits.map((d) => {
1217
+ const df = f(d);
1218
+ return {
1219
+ coinType: str(f(df.coin_type)?.name),
1220
+ ctokenAmount: num(df.deposited_ctoken_amount),
1221
+ reserveIdx: num(df.reserve_array_index)
1222
+ };
1223
+ }) : [];
1224
+ const borrows = Array.isArray(raw.borrows) ? raw.borrows.map((b) => {
1225
+ const bf = f(b);
1226
+ return {
1227
+ coinType: str(f(bf.coin_type)?.name),
1228
+ borrowedWad: num(f(bf.borrowed_amount)?.value),
1229
+ cumBorrowRateWad: num(f(bf.cumulative_borrow_rate)?.value),
1230
+ reserveIdx: num(bf.reserve_array_index)
1231
+ };
1232
+ }) : [];
1233
+ return { deposits, borrows };
1234
+ }
1235
+ var SuilendAdapter = class {
1236
+ id = "suilend";
1237
+ name = "Suilend";
1238
+ version = "2.0.0";
1239
+ capabilities = ["save", "withdraw", "borrow", "repay"];
1240
+ supportedAssets = [...STABLE_ASSETS, "SUI", "ETH", "BTC"];
1241
+ supportsSameAssetBorrow = false;
1242
+ client;
1243
+ publishedAt = null;
1244
+ reserveCache = null;
1245
+ async init(client) {
1246
+ this.client = client;
1247
+ }
1248
+ initSync(client) {
1249
+ this.client = client;
1250
+ }
1251
+ // -- On-chain reads -------------------------------------------------------
1252
+ async resolvePackage() {
1253
+ if (this.publishedAt) return this.publishedAt;
1254
+ try {
1255
+ const cap = await this.client.getObject({ id: UPGRADE_CAP_ID, options: { showContent: true } });
1256
+ if (cap.data?.content?.dataType === "moveObject") {
1257
+ const fields = cap.data.content.fields;
1258
+ this.publishedAt = str(fields.package);
1259
+ return this.publishedAt;
1260
+ }
1261
+ } catch {
1262
+ }
1263
+ this.publishedAt = FALLBACK_PUBLISHED_AT;
1264
+ return this.publishedAt;
1265
+ }
1266
+ async loadReserves(fresh = false) {
1267
+ if (this.reserveCache && !fresh) return this.reserveCache;
1268
+ const market = await this.client.getObject({
1269
+ id: LENDING_MARKET_ID,
1270
+ options: { showContent: true }
1271
+ });
1272
+ if (market.data?.content?.dataType !== "moveObject") {
1273
+ throw new T2000Error("PROTOCOL_UNAVAILABLE", "Failed to read Suilend lending market");
1274
+ }
1275
+ const fields = market.data.content.fields;
1276
+ const reservesRaw = fields.reserves;
1277
+ if (!Array.isArray(reservesRaw)) {
1278
+ throw new T2000Error("PROTOCOL_UNAVAILABLE", "Failed to parse Suilend reserves");
1279
+ }
1280
+ this.reserveCache = reservesRaw.map((r, i) => parseReserve(r, i));
1281
+ return this.reserveCache;
1282
+ }
1283
+ findReserve(reserves, asset) {
1284
+ let coinType;
1285
+ if (asset in SUPPORTED_ASSETS) {
1286
+ coinType = SUPPORTED_ASSETS[asset].type;
1287
+ } else if (asset.includes("::")) {
1288
+ coinType = asset;
1289
+ } else {
1290
+ return void 0;
1291
+ }
1292
+ try {
1293
+ const normalized = normalizeStructTag(coinType);
1294
+ return reserves.find((r) => {
1295
+ try {
1296
+ return normalizeStructTag(r.coinType) === normalized;
1297
+ } catch {
1298
+ return false;
1299
+ }
1300
+ });
1301
+ } catch {
1302
+ return void 0;
1303
+ }
1304
+ }
1305
+ async fetchObligationCaps(address) {
1306
+ const capType = `${SUILEND_PACKAGE}::lending_market::ObligationOwnerCap<${LENDING_MARKET_TYPE}>`;
1307
+ const caps = [];
1308
+ let cursor;
1309
+ let hasNext = true;
1310
+ while (hasNext) {
1311
+ const page = await this.client.getOwnedObjects({
1312
+ owner: address,
1313
+ filter: { StructType: capType },
1314
+ options: { showContent: true },
1315
+ cursor: cursor ?? void 0
1316
+ });
1317
+ for (const item of page.data) {
1318
+ if (item.data?.content?.dataType !== "moveObject") continue;
1319
+ const fields = item.data.content.fields;
1320
+ caps.push({
1321
+ id: item.data.objectId,
1322
+ obligationId: str(fields.obligation_id)
1323
+ });
1324
+ }
1325
+ cursor = page.nextCursor;
1326
+ hasNext = page.hasNextPage;
1327
+ }
1328
+ return caps;
1329
+ }
1330
+ async fetchObligation(obligationId) {
1331
+ const obj = await this.client.getObject({ id: obligationId, options: { showContent: true } });
1332
+ if (obj.data?.content?.dataType !== "moveObject") {
1333
+ throw new T2000Error("PROTOCOL_UNAVAILABLE", "Failed to read Suilend obligation");
1334
+ }
1335
+ return parseObligation(obj.data.content.fields);
1336
+ }
1337
+ resolveSymbol(coinType) {
1338
+ try {
1339
+ const normalized = normalizeStructTag(coinType);
1340
+ for (const [key, info] of Object.entries(SUPPORTED_ASSETS)) {
1341
+ try {
1342
+ if (normalizeStructTag(info.type) === normalized) return key;
1343
+ } catch {
1344
+ }
1345
+ }
1346
+ } catch {
1347
+ }
1348
+ const parts = coinType.split("::");
1349
+ return parts[parts.length - 1] || "UNKNOWN";
1350
+ }
1351
+ // -- Adapter interface ----------------------------------------------------
1352
+ async getRates(asset) {
1353
+ const reserves = await this.loadReserves();
1354
+ const reserve = this.findReserve(reserves, asset);
1355
+ if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `Suilend does not support ${asset}`);
1356
+ const { borrowAprPct, depositAprPct } = computeRates(reserve);
1357
+ return { asset, saveApy: depositAprPct, borrowApy: borrowAprPct };
1358
+ }
1359
+ async getPositions(address) {
1360
+ const supplies = [];
1361
+ const borrows = [];
1362
+ const caps = await this.fetchObligationCaps(address);
1363
+ if (caps.length === 0) return { supplies, borrows };
1364
+ const [reserves, obligation] = await Promise.all([
1365
+ this.loadReserves(),
1366
+ this.fetchObligation(caps[0].obligationId)
1367
+ ]);
1368
+ for (const dep of obligation.deposits) {
1369
+ const reserve = reserves[dep.reserveIdx];
1370
+ if (!reserve) continue;
1371
+ const ratio = cTokenRatio(reserve);
1372
+ const amount = dep.ctokenAmount * ratio / 10 ** reserve.mintDecimals;
1373
+ const { depositAprPct } = computeRates(reserve);
1374
+ supplies.push({ asset: this.resolveSymbol(dep.coinType), amount, apy: depositAprPct });
1375
+ }
1376
+ for (const bor of obligation.borrows) {
1377
+ const reserve = reserves[bor.reserveIdx];
1378
+ if (!reserve) continue;
1379
+ const rawAmount = bor.borrowedWad / WAD / 10 ** reserve.mintDecimals;
1380
+ const reserveRate = reserve.cumulativeBorrowRateWad / WAD;
1381
+ const posRate = bor.cumBorrowRateWad / WAD;
1382
+ const compounded = posRate > 0 ? rawAmount * (reserveRate / posRate) : rawAmount;
1383
+ const { borrowAprPct } = computeRates(reserve);
1384
+ borrows.push({ asset: this.resolveSymbol(bor.coinType), amount: compounded, apy: borrowAprPct });
1385
+ }
1386
+ return { supplies, borrows };
1387
+ }
1388
+ async getHealth(address) {
1389
+ const caps = await this.fetchObligationCaps(address);
1390
+ if (caps.length === 0) {
1391
+ return { healthFactor: Infinity, supplied: 0, borrowed: 0, maxBorrow: 0, liquidationThreshold: 0 };
1392
+ }
1393
+ const [reserves, obligation] = await Promise.all([
1394
+ this.loadReserves(),
1395
+ this.fetchObligation(caps[0].obligationId)
1396
+ ]);
1397
+ let supplied = 0;
1398
+ let borrowed = 0;
1399
+ let weightedCloseLtv = 0;
1400
+ let weightedOpenLtv = 0;
1401
+ for (const dep of obligation.deposits) {
1402
+ const reserve = reserves[dep.reserveIdx];
1403
+ if (!reserve) continue;
1404
+ const ratio = cTokenRatio(reserve);
1405
+ const amount = dep.ctokenAmount * ratio / 10 ** reserve.mintDecimals;
1406
+ supplied += amount;
1407
+ weightedCloseLtv += amount * (reserve.closeLtvPct / 100);
1408
+ weightedOpenLtv += amount * (reserve.openLtvPct / 100);
1409
+ }
1410
+ for (const bor of obligation.borrows) {
1411
+ const reserve = reserves[bor.reserveIdx];
1412
+ if (!reserve) continue;
1413
+ const rawAmount = bor.borrowedWad / WAD / 10 ** reserve.mintDecimals;
1414
+ const reserveRate = reserve.cumulativeBorrowRateWad / WAD;
1415
+ const posRate = bor.cumBorrowRateWad / WAD;
1416
+ borrowed += posRate > 0 ? rawAmount * (reserveRate / posRate) : rawAmount;
1417
+ }
1418
+ const liqThreshold = supplied > 0 ? weightedCloseLtv / supplied : 0.75;
1419
+ const openLtv = supplied > 0 ? weightedOpenLtv / supplied : 0.7;
1420
+ const healthFactor = borrowed > 0 ? supplied * liqThreshold / borrowed : Infinity;
1421
+ const maxBorrow = Math.max(0, supplied * openLtv - borrowed);
1422
+ return { healthFactor, supplied, borrowed, maxBorrow, liquidationThreshold: liqThreshold };
1423
+ }
1424
+ async buildSaveTx(address, amount, asset, options) {
1425
+ const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
1426
+ const assetInfo = SUPPORTED_ASSETS[assetKey];
1427
+ const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
1428
+ const reserve = this.findReserve(reserves, assetKey);
1429
+ if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend. Try: NAVI or a different asset.`);
1430
+ const caps = await this.fetchObligationCaps(address);
1431
+ const tx = new Transaction();
1432
+ tx.setSender(address);
1433
+ let capRef;
1434
+ if (caps.length === 0) {
1435
+ const [newCap] = tx.moveCall({
1436
+ target: `${pkg}::lending_market::create_obligation`,
1437
+ typeArguments: [LENDING_MARKET_TYPE],
1438
+ arguments: [tx.object(LENDING_MARKET_ID)]
1439
+ });
1440
+ capRef = newCap;
1441
+ } else {
1442
+ capRef = caps[0].id;
1443
+ }
1444
+ const allCoins = await this.fetchAllCoins(address, assetInfo.type);
1445
+ if (allCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins found`);
1446
+ const primaryCoinId = allCoins[0].coinObjectId;
1447
+ if (allCoins.length > 1) {
1448
+ tx.mergeCoins(tx.object(primaryCoinId), allCoins.slice(1).map((c) => tx.object(c.coinObjectId)));
1449
+ }
1450
+ const rawAmount = stableToRaw(amount, assetInfo.decimals).toString();
1451
+ const [depositCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawAmount]);
1452
+ if (options?.collectFee) {
1453
+ addCollectFeeToTx(tx, depositCoin, "save");
1454
+ }
1455
+ const [ctokens] = tx.moveCall({
1456
+ target: `${pkg}::lending_market::deposit_liquidity_and_mint_ctokens`,
1457
+ typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1458
+ arguments: [
1459
+ tx.object(LENDING_MARKET_ID),
1460
+ tx.pure.u64(reserve.arrayIndex),
1461
+ tx.object(CLOCK2),
1462
+ depositCoin
1463
+ ]
1464
+ });
1465
+ tx.moveCall({
1466
+ target: `${pkg}::lending_market::deposit_ctokens_into_obligation`,
1467
+ typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1468
+ arguments: [
1469
+ tx.object(LENDING_MARKET_ID),
1470
+ tx.pure.u64(reserve.arrayIndex),
1471
+ typeof capRef === "string" ? tx.object(capRef) : capRef,
1472
+ tx.object(CLOCK2),
1473
+ ctokens
1474
+ ]
1475
+ });
1476
+ if (typeof capRef !== "string") {
1477
+ tx.transferObjects([capRef], address);
1478
+ }
1479
+ return { tx };
1480
+ }
1481
+ async buildWithdrawTx(address, amount, asset) {
1482
+ const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
1483
+ const assetInfo = SUPPORTED_ASSETS[assetKey];
1484
+ const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves(true)]);
1485
+ const reserve = this.findReserve(reserves, assetKey);
1486
+ if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
1487
+ const caps = await this.fetchObligationCaps(address);
1488
+ if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found");
1489
+ const positions = await this.getPositions(address);
1490
+ const deposited = positions.supplies.find((s) => s.asset === assetKey)?.amount ?? 0;
1491
+ const effectiveAmount = Math.min(amount, deposited);
1492
+ if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on Suilend`);
1493
+ const ratio = cTokenRatio(reserve);
1494
+ const ctokenAmount = Math.ceil(effectiveAmount * 10 ** reserve.mintDecimals / ratio);
1495
+ const tx = new Transaction();
1496
+ tx.setSender(address);
1497
+ const [ctokens] = tx.moveCall({
1498
+ target: `${pkg}::lending_market::withdraw_ctokens`,
1499
+ typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1500
+ arguments: [
1501
+ tx.object(LENDING_MARKET_ID),
1502
+ tx.pure.u64(reserve.arrayIndex),
1503
+ tx.object(caps[0].id),
1504
+ tx.object(CLOCK2),
1505
+ tx.pure.u64(ctokenAmount)
1506
+ ]
1507
+ });
1508
+ const exemptionType = `${SUILEND_PACKAGE}::lending_market::RateLimiterExemption<${LENDING_MARKET_TYPE}, ${assetInfo.type}>`;
1509
+ const [none] = tx.moveCall({
1510
+ target: "0x1::option::none",
1511
+ typeArguments: [exemptionType]
1512
+ });
1513
+ const [coin] = tx.moveCall({
1514
+ target: `${pkg}::lending_market::redeem_ctokens_and_withdraw_liquidity`,
1515
+ typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1516
+ arguments: [
1517
+ tx.object(LENDING_MARKET_ID),
1518
+ tx.pure.u64(reserve.arrayIndex),
1519
+ tx.object(CLOCK2),
1520
+ ctokens,
1521
+ none
1522
+ ]
1523
+ });
1524
+ tx.transferObjects([coin], address);
1525
+ return { tx, effectiveAmount };
1526
+ }
1527
+ async addWithdrawToTx(tx, address, amount, asset) {
1528
+ const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
1529
+ const assetInfo = SUPPORTED_ASSETS[assetKey];
1530
+ const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves(true)]);
1531
+ const reserve = this.findReserve(reserves, assetKey);
1532
+ if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
1533
+ const caps = await this.fetchObligationCaps(address);
1534
+ if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found");
1535
+ const positions = await this.getPositions(address);
1536
+ const deposited = positions.supplies.find((s) => s.asset === assetKey)?.amount ?? 0;
1537
+ const effectiveAmount = Math.min(amount, deposited);
1538
+ if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on Suilend`);
1539
+ const ratio = cTokenRatio(reserve);
1540
+ const ctokenAmount = Math.ceil(effectiveAmount * 10 ** reserve.mintDecimals / ratio);
1541
+ const [ctokens] = tx.moveCall({
1542
+ target: `${pkg}::lending_market::withdraw_ctokens`,
1543
+ typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1544
+ arguments: [
1545
+ tx.object(LENDING_MARKET_ID),
1546
+ tx.pure.u64(reserve.arrayIndex),
1547
+ tx.object(caps[0].id),
1548
+ tx.object(CLOCK2),
1549
+ tx.pure.u64(ctokenAmount)
1550
+ ]
1551
+ });
1552
+ const exemptionType = `${SUILEND_PACKAGE}::lending_market::RateLimiterExemption<${LENDING_MARKET_TYPE}, ${assetInfo.type}>`;
1553
+ const [none] = tx.moveCall({
1554
+ target: "0x1::option::none",
1555
+ typeArguments: [exemptionType]
1556
+ });
1557
+ const [coin] = tx.moveCall({
1558
+ target: `${pkg}::lending_market::redeem_ctokens_and_withdraw_liquidity`,
1559
+ typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1560
+ arguments: [
1561
+ tx.object(LENDING_MARKET_ID),
1562
+ tx.pure.u64(reserve.arrayIndex),
1563
+ tx.object(CLOCK2),
1564
+ ctokens,
1565
+ none
1566
+ ]
1567
+ });
1568
+ return { coin, effectiveAmount };
1569
+ }
1570
+ async addSaveToTx(tx, address, coin, asset, options) {
1571
+ const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
1572
+ const assetInfo = SUPPORTED_ASSETS[assetKey];
1573
+ const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
1574
+ const reserve = this.findReserve(reserves, assetKey);
1575
+ if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
1576
+ const caps = await this.fetchObligationCaps(address);
1577
+ let capRef;
1578
+ if (caps.length === 0) {
1579
+ const [newCap] = tx.moveCall({
1580
+ target: `${pkg}::lending_market::create_obligation`,
1581
+ typeArguments: [LENDING_MARKET_TYPE],
1582
+ arguments: [tx.object(LENDING_MARKET_ID)]
1583
+ });
1584
+ capRef = newCap;
1585
+ } else {
1586
+ capRef = caps[0].id;
1587
+ }
1588
+ if (options?.collectFee) {
1589
+ addCollectFeeToTx(tx, coin, "save");
1590
+ }
1591
+ const [ctokens] = tx.moveCall({
1592
+ target: `${pkg}::lending_market::deposit_liquidity_and_mint_ctokens`,
1593
+ typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1594
+ arguments: [
1595
+ tx.object(LENDING_MARKET_ID),
1596
+ tx.pure.u64(reserve.arrayIndex),
1597
+ tx.object(CLOCK2),
1598
+ coin
1599
+ ]
1600
+ });
1601
+ tx.moveCall({
1602
+ target: `${pkg}::lending_market::deposit_ctokens_into_obligation`,
1603
+ typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1604
+ arguments: [
1605
+ tx.object(LENDING_MARKET_ID),
1606
+ tx.pure.u64(reserve.arrayIndex),
1607
+ typeof capRef === "string" ? tx.object(capRef) : capRef,
1608
+ tx.object(CLOCK2),
1609
+ ctokens
1610
+ ]
1611
+ });
1612
+ if (typeof capRef !== "string") {
1613
+ tx.transferObjects([capRef], address);
1614
+ }
1615
+ }
1616
+ async buildBorrowTx(address, amount, asset, options) {
1617
+ const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
1618
+ const assetInfo = SUPPORTED_ASSETS[assetKey];
1619
+ const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
1620
+ const reserve = this.findReserve(reserves, assetKey);
1621
+ if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend. Try: NAVI or a different asset.`);
1622
+ const caps = await this.fetchObligationCaps(address);
1623
+ if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found. Deposit collateral first with: t2000 save <amount>");
1624
+ const rawAmount = stableToRaw(amount, assetInfo.decimals);
1625
+ const tx = new Transaction();
1626
+ tx.setSender(address);
1627
+ const [coin] = tx.moveCall({
1628
+ target: `${pkg}::lending_market::borrow`,
1629
+ typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1630
+ arguments: [
1631
+ tx.object(LENDING_MARKET_ID),
1632
+ tx.pure.u64(reserve.arrayIndex),
1633
+ tx.object(caps[0].id),
1634
+ tx.object(CLOCK2),
1635
+ tx.pure.u64(rawAmount)
1636
+ ]
1637
+ });
1638
+ if (options?.collectFee) {
1639
+ addCollectFeeToTx(tx, coin, "borrow");
1640
+ }
1641
+ tx.transferObjects([coin], address);
1642
+ return { tx };
1643
+ }
1644
+ async buildRepayTx(address, amount, asset) {
1645
+ const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
1646
+ const assetInfo = SUPPORTED_ASSETS[assetKey];
1647
+ const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
1648
+ const reserve = this.findReserve(reserves, assetKey);
1649
+ if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
1650
+ const caps = await this.fetchObligationCaps(address);
1651
+ if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend obligation found");
1652
+ const allCoins = await this.fetchAllCoins(address, assetInfo.type);
1653
+ if (allCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins to repay with`);
1654
+ const rawAmount = stableToRaw(amount, assetInfo.decimals);
1655
+ const tx = new Transaction();
1656
+ tx.setSender(address);
1657
+ const primaryCoinId = allCoins[0].coinObjectId;
1658
+ if (allCoins.length > 1) {
1659
+ tx.mergeCoins(tx.object(primaryCoinId), allCoins.slice(1).map((c) => tx.object(c.coinObjectId)));
1660
+ }
1661
+ const [repayCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawAmount.toString()]);
1662
+ tx.moveCall({
1663
+ target: `${pkg}::lending_market::repay`,
1664
+ typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1665
+ arguments: [
1666
+ tx.object(LENDING_MARKET_ID),
1667
+ tx.pure.u64(reserve.arrayIndex),
1668
+ tx.object(caps[0].id),
1669
+ tx.object(CLOCK2),
1670
+ repayCoin
1671
+ ]
1672
+ });
1673
+ return { tx };
1674
+ }
1675
+ async addRepayToTx(tx, address, coin, asset) {
1676
+ const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
1677
+ const assetInfo = SUPPORTED_ASSETS[assetKey];
1678
+ const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
1679
+ const reserve = this.findReserve(reserves, assetKey);
1680
+ if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
1681
+ const caps = await this.fetchObligationCaps(address);
1682
+ if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend obligation found");
1683
+ tx.moveCall({
1684
+ target: `${pkg}::lending_market::repay`,
1685
+ typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1686
+ arguments: [
1687
+ tx.object(LENDING_MARKET_ID),
1688
+ tx.pure.u64(reserve.arrayIndex),
1689
+ tx.object(caps[0].id),
1690
+ tx.object(CLOCK2),
1691
+ coin
1692
+ ]
1693
+ });
1694
+ }
1695
+ async maxWithdraw(address, _asset) {
1696
+ const health = await this.getHealth(address);
1697
+ let maxAmount;
1698
+ if (health.borrowed === 0) {
1699
+ maxAmount = health.supplied;
1700
+ } else {
1701
+ maxAmount = Math.max(0, health.supplied - health.borrowed * MIN_HEALTH_FACTOR2 / health.liquidationThreshold);
1702
+ }
1703
+ const remainingSupply = health.supplied - maxAmount;
1704
+ const hfAfter = health.borrowed > 0 ? remainingSupply * health.liquidationThreshold / health.borrowed : Infinity;
1705
+ return { maxAmount, healthFactorAfter: hfAfter, currentHF: health.healthFactor };
1706
+ }
1707
+ async maxBorrow(address, _asset) {
1708
+ const health = await this.getHealth(address);
1709
+ const maxAmount = health.maxBorrow;
1710
+ return { maxAmount, healthFactorAfter: MIN_HEALTH_FACTOR2, currentHF: health.healthFactor };
1711
+ }
1712
+ async fetchAllCoins(owner, coinType) {
1713
+ const all = [];
1714
+ let cursor = null;
1715
+ let hasNext = true;
1716
+ while (hasNext) {
1717
+ const page = await this.client.getCoins({ owner, coinType, cursor: cursor ?? void 0 });
1718
+ all.push(...page.data.map((c) => ({ coinObjectId: c.coinObjectId, balance: c.balance })));
1719
+ cursor = page.nextCursor;
1720
+ hasNext = page.hasNextPage;
1721
+ }
1722
+ return all;
1723
+ }
1724
+ };
1725
+ var descriptor4 = {
1726
+ id: "sentinel",
1727
+ name: "Sui Sentinel",
1728
+ packages: [SENTINEL.PACKAGE],
1729
+ actionMap: {
1730
+ "sentinel::request_attack": "sentinel_attack",
1731
+ "sentinel::consume_prompt": "sentinel_settle"
1732
+ }
1733
+ };
1734
+
1735
+ // src/adapters/index.ts
1736
+ var allDescriptors = [
1737
+ descriptor,
1738
+ descriptor3,
1739
+ descriptor2,
1740
+ descriptor4
1741
+ ];
1742
+
1743
+ export { CetusAdapter, NaviAdapter, ProtocolRegistry, SuilendAdapter, allDescriptors, descriptor2 as cetusDescriptor, descriptor as naviDescriptor, descriptor4 as sentinelDescriptor, descriptor3 as suilendDescriptor };
1744
+ //# sourceMappingURL=index.js.map
1745
+ //# sourceMappingURL=index.js.map