@t2000/sdk 0.3.0 → 0.4.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/adapters/index.cjs +856 -0
- package/dist/adapters/index.cjs.map +1 -0
- package/dist/adapters/index.d.cts +3 -0
- package/dist/adapters/index.d.ts +3 -0
- package/dist/adapters/index.js +851 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/index-DYQv9Wxo.d.cts +393 -0
- package/dist/index-DYQv9Wxo.d.ts +393 -0
- package/dist/index.cjs +703 -151
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -172
- package/dist/index.d.ts +17 -172
- package/dist/index.js +702 -154
- package/dist/index.js.map +1 -1
- package/package.json +15 -2
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'eventemitter3';
|
|
2
2
|
import { SuiClient } from '@mysten/sui/client';
|
|
3
|
-
import { normalizeSuiAddress, isValidSuiAddress } from '@mysten/sui/utils';
|
|
3
|
+
import { normalizeSuiAddress, isValidSuiAddress, normalizeStructTag } from '@mysten/sui/utils';
|
|
4
4
|
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
|
|
5
5
|
import { decodeSuiPrivateKey } from '@mysten/sui/cryptography';
|
|
6
6
|
import { createHash, randomBytes, createCipheriv, createDecipheriv, scryptSync } from 'crypto';
|
|
@@ -8,9 +8,9 @@ import { access, mkdir, writeFile, readFile } from 'fs/promises';
|
|
|
8
8
|
import { dirname, resolve } from 'path';
|
|
9
9
|
import { homedir } from 'os';
|
|
10
10
|
import { Transaction } from '@mysten/sui/transactions';
|
|
11
|
-
import { getPool, getCoins, mergeCoinsPTB, depositCoinPTB,
|
|
12
|
-
import { CetusClmmSDK } from '@cetusprotocol/sui-clmm-sdk';
|
|
11
|
+
import { getPool, getLendingState, getHealthFactor as getHealthFactor$1, getCoins, mergeCoinsPTB, depositCoinPTB, withdrawCoinPTB, borrowCoinPTB, repayCoinPTB, getPriceFeeds, filterPriceFeeds, updateOraclePricesPTB } from '@naviprotocol/lending';
|
|
13
12
|
import { bcs } from '@mysten/sui/bcs';
|
|
13
|
+
import { AggregatorClient, Env } from '@cetusprotocol/aggregator-sdk';
|
|
14
14
|
|
|
15
15
|
// src/t2000.ts
|
|
16
16
|
|
|
@@ -522,8 +522,8 @@ async function buildRepayTx(client, address, amount) {
|
|
|
522
522
|
await repayCoinPTB(tx, USDC_TYPE, coinObj, { ...ENV, amount: rawAmount });
|
|
523
523
|
return tx;
|
|
524
524
|
}
|
|
525
|
-
async function getHealthFactor(client,
|
|
526
|
-
const address =
|
|
525
|
+
async function getHealthFactor(client, addressOrKeypair) {
|
|
526
|
+
const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
|
|
527
527
|
const [healthFactor, state, pool] = await Promise.all([
|
|
528
528
|
getHealthFactor$1(address, clientOpt(client, true)),
|
|
529
529
|
getLendingState(address, clientOpt(client, true)),
|
|
@@ -555,8 +555,8 @@ async function getRates(client) {
|
|
|
555
555
|
return { USDC: { saveApy: 4, borrowApy: 6 } };
|
|
556
556
|
}
|
|
557
557
|
}
|
|
558
|
-
async function getPositions(client,
|
|
559
|
-
const address =
|
|
558
|
+
async function getPositions(client, addressOrKeypair) {
|
|
559
|
+
const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
|
|
560
560
|
const state = await getLendingState(address, clientOpt(client, true));
|
|
561
561
|
const positions = [];
|
|
562
562
|
for (const pos of state) {
|
|
@@ -584,8 +584,8 @@ async function getPositions(client, keypair) {
|
|
|
584
584
|
}
|
|
585
585
|
return { positions };
|
|
586
586
|
}
|
|
587
|
-
async function maxWithdrawAmount(client,
|
|
588
|
-
const hf = await getHealthFactor(client,
|
|
587
|
+
async function maxWithdrawAmount(client, addressOrKeypair) {
|
|
588
|
+
const hf = await getHealthFactor(client, addressOrKeypair);
|
|
589
589
|
const ltv = hf.liquidationThreshold > 0 ? hf.liquidationThreshold : 0.75;
|
|
590
590
|
let maxAmount;
|
|
591
591
|
if (hf.borrowed === 0) {
|
|
@@ -601,8 +601,8 @@ async function maxWithdrawAmount(client, keypair) {
|
|
|
601
601
|
currentHF: hf.healthFactor
|
|
602
602
|
};
|
|
603
603
|
}
|
|
604
|
-
async function maxBorrowAmount(client,
|
|
605
|
-
const hf = await getHealthFactor(client,
|
|
604
|
+
async function maxBorrowAmount(client, addressOrKeypair) {
|
|
605
|
+
const hf = await getHealthFactor(client, addressOrKeypair);
|
|
606
606
|
const ltv = hf.liquidationThreshold > 0 ? hf.liquidationThreshold : 0.75;
|
|
607
607
|
const maxAmount = Math.max(0, hf.supplied * ltv / MIN_HEALTH_FACTOR - hf.borrowed);
|
|
608
608
|
return {
|
|
@@ -611,108 +611,6 @@ async function maxBorrowAmount(client, keypair) {
|
|
|
611
611
|
currentHF: hf.healthFactor
|
|
612
612
|
};
|
|
613
613
|
}
|
|
614
|
-
var DEFAULT_SLIPPAGE_BPS = 300;
|
|
615
|
-
function isA2B(from) {
|
|
616
|
-
return from === "USDC";
|
|
617
|
-
}
|
|
618
|
-
var _cetusSDK = null;
|
|
619
|
-
function getCetusSDK() {
|
|
620
|
-
if (!_cetusSDK) {
|
|
621
|
-
_cetusSDK = CetusClmmSDK.createSDK({ env: "mainnet" });
|
|
622
|
-
}
|
|
623
|
-
return _cetusSDK;
|
|
624
|
-
}
|
|
625
|
-
async function buildSwapTx(params) {
|
|
626
|
-
const { client, address, fromAsset, toAsset, amount, maxSlippageBps = DEFAULT_SLIPPAGE_BPS } = params;
|
|
627
|
-
const a2b = isA2B(fromAsset);
|
|
628
|
-
const fromInfo = SUPPORTED_ASSETS[fromAsset];
|
|
629
|
-
const toInfo = SUPPORTED_ASSETS[toAsset];
|
|
630
|
-
const rawAmount = BigInt(Math.floor(amount * 10 ** fromInfo.decimals));
|
|
631
|
-
const sdk = getCetusSDK();
|
|
632
|
-
sdk.setSenderAddress(address);
|
|
633
|
-
const pool = await sdk.Pool.getPool(CETUS_USDC_SUI_POOL);
|
|
634
|
-
const preSwapResult = await sdk.Swap.preSwap({
|
|
635
|
-
pool,
|
|
636
|
-
current_sqrt_price: pool.current_sqrt_price,
|
|
637
|
-
coin_type_a: pool.coin_type_a,
|
|
638
|
-
coin_type_b: pool.coin_type_b,
|
|
639
|
-
decimals_a: 6,
|
|
640
|
-
decimals_b: 9,
|
|
641
|
-
a2b,
|
|
642
|
-
by_amount_in: true,
|
|
643
|
-
amount: rawAmount.toString()
|
|
644
|
-
});
|
|
645
|
-
const estimatedOut = Number(preSwapResult.estimated_amount_out);
|
|
646
|
-
const slippageFactor = (1e4 - maxSlippageBps) / 1e4;
|
|
647
|
-
const amountLimit = Math.floor(estimatedOut * slippageFactor);
|
|
648
|
-
const swapPayload = await sdk.Swap.createSwapPayload({
|
|
649
|
-
pool_id: pool.id,
|
|
650
|
-
coin_type_a: pool.coin_type_a,
|
|
651
|
-
coin_type_b: pool.coin_type_b,
|
|
652
|
-
a2b,
|
|
653
|
-
by_amount_in: true,
|
|
654
|
-
amount: preSwapResult.amount.toString(),
|
|
655
|
-
amount_limit: amountLimit.toString()
|
|
656
|
-
});
|
|
657
|
-
return {
|
|
658
|
-
tx: swapPayload,
|
|
659
|
-
estimatedOut,
|
|
660
|
-
toDecimals: toInfo.decimals
|
|
661
|
-
};
|
|
662
|
-
}
|
|
663
|
-
async function getPoolPrice(client) {
|
|
664
|
-
try {
|
|
665
|
-
const pool = await client.getObject({
|
|
666
|
-
id: CETUS_USDC_SUI_POOL,
|
|
667
|
-
options: { showContent: true }
|
|
668
|
-
});
|
|
669
|
-
if (pool.data?.content?.dataType === "moveObject") {
|
|
670
|
-
const fields = pool.data.content.fields;
|
|
671
|
-
const currentSqrtPrice = BigInt(String(fields.current_sqrt_price ?? "0"));
|
|
672
|
-
if (currentSqrtPrice > 0n) {
|
|
673
|
-
const Q64 = 2n ** 64n;
|
|
674
|
-
const sqrtPriceFloat = Number(currentSqrtPrice) / Number(Q64);
|
|
675
|
-
const rawPrice = sqrtPriceFloat * sqrtPriceFloat;
|
|
676
|
-
const suiPriceUsd = 1e3 / rawPrice;
|
|
677
|
-
if (suiPriceUsd > 0.01 && suiPriceUsd < 1e3) return suiPriceUsd;
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
} catch {
|
|
681
|
-
}
|
|
682
|
-
return 3.5;
|
|
683
|
-
}
|
|
684
|
-
async function getSwapQuote(client, fromAsset, toAsset, amount) {
|
|
685
|
-
const a2b = isA2B(fromAsset);
|
|
686
|
-
const fromInfo = SUPPORTED_ASSETS[fromAsset];
|
|
687
|
-
const toInfo = SUPPORTED_ASSETS[toAsset];
|
|
688
|
-
const rawAmount = BigInt(Math.floor(amount * 10 ** fromInfo.decimals));
|
|
689
|
-
const poolPrice = await getPoolPrice(client);
|
|
690
|
-
try {
|
|
691
|
-
const sdk = getCetusSDK();
|
|
692
|
-
const pool = await sdk.Pool.getPool(CETUS_USDC_SUI_POOL);
|
|
693
|
-
const preSwapResult = await sdk.Swap.preSwap({
|
|
694
|
-
pool,
|
|
695
|
-
current_sqrt_price: pool.current_sqrt_price,
|
|
696
|
-
coin_type_a: pool.coin_type_a,
|
|
697
|
-
coin_type_b: pool.coin_type_b,
|
|
698
|
-
decimals_a: 6,
|
|
699
|
-
decimals_b: 9,
|
|
700
|
-
a2b,
|
|
701
|
-
by_amount_in: true,
|
|
702
|
-
amount: rawAmount.toString()
|
|
703
|
-
});
|
|
704
|
-
const expectedOutput = Number(preSwapResult.estimated_amount_out) / 10 ** toInfo.decimals;
|
|
705
|
-
return { expectedOutput, priceImpact: 0, poolPrice };
|
|
706
|
-
} catch {
|
|
707
|
-
let expectedOutput;
|
|
708
|
-
if (fromAsset === "USDC") {
|
|
709
|
-
expectedOutput = amount / poolPrice;
|
|
710
|
-
} else {
|
|
711
|
-
expectedOutput = amount * poolPrice;
|
|
712
|
-
}
|
|
713
|
-
return { expectedOutput, priceImpact: 0, poolPrice };
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
614
|
|
|
717
615
|
// src/protocols/yieldTracker.ts
|
|
718
616
|
async function getEarnings(client, keypair) {
|
|
@@ -924,6 +822,298 @@ async function attack(client, signer, sentinelId, prompt, feeMist) {
|
|
|
924
822
|
feePaid: Number(fee) / Number(MIST_PER_SUI)
|
|
925
823
|
};
|
|
926
824
|
}
|
|
825
|
+
|
|
826
|
+
// src/adapters/registry.ts
|
|
827
|
+
var ProtocolRegistry = class {
|
|
828
|
+
lending = /* @__PURE__ */ new Map();
|
|
829
|
+
swap = /* @__PURE__ */ new Map();
|
|
830
|
+
registerLending(adapter) {
|
|
831
|
+
this.lending.set(adapter.id, adapter);
|
|
832
|
+
}
|
|
833
|
+
registerSwap(adapter) {
|
|
834
|
+
this.swap.set(adapter.id, adapter);
|
|
835
|
+
}
|
|
836
|
+
async bestSaveRate(asset) {
|
|
837
|
+
const candidates = [];
|
|
838
|
+
for (const adapter of this.lending.values()) {
|
|
839
|
+
if (!adapter.supportedAssets.includes(asset)) continue;
|
|
840
|
+
if (!adapter.capabilities.includes("save")) continue;
|
|
841
|
+
try {
|
|
842
|
+
const rate = await adapter.getRates(asset);
|
|
843
|
+
candidates.push({ adapter, rate });
|
|
844
|
+
} catch {
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (candidates.length === 0) {
|
|
848
|
+
throw new Error(`No lending adapter supports saving ${asset}`);
|
|
849
|
+
}
|
|
850
|
+
candidates.sort((a, b) => b.rate.saveApy - a.rate.saveApy);
|
|
851
|
+
return candidates[0];
|
|
852
|
+
}
|
|
853
|
+
async bestBorrowRate(asset, opts) {
|
|
854
|
+
const candidates = [];
|
|
855
|
+
for (const adapter of this.lending.values()) {
|
|
856
|
+
if (!adapter.supportedAssets.includes(asset)) continue;
|
|
857
|
+
if (!adapter.capabilities.includes("borrow")) continue;
|
|
858
|
+
if (opts?.requireSameAssetBorrow && !adapter.supportsSameAssetBorrow) continue;
|
|
859
|
+
try {
|
|
860
|
+
const rate = await adapter.getRates(asset);
|
|
861
|
+
candidates.push({ adapter, rate });
|
|
862
|
+
} catch {
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (candidates.length === 0) {
|
|
866
|
+
throw new Error(`No lending adapter supports borrowing ${asset}`);
|
|
867
|
+
}
|
|
868
|
+
candidates.sort((a, b) => a.rate.borrowApy - b.rate.borrowApy);
|
|
869
|
+
return candidates[0];
|
|
870
|
+
}
|
|
871
|
+
async bestSwapQuote(from, to, amount) {
|
|
872
|
+
const candidates = [];
|
|
873
|
+
for (const adapter of this.swap.values()) {
|
|
874
|
+
const pairs = adapter.getSupportedPairs();
|
|
875
|
+
if (!pairs.some((p) => p.from === from && p.to === to)) continue;
|
|
876
|
+
try {
|
|
877
|
+
const quote = await adapter.getQuote(from, to, amount);
|
|
878
|
+
candidates.push({ adapter, quote });
|
|
879
|
+
} catch {
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
if (candidates.length === 0) {
|
|
883
|
+
throw new Error(`No swap adapter supports ${from} \u2192 ${to}`);
|
|
884
|
+
}
|
|
885
|
+
candidates.sort((a, b) => b.quote.expectedOutput - a.quote.expectedOutput);
|
|
886
|
+
return candidates[0];
|
|
887
|
+
}
|
|
888
|
+
async allRates(asset) {
|
|
889
|
+
const results = [];
|
|
890
|
+
for (const adapter of this.lending.values()) {
|
|
891
|
+
if (!adapter.supportedAssets.includes(asset)) continue;
|
|
892
|
+
try {
|
|
893
|
+
const rates = await adapter.getRates(asset);
|
|
894
|
+
results.push({ protocol: adapter.name, protocolId: adapter.id, rates });
|
|
895
|
+
} catch {
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return results;
|
|
899
|
+
}
|
|
900
|
+
async allPositions(address) {
|
|
901
|
+
const results = [];
|
|
902
|
+
for (const adapter of this.lending.values()) {
|
|
903
|
+
try {
|
|
904
|
+
const positions = await adapter.getPositions(address);
|
|
905
|
+
if (positions.supplies.length > 0 || positions.borrows.length > 0) {
|
|
906
|
+
results.push({ protocol: adapter.name, protocolId: adapter.id, positions });
|
|
907
|
+
}
|
|
908
|
+
} catch {
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
return results;
|
|
912
|
+
}
|
|
913
|
+
getLending(id) {
|
|
914
|
+
return this.lending.get(id);
|
|
915
|
+
}
|
|
916
|
+
getSwap(id) {
|
|
917
|
+
return this.swap.get(id);
|
|
918
|
+
}
|
|
919
|
+
listLending() {
|
|
920
|
+
return [...this.lending.values()];
|
|
921
|
+
}
|
|
922
|
+
listSwap() {
|
|
923
|
+
return [...this.swap.values()];
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
// src/adapters/navi.ts
|
|
928
|
+
var NaviAdapter = class {
|
|
929
|
+
id = "navi";
|
|
930
|
+
name = "NAVI Protocol";
|
|
931
|
+
version = "1.0.0";
|
|
932
|
+
capabilities = ["save", "withdraw", "borrow", "repay"];
|
|
933
|
+
supportedAssets = ["USDC"];
|
|
934
|
+
supportsSameAssetBorrow = true;
|
|
935
|
+
client;
|
|
936
|
+
async init(client) {
|
|
937
|
+
this.client = client;
|
|
938
|
+
}
|
|
939
|
+
initSync(client) {
|
|
940
|
+
this.client = client;
|
|
941
|
+
}
|
|
942
|
+
async getRates(asset) {
|
|
943
|
+
const rates = await getRates(this.client);
|
|
944
|
+
const key = asset.toUpperCase();
|
|
945
|
+
const r = rates[key];
|
|
946
|
+
if (!r) throw new Error(`NAVI does not support ${asset}`);
|
|
947
|
+
return { asset, saveApy: r.saveApy, borrowApy: r.borrowApy };
|
|
948
|
+
}
|
|
949
|
+
async getPositions(address) {
|
|
950
|
+
const result = await getPositions(this.client, address);
|
|
951
|
+
return {
|
|
952
|
+
supplies: result.positions.filter((p) => p.type === "save").map((p) => ({ asset: p.asset, amount: p.amount, apy: p.apy })),
|
|
953
|
+
borrows: result.positions.filter((p) => p.type === "borrow").map((p) => ({ asset: p.asset, amount: p.amount, apy: p.apy }))
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
async getHealth(address) {
|
|
957
|
+
return getHealthFactor(this.client, address);
|
|
958
|
+
}
|
|
959
|
+
async buildSaveTx(address, amount, _asset, options) {
|
|
960
|
+
const tx = await buildSaveTx(this.client, address, amount, options);
|
|
961
|
+
return { tx };
|
|
962
|
+
}
|
|
963
|
+
async buildWithdrawTx(address, amount, _asset) {
|
|
964
|
+
const result = await buildWithdrawTx(this.client, address, amount);
|
|
965
|
+
return { tx: result.tx, effectiveAmount: result.effectiveAmount };
|
|
966
|
+
}
|
|
967
|
+
async buildBorrowTx(address, amount, _asset, options) {
|
|
968
|
+
const tx = await buildBorrowTx(this.client, address, amount, options);
|
|
969
|
+
return { tx };
|
|
970
|
+
}
|
|
971
|
+
async buildRepayTx(address, amount, _asset) {
|
|
972
|
+
const tx = await buildRepayTx(this.client, address, amount);
|
|
973
|
+
return { tx };
|
|
974
|
+
}
|
|
975
|
+
async maxWithdraw(address, _asset) {
|
|
976
|
+
return maxWithdrawAmount(this.client, address);
|
|
977
|
+
}
|
|
978
|
+
async maxBorrow(address, _asset) {
|
|
979
|
+
return maxBorrowAmount(this.client, address);
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
var DEFAULT_SLIPPAGE_BPS = 300;
|
|
983
|
+
function createAggregatorClient(client, signer) {
|
|
984
|
+
return new AggregatorClient({
|
|
985
|
+
client,
|
|
986
|
+
signer,
|
|
987
|
+
env: Env.Mainnet
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
async function buildSwapTx(params) {
|
|
991
|
+
const { client, address, fromAsset, toAsset, amount, maxSlippageBps = DEFAULT_SLIPPAGE_BPS } = params;
|
|
992
|
+
const fromInfo = SUPPORTED_ASSETS[fromAsset];
|
|
993
|
+
const toInfo = SUPPORTED_ASSETS[toAsset];
|
|
994
|
+
const rawAmount = BigInt(Math.floor(amount * 10 ** fromInfo.decimals));
|
|
995
|
+
const aggClient = createAggregatorClient(client, address);
|
|
996
|
+
const result = await aggClient.findRouters({
|
|
997
|
+
from: fromInfo.type,
|
|
998
|
+
target: toInfo.type,
|
|
999
|
+
amount: rawAmount,
|
|
1000
|
+
byAmountIn: true
|
|
1001
|
+
});
|
|
1002
|
+
if (!result || result.insufficientLiquidity) {
|
|
1003
|
+
throw new T2000Error(
|
|
1004
|
+
"ASSET_NOT_SUPPORTED",
|
|
1005
|
+
`No swap route found for ${fromAsset} \u2192 ${toAsset}`
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
const tx = new Transaction();
|
|
1009
|
+
const slippage = maxSlippageBps / 1e4;
|
|
1010
|
+
await aggClient.fastRouterSwap({
|
|
1011
|
+
router: result,
|
|
1012
|
+
txb: tx,
|
|
1013
|
+
slippage
|
|
1014
|
+
});
|
|
1015
|
+
const estimatedOut = Number(result.amountOut.toString());
|
|
1016
|
+
return {
|
|
1017
|
+
tx,
|
|
1018
|
+
estimatedOut,
|
|
1019
|
+
toDecimals: toInfo.decimals
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
async function getPoolPrice(client) {
|
|
1023
|
+
try {
|
|
1024
|
+
const pool = await client.getObject({
|
|
1025
|
+
id: CETUS_USDC_SUI_POOL,
|
|
1026
|
+
options: { showContent: true }
|
|
1027
|
+
});
|
|
1028
|
+
if (pool.data?.content?.dataType === "moveObject") {
|
|
1029
|
+
const fields = pool.data.content.fields;
|
|
1030
|
+
const currentSqrtPrice = BigInt(String(fields.current_sqrt_price ?? "0"));
|
|
1031
|
+
if (currentSqrtPrice > 0n) {
|
|
1032
|
+
const Q64 = 2n ** 64n;
|
|
1033
|
+
const sqrtPriceFloat = Number(currentSqrtPrice) / Number(Q64);
|
|
1034
|
+
const rawPrice = sqrtPriceFloat * sqrtPriceFloat;
|
|
1035
|
+
const suiPriceUsd = 1e3 / rawPrice;
|
|
1036
|
+
if (suiPriceUsd > 0.01 && suiPriceUsd < 1e3) return suiPriceUsd;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
} catch {
|
|
1040
|
+
}
|
|
1041
|
+
return 3.5;
|
|
1042
|
+
}
|
|
1043
|
+
async function getSwapQuote(client, fromAsset, toAsset, amount) {
|
|
1044
|
+
const fromInfo = SUPPORTED_ASSETS[fromAsset];
|
|
1045
|
+
const toInfo = SUPPORTED_ASSETS[toAsset];
|
|
1046
|
+
const rawAmount = BigInt(Math.floor(amount * 10 ** fromInfo.decimals));
|
|
1047
|
+
const poolPrice = await getPoolPrice(client);
|
|
1048
|
+
try {
|
|
1049
|
+
const aggClient = createAggregatorClient(client);
|
|
1050
|
+
const result = await aggClient.findRouters({
|
|
1051
|
+
from: fromInfo.type,
|
|
1052
|
+
target: toInfo.type,
|
|
1053
|
+
amount: rawAmount,
|
|
1054
|
+
byAmountIn: true
|
|
1055
|
+
});
|
|
1056
|
+
if (!result || result.insufficientLiquidity) {
|
|
1057
|
+
return fallbackQuote(fromAsset, amount, poolPrice);
|
|
1058
|
+
}
|
|
1059
|
+
const expectedOutput = Number(result.amountOut.toString()) / 10 ** toInfo.decimals;
|
|
1060
|
+
const priceImpact = result.deviationRatio ?? 0;
|
|
1061
|
+
return { expectedOutput, priceImpact, poolPrice };
|
|
1062
|
+
} catch {
|
|
1063
|
+
return fallbackQuote(fromAsset, amount, poolPrice);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
function fallbackQuote(fromAsset, amount, poolPrice) {
|
|
1067
|
+
const expectedOutput = fromAsset === "USDC" ? amount / poolPrice : amount * poolPrice;
|
|
1068
|
+
return { expectedOutput, priceImpact: 0, poolPrice };
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// src/adapters/cetus.ts
|
|
1072
|
+
var CetusAdapter = class {
|
|
1073
|
+
id = "cetus";
|
|
1074
|
+
name = "Cetus";
|
|
1075
|
+
version = "1.0.0";
|
|
1076
|
+
capabilities = ["swap"];
|
|
1077
|
+
client;
|
|
1078
|
+
async init(client) {
|
|
1079
|
+
this.client = client;
|
|
1080
|
+
}
|
|
1081
|
+
initSync(client) {
|
|
1082
|
+
this.client = client;
|
|
1083
|
+
}
|
|
1084
|
+
async getQuote(from, to, amount) {
|
|
1085
|
+
return getSwapQuote(
|
|
1086
|
+
this.client,
|
|
1087
|
+
from.toUpperCase(),
|
|
1088
|
+
to.toUpperCase(),
|
|
1089
|
+
amount
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
async buildSwapTx(address, from, to, amount, maxSlippageBps) {
|
|
1093
|
+
const result = await buildSwapTx({
|
|
1094
|
+
client: this.client,
|
|
1095
|
+
address,
|
|
1096
|
+
fromAsset: from.toUpperCase(),
|
|
1097
|
+
toAsset: to.toUpperCase(),
|
|
1098
|
+
amount,
|
|
1099
|
+
maxSlippageBps
|
|
1100
|
+
});
|
|
1101
|
+
return {
|
|
1102
|
+
tx: result.tx,
|
|
1103
|
+
estimatedOut: result.estimatedOut,
|
|
1104
|
+
toDecimals: result.toDecimals
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
getSupportedPairs() {
|
|
1108
|
+
return [
|
|
1109
|
+
{ from: "USDC", to: "SUI" },
|
|
1110
|
+
{ from: "SUI", to: "USDC" }
|
|
1111
|
+
];
|
|
1112
|
+
}
|
|
1113
|
+
async getPoolPrice() {
|
|
1114
|
+
return getPoolPrice(this.client);
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
927
1117
|
function hasLeadingZeroBits(hash, bits) {
|
|
928
1118
|
const fullBytes = Math.floor(bits / 8);
|
|
929
1119
|
const remainingBits = bits % 8;
|
|
@@ -1156,11 +1346,23 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
1156
1346
|
keypair;
|
|
1157
1347
|
client;
|
|
1158
1348
|
_address;
|
|
1159
|
-
|
|
1349
|
+
registry;
|
|
1350
|
+
constructor(keypair, client, registry) {
|
|
1160
1351
|
super();
|
|
1161
1352
|
this.keypair = keypair;
|
|
1162
1353
|
this.client = client;
|
|
1163
1354
|
this._address = getAddress(keypair);
|
|
1355
|
+
this.registry = registry ?? _T2000.createDefaultRegistry(client);
|
|
1356
|
+
}
|
|
1357
|
+
static createDefaultRegistry(client) {
|
|
1358
|
+
const registry = new ProtocolRegistry();
|
|
1359
|
+
const naviAdapter = new NaviAdapter();
|
|
1360
|
+
naviAdapter.initSync(client);
|
|
1361
|
+
registry.registerLending(naviAdapter);
|
|
1362
|
+
const cetusAdapter = new CetusAdapter();
|
|
1363
|
+
cetusAdapter.initSync(client);
|
|
1364
|
+
registry.registerSwap(cetusAdapter);
|
|
1365
|
+
return registry;
|
|
1164
1366
|
}
|
|
1165
1367
|
static async create(options = {}) {
|
|
1166
1368
|
const { keyPath, pin, passphrase, network = DEFAULT_NETWORK, rpcUrl, sponsored, name } = options;
|
|
@@ -1281,6 +1483,11 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
1281
1483
|
exportKey() {
|
|
1282
1484
|
return exportPrivateKey(this.keypair);
|
|
1283
1485
|
}
|
|
1486
|
+
async registerAdapter(adapter) {
|
|
1487
|
+
await adapter.init(this.client);
|
|
1488
|
+
if ("buildSaveTx" in adapter) this.registry.registerLending(adapter);
|
|
1489
|
+
if ("buildSwapTx" in adapter) this.registry.registerSwap(adapter);
|
|
1490
|
+
}
|
|
1284
1491
|
// -- Savings --
|
|
1285
1492
|
async save(params) {
|
|
1286
1493
|
const asset = (params.asset ?? "USDC").toUpperCase();
|
|
@@ -1303,12 +1510,12 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
1303
1510
|
}
|
|
1304
1511
|
const fee = calculateFee("save", amount);
|
|
1305
1512
|
const saveAmount = amount;
|
|
1306
|
-
const
|
|
1307
|
-
|
|
1308
|
-
this.
|
|
1309
|
-
|
|
1310
|
-
);
|
|
1311
|
-
const rates = await getRates(
|
|
1513
|
+
const adapter = await this.resolveLending(params.protocol, asset, "save");
|
|
1514
|
+
const gasResult = await executeWithGas(this.client, this.keypair, async () => {
|
|
1515
|
+
const { tx } = await adapter.buildSaveTx(this._address, saveAmount, asset, { collectFee: true });
|
|
1516
|
+
return tx;
|
|
1517
|
+
});
|
|
1518
|
+
const rates = await adapter.getRates(asset);
|
|
1312
1519
|
reportFee(this._address, "save", fee.amount, fee.rate, gasResult.digest);
|
|
1313
1520
|
this.emitBalanceChange("USDC", saveAmount, "save", gasResult.digest);
|
|
1314
1521
|
let savingsBalance = saveAmount;
|
|
@@ -1321,7 +1528,7 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
1321
1528
|
success: true,
|
|
1322
1529
|
tx: gasResult.digest,
|
|
1323
1530
|
amount: saveAmount,
|
|
1324
|
-
apy: rates.
|
|
1531
|
+
apy: rates.saveApy,
|
|
1325
1532
|
fee: fee.amount,
|
|
1326
1533
|
gasCost: gasResult.gasCostSui,
|
|
1327
1534
|
gasMethod: gasResult.gasMethod,
|
|
@@ -1333,18 +1540,19 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
1333
1540
|
if (asset !== "USDC") {
|
|
1334
1541
|
throw new T2000Error("ASSET_NOT_SUPPORTED", `Only USDC is supported for withdraw. Got: ${asset}`);
|
|
1335
1542
|
}
|
|
1543
|
+
const adapter = await this.resolveLending(params.protocol, asset, "withdraw");
|
|
1336
1544
|
let amount;
|
|
1337
1545
|
if (params.amount === "all") {
|
|
1338
|
-
const maxResult = await
|
|
1546
|
+
const maxResult = await adapter.maxWithdraw(this._address, asset);
|
|
1339
1547
|
amount = maxResult.maxAmount;
|
|
1340
1548
|
if (amount <= 0) {
|
|
1341
1549
|
throw new T2000Error("NO_COLLATERAL", "No savings to withdraw");
|
|
1342
1550
|
}
|
|
1343
1551
|
} else {
|
|
1344
1552
|
amount = params.amount;
|
|
1345
|
-
const hf = await this.
|
|
1553
|
+
const hf = await adapter.getHealth(this._address);
|
|
1346
1554
|
if (hf.borrowed > 0) {
|
|
1347
|
-
const maxResult = await
|
|
1555
|
+
const maxResult = await adapter.maxWithdraw(this._address, asset);
|
|
1348
1556
|
if (amount > maxResult.maxAmount) {
|
|
1349
1557
|
throw new T2000Error(
|
|
1350
1558
|
"WITHDRAW_WOULD_LIQUIDATE",
|
|
@@ -1361,7 +1569,7 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
1361
1569
|
const withdrawAmount = amount;
|
|
1362
1570
|
let effectiveAmount = withdrawAmount;
|
|
1363
1571
|
const gasResult = await executeWithGas(this.client, this.keypair, async () => {
|
|
1364
|
-
const built = await buildWithdrawTx(this.
|
|
1572
|
+
const built = await adapter.buildWithdrawTx(this._address, withdrawAmount, asset);
|
|
1365
1573
|
effectiveAmount = built.effectiveAmount;
|
|
1366
1574
|
return built.tx;
|
|
1367
1575
|
});
|
|
@@ -1375,7 +1583,8 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
1375
1583
|
};
|
|
1376
1584
|
}
|
|
1377
1585
|
async maxWithdraw() {
|
|
1378
|
-
|
|
1586
|
+
const adapter = await this.resolveLending(void 0, "USDC", "withdraw");
|
|
1587
|
+
return adapter.maxWithdraw(this._address, "USDC");
|
|
1379
1588
|
}
|
|
1380
1589
|
// -- Borrowing --
|
|
1381
1590
|
async borrow(params) {
|
|
@@ -1383,7 +1592,8 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
1383
1592
|
if (asset !== "USDC") {
|
|
1384
1593
|
throw new T2000Error("ASSET_NOT_SUPPORTED", `Only USDC is supported for borrow. Got: ${asset}`);
|
|
1385
1594
|
}
|
|
1386
|
-
const
|
|
1595
|
+
const adapter = await this.resolveLending(params.protocol, asset, "borrow");
|
|
1596
|
+
const maxResult = await adapter.maxBorrow(this._address, asset);
|
|
1387
1597
|
if (params.amount > maxResult.maxAmount) {
|
|
1388
1598
|
throw new T2000Error("HEALTH_FACTOR_TOO_LOW", `Max safe borrow: $${maxResult.maxAmount.toFixed(2)}`, {
|
|
1389
1599
|
maxBorrow: maxResult.maxAmount,
|
|
@@ -1392,12 +1602,11 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
1392
1602
|
}
|
|
1393
1603
|
const fee = calculateFee("borrow", params.amount);
|
|
1394
1604
|
const borrowAmount = params.amount;
|
|
1395
|
-
const gasResult = await executeWithGas(
|
|
1396
|
-
this.
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
);
|
|
1400
|
-
const hf = await getHealthFactor(this.client, this.keypair);
|
|
1605
|
+
const gasResult = await executeWithGas(this.client, this.keypair, async () => {
|
|
1606
|
+
const { tx } = await adapter.buildBorrowTx(this._address, borrowAmount, asset, { collectFee: true });
|
|
1607
|
+
return tx;
|
|
1608
|
+
});
|
|
1609
|
+
const hf = await adapter.getHealth(this._address);
|
|
1401
1610
|
reportFee(this._address, "borrow", fee.amount, fee.rate, gasResult.digest);
|
|
1402
1611
|
this.emitBalanceChange("USDC", borrowAmount, "borrow", gasResult.digest);
|
|
1403
1612
|
return {
|
|
@@ -1415,9 +1624,10 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
1415
1624
|
if (asset !== "USDC") {
|
|
1416
1625
|
throw new T2000Error("ASSET_NOT_SUPPORTED", `Only USDC is supported for repay. Got: ${asset}`);
|
|
1417
1626
|
}
|
|
1627
|
+
const adapter = await this.resolveLending(params.protocol, asset, "repay");
|
|
1418
1628
|
let amount;
|
|
1419
1629
|
if (params.amount === "all") {
|
|
1420
|
-
const hf2 = await this.
|
|
1630
|
+
const hf2 = await adapter.getHealth(this._address);
|
|
1421
1631
|
amount = hf2.borrowed;
|
|
1422
1632
|
if (amount <= 0) {
|
|
1423
1633
|
throw new T2000Error("NO_COLLATERAL", "No outstanding borrow to repay");
|
|
@@ -1426,12 +1636,11 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
1426
1636
|
amount = params.amount;
|
|
1427
1637
|
}
|
|
1428
1638
|
const repayAmount = amount;
|
|
1429
|
-
const gasResult = await executeWithGas(
|
|
1430
|
-
this.
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
);
|
|
1434
|
-
const hf = await getHealthFactor(this.client, this.keypair);
|
|
1639
|
+
const gasResult = await executeWithGas(this.client, this.keypair, async () => {
|
|
1640
|
+
const { tx } = await adapter.buildRepayTx(this._address, repayAmount, asset);
|
|
1641
|
+
return tx;
|
|
1642
|
+
});
|
|
1643
|
+
const hf = await adapter.getHealth(this._address);
|
|
1435
1644
|
this.emitBalanceChange("USDC", repayAmount, "repay", gasResult.digest);
|
|
1436
1645
|
return {
|
|
1437
1646
|
success: true,
|
|
@@ -1443,10 +1652,12 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
1443
1652
|
};
|
|
1444
1653
|
}
|
|
1445
1654
|
async maxBorrow() {
|
|
1446
|
-
|
|
1655
|
+
const adapter = await this.resolveLending(void 0, "USDC", "borrow");
|
|
1656
|
+
return adapter.maxBorrow(this._address, "USDC");
|
|
1447
1657
|
}
|
|
1448
1658
|
async healthFactor() {
|
|
1449
|
-
const
|
|
1659
|
+
const adapter = await this.resolveLending(void 0, "USDC", "save");
|
|
1660
|
+
const hf = await adapter.getHealth(this._address);
|
|
1450
1661
|
if (hf.healthFactor < 1.2) {
|
|
1451
1662
|
this.emit("healthCritical", { healthFactor: hf.healthFactor, threshold: 1.5, severity: "critical" });
|
|
1452
1663
|
} else if (hf.healthFactor < 2) {
|
|
@@ -1464,19 +1675,21 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
1464
1675
|
if (fromAsset === toAsset) {
|
|
1465
1676
|
throw new T2000Error("INVALID_AMOUNT", "Cannot swap same asset");
|
|
1466
1677
|
}
|
|
1678
|
+
let adapter;
|
|
1679
|
+
if (params.protocol) {
|
|
1680
|
+
const found = this.registry.getSwap(params.protocol);
|
|
1681
|
+
if (!found) throw new T2000Error("ASSET_NOT_SUPPORTED", `Swap adapter '${params.protocol}' not found`);
|
|
1682
|
+
adapter = found;
|
|
1683
|
+
} else {
|
|
1684
|
+
const best = await this.registry.bestSwapQuote(fromAsset, toAsset, params.amount);
|
|
1685
|
+
adapter = best.adapter;
|
|
1686
|
+
}
|
|
1467
1687
|
const fee = calculateFee("swap", params.amount);
|
|
1468
1688
|
const swapAmount = params.amount;
|
|
1469
1689
|
const slippageBps = params.maxSlippage ? params.maxSlippage * 100 : void 0;
|
|
1470
1690
|
let swapMeta = { estimatedOut: 0, toDecimals: 0 };
|
|
1471
1691
|
const gasResult = await executeWithGas(this.client, this.keypair, async () => {
|
|
1472
|
-
const built = await buildSwapTx(
|
|
1473
|
-
client: this.client,
|
|
1474
|
-
address: this._address,
|
|
1475
|
-
fromAsset,
|
|
1476
|
-
toAsset,
|
|
1477
|
-
amount: swapAmount,
|
|
1478
|
-
maxSlippageBps: slippageBps
|
|
1479
|
-
});
|
|
1692
|
+
const built = await adapter.buildSwapTx(this._address, fromAsset, toAsset, swapAmount, slippageBps);
|
|
1480
1693
|
swapMeta = { estimatedOut: built.estimatedOut, toDecimals: built.toDecimals };
|
|
1481
1694
|
return built.tx;
|
|
1482
1695
|
});
|
|
@@ -1515,17 +1728,39 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
1515
1728
|
async swapQuote(params) {
|
|
1516
1729
|
const fromAsset = params.from.toUpperCase();
|
|
1517
1730
|
const toAsset = params.to.toUpperCase();
|
|
1518
|
-
const
|
|
1731
|
+
const best = await this.registry.bestSwapQuote(fromAsset, toAsset, params.amount);
|
|
1519
1732
|
const fee = calculateFee("swap", params.amount);
|
|
1520
|
-
return { ...quote, fee: { amount: fee.amount, rate: fee.rate } };
|
|
1733
|
+
return { ...best.quote, fee: { amount: fee.amount, rate: fee.rate } };
|
|
1521
1734
|
}
|
|
1522
1735
|
// -- Info --
|
|
1523
1736
|
async positions() {
|
|
1524
|
-
|
|
1737
|
+
const allPositions = await this.registry.allPositions(this._address);
|
|
1738
|
+
const positions = allPositions.flatMap(
|
|
1739
|
+
(p) => [
|
|
1740
|
+
...p.positions.supplies.map((s) => ({
|
|
1741
|
+
protocol: p.protocolId,
|
|
1742
|
+
asset: s.asset,
|
|
1743
|
+
type: "save",
|
|
1744
|
+
amount: s.amount,
|
|
1745
|
+
apy: s.apy
|
|
1746
|
+
})),
|
|
1747
|
+
...p.positions.borrows.map((b) => ({
|
|
1748
|
+
protocol: p.protocolId,
|
|
1749
|
+
asset: b.asset,
|
|
1750
|
+
type: "borrow",
|
|
1751
|
+
amount: b.amount,
|
|
1752
|
+
apy: b.apy
|
|
1753
|
+
}))
|
|
1754
|
+
]
|
|
1755
|
+
);
|
|
1756
|
+
return { positions };
|
|
1525
1757
|
}
|
|
1526
1758
|
async rates() {
|
|
1527
1759
|
return getRates(this.client);
|
|
1528
1760
|
}
|
|
1761
|
+
async allRates(asset = "USDC") {
|
|
1762
|
+
return this.registry.allRates(asset);
|
|
1763
|
+
}
|
|
1529
1764
|
async earnings() {
|
|
1530
1765
|
const result = await getEarnings(this.client, this.keypair);
|
|
1531
1766
|
if (result.totalYieldEarned > 0) {
|
|
@@ -1552,6 +1787,29 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
1552
1787
|
return attack(this.client, this.keypair, id, prompt, fee);
|
|
1553
1788
|
}
|
|
1554
1789
|
// -- Helpers --
|
|
1790
|
+
async resolveLending(protocol, asset, capability) {
|
|
1791
|
+
if (protocol) {
|
|
1792
|
+
const adapter = this.registry.getLending(protocol);
|
|
1793
|
+
if (!adapter) throw new T2000Error("ASSET_NOT_SUPPORTED", `Lending adapter '${protocol}' not found`);
|
|
1794
|
+
return adapter;
|
|
1795
|
+
}
|
|
1796
|
+
if (capability === "save") {
|
|
1797
|
+
const { adapter } = await this.registry.bestSaveRate(asset);
|
|
1798
|
+
return adapter;
|
|
1799
|
+
}
|
|
1800
|
+
if (capability === "borrow" || capability === "repay") {
|
|
1801
|
+
const adapters2 = this.registry.listLending().filter(
|
|
1802
|
+
(a) => a.supportedAssets.includes(asset) && a.capabilities.includes(capability) && (capability !== "borrow" || a.supportsSameAssetBorrow)
|
|
1803
|
+
);
|
|
1804
|
+
if (adapters2.length === 0) throw new T2000Error("ASSET_NOT_SUPPORTED", `No adapter supports ${capability} ${asset}`);
|
|
1805
|
+
return adapters2[0];
|
|
1806
|
+
}
|
|
1807
|
+
const adapters = this.registry.listLending().filter(
|
|
1808
|
+
(a) => a.supportedAssets.includes(asset) && a.capabilities.includes(capability)
|
|
1809
|
+
);
|
|
1810
|
+
if (adapters.length === 0) throw new T2000Error("ASSET_NOT_SUPPORTED", `No adapter supports ${capability} ${asset}`);
|
|
1811
|
+
return adapters[0];
|
|
1812
|
+
}
|
|
1555
1813
|
emitBalanceChange(asset, amount, cause, tx) {
|
|
1556
1814
|
this.emit("balanceChange", { asset, previous: 0, current: 0, cause, tx });
|
|
1557
1815
|
}
|
|
@@ -1649,7 +1907,297 @@ function parseMoveAbort(errorStr) {
|
|
|
1649
1907
|
}
|
|
1650
1908
|
return { reason: errorStr };
|
|
1651
1909
|
}
|
|
1910
|
+
var USDC_TYPE2 = SUPPORTED_ASSETS.USDC.type;
|
|
1911
|
+
SUPPORTED_ASSETS.USDC.decimals;
|
|
1912
|
+
var WAD = 1e18;
|
|
1913
|
+
var MIN_HEALTH_FACTOR2 = 1.5;
|
|
1914
|
+
function interpolateRate(utilBreakpoints, aprBreakpoints, utilizationPct) {
|
|
1915
|
+
if (utilBreakpoints.length === 0) return 0;
|
|
1916
|
+
if (utilizationPct <= utilBreakpoints[0]) return aprBreakpoints[0];
|
|
1917
|
+
if (utilizationPct >= utilBreakpoints[utilBreakpoints.length - 1]) {
|
|
1918
|
+
return aprBreakpoints[aprBreakpoints.length - 1];
|
|
1919
|
+
}
|
|
1920
|
+
for (let i = 1; i < utilBreakpoints.length; i++) {
|
|
1921
|
+
if (utilizationPct <= utilBreakpoints[i]) {
|
|
1922
|
+
const t = (utilizationPct - utilBreakpoints[i - 1]) / (utilBreakpoints[i] - utilBreakpoints[i - 1]);
|
|
1923
|
+
return aprBreakpoints[i - 1] + t * (aprBreakpoints[i] - aprBreakpoints[i - 1]);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
return aprBreakpoints[aprBreakpoints.length - 1];
|
|
1927
|
+
}
|
|
1928
|
+
function computeRatesFromReserve(reserve) {
|
|
1929
|
+
const decimals = reserve.mintDecimals;
|
|
1930
|
+
const available = Number(reserve.availableAmount) / 10 ** decimals;
|
|
1931
|
+
const borrowed = Number(reserve.borrowedAmount.value) / WAD / 10 ** decimals;
|
|
1932
|
+
const totalDeposited = available + borrowed;
|
|
1933
|
+
const utilizationPct = totalDeposited > 0 ? borrowed / totalDeposited * 100 : 0;
|
|
1934
|
+
const config = reserve.config.element;
|
|
1935
|
+
if (!config) return { borrowAprPct: 0, depositAprPct: 0, utilizationPct: 0 };
|
|
1936
|
+
const utils = config.interestRateUtils.map(Number);
|
|
1937
|
+
const aprs = config.interestRateAprs.map((a) => Number(a) / 100);
|
|
1938
|
+
const borrowAprPct = interpolateRate(utils, aprs, utilizationPct);
|
|
1939
|
+
const spreadFeeBps = Number(config.spreadFeeBps);
|
|
1940
|
+
const depositAprPct = utilizationPct / 100 * (borrowAprPct / 100) * (1 - spreadFeeBps / 1e4) * 100;
|
|
1941
|
+
return { borrowAprPct, depositAprPct, utilizationPct };
|
|
1942
|
+
}
|
|
1943
|
+
function cTokenRatio(reserve) {
|
|
1944
|
+
if (reserve.ctokenSupply === 0n) return 1;
|
|
1945
|
+
const available = Number(reserve.availableAmount);
|
|
1946
|
+
const borrowed = Number(reserve.borrowedAmount.value) / WAD;
|
|
1947
|
+
const spreadFees = Number(reserve.unclaimedSpreadFees.value) / WAD;
|
|
1948
|
+
const totalSupply = available + borrowed - spreadFees;
|
|
1949
|
+
return totalSupply / Number(reserve.ctokenSupply);
|
|
1950
|
+
}
|
|
1951
|
+
var SuilendAdapter = class {
|
|
1952
|
+
id = "suilend";
|
|
1953
|
+
name = "Suilend";
|
|
1954
|
+
version = "1.0.0";
|
|
1955
|
+
capabilities = ["save", "withdraw"];
|
|
1956
|
+
supportedAssets = ["USDC"];
|
|
1957
|
+
supportsSameAssetBorrow = false;
|
|
1958
|
+
client;
|
|
1959
|
+
suilend;
|
|
1960
|
+
lendingMarketType;
|
|
1961
|
+
initialized = false;
|
|
1962
|
+
async init(client) {
|
|
1963
|
+
let sdk;
|
|
1964
|
+
try {
|
|
1965
|
+
sdk = await import('@suilend/sdk');
|
|
1966
|
+
} catch {
|
|
1967
|
+
throw new T2000Error(
|
|
1968
|
+
"PROTOCOL_UNAVAILABLE",
|
|
1969
|
+
"Suilend SDK not installed. Run: npm install @suilend/sdk@^1"
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
this.client = client;
|
|
1973
|
+
this.lendingMarketType = sdk.LENDING_MARKET_TYPE;
|
|
1974
|
+
try {
|
|
1975
|
+
this.suilend = await sdk.SuilendClient.initialize(
|
|
1976
|
+
sdk.LENDING_MARKET_ID,
|
|
1977
|
+
sdk.LENDING_MARKET_TYPE,
|
|
1978
|
+
client
|
|
1979
|
+
);
|
|
1980
|
+
} catch (err) {
|
|
1981
|
+
throw new T2000Error(
|
|
1982
|
+
"PROTOCOL_UNAVAILABLE",
|
|
1983
|
+
`Failed to initialize Suilend: ${err instanceof Error ? err.message : String(err)}`
|
|
1984
|
+
);
|
|
1985
|
+
}
|
|
1986
|
+
this.initialized = true;
|
|
1987
|
+
}
|
|
1988
|
+
ensureInit() {
|
|
1989
|
+
if (!this.initialized) {
|
|
1990
|
+
throw new T2000Error(
|
|
1991
|
+
"PROTOCOL_UNAVAILABLE",
|
|
1992
|
+
"SuilendAdapter not initialized. Call init() first."
|
|
1993
|
+
);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
findReserve(asset) {
|
|
1997
|
+
const upper = asset.toUpperCase();
|
|
1998
|
+
let coinType;
|
|
1999
|
+
if (upper === "USDC") coinType = USDC_TYPE2;
|
|
2000
|
+
else if (upper === "SUI") coinType = "0x2::sui::SUI";
|
|
2001
|
+
else if (asset.includes("::")) coinType = asset;
|
|
2002
|
+
else return void 0;
|
|
2003
|
+
try {
|
|
2004
|
+
const normalized = normalizeStructTag(coinType);
|
|
2005
|
+
return this.suilend.lendingMarket.reserves.find(
|
|
2006
|
+
(r) => normalizeStructTag(r.coinType.name) === normalized
|
|
2007
|
+
);
|
|
2008
|
+
} catch {
|
|
2009
|
+
return void 0;
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
async getObligationCaps(address) {
|
|
2013
|
+
const SuilendClientStatic = (await import('@suilend/sdk')).SuilendClient;
|
|
2014
|
+
return SuilendClientStatic.getObligationOwnerCaps(
|
|
2015
|
+
address,
|
|
2016
|
+
[this.lendingMarketType],
|
|
2017
|
+
this.client
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
resolveSymbol(coinType) {
|
|
2021
|
+
const normalized = normalizeStructTag(coinType);
|
|
2022
|
+
if (normalized === normalizeStructTag(USDC_TYPE2)) return "USDC";
|
|
2023
|
+
if (normalized === normalizeStructTag("0x2::sui::SUI")) return "SUI";
|
|
2024
|
+
const parts = coinType.split("::");
|
|
2025
|
+
return parts[parts.length - 1] || "UNKNOWN";
|
|
2026
|
+
}
|
|
2027
|
+
async getRates(asset) {
|
|
2028
|
+
this.ensureInit();
|
|
2029
|
+
const reserve = this.findReserve(asset);
|
|
2030
|
+
if (!reserve) {
|
|
2031
|
+
throw new T2000Error("ASSET_NOT_SUPPORTED", `Suilend does not support ${asset}`);
|
|
2032
|
+
}
|
|
2033
|
+
const { borrowAprPct, depositAprPct } = computeRatesFromReserve(reserve);
|
|
2034
|
+
return {
|
|
2035
|
+
asset,
|
|
2036
|
+
saveApy: depositAprPct,
|
|
2037
|
+
borrowApy: borrowAprPct
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
2040
|
+
async getPositions(address) {
|
|
2041
|
+
this.ensureInit();
|
|
2042
|
+
const supplies = [];
|
|
2043
|
+
const borrows = [];
|
|
2044
|
+
const caps = await this.getObligationCaps(address);
|
|
2045
|
+
if (caps.length === 0) return { supplies, borrows };
|
|
2046
|
+
const obligation = await this.suilend.getObligation(caps[0].obligationId);
|
|
2047
|
+
for (const deposit of obligation.deposits) {
|
|
2048
|
+
const coinType = normalizeStructTag(deposit.coinType.name);
|
|
2049
|
+
const reserve = this.suilend.lendingMarket.reserves.find(
|
|
2050
|
+
(r) => normalizeStructTag(r.coinType.name) === coinType
|
|
2051
|
+
);
|
|
2052
|
+
if (!reserve) continue;
|
|
2053
|
+
const ctokenAmount = Number(deposit.depositedCtokenAmount.toString());
|
|
2054
|
+
const ratio = cTokenRatio(reserve);
|
|
2055
|
+
const amount = ctokenAmount * ratio / 10 ** reserve.mintDecimals;
|
|
2056
|
+
const { depositAprPct } = computeRatesFromReserve(reserve);
|
|
2057
|
+
supplies.push({ asset: this.resolveSymbol(coinType), amount, apy: depositAprPct });
|
|
2058
|
+
}
|
|
2059
|
+
for (const borrow of obligation.borrows) {
|
|
2060
|
+
const coinType = normalizeStructTag(borrow.coinType.name);
|
|
2061
|
+
const reserve = this.suilend.lendingMarket.reserves.find(
|
|
2062
|
+
(r) => normalizeStructTag(r.coinType.name) === coinType
|
|
2063
|
+
);
|
|
2064
|
+
if (!reserve) continue;
|
|
2065
|
+
const rawBorrowed = Number(borrow.borrowedAmount.value.toString()) / WAD;
|
|
2066
|
+
const amount = rawBorrowed / 10 ** reserve.mintDecimals;
|
|
2067
|
+
const reserveRate = Number(reserve.cumulativeBorrowRate.value.toString()) / WAD;
|
|
2068
|
+
const posRate = Number(borrow.cumulativeBorrowRate.value.toString()) / WAD;
|
|
2069
|
+
const compounded = posRate > 0 ? amount * (reserveRate / posRate) : amount;
|
|
2070
|
+
const { borrowAprPct } = computeRatesFromReserve(reserve);
|
|
2071
|
+
borrows.push({ asset: this.resolveSymbol(coinType), amount: compounded, apy: borrowAprPct });
|
|
2072
|
+
}
|
|
2073
|
+
return { supplies, borrows };
|
|
2074
|
+
}
|
|
2075
|
+
async getHealth(address) {
|
|
2076
|
+
this.ensureInit();
|
|
2077
|
+
const caps = await this.getObligationCaps(address);
|
|
2078
|
+
if (caps.length === 0) {
|
|
2079
|
+
return { healthFactor: Infinity, supplied: 0, borrowed: 0, maxBorrow: 0, liquidationThreshold: 0 };
|
|
2080
|
+
}
|
|
2081
|
+
const positions = await this.getPositions(address);
|
|
2082
|
+
const supplied = positions.supplies.reduce((s, p) => s + p.amount, 0);
|
|
2083
|
+
const borrowed = positions.borrows.reduce((s, p) => s + p.amount, 0);
|
|
2084
|
+
const reserve = this.findReserve("USDC");
|
|
2085
|
+
const closeLtv = reserve?.config?.element?.closeLtvPct ?? 75;
|
|
2086
|
+
const openLtv = reserve?.config?.element?.openLtvPct ?? 70;
|
|
2087
|
+
const liqThreshold = closeLtv / 100;
|
|
2088
|
+
const healthFactor = borrowed > 0 ? supplied * liqThreshold / borrowed : Infinity;
|
|
2089
|
+
const maxBorrow = Math.max(0, supplied * (openLtv / 100) - borrowed);
|
|
2090
|
+
return { healthFactor, supplied, borrowed, maxBorrow, liquidationThreshold: liqThreshold };
|
|
2091
|
+
}
|
|
2092
|
+
async buildSaveTx(address, amount, _asset, options) {
|
|
2093
|
+
this.ensureInit();
|
|
2094
|
+
const rawAmount = usdcToRaw(amount).toString();
|
|
2095
|
+
const tx = new Transaction();
|
|
2096
|
+
tx.setSender(address);
|
|
2097
|
+
const caps = await this.getObligationCaps(address);
|
|
2098
|
+
let capRef;
|
|
2099
|
+
if (caps.length === 0) {
|
|
2100
|
+
const [newCap] = this.suilend.createObligation(tx);
|
|
2101
|
+
capRef = newCap;
|
|
2102
|
+
} else {
|
|
2103
|
+
capRef = caps[0].id;
|
|
2104
|
+
}
|
|
2105
|
+
const allCoins = await this.fetchAllCoins(address, USDC_TYPE2);
|
|
2106
|
+
if (allCoins.length === 0) {
|
|
2107
|
+
throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins found");
|
|
2108
|
+
}
|
|
2109
|
+
const primaryCoinId = allCoins[0].coinObjectId;
|
|
2110
|
+
if (allCoins.length > 1) {
|
|
2111
|
+
tx.mergeCoins(
|
|
2112
|
+
tx.object(primaryCoinId),
|
|
2113
|
+
allCoins.slice(1).map((c) => tx.object(c.coinObjectId))
|
|
2114
|
+
);
|
|
2115
|
+
}
|
|
2116
|
+
const [depositCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawAmount]);
|
|
2117
|
+
if (options?.collectFee) {
|
|
2118
|
+
addCollectFeeToTx(tx, depositCoin, "save");
|
|
2119
|
+
}
|
|
2120
|
+
this.suilend.deposit(depositCoin, USDC_TYPE2, capRef, tx);
|
|
2121
|
+
return { tx };
|
|
2122
|
+
}
|
|
2123
|
+
async buildWithdrawTx(address, amount, _asset) {
|
|
2124
|
+
this.ensureInit();
|
|
2125
|
+
const caps = await this.getObligationCaps(address);
|
|
2126
|
+
if (caps.length === 0) {
|
|
2127
|
+
throw new T2000Error("NO_COLLATERAL", "No Suilend position found");
|
|
2128
|
+
}
|
|
2129
|
+
const positions = await this.getPositions(address);
|
|
2130
|
+
const usdcSupply = positions.supplies.find((s) => s.asset === "USDC");
|
|
2131
|
+
const deposited = usdcSupply?.amount ?? 0;
|
|
2132
|
+
const effectiveAmount = Math.min(amount, deposited);
|
|
2133
|
+
if (effectiveAmount <= 0) {
|
|
2134
|
+
throw new T2000Error("NO_COLLATERAL", "Nothing to withdraw from Suilend");
|
|
2135
|
+
}
|
|
2136
|
+
const rawAmount = usdcToRaw(effectiveAmount).toString();
|
|
2137
|
+
const tx = new Transaction();
|
|
2138
|
+
tx.setSender(address);
|
|
2139
|
+
await this.suilend.withdrawAndSendToUser(
|
|
2140
|
+
address,
|
|
2141
|
+
caps[0].id,
|
|
2142
|
+
caps[0].obligationId,
|
|
2143
|
+
USDC_TYPE2,
|
|
2144
|
+
rawAmount,
|
|
2145
|
+
tx
|
|
2146
|
+
);
|
|
2147
|
+
return { tx, effectiveAmount };
|
|
2148
|
+
}
|
|
2149
|
+
async buildBorrowTx(_address, _amount, _asset, _options) {
|
|
2150
|
+
throw new T2000Error(
|
|
2151
|
+
"ASSET_NOT_SUPPORTED",
|
|
2152
|
+
"SuilendAdapter.buildBorrowTx() not available \u2014 Suilend requires different collateral/borrow assets. Deferred to Phase 10."
|
|
2153
|
+
);
|
|
2154
|
+
}
|
|
2155
|
+
async buildRepayTx(_address, _amount, _asset) {
|
|
2156
|
+
throw new T2000Error(
|
|
2157
|
+
"ASSET_NOT_SUPPORTED",
|
|
2158
|
+
"SuilendAdapter.buildRepayTx() not available \u2014 deferred to Phase 10."
|
|
2159
|
+
);
|
|
2160
|
+
}
|
|
2161
|
+
async maxWithdraw(address, _asset) {
|
|
2162
|
+
this.ensureInit();
|
|
2163
|
+
const health = await this.getHealth(address);
|
|
2164
|
+
let maxAmount;
|
|
2165
|
+
if (health.borrowed === 0) {
|
|
2166
|
+
maxAmount = health.supplied;
|
|
2167
|
+
} else {
|
|
2168
|
+
maxAmount = Math.max(
|
|
2169
|
+
0,
|
|
2170
|
+
health.supplied - health.borrowed * MIN_HEALTH_FACTOR2 / health.liquidationThreshold
|
|
2171
|
+
);
|
|
2172
|
+
}
|
|
2173
|
+
const remainingSupply = health.supplied - maxAmount;
|
|
2174
|
+
const hfAfter = health.borrowed > 0 ? remainingSupply * health.liquidationThreshold / health.borrowed : Infinity;
|
|
2175
|
+
return {
|
|
2176
|
+
maxAmount,
|
|
2177
|
+
healthFactorAfter: hfAfter,
|
|
2178
|
+
currentHF: health.healthFactor
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
async maxBorrow(_address, _asset) {
|
|
2182
|
+
throw new T2000Error(
|
|
2183
|
+
"ASSET_NOT_SUPPORTED",
|
|
2184
|
+
"SuilendAdapter.maxBorrow() not available \u2014 deferred to Phase 10."
|
|
2185
|
+
);
|
|
2186
|
+
}
|
|
2187
|
+
async fetchAllCoins(owner, coinType) {
|
|
2188
|
+
const all = [];
|
|
2189
|
+
let cursor = null;
|
|
2190
|
+
let hasNext = true;
|
|
2191
|
+
while (hasNext) {
|
|
2192
|
+
const page = await this.client.getCoins({ owner, coinType, cursor: cursor ?? void 0 });
|
|
2193
|
+
all.push(...page.data.map((c) => ({ coinObjectId: c.coinObjectId, balance: c.balance })));
|
|
2194
|
+
cursor = page.nextCursor;
|
|
2195
|
+
hasNext = page.hasNextPage;
|
|
2196
|
+
}
|
|
2197
|
+
return all;
|
|
2198
|
+
}
|
|
2199
|
+
};
|
|
1652
2200
|
|
|
1653
|
-
export { BPS_DENOMINATOR, CLOCK_ID, DEFAULT_NETWORK, MIST_PER_SUI, SENTINEL, SUI_DECIMALS, SUPPORTED_ASSETS, T2000, T2000Error, USDC_DECIMALS, addCollectFeeToTx, calculateFee, executeAutoTopUp, executeWithGas, exportPrivateKey, formatSui, formatUsd, generateKeypair, getAddress, getGasStatus, getPoolPrice, getRates, getSentinelInfo, getSwapQuote, keypairFromPrivateKey, listSentinels, loadKey, mapMoveAbortCode, mapWalletError, mistToSui, rawToUsdc, requestAttack, saveKey, attack as sentinelAttack, settleAttack, shouldAutoTopUp, simulateTransaction, solveHashcash, submitPrompt, suiToMist, throwIfSimulationFailed, truncateAddress, usdcToRaw, validateAddress, walletExists };
|
|
2201
|
+
export { BPS_DENOMINATOR, CLOCK_ID, CetusAdapter, DEFAULT_NETWORK, MIST_PER_SUI, NaviAdapter, ProtocolRegistry, SENTINEL, SUI_DECIMALS, SUPPORTED_ASSETS, SuilendAdapter, T2000, T2000Error, USDC_DECIMALS, addCollectFeeToTx, calculateFee, executeAutoTopUp, executeWithGas, exportPrivateKey, formatSui, formatUsd, generateKeypair, getAddress, getGasStatus, getPoolPrice, getRates, getSentinelInfo, getSwapQuote, keypairFromPrivateKey, listSentinels, loadKey, mapMoveAbortCode, mapWalletError, mistToSui, rawToUsdc, requestAttack, saveKey, attack as sentinelAttack, settleAttack, shouldAutoTopUp, simulateTransaction, solveHashcash, submitPrompt, suiToMist, throwIfSimulationFailed, truncateAddress, usdcToRaw, validateAddress, walletExists };
|
|
1654
2202
|
//# sourceMappingURL=index.js.map
|
|
1655
2203
|
//# sourceMappingURL=index.js.map
|