@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.
package/dist/index.cjs CHANGED
@@ -31,14 +31,35 @@ var SUPPORTED_ASSETS = {
31
31
  USDC: {
32
32
  type: "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC",
33
33
  decimals: 6,
34
- symbol: "USDC"
34
+ symbol: "USDC",
35
+ displayName: "USDC"
36
+ },
37
+ USDT: {
38
+ type: "0x375f70cf2ae4c00bf37117d0c85a2c71545e6ee05c4a5c7d282cd66a4504b068::usdt::USDT",
39
+ decimals: 6,
40
+ symbol: "USDT",
41
+ displayName: "suiUSDT"
42
+ },
43
+ USDe: {
44
+ type: "0x41d587e5336f1c86cad50d38a7136db99333bb9bda91cea4ba69115defeb1402::sui_usde::SUI_USDE",
45
+ decimals: 6,
46
+ symbol: "USDe",
47
+ displayName: "suiUSDe"
48
+ },
49
+ USDsui: {
50
+ type: "0x44f838219cf67b058f3b37907b655f226153c18e33dfcd0da559a844fea9b1c1::usdsui::USDSUI",
51
+ decimals: 6,
52
+ symbol: "USDsui",
53
+ displayName: "USDsui"
35
54
  },
36
55
  SUI: {
37
56
  type: "0x2::sui::SUI",
38
57
  decimals: 9,
39
- symbol: "SUI"
58
+ symbol: "SUI",
59
+ displayName: "SUI"
40
60
  }
41
61
  };
62
+ var STABLE_ASSETS = ["USDC", "USDT", "USDe", "USDsui"];
42
63
  var T2000_PACKAGE_ID = process.env.T2000_PACKAGE_ID ?? "0xab92e9f1fe549ad3d6a52924a73181b45791e76120b975138fac9ec9b75db9f3";
43
64
  var T2000_CONFIG_ID = process.env.T2000_CONFIG_ID ?? "0x408add9aa9322f93cfd87523d8f603006eb8713894f4c460283c58a6888dae8a";
44
65
  var T2000_TREASURY_ID = process.env.T2000_TREASURY_ID ?? "0x3bb501b8300125dca59019247941a42af6b292a150ce3cfcce9449456be2ec91";
