@t2000/sdk 0.7.2 → 0.8.0

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