@t2000/sdk 0.3.0 → 0.4.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,856 @@
1
+ 'use strict';
2
+
3
+ var lending = require('@naviprotocol/lending');
4
+ var transactions = require('@mysten/sui/transactions');
5
+ var aggregatorSdk = require('@cetusprotocol/aggregator-sdk');
6
+ var utils = require('@mysten/sui/utils');
7
+
8
+ // src/adapters/registry.ts
9
+ var ProtocolRegistry = class {
10
+ lending = /* @__PURE__ */ new Map();
11
+ swap = /* @__PURE__ */ new Map();
12
+ registerLending(adapter) {
13
+ this.lending.set(adapter.id, adapter);
14
+ }
15
+ registerSwap(adapter) {
16
+ this.swap.set(adapter.id, adapter);
17
+ }
18
+ async bestSaveRate(asset) {
19
+ const candidates = [];
20
+ for (const adapter of this.lending.values()) {
21
+ if (!adapter.supportedAssets.includes(asset)) continue;
22
+ if (!adapter.capabilities.includes("save")) continue;
23
+ try {
24
+ const rate = await adapter.getRates(asset);
25
+ candidates.push({ adapter, rate });
26
+ } catch {
27
+ }
28
+ }
29
+ if (candidates.length === 0) {
30
+ throw new Error(`No lending adapter supports saving ${asset}`);
31
+ }
32
+ candidates.sort((a, b) => b.rate.saveApy - a.rate.saveApy);
33
+ return candidates[0];
34
+ }
35
+ async bestBorrowRate(asset, opts) {
36
+ const candidates = [];
37
+ for (const adapter of this.lending.values()) {
38
+ if (!adapter.supportedAssets.includes(asset)) continue;
39
+ if (!adapter.capabilities.includes("borrow")) continue;
40
+ if (opts?.requireSameAssetBorrow && !adapter.supportsSameAssetBorrow) continue;
41
+ try {
42
+ const rate = await adapter.getRates(asset);
43
+ candidates.push({ adapter, rate });
44
+ } catch {
45
+ }
46
+ }
47
+ if (candidates.length === 0) {
48
+ throw new Error(`No lending adapter supports borrowing ${asset}`);
49
+ }
50
+ candidates.sort((a, b) => a.rate.borrowApy - b.rate.borrowApy);
51
+ return candidates[0];
52
+ }
53
+ async bestSwapQuote(from, to, amount) {
54
+ const candidates = [];
55
+ for (const adapter of this.swap.values()) {
56
+ const pairs = adapter.getSupportedPairs();
57
+ if (!pairs.some((p) => p.from === from && p.to === to)) continue;
58
+ try {
59
+ const quote = await adapter.getQuote(from, to, amount);
60
+ candidates.push({ adapter, quote });
61
+ } catch {
62
+ }
63
+ }
64
+ if (candidates.length === 0) {
65
+ throw new Error(`No swap adapter supports ${from} \u2192 ${to}`);
66
+ }
67
+ candidates.sort((a, b) => b.quote.expectedOutput - a.quote.expectedOutput);
68
+ return candidates[0];
69
+ }
70
+ async allRates(asset) {
71
+ const results = [];
72
+ for (const adapter of this.lending.values()) {
73
+ if (!adapter.supportedAssets.includes(asset)) continue;
74
+ try {
75
+ const rates = await adapter.getRates(asset);
76
+ results.push({ protocol: adapter.name, protocolId: adapter.id, rates });
77
+ } catch {
78
+ }
79
+ }
80
+ return results;
81
+ }
82
+ async allPositions(address) {
83
+ const results = [];
84
+ for (const adapter of this.lending.values()) {
85
+ try {
86
+ const positions = await adapter.getPositions(address);
87
+ if (positions.supplies.length > 0 || positions.borrows.length > 0) {
88
+ results.push({ protocol: adapter.name, protocolId: adapter.id, positions });
89
+ }
90
+ } catch {
91
+ }
92
+ }
93
+ return results;
94
+ }
95
+ getLending(id) {
96
+ return this.lending.get(id);
97
+ }
98
+ getSwap(id) {
99
+ return this.swap.get(id);
100
+ }
101
+ listLending() {
102
+ return [...this.lending.values()];
103
+ }
104
+ listSwap() {
105
+ return [...this.swap.values()];
106
+ }
107
+ };
108
+
109
+ // src/constants.ts
110
+ var USDC_DECIMALS = 6;
111
+ var SAVE_FEE_BPS = 10n;
112
+ var SWAP_FEE_BPS = 0n;
113
+ var BORROW_FEE_BPS = 5n;
114
+ var SUPPORTED_ASSETS = {
115
+ USDC: {
116
+ type: "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC",
117
+ decimals: 6,
118
+ symbol: "USDC"
119
+ },
120
+ SUI: {
121
+ type: "0x2::sui::SUI",
122
+ decimals: 9,
123
+ symbol: "SUI"
124
+ }
125
+ };
126
+ var T2000_PACKAGE_ID = process.env.T2000_PACKAGE_ID ?? "0xab92e9f1fe549ad3d6a52924a73181b45791e76120b975138fac9ec9b75db9f3";
127
+ var T2000_CONFIG_ID = process.env.T2000_CONFIG_ID ?? "0x408add9aa9322f93cfd87523d8f603006eb8713894f4c460283c58a6888dae8a";
128
+ var T2000_TREASURY_ID = process.env.T2000_TREASURY_ID ?? "0x3bb501b8300125dca59019247941a42af6b292a150ce3cfcce9449456be2ec91";
129
+ process.env.T2000_API_URL ?? "https://api.t2000.ai";
130
+ var CETUS_USDC_SUI_POOL = "0x51e883ba7c0b566a26cbc8a94cd33eb0abd418a77cc1e60ad22fd9b1f29cd2ab";
131
+
132
+ // src/errors.ts
133
+ var T2000Error = class extends Error {
134
+ code;
135
+ data;
136
+ retryable;
137
+ constructor(code, message, data, retryable = false) {
138
+ super(message);
139
+ this.name = "T2000Error";
140
+ this.code = code;
141
+ this.data = data;
142
+ this.retryable = retryable;
143
+ }
144
+ toJSON() {
145
+ return {
146
+ error: this.code,
147
+ message: this.message,
148
+ ...this.data && { data: this.data },
149
+ retryable: this.retryable
150
+ };
151
+ }
152
+ };
153
+
154
+ // src/utils/format.ts
155
+ function usdcToRaw(amount) {
156
+ return BigInt(Math.round(amount * 10 ** USDC_DECIMALS));
157
+ }
158
+
159
+ // src/protocols/protocolFee.ts
160
+ var FEE_RATES = {
161
+ save: SAVE_FEE_BPS,
162
+ swap: SWAP_FEE_BPS,
163
+ borrow: BORROW_FEE_BPS
164
+ };
165
+ var OP_CODES = {
166
+ save: 0,
167
+ swap: 1,
168
+ borrow: 2
169
+ };
170
+ function addCollectFeeToTx(tx, paymentCoin, operation) {
171
+ const bps = FEE_RATES[operation];
172
+ if (bps <= 0n) return;
173
+ tx.moveCall({
174
+ target: `${T2000_PACKAGE_ID}::treasury::collect_fee`,
175
+ typeArguments: [SUPPORTED_ASSETS.USDC.type],
176
+ arguments: [
177
+ tx.object(T2000_TREASURY_ID),
178
+ tx.object(T2000_CONFIG_ID),
179
+ paymentCoin,
180
+ tx.pure.u8(OP_CODES[operation])
181
+ ]
182
+ });
183
+ }
184
+
185
+ // src/protocols/navi.ts
186
+ var ENV = { env: "prod" };
187
+ var USDC_TYPE = SUPPORTED_ASSETS.USDC.type;
188
+ var RATE_DECIMALS = 27;
189
+ var LTV_DECIMALS = 27;
190
+ var MIN_HEALTH_FACTOR = 1.5;
191
+ var WITHDRAW_DUST_BUFFER = 1e-3;
192
+ var NAVI_BALANCE_DECIMALS = 9;
193
+ function clientOpt(client, fresh = false) {
194
+ return { client, ...ENV, ...fresh ? { disableCache: true } : {} };
195
+ }
196
+ function rateToApy(rawRate) {
197
+ if (!rawRate || rawRate === "0") return 0;
198
+ return Number(BigInt(rawRate)) / 10 ** RATE_DECIMALS * 100;
199
+ }
200
+ function parseLtv(rawLtv) {
201
+ if (!rawLtv || rawLtv === "0") return 0.75;
202
+ return Number(BigInt(rawLtv)) / 10 ** LTV_DECIMALS;
203
+ }
204
+ function parseLiqThreshold(val) {
205
+ if (typeof val === "number") return val;
206
+ const n = Number(val);
207
+ if (n > 1) return Number(BigInt(val)) / 10 ** LTV_DECIMALS;
208
+ return n;
209
+ }
210
+ function findUsdcPosition(state) {
211
+ return state.find(
212
+ (p) => p.pool.token.symbol === "USDC" || p.pool.coinType.toLowerCase().includes("usdc")
213
+ );
214
+ }
215
+ async function updateOracle(tx, client, address) {
216
+ try {
217
+ const [feeds, state] = await Promise.all([
218
+ lending.getPriceFeeds(ENV),
219
+ lending.getLendingState(address, clientOpt(client))
220
+ ]);
221
+ const relevant = lending.filterPriceFeeds(feeds, { lendingState: state });
222
+ if (relevant.length > 0) {
223
+ await lending.updateOraclePricesPTB(tx, relevant, { ...ENV, updatePythPriceFeeds: true });
224
+ }
225
+ } catch {
226
+ }
227
+ }
228
+ async function buildSaveTx(client, address, amount, options = {}) {
229
+ const rawAmount = Number(usdcToRaw(amount));
230
+ const coins = await lending.getCoins(address, { coinType: USDC_TYPE, client });
231
+ if (!coins || coins.length === 0) {
232
+ throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins found");
233
+ }
234
+ const tx = new transactions.Transaction();
235
+ tx.setSender(address);
236
+ const coinObj = lending.mergeCoinsPTB(tx, coins, { balance: rawAmount });
237
+ if (options.collectFee) {
238
+ addCollectFeeToTx(tx, coinObj, "save");
239
+ }
240
+ await lending.depositCoinPTB(tx, USDC_TYPE, coinObj, ENV);
241
+ return tx;
242
+ }
243
+ async function buildWithdrawTx(client, address, amount) {
244
+ const state = await lending.getLendingState(address, clientOpt(client, true));
245
+ const usdcPos = findUsdcPosition(state);
246
+ const deposited = usdcPos ? Number(usdcPos.supplyBalance) / 10 ** NAVI_BALANCE_DECIMALS : 0;
247
+ const effectiveAmount = Math.min(amount, Math.max(0, deposited - WITHDRAW_DUST_BUFFER));
248
+ if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", "Nothing to withdraw");
249
+ const rawAmount = Number(usdcToRaw(effectiveAmount));
250
+ const tx = new transactions.Transaction();
251
+ tx.setSender(address);
252
+ await updateOracle(tx, client, address);
253
+ const withdrawnCoin = await lending.withdrawCoinPTB(tx, USDC_TYPE, rawAmount, ENV);
254
+ tx.transferObjects([withdrawnCoin], address);
255
+ return { tx, effectiveAmount };
256
+ }
257
+ async function buildBorrowTx(client, address, amount, options = {}) {
258
+ const rawAmount = Number(usdcToRaw(amount));
259
+ const tx = new transactions.Transaction();
260
+ tx.setSender(address);
261
+ await updateOracle(tx, client, address);
262
+ const borrowedCoin = await lending.borrowCoinPTB(tx, USDC_TYPE, rawAmount, ENV);
263
+ if (options.collectFee) {
264
+ addCollectFeeToTx(tx, borrowedCoin, "borrow");
265
+ }
266
+ tx.transferObjects([borrowedCoin], address);
267
+ return tx;
268
+ }
269
+ async function buildRepayTx(client, address, amount) {
270
+ const rawAmount = Number(usdcToRaw(amount));
271
+ const coins = await lending.getCoins(address, { coinType: USDC_TYPE, client });
272
+ if (!coins || coins.length === 0) {
273
+ throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins to repay with");
274
+ }
275
+ const tx = new transactions.Transaction();
276
+ tx.setSender(address);
277
+ const coinObj = lending.mergeCoinsPTB(tx, coins, { balance: rawAmount });
278
+ await lending.repayCoinPTB(tx, USDC_TYPE, coinObj, { ...ENV, amount: rawAmount });
279
+ return tx;
280
+ }
281
+ async function getHealthFactor(client, addressOrKeypair) {
282
+ const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
283
+ const [healthFactor, state, pool] = await Promise.all([
284
+ lending.getHealthFactor(address, clientOpt(client, true)),
285
+ lending.getLendingState(address, clientOpt(client, true)),
286
+ lending.getPool(USDC_TYPE, ENV)
287
+ ]);
288
+ const usdcPos = findUsdcPosition(state);
289
+ const supplied = usdcPos ? Number(usdcPos.supplyBalance) / 10 ** NAVI_BALANCE_DECIMALS : 0;
290
+ const borrowed = usdcPos ? Number(usdcPos.borrowBalance) / 10 ** NAVI_BALANCE_DECIMALS : 0;
291
+ const ltv = parseLtv(pool.ltv);
292
+ const liqThreshold = parseLiqThreshold(pool.liquidationFactor.threshold);
293
+ const maxBorrowVal = Math.max(0, supplied * ltv - borrowed);
294
+ return {
295
+ healthFactor: borrowed > 0 ? healthFactor : Infinity,
296
+ supplied,
297
+ borrowed,
298
+ maxBorrow: maxBorrowVal,
299
+ liquidationThreshold: liqThreshold
300
+ };
301
+ }
302
+ async function getRates(client) {
303
+ try {
304
+ const pool = await lending.getPool(USDC_TYPE, ENV);
305
+ let saveApy = rateToApy(pool.currentSupplyRate);
306
+ let borrowApy = rateToApy(pool.currentBorrowRate);
307
+ if (saveApy <= 0 || saveApy > 100) saveApy = 4;
308
+ if (borrowApy <= 0 || borrowApy > 100) borrowApy = 6;
309
+ return { USDC: { saveApy, borrowApy } };
310
+ } catch {
311
+ return { USDC: { saveApy: 4, borrowApy: 6 } };
312
+ }
313
+ }
314
+ async function getPositions(client, addressOrKeypair) {
315
+ const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
316
+ const state = await lending.getLendingState(address, clientOpt(client, true));
317
+ const positions = [];
318
+ for (const pos of state) {
319
+ const symbol = pos.pool.token?.symbol ?? "UNKNOWN";
320
+ const supplyBal = Number(pos.supplyBalance) / 10 ** NAVI_BALANCE_DECIMALS;
321
+ const borrowBal = Number(pos.borrowBalance) / 10 ** NAVI_BALANCE_DECIMALS;
322
+ if (supplyBal > 1e-4) {
323
+ positions.push({
324
+ protocol: "navi",
325
+ asset: symbol,
326
+ type: "save",
327
+ amount: supplyBal,
328
+ apy: rateToApy(pos.pool.currentSupplyRate)
329
+ });
330
+ }
331
+ if (borrowBal > 1e-4) {
332
+ positions.push({
333
+ protocol: "navi",
334
+ asset: symbol,
335
+ type: "borrow",
336
+ amount: borrowBal,
337
+ apy: rateToApy(pos.pool.currentBorrowRate)
338
+ });
339
+ }
340
+ }
341
+ return { positions };
342
+ }
343
+ async function maxWithdrawAmount(client, addressOrKeypair) {
344
+ const hf = await getHealthFactor(client, addressOrKeypair);
345
+ const ltv = hf.liquidationThreshold > 0 ? hf.liquidationThreshold : 0.75;
346
+ let maxAmount;
347
+ if (hf.borrowed === 0) {
348
+ maxAmount = hf.supplied;
349
+ } else {
350
+ maxAmount = Math.max(0, hf.supplied - hf.borrowed * MIN_HEALTH_FACTOR / ltv);
351
+ }
352
+ const remainingSupply = hf.supplied - maxAmount;
353
+ const hfAfter = hf.borrowed > 0 ? remainingSupply / hf.borrowed : Infinity;
354
+ return {
355
+ maxAmount,
356
+ healthFactorAfter: hfAfter,
357
+ currentHF: hf.healthFactor
358
+ };
359
+ }
360
+ async function maxBorrowAmount(client, addressOrKeypair) {
361
+ const hf = await getHealthFactor(client, addressOrKeypair);
362
+ const ltv = hf.liquidationThreshold > 0 ? hf.liquidationThreshold : 0.75;
363
+ const maxAmount = Math.max(0, hf.supplied * ltv / MIN_HEALTH_FACTOR - hf.borrowed);
364
+ return {
365
+ maxAmount,
366
+ healthFactorAfter: MIN_HEALTH_FACTOR,
367
+ currentHF: hf.healthFactor
368
+ };
369
+ }
370
+
371
+ // src/adapters/navi.ts
372
+ var NaviAdapter = class {
373
+ id = "navi";
374
+ name = "NAVI Protocol";
375
+ version = "1.0.0";
376
+ capabilities = ["save", "withdraw", "borrow", "repay"];
377
+ supportedAssets = ["USDC"];
378
+ supportsSameAssetBorrow = true;
379
+ client;
380
+ async init(client) {
381
+ this.client = client;
382
+ }
383
+ initSync(client) {
384
+ this.client = client;
385
+ }
386
+ async getRates(asset) {
387
+ const rates = await getRates(this.client);
388
+ const key = asset.toUpperCase();
389
+ const r = rates[key];
390
+ if (!r) throw new Error(`NAVI does not support ${asset}`);
391
+ return { asset, saveApy: r.saveApy, borrowApy: r.borrowApy };
392
+ }
393
+ async getPositions(address) {
394
+ const result = await getPositions(this.client, address);
395
+ return {
396
+ supplies: result.positions.filter((p) => p.type === "save").map((p) => ({ asset: p.asset, amount: p.amount, apy: p.apy })),
397
+ borrows: result.positions.filter((p) => p.type === "borrow").map((p) => ({ asset: p.asset, amount: p.amount, apy: p.apy }))
398
+ };
399
+ }
400
+ async getHealth(address) {
401
+ return getHealthFactor(this.client, address);
402
+ }
403
+ async buildSaveTx(address, amount, _asset, options) {
404
+ const tx = await buildSaveTx(this.client, address, amount, options);
405
+ return { tx };
406
+ }
407
+ async buildWithdrawTx(address, amount, _asset) {
408
+ const result = await buildWithdrawTx(this.client, address, amount);
409
+ return { tx: result.tx, effectiveAmount: result.effectiveAmount };
410
+ }
411
+ async buildBorrowTx(address, amount, _asset, options) {
412
+ const tx = await buildBorrowTx(this.client, address, amount, options);
413
+ return { tx };
414
+ }
415
+ async buildRepayTx(address, amount, _asset) {
416
+ const tx = await buildRepayTx(this.client, address, amount);
417
+ return { tx };
418
+ }
419
+ async maxWithdraw(address, _asset) {
420
+ return maxWithdrawAmount(this.client, address);
421
+ }
422
+ async maxBorrow(address, _asset) {
423
+ return maxBorrowAmount(this.client, address);
424
+ }
425
+ };
426
+ var DEFAULT_SLIPPAGE_BPS = 300;
427
+ function createAggregatorClient(client, signer) {
428
+ return new aggregatorSdk.AggregatorClient({
429
+ client,
430
+ signer,
431
+ env: aggregatorSdk.Env.Mainnet
432
+ });
433
+ }
434
+ async function buildSwapTx(params) {
435
+ const { client, address, fromAsset, toAsset, amount, maxSlippageBps = DEFAULT_SLIPPAGE_BPS } = params;
436
+ const fromInfo = SUPPORTED_ASSETS[fromAsset];
437
+ const toInfo = SUPPORTED_ASSETS[toAsset];
438
+ const rawAmount = BigInt(Math.floor(amount * 10 ** fromInfo.decimals));
439
+ const aggClient = createAggregatorClient(client, address);
440
+ const result = await aggClient.findRouters({
441
+ from: fromInfo.type,
442
+ target: toInfo.type,
443
+ amount: rawAmount,
444
+ byAmountIn: true
445
+ });
446
+ if (!result || result.insufficientLiquidity) {
447
+ throw new T2000Error(
448
+ "ASSET_NOT_SUPPORTED",
449
+ `No swap route found for ${fromAsset} \u2192 ${toAsset}`
450
+ );
451
+ }
452
+ const tx = new transactions.Transaction();
453
+ const slippage = maxSlippageBps / 1e4;
454
+ await aggClient.fastRouterSwap({
455
+ router: result,
456
+ txb: tx,
457
+ slippage
458
+ });
459
+ const estimatedOut = Number(result.amountOut.toString());
460
+ return {
461
+ tx,
462
+ estimatedOut,
463
+ toDecimals: toInfo.decimals
464
+ };
465
+ }
466
+ async function getPoolPrice(client) {
467
+ try {
468
+ const pool = await client.getObject({
469
+ id: CETUS_USDC_SUI_POOL,
470
+ options: { showContent: true }
471
+ });
472
+ if (pool.data?.content?.dataType === "moveObject") {
473
+ const fields = pool.data.content.fields;
474
+ const currentSqrtPrice = BigInt(String(fields.current_sqrt_price ?? "0"));
475
+ if (currentSqrtPrice > 0n) {
476
+ const Q64 = 2n ** 64n;
477
+ const sqrtPriceFloat = Number(currentSqrtPrice) / Number(Q64);
478
+ const rawPrice = sqrtPriceFloat * sqrtPriceFloat;
479
+ const suiPriceUsd = 1e3 / rawPrice;
480
+ if (suiPriceUsd > 0.01 && suiPriceUsd < 1e3) return suiPriceUsd;
481
+ }
482
+ }
483
+ } catch {
484
+ }
485
+ return 3.5;
486
+ }
487
+ async function getSwapQuote(client, fromAsset, toAsset, amount) {
488
+ const fromInfo = SUPPORTED_ASSETS[fromAsset];
489
+ const toInfo = SUPPORTED_ASSETS[toAsset];
490
+ const rawAmount = BigInt(Math.floor(amount * 10 ** fromInfo.decimals));
491
+ const poolPrice = await getPoolPrice(client);
492
+ try {
493
+ const aggClient = createAggregatorClient(client);
494
+ const result = await aggClient.findRouters({
495
+ from: fromInfo.type,
496
+ target: toInfo.type,
497
+ amount: rawAmount,
498
+ byAmountIn: true
499
+ });
500
+ if (!result || result.insufficientLiquidity) {
501
+ return fallbackQuote(fromAsset, amount, poolPrice);
502
+ }
503
+ const expectedOutput = Number(result.amountOut.toString()) / 10 ** toInfo.decimals;
504
+ const priceImpact = result.deviationRatio ?? 0;
505
+ return { expectedOutput, priceImpact, poolPrice };
506
+ } catch {
507
+ return fallbackQuote(fromAsset, amount, poolPrice);
508
+ }
509
+ }
510
+ function fallbackQuote(fromAsset, amount, poolPrice) {
511
+ const expectedOutput = fromAsset === "USDC" ? amount / poolPrice : amount * poolPrice;
512
+ return { expectedOutput, priceImpact: 0, poolPrice };
513
+ }
514
+
515
+ // src/adapters/cetus.ts
516
+ var CetusAdapter = class {
517
+ id = "cetus";
518
+ name = "Cetus";
519
+ version = "1.0.0";
520
+ capabilities = ["swap"];
521
+ client;
522
+ async init(client) {
523
+ this.client = client;
524
+ }
525
+ initSync(client) {
526
+ this.client = client;
527
+ }
528
+ async getQuote(from, to, amount) {
529
+ return getSwapQuote(
530
+ this.client,
531
+ from.toUpperCase(),
532
+ to.toUpperCase(),
533
+ amount
534
+ );
535
+ }
536
+ async buildSwapTx(address, from, to, amount, maxSlippageBps) {
537
+ const result = await buildSwapTx({
538
+ client: this.client,
539
+ address,
540
+ fromAsset: from.toUpperCase(),
541
+ toAsset: to.toUpperCase(),
542
+ amount,
543
+ maxSlippageBps
544
+ });
545
+ return {
546
+ tx: result.tx,
547
+ estimatedOut: result.estimatedOut,
548
+ toDecimals: result.toDecimals
549
+ };
550
+ }
551
+ getSupportedPairs() {
552
+ return [
553
+ { from: "USDC", to: "SUI" },
554
+ { from: "SUI", to: "USDC" }
555
+ ];
556
+ }
557
+ async getPoolPrice() {
558
+ return getPoolPrice(this.client);
559
+ }
560
+ };
561
+ var USDC_TYPE2 = SUPPORTED_ASSETS.USDC.type;
562
+ var WAD = 1e18;
563
+ var MIN_HEALTH_FACTOR2 = 1.5;
564
+ function interpolateRate(utilBreakpoints, aprBreakpoints, utilizationPct) {
565
+ if (utilBreakpoints.length === 0) return 0;
566
+ if (utilizationPct <= utilBreakpoints[0]) return aprBreakpoints[0];
567
+ if (utilizationPct >= utilBreakpoints[utilBreakpoints.length - 1]) {
568
+ return aprBreakpoints[aprBreakpoints.length - 1];
569
+ }
570
+ for (let i = 1; i < utilBreakpoints.length; i++) {
571
+ if (utilizationPct <= utilBreakpoints[i]) {
572
+ const t = (utilizationPct - utilBreakpoints[i - 1]) / (utilBreakpoints[i] - utilBreakpoints[i - 1]);
573
+ return aprBreakpoints[i - 1] + t * (aprBreakpoints[i] - aprBreakpoints[i - 1]);
574
+ }
575
+ }
576
+ return aprBreakpoints[aprBreakpoints.length - 1];
577
+ }
578
+ function computeRatesFromReserve(reserve) {
579
+ const decimals = reserve.mintDecimals;
580
+ const available = Number(reserve.availableAmount) / 10 ** decimals;
581
+ const borrowed = Number(reserve.borrowedAmount.value) / WAD / 10 ** decimals;
582
+ const totalDeposited = available + borrowed;
583
+ const utilizationPct = totalDeposited > 0 ? borrowed / totalDeposited * 100 : 0;
584
+ const config = reserve.config.element;
585
+ if (!config) return { borrowAprPct: 0, depositAprPct: 0, utilizationPct: 0 };
586
+ const utils = config.interestRateUtils.map(Number);
587
+ const aprs = config.interestRateAprs.map((a) => Number(a) / 100);
588
+ const borrowAprPct = interpolateRate(utils, aprs, utilizationPct);
589
+ const spreadFeeBps = Number(config.spreadFeeBps);
590
+ const depositAprPct = utilizationPct / 100 * (borrowAprPct / 100) * (1 - spreadFeeBps / 1e4) * 100;
591
+ return { borrowAprPct, depositAprPct, utilizationPct };
592
+ }
593
+ function cTokenRatio(reserve) {
594
+ if (reserve.ctokenSupply === 0n) return 1;
595
+ const available = Number(reserve.availableAmount);
596
+ const borrowed = Number(reserve.borrowedAmount.value) / WAD;
597
+ const spreadFees = Number(reserve.unclaimedSpreadFees.value) / WAD;
598
+ const totalSupply = available + borrowed - spreadFees;
599
+ return totalSupply / Number(reserve.ctokenSupply);
600
+ }
601
+ var SuilendAdapter = class {
602
+ id = "suilend";
603
+ name = "Suilend";
604
+ version = "1.0.0";
605
+ capabilities = ["save", "withdraw"];
606
+ supportedAssets = ["USDC"];
607
+ supportsSameAssetBorrow = false;
608
+ client;
609
+ suilend;
610
+ lendingMarketType;
611
+ initialized = false;
612
+ async init(client) {
613
+ let sdk;
614
+ try {
615
+ sdk = await import('@suilend/sdk');
616
+ } catch {
617
+ throw new T2000Error(
618
+ "PROTOCOL_UNAVAILABLE",
619
+ "Suilend SDK not installed. Run: npm install @suilend/sdk@^1"
620
+ );
621
+ }
622
+ this.client = client;
623
+ this.lendingMarketType = sdk.LENDING_MARKET_TYPE;
624
+ try {
625
+ this.suilend = await sdk.SuilendClient.initialize(
626
+ sdk.LENDING_MARKET_ID,
627
+ sdk.LENDING_MARKET_TYPE,
628
+ client
629
+ );
630
+ } catch (err) {
631
+ throw new T2000Error(
632
+ "PROTOCOL_UNAVAILABLE",
633
+ `Failed to initialize Suilend: ${err instanceof Error ? err.message : String(err)}`
634
+ );
635
+ }
636
+ this.initialized = true;
637
+ }
638
+ ensureInit() {
639
+ if (!this.initialized) {
640
+ throw new T2000Error(
641
+ "PROTOCOL_UNAVAILABLE",
642
+ "SuilendAdapter not initialized. Call init() first."
643
+ );
644
+ }
645
+ }
646
+ findReserve(asset) {
647
+ const upper = asset.toUpperCase();
648
+ let coinType;
649
+ if (upper === "USDC") coinType = USDC_TYPE2;
650
+ else if (upper === "SUI") coinType = "0x2::sui::SUI";
651
+ else if (asset.includes("::")) coinType = asset;
652
+ else return void 0;
653
+ try {
654
+ const normalized = utils.normalizeStructTag(coinType);
655
+ return this.suilend.lendingMarket.reserves.find(
656
+ (r) => utils.normalizeStructTag(r.coinType.name) === normalized
657
+ );
658
+ } catch {
659
+ return void 0;
660
+ }
661
+ }
662
+ async getObligationCaps(address) {
663
+ const SuilendClientStatic = (await import('@suilend/sdk')).SuilendClient;
664
+ return SuilendClientStatic.getObligationOwnerCaps(
665
+ address,
666
+ [this.lendingMarketType],
667
+ this.client
668
+ );
669
+ }
670
+ resolveSymbol(coinType) {
671
+ const normalized = utils.normalizeStructTag(coinType);
672
+ if (normalized === utils.normalizeStructTag(USDC_TYPE2)) return "USDC";
673
+ if (normalized === utils.normalizeStructTag("0x2::sui::SUI")) return "SUI";
674
+ const parts = coinType.split("::");
675
+ return parts[parts.length - 1] || "UNKNOWN";
676
+ }
677
+ async getRates(asset) {
678
+ this.ensureInit();
679
+ const reserve = this.findReserve(asset);
680
+ if (!reserve) {
681
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `Suilend does not support ${asset}`);
682
+ }
683
+ const { borrowAprPct, depositAprPct } = computeRatesFromReserve(reserve);
684
+ return {
685
+ asset,
686
+ saveApy: depositAprPct,
687
+ borrowApy: borrowAprPct
688
+ };
689
+ }
690
+ async getPositions(address) {
691
+ this.ensureInit();
692
+ const supplies = [];
693
+ const borrows = [];
694
+ const caps = await this.getObligationCaps(address);
695
+ if (caps.length === 0) return { supplies, borrows };
696
+ const obligation = await this.suilend.getObligation(caps[0].obligationId);
697
+ for (const deposit of obligation.deposits) {
698
+ const coinType = utils.normalizeStructTag(deposit.coinType.name);
699
+ const reserve = this.suilend.lendingMarket.reserves.find(
700
+ (r) => utils.normalizeStructTag(r.coinType.name) === coinType
701
+ );
702
+ if (!reserve) continue;
703
+ const ctokenAmount = Number(deposit.depositedCtokenAmount.toString());
704
+ const ratio = cTokenRatio(reserve);
705
+ const amount = ctokenAmount * ratio / 10 ** reserve.mintDecimals;
706
+ const { depositAprPct } = computeRatesFromReserve(reserve);
707
+ supplies.push({ asset: this.resolveSymbol(coinType), amount, apy: depositAprPct });
708
+ }
709
+ for (const borrow of obligation.borrows) {
710
+ const coinType = utils.normalizeStructTag(borrow.coinType.name);
711
+ const reserve = this.suilend.lendingMarket.reserves.find(
712
+ (r) => utils.normalizeStructTag(r.coinType.name) === coinType
713
+ );
714
+ if (!reserve) continue;
715
+ const rawBorrowed = Number(borrow.borrowedAmount.value.toString()) / WAD;
716
+ const amount = rawBorrowed / 10 ** reserve.mintDecimals;
717
+ const reserveRate = Number(reserve.cumulativeBorrowRate.value.toString()) / WAD;
718
+ const posRate = Number(borrow.cumulativeBorrowRate.value.toString()) / WAD;
719
+ const compounded = posRate > 0 ? amount * (reserveRate / posRate) : amount;
720
+ const { borrowAprPct } = computeRatesFromReserve(reserve);
721
+ borrows.push({ asset: this.resolveSymbol(coinType), amount: compounded, apy: borrowAprPct });
722
+ }
723
+ return { supplies, borrows };
724
+ }
725
+ async getHealth(address) {
726
+ this.ensureInit();
727
+ const caps = await this.getObligationCaps(address);
728
+ if (caps.length === 0) {
729
+ return { healthFactor: Infinity, supplied: 0, borrowed: 0, maxBorrow: 0, liquidationThreshold: 0 };
730
+ }
731
+ const positions = await this.getPositions(address);
732
+ const supplied = positions.supplies.reduce((s, p) => s + p.amount, 0);
733
+ const borrowed = positions.borrows.reduce((s, p) => s + p.amount, 0);
734
+ const reserve = this.findReserve("USDC");
735
+ const closeLtv = reserve?.config?.element?.closeLtvPct ?? 75;
736
+ const openLtv = reserve?.config?.element?.openLtvPct ?? 70;
737
+ const liqThreshold = closeLtv / 100;
738
+ const healthFactor = borrowed > 0 ? supplied * liqThreshold / borrowed : Infinity;
739
+ const maxBorrow = Math.max(0, supplied * (openLtv / 100) - borrowed);
740
+ return { healthFactor, supplied, borrowed, maxBorrow, liquidationThreshold: liqThreshold };
741
+ }
742
+ async buildSaveTx(address, amount, _asset, options) {
743
+ this.ensureInit();
744
+ const rawAmount = usdcToRaw(amount).toString();
745
+ const tx = new transactions.Transaction();
746
+ tx.setSender(address);
747
+ const caps = await this.getObligationCaps(address);
748
+ let capRef;
749
+ if (caps.length === 0) {
750
+ const [newCap] = this.suilend.createObligation(tx);
751
+ capRef = newCap;
752
+ } else {
753
+ capRef = caps[0].id;
754
+ }
755
+ const allCoins = await this.fetchAllCoins(address, USDC_TYPE2);
756
+ if (allCoins.length === 0) {
757
+ throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins found");
758
+ }
759
+ const primaryCoinId = allCoins[0].coinObjectId;
760
+ if (allCoins.length > 1) {
761
+ tx.mergeCoins(
762
+ tx.object(primaryCoinId),
763
+ allCoins.slice(1).map((c) => tx.object(c.coinObjectId))
764
+ );
765
+ }
766
+ const [depositCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawAmount]);
767
+ if (options?.collectFee) {
768
+ addCollectFeeToTx(tx, depositCoin, "save");
769
+ }
770
+ this.suilend.deposit(depositCoin, USDC_TYPE2, capRef, tx);
771
+ return { tx };
772
+ }
773
+ async buildWithdrawTx(address, amount, _asset) {
774
+ this.ensureInit();
775
+ const caps = await this.getObligationCaps(address);
776
+ if (caps.length === 0) {
777
+ throw new T2000Error("NO_COLLATERAL", "No Suilend position found");
778
+ }
779
+ const positions = await this.getPositions(address);
780
+ const usdcSupply = positions.supplies.find((s) => s.asset === "USDC");
781
+ const deposited = usdcSupply?.amount ?? 0;
782
+ const effectiveAmount = Math.min(amount, deposited);
783
+ if (effectiveAmount <= 0) {
784
+ throw new T2000Error("NO_COLLATERAL", "Nothing to withdraw from Suilend");
785
+ }
786
+ const rawAmount = usdcToRaw(effectiveAmount).toString();
787
+ const tx = new transactions.Transaction();
788
+ tx.setSender(address);
789
+ await this.suilend.withdrawAndSendToUser(
790
+ address,
791
+ caps[0].id,
792
+ caps[0].obligationId,
793
+ USDC_TYPE2,
794
+ rawAmount,
795
+ tx
796
+ );
797
+ return { tx, effectiveAmount };
798
+ }
799
+ async buildBorrowTx(_address, _amount, _asset, _options) {
800
+ throw new T2000Error(
801
+ "ASSET_NOT_SUPPORTED",
802
+ "SuilendAdapter.buildBorrowTx() not available \u2014 Suilend requires different collateral/borrow assets. Deferred to Phase 10."
803
+ );
804
+ }
805
+ async buildRepayTx(_address, _amount, _asset) {
806
+ throw new T2000Error(
807
+ "ASSET_NOT_SUPPORTED",
808
+ "SuilendAdapter.buildRepayTx() not available \u2014 deferred to Phase 10."
809
+ );
810
+ }
811
+ async maxWithdraw(address, _asset) {
812
+ this.ensureInit();
813
+ const health = await this.getHealth(address);
814
+ let maxAmount;
815
+ if (health.borrowed === 0) {
816
+ maxAmount = health.supplied;
817
+ } else {
818
+ maxAmount = Math.max(
819
+ 0,
820
+ health.supplied - health.borrowed * MIN_HEALTH_FACTOR2 / health.liquidationThreshold
821
+ );
822
+ }
823
+ const remainingSupply = health.supplied - maxAmount;
824
+ const hfAfter = health.borrowed > 0 ? remainingSupply * health.liquidationThreshold / health.borrowed : Infinity;
825
+ return {
826
+ maxAmount,
827
+ healthFactorAfter: hfAfter,
828
+ currentHF: health.healthFactor
829
+ };
830
+ }
831
+ async maxBorrow(_address, _asset) {
832
+ throw new T2000Error(
833
+ "ASSET_NOT_SUPPORTED",
834
+ "SuilendAdapter.maxBorrow() not available \u2014 deferred to Phase 10."
835
+ );
836
+ }
837
+ async fetchAllCoins(owner, coinType) {
838
+ const all = [];
839
+ let cursor = null;
840
+ let hasNext = true;
841
+ while (hasNext) {
842
+ const page = await this.client.getCoins({ owner, coinType, cursor: cursor ?? void 0 });
843
+ all.push(...page.data.map((c) => ({ coinObjectId: c.coinObjectId, balance: c.balance })));
844
+ cursor = page.nextCursor;
845
+ hasNext = page.hasNextPage;
846
+ }
847
+ return all;
848
+ }
849
+ };
850
+
851
+ exports.CetusAdapter = CetusAdapter;
852
+ exports.NaviAdapter = NaviAdapter;
853
+ exports.ProtocolRegistry = ProtocolRegistry;
854
+ exports.SuilendAdapter = SuilendAdapter;
855
+ //# sourceMappingURL=index.cjs.map
856
+ //# sourceMappingURL=index.cjs.map