@t2000/sdk 0.7.2 → 0.8.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.
@@ -3,6 +3,52 @@ import { bcs } from '@mysten/sui/bcs';
3
3
  import { AggregatorClient, Env } from '@cetusprotocol/aggregator-sdk';
4
4
  import { normalizeStructTag } from '@mysten/sui/utils';
5
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
+ };
42
+ var STABLE_ASSETS = ["USDC", "USDT", "USDe", "USDsui"];
43
+ var T2000_PACKAGE_ID = process.env.T2000_PACKAGE_ID ?? "0xab92e9f1fe549ad3d6a52924a73181b45791e76120b975138fac9ec9b75db9f3";
44
+ var T2000_CONFIG_ID = process.env.T2000_CONFIG_ID ?? "0x408add9aa9322f93cfd87523d8f603006eb8713894f4c460283c58a6888dae8a";
45
+ var T2000_TREASURY_ID = process.env.T2000_TREASURY_ID ?? "0x3bb501b8300125dca59019247941a42af6b292a150ce3cfcce9449456be2ec91";
46
+ process.env.T2000_API_URL ?? "https://api.t2000.ai";
47
+ var CETUS_USDC_SUI_POOL = "0x51e883ba7c0b566a26cbc8a94cd33eb0abd418a77cc1e60ad22fd9b1f29cd2ab";
48
+ var CETUS_PACKAGE = "0x1eabed72c53feb3805120a081dc15963c204dc8d091542592abaf7a35689b2fb";
49
+ var SENTINEL = {
50
+ PACKAGE: "0x88b83f36dafcd5f6dcdcf1d2cb5889b03f61264ab3cee9cae35db7aa940a21b7"};
51
+
6
52
  // src/errors.ts
