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