@t2000/sdk 0.17.29 → 0.18.3
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.d.cts +1 -1
- package/dist/adapters/index.d.ts +4 -10
- package/dist/adapters/index.js +2107 -15
- package/dist/adapters/index.js.map +1 -1
- package/dist/{index-DdtOBw42.d.ts → index-Co0lp99l.d.cts} +23 -1
- package/dist/{index-DdtOBw42.d.cts → index-Co0lp99l.d.ts} +23 -1
- package/dist/index.cjs +102 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -3
- package/dist/index.d.ts +654 -30
- package/dist/index.js +6154 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/adapters/cetus.d.ts +0 -29
- package/dist/adapters/cetus.d.ts.map +0 -1
- package/dist/adapters/cetus.js +0 -74
- package/dist/adapters/cetus.js.map +0 -1
- package/dist/adapters/cetus.test.d.ts +0 -2
- package/dist/adapters/cetus.test.d.ts.map +0 -1
- package/dist/adapters/cetus.test.js +0 -57
- package/dist/adapters/cetus.test.js.map +0 -1
- package/dist/adapters/compliance.test.d.ts +0 -8
- package/dist/adapters/compliance.test.d.ts.map +0 -1
- package/dist/adapters/compliance.test.js +0 -202
- package/dist/adapters/compliance.test.js.map +0 -1
- package/dist/adapters/index.d.ts.map +0 -1
- package/dist/adapters/navi.d.ts +0 -41
- package/dist/adapters/navi.d.ts.map +0 -1
- package/dist/adapters/navi.js +0 -102
- package/dist/adapters/navi.js.map +0 -1
- package/dist/adapters/navi.test.d.ts +0 -2
- package/dist/adapters/navi.test.d.ts.map +0 -1
- package/dist/adapters/navi.test.js +0 -164
- package/dist/adapters/navi.test.js.map +0 -1
- package/dist/adapters/registry.d.ts +0 -47
- package/dist/adapters/registry.d.ts.map +0 -1
- package/dist/adapters/registry.js +0 -162
- package/dist/adapters/registry.js.map +0 -1
- package/dist/adapters/registry.test.d.ts +0 -2
- package/dist/adapters/registry.test.d.ts.map +0 -1
- package/dist/adapters/registry.test.js +0 -197
- package/dist/adapters/registry.test.js.map +0 -1
- package/dist/adapters/suilend.d.ts +0 -71
- package/dist/adapters/suilend.d.ts.map +0 -1
- package/dist/adapters/suilend.js +0 -826
- package/dist/adapters/suilend.js.map +0 -1
- package/dist/adapters/suilend.test.d.ts +0 -2
- package/dist/adapters/suilend.test.d.ts.map +0 -1
- package/dist/adapters/suilend.test.js +0 -294
- package/dist/adapters/suilend.test.js.map +0 -1
- package/dist/adapters/types.d.ts +0 -160
- package/dist/adapters/types.d.ts.map +0 -1
- package/dist/adapters/types.js +0 -2
- package/dist/adapters/types.js.map +0 -1
- package/dist/auto-invest.d.ts +0 -23
- package/dist/auto-invest.d.ts.map +0 -1
- package/dist/auto-invest.js +0 -131
- package/dist/auto-invest.js.map +0 -1
- package/dist/auto-invest.test.d.ts +0 -2
- package/dist/auto-invest.test.d.ts.map +0 -1
- package/dist/auto-invest.test.js +0 -220
- package/dist/auto-invest.test.js.map +0 -1
- package/dist/constants.d.ts +0 -177
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js +0 -135
- package/dist/constants.js.map +0 -1
- package/dist/contacts.d.ts +0 -25
- package/dist/contacts.d.ts.map +0 -1
- package/dist/contacts.js +0 -83
- package/dist/contacts.js.map +0 -1
- package/dist/contacts.test.d.ts +0 -2
- package/dist/contacts.test.d.ts.map +0 -1
- package/dist/contacts.test.js +0 -164
- package/dist/contacts.test.js.map +0 -1
- package/dist/errors.d.ts +0 -26
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -81
- package/dist/errors.js.map +0 -1
- package/dist/errors.test.d.ts +0 -2
- package/dist/errors.test.d.ts.map +0 -1
- package/dist/errors.test.js +0 -48
- package/dist/errors.test.js.map +0 -1
- package/dist/gas/autoTopUp.d.ts +0 -18
- package/dist/gas/autoTopUp.d.ts.map +0 -1
- package/dist/gas/autoTopUp.js +0 -55
- package/dist/gas/autoTopUp.js.map +0 -1
- package/dist/gas/autoTopUp.test.d.ts +0 -2
- package/dist/gas/autoTopUp.test.d.ts.map +0 -1
- package/dist/gas/autoTopUp.test.js +0 -59
- package/dist/gas/autoTopUp.test.js.map +0 -1
- package/dist/gas/gasStation.d.ts +0 -23
- package/dist/gas/gasStation.d.ts.map +0 -1
- package/dist/gas/gasStation.js +0 -58
- package/dist/gas/gasStation.js.map +0 -1
- package/dist/gas/index.d.ts +0 -4
- package/dist/gas/index.d.ts.map +0 -1
- package/dist/gas/index.js +0 -4
- package/dist/gas/index.js.map +0 -1
- package/dist/gas/manager.d.ts +0 -24
- package/dist/gas/manager.d.ts.map +0 -1
- package/dist/gas/manager.js +0 -142
- package/dist/gas/manager.js.map +0 -1
- package/dist/gas/manager.test.d.ts +0 -2
- package/dist/gas/manager.test.d.ts.map +0 -1
- package/dist/gas/manager.test.js +0 -220
- package/dist/gas/manager.test.js.map +0 -1
- package/dist/gas/serialization.test.d.ts +0 -2
- package/dist/gas/serialization.test.d.ts.map +0 -1
- package/dist/gas/serialization.test.js +0 -47
- package/dist/gas/serialization.test.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/invest.test.d.ts +0 -2
- package/dist/invest.test.d.ts.map +0 -1
- package/dist/invest.test.js +0 -256
- package/dist/invest.test.js.map +0 -1
- package/dist/portfolio.d.ts +0 -39
- package/dist/portfolio.d.ts.map +0 -1
- package/dist/portfolio.js +0 -201
- package/dist/portfolio.js.map +0 -1
- package/dist/portfolio.test.d.ts +0 -2
- package/dist/portfolio.test.d.ts.map +0 -1
- package/dist/portfolio.test.js +0 -301
- package/dist/portfolio.test.js.map +0 -1
- package/dist/protocols/cetus.d.ts +0 -73
- package/dist/protocols/cetus.d.ts.map +0 -1
- package/dist/protocols/cetus.js +0 -267
- package/dist/protocols/cetus.js.map +0 -1
- package/dist/protocols/cetus.test.d.ts +0 -2
- package/dist/protocols/cetus.test.d.ts.map +0 -1
- package/dist/protocols/cetus.test.js +0 -325
- package/dist/protocols/cetus.test.js.map +0 -1
- package/dist/protocols/navi.d.ts +0 -59
- package/dist/protocols/navi.d.ts.map +0 -1
- package/dist/protocols/navi.js +0 -945
- package/dist/protocols/navi.js.map +0 -1
- package/dist/protocols/navi.test.d.ts +0 -2
- package/dist/protocols/navi.test.d.ts.map +0 -1
- package/dist/protocols/navi.test.js +0 -339
- package/dist/protocols/navi.test.js.map +0 -1
- package/dist/protocols/protocolFee.d.ts +0 -17
- package/dist/protocols/protocolFee.d.ts.map +0 -1
- package/dist/protocols/protocolFee.js +0 -62
- package/dist/protocols/protocolFee.js.map +0 -1
- package/dist/protocols/protocolFee.test.d.ts +0 -2
- package/dist/protocols/protocolFee.test.d.ts.map +0 -1
- package/dist/protocols/protocolFee.test.js +0 -137
- package/dist/protocols/protocolFee.test.js.map +0 -1
- package/dist/protocols/sentinel.d.ts +0 -18
- package/dist/protocols/sentinel.d.ts.map +0 -1
- package/dist/protocols/sentinel.js +0 -188
- package/dist/protocols/sentinel.js.map +0 -1
- package/dist/protocols/sentinel.test.d.ts +0 -2
- package/dist/protocols/sentinel.test.d.ts.map +0 -1
- package/dist/protocols/sentinel.test.js +0 -199
- package/dist/protocols/sentinel.test.js.map +0 -1
- package/dist/protocols/yieldTracker.d.ts +0 -6
- package/dist/protocols/yieldTracker.d.ts.map +0 -1
- package/dist/protocols/yieldTracker.js +0 -29
- package/dist/protocols/yieldTracker.js.map +0 -1
- package/dist/safeguards/enforcer.d.ts +0 -18
- package/dist/safeguards/enforcer.d.ts.map +0 -1
- package/dist/safeguards/enforcer.js +0 -130
- package/dist/safeguards/enforcer.js.map +0 -1
- package/dist/safeguards/enforcer.test.d.ts +0 -2
- package/dist/safeguards/enforcer.test.d.ts.map +0 -1
- package/dist/safeguards/enforcer.test.js +0 -212
- package/dist/safeguards/enforcer.test.js.map +0 -1
- package/dist/safeguards/errors.d.ts +0 -24
- package/dist/safeguards/errors.d.ts.map +0 -1
- package/dist/safeguards/errors.js +0 -31
- package/dist/safeguards/errors.js.map +0 -1
- package/dist/safeguards/index.d.ts +0 -6
- package/dist/safeguards/index.d.ts.map +0 -1
- package/dist/safeguards/index.js +0 -4
- package/dist/safeguards/index.js.map +0 -1
- package/dist/safeguards/types.d.ts +0 -16
- package/dist/safeguards/types.d.ts.map +0 -1
- package/dist/safeguards/types.js +0 -13
- package/dist/safeguards/types.js.map +0 -1
- package/dist/strategy.d.ts +0 -22
- package/dist/strategy.d.ts.map +0 -1
- package/dist/strategy.js +0 -113
- package/dist/strategy.js.map +0 -1
- package/dist/strategy.test.d.ts +0 -2
- package/dist/strategy.test.d.ts.map +0 -1
- package/dist/strategy.test.js +0 -212
- package/dist/strategy.test.js.map +0 -1
- package/dist/t2000.d.ts +0 -219
- package/dist/t2000.d.ts.map +0 -1
- package/dist/t2000.integration.test.d.ts +0 -2
- package/dist/t2000.integration.test.d.ts.map +0 -1
- package/dist/t2000.integration.test.js +0 -954
- package/dist/t2000.integration.test.js.map +0 -1
- package/dist/t2000.js +0 -2416
- package/dist/t2000.js.map +0 -1
- package/dist/types.d.ts +0 -419
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/utils/format.d.ts +0 -22
- package/dist/utils/format.d.ts.map +0 -1
- package/dist/utils/format.js +0 -71
- package/dist/utils/format.js.map +0 -1
- package/dist/utils/format.test.d.ts +0 -2
- package/dist/utils/format.test.d.ts.map +0 -1
- package/dist/utils/format.test.js +0 -187
- package/dist/utils/format.test.js.map +0 -1
- package/dist/utils/hashcash.d.ts +0 -2
- package/dist/utils/hashcash.d.ts.map +0 -1
- package/dist/utils/hashcash.js +0 -27
- package/dist/utils/hashcash.js.map +0 -1
- package/dist/utils/hashcash.test.d.ts +0 -2
- package/dist/utils/hashcash.test.d.ts.map +0 -1
- package/dist/utils/hashcash.test.js +0 -40
- package/dist/utils/hashcash.test.js.map +0 -1
- package/dist/utils/retry.d.ts +0 -9
- package/dist/utils/retry.d.ts.map +0 -1
- package/dist/utils/retry.js +0 -47
- package/dist/utils/retry.js.map +0 -1
- package/dist/utils/simulate.d.ts +0 -15
- package/dist/utils/simulate.d.ts.map +0 -1
- package/dist/utils/simulate.js +0 -75
- package/dist/utils/simulate.js.map +0 -1
- package/dist/utils/simulate.test.d.ts +0 -2
- package/dist/utils/simulate.test.d.ts.map +0 -1
- package/dist/utils/simulate.test.js +0 -80
- package/dist/utils/simulate.test.js.map +0 -1
- package/dist/utils/sui.d.ts +0 -6
- package/dist/utils/sui.d.ts.map +0 -1
- package/dist/utils/sui.js +0 -28
- package/dist/utils/sui.js.map +0 -1
- package/dist/utils/sui.test.d.ts +0 -2
- package/dist/utils/sui.test.d.ts.map +0 -1
- package/dist/utils/sui.test.js +0 -58
- package/dist/utils/sui.test.js.map +0 -1
- package/dist/wallet/balance.d.ts +0 -4
- package/dist/wallet/balance.d.ts.map +0 -1
- package/dist/wallet/balance.js +0 -98
- package/dist/wallet/balance.js.map +0 -1
- package/dist/wallet/history.d.ts +0 -4
- package/dist/wallet/history.d.ts.map +0 -1
- package/dist/wallet/history.js +0 -38
- package/dist/wallet/history.js.map +0 -1
- package/dist/wallet/keyManager.d.ts +0 -9
- package/dist/wallet/keyManager.d.ts.map +0 -1
- package/dist/wallet/keyManager.js +0 -113
- package/dist/wallet/keyManager.js.map +0 -1
- package/dist/wallet/keyManager.test.d.ts +0 -2
- package/dist/wallet/keyManager.test.d.ts.map +0 -1
- package/dist/wallet/keyManager.test.js +0 -55
- package/dist/wallet/keyManager.test.js.map +0 -1
- package/dist/wallet/send.d.ts +0 -24
- package/dist/wallet/send.d.ts.map +0 -1
- package/dist/wallet/send.js +0 -95
- package/dist/wallet/send.js.map +0 -1
- package/dist/wallet/send.test.d.ts +0 -2
- package/dist/wallet/send.test.d.ts.map +0 -1
- package/dist/wallet/send.test.js +0 -69
- package/dist/wallet/send.test.js.map +0 -1
package/dist/adapters/suilend.js
DELETED
|
@@ -1,826 +0,0 @@
|
|
|
1
|
-
import { Transaction } from '@mysten/sui/transactions';
|
|
2
|
-
import { normalizeStructTag } from '@mysten/sui/utils';
|
|
3
|
-
import { SUPPORTED_ASSETS, STABLE_ASSETS } from '../constants.js';
|
|
4
|
-
import { stableToRaw } from '../utils/format.js';
|
|
5
|
-
import { T2000Error } from '../errors.js';
|
|
6
|
-
import { addCollectFeeToTx } from '../protocols/protocolFee.js';
|
|
7
|
-
const USDC_TYPE = SUPPORTED_ASSETS.USDC.type;
|
|
8
|
-
const WAD = 1e18;
|
|
9
|
-
const MIN_HEALTH_FACTOR = 1.5;
|
|
10
|
-
const CLOCK = '0x6';
|
|
11
|
-
const SUI_SYSTEM_STATE = '0x5';
|
|
12
|
-
const LENDING_MARKET_ID = '0x84030d26d85eaa7035084a057f2f11f701b7e2e4eda87551becbc7c97505ece1';
|
|
13
|
-
const LENDING_MARKET_TYPE = '0xf95b06141ed4a174f239417323bde3f209b972f5930d8521ea38a52aff3a6ddf::suilend::MAIN_POOL';
|
|
14
|
-
const SUILEND_PACKAGE = '0xf95b06141ed4a174f239417323bde3f209b972f5930d8521ea38a52aff3a6ddf';
|
|
15
|
-
const UPGRADE_CAP_ID = '0x3d4ef1859c3ee9fc72858f588b56a09da5466e64f8cc4e90a7b3b909fba8a7ae';
|
|
16
|
-
const FALLBACK_PUBLISHED_AT = '0x3d4353f3bd3565329655e6b77bc2abfd31e558b86662ebd078ae453d416bc10f';
|
|
17
|
-
export const descriptor = {
|
|
18
|
-
id: 'suilend',
|
|
19
|
-
name: 'Suilend',
|
|
20
|
-
packages: [SUILEND_PACKAGE],
|
|
21
|
-
actionMap: {
|
|
22
|
-
'lending_market::deposit_liquidity_and_mint_ctokens': 'save',
|
|
23
|
-
'lending_market::deposit_ctokens_into_obligation': 'save',
|
|
24
|
-
'lending_market::create_obligation': 'save',
|
|
25
|
-
'lending_market::withdraw_ctokens': 'withdraw',
|
|
26
|
-
'lending_market::redeem_ctokens_and_withdraw_liquidity': 'withdraw',
|
|
27
|
-
'lending_market::redeem_ctokens_and_withdraw_liquidity_request': 'withdraw',
|
|
28
|
-
'lending_market::fulfill_liquidity_request': 'withdraw',
|
|
29
|
-
'lending_market::unstake_sui_from_staker': 'withdraw',
|
|
30
|
-
'lending_market::borrow': 'borrow',
|
|
31
|
-
'lending_market::repay': 'repay',
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
// Rate math (unchanged from SDK-based version)
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
function interpolateRate(utilBreakpoints, aprBreakpoints, utilizationPct) {
|
|
38
|
-
if (utilBreakpoints.length === 0)
|
|
39
|
-
return 0;
|
|
40
|
-
if (utilizationPct <= utilBreakpoints[0])
|
|
41
|
-
return aprBreakpoints[0];
|
|
42
|
-
if (utilizationPct >= utilBreakpoints[utilBreakpoints.length - 1]) {
|
|
43
|
-
return aprBreakpoints[aprBreakpoints.length - 1];
|
|
44
|
-
}
|
|
45
|
-
for (let i = 1; i < utilBreakpoints.length; i++) {
|
|
46
|
-
if (utilizationPct <= utilBreakpoints[i]) {
|
|
47
|
-
const t = (utilizationPct - utilBreakpoints[i - 1]) /
|
|
48
|
-
(utilBreakpoints[i] - utilBreakpoints[i - 1]);
|
|
49
|
-
return aprBreakpoints[i - 1] + t * (aprBreakpoints[i] - aprBreakpoints[i - 1]);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return aprBreakpoints[aprBreakpoints.length - 1];
|
|
53
|
-
}
|
|
54
|
-
function computeRates(reserve) {
|
|
55
|
-
const available = reserve.availableAmount / 10 ** reserve.mintDecimals;
|
|
56
|
-
const borrowed = reserve.borrowedAmountWad / WAD / 10 ** reserve.mintDecimals;
|
|
57
|
-
const totalDeposited = available + borrowed;
|
|
58
|
-
const utilizationPct = totalDeposited > 0 ? (borrowed / totalDeposited) * 100 : 0;
|
|
59
|
-
if (reserve.interestRateUtils.length === 0)
|
|
60
|
-
return { borrowAprPct: 0, depositAprPct: 0 };
|
|
61
|
-
const aprs = reserve.interestRateAprs.map((a) => a / 100);
|
|
62
|
-
const borrowAprPct = interpolateRate(reserve.interestRateUtils, aprs, utilizationPct);
|
|
63
|
-
const depositAprPct = (utilizationPct / 100) *
|
|
64
|
-
(borrowAprPct / 100) *
|
|
65
|
-
(1 - reserve.spreadFeeBps / 10000) *
|
|
66
|
-
100;
|
|
67
|
-
return { borrowAprPct, depositAprPct };
|
|
68
|
-
}
|
|
69
|
-
const MS_PER_YEAR = 365.25 * 24 * 3600 * 1000;
|
|
70
|
-
function computeDepositRewardApr(reserve, allReserves) {
|
|
71
|
-
if (reserve.depositTotalShares <= 0 || reserve.price <= 0)
|
|
72
|
-
return 0;
|
|
73
|
-
const totalDepositValue = (reserve.depositTotalShares / 10 ** reserve.mintDecimals) * reserve.price;
|
|
74
|
-
if (totalDepositValue <= 0)
|
|
75
|
-
return 0;
|
|
76
|
-
const priceMap = new Map();
|
|
77
|
-
for (const r of allReserves) {
|
|
78
|
-
if (r.price > 0)
|
|
79
|
-
priceMap.set(r.coinType, { price: r.price, decimals: r.mintDecimals });
|
|
80
|
-
}
|
|
81
|
-
let rewardApr = 0;
|
|
82
|
-
for (const rw of reserve.depositPoolRewards) {
|
|
83
|
-
const info = priceMap.get(rw.coinType);
|
|
84
|
-
if (!info || info.price <= 0)
|
|
85
|
-
continue;
|
|
86
|
-
const durationMs = rw.endTimeMs - rw.startTimeMs;
|
|
87
|
-
if (durationMs <= 0)
|
|
88
|
-
continue;
|
|
89
|
-
const annualTokens = (rw.totalRewards / 10 ** info.decimals) * (MS_PER_YEAR / durationMs);
|
|
90
|
-
rewardApr += (annualTokens * info.price) / totalDepositValue * 100;
|
|
91
|
-
}
|
|
92
|
-
return rewardApr;
|
|
93
|
-
}
|
|
94
|
-
function cTokenRatio(reserve) {
|
|
95
|
-
if (reserve.ctokenSupply === 0)
|
|
96
|
-
return 1;
|
|
97
|
-
const totalSupply = reserve.availableAmount +
|
|
98
|
-
reserve.borrowedAmountWad / WAD -
|
|
99
|
-
reserve.unclaimedSpreadFeesWad / WAD;
|
|
100
|
-
return totalSupply / reserve.ctokenSupply;
|
|
101
|
-
}
|
|
102
|
-
function f(obj) {
|
|
103
|
-
if (obj && typeof obj === 'object' && 'fields' in obj)
|
|
104
|
-
return obj.fields;
|
|
105
|
-
return obj;
|
|
106
|
-
}
|
|
107
|
-
function str(v) { return String(v ?? '0'); }
|
|
108
|
-
function num(v) { return Number(str(v)); }
|
|
109
|
-
function parseReserve(raw, index) {
|
|
110
|
-
const r = f(raw);
|
|
111
|
-
const coinTypeField = f(r.coin_type);
|
|
112
|
-
const config = f(f(r.config)?.element);
|
|
113
|
-
const dMgr = f(r.deposits_pool_reward_manager);
|
|
114
|
-
const rawRewards = Array.isArray(dMgr?.pool_rewards) ? dMgr.pool_rewards : [];
|
|
115
|
-
const now = Date.now();
|
|
116
|
-
const depositPoolRewards = rawRewards
|
|
117
|
-
.map((rw, idx) => {
|
|
118
|
-
if (rw === null)
|
|
119
|
-
return null;
|
|
120
|
-
const rwf = f(rw);
|
|
121
|
-
return {
|
|
122
|
-
coinType: str(f(rwf.coin_type)?.name),
|
|
123
|
-
totalRewards: num(rwf.total_rewards),
|
|
124
|
-
startTimeMs: num(rwf.start_time_ms),
|
|
125
|
-
endTimeMs: num(rwf.end_time_ms),
|
|
126
|
-
rewardIndex: idx,
|
|
127
|
-
};
|
|
128
|
-
})
|
|
129
|
-
.filter((rw) => rw !== null && rw.endTimeMs > now && rw.totalRewards > 0);
|
|
130
|
-
return {
|
|
131
|
-
coinType: str(coinTypeField?.name),
|
|
132
|
-
mintDecimals: num(r.mint_decimals),
|
|
133
|
-
availableAmount: num(r.available_amount),
|
|
134
|
-
borrowedAmountWad: num(f(r.borrowed_amount)?.value),
|
|
135
|
-
ctokenSupply: num(r.ctoken_supply),
|
|
136
|
-
unclaimedSpreadFeesWad: num(f(r.unclaimed_spread_fees)?.value),
|
|
137
|
-
cumulativeBorrowRateWad: num(f(r.cumulative_borrow_rate)?.value),
|
|
138
|
-
openLtvPct: num(config?.open_ltv_pct),
|
|
139
|
-
closeLtvPct: num(config?.close_ltv_pct),
|
|
140
|
-
spreadFeeBps: num(config?.spread_fee_bps),
|
|
141
|
-
interestRateUtils: Array.isArray(config?.interest_rate_utils) ? config.interest_rate_utils.map(num) : [],
|
|
142
|
-
interestRateAprs: Array.isArray(config?.interest_rate_aprs) ? config.interest_rate_aprs.map(num) : [],
|
|
143
|
-
arrayIndex: index,
|
|
144
|
-
price: num(f(r.price)?.value) / WAD,
|
|
145
|
-
depositTotalShares: num(dMgr?.total_shares),
|
|
146
|
-
depositPoolRewards,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
function parseObligation(raw) {
|
|
150
|
-
const deposits = Array.isArray(raw.deposits)
|
|
151
|
-
? raw.deposits.map((d) => {
|
|
152
|
-
const df = f(d);
|
|
153
|
-
return {
|
|
154
|
-
coinType: str(f(df.coin_type)?.name),
|
|
155
|
-
ctokenAmount: num(df.deposited_ctoken_amount),
|
|
156
|
-
reserveIdx: num(df.reserve_array_index),
|
|
157
|
-
};
|
|
158
|
-
})
|
|
159
|
-
: [];
|
|
160
|
-
const borrows = Array.isArray(raw.borrows)
|
|
161
|
-
? raw.borrows.map((b) => {
|
|
162
|
-
const bf = f(b);
|
|
163
|
-
return {
|
|
164
|
-
coinType: str(f(bf.coin_type)?.name),
|
|
165
|
-
borrowedWad: num(f(bf.borrowed_amount)?.value),
|
|
166
|
-
cumBorrowRateWad: num(f(bf.cumulative_borrow_rate)?.value),
|
|
167
|
-
reserveIdx: num(bf.reserve_array_index),
|
|
168
|
-
};
|
|
169
|
-
})
|
|
170
|
-
: [];
|
|
171
|
-
return { deposits, borrows };
|
|
172
|
-
}
|
|
173
|
-
// ---------------------------------------------------------------------------
|
|
174
|
-
// Adapter
|
|
175
|
-
// ---------------------------------------------------------------------------
|
|
176
|
-
/**
|
|
177
|
-
* Suilend adapter — contract-first, no SDK dependency.
|
|
178
|
-
* Interacts directly with Suilend Move contracts via RPC + PTB moveCall.
|
|
179
|
-
*/
|
|
180
|
-
export class SuilendAdapter {
|
|
181
|
-
id = 'suilend';
|
|
182
|
-
name = 'Suilend';
|
|
183
|
-
version = '2.0.0';
|
|
184
|
-
capabilities = ['save', 'withdraw', 'borrow', 'repay'];
|
|
185
|
-
supportedAssets = [...STABLE_ASSETS, 'SUI', 'ETH', 'BTC', 'GOLD'];
|
|
186
|
-
supportsSameAssetBorrow = false;
|
|
187
|
-
client;
|
|
188
|
-
publishedAt = null;
|
|
189
|
-
reserveCache = null;
|
|
190
|
-
async init(client) {
|
|
191
|
-
this.client = client;
|
|
192
|
-
}
|
|
193
|
-
initSync(client) {
|
|
194
|
-
this.client = client;
|
|
195
|
-
}
|
|
196
|
-
// -- On-chain reads -------------------------------------------------------
|
|
197
|
-
async resolvePackage() {
|
|
198
|
-
if (this.publishedAt)
|
|
199
|
-
return this.publishedAt;
|
|
200
|
-
try {
|
|
201
|
-
const cap = await this.client.getObject({ id: UPGRADE_CAP_ID, options: { showContent: true } });
|
|
202
|
-
if (cap.data?.content?.dataType === 'moveObject') {
|
|
203
|
-
const fields = cap.data.content.fields;
|
|
204
|
-
this.publishedAt = str(fields.package);
|
|
205
|
-
return this.publishedAt;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
catch { /* use fallback */ }
|
|
209
|
-
this.publishedAt = FALLBACK_PUBLISHED_AT;
|
|
210
|
-
return this.publishedAt;
|
|
211
|
-
}
|
|
212
|
-
async loadReserves(fresh = false) {
|
|
213
|
-
if (this.reserveCache && !fresh)
|
|
214
|
-
return this.reserveCache;
|
|
215
|
-
const market = await this.client.getObject({
|
|
216
|
-
id: LENDING_MARKET_ID,
|
|
217
|
-
options: { showContent: true },
|
|
218
|
-
});
|
|
219
|
-
if (market.data?.content?.dataType !== 'moveObject') {
|
|
220
|
-
throw new T2000Error('PROTOCOL_UNAVAILABLE', 'Failed to read Suilend lending market');
|
|
221
|
-
}
|
|
222
|
-
const fields = market.data.content.fields;
|
|
223
|
-
const reservesRaw = fields.reserves;
|
|
224
|
-
if (!Array.isArray(reservesRaw)) {
|
|
225
|
-
throw new T2000Error('PROTOCOL_UNAVAILABLE', 'Failed to parse Suilend reserves');
|
|
226
|
-
}
|
|
227
|
-
this.reserveCache = reservesRaw.map((r, i) => parseReserve(r, i));
|
|
228
|
-
return this.reserveCache;
|
|
229
|
-
}
|
|
230
|
-
findReserve(reserves, asset) {
|
|
231
|
-
let coinType;
|
|
232
|
-
if (asset in SUPPORTED_ASSETS) {
|
|
233
|
-
coinType = SUPPORTED_ASSETS[asset].type;
|
|
234
|
-
}
|
|
235
|
-
else if (asset.includes('::')) {
|
|
236
|
-
coinType = asset;
|
|
237
|
-
}
|
|
238
|
-
else {
|
|
239
|
-
return undefined;
|
|
240
|
-
}
|
|
241
|
-
try {
|
|
242
|
-
const normalized = normalizeStructTag(coinType);
|
|
243
|
-
return reserves.find((r) => {
|
|
244
|
-
try {
|
|
245
|
-
return normalizeStructTag(r.coinType) === normalized;
|
|
246
|
-
}
|
|
247
|
-
catch {
|
|
248
|
-
return false;
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
catch {
|
|
253
|
-
return undefined;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
async fetchObligationCaps(address) {
|
|
257
|
-
const capType = `${SUILEND_PACKAGE}::lending_market::ObligationOwnerCap<${LENDING_MARKET_TYPE}>`;
|
|
258
|
-
const caps = [];
|
|
259
|
-
let cursor;
|
|
260
|
-
let hasNext = true;
|
|
261
|
-
while (hasNext) {
|
|
262
|
-
const page = await this.client.getOwnedObjects({
|
|
263
|
-
owner: address,
|
|
264
|
-
filter: { StructType: capType },
|
|
265
|
-
options: { showContent: true },
|
|
266
|
-
cursor: cursor ?? undefined,
|
|
267
|
-
});
|
|
268
|
-
for (const item of page.data) {
|
|
269
|
-
if (item.data?.content?.dataType !== 'moveObject')
|
|
270
|
-
continue;
|
|
271
|
-
const fields = item.data.content.fields;
|
|
272
|
-
caps.push({
|
|
273
|
-
id: item.data.objectId,
|
|
274
|
-
obligationId: str(fields.obligation_id),
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
cursor = page.nextCursor;
|
|
278
|
-
hasNext = page.hasNextPage;
|
|
279
|
-
}
|
|
280
|
-
return caps;
|
|
281
|
-
}
|
|
282
|
-
async fetchObligation(obligationId) {
|
|
283
|
-
const obj = await this.client.getObject({ id: obligationId, options: { showContent: true } });
|
|
284
|
-
if (obj.data?.content?.dataType !== 'moveObject') {
|
|
285
|
-
throw new T2000Error('PROTOCOL_UNAVAILABLE', 'Failed to read Suilend obligation');
|
|
286
|
-
}
|
|
287
|
-
return parseObligation(obj.data.content.fields);
|
|
288
|
-
}
|
|
289
|
-
resolveSymbol(coinType) {
|
|
290
|
-
try {
|
|
291
|
-
const normalized = normalizeStructTag(coinType);
|
|
292
|
-
for (const [key, info] of Object.entries(SUPPORTED_ASSETS)) {
|
|
293
|
-
try {
|
|
294
|
-
if (normalizeStructTag(info.type) === normalized)
|
|
295
|
-
return key;
|
|
296
|
-
}
|
|
297
|
-
catch { /* skip */ }
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
catch { /* fall through */ }
|
|
301
|
-
const parts = coinType.split('::');
|
|
302
|
-
return parts[parts.length - 1] || 'UNKNOWN';
|
|
303
|
-
}
|
|
304
|
-
// -- Adapter interface ----------------------------------------------------
|
|
305
|
-
async getRates(asset) {
|
|
306
|
-
const reserves = await this.loadReserves();
|
|
307
|
-
const reserve = this.findReserve(reserves, asset);
|
|
308
|
-
if (!reserve)
|
|
309
|
-
throw new T2000Error('ASSET_NOT_SUPPORTED', `Suilend does not support ${asset}`);
|
|
310
|
-
const { borrowAprPct, depositAprPct } = computeRates(reserve);
|
|
311
|
-
const rewardApr = computeDepositRewardApr(reserve, reserves);
|
|
312
|
-
return { asset, saveApy: depositAprPct + rewardApr, borrowApy: borrowAprPct };
|
|
313
|
-
}
|
|
314
|
-
async getPositions(address) {
|
|
315
|
-
const supplies = [];
|
|
316
|
-
const borrows = [];
|
|
317
|
-
const caps = await this.fetchObligationCaps(address);
|
|
318
|
-
if (caps.length === 0)
|
|
319
|
-
return { supplies, borrows };
|
|
320
|
-
const [reserves, obligation] = await Promise.all([
|
|
321
|
-
this.loadReserves(),
|
|
322
|
-
this.fetchObligation(caps[0].obligationId),
|
|
323
|
-
]);
|
|
324
|
-
for (const dep of obligation.deposits) {
|
|
325
|
-
const reserve = reserves[dep.reserveIdx];
|
|
326
|
-
if (!reserve)
|
|
327
|
-
continue;
|
|
328
|
-
const ratio = cTokenRatio(reserve);
|
|
329
|
-
const amount = (dep.ctokenAmount * ratio) / 10 ** reserve.mintDecimals;
|
|
330
|
-
const { depositAprPct } = computeRates(reserve);
|
|
331
|
-
const rewardApr = computeDepositRewardApr(reserve, reserves);
|
|
332
|
-
supplies.push({ asset: this.resolveSymbol(dep.coinType), amount, apy: depositAprPct + rewardApr });
|
|
333
|
-
}
|
|
334
|
-
for (const bor of obligation.borrows) {
|
|
335
|
-
const reserve = reserves[bor.reserveIdx];
|
|
336
|
-
if (!reserve)
|
|
337
|
-
continue;
|
|
338
|
-
const rawAmount = bor.borrowedWad / WAD / 10 ** reserve.mintDecimals;
|
|
339
|
-
const reserveRate = reserve.cumulativeBorrowRateWad / WAD;
|
|
340
|
-
const posRate = bor.cumBorrowRateWad / WAD;
|
|
341
|
-
const compounded = posRate > 0 ? rawAmount * (reserveRate / posRate) : rawAmount;
|
|
342
|
-
const { borrowAprPct } = computeRates(reserve);
|
|
343
|
-
borrows.push({ asset: this.resolveSymbol(bor.coinType), amount: compounded, apy: borrowAprPct });
|
|
344
|
-
}
|
|
345
|
-
return { supplies, borrows };
|
|
346
|
-
}
|
|
347
|
-
async getHealth(address) {
|
|
348
|
-
const caps = await this.fetchObligationCaps(address);
|
|
349
|
-
if (caps.length === 0) {
|
|
350
|
-
return { healthFactor: Infinity, supplied: 0, borrowed: 0, maxBorrow: 0, liquidationThreshold: 0 };
|
|
351
|
-
}
|
|
352
|
-
const [reserves, obligation] = await Promise.all([
|
|
353
|
-
this.loadReserves(),
|
|
354
|
-
this.fetchObligation(caps[0].obligationId),
|
|
355
|
-
]);
|
|
356
|
-
let supplied = 0;
|
|
357
|
-
let borrowed = 0;
|
|
358
|
-
let weightedCloseLtv = 0;
|
|
359
|
-
let weightedOpenLtv = 0;
|
|
360
|
-
for (const dep of obligation.deposits) {
|
|
361
|
-
const reserve = reserves[dep.reserveIdx];
|
|
362
|
-
if (!reserve)
|
|
363
|
-
continue;
|
|
364
|
-
const ratio = cTokenRatio(reserve);
|
|
365
|
-
const amount = (dep.ctokenAmount * ratio) / 10 ** reserve.mintDecimals;
|
|
366
|
-
supplied += amount;
|
|
367
|
-
weightedCloseLtv += amount * (reserve.closeLtvPct / 100);
|
|
368
|
-
weightedOpenLtv += amount * (reserve.openLtvPct / 100);
|
|
369
|
-
}
|
|
370
|
-
for (const bor of obligation.borrows) {
|
|
371
|
-
const reserve = reserves[bor.reserveIdx];
|
|
372
|
-
if (!reserve)
|
|
373
|
-
continue;
|
|
374
|
-
const rawAmount = bor.borrowedWad / WAD / 10 ** reserve.mintDecimals;
|
|
375
|
-
const reserveRate = reserve.cumulativeBorrowRateWad / WAD;
|
|
376
|
-
const posRate = bor.cumBorrowRateWad / WAD;
|
|
377
|
-
borrowed += posRate > 0 ? rawAmount * (reserveRate / posRate) : rawAmount;
|
|
378
|
-
}
|
|
379
|
-
const liqThreshold = supplied > 0 ? weightedCloseLtv / supplied : 0.75;
|
|
380
|
-
const openLtv = supplied > 0 ? weightedOpenLtv / supplied : 0.70;
|
|
381
|
-
const healthFactor = borrowed > 0 ? (supplied * liqThreshold) / borrowed : Infinity;
|
|
382
|
-
const maxBorrow = Math.max(0, supplied * openLtv - borrowed);
|
|
383
|
-
return { healthFactor, supplied, borrowed, maxBorrow, liquidationThreshold: liqThreshold };
|
|
384
|
-
}
|
|
385
|
-
async buildSaveTx(address, amount, asset, options) {
|
|
386
|
-
const assetKey = (asset in SUPPORTED_ASSETS ? asset : 'USDC');
|
|
387
|
-
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
388
|
-
const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
|
|
389
|
-
const reserve = this.findReserve(reserves, assetKey);
|
|
390
|
-
if (!reserve)
|
|
391
|
-
throw new T2000Error('ASSET_NOT_SUPPORTED', `${assetInfo.displayName} reserve not found on Suilend. Try: NAVI or a different asset.`);
|
|
392
|
-
const caps = await this.fetchObligationCaps(address);
|
|
393
|
-
const tx = new Transaction();
|
|
394
|
-
tx.setSender(address);
|
|
395
|
-
let capRef;
|
|
396
|
-
if (caps.length === 0) {
|
|
397
|
-
const [newCap] = tx.moveCall({
|
|
398
|
-
target: `${pkg}::lending_market::create_obligation`,
|
|
399
|
-
typeArguments: [LENDING_MARKET_TYPE],
|
|
400
|
-
arguments: [tx.object(LENDING_MARKET_ID)],
|
|
401
|
-
});
|
|
402
|
-
capRef = newCap;
|
|
403
|
-
}
|
|
404
|
-
else {
|
|
405
|
-
capRef = caps[0].id;
|
|
406
|
-
}
|
|
407
|
-
const allCoins = await this.fetchAllCoins(address, assetInfo.type);
|
|
408
|
-
if (allCoins.length === 0)
|
|
409
|
-
throw new T2000Error('INSUFFICIENT_BALANCE', `No ${assetInfo.displayName} coins found`);
|
|
410
|
-
const primaryCoinId = allCoins[0].coinObjectId;
|
|
411
|
-
if (allCoins.length > 1) {
|
|
412
|
-
tx.mergeCoins(tx.object(primaryCoinId), allCoins.slice(1).map((c) => tx.object(c.coinObjectId)));
|
|
413
|
-
}
|
|
414
|
-
const rawAmount = stableToRaw(amount, assetInfo.decimals).toString();
|
|
415
|
-
const [depositCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawAmount]);
|
|
416
|
-
if (options?.collectFee) {
|
|
417
|
-
addCollectFeeToTx(tx, depositCoin, 'save');
|
|
418
|
-
}
|
|
419
|
-
const [ctokens] = tx.moveCall({
|
|
420
|
-
target: `${pkg}::lending_market::deposit_liquidity_and_mint_ctokens`,
|
|
421
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
422
|
-
arguments: [
|
|
423
|
-
tx.object(LENDING_MARKET_ID),
|
|
424
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
425
|
-
tx.object(CLOCK),
|
|
426
|
-
depositCoin,
|
|
427
|
-
],
|
|
428
|
-
});
|
|
429
|
-
tx.moveCall({
|
|
430
|
-
target: `${pkg}::lending_market::deposit_ctokens_into_obligation`,
|
|
431
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
432
|
-
arguments: [
|
|
433
|
-
tx.object(LENDING_MARKET_ID),
|
|
434
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
435
|
-
typeof capRef === 'string' ? tx.object(capRef) : capRef,
|
|
436
|
-
tx.object(CLOCK),
|
|
437
|
-
ctokens,
|
|
438
|
-
],
|
|
439
|
-
});
|
|
440
|
-
if (typeof capRef !== 'string') {
|
|
441
|
-
tx.transferObjects([capRef], address);
|
|
442
|
-
}
|
|
443
|
-
return { tx };
|
|
444
|
-
}
|
|
445
|
-
async buildWithdrawTx(address, amount, asset) {
|
|
446
|
-
const assetKey = (asset in SUPPORTED_ASSETS ? asset : 'USDC');
|
|
447
|
-
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
448
|
-
const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves(true)]);
|
|
449
|
-
const reserve = this.findReserve(reserves, assetKey);
|
|
450
|
-
if (!reserve)
|
|
451
|
-
throw new T2000Error('ASSET_NOT_SUPPORTED', `${assetInfo.displayName} reserve not found on Suilend`);
|
|
452
|
-
const caps = await this.fetchObligationCaps(address);
|
|
453
|
-
if (caps.length === 0)
|
|
454
|
-
throw new T2000Error('NO_COLLATERAL', 'No Suilend position found');
|
|
455
|
-
const obligation = await this.fetchObligation(caps[0].obligationId);
|
|
456
|
-
const dep = obligation.deposits.find(d => d.reserveIdx === reserve.arrayIndex);
|
|
457
|
-
const ratio = cTokenRatio(reserve);
|
|
458
|
-
const deposited = dep ? (dep.ctokenAmount * ratio) / 10 ** reserve.mintDecimals : 0;
|
|
459
|
-
const effectiveAmount = Math.min(amount, deposited);
|
|
460
|
-
if (effectiveAmount <= 0)
|
|
461
|
-
throw new T2000Error('NO_COLLATERAL', `Nothing to withdraw for ${assetInfo.displayName} on Suilend`);
|
|
462
|
-
const U64_MAX = '18446744073709551615';
|
|
463
|
-
const isFullWithdraw = dep && effectiveAmount >= deposited * 0.999;
|
|
464
|
-
const withdrawArg = isFullWithdraw
|
|
465
|
-
? U64_MAX
|
|
466
|
-
: String(Math.floor(effectiveAmount * 10 ** reserve.mintDecimals / ratio));
|
|
467
|
-
const tx = new Transaction();
|
|
468
|
-
tx.setSender(address);
|
|
469
|
-
const [ctokens] = tx.moveCall({
|
|
470
|
-
target: `${pkg}::lending_market::withdraw_ctokens`,
|
|
471
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
472
|
-
arguments: [
|
|
473
|
-
tx.object(LENDING_MARKET_ID),
|
|
474
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
475
|
-
tx.object(caps[0].id),
|
|
476
|
-
tx.object(CLOCK),
|
|
477
|
-
tx.pure('u64', BigInt(withdrawArg)),
|
|
478
|
-
],
|
|
479
|
-
});
|
|
480
|
-
const coin = this.redeemCtokens(tx, pkg, reserve, assetInfo.type, assetKey, ctokens);
|
|
481
|
-
tx.transferObjects([coin], address);
|
|
482
|
-
return { tx, effectiveAmount };
|
|
483
|
-
}
|
|
484
|
-
async addWithdrawToTx(tx, address, amount, asset) {
|
|
485
|
-
const assetKey = (asset in SUPPORTED_ASSETS ? asset : 'USDC');
|
|
486
|
-
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
487
|
-
const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves(true)]);
|
|
488
|
-
const reserve = this.findReserve(reserves, assetKey);
|
|
489
|
-
if (!reserve)
|
|
490
|
-
throw new T2000Error('ASSET_NOT_SUPPORTED', `${assetInfo.displayName} reserve not found on Suilend`);
|
|
491
|
-
const caps = await this.fetchObligationCaps(address);
|
|
492
|
-
if (caps.length === 0)
|
|
493
|
-
throw new T2000Error('NO_COLLATERAL', 'No Suilend position found');
|
|
494
|
-
const obligation = await this.fetchObligation(caps[0].obligationId);
|
|
495
|
-
const dep = obligation.deposits.find(d => d.reserveIdx === reserve.arrayIndex);
|
|
496
|
-
const ratio = cTokenRatio(reserve);
|
|
497
|
-
const deposited = dep ? (dep.ctokenAmount * ratio) / 10 ** reserve.mintDecimals : 0;
|
|
498
|
-
const effectiveAmount = Math.min(amount, deposited);
|
|
499
|
-
if (effectiveAmount <= 0)
|
|
500
|
-
throw new T2000Error('NO_COLLATERAL', `Nothing to withdraw for ${assetInfo.displayName} on Suilend`);
|
|
501
|
-
const ctokenAmount = (dep && effectiveAmount >= deposited * 0.999)
|
|
502
|
-
? dep.ctokenAmount
|
|
503
|
-
: Math.floor(effectiveAmount * 10 ** reserve.mintDecimals / ratio);
|
|
504
|
-
const [ctokens] = tx.moveCall({
|
|
505
|
-
target: `${pkg}::lending_market::withdraw_ctokens`,
|
|
506
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
507
|
-
arguments: [
|
|
508
|
-
tx.object(LENDING_MARKET_ID),
|
|
509
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
510
|
-
tx.object(caps[0].id),
|
|
511
|
-
tx.object(CLOCK),
|
|
512
|
-
tx.pure.u64(ctokenAmount),
|
|
513
|
-
],
|
|
514
|
-
});
|
|
515
|
-
const coin = this.redeemCtokens(tx, pkg, reserve, assetInfo.type, assetKey, ctokens);
|
|
516
|
-
return { coin: coin, effectiveAmount };
|
|
517
|
-
}
|
|
518
|
-
/**
|
|
519
|
-
* 3-step cToken redemption matching the official Suilend SDK flow:
|
|
520
|
-
* 1. redeem_ctokens_and_withdraw_liquidity_request — creates a LiquidityRequest
|
|
521
|
-
* 2. unstake_sui_from_staker — (SUI only) unstakes from validators to replenish available_liquidity
|
|
522
|
-
* 3. fulfill_liquidity_request — splits underlying tokens from the reserve
|
|
523
|
-
*/
|
|
524
|
-
redeemCtokens(tx, pkg, reserve, coinType, assetKey, ctokens) {
|
|
525
|
-
const exemptionType = `${SUILEND_PACKAGE}::lending_market::RateLimiterExemption<${LENDING_MARKET_TYPE}, ${coinType}>`;
|
|
526
|
-
const [none] = tx.moveCall({
|
|
527
|
-
target: '0x1::option::none',
|
|
528
|
-
typeArguments: [exemptionType],
|
|
529
|
-
});
|
|
530
|
-
const [liquidityRequest] = tx.moveCall({
|
|
531
|
-
target: `${pkg}::lending_market::redeem_ctokens_and_withdraw_liquidity_request`,
|
|
532
|
-
typeArguments: [LENDING_MARKET_TYPE, coinType],
|
|
533
|
-
arguments: [
|
|
534
|
-
tx.object(LENDING_MARKET_ID),
|
|
535
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
536
|
-
tx.object(CLOCK),
|
|
537
|
-
ctokens,
|
|
538
|
-
none,
|
|
539
|
-
],
|
|
540
|
-
});
|
|
541
|
-
if (assetKey === 'SUI') {
|
|
542
|
-
tx.moveCall({
|
|
543
|
-
target: `${pkg}::lending_market::unstake_sui_from_staker`,
|
|
544
|
-
typeArguments: [LENDING_MARKET_TYPE],
|
|
545
|
-
arguments: [
|
|
546
|
-
tx.object(LENDING_MARKET_ID),
|
|
547
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
548
|
-
liquidityRequest,
|
|
549
|
-
tx.object(SUI_SYSTEM_STATE),
|
|
550
|
-
],
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
const [coin] = tx.moveCall({
|
|
554
|
-
target: `${pkg}::lending_market::fulfill_liquidity_request`,
|
|
555
|
-
typeArguments: [LENDING_MARKET_TYPE, coinType],
|
|
556
|
-
arguments: [
|
|
557
|
-
tx.object(LENDING_MARKET_ID),
|
|
558
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
559
|
-
liquidityRequest,
|
|
560
|
-
],
|
|
561
|
-
});
|
|
562
|
-
return coin;
|
|
563
|
-
}
|
|
564
|
-
async addSaveToTx(tx, address, coin, asset, options) {
|
|
565
|
-
const assetKey = (asset in SUPPORTED_ASSETS ? asset : 'USDC');
|
|
566
|
-
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
567
|
-
const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
|
|
568
|
-
const reserve = this.findReserve(reserves, assetKey);
|
|
569
|
-
if (!reserve)
|
|
570
|
-
throw new T2000Error('ASSET_NOT_SUPPORTED', `${assetInfo.displayName} reserve not found on Suilend`);
|
|
571
|
-
const caps = await this.fetchObligationCaps(address);
|
|
572
|
-
let capRef;
|
|
573
|
-
if (caps.length === 0) {
|
|
574
|
-
const [newCap] = tx.moveCall({
|
|
575
|
-
target: `${pkg}::lending_market::create_obligation`,
|
|
576
|
-
typeArguments: [LENDING_MARKET_TYPE],
|
|
577
|
-
arguments: [tx.object(LENDING_MARKET_ID)],
|
|
578
|
-
});
|
|
579
|
-
capRef = newCap;
|
|
580
|
-
}
|
|
581
|
-
else {
|
|
582
|
-
capRef = caps[0].id;
|
|
583
|
-
}
|
|
584
|
-
if (options?.collectFee) {
|
|
585
|
-
addCollectFeeToTx(tx, coin, 'save');
|
|
586
|
-
}
|
|
587
|
-
const [ctokens] = tx.moveCall({
|
|
588
|
-
target: `${pkg}::lending_market::deposit_liquidity_and_mint_ctokens`,
|
|
589
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
590
|
-
arguments: [
|
|
591
|
-
tx.object(LENDING_MARKET_ID),
|
|
592
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
593
|
-
tx.object(CLOCK),
|
|
594
|
-
coin,
|
|
595
|
-
],
|
|
596
|
-
});
|
|
597
|
-
tx.moveCall({
|
|
598
|
-
target: `${pkg}::lending_market::deposit_ctokens_into_obligation`,
|
|
599
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
600
|
-
arguments: [
|
|
601
|
-
tx.object(LENDING_MARKET_ID),
|
|
602
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
603
|
-
typeof capRef === 'string' ? tx.object(capRef) : capRef,
|
|
604
|
-
tx.object(CLOCK),
|
|
605
|
-
ctokens,
|
|
606
|
-
],
|
|
607
|
-
});
|
|
608
|
-
if (typeof capRef !== 'string') {
|
|
609
|
-
tx.transferObjects([capRef], address);
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
async buildBorrowTx(address, amount, asset, options) {
|
|
613
|
-
const assetKey = (asset in SUPPORTED_ASSETS ? asset : 'USDC');
|
|
614
|
-
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
615
|
-
const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
|
|
616
|
-
const reserve = this.findReserve(reserves, assetKey);
|
|
617
|
-
if (!reserve)
|
|
618
|
-
throw new T2000Error('ASSET_NOT_SUPPORTED', `${assetInfo.displayName} reserve not found on Suilend. Try: NAVI or a different asset.`);
|
|
619
|
-
const caps = await this.fetchObligationCaps(address);
|
|
620
|
-
if (caps.length === 0)
|
|
621
|
-
throw new T2000Error('NO_COLLATERAL', 'No Suilend position found. Deposit collateral first with: t2000 save <amount>');
|
|
622
|
-
const rawAmount = stableToRaw(amount, assetInfo.decimals);
|
|
623
|
-
const tx = new Transaction();
|
|
624
|
-
tx.setSender(address);
|
|
625
|
-
const [coin] = tx.moveCall({
|
|
626
|
-
target: `${pkg}::lending_market::borrow`,
|
|
627
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
628
|
-
arguments: [
|
|
629
|
-
tx.object(LENDING_MARKET_ID),
|
|
630
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
631
|
-
tx.object(caps[0].id),
|
|
632
|
-
tx.object(CLOCK),
|
|
633
|
-
tx.pure.u64(rawAmount),
|
|
634
|
-
],
|
|
635
|
-
});
|
|
636
|
-
if (options?.collectFee) {
|
|
637
|
-
addCollectFeeToTx(tx, coin, 'borrow');
|
|
638
|
-
}
|
|
639
|
-
tx.transferObjects([coin], address);
|
|
640
|
-
return { tx };
|
|
641
|
-
}
|
|
642
|
-
async buildRepayTx(address, amount, asset) {
|
|
643
|
-
const assetKey = (asset in SUPPORTED_ASSETS ? asset : 'USDC');
|
|
644
|
-
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
645
|
-
const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
|
|
646
|
-
const reserve = this.findReserve(reserves, assetKey);
|
|
647
|
-
if (!reserve)
|
|
648
|
-
throw new T2000Error('ASSET_NOT_SUPPORTED', `${assetInfo.displayName} reserve not found on Suilend`);
|
|
649
|
-
const caps = await this.fetchObligationCaps(address);
|
|
650
|
-
if (caps.length === 0)
|
|
651
|
-
throw new T2000Error('NO_COLLATERAL', 'No Suilend obligation found');
|
|
652
|
-
const allCoins = await this.fetchAllCoins(address, assetInfo.type);
|
|
653
|
-
if (allCoins.length === 0)
|
|
654
|
-
throw new T2000Error('INSUFFICIENT_BALANCE', `No ${assetInfo.displayName} coins to repay with`);
|
|
655
|
-
const rawAmount = stableToRaw(amount, assetInfo.decimals);
|
|
656
|
-
const tx = new Transaction();
|
|
657
|
-
tx.setSender(address);
|
|
658
|
-
const primaryCoinId = allCoins[0].coinObjectId;
|
|
659
|
-
if (allCoins.length > 1) {
|
|
660
|
-
tx.mergeCoins(tx.object(primaryCoinId), allCoins.slice(1).map((c) => tx.object(c.coinObjectId)));
|
|
661
|
-
}
|
|
662
|
-
const [repayCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawAmount.toString()]);
|
|
663
|
-
tx.moveCall({
|
|
664
|
-
target: `${pkg}::lending_market::repay`,
|
|
665
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
666
|
-
arguments: [
|
|
667
|
-
tx.object(LENDING_MARKET_ID),
|
|
668
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
669
|
-
tx.object(caps[0].id),
|
|
670
|
-
tx.object(CLOCK),
|
|
671
|
-
repayCoin,
|
|
672
|
-
],
|
|
673
|
-
});
|
|
674
|
-
return { tx };
|
|
675
|
-
}
|
|
676
|
-
async addRepayToTx(tx, address, coin, asset) {
|
|
677
|
-
const assetKey = (asset in SUPPORTED_ASSETS ? asset : 'USDC');
|
|
678
|
-
const assetInfo = SUPPORTED_ASSETS[assetKey];
|
|
679
|
-
const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
|
|
680
|
-
const reserve = this.findReserve(reserves, assetKey);
|
|
681
|
-
if (!reserve)
|
|
682
|
-
throw new T2000Error('ASSET_NOT_SUPPORTED', `${assetInfo.displayName} reserve not found on Suilend`);
|
|
683
|
-
const caps = await this.fetchObligationCaps(address);
|
|
684
|
-
if (caps.length === 0)
|
|
685
|
-
throw new T2000Error('NO_COLLATERAL', 'No Suilend obligation found');
|
|
686
|
-
tx.moveCall({
|
|
687
|
-
target: `${pkg}::lending_market::repay`,
|
|
688
|
-
typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
|
|
689
|
-
arguments: [
|
|
690
|
-
tx.object(LENDING_MARKET_ID),
|
|
691
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
692
|
-
tx.object(caps[0].id),
|
|
693
|
-
tx.object(CLOCK),
|
|
694
|
-
coin,
|
|
695
|
-
],
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
async maxWithdraw(address, _asset) {
|
|
699
|
-
const health = await this.getHealth(address);
|
|
700
|
-
let maxAmount;
|
|
701
|
-
if (health.borrowed === 0) {
|
|
702
|
-
maxAmount = health.supplied;
|
|
703
|
-
}
|
|
704
|
-
else {
|
|
705
|
-
maxAmount = Math.max(0, health.supplied - (health.borrowed * MIN_HEALTH_FACTOR) / health.liquidationThreshold);
|
|
706
|
-
}
|
|
707
|
-
const remainingSupply = health.supplied - maxAmount;
|
|
708
|
-
const hfAfter = health.borrowed > 0
|
|
709
|
-
? (remainingSupply * health.liquidationThreshold) / health.borrowed
|
|
710
|
-
: Infinity;
|
|
711
|
-
return { maxAmount, healthFactorAfter: hfAfter, currentHF: health.healthFactor };
|
|
712
|
-
}
|
|
713
|
-
async maxBorrow(address, _asset) {
|
|
714
|
-
const health = await this.getHealth(address);
|
|
715
|
-
const maxAmount = health.maxBorrow;
|
|
716
|
-
return { maxAmount, healthFactorAfter: MIN_HEALTH_FACTOR, currentHF: health.healthFactor };
|
|
717
|
-
}
|
|
718
|
-
async fetchAllCoins(owner, coinType) {
|
|
719
|
-
const all = [];
|
|
720
|
-
let cursor = null;
|
|
721
|
-
let hasNext = true;
|
|
722
|
-
while (hasNext) {
|
|
723
|
-
const page = await this.client.getCoins({ owner, coinType, cursor: cursor ?? undefined });
|
|
724
|
-
all.push(...page.data.map((c) => ({ coinObjectId: c.coinObjectId, balance: c.balance })));
|
|
725
|
-
cursor = page.nextCursor;
|
|
726
|
-
hasNext = page.hasNextPage;
|
|
727
|
-
}
|
|
728
|
-
return all;
|
|
729
|
-
}
|
|
730
|
-
// -- Claim Rewards --------------------------------------------------------
|
|
731
|
-
isClaimableReward(coinType) {
|
|
732
|
-
const ct = coinType.toLowerCase();
|
|
733
|
-
return ct.includes('spring_sui') || ct.includes('deep::deep') || ct.includes('cert::cert');
|
|
734
|
-
}
|
|
735
|
-
async getPendingRewards(address) {
|
|
736
|
-
const caps = await this.fetchObligationCaps(address);
|
|
737
|
-
if (caps.length === 0)
|
|
738
|
-
return [];
|
|
739
|
-
const [reserves, obligation] = await Promise.all([
|
|
740
|
-
this.loadReserves(true),
|
|
741
|
-
this.fetchObligation(caps[0].obligationId),
|
|
742
|
-
]);
|
|
743
|
-
const rewards = [];
|
|
744
|
-
for (const dep of obligation.deposits) {
|
|
745
|
-
const reserve = reserves[dep.reserveIdx];
|
|
746
|
-
if (!reserve)
|
|
747
|
-
continue;
|
|
748
|
-
for (const rw of reserve.depositPoolRewards) {
|
|
749
|
-
if (!this.isClaimableReward(rw.coinType))
|
|
750
|
-
continue;
|
|
751
|
-
const durationMs = rw.endTimeMs - rw.startTimeMs;
|
|
752
|
-
if (durationMs <= 0)
|
|
753
|
-
continue;
|
|
754
|
-
const assetSymbol = this.resolveSymbol(dep.coinType);
|
|
755
|
-
if (!(assetSymbol in SUPPORTED_ASSETS))
|
|
756
|
-
continue;
|
|
757
|
-
rewards.push({
|
|
758
|
-
protocol: 'suilend',
|
|
759
|
-
asset: assetSymbol,
|
|
760
|
-
coinType: rw.coinType,
|
|
761
|
-
symbol: rw.coinType.includes('spring_sui') ? 'sSUI'
|
|
762
|
-
: rw.coinType.includes('deep::') ? 'DEEP'
|
|
763
|
-
: rw.coinType.split('::').pop() ?? 'UNKNOWN',
|
|
764
|
-
amount: 0,
|
|
765
|
-
estimatedValueUsd: 0,
|
|
766
|
-
});
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
return rewards;
|
|
770
|
-
}
|
|
771
|
-
async addClaimRewardsToTx(tx, address) {
|
|
772
|
-
const caps = await this.fetchObligationCaps(address);
|
|
773
|
-
if (caps.length === 0)
|
|
774
|
-
return [];
|
|
775
|
-
const [pkg, reserves, obligation] = await Promise.all([
|
|
776
|
-
this.resolvePackage(),
|
|
777
|
-
this.loadReserves(true),
|
|
778
|
-
this.fetchObligation(caps[0].obligationId),
|
|
779
|
-
]);
|
|
780
|
-
const claimsByToken = new Map();
|
|
781
|
-
const claimed = [];
|
|
782
|
-
for (const dep of obligation.deposits) {
|
|
783
|
-
const reserve = reserves[dep.reserveIdx];
|
|
784
|
-
if (!reserve)
|
|
785
|
-
continue;
|
|
786
|
-
for (const rw of reserve.depositPoolRewards) {
|
|
787
|
-
if (!this.isClaimableReward(rw.coinType))
|
|
788
|
-
continue;
|
|
789
|
-
const [coin] = tx.moveCall({
|
|
790
|
-
target: `${pkg}::lending_market::claim_rewards`,
|
|
791
|
-
typeArguments: [LENDING_MARKET_TYPE, rw.coinType],
|
|
792
|
-
arguments: [
|
|
793
|
-
tx.object(LENDING_MARKET_ID),
|
|
794
|
-
tx.object(caps[0].id),
|
|
795
|
-
tx.object(CLOCK),
|
|
796
|
-
tx.pure.u64(reserve.arrayIndex),
|
|
797
|
-
tx.pure.u64(rw.rewardIndex),
|
|
798
|
-
tx.pure.bool(true),
|
|
799
|
-
],
|
|
800
|
-
});
|
|
801
|
-
const existing = claimsByToken.get(rw.coinType) ?? [];
|
|
802
|
-
existing.push(coin);
|
|
803
|
-
claimsByToken.set(rw.coinType, existing);
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
for (const [coinType, coins] of claimsByToken) {
|
|
807
|
-
if (coins.length > 1) {
|
|
808
|
-
tx.mergeCoins(coins[0], coins.slice(1));
|
|
809
|
-
}
|
|
810
|
-
tx.transferObjects([coins[0]], address);
|
|
811
|
-
const symbol = coinType.includes('spring_sui') ? 'SPRING_SUI'
|
|
812
|
-
: coinType.includes('deep::') ? 'DEEP'
|
|
813
|
-
: coinType.split('::').pop() ?? 'UNKNOWN';
|
|
814
|
-
claimed.push({
|
|
815
|
-
protocol: 'suilend',
|
|
816
|
-
asset: '',
|
|
817
|
-
coinType,
|
|
818
|
-
symbol,
|
|
819
|
-
amount: 0,
|
|
820
|
-
estimatedValueUsd: 0,
|
|
821
|
-
});
|
|
822
|
-
}
|
|
823
|
-
return claimed;
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
//# sourceMappingURL=suilend.js.map
|