7
53
  var T2000Error = class extends Error {
8
54
  code;
@@ -87,6 +133,41 @@ var ProtocolRegistry = class {
87
133
  candidates.sort((a, b) => b.quote.expectedOutput - a.quote.expectedOutput);
88
134
  return candidates[0];
89
135
  }
136
+ async bestSaveRateAcrossAssets() {
137
+ const candidates = [];
138
+ for (const asset of STABLE_ASSETS) {
139
+ for (const adapter of this.lending.values()) {
140
+ if (!adapter.supportedAssets.includes(asset)) continue;
141
+ if (!adapter.capabilities.includes("save")) continue;
142
+ try {
143
+ const rate = await adapter.getRates(asset);
144
+ candidates.push({ adapter, rate, asset });
145
+ } catch {
146
+ }
147
+ }
148
+ }
149
+ if (candidates.length === 0) {
150
+ throw new T2000Error("ASSET_NOT_SUPPORTED", "No lending adapter found for any stablecoin");
151
+ }
152
+ candidates.sort((a, b) => b.rate.saveApy - a.rate.saveApy);
153
+ return candidates[0];
154
+ }
155
+ async allRatesAcrossAssets() {
156
+ const results = [];
157
+ for (const asset of STABLE_ASSETS) {
158
+ for (const adapter of this.lending.values()) {
159
+ if (!adapter.supportedAssets.includes(asset)) continue;
160
+ try {
161
+ const rates = await adapter.getRates(asset);
162
+ if (rates.saveApy > 0 || rates.borrowApy > 0) {
163
+ results.push({ protocol: adapter.name, protocolId: adapter.id, asset, rates });
164
+ }
165
+ } catch {
166
+ }
167
+ }
168
+ }
169
+ return results;
170
+ }
90
171
  async allRates(asset) {
91
172
  const results = [];
92
173
  for (const adapter of this.lending.values()) {
@@ -124,38 +205,33 @@ var ProtocolRegistry = class {
124
205
  listSwap() {
125
206
  return [...this.swap.values()];
126
207
  }
127
- };
128
-
129
- // src/constants.ts
130
- var USDC_DECIMALS = 6;
131
- var SAVE_FEE_BPS = 10n;
132
- var SWAP_FEE_BPS = 0n;
133
- var BORROW_FEE_BPS = 5n;
134
- var SUPPORTED_ASSETS = {
135
- USDC: {
136
- type: "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC",
137
- decimals: 6,
138
- symbol: "USDC"
139
- },
140
- SUI: {
141
- type: "0x2::sui::SUI",
142
- decimals: 9,
143
- symbol: "SUI"
208
+ isSupportedAsset(asset, capability) {
209
+ for (const adapter of this.lending.values()) {
210
+ if (!adapter.supportedAssets.includes(asset)) continue;
211
+ if (capability && !adapter.capabilities.includes(capability)) continue;
212
+ return true;
213
+ }
214
+ return false;
215
+ }
216
+ getSupportedAssets(capability) {
217
+ const assets = /* @__PURE__ */ new Set();
218
+ for (const adapter of this.lending.values()) {
219
+ if (capability && !adapter.capabilities.includes(capability)) continue;
220
+ for (const a of adapter.supportedAssets) {
221
+ assets.add(a);
222
+ }
223
+ }
224
+ return [...assets];
144
225
  }
145
226
  };
146
- var T2000_PACKAGE_ID = process.env.T2000_PACKAGE_ID ?? "0xab92e9f1fe549ad3d6a52924a73181b45791e76120b975138fac9ec9b75db9f3";
147
- var T2000_CONFIG_ID = process.env.T2000_CONFIG_ID ?? "0x408add9aa9322f93cfd87523d8f603006eb8713894f4c460283c58a6888dae8a";
148
- var T2000_TREASURY_ID = process.env.T2000_TREASURY_ID ?? "0x3bb501b8300125dca59019247941a42af6b292a150ce3cfcce9449456be2ec91";
149
- process.env.T2000_API_URL ?? "https://api.t2000.ai";
150
- var CETUS_USDC_SUI_POOL = "0x51e883ba7c0b566a26cbc8a94cd33eb0abd418a77cc1e60ad22fd9b1f29cd2ab";
151
- var CETUS_PACKAGE = "0x1eabed72c53feb3805120a081dc15963c204dc8d091542592abaf7a35689b2fb";
152
- var SENTINEL = {
153
- PACKAGE: "0x88b83f36dafcd5f6dcdcf1d2cb5889b03f61264ab3cee9cae35db7aa940a21b7"};
154
227
 
155
228
  // src/utils/format.ts
156
- function usdcToRaw(amount) {
157
- return BigInt(Math.round(amount * 10 ** USDC_DECIMALS));
229
+ function stableToRaw(amount, decimals) {
230
+ return BigInt(Math.round(amount * 10 ** decimals));
158
231
  }
232
+ new Map(
233
+ Object.keys(SUPPORTED_ASSETS).map((k) => [k.toUpperCase(), k])
234
+ );
159
235
 
160
236
  // src/protocols/protocolFee.ts
161
237
  var FEE_RATES = {
@@ -182,9 +258,6 @@ function addCollectFeeToTx(tx, paymentCoin, operation) {
182
258
  ]
183
259
  });
184
260
  }
185
-
186
- // src/protocols/navi.ts
187
- var USDC_TYPE = SUPPORTED_ASSETS.USDC.type;
188
261
  var RATE_DECIMALS = 27;
189
262
  var LTV_DECIMALS = 27;
190
263
  var MIN_HEALTH_FACTOR = 1.5;
@@ -245,13 +318,24 @@ async function getPools(fresh = false) {
245
318
  poolsCache = { data, ts: Date.now() };
246
319
  return data;
247
320
  }
248
- async function getUsdcPool() {
321
+ function matchesCoinType(poolType, targetType) {
322
+ const poolSuffix = poolType.split("::").slice(1).join("::").toLowerCase();
323
+ const targetSuffix = targetType.split("::").slice(1).join("::").toLowerCase();
324
+ return poolSuffix === targetSuffix;
325
+ }
326
+ async function getPool(asset = "USDC") {
249
327
  const pools = await getPools();
250
- const usdc = pools.find(
251
- (p) => p.token?.symbol === "USDC" || p.coinType?.toLowerCase().includes("usdc")
328
+ const targetType = SUPPORTED_ASSETS[asset].type;
329
+ const pool = pools.find(
330
+ (p) => matchesCoinType(p.suiCoinType || p.coinType || "", targetType)
252
331
  );
253
- if (!usdc) throw new T2000Error("PROTOCOL_UNAVAILABLE", "USDC pool not found on NAVI");
254
- return usdc;
332
+ if (!pool) {
333
+ throw new T2000Error(
334
+ "ASSET_NOT_SUPPORTED",
335
+ `${SUPPORTED_ASSETS[asset].displayName} pool not found on NAVI. Try: ${STABLE_ASSETS.filter((a) => a !== asset).join(", ")}`
336
+ );
337
+ }
338
+ return pool;
255
339
  }
256
340
  function addOracleUpdate(tx, config, pool) {
257
341
  const feed = config.oracle.feeds?.find((f2) => f2.assetId === pool.id);
@@ -339,10 +423,12 @@ async function buildSaveTx(client, address, amount, options = {}) {
339
423
  if (!amount || amount <= 0 || !Number.isFinite(amount)) {
340
424
  throw new T2000Error("INVALID_AMOUNT", "Save amount must be a positive number");
341
425
  }
342
- const rawAmount = Number(usdcToRaw(amount));
343
- const [config, pool] = await Promise.all([getConfig(), getUsdcPool()]);
344
- const coins = await fetchCoins(client, address, USDC_TYPE);
345
- if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins found");
426
+ const asset = options.asset ?? "USDC";
427
+ const assetInfo = SUPPORTED_ASSETS[asset];
428
+ const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
429
+ const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
430
+ const coins = await fetchCoins(client, address, assetInfo.type);
431
+ if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins found`);
346
432
  const tx = new Transaction();
347
433
  tx.setSender(address);
348
434
  const coinObj = mergeCoins(tx, coins);
@@ -365,18 +451,20 @@ async function buildSaveTx(client, address, amount, options = {}) {
365
451
  });
366
452
  return tx;
367
453
  }
368
- async function buildWithdrawTx(client, address, amount) {
454
+ async function buildWithdrawTx(client, address, amount, options = {}) {
455
+ const asset = options.asset ?? "USDC";
456
+ const assetInfo = SUPPORTED_ASSETS[asset];
369
457
  const [config, pool, pools, states] = await Promise.all([
370
458
  getConfig(),
371
- getUsdcPool(),
459
+ getPool(asset),
372
460
  getPools(),
373
461
  getUserState(client, address)
374
462
  ]);
375
- const usdcState = states.find((s) => s.assetId === pool.id);
376
- const deposited = usdcState ? compoundBalance(usdcState.supplyBalance, pool.currentSupplyIndex) : 0;
463
+ const assetState = states.find((s) => s.assetId === pool.id);
464
+ const deposited = assetState ? compoundBalance(assetState.supplyBalance, pool.currentSupplyIndex) : 0;
377
465
  const effectiveAmount = Math.min(amount, Math.max(0, deposited - WITHDRAW_DUST_BUFFER));
378
- if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", "Nothing to withdraw");
379
- const rawAmount = Number(usdcToRaw(effectiveAmount));
466
+ if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on NAVI`);
467
+ const rawAmount = Number(stableToRaw(effectiveAmount, assetInfo.decimals));
380
468
  const tx = new Transaction();
381
469
  tx.setSender(address);
382
470
  addOracleUpdate(tx, config, pool);
@@ -407,8 +495,10 @@ async function buildBorrowTx(client, address, amount, options = {}) {
407
495
  if (!amount || amount <= 0 || !Number.isFinite(amount)) {
408
496
  throw new T2000Error("INVALID_AMOUNT", "Borrow amount must be a positive number");
409
497
  }
410
- const rawAmount = Number(usdcToRaw(amount));
411
- const [config, pool] = await Promise.all([getConfig(), getUsdcPool()]);
498
+ const asset = options.asset ?? "USDC";
499
+ const assetInfo = SUPPORTED_ASSETS[asset];
500
+ const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
501
+ const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
412
502
  const tx = new Transaction();
413
503
  tx.setSender(address);
414
504
  addOracleUpdate(tx, config, pool);
@@ -438,14 +528,16 @@ async function buildBorrowTx(client, address, amount, options = {}) {
438
528
  tx.transferObjects([borrowedCoin], address);
439
529
  return tx;
440
530
  }
441
- async function buildRepayTx(client, address, amount) {
531
+ async function buildRepayTx(client, address, amount, options = {}) {
442
532
  if (!amount || amount <= 0 || !Number.isFinite(amount)) {
443
533
  throw new T2000Error("INVALID_AMOUNT", "Repay amount must be a positive number");
444
534
  }
445
- const rawAmount = Number(usdcToRaw(amount));
446
- const [config, pool] = await Promise.all([getConfig(), getUsdcPool()]);
447
- const coins = await fetchCoins(client, address, USDC_TYPE);
448
- if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins to repay with");
535
+ const asset = options.asset ?? "USDC";
536
+ const assetInfo = SUPPORTED_ASSETS[asset];
537
+ const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
538
+ const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
539
+ const coins = await fetchCoins(client, address, assetInfo.type);
540
+ if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins to repay with`);
449
541
  const tx = new Transaction();
450
542
  tx.setSender(address);
451
543
  addOracleUpdate(tx, config, pool);
@@ -469,21 +561,36 @@ async function buildRepayTx(client, address, amount) {
469
561
  }
470
562
  async function getHealthFactor(client, addressOrKeypair) {
471
563
  const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
472
- const [config, pool, states] = await Promise.all([
564
+ const [config, pools, states] = await Promise.all([
473
565
  getConfig(),
474
- getUsdcPool(),
566
+ getPools(),
475
567
  getUserState(client, address)
476
568
  ]);
477
- const usdcState = states.find((s) => s.assetId === pool.id);
478
- const supplied = usdcState ? compoundBalance(usdcState.supplyBalance, pool.currentSupplyIndex) : 0;
479
- const borrowed = usdcState ? compoundBalance(usdcState.borrowBalance, pool.currentBorrowIndex) : 0;
480
- const ltv = parseLtv(pool.ltv);
481
- const liqThreshold = parseLiqThreshold(pool.liquidationFactor.threshold);
569
+ let supplied = 0;
570
+ let borrowed = 0;
571
+ let weightedLtv = 0;
572
+ let weightedLiqThreshold = 0;
573
+ for (const state of states) {
574
+ const pool = pools.find((p) => p.id === state.assetId);
575
+ if (!pool) continue;
576
+ const supplyBal = compoundBalance(state.supplyBalance, pool.currentSupplyIndex);
577
+ const borrowBal = compoundBalance(state.borrowBalance, pool.currentBorrowIndex);
578
+ const price = pool.token?.price ?? 1;
579
+ supplied += supplyBal * price;
580
+ borrowed += borrowBal * price;
581
+ if (supplyBal > 0) {
582
+ weightedLtv += supplyBal * price * parseLtv(pool.ltv);
583
+ weightedLiqThreshold += supplyBal * price * parseLiqThreshold(pool.liquidationFactor.threshold);
584
+ }
585
+ }
586
+ const ltv = supplied > 0 ? weightedLtv / supplied : 0.75;
587
+ const liqThreshold = supplied > 0 ? weightedLiqThreshold / supplied : 0.75;
482
588
  const maxBorrowVal = Math.max(0, supplied * ltv - borrowed);
589
+ const usdcPool = pools.find((p) => matchesCoinType(p.suiCoinType || p.coinType || "", SUPPORTED_ASSETS.USDC.type));
483
590
  let healthFactor;
484
591
  if (borrowed <= 0) {
485
592
  healthFactor = Infinity;
486
- } else {
593
+ } else if (usdcPool) {
487
594
  try {
488
595
  const tx = new Transaction();
489
596
  tx.moveCall({
@@ -492,14 +599,14 @@ async function getHealthFactor(client, addressOrKeypair) {
492
599
  tx.object(CLOCK),
493
600
  tx.object(config.storage),
494
601
  tx.object(config.oracle.priceOracle),
495
- tx.pure.u8(pool.id),
602
+ tx.pure.u8(usdcPool.id),
496
603
  tx.pure.address(address),
497
- tx.pure.u8(pool.id),
604
+ tx.pure.u8(usdcPool.id),
498
605
  tx.pure.u64(0),
499
606
  tx.pure.u64(0),
500
607
  tx.pure.bool(false)
501
608
  ],
502
- typeArguments: [pool.suiCoinType]
609
+ typeArguments: [usdcPool.suiCoinType]
503
610
  });
504
611
  const result = await client.devInspectTransactionBlock({
505
612
  transactionBlock: tx,
@@ -509,11 +616,13 @@ async function getHealthFactor(client, addressOrKeypair) {
509
616
  if (decoded !== void 0) {
510
617
  healthFactor = normalizeHealthFactor(Number(decoded));
511
618
  } else {
512
- healthFactor = borrowed > 0 ? supplied * liqThreshold / borrowed : Infinity;
619
+ healthFactor = supplied * liqThreshold / borrowed;
513
620
  }
514
621
  } catch {
515
- healthFactor = borrowed > 0 ? supplied * liqThreshold / borrowed : Infinity;
622
+ healthFactor = supplied * liqThreshold / borrowed;
516
623
  }
624
+ } else {
625
+ healthFactor = supplied * liqThreshold / borrowed;
517
626
  }
518
627
  return {
519
628
  healthFactor,
@@ -525,12 +634,20 @@ async function getHealthFactor(client, addressOrKeypair) {
525
634
  }
526
635
  async function getRates(client) {
527
636
  try {
528
- const pool = await getUsdcPool();
529
- let saveApy = rateToApy(pool.currentSupplyRate);
530
- let borrowApy = rateToApy(pool.currentBorrowRate);
531
- if (saveApy <= 0 || saveApy > 100) saveApy = 4;
532
- if (borrowApy <= 0 || borrowApy > 100) borrowApy = 6;
533
- return { USDC: { saveApy, borrowApy } };
637
+ const pools = await getPools();
638
+ const result = {};
639
+ for (const asset of STABLE_ASSETS) {
640
+ const targetType = SUPPORTED_ASSETS[asset].type;
641
+ const pool = pools.find((p) => matchesCoinType(p.suiCoinType || p.coinType || "", targetType));
642
+ if (!pool) continue;
643
+ let saveApy = rateToApy(pool.currentSupplyRate);
644
+ let borrowApy = rateToApy(pool.currentBorrowRate);
645
+ if (saveApy <= 0 || saveApy > 100) saveApy = 0;
646
+ if (borrowApy <= 0 || borrowApy > 100) borrowApy = 0;
647
+ result[asset] = { saveApy, borrowApy };
648
+ }
649
+ if (!result.USDC) result.USDC = { saveApy: 4, borrowApy: 6 };
650
+ return result;
534
651
  } catch {
535
652
  return { USDC: { saveApy: 4, borrowApy: 6 } };
536
653
  }
@@ -608,7 +725,7 @@ var NaviAdapter = class {
608
725
  name = "NAVI Protocol";
609
726
  version = "1.0.0";
610
727
  capabilities = ["save", "withdraw", "borrow", "repay"];
611
- supportedAssets = ["USDC"];
728
+ supportedAssets = [...STABLE_ASSETS];
612
729
  supportsSameAssetBorrow = true;
613
730
  client;
614
731
  async init(client) {
@@ -634,20 +751,24 @@ var NaviAdapter = class {
634
751
  async getHealth(address) {
635
752
  return getHealthFactor(this.client, address);
636
753
  }
637
- async buildSaveTx(address, amount, _asset, options) {
638
- const tx = await buildSaveTx(this.client, address, amount, options);
754
+ async buildSaveTx(address, amount, asset, options) {
755
+ const stableAsset = asset?.toUpperCase() === "USDC" ? "USDC" : asset;
756
+ const tx = await buildSaveTx(this.client, address, amount, { ...options, asset: stableAsset });
639
757
  return { tx };
640
758
  }
641
- async buildWithdrawTx(address, amount, _asset) {
642
- const result = await buildWithdrawTx(this.client, address, amount);
759
+ async buildWithdrawTx(address, amount, asset) {
760
+ const stableAsset = asset?.toUpperCase() === "USDC" ? "USDC" : asset;
761
+ const result = await buildWithdrawTx(this.client, address, amount, { asset: stableAsset });
643
762
  return { tx: result.tx, effectiveAmount: result.effectiveAmount };
644
763
  }
645
- async buildBorrowTx(address, amount, _asset, options) {
646
- const tx = await buildBorrowTx(this.client, address, amount, options);
764
+ async buildBorrowTx(address, amount, asset, options) {
765
+ const stableAsset = asset?.toUpperCase() === "USDC" ? "USDC" : asset;
766
+ const tx = await buildBorrowTx(this.client, address, amount, { ...options, asset: stableAsset });
647
767
  return { tx };
648
768
  }
649
- async buildRepayTx(address, amount, _asset) {
650
- const tx = await buildRepayTx(this.client, address, amount);
769
+ async buildRepayTx(address, amount, asset) {
770
+ const stableAsset = asset?.toUpperCase() === "USDC" ? "USDC" : asset;
771
+ const tx = await buildRepayTx(this.client, address, amount, { asset: stableAsset });
651
772
  return { tx };
652
773
  }
653
774
  async maxWithdraw(address, _asset) {
@@ -795,16 +916,21 @@ var CetusAdapter = class {
795
916
  };
796
917
  }
797
918
  getSupportedPairs() {
798
- return [
919
+ const pairs = [
799
920
  { from: "USDC", to: "SUI" },
800
921
  { from: "SUI", to: "USDC" }
801
922
  ];
923
+ for (const a of STABLE_ASSETS) {
924
+ for (const b of STABLE_ASSETS) {
925
+ if (a !== b) pairs.push({ from: a, to: b });
926
+ }
927
+ }
928
+ return pairs;
802
929
  }
803
930
  async getPoolPrice() {
804
931
  return getPoolPrice(this.client);
805
932
  }
806
933
  };
807
- var USDC_TYPE2 = SUPPORTED_ASSETS.USDC.type;
808
934
  var WAD = 1e18;
809
935
  var MIN_HEALTH_FACTOR2 = 1.5;
810
936
  var CLOCK2 = "0x6";
@@ -911,8 +1037,8 @@ var SuilendAdapter = class {
911
1037
  id = "suilend";
912
1038
  name = "Suilend";
913
1039
  version = "2.0.0";
914
- capabilities = ["save", "withdraw"];
915
- supportedAssets = ["USDC"];
1040
+ capabilities = ["save", "withdraw", "borrow", "repay"];
1041
+ supportedAssets = [...STABLE_ASSETS];
916
1042
  supportsSameAssetBorrow = false;
917
1043
  client;
918
1044
  publishedAt = null;
@@ -956,12 +1082,14 @@ var SuilendAdapter = class {
956
1082
  return this.reserveCache;
957
1083
  }
958
1084
  findReserve(reserves, asset) {
959
- const upper = asset.toUpperCase();
960
1085
  let coinType;
961
- if (upper === "USDC") coinType = USDC_TYPE2;
962
- else if (upper === "SUI") coinType = "0x2::sui::SUI";
963
- else if (asset.includes("::")) coinType = asset;
964
- else return void 0;
1086
+ if (asset in SUPPORTED_ASSETS) {
1087
+ coinType = SUPPORTED_ASSETS[asset].type;
1088
+ } else if (asset.includes("::")) {
1089
+ coinType = asset;
1090
+ } else {
1091
+ return void 0;
1092
+ }
965
1093
  try {
966
1094
  const normalized = normalizeStructTag(coinType);
967
1095
  return reserves.find((r) => {
@@ -1010,8 +1138,12 @@ var SuilendAdapter = class {
1010
1138
  resolveSymbol(coinType) {
1011
1139
  try {
1012
1140
  const normalized = normalizeStructTag(coinType);
1013
- if (normalized === normalizeStructTag(USDC_TYPE2)) return "USDC";
1014
- if (normalized === normalizeStructTag("0x2::sui::SUI")) return "SUI";
1141
+ for (const [key, info] of Object.entries(SUPPORTED_ASSETS)) {
1142
+ try {
1143
+ if (normalizeStructTag(info.type) === normalized) return key;
1144
+ } catch {
1145
+ }
1146
+ }
1015
1147
  } catch {
1016
1148
  }
1017
1149
  const parts = coinType.split("::");
@@ -1059,22 +1191,43 @@ var SuilendAdapter = class {
1059
1191
  if (caps.length === 0) {
1060
1192
  return { healthFactor: Infinity, supplied: 0, borrowed: 0, maxBorrow: 0, liquidationThreshold: 0 };
1061
1193
  }
1062
- const positions = await this.getPositions(address);
1063
- const supplied = positions.supplies.reduce((s, p) => s + p.amount, 0);
1064
- const borrowed = positions.borrows.reduce((s, p) => s + p.amount, 0);
1065
- const reserves = await this.loadReserves();
1066
- const reserve = this.findReserve(reserves, "USDC");
1067
- const closeLtv = reserve?.closeLtvPct ?? 75;
1068
- const openLtv = reserve?.openLtvPct ?? 70;
1069
- const liqThreshold = closeLtv / 100;
1194
+ const [reserves, obligation] = await Promise.all([
1195
+ this.loadReserves(),
1196
+ this.fetchObligation(caps[0].obligationId)
1197
+ ]);
1198
+ let supplied = 0;
1199
+ let borrowed = 0;
1200
+ let weightedCloseLtv = 0;
1201
+ let weightedOpenLtv = 0;
1202
+ for (const dep of obligation.deposits) {
1203
+ const reserve = reserves[dep.reserveIdx];
1204
+ if (!reserve) continue;
1205
+ const ratio = cTokenRatio(reserve);
1206
+ const amount = dep.ctokenAmount * ratio / 10 ** reserve.mintDecimals;
1207
+ supplied += amount;
1208
+ weightedCloseLtv += amount * (reserve.closeLtvPct / 100);
1209
+ weightedOpenLtv += amount * (reserve.openLtvPct / 100);
1210
+ }
1211
+ for (const bor of obligation.borrows) {
1212
+ const reserve = reserves[bor.reserveIdx];
1213
+ if (!reserve) continue;
1214
+ const rawAmount = bor.borrowedWad / WAD / 10 ** reserve.mintDecimals;
1215
+ const reserveRate = reserve.cumulativeBorrowRateWad / WAD;
1216
+ const posRate = bor.cumBorrowRateWad / WAD;
1217
+ borrowed += posRate > 0 ? rawAmount * (reserveRate / posRate) : rawAmount;
1218
+ }
1219
+ const liqThreshold = supplied > 0 ? weightedCloseLtv / supplied : 0.75;
1220
+ const openLtv = supplied > 0 ? weightedOpenLtv / supplied : 0.7;
1070
1221
  const healthFactor = borrowed > 0 ? supplied * liqThreshold / borrowed : Infinity;
1071
- const maxBorrow = Math.max(0, supplied * (openLtv / 100) - borrowed);
1222
+ const maxBorrow = Math.max(0, supplied * openLtv - borrowed);
1072
1223
  return { healthFactor, supplied, borrowed, maxBorrow, liquidationThreshold: liqThreshold };
1073
1224
  }
1074
- async buildSaveTx(address, amount, _asset, options) {
1225
+ async buildSaveTx(address, amount, asset, options) {
1226
+ const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
1227
+ const assetInfo = SUPPORTED_ASSETS[assetKey];
1075
1228
  const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
1076
- const usdcReserve = this.findReserve(reserves, "USDC");
1077
- if (!usdcReserve) throw new T2000Error("ASSET_NOT_SUPPORTED", "USDC reserve not found on Suilend");
1229
+ const reserve = this.findReserve(reserves, assetKey);
1230
+ if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend. Try: NAVI or a different asset.`);
1078
1231
  const caps = await this.fetchObligationCaps(address);
1079
1232
  const tx = new Transaction();
1080
1233
  tx.setSender(address);
@@ -1089,33 +1242,33 @@ var SuilendAdapter = class {
1089
1242
  } else {
1090
1243
  capRef = caps[0].id;
1091
1244
  }
1092
- const allCoins = await this.fetchAllCoins(address, USDC_TYPE2);
1093
- if (allCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins found");
1245
+ const allCoins = await this.fetchAllCoins(address, assetInfo.type);
1246
+ if (allCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins found`);
1094
1247
  const primaryCoinId = allCoins[0].coinObjectId;
1095
1248
  if (allCoins.length > 1) {
1096
1249
  tx.mergeCoins(tx.object(primaryCoinId), allCoins.slice(1).map((c) => tx.object(c.coinObjectId)));
1097
1250
  }
1098
- const rawAmount = usdcToRaw(amount).toString();
1251
+ const rawAmount = stableToRaw(amount, assetInfo.decimals).toString();
1099
1252
  const [depositCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawAmount]);
1100
1253
  if (options?.collectFee) {
1101
1254
  addCollectFeeToTx(tx, depositCoin, "save");
1102
1255
  }
1103
1256
  const [ctokens] = tx.moveCall({
1104
1257
  target: `${pkg}::lending_market::deposit_liquidity_and_mint_ctokens`,
1105
- typeArguments: [LENDING_MARKET_TYPE, USDC_TYPE2],
1258
+ typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1106
1259
  arguments: [
1107
1260
  tx.object(LENDING_MARKET_ID),
1108
- tx.pure.u64(usdcReserve.arrayIndex),
1261
+ tx.pure.u64(reserve.arrayIndex),
1109
1262
  tx.object(CLOCK2),
1110
1263
  depositCoin
1111
1264
  ]
1112
1265
  });
1113
1266
  tx.moveCall({
1114
1267
  target: `${pkg}::lending_market::deposit_ctokens_into_obligation`,
1115
- typeArguments: [LENDING_MARKET_TYPE, USDC_TYPE2],
1268
+ typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1116
1269
  arguments: [
1117
1270
  tx.object(LENDING_MARKET_ID),
1118
- tx.pure.u64(usdcReserve.arrayIndex),
1271
+ tx.pure.u64(reserve.arrayIndex),
1119
1272
  typeof capRef === "string" ? tx.object(capRef) : capRef,
1120
1273
  tx.object(CLOCK2),
1121
1274
  ctokens
@@ -1126,42 +1279,44 @@ var SuilendAdapter = class {
1126
1279
  }
1127
1280
  return { tx };
1128
1281
  }
1129
- async buildWithdrawTx(address, amount, _asset) {
1282
+ async buildWithdrawTx(address, amount, asset) {
1283
+ const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
1284
+ const assetInfo = SUPPORTED_ASSETS[assetKey];
1130
1285
  const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves(true)]);
1131
- const usdcReserve = this.findReserve(reserves, "USDC");
1132
- if (!usdcReserve) throw new T2000Error("ASSET_NOT_SUPPORTED", "USDC reserve not found on Suilend");
1286
+ const reserve = this.findReserve(reserves, assetKey);
1287
+ if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
1133
1288
  const caps = await this.fetchObligationCaps(address);
1134
1289
  if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found");
1135
1290
  const positions = await this.getPositions(address);
1136
- const deposited = positions.supplies.find((s) => s.asset === "USDC")?.amount ?? 0;
1291
+ const deposited = positions.supplies.find((s) => s.asset === assetKey)?.amount ?? 0;
1137
1292
  const effectiveAmount = Math.min(amount, deposited);
1138
- if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", "Nothing to withdraw from Suilend");
1139
- const ratio = cTokenRatio(usdcReserve);
1140
- const ctokenAmount = Math.ceil(effectiveAmount * 10 ** usdcReserve.mintDecimals / ratio);
1293
+ if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on Suilend`);
1294
+ const ratio = cTokenRatio(reserve);
1295
+ const ctokenAmount = Math.ceil(effectiveAmount * 10 ** reserve.mintDecimals / ratio);
1141
1296
  const tx = new Transaction();
1142
1297
  tx.setSender(address);
1143
1298
  const [ctokens] = tx.moveCall({
1144
1299
  target: `${pkg}::lending_market::withdraw_ctokens`,
1145
- typeArguments: [LENDING_MARKET_TYPE, USDC_TYPE2],
1300
+ typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1146
1301
  arguments: [
1147
1302
  tx.object(LENDING_MARKET_ID),
1148
- tx.pure.u64(usdcReserve.arrayIndex),
1303
+ tx.pure.u64(reserve.arrayIndex),
1149
1304
  tx.object(caps[0].id),
1150
1305
  tx.object(CLOCK2),
1151
1306
  tx.pure.u64(ctokenAmount)
1152
1307
  ]
1153
1308
  });
1154
- const exemptionType = `${SUILEND_PACKAGE}::lending_market::RateLimiterExemption<${LENDING_MARKET_TYPE}, ${USDC_TYPE2}>`;
1309
+ const exemptionType = `${SUILEND_PACKAGE}::lending_market::RateLimiterExemption<${LENDING_MARKET_TYPE}, ${assetInfo.type}>`;
1155
1310
  const [none] = tx.moveCall({
1156
1311
  target: "0x1::option::none",
1157
1312
  typeArguments: [exemptionType]
1158
1313
  });
1159
1314
  const [coin] = tx.moveCall({
1160
1315
  target: `${pkg}::lending_market::redeem_ctokens_and_withdraw_liquidity`,
1161
- typeArguments: [LENDING_MARKET_TYPE, USDC_TYPE2],
1316
+ typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1162
1317
  arguments: [
1163
1318
  tx.object(LENDING_MARKET_ID),
1164
- tx.pure.u64(usdcReserve.arrayIndex),
1319
+ tx.pure.u64(reserve.arrayIndex),
1165
1320
  tx.object(CLOCK2),
1166
1321
  ctokens,
1167
1322
  none
@@ -1170,11 +1325,64 @@ var SuilendAdapter = class {
1170
1325
  tx.transferObjects([coin], address);
1171
1326
  return { tx, effectiveAmount };
1172
1327
  }
1173
- async buildBorrowTx(_address, _amount, _asset, _options) {
1174
- throw new T2000Error("ASSET_NOT_SUPPORTED", "Suilend borrow requires different collateral/borrow assets. Deferred to Phase 10.");
1328
+ async buildBorrowTx(address, amount, asset, options) {
1329
+ const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
1330
+ const assetInfo = SUPPORTED_ASSETS[assetKey];
1331
+ const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
1332
+ const reserve = this.findReserve(reserves, assetKey);
1333
+ if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend. Try: NAVI or a different asset.`);
1334
+ const caps = await this.fetchObligationCaps(address);
1335
+ if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found. Deposit collateral first with: t2000 save <amount>");
1336
+ const rawAmount = stableToRaw(amount, assetInfo.decimals);
1337
+ const tx = new Transaction();
1338
+ tx.setSender(address);
1339
+ const [coin] = tx.moveCall({
1340
+ target: `${pkg}::lending_market::borrow`,
1341
+ typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1342
+ arguments: [
1343
+ tx.object(LENDING_MARKET_ID),
1344
+ tx.pure.u64(reserve.arrayIndex),
1345
+ tx.object(caps[0].id),
1346
+ tx.object(CLOCK2),
1347
+ tx.pure.u64(rawAmount)
1348
+ ]
1349
+ });
1350
+ if (options?.collectFee) {
1351
+ addCollectFeeToTx(tx, coin, "borrow");
1352
+ }
1353
+ tx.transferObjects([coin], address);
1354
+ return { tx };
1175
1355
  }
1176
- async buildRepayTx(_address, _amount, _asset) {
1177
- throw new T2000Error("ASSET_NOT_SUPPORTED", "Suilend repay deferred to Phase 10.");
1356
+ async buildRepayTx(address, amount, asset) {
1357
+ const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
1358
+ const assetInfo = SUPPORTED_ASSETS[assetKey];
1359
+ const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
1360
+ const reserve = this.findReserve(reserves, assetKey);
1361
+ if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
1362
+ const caps = await this.fetchObligationCaps(address);
1363
+ if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend obligation found");
1364
+ const allCoins = await this.fetchAllCoins(address, assetInfo.type);
1365
+ if (allCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins to repay with`);
1366
+ const rawAmount = stableToRaw(amount, assetInfo.decimals);
1367
+ const tx = new Transaction();
1368
+ tx.setSender(address);
1369
+ const primaryCoinId = allCoins[0].coinObjectId;
1370
+ if (allCoins.length > 1) {
1371
+ tx.mergeCoins(tx.object(primaryCoinId), allCoins.slice(1).map((c) => tx.object(c.coinObjectId)));
1372
+ }
1373
+ const [repayCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawAmount.toString()]);
1374
+ tx.moveCall({
1375
+ target: `${pkg}::lending_market::repay`,
1376
+ typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1377
+ arguments: [
1378
+ tx.object(LENDING_MARKET_ID),
1379
+ tx.pure.u64(reserve.arrayIndex),
1380
+ tx.object(caps[0].id),
1381
+ tx.object(CLOCK2),
1382
+ repayCoin
1383
+ ]
1384
+ });
1385
+ return { tx };
1178
1386
  }
1179
1387
  async maxWithdraw(address, _asset) {
1180
1388
  const health = await this.getHealth(address);
@@ -1188,8 +1396,10 @@ var SuilendAdapter = class {
1188
1396
  const hfAfter = health.borrowed > 0 ? remainingSupply * health.liquidationThreshold / health.borrowed : Infinity;
1189
1397
  return { maxAmount, healthFactorAfter: hfAfter, currentHF: health.healthFactor };
1190
1398
  }
1191
- async maxBorrow(_address, _asset) {
1192
- throw new T2000Error("ASSET_NOT_SUPPORTED", "Suilend maxBorrow deferred to Phase 10.");
1399
+ async maxBorrow(address, _asset) {
1400
+ const health = await this.getHealth(address);
1401
+ const maxAmount = health.maxBorrow;
1402
+ return { maxAmount, healthFactorAfter: MIN_HEALTH_FACTOR2, currentHF: health.healthFactor };
1193
1403
  }
1194
1404
  async fetchAllCoins(owner, coinType) {
1195
1405
  const all = [];