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