@t2000/sdk 0.7.2 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -6
- package/dist/adapters/index.cjs +338 -128
- package/dist/adapters/index.cjs.map +1 -1
- package/dist/adapters/index.d.cts +1 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.js +338 -128
- package/dist/adapters/index.js.map +1 -1
- package/dist/{index-rT0oHn8M.d.cts → index-DNjooNFy.d.cts} +54 -14
- package/dist/{index-rT0oHn8M.d.ts → index-DNjooNFy.d.ts} +54 -14
- package/dist/index.cjs +641 -161
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +45 -3
- package/dist/index.d.ts +45 -3
- package/dist/index.js +637 -162
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
324
|
-
client.getBalance({ owner: address, coinType: SUPPORTED_ASSETS.
|
|
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
|
|
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 =
|
|
376
|
+
const total = totalStables + savings + usdEquiv;
|
|
333
377
|
return {
|
|
334
|
-
available:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
494
|
-
|
|
543
|
+
const targetType = SUPPORTED_ASSETS[asset].type;
|
|
544
|
+
const pool = pools.find(
|
|
545
|
+
(p) => matchesCoinType(p.suiCoinType || p.coinType || "", targetType)
|
|
495
546
|
);
|
|
496
|
-
if (!
|
|
497
|
-
|
|
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
|
|
586
|
-
const
|
|
587
|
-
const
|
|
588
|
-
|
|
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
|
-
|
|
674
|
+
getPool(asset),
|
|
615
675
|
getPools(),
|
|
616
676
|
getUserState(client, address)
|
|
617
677
|
]);
|
|
618
|
-
const
|
|
619
|
-
const deposited =
|
|
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",
|
|
622
|
-
const rawAmount = Number(
|
|
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
|
|
654
|
-
const
|
|
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
|
|
689
|
-
const
|
|
690
|
-
const
|
|
691
|
-
|
|
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,
|
|
779
|
+
const [config, pools, states] = await Promise.all([
|
|
716
780
|
getConfig(),
|
|
717
|
-
|
|
781
|
+
getPools(),
|
|
718
782
|
getUserState(client, address)
|
|
719
783
|
]);
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
const
|
|
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(
|
|
817
|
+
tx.pure.u8(usdcPool.id),
|
|
739
818
|
tx.pure.address(address),
|
|
740
|
-
tx.pure.u8(
|
|
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: [
|
|
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 =
|
|
834
|
+
healthFactor = supplied * liqThreshold / borrowed;
|
|
756
835
|
}
|
|
757
836
|
} catch {
|
|
758
|
-
healthFactor =
|
|
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
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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 = [
|
|
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,
|
|
1202
|
-
const
|
|
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,
|
|
1206
|
-
const
|
|
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,
|
|
1210
|
-
const
|
|
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,
|
|
1214
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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 = [
|
|
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 (
|
|
1526
|
-
|
|
1527
|
-
else if (asset.includes("::"))
|
|
1528
|
-
|
|
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
|
-
|
|
1578
|
-
|
|
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
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
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 *
|
|
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,
|
|
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
|
|
1641
|
-
if (!
|
|
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,
|
|
1657
|
-
if (allCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE",
|
|
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 =
|
|
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,
|
|
1848
|
+
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
1670
1849
|
arguments: [
|
|
1671
1850
|
tx.object(LENDING_MARKET_ID),
|
|
1672
|
-
tx.pure.u64(
|
|
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,
|
|
1858
|
+
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
1680
1859
|
arguments: [
|
|
1681
1860
|
tx.object(LENDING_MARKET_ID),
|
|
1682
|
-
tx.pure.u64(
|
|
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,
|
|
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
|
|
1696
|
-
if (!
|
|
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 ===
|
|
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",
|
|
1703
|
-
const ratio = cTokenRatio(
|
|
1704
|
-
const ctokenAmount = Math.ceil(effectiveAmount * 10 **
|
|
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,
|
|
1890
|
+
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
1710
1891
|
arguments: [
|
|
1711
1892
|
tx.object(LENDING_MARKET_ID),
|
|
1712
|
-
tx.pure.u64(
|
|
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}, ${
|
|
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,
|
|
1906
|
+
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
1726
1907
|
arguments: [
|
|
1727
1908
|
tx.object(LENDING_MARKET_ID),
|
|
1728
|
-
tx.pure.u64(
|
|
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(
|
|
1738
|
-
|
|
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(
|
|
1741
|
-
|
|
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(
|
|
1756
|
-
|
|
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")
|
|
2151
|
-
if (asset
|
|
2152
|
-
|
|
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
|
|
2158
|
-
|
|
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",
|
|
2161
|
-
reason: "gas_reserve_required",
|
|
2162
|
-
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
|
-
|
|
2169
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
2181
|
-
|
|
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")
|
|
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(
|
|
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(
|
|
2489
|
+
async withdrawAllProtocols(_asset) {
|
|
2250
2490
|
const allPositions = await this.registry.allPositions(this._address);
|
|
2251
|
-
const
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
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
|
|
2262
|
-
const adapter = this.registry.getLending(
|
|
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(
|
|
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")
|
|
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
|
|
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:
|
|
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
|
-
|
|
2315
|
-
|
|
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")
|
|
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(
|
|
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
|
|
2375
|
-
const toAsset = params.to
|
|
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
|
|
2435
|
-
const toAsset = params.to
|
|
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.
|
|
2465
|
-
|
|
2466
|
-
const
|
|
2467
|
-
|
|
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)
|
|
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)
|
|
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;
|