@@ -238,6 +259,15 @@ function usdcToRaw(amount) {
238
259
  function rawToUsdc(raw) {
239
260
  return Number(raw) / 10 ** USDC_DECIMALS;
240
261
  }
262
+ function stableToRaw(amount, decimals) {
263
+ return BigInt(Math.round(amount * 10 ** decimals));
264
+ }
265
+ function rawToStable(raw, decimals) {
266
+ return Number(raw) / 10 ** decimals;
267
+ }
268
+ function getDecimals(asset) {
269
+ return SUPPORTED_ASSETS[asset].decimals;
270
+ }
241
271
  function displayToRaw(amount, decimals) {
242
272
  return BigInt(Math.round(amount * 10 ** decimals));
243
273
  }
@@ -248,6 +278,12 @@ function formatSui(amount) {
248
278
  if (amount < 1e-3) return `${amount.toFixed(6)} SUI`;
249
279
  return `${amount.toFixed(3)} SUI`;
250
280
  }
281
+ var ASSET_LOOKUP = new Map(
282
+ Object.keys(SUPPORTED_ASSETS).map((k) => [k.toUpperCase(), k])
283
+ );
284
+ function normalizeAsset(input) {
285
+ return ASSET_LOOKUP.get(input.toUpperCase()) ?? input;
286
+ }
251
287
 
252
288
  // src/wallet/send.ts
253
289
  async function buildSendTx({
@@ -320,26 +356,35 @@ async function fetchSuiPrice(client) {
320
356
  return _cachedSuiPrice;
321
357
  }
322
358
  async function queryBalance(client, address) {
323
- const [usdcBalance, suiBalance, suiPriceUsd] = await Promise.all([
324
- client.getBalance({ owner: address, coinType: SUPPORTED_ASSETS.USDC.type }),
359
+ const stableBalancePromises = STABLE_ASSETS.map(
360
+ (asset) => client.getBalance({ owner: address, coinType: SUPPORTED_ASSETS[asset].type }).then((b) => ({ asset, amount: Number(b.totalBalance) / 10 ** SUPPORTED_ASSETS[asset].decimals }))
361
+ );
362
+ const [suiBalance, suiPriceUsd, ...stableResults] = await Promise.all([
325
363
  client.getBalance({ owner: address, coinType: SUPPORTED_ASSETS.SUI.type }),
326
- fetchSuiPrice(client)
364
+ fetchSuiPrice(client),
365
+ ...stableBalancePromises
327
366
  ]);
328
- const usdcAmount = Number(usdcBalance.totalBalance) / 10 ** SUPPORTED_ASSETS.USDC.decimals;
367
+ const stables = {};
368
+ let totalStables = 0;
369
+ for (const { asset, amount } of stableResults) {
370
+ stables[asset] = amount;
371
+ totalStables += amount;
372
+ }
329
373
  const suiAmount = Number(suiBalance.totalBalance) / Number(MIST_PER_SUI);
330
374
  const savings = 0;
331
375
  const usdEquiv = suiAmount * suiPriceUsd;
332
- const total = usdcAmount + savings + usdEquiv;
376
+ const total = totalStables + savings + usdEquiv;
333
377
  return {
334
- available: usdcAmount,
378
+ available: totalStables,
335
379
  savings,
336
380
  gasReserve: {
337
381
  sui: suiAmount,
338
382
  usdEquiv
339
383
  },
340
384
  total,
385
+ stables,
341
386
  assets: {
342
- USDC: usdcAmount,
387
+ USDC: stables.USDC ?? 0,
343
388
  SUI: suiAmount
344
389
  }
345
390
  };
@@ -427,7 +472,7 @@ async function reportFee(agentAddress, operation, feeAmount, feeRate, txDigest)
427
472
  } catch {
428
473
  }
429
474
  }
430
- var USDC_TYPE = SUPPORTED_ASSETS.USDC.type;
475
+ SUPPORTED_ASSETS.USDC.type;
431
476
  var RATE_DECIMALS = 27;
432
477
  var LTV_DECIMALS = 27;
433
478
  var MIN_HEALTH_FACTOR = 1.5;
@@ -488,13 +533,24 @@ async function getPools(fresh = false) {
488
533
  poolsCache = { data, ts: Date.now() };
489
534
  return data;
490
535
  }
491
- async function getUsdcPool() {
536
+ function matchesCoinType(poolType, targetType) {
537
+ const poolSuffix = poolType.split("::").slice(1).join("::").toLowerCase();
538
+ const targetSuffix = targetType.split("::").slice(1).join("::").toLowerCase();
539
+ return poolSuffix === targetSuffix;
540
+ }
541
+ async function getPool(asset = "USDC") {
492
542
  const pools = await getPools();
493
- const usdc = pools.find(
494
- (p) => p.token?.symbol === "USDC" || p.coinType?.toLowerCase().includes("usdc")
543
+ const targetType = SUPPORTED_ASSETS[asset].type;
544
+ const pool = pools.find(
545
+ (p) => matchesCoinType(p.suiCoinType || p.coinType || "", targetType)
495
546
  );
496
- if (!usdc) throw new T2000Error("PROTOCOL_UNAVAILABLE", "USDC pool not found on NAVI");
497
- return usdc;
547
+ if (!pool) {
548
+ throw new T2000Error(
549
+ "ASSET_NOT_SUPPORTED",
550
+ `${SUPPORTED_ASSETS[asset].displayName} pool not found on NAVI. Try: ${STABLE_ASSETS.filter((a) => a !== asset).join(", ")}`
551
+ );
552
+ }
553
+ return pool;
498
554
  }
499
555
  function addOracleUpdate(tx, config, pool) {
500
556
  const feed = config.oracle.feeds?.find((f2) => f2.assetId === pool.id);
@@ -582,10 +638,12 @@ async function buildSaveTx(client, address, amount, options = {}) {
582
638
  if (!amount || amount <= 0 || !Number.isFinite(amount)) {
583
639
  throw new T2000Error("INVALID_AMOUNT", "Save amount must be a positive number");
584
640
  }
585
- const rawAmount = Number(usdcToRaw(amount));
586
- const [config, pool] = await Promise.all([getConfig(), getUsdcPool()]);
587
- const coins = await fetchCoins(client, address, USDC_TYPE);
588
- if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins found");
641
+ const asset = options.asset ?? "USDC";
642
+ const assetInfo = SUPPORTED_ASSETS[asset];
643
+ const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
644
+ const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
645
+ const coins = await fetchCoins(client, address, assetInfo.type);
646
+ if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins found`);
589
647
  const tx = new transactions.Transaction();
590
648
  tx.setSender(address);
591
649
  const coinObj = mergeCoins(tx, coins);
@@ -608,18 +666,20 @@ async function buildSaveTx(client, address, amount, options = {}) {
608
666
  });
609
667
  return tx;
610
668
  }
611
- async function buildWithdrawTx(client, address, amount) {
669
+ async function buildWithdrawTx(client, address, amount, options = {}) {
670
+ const asset = options.asset ?? "USDC";
671
+ const assetInfo = SUPPORTED_ASSETS[asset];
612
672
  const [config, pool, pools, states] = await Promise.all([
613
673
  getConfig(),
614
- getUsdcPool(),
674
+ getPool(asset),
615
675
  getPools(),
616
676
  getUserState(client, address)
617
677
  ]);
618
- const usdcState = states.find((s) => s.assetId === pool.id);
619
- const deposited = usdcState ? compoundBalance(usdcState.supplyBalance, pool.currentSupplyIndex) : 0;
678
+ const assetState = states.find((s) => s.assetId === pool.id);
679
+ const deposited = assetState ? compoundBalance(assetState.supplyBalance, pool.currentSupplyIndex) : 0;
620
680
  const effectiveAmount = Math.min(amount, Math.max(0, deposited - WITHDRAW_DUST_BUFFER));
621
- if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", "Nothing to withdraw");
622
- const rawAmount = Number(usdcToRaw(effectiveAmount));
681
+ if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on NAVI`);
682
+ const rawAmount = Number(stableToRaw(effectiveAmount, assetInfo.decimals));
623
683
  const tx = new transactions.Transaction();
624
684
  tx.setSender(address);
625
685
  addOracleUpdate(tx, config, pool);
@@ -650,8 +710,10 @@ async function buildBorrowTx(client, address, amount, options = {}) {
650
710
  if (!amount || amount <= 0 || !Number.isFinite(amount)) {
651
711
  throw new T2000Error("INVALID_AMOUNT", "Borrow amount must be a positive number");
652
712
  }
653
- const rawAmount = Number(usdcToRaw(amount));
654
- const [config, pool] = await Promise.all([getConfig(), getUsdcPool()]);
713
+ const asset = options.asset ?? "USDC";
714
+ const assetInfo = SUPPORTED_ASSETS[asset];
715
+ const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
716
+ const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
655
717
  const tx = new transactions.Transaction();
656
718
  tx.setSender(address);
657
719
  addOracleUpdate(tx, config, pool);
@@ -681,14 +743,16 @@ async function buildBorrowTx(client, address, amount, options = {}) {
681
743
  tx.transferObjects([borrowedCoin], address);
682
744
  return tx;
683
745
  }
684
- async function buildRepayTx(client, address, amount) {
746
+ async function buildRepayTx(client, address, amount, options = {}) {
685
747
  if (!amount || amount <= 0 || !Number.isFinite(amount)) {
686
748
  throw new T2000Error("INVALID_AMOUNT", "Repay amount must be a positive number");
687
749
  }
688
- const rawAmount = Number(usdcToRaw(amount));
689
- const [config, pool] = await Promise.all([getConfig(), getUsdcPool()]);
690
- const coins = await fetchCoins(client, address, USDC_TYPE);
691
- if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins to repay with");
750
+ const asset = options.asset ?? "USDC";
751
+ const assetInfo = SUPPORTED_ASSETS[asset];
752
+ const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
753
+ const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
754
+ const coins = await fetchCoins(client, address, assetInfo.type);
755
+ if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins to repay with`);
692
756
  const tx = new transactions.Transaction();
693
757
  tx.setSender(address);
694
758
  addOracleUpdate(tx, config, pool);
@@ -712,21 +776,36 @@ async function buildRepayTx(client, address, amount) {
712
776
  }
713
777
  async function getHealthFactor(client, addressOrKeypair) {
714
778
  const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
715
- const [config, pool, states] = await Promise.all([
779
+ const [config, pools, states] = await Promise.all([
716
780
  getConfig(),
717
- getUsdcPool(),
781
+ getPools(),
718
782
  getUserState(client, address)
719
783
  ]);
720
- const usdcState = states.find((s) => s.assetId === pool.id);
721
- const supplied = usdcState ? compoundBalance(usdcState.supplyBalance, pool.currentSupplyIndex) : 0;
722
- const borrowed = usdcState ? compoundBalance(usdcState.borrowBalance, pool.currentBorrowIndex) : 0;
723
- const ltv = parseLtv(pool.ltv);
724
- const liqThreshold = parseLiqThreshold(pool.liquidationFactor.threshold);
784
+ let supplied = 0;
785
+ let borrowed = 0;
786
+ let weightedLtv = 0;
787
+ let weightedLiqThreshold = 0;
788
+ for (const state of states) {
789
+ const pool = pools.find((p) => p.id === state.assetId);
790
+ if (!pool) continue;
791
+ const supplyBal = compoundBalance(state.supplyBalance, pool.currentSupplyIndex);
792
+ const borrowBal = compoundBalance(state.borrowBalance, pool.currentBorrowIndex);
793
+ const price = pool.token?.price ?? 1;
794
+ supplied += supplyBal * price;
795
+ borrowed += borrowBal * price;
796
+ if (supplyBal > 0) {
797
+ weightedLtv += supplyBal * price * parseLtv(pool.ltv);
798
+ weightedLiqThreshold += supplyBal * price * parseLiqThreshold(pool.liquidationFactor.threshold);
799
+ }
800
+ }
801
+ const ltv = supplied > 0 ? weightedLtv / supplied : 0.75;
802
+ const liqThreshold = supplied > 0 ? weightedLiqThreshold / supplied : 0.75;
725
803
  const maxBorrowVal = Math.max(0, supplied * ltv - borrowed);
804
+ const usdcPool = pools.find((p) => matchesCoinType(p.suiCoinType || p.coinType || "", SUPPORTED_ASSETS.USDC.type));
726
805
  let healthFactor;
727
806
  if (borrowed <= 0) {
728
807
  healthFactor = Infinity;
729
- } else {
808
+ } else if (usdcPool) {
730
809
  try {
731
810
  const tx = new transactions.Transaction();
732
811
  tx.moveCall({
@@ -735,14 +814,14 @@ async function getHealthFactor(client, addressOrKeypair) {
735
814
  tx.object(CLOCK),
736
815
  tx.object(config.storage),
737
816
  tx.object(config.oracle.priceOracle),
738
- tx.pure.u8(pool.id),
817
+ tx.pure.u8(usdcPool.id),
739
818
  tx.pure.address(address),
740
- tx.pure.u8(pool.id),
819
+ tx.pure.u8(usdcPool.id),
741
820
  tx.pure.u64(0),
742
821
  tx.pure.u64(0),
743
822
  tx.pure.bool(false)
744
823
  ],
745
- typeArguments: [pool.suiCoinType]
824
+ typeArguments: [usdcPool.suiCoinType]
746
825
  });
747
826
  const result = await client.devInspectTransactionBlock({
748
827
  transactionBlock: tx,
@@ -752,11 +831,13 @@ async function getHealthFactor(client, addressOrKeypair) {
752
831
  if (decoded !== void 0) {
753
832
  healthFactor = normalizeHealthFactor(Number(decoded));
754
833
  } else {
755
- healthFactor = borrowed > 0 ? supplied * liqThreshold / borrowed : Infinity;
834
+ healthFactor = supplied * liqThreshold / borrowed;
756
835
  }
757
836
  } catch {
758
- healthFactor = borrowed > 0 ? supplied * liqThreshold / borrowed : Infinity;
837
+ healthFactor = supplied * liqThreshold / borrowed;
759
838
  }
839
+ } else {
840
+ healthFactor = supplied * liqThreshold / borrowed;
760
841
  }
761
842
  return {
762
843
  healthFactor,
@@ -768,12 +849,20 @@ async function getHealthFactor(client, addressOrKeypair) {
768
849
  }
769
850
  async function getRates(client) {
770
851
  try {
771
- const pool = await getUsdcPool();
772
- let saveApy = rateToApy(pool.currentSupplyRate);
773
- let borrowApy = rateToApy(pool.currentBorrowRate);
774
- if (saveApy <= 0 || saveApy > 100) saveApy = 4;
775
- if (borrowApy <= 0 || borrowApy > 100) borrowApy = 6;
776
- return { USDC: { saveApy, borrowApy } };
852
+ const pools = await getPools();
853
+ const result = {};
854
+ for (const asset of STABLE_ASSETS) {
855
+ const targetType = SUPPORTED_ASSETS[asset].type;
856
+ const pool = pools.find((p) => matchesCoinType(p.suiCoinType || p.coinType || "", targetType));
857
+ if (!pool) continue;
858
+ let saveApy = rateToApy(pool.currentSupplyRate);
859
+ let borrowApy = rateToApy(pool.currentBorrowRate);
860
+ if (saveApy <= 0 || saveApy > 100) saveApy = 0;
861
+ if (borrowApy <= 0 || borrowApy > 100) borrowApy = 0;
862
+ result[asset] = { saveApy, borrowApy };
863
+ }
864
+ if (!result.USDC) result.USDC = { saveApy: 4, borrowApy: 6 };
865
+ return result;
777
866
  } catch {
778
867
  return { USDC: { saveApy: 4, borrowApy: 6 } };
779
868
  }
@@ -1111,6 +1200,41 @@ var ProtocolRegistry = class {
1111
1200
  candidates.sort((a, b) => b.quote.expectedOutput - a.quote.expectedOutput);
1112
1201
  return candidates[0];
1113
1202
  }
1203
+ async bestSaveRateAcrossAssets() {
1204
+ const candidates = [];
1205
+ for (const asset of STABLE_ASSETS) {
1206
+ for (const adapter of this.lending.values()) {
1207
+ if (!adapter.supportedAssets.includes(asset)) continue;
1208
+ if (!adapter.capabilities.includes("save")) continue;
1209
+ try {
1210
+ const rate = await adapter.getRates(asset);
1211
+ candidates.push({ adapter, rate, asset });
1212
+ } catch {
1213
+ }
1214
+ }
1215
+ }
1216
+ if (candidates.length === 0) {
1217
+ throw new T2000Error("ASSET_NOT_SUPPORTED", "No lending adapter found for any stablecoin");
1218
+ }
1219
+ candidates.sort((a, b) => b.rate.saveApy - a.rate.saveApy);
1220
+ return candidates[0];
1221
+ }
1222
+ async allRatesAcrossAssets() {
1223
+ const results = [];
1224
+ for (const asset of STABLE_ASSETS) {
1225
+ for (const adapter of this.lending.values()) {
1226
+ if (!adapter.supportedAssets.includes(asset)) continue;
1227
+ try {
1228
+ const rates = await adapter.getRates(asset);
1229
+ if (rates.saveApy > 0 || rates.borrowApy > 0) {
1230
+ results.push({ protocol: adapter.name, protocolId: adapter.id, asset, rates });
1231
+ }
1232
+ } catch {
1233
+ }
1234
+ }
1235
+ }
1236
+ return results;
1237
+ }
1114
1238
  async allRates(asset) {
1115
1239
  const results = [];
1116
1240
  for (const adapter of this.lending.values()) {
@@ -1148,6 +1272,24 @@ var ProtocolRegistry = class {
1148
1272
  listSwap() {
1149
1273
  return [...this.swap.values()];
1150
1274
  }
1275
+ isSupportedAsset(asset, capability) {
1276
+ for (const adapter of this.lending.values()) {
1277
+ if (!adapter.supportedAssets.includes(asset)) continue;
1278
+ if (capability && !adapter.capabilities.includes(capability)) continue;
1279
+ return true;
1280
+ }
1281
+ return false;
1282
+ }
1283
+ getSupportedAssets(capability) {
1284
+ const assets = /* @__PURE__ */ new Set();
1285
+ for (const adapter of this.lending.values()) {
1286
+ if (capability && !adapter.capabilities.includes(capability)) continue;
1287
+ for (const a of adapter.supportedAssets) {
1288
+ assets.add(a);
1289
+ }
1290
+ }
1291
+ return [...assets];
1292
+ }
1151
1293
  };
1152
1294
 
1153
1295
  // src/adapters/navi.ts
@@ -1172,7 +1314,7 @@ var NaviAdapter = class {
1172
1314
  name = "NAVI Protocol";
1173
1315
  version = "1.0.0";
1174
1316
  capabilities = ["save", "withdraw", "borrow", "repay"];
1175
- supportedAssets = ["USDC"];
1317
+ supportedAssets = [...STABLE_ASSETS];
1176
1318
  supportsSameAssetBorrow = true;
1177
1319
  client;
1178
1320
  async init(client) {
@@ -1198,20 +1340,24 @@ var NaviAdapter = class {
1198
1340
  async getHealth(address) {
1199
1341
  return getHealthFactor(this.client, address);
1200
1342
  }
1201
- async buildSaveTx(address, amount, _asset, options) {
1202
- const tx = await buildSaveTx(this.client, address, amount, options);
1343
+ async buildSaveTx(address, amount, asset, options) {
1344
+ const stableAsset = asset?.toUpperCase() === "USDC" ? "USDC" : asset;
1345
+ const tx = await buildSaveTx(this.client, address, amount, { ...options, asset: stableAsset });
1203
1346
  return { tx };
1204
1347
  }
1205
- async buildWithdrawTx(address, amount, _asset) {
1206
- const result = await buildWithdrawTx(this.client, address, amount);
1348
+ async buildWithdrawTx(address, amount, asset) {
1349
+ const stableAsset = asset?.toUpperCase() === "USDC" ? "USDC" : asset;
1350
+ const result = await buildWithdrawTx(this.client, address, amount, { asset: stableAsset });
1207
1351
  return { tx: result.tx, effectiveAmount: result.effectiveAmount };
1208
1352
  }
1209
- async buildBorrowTx(address, amount, _asset, options) {
1210
- const tx = await buildBorrowTx(this.client, address, amount, options);
1353
+ async buildBorrowTx(address, amount, asset, options) {
1354
+ const stableAsset = asset?.toUpperCase() === "USDC" ? "USDC" : asset;
1355
+ const tx = await buildBorrowTx(this.client, address, amount, { ...options, asset: stableAsset });
1211
1356
  return { tx };
1212
1357
  }
1213
- async buildRepayTx(address, amount, _asset) {
1214
- const tx = await buildRepayTx(this.client, address, amount);
1358
+ async buildRepayTx(address, amount, asset) {
1359
+ const stableAsset = asset?.toUpperCase() === "USDC" ? "USDC" : asset;
1360
+ const tx = await buildRepayTx(this.client, address, amount, { asset: stableAsset });
1215
1361
  return { tx };
1216
1362
  }
1217
1363
  async maxWithdraw(address, _asset) {
@@ -1359,16 +1505,22 @@ var CetusAdapter = class {
1359
1505
  };
1360
1506
  }
1361
1507
  getSupportedPairs() {
1362
- return [
1508
+ const pairs = [
1363
1509
  { from: "USDC", to: "SUI" },
1364
1510
  { from: "SUI", to: "USDC" }
1365
1511
  ];
1512
+ for (const a of STABLE_ASSETS) {
1513
+ for (const b of STABLE_ASSETS) {
1514
+ if (a !== b) pairs.push({ from: a, to: b });
1515
+ }
1516
+ }
1517
+ return pairs;
1366
1518
  }
1367
1519
  async getPoolPrice() {
1368
1520
  return getPoolPrice(this.client);
1369
1521
  }
1370
1522
  };
1371
- var USDC_TYPE2 = SUPPORTED_ASSETS.USDC.type;
1523
+ SUPPORTED_ASSETS.USDC.type;
1372
1524
  var WAD = 1e18;
1373
1525
  var MIN_HEALTH_FACTOR2 = 1.5;
1374
1526
  var CLOCK2 = "0x6";
@@ -1475,8 +1627,8 @@ var SuilendAdapter = class {
1475
1627
  id = "suilend";
1476
1628
  name = "Suilend";
1477
1629
  version = "2.0.0";
1478
- capabilities = ["save", "withdraw"];
1479
- supportedAssets = ["USDC"];
1630
+ capabilities = ["save", "withdraw", "borrow", "repay"];
1631
+ supportedAssets = [...STABLE_ASSETS];
1480
1632
  supportsSameAssetBorrow = false;
1481
1633
  client;
1482
1634
  publishedAt = null;
@@ -1520,12 +1672,14 @@ var SuilendAdapter = class {
1520
1672
  return this.reserveCache;
1521
1673
  }
1522
1674
  findReserve(reserves, asset) {
1523
- const upper = asset.toUpperCase();
1524
1675
  let coinType;
1525
- if (upper === "USDC") coinType = USDC_TYPE2;
1526
- else if (upper === "SUI") coinType = "0x2::sui::SUI";
1527
- else if (asset.includes("::")) coinType = asset;
1528
- else return void 0;
1676
+ if (asset in SUPPORTED_ASSETS) {
1677
+ coinType = SUPPORTED_ASSETS[asset].type;
1678
+ } else if (asset.includes("::")) {
1679
+ coinType = asset;
1680
+ } else {
1681
+ return void 0;
1682
+ }
1529
1683
  try {
1530
1684
  const normalized = utils.normalizeStructTag(coinType);
1531
1685
  return reserves.find((r) => {
@@ -1574,8 +1728,12 @@ var SuilendAdapter = class {
1574
1728
  resolveSymbol(coinType) {
1575
1729
  try {
1576
1730
  const normalized = utils.normalizeStructTag(coinType);
1577
- if (normalized === utils.normalizeStructTag(USDC_TYPE2)) return "USDC";
1578
- if (normalized === utils.normalizeStructTag("0x2::sui::SUI")) return "SUI";
1731
+ for (const [key, info] of Object.entries(SUPPORTED_ASSETS)) {
1732
+ try {
1733
+ if (utils.normalizeStructTag(info.type) === normalized) return key;
1734
+ } catch {
1735
+ }
1736
+ }
1579
1737
  } catch {
1580
1738
  }
1581
1739
  const parts = coinType.split("::");
@@ -1623,22 +1781,43 @@ var SuilendAdapter = class {
1623
1781
  if (caps.length === 0) {
1624
1782
  return { healthFactor: Infinity, supplied: 0, borrowed: 0, maxBorrow: 0, liquidationThreshold: 0 };
1625
1783
  }
1626
- const positions = await this.getPositions(address);
1627
- const supplied = positions.supplies.reduce((s, p) => s + p.amount, 0);
1628
- const borrowed = positions.borrows.reduce((s, p) => s + p.amount, 0);
1629
- const reserves = await this.loadReserves();
1630
- const reserve = this.findReserve(reserves, "USDC");
1631
- const closeLtv = reserve?.closeLtvPct ?? 75;
1632
- const openLtv = reserve?.openLtvPct ?? 70;
1633
- const liqThreshold = closeLtv / 100;
1784
+ const [reserves, obligation] = await Promise.all([
1785
+ this.loadReserves(),
1786
+ this.fetchObligation(caps[0].obligationId)
1787
+ ]);
1788
+ let supplied = 0;
1789
+ let borrowed = 0;
1790
+ let weightedCloseLtv = 0;
1791
+ let weightedOpenLtv = 0;
1792
+ for (const dep of obligation.deposits) {
1793
+ const reserve = reserves[dep.reserveIdx];
1794
+ if (!reserve) continue;
1795
+ const ratio = cTokenRatio(reserve);
1796
+ const amount = dep.ctokenAmount * ratio / 10 ** reserve.mintDecimals;
1797
+ supplied += amount;
1798
+ weightedCloseLtv += amount * (reserve.closeLtvPct / 100);
1799
+ weightedOpenLtv += amount * (reserve.openLtvPct / 100);
1800
+ }
1801
+ for (const bor of obligation.borrows) {
1802
+ const reserve = reserves[bor.reserveIdx];
1803
+ if (!reserve) continue;
1804
+ const rawAmount = bor.borrowedWad / WAD / 10 ** reserve.mintDecimals;
1805
+ const reserveRate = reserve.cumulativeBorrowRateWad / WAD;
1806
+ const posRate = bor.cumBorrowRateWad / WAD;
1807
+ borrowed += posRate > 0 ? rawAmount * (reserveRate / posRate) : rawAmount;
1808
+ }
1809
+ const liqThreshold = supplied > 0 ? weightedCloseLtv / supplied : 0.75;
1810
+ const openLtv = supplied > 0 ? weightedOpenLtv / supplied : 0.7;
1634
1811
  const healthFactor = borrowed > 0 ? supplied * liqThreshold / borrowed : Infinity;
1635
- const maxBorrow = Math.max(0, supplied * (openLtv / 100) - borrowed);
1812
+ const maxBorrow = Math.max(0, supplied * openLtv - borrowed);
1636
1813
  return { healthFactor, supplied, borrowed, maxBorrow, liquidationThreshold: liqThreshold };
1637
1814
  }
1638
- async buildSaveTx(address, amount, _asset, options) {
1815
+ async buildSaveTx(address, amount, asset, options) {
1816
+ const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
1817
+ const assetInfo = SUPPORTED_ASSETS[assetKey];
1639
1818
  const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
1640
- const usdcReserve = this.findReserve(reserves, "USDC");
1641
- if (!usdcReserve) throw new T2000Error("ASSET_NOT_SUPPORTED", "USDC reserve not found on Suilend");
1819
+ const reserve = this.findReserve(reserves, assetKey);
1820
+ if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend. Try: NAVI or a different asset.`);
1642
1821
  const caps = await this.fetchObligationCaps(address);
1643
1822
  const tx = new transactions.Transaction();
1644
1823
  tx.setSender(address);
@@ -1653,33 +1832,33 @@ var SuilendAdapter = class {
1653
1832
  } else {
1654
1833
  capRef = caps[0].id;
1655
1834
  }
1656
- const allCoins = await this.fetchAllCoins(address, USDC_TYPE2);
1657
- if (allCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins found");
1835
+ const allCoins = await this.fetchAllCoins(address, assetInfo.type);
1836
+ if (allCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins found`);
1658
1837
  const primaryCoinId = allCoins[0].coinObjectId;
1659
1838
  if (allCoins.length > 1) {
1660
1839
  tx.mergeCoins(tx.object(primaryCoinId), allCoins.slice(1).map((c) => tx.object(c.coinObjectId)));
1661
1840
  }
1662
- const rawAmount = usdcToRaw(amount).toString();
1841
+ const rawAmount = stableToRaw(amount, assetInfo.decimals).toString();
1663
1842
  const [depositCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawAmount]);
1664
1843
  if (options?.collectFee) {
1665
1844
  addCollectFeeToTx(tx, depositCoin, "save");
1666
1845
  }
1667
1846
  const [ctokens] = tx.moveCall({
1668
1847
  target: `${pkg}::lending_market::deposit_liquidity_and_mint_ctokens`,
1669
- typeArguments: [LENDING_MARKET_TYPE, USDC_TYPE2],
1848
+ typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1670
1849
  arguments: [
1671
1850
  tx.object(LENDING_MARKET_ID),
1672
- tx.pure.u64(usdcReserve.arrayIndex),
1851
+ tx.pure.u64(reserve.arrayIndex),
1673
1852
  tx.object(CLOCK2),
1674
1853
  depositCoin
1675
1854
  ]
1676
1855
  });
1677
1856
  tx.moveCall({
1678
1857
  target: `${pkg}::lending_market::deposit_ctokens_into_obligation`,
1679
- typeArguments: [LENDING_MARKET_TYPE, USDC_TYPE2],
1858
+ typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1680
1859
  arguments: [
1681
1860
  tx.object(LENDING_MARKET_ID),
1682
- tx.pure.u64(usdcReserve.arrayIndex),
1861
+ tx.pure.u64(reserve.arrayIndex),
1683
1862
  typeof capRef === "string" ? tx.object(capRef) : capRef,
1684
1863
  tx.object(CLOCK2),
1685
1864
  ctokens
@@ -1690,42 +1869,44 @@ var SuilendAdapter = class {
1690
1869
  }
1691
1870
  return { tx };
1692
1871
  }
1693
- async buildWithdrawTx(address, amount, _asset) {
1872
+ async buildWithdrawTx(address, amount, asset) {
1873
+ const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
1874
+ const assetInfo = SUPPORTED_ASSETS[assetKey];
1694
1875
  const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves(true)]);
1695
- const usdcReserve = this.findReserve(reserves, "USDC");
1696
- if (!usdcReserve) throw new T2000Error("ASSET_NOT_SUPPORTED", "USDC reserve not found on Suilend");
1876
+ const reserve = this.findReserve(reserves, assetKey);
1877
+ if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
1697
1878
  const caps = await this.fetchObligationCaps(address);
1698
1879
  if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found");
1699
1880
  const positions = await this.getPositions(address);
1700
- const deposited = positions.supplies.find((s) => s.asset === "USDC")?.amount ?? 0;
1881
+ const deposited = positions.supplies.find((s) => s.asset === assetKey)?.amount ?? 0;
1701
1882
  const effectiveAmount = Math.min(amount, deposited);
1702
- if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", "Nothing to withdraw from Suilend");
1703
- const ratio = cTokenRatio(usdcReserve);
1704
- const ctokenAmount = Math.ceil(effectiveAmount * 10 ** usdcReserve.mintDecimals / ratio);
1883
+ if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on Suilend`);
1884
+ const ratio = cTokenRatio(reserve);
1885
+ const ctokenAmount = Math.ceil(effectiveAmount * 10 ** reserve.mintDecimals / ratio);
1705
1886
  const tx = new transactions.Transaction();
1706
1887
  tx.setSender(address);
1707
1888
  const [ctokens] = tx.moveCall({
1708
1889
  target: `${pkg}::lending_market::withdraw_ctokens`,
1709
- typeArguments: [LENDING_MARKET_TYPE, USDC_TYPE2],
1890
+ typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1710
1891
  arguments: [
1711
1892
  tx.object(LENDING_MARKET_ID),
1712
- tx.pure.u64(usdcReserve.arrayIndex),
1893
+ tx.pure.u64(reserve.arrayIndex),
1713
1894
  tx.object(caps[0].id),
1714
1895
  tx.object(CLOCK2),
1715
1896
  tx.pure.u64(ctokenAmount)
1716
1897
  ]
1717
1898
  });
1718
- const exemptionType = `${SUILEND_PACKAGE}::lending_market::RateLimiterExemption<${LENDING_MARKET_TYPE}, ${USDC_TYPE2}>`;
1899
+ const exemptionType = `${SUILEND_PACKAGE}::lending_market::RateLimiterExemption<${LENDING_MARKET_TYPE}, ${assetInfo.type}>`;
1719
1900
  const [none] = tx.moveCall({
1720
1901
  target: "0x1::option::none",
1721
1902
  typeArguments: [exemptionType]
1722
1903
  });
1723
1904
  const [coin] = tx.moveCall({
1724
1905
  target: `${pkg}::lending_market::redeem_ctokens_and_withdraw_liquidity`,
1725
- typeArguments: [LENDING_MARKET_TYPE, USDC_TYPE2],
1906
+ typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1726
1907
  arguments: [
1727
1908
  tx.object(LENDING_MARKET_ID),
1728
- tx.pure.u64(usdcReserve.arrayIndex),
1909
+ tx.pure.u64(reserve.arrayIndex),
1729
1910
  tx.object(CLOCK2),
1730
1911
  ctokens,
1731
1912
  none
@@ -1734,11 +1915,64 @@ var SuilendAdapter = class {
1734
1915
  tx.transferObjects([coin], address);
1735
1916
  return { tx, effectiveAmount };
1736
1917
  }
1737
- async buildBorrowTx(_address, _amount, _asset, _options) {
1738
- throw new T2000Error("ASSET_NOT_SUPPORTED", "Suilend borrow requires different collateral/borrow assets. Deferred to Phase 10.");
1918
+ async buildBorrowTx(address, amount, asset, options) {
1919
+ const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
1920
+ const assetInfo = SUPPORTED_ASSETS[assetKey];
1921
+ const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
1922
+ const reserve = this.findReserve(reserves, assetKey);
1923
+ if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend. Try: NAVI or a different asset.`);
1924
+ const caps = await this.fetchObligationCaps(address);
1925
+ if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found. Deposit collateral first with: t2000 save <amount>");
1926
+ const rawAmount = stableToRaw(amount, assetInfo.decimals);
1927
+ const tx = new transactions.Transaction();
1928
+ tx.setSender(address);
1929
+ const [coin] = tx.moveCall({
1930
+ target: `${pkg}::lending_market::borrow`,
1931
+ typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1932
+ arguments: [
1933
+ tx.object(LENDING_MARKET_ID),
1934
+ tx.pure.u64(reserve.arrayIndex),
1935
+ tx.object(caps[0].id),
1936
+ tx.object(CLOCK2),
1937
+ tx.pure.u64(rawAmount)
1938
+ ]
1939
+ });
1940
+ if (options?.collectFee) {
1941
+ addCollectFeeToTx(tx, coin, "borrow");
1942
+ }
1943
+ tx.transferObjects([coin], address);
1944
+ return { tx };
1739
1945
  }
1740
- async buildRepayTx(_address, _amount, _asset) {
1741
- throw new T2000Error("ASSET_NOT_SUPPORTED", "Suilend repay deferred to Phase 10.");
1946
+ async buildRepayTx(address, amount, asset) {
1947
+ const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
1948
+ const assetInfo = SUPPORTED_ASSETS[assetKey];
1949
+ const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
1950
+ const reserve = this.findReserve(reserves, assetKey);
1951
+ if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
1952
+ const caps = await this.fetchObligationCaps(address);
1953
+ if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend obligation found");
1954
+ const allCoins = await this.fetchAllCoins(address, assetInfo.type);
1955
+ if (allCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins to repay with`);
1956
+ const rawAmount = stableToRaw(amount, assetInfo.decimals);
1957
+ const tx = new transactions.Transaction();
1958
+ tx.setSender(address);
1959
+ const primaryCoinId = allCoins[0].coinObjectId;
1960
+ if (allCoins.length > 1) {
1961
+ tx.mergeCoins(tx.object(primaryCoinId), allCoins.slice(1).map((c) => tx.object(c.coinObjectId)));
1962
+ }
1963
+ const [repayCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawAmount.toString()]);
1964
+ tx.moveCall({
1965
+ target: `${pkg}::lending_market::repay`,
1966
+ typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1967
+ arguments: [
1968
+ tx.object(LENDING_MARKET_ID),
1969
+ tx.pure.u64(reserve.arrayIndex),
1970
+ tx.object(caps[0].id),
1971
+ tx.object(CLOCK2),
1972
+ repayCoin
1973
+ ]
1974
+ });
1975
+ return { tx };
1742
1976
  }
1743
1977
  async maxWithdraw(address, _asset) {
1744
1978
  const health = await this.getHealth(address);
@@ -1752,8 +1986,10 @@ var SuilendAdapter = class {
1752
1986
  const hfAfter = health.borrowed > 0 ? remainingSupply * health.liquidationThreshold / health.borrowed : Infinity;
1753
1987
  return { maxAmount, healthFactorAfter: hfAfter, currentHF: health.healthFactor };
1754
1988
  }
1755
- async maxBorrow(_address, _asset) {
1756
- throw new T2000Error("ASSET_NOT_SUPPORTED", "Suilend maxBorrow deferred to Phase 10.");
1989
+ async maxBorrow(address, _asset) {
1990
+ const health = await this.getHealth(address);
1991
+ const maxAmount = health.maxBorrow;
1992
+ return { maxAmount, healthFactorAfter: MIN_HEALTH_FACTOR2, currentHF: health.healthFactor };
1757
1993
  }
1758
1994
  async fetchAllCoins(owner, coinType) {
1759
1995
  const all = [];
@@ -2147,48 +2383,55 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
2147
2383
  }
2148
2384
  // -- Savings --
2149
2385
  async save(params) {
2150
- const asset = (params.asset ?? "USDC").toUpperCase();
2151
- if (asset !== "USDC") {
2152
- throw new T2000Error("ASSET_NOT_SUPPORTED", `Only USDC is supported for save. Got: ${asset}`);
2386
+ const asset = normalizeAsset(params.asset ?? "USDC");
2387
+ if (!this.registry.isSupportedAsset(asset, "save")) {
2388
+ const supported = this.registry.getSupportedAssets("save").join(", ");
2389
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `${asset} is not supported for save. Supported: ${supported}`);
2153
2390
  }
2154
2391
  let amount;
2155
2392
  if (params.amount === "all") {
2156
2393
  const bal = await queryBalance(this.client, this._address);
2157
- const GAS_RESERVE_USDC = 1;
2158
- amount = bal.available - GAS_RESERVE_USDC;
2394
+ const assetBalance = bal.stables[asset] ?? 0;
2395
+ const reserve = asset === "USDC" ? 1 : 0;
2396
+ amount = assetBalance - reserve;
2159
2397
  if (amount <= 0) {
2160
- throw new T2000Error("INSUFFICIENT_BALANCE", "Balance too low to save after $1 gas reserve", {
2161
- reason: "gas_reserve_required",
2162
- available: bal.available
2398
+ throw new T2000Error("INSUFFICIENT_BALANCE", `Balance too low to save${asset === "USDC" ? " after $1 gas reserve" : ""}`, {
2399
+ reason: asset === "USDC" ? "gas_reserve_required" : "zero_balance",
2400
+ available: assetBalance
2163
2401
  });
2164
2402
  }
2165
2403
  } else {
2166
2404
  amount = params.amount;
2167
2405
  const bal = await queryBalance(this.client, this._address);
2168
- if (amount > bal.available) {
2169
- throw new T2000Error("INSUFFICIENT_BALANCE", `Insufficient USDC. Available: $${bal.available.toFixed(2)}, requested: $${amount.toFixed(2)}`);
2406
+ const assetBalance = bal.stables[asset] ?? 0;
2407
+ if (amount > assetBalance) {
2408
+ throw new T2000Error("INSUFFICIENT_BALANCE", `Insufficient ${asset}. Available: $${assetBalance.toFixed(2)}, requested: $${amount.toFixed(2)}`);
2170
2409
  }
2171
2410
  }
2172
- const fee = calculateFee("save", amount);
2411
+ const shouldCollectFee = asset === "USDC";
2412
+ const fee = shouldCollectFee ? calculateFee("save", amount) : { amount: 0, rate: 0};
2173
2413
  const saveAmount = amount;
2174
2414
  const adapter = await this.resolveLending(params.protocol, asset, "save");
2175
2415
  const gasResult = await executeWithGas(this.client, this.keypair, async () => {
2176
- const { tx } = await adapter.buildSaveTx(this._address, saveAmount, asset, { collectFee: true });
2416
+ const { tx } = await adapter.buildSaveTx(this._address, saveAmount, asset, { collectFee: shouldCollectFee });
2177
2417
  return tx;
2178
2418
  });
2179
2419
  const rates = await adapter.getRates(asset);
2180
- reportFee(this._address, "save", fee.amount, fee.rate, gasResult.digest);
2181
- this.emitBalanceChange("USDC", saveAmount, "save", gasResult.digest);
2420
+ if (shouldCollectFee) {
2421
+ reportFee(this._address, "save", fee.amount, fee.rate, gasResult.digest);
2422
+ }
2423
+ this.emitBalanceChange(asset, saveAmount, "save", gasResult.digest);
2182
2424
  let savingsBalance = saveAmount;
2183
2425
  try {
2184
2426
  const positions = await this.positions();
2185
- savingsBalance = positions.positions.filter((p) => p.type === "save").reduce((sum, p) => sum + p.amount, 0);
2427
+ savingsBalance = positions.positions.filter((p) => p.type === "save" && p.asset === asset).reduce((sum, p) => sum + p.amount, 0);
2186
2428
  } catch {
2187
2429
  }
2188
2430
  return {
2189
2431
  success: true,
2190
2432
  tx: gasResult.digest,
2191
2433
  amount: saveAmount,
2434
+ asset,
2192
2435
  apy: rates.saveApy,
2193
2436
  fee: fee.amount,
2194
2437
  gasCost: gasResult.gasCostSui,
@@ -2197,10 +2440,7 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
2197
2440
  };
2198
2441
  }
2199
2442
  async withdraw(params) {
2200
- const asset = (params.asset ?? "USDC").toUpperCase();
2201
- if (asset !== "USDC") {
2202
- throw new T2000Error("ASSET_NOT_SUPPORTED", `Only USDC is supported for withdraw. Got: ${asset}`);
2203
- }
2443
+ const asset = normalizeAsset(params.asset ?? "USDC");
2204
2444
  if (params.amount === "all" && !params.protocol) {
2205
2445
  return this.withdrawAllProtocols(asset);
2206
2446
  }
@@ -2237,7 +2477,7 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
2237
2477
  effectiveAmount = built.effectiveAmount;
2238
2478
  return built.tx;
2239
2479
  });
2240
- this.emitBalanceChange("USDC", effectiveAmount, "withdraw", gasResult.digest);
2480
+ this.emitBalanceChange(asset, effectiveAmount, "withdraw", gasResult.digest);
2241
2481
  return {
2242
2482
  success: true,
2243
2483
  tx: gasResult.digest,
@@ -2246,26 +2486,31 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
2246
2486
  gasMethod: gasResult.gasMethod
2247
2487
  };
2248
2488
  }
2249
- async withdrawAllProtocols(asset) {
2489
+ async withdrawAllProtocols(_asset) {
2250
2490
  const allPositions = await this.registry.allPositions(this._address);
2251
- const withSupply = allPositions.filter(
2252
- (p) => p.positions.supplies.some((s) => s.asset === asset && s.amount > 1e-3)
2253
- );
2254
- if (withSupply.length === 0) {
2491
+ const withdrawable = [];
2492
+ for (const pos of allPositions) {
2493
+ for (const supply of pos.positions.supplies) {
2494
+ if (supply.amount > 1e-3) {
2495
+ withdrawable.push({ protocolId: pos.protocolId, asset: supply.asset, amount: supply.amount });
2496
+ }
2497
+ }
2498
+ }
2499
+ if (withdrawable.length === 0) {
2255
2500
  throw new T2000Error("NO_COLLATERAL", "No savings to withdraw across any protocol");
2256
2501
  }
2257
2502
  let totalWithdrawn = 0;
2258
2503
  let lastDigest = "";
2259
2504
  let totalGasCost = 0;
2260
2505
  let lastGasMethod = "self-funded";
2261
- for (const pos of withSupply) {
2262
- const adapter = this.registry.getLending(pos.protocolId);
2506
+ for (const entry of withdrawable) {
2507
+ const adapter = this.registry.getLending(entry.protocolId);
2263
2508
  if (!adapter) continue;
2264
- const maxResult = await adapter.maxWithdraw(this._address, asset);
2509
+ const maxResult = await adapter.maxWithdraw(this._address, entry.asset);
2265
2510
  if (maxResult.maxAmount <= 1e-3) continue;
2266
2511
  let effectiveAmount = maxResult.maxAmount;
2267
2512
  const gasResult = await executeWithGas(this.client, this.keypair, async () => {
2268
- const built = await adapter.buildWithdrawTx(this._address, maxResult.maxAmount, asset);
2513
+ const built = await adapter.buildWithdrawTx(this._address, maxResult.maxAmount, entry.asset);
2269
2514
  effectiveAmount = built.effectiveAmount;
2270
2515
  return built.tx;
2271
2516
  });
@@ -2273,7 +2518,7 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
2273
2518
  lastDigest = gasResult.digest;
2274
2519
  totalGasCost += gasResult.gasCostSui;
2275
2520
  lastGasMethod = gasResult.gasMethod;
2276
- this.emitBalanceChange("USDC", effectiveAmount, "withdraw", gasResult.digest);
2521
+ this.emitBalanceChange(entry.asset, effectiveAmount, "withdraw", gasResult.digest);
2277
2522
  }
2278
2523
  if (totalWithdrawn <= 0) {
2279
2524
  throw new T2000Error("NO_COLLATERAL", "No savings to withdraw across any protocol");
@@ -2292,27 +2537,30 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
2292
2537
  }
2293
2538
  // -- Borrowing --
2294
2539
  async borrow(params) {
2295
- const asset = (params.asset ?? "USDC").toUpperCase();
2296
- if (asset !== "USDC") {
2297
- throw new T2000Error("ASSET_NOT_SUPPORTED", `Only USDC is supported for borrow. Got: ${asset}`);
2298
- }
2540
+ const asset = normalizeAsset(params.asset ?? "USDC");
2299
2541
  const adapter = await this.resolveLending(params.protocol, asset, "borrow");
2300
2542
  const maxResult = await adapter.maxBorrow(this._address, asset);
2543
+ if (maxResult.maxAmount <= 0) {
2544
+ throw new T2000Error("NO_COLLATERAL", "No collateral deposited. Save first with `t2000 save <amount>`.");
2545
+ }
2301
2546
  if (params.amount > maxResult.maxAmount) {
2302
2547
  throw new T2000Error("HEALTH_FACTOR_TOO_LOW", `Max safe borrow: $${maxResult.maxAmount.toFixed(2)}`, {
2303
2548
  maxBorrow: maxResult.maxAmount,
2304
2549
  currentHF: maxResult.currentHF
2305
2550
  });
2306
2551
  }
2307
- const fee = calculateFee("borrow", params.amount);
2552
+ const shouldCollectFee = asset === "USDC";
2553
+ const fee = shouldCollectFee ? calculateFee("borrow", params.amount) : { amount: 0, rate: 0};
2308
2554
  const borrowAmount = params.amount;
2309
2555
  const gasResult = await executeWithGas(this.client, this.keypair, async () => {
2310
- const { tx } = await adapter.buildBorrowTx(this._address, borrowAmount, asset, { collectFee: true });
2556
+ const { tx } = await adapter.buildBorrowTx(this._address, borrowAmount, asset, { collectFee: shouldCollectFee });
2311
2557
  return tx;
2312
2558
  });
2313
2559
  const hf = await adapter.getHealth(this._address);
2314
- reportFee(this._address, "borrow", fee.amount, fee.rate, gasResult.digest);
2315
- this.emitBalanceChange("USDC", borrowAmount, "borrow", gasResult.digest);
2560
+ if (shouldCollectFee) {
2561
+ reportFee(this._address, "borrow", fee.amount, fee.rate, gasResult.digest);
2562
+ }
2563
+ this.emitBalanceChange(asset, borrowAmount, "borrow", gasResult.digest);
2316
2564
  return {
2317
2565
  success: true,
2318
2566
  tx: gasResult.digest,
@@ -2324,10 +2572,7 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
2324
2572
  };
2325
2573
  }
2326
2574
  async repay(params) {
2327
- const asset = (params.asset ?? "USDC").toUpperCase();
2328
- if (asset !== "USDC") {
2329
- throw new T2000Error("ASSET_NOT_SUPPORTED", `Only USDC is supported for repay. Got: ${asset}`);
2330
- }
2575
+ const asset = normalizeAsset(params.asset ?? "USDC");
2331
2576
  const adapter = await this.resolveLending(params.protocol, asset, "repay");
2332
2577
  let amount;
2333
2578
  if (params.amount === "all") {
@@ -2345,7 +2590,7 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
2345
2590
  return tx;
2346
2591
  });
2347
2592
  const hf = await adapter.getHealth(this._address);
2348
- this.emitBalanceChange("USDC", repayAmount, "repay", gasResult.digest);
2593
+ this.emitBalanceChange(asset, repayAmount, "repay", gasResult.digest);
2349
2594
  return {
2350
2595
  success: true,
2351
2596
  tx: gasResult.digest,
@@ -2371,8 +2616,8 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
2371
2616
  }
2372
2617
  // -- Swap --
2373
2618
  async swap(params) {
2374
- const fromAsset = params.from.toUpperCase();
2375
- const toAsset = params.to.toUpperCase();
2619
+ const fromAsset = normalizeAsset(params.from);
2620
+ const toAsset = normalizeAsset(params.to);
2376
2621
  if (!(fromAsset in SUPPORTED_ASSETS) || !(toAsset in SUPPORTED_ASSETS)) {
2377
2622
  throw new T2000Error("ASSET_NOT_SUPPORTED", `Swap pair ${fromAsset}/${toAsset} is not supported`);
2378
2623
  }
@@ -2431,8 +2676,8 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
2431
2676
  };
2432
2677
  }
2433
2678
  async swapQuote(params) {
2434
- const fromAsset = params.from.toUpperCase();
2435
- const toAsset = params.to.toUpperCase();
2679
+ const fromAsset = normalizeAsset(params.from);
2680
+ const toAsset = normalizeAsset(params.to);
2436
2681
  const best = await this.registry.bestSwapQuote(fromAsset, toAsset, params.amount);
2437
2682
  const fee = calculateFee("swap", params.amount);
2438
2683
  return { ...best.quote, fee: { amount: fee.amount, rate: fee.rate } };
@@ -2461,14 +2706,225 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
2461
2706
  return { positions };
2462
2707
  }
2463
2708
  async rates() {
2464
- const allRatesResult = await this.registry.allRates("USDC");
2465
- if (allRatesResult.length === 0) return { USDC: { saveApy: 0, borrowApy: 0 } };
2466
- const best = allRatesResult.reduce((a, b) => b.rates.saveApy > a.rates.saveApy ? b : a);
2467
- return { USDC: { saveApy: best.rates.saveApy, borrowApy: best.rates.borrowApy } };
2709
+ const allRatesResult = await this.registry.allRatesAcrossAssets();
2710
+ const result = {};
2711
+ for (const entry of allRatesResult) {
2712
+ if (!result[entry.asset] || entry.rates.saveApy > result[entry.asset].saveApy) {
2713
+ result[entry.asset] = { saveApy: entry.rates.saveApy, borrowApy: entry.rates.borrowApy };
2714
+ }
2715
+ }
2716
+ if (!result.USDC) result.USDC = { saveApy: 0, borrowApy: 0 };
2717
+ return result;
2468
2718
  }
2469
2719
  async allRates(asset = "USDC") {
2470
2720
  return this.registry.allRates(asset);
2471
2721
  }
2722
+ async allRatesAcrossAssets() {
2723
+ return this.registry.allRatesAcrossAssets();
2724
+ }
2725
+ async rebalance(opts = {}) {
2726
+ const dryRun = opts.dryRun ?? false;
2727
+ const minYieldDiff = opts.minYieldDiff ?? 0.5;
2728
+ const maxBreakEven = opts.maxBreakEven ?? 30;
2729
+ const [allPositions, allRates] = await Promise.all([
2730
+ this.registry.allPositions(this._address),
2731
+ this.registry.allRatesAcrossAssets()
2732
+ ]);
2733
+ const savePositions = allPositions.flatMap(
2734
+ (p) => p.positions.supplies.filter((s) => s.amount > 0.01).map((s) => ({
2735
+ protocolId: p.protocolId,
2736
+ protocol: p.protocol,
2737
+ asset: s.asset,
2738
+ amount: s.amount,
2739
+ apy: s.apy
2740
+ }))
2741
+ );
2742
+ if (savePositions.length === 0) {
2743
+ throw new T2000Error("NO_COLLATERAL", "No savings positions to rebalance. Use `t2000 save <amount>` first.");
2744
+ }
2745
+ const borrowPositions = allPositions.flatMap(
2746
+ (p) => p.positions.borrows.filter((b) => b.amount > 0.01)
2747
+ );
2748
+ if (borrowPositions.length > 0) {
2749
+ const healthResults = await Promise.all(
2750
+ allPositions.filter((p) => p.positions.borrows.some((b) => b.amount > 0.01)).map(async (p) => {
2751
+ const adapter = this.registry.getLending(p.protocolId);
2752
+ if (!adapter) return null;
2753
+ return adapter.getHealth(this._address);
2754
+ })
2755
+ );
2756
+ for (const hf of healthResults) {
2757
+ if (hf && hf.healthFactor < 1.5) {
2758
+ throw new T2000Error(
2759
+ "HEALTH_FACTOR_TOO_LOW",
2760
+ `Cannot rebalance \u2014 health factor is ${hf.healthFactor.toFixed(2)} (minimum 1.5). Repay some debt first.`,
2761
+ { healthFactor: hf.healthFactor }
2762
+ );
2763
+ }
2764
+ }
2765
+ }
2766
+ const bestRate = allRates.reduce(
2767
+ (best, r) => r.rates.saveApy > best.rates.saveApy ? r : best
2768
+ );
2769
+ const current = savePositions.reduce(
2770
+ (worst, p) => p.apy < worst.apy ? p : worst
2771
+ );
2772
+ const apyDiff = bestRate.rates.saveApy - current.apy;
2773
+ const isSameProtocol = current.protocolId === bestRate.protocolId;
2774
+ const isSameAsset = current.asset === bestRate.asset;
2775
+ if (apyDiff < minYieldDiff) {
2776
+ return {
2777
+ executed: false,
2778
+ steps: [],
2779
+ fromProtocol: current.protocol,
2780
+ fromAsset: current.asset,
2781
+ toProtocol: bestRate.protocol,
2782
+ toAsset: bestRate.asset,
2783
+ amount: current.amount,
2784
+ currentApy: current.apy,
2785
+ newApy: bestRate.rates.saveApy,
2786
+ annualGain: current.amount * apyDiff / 100,
2787
+ estimatedSwapCost: 0,
2788
+ breakEvenDays: Infinity,
2789
+ txDigests: [],
2790
+ totalGasCost: 0
2791
+ };
2792
+ }
2793
+ if (isSameProtocol && isSameAsset) {
2794
+ return {
2795
+ executed: false,
2796
+ steps: [],
2797
+ fromProtocol: current.protocol,
2798
+ fromAsset: current.asset,
2799
+ toProtocol: bestRate.protocol,
2800
+ toAsset: bestRate.asset,
2801
+ amount: current.amount,
2802
+ currentApy: current.apy,
2803
+ newApy: bestRate.rates.saveApy,
2804
+ annualGain: 0,
2805
+ estimatedSwapCost: 0,
2806
+ breakEvenDays: Infinity,
2807
+ txDigests: [],
2808
+ totalGasCost: 0
2809
+ };
2810
+ }
2811
+ const steps = [];
2812
+ let estimatedSwapCost = 0;
2813
+ steps.push({
2814
+ action: "withdraw",
2815
+ protocol: current.protocolId,
2816
+ fromAsset: current.asset,
2817
+ amount: current.amount
2818
+ });
2819
+ let amountToDeposit = current.amount;
2820
+ if (!isSameAsset) {
2821
+ try {
2822
+ const quote = await this.registry.bestSwapQuote(current.asset, bestRate.asset, current.amount);
2823
+ amountToDeposit = quote.quote.expectedOutput;
2824
+ estimatedSwapCost = Math.abs(current.amount - amountToDeposit);
2825
+ } catch {
2826
+ estimatedSwapCost = current.amount * 3e-3;
2827
+ amountToDeposit = current.amount - estimatedSwapCost;
2828
+ }
2829
+ steps.push({
2830
+ action: "swap",
2831
+ fromAsset: current.asset,
2832
+ toAsset: bestRate.asset,
2833
+ amount: current.amount,
2834
+ estimatedOutput: amountToDeposit
2835
+ });
2836
+ }
2837
+ steps.push({
2838
+ action: "deposit",
2839
+ protocol: bestRate.protocolId,
2840
+ toAsset: bestRate.asset,
2841
+ amount: amountToDeposit
2842
+ });
2843
+ const annualGain = amountToDeposit * apyDiff / 100;
2844
+ const breakEvenDays = estimatedSwapCost > 0 ? Math.ceil(estimatedSwapCost / annualGain * 365) : 0;
2845
+ if (breakEvenDays > maxBreakEven && estimatedSwapCost > 0) {
2846
+ return {
2847
+ executed: false,
2848
+ steps,
2849
+ fromProtocol: current.protocol,
2850
+ fromAsset: current.asset,
2851
+ toProtocol: bestRate.protocol,
2852
+ toAsset: bestRate.asset,
2853
+ amount: current.amount,
2854
+ currentApy: current.apy,
2855
+ newApy: bestRate.rates.saveApy,
2856
+ annualGain,
2857
+ estimatedSwapCost,
2858
+ breakEvenDays,
2859
+ txDigests: [],
2860
+ totalGasCost: 0
2861
+ };
2862
+ }
2863
+ if (dryRun) {
2864
+ return {
2865
+ executed: false,
2866
+ steps,
2867
+ fromProtocol: current.protocol,
2868
+ fromAsset: current.asset,
2869
+ toProtocol: bestRate.protocol,
2870
+ toAsset: bestRate.asset,
2871
+ amount: current.amount,
2872
+ currentApy: current.apy,
2873
+ newApy: bestRate.rates.saveApy,
2874
+ annualGain,
2875
+ estimatedSwapCost,
2876
+ breakEvenDays,
2877
+ txDigests: [],
2878
+ totalGasCost: 0
2879
+ };
2880
+ }
2881
+ const txDigests = [];
2882
+ let totalGasCost = 0;
2883
+ const withdrawAdapter = this.registry.getLending(current.protocolId);
2884
+ if (!withdrawAdapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", `Protocol ${current.protocolId} not found`);
2885
+ const withdrawResult = await executeWithGas(this.client, this.keypair, async () => {
2886
+ const built = await withdrawAdapter.buildWithdrawTx(this._address, current.amount, current.asset);
2887
+ amountToDeposit = isSameAsset ? built.effectiveAmount : built.effectiveAmount;
2888
+ return built.tx;
2889
+ });
2890
+ txDigests.push(withdrawResult.digest);
2891
+ totalGasCost += withdrawResult.gasCostSui;
2892
+ if (!isSameAsset) {
2893
+ const swapAdapter = this.registry.listSwap()[0];
2894
+ if (!swapAdapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", "No swap adapter available");
2895
+ const swapResult = await executeWithGas(this.client, this.keypair, async () => {
2896
+ const built = await swapAdapter.buildSwapTx(this._address, current.asset, bestRate.asset, amountToDeposit);
2897
+ amountToDeposit = built.estimatedOut / 10 ** built.toDecimals;
2898
+ return built.tx;
2899
+ });
2900
+ txDigests.push(swapResult.digest);
2901
+ totalGasCost += swapResult.gasCostSui;
2902
+ }
2903
+ const depositAdapter = this.registry.getLending(bestRate.protocolId);
2904
+ if (!depositAdapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", `Protocol ${bestRate.protocolId} not found`);
2905
+ const depositResult = await executeWithGas(this.client, this.keypair, async () => {
2906
+ const { tx } = await depositAdapter.buildSaveTx(this._address, amountToDeposit, bestRate.asset, { collectFee: bestRate.asset === "USDC" });
2907
+ return tx;
2908
+ });
2909
+ txDigests.push(depositResult.digest);
2910
+ totalGasCost += depositResult.gasCostSui;
2911
+ return {
2912
+ executed: true,
2913
+ steps,
2914
+ fromProtocol: current.protocol,
2915
+ fromAsset: current.asset,
2916
+ toProtocol: bestRate.protocol,
2917
+ toAsset: bestRate.asset,
2918
+ amount: current.amount,
2919
+ currentApy: current.apy,
2920
+ newApy: bestRate.rates.saveApy,
2921
+ annualGain,
2922
+ estimatedSwapCost,
2923
+ breakEvenDays,
2924
+ txDigests,
2925
+ totalGasCost
2926
+ };
2927
+ }
2472
2928
  async earnings() {
2473
2929
  const result = await getEarnings(this.client, this.keypair);
2474
2930
  if (result.totalYieldEarned > 0) {
@@ -2509,13 +2965,32 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
2509
2965
  const adapters2 = this.registry.listLending().filter(
2510
2966
  (a) => a.supportedAssets.includes(asset) && a.capabilities.includes(capability) && (capability !== "borrow" || a.supportsSameAssetBorrow)
2511
2967
  );
2512
- if (adapters2.length === 0) throw new T2000Error("ASSET_NOT_SUPPORTED", `No adapter supports ${capability} ${asset}`);
2968
+ if (adapters2.length === 0) {
2969
+ const alternatives = this.registry.listLending().filter(
2970
+ (a) => a.capabilities.includes(capability) && (capability !== "borrow" || a.supportsSameAssetBorrow)
2971
+ );
2972
+ if (alternatives.length > 0) {
2973
+ const altList = alternatives.map((a) => a.name).join(", ");
2974
+ const altAssets = [...new Set(alternatives.flatMap((a) => [...a.supportedAssets]))].join(", ");
2975
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `No protocol supports ${capability} for ${asset}. Available for ${capability}: ${altList} (assets: ${altAssets})`);
2976
+ }
2977
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `No adapter supports ${capability} ${asset}`);
2978
+ }
2513
2979
  return adapters2[0];
2514
2980
  }
2515
2981
  const adapters = this.registry.listLending().filter(
2516
2982
  (a) => a.supportedAssets.includes(asset) && a.capabilities.includes(capability)
2517
2983
  );
2518
- if (adapters.length === 0) throw new T2000Error("ASSET_NOT_SUPPORTED", `No adapter supports ${capability} ${asset}`);
2984
+ if (adapters.length === 0) {
2985
+ const alternatives = this.registry.listLending().filter(
2986
+ (a) => a.capabilities.includes(capability)
2987
+ );
2988
+ if (alternatives.length > 0) {
2989
+ const altList = alternatives.map((a) => `${a.name} (${[...a.supportedAssets].join(", ")})`).join("; ");
2990
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `No protocol supports ${capability} for ${asset}. Try: ${altList}`);
2991
+ }
2992
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `No adapter supports ${capability} ${asset}`);
2993
+ }
2519
2994
  return adapters[0];
2520
2995
  }
2521
2996
  emitBalanceChange(asset, amount, cause, tx) {
@@ -2632,6 +3107,7 @@ exports.MIST_PER_SUI = MIST_PER_SUI;
2632
3107
  exports.NaviAdapter = NaviAdapter;
2633
3108
  exports.ProtocolRegistry = ProtocolRegistry;
2634
3109
  exports.SENTINEL = SENTINEL;
3110
+ exports.STABLE_ASSETS = STABLE_ASSETS;
2635
3111
  exports.SUI_DECIMALS = SUI_DECIMALS;
2636
3112
  exports.SUPPORTED_ASSETS = SUPPORTED_ASSETS;
2637
3113
  exports.SuilendAdapter = SuilendAdapter;
@@ -2649,6 +3125,7 @@ exports.formatSui = formatSui;
2649
3125
  exports.formatUsd = formatUsd;
2650
3126
  exports.generateKeypair = generateKeypair;
2651
3127
  exports.getAddress = getAddress;
3128
+ exports.getDecimals = getDecimals;
2652
3129
  exports.getGasStatus = getGasStatus;
2653
3130
  exports.getPoolPrice = getPoolPrice;
2654
3131
  exports.getRates = getRates;
@@ -2661,6 +3138,8 @@ exports.mapMoveAbortCode = mapMoveAbortCode;
2661
3138
  exports.mapWalletError = mapWalletError;
2662
3139
  exports.mistToSui = mistToSui;
2663
3140
  exports.naviDescriptor = descriptor2;
3141
+ exports.normalizeAsset = normalizeAsset;
3142
+ exports.rawToStable = rawToStable;
2664
3143
  exports.rawToUsdc = rawToUsdc;
2665
3144
  exports.requestAttack = requestAttack;
2666
3145
  exports.saveKey = saveKey;
@@ -2670,6 +3149,7 @@ exports.settleAttack = settleAttack;
2670
3149
  exports.shouldAutoTopUp = shouldAutoTopUp;
2671
3150
  exports.simulateTransaction = simulateTransaction;
2672
3151
  exports.solveHashcash = solveHashcash;
3152
+ exports.stableToRaw = stableToRaw;
2673
3153
  exports.submitPrompt = submitPrompt;
2674
3154
  exports.suiToMist = suiToMist;
2675
3155
  exports.suilendDescriptor = descriptor4;