@t2000/sdk 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/adapters/index.cjs +485 -228
- package/dist/adapters/index.cjs.map +1 -1
- package/dist/adapters/index.d.cts +2 -2
- package/dist/adapters/index.d.ts +2 -2
- package/dist/adapters/index.js +485 -228
- package/dist/adapters/index.js.map +1 -1
- package/dist/{index-BuaGAa6b.d.cts → index-DMDq8uxe.d.cts} +18 -24
- package/dist/{index-BuaGAa6b.d.ts → index-DMDq8uxe.d.ts} +18 -24
- package/dist/index.cjs +488 -233
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -16
- package/dist/index.d.ts +16 -16
- package/dist/index.js +488 -233
- package/dist/index.js.map +1 -1
- package/package.json +2 -11
package/dist/adapters/index.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var lending = require('@naviprotocol/lending');
|
|
4
3
|
var transactions = require('@mysten/sui/transactions');
|
|
4
|
+
var bcs = require('@mysten/sui/bcs');
|
|
5
5
|
var aggregatorSdk = require('@cetusprotocol/aggregator-sdk');
|
|
6
6
|
var utils = require('@mysten/sui/utils');
|
|
7
7
|
|
|
@@ -183,15 +183,59 @@ function addCollectFeeToTx(tx, paymentCoin, operation) {
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
// src/protocols/navi.ts
|
|
186
|
-
var ENV = { env: "prod" };
|
|
187
186
|
var USDC_TYPE = SUPPORTED_ASSETS.USDC.type;
|
|
188
187
|
var RATE_DECIMALS = 27;
|
|
189
188
|
var LTV_DECIMALS = 27;
|
|
190
189
|
var MIN_HEALTH_FACTOR = 1.5;
|
|
191
190
|
var WITHDRAW_DUST_BUFFER = 1e-3;
|
|
191
|
+
var CLOCK = "0x06";
|
|
192
|
+
var SUI_SYSTEM_STATE = "0x05";
|
|
192
193
|
var NAVI_BALANCE_DECIMALS = 9;
|
|
193
|
-
|
|
194
|
-
|
|
194
|
+
var CONFIG_API = "https://open-api.naviprotocol.io/api/navi/config?env=prod";
|
|
195
|
+
var POOLS_API = "https://open-api.naviprotocol.io/api/navi/pools?env=prod";
|
|
196
|
+
function toBigInt(v) {
|
|
197
|
+
if (typeof v === "bigint") return v;
|
|
198
|
+
return BigInt(String(v));
|
|
199
|
+
}
|
|
200
|
+
var UserStateInfo = bcs.bcs.struct("UserStateInfo", {
|
|
201
|
+
asset_id: bcs.bcs.u8(),
|
|
202
|
+
borrow_balance: bcs.bcs.u256(),
|
|
203
|
+
supply_balance: bcs.bcs.u256()
|
|
204
|
+
});
|
|
205
|
+
function decodeDevInspect(result, schema) {
|
|
206
|
+
const rv = result.results?.[0]?.returnValues?.[0];
|
|
207
|
+
if (result.error || !rv) return void 0;
|
|
208
|
+
const bytes = Uint8Array.from(rv[0]);
|
|
209
|
+
return schema.parse(bytes);
|
|
210
|
+
}
|
|
211
|
+
var configCache = null;
|
|
212
|
+
var poolsCache = null;
|
|
213
|
+
var CACHE_TTL = 5 * 6e4;
|
|
214
|
+
async function fetchJson(url) {
|
|
215
|
+
const res = await fetch(url);
|
|
216
|
+
if (!res.ok) throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI API error: ${res.status}`);
|
|
217
|
+
const json = await res.json();
|
|
218
|
+
return json.data ?? json;
|
|
219
|
+
}
|
|
220
|
+
async function getConfig(fresh = false) {
|
|
221
|
+
if (configCache && !fresh && Date.now() - configCache.ts < CACHE_TTL) return configCache.data;
|
|
222
|
+
const data = await fetchJson(CONFIG_API);
|
|
223
|
+
configCache = { data, ts: Date.now() };
|
|
224
|
+
return data;
|
|
225
|
+
}
|
|
226
|
+
async function getPools(fresh = false) {
|
|
227
|
+
if (poolsCache && !fresh && Date.now() - poolsCache.ts < CACHE_TTL) return poolsCache.data;
|
|
228
|
+
const data = await fetchJson(POOLS_API);
|
|
229
|
+
poolsCache = { data, ts: Date.now() };
|
|
230
|
+
return data;
|
|
231
|
+
}
|
|
232
|
+
async function getUsdcPool() {
|
|
233
|
+
const pools = await getPools();
|
|
234
|
+
const usdc = pools.find(
|
|
235
|
+
(p) => p.token?.symbol === "USDC" || p.coinType?.toLowerCase().includes("usdc")
|
|
236
|
+
);
|
|
237
|
+
if (!usdc) throw new T2000Error("PROTOCOL_UNAVAILABLE", "USDC pool not found on NAVI");
|
|
238
|
+
return usdc;
|
|
195
239
|
}
|
|
196
240
|
function rateToApy(rawRate) {
|
|
197
241
|
if (!rawRate || rawRate === "0") return 0;
|
|
@@ -207,59 +251,146 @@ function parseLiqThreshold(val) {
|
|
|
207
251
|
if (n > 1) return Number(BigInt(val)) / 10 ** LTV_DECIMALS;
|
|
208
252
|
return n;
|
|
209
253
|
}
|
|
210
|
-
function
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
);
|
|
254
|
+
function normalizeHealthFactor(raw) {
|
|
255
|
+
const v = raw / 10 ** RATE_DECIMALS;
|
|
256
|
+
return v > 1e5 ? Infinity : v;
|
|
214
257
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
258
|
+
function compoundBalance(rawBalance, currentIndex) {
|
|
259
|
+
if (!rawBalance || !currentIndex || currentIndex === "0") return 0;
|
|
260
|
+
const scale = BigInt("1" + "0".repeat(RATE_DECIMALS));
|
|
261
|
+
const half = scale / 2n;
|
|
262
|
+
const result = (rawBalance * scale + half) / BigInt(currentIndex);
|
|
263
|
+
return Number(result) / 10 ** NAVI_BALANCE_DECIMALS;
|
|
264
|
+
}
|
|
265
|
+
async function getUserState(client, address) {
|
|
266
|
+
const config = await getConfig();
|
|
267
|
+
const tx = new transactions.Transaction();
|
|
268
|
+
tx.moveCall({
|
|
269
|
+
target: `${config.uiGetter}::getter_unchecked::get_user_state`,
|
|
270
|
+
arguments: [tx.object(config.storage), tx.pure.address(address)]
|
|
271
|
+
});
|
|
272
|
+
const result = await client.devInspectTransactionBlock({
|
|
273
|
+
transactionBlock: tx,
|
|
274
|
+
sender: address
|
|
275
|
+
});
|
|
276
|
+
const decoded = decodeDevInspect(result, bcs.bcs.vector(UserStateInfo));
|
|
277
|
+
if (!decoded) return [];
|
|
278
|
+
return decoded.map((s) => ({
|
|
279
|
+
assetId: s.asset_id,
|
|
280
|
+
supplyBalance: toBigInt(s.supply_balance),
|
|
281
|
+
borrowBalance: toBigInt(s.borrow_balance)
|
|
282
|
+
})).filter((s) => s.supplyBalance !== 0n || s.borrowBalance !== 0n);
|
|
283
|
+
}
|
|
284
|
+
async function fetchCoins(client, owner, coinType) {
|
|
285
|
+
const all = [];
|
|
286
|
+
let cursor;
|
|
287
|
+
let hasNext = true;
|
|
288
|
+
while (hasNext) {
|
|
289
|
+
const page = await client.getCoins({ owner, coinType, cursor: cursor ?? void 0 });
|
|
290
|
+
all.push(...page.data.map((c) => ({ coinObjectId: c.coinObjectId, balance: c.balance })));
|
|
291
|
+
cursor = page.nextCursor;
|
|
292
|
+
hasNext = page.hasNextPage;
|
|
293
|
+
}
|
|
294
|
+
return all;
|
|
295
|
+
}
|
|
296
|
+
function mergeCoinsPtb(tx, coins, amount) {
|
|
297
|
+
if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No coins to merge");
|
|
298
|
+
const primary = tx.object(coins[0].coinObjectId);
|
|
299
|
+
if (coins.length > 1) {
|
|
300
|
+
tx.mergeCoins(primary, coins.slice(1).map((c) => tx.object(c.coinObjectId)));
|
|
226
301
|
}
|
|
302
|
+
const [split] = tx.splitCoins(primary, [amount]);
|
|
303
|
+
return split;
|
|
227
304
|
}
|
|
228
305
|
async function buildSaveTx(client, address, amount, options = {}) {
|
|
229
306
|
const rawAmount = Number(usdcToRaw(amount));
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
307
|
+
const [config, pool] = await Promise.all([getConfig(), getUsdcPool()]);
|
|
308
|
+
const coins = await fetchCoins(client, address, USDC_TYPE);
|
|
309
|
+
if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins found");
|
|
234
310
|
const tx = new transactions.Transaction();
|
|
235
311
|
tx.setSender(address);
|
|
236
|
-
const coinObj =
|
|
312
|
+
const coinObj = mergeCoinsPtb(tx, coins, rawAmount);
|
|
237
313
|
if (options.collectFee) {
|
|
238
314
|
addCollectFeeToTx(tx, coinObj, "save");
|
|
239
315
|
}
|
|
240
|
-
|
|
316
|
+
tx.moveCall({
|
|
317
|
+
target: `${config.package}::incentive_v3::entry_deposit`,
|
|
318
|
+
arguments: [
|
|
319
|
+
tx.object(CLOCK),
|
|
320
|
+
tx.object(config.storage),
|
|
321
|
+
tx.object(pool.contract.pool),
|
|
322
|
+
tx.pure.u8(pool.id),
|
|
323
|
+
coinObj,
|
|
324
|
+
tx.pure.u64(rawAmount),
|
|
325
|
+
tx.object(config.incentiveV2),
|
|
326
|
+
tx.object(config.incentiveV3)
|
|
327
|
+
],
|
|
328
|
+
typeArguments: [pool.suiCoinType]
|
|
329
|
+
});
|
|
241
330
|
return tx;
|
|
242
331
|
}
|
|
243
332
|
async function buildWithdrawTx(client, address, amount) {
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
333
|
+
const [config, pool, pools, states] = await Promise.all([
|
|
334
|
+
getConfig(),
|
|
335
|
+
getUsdcPool(),
|
|
336
|
+
getPools(),
|
|
337
|
+
getUserState(client, address)
|
|
338
|
+
]);
|
|
339
|
+
const usdcState = states.find((s) => s.assetId === pool.id);
|
|
340
|
+
const deposited = usdcState ? compoundBalance(usdcState.supplyBalance, pool.currentSupplyIndex) : 0;
|
|
247
341
|
const effectiveAmount = Math.min(amount, Math.max(0, deposited - WITHDRAW_DUST_BUFFER));
|
|
248
342
|
if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", "Nothing to withdraw");
|
|
249
343
|
const rawAmount = Number(usdcToRaw(effectiveAmount));
|
|
250
344
|
const tx = new transactions.Transaction();
|
|
251
345
|
tx.setSender(address);
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
346
|
+
const [balance] = tx.moveCall({
|
|
347
|
+
target: `${config.package}::incentive_v3::withdraw_v2`,
|
|
348
|
+
arguments: [
|
|
349
|
+
tx.object(CLOCK),
|
|
350
|
+
tx.object(config.oracle.priceOracle),
|
|
351
|
+
tx.object(config.storage),
|
|
352
|
+
tx.object(pool.contract.pool),
|
|
353
|
+
tx.pure.u8(pool.id),
|
|
354
|
+
tx.pure.u64(rawAmount),
|
|
355
|
+
tx.object(config.incentiveV2),
|
|
356
|
+
tx.object(config.incentiveV3),
|
|
357
|
+
tx.object(SUI_SYSTEM_STATE)
|
|
358
|
+
],
|
|
359
|
+
typeArguments: [pool.suiCoinType]
|
|
360
|
+
});
|
|
361
|
+
const [coin] = tx.moveCall({
|
|
362
|
+
target: "0x2::coin::from_balance",
|
|
363
|
+
arguments: [balance],
|
|
364
|
+
typeArguments: [pool.suiCoinType]
|
|
365
|
+
});
|
|
366
|
+
tx.transferObjects([coin], address);
|
|
255
367
|
return { tx, effectiveAmount };
|
|
256
368
|
}
|
|
257
369
|
async function buildBorrowTx(client, address, amount, options = {}) {
|
|
258
370
|
const rawAmount = Number(usdcToRaw(amount));
|
|
371
|
+
const [config, pool] = await Promise.all([getConfig(), getUsdcPool()]);
|
|
259
372
|
const tx = new transactions.Transaction();
|
|
260
373
|
tx.setSender(address);
|
|
261
|
-
|
|
262
|
-
|
|
374
|
+
const [balance] = tx.moveCall({
|
|
375
|
+
target: `${config.package}::incentive_v3::borrow_v2`,
|
|
376
|
+
arguments: [
|
|
377
|
+
tx.object(CLOCK),
|
|
378
|
+
tx.object(config.oracle.priceOracle),
|
|
379
|
+
tx.object(config.storage),
|
|
380
|
+
tx.object(pool.contract.pool),
|
|
381
|
+
tx.pure.u8(pool.id),
|
|
382
|
+
tx.pure.u64(rawAmount),
|
|
383
|
+
tx.object(config.incentiveV2),
|
|
384
|
+
tx.object(config.incentiveV3),
|
|
385
|
+
tx.object(SUI_SYSTEM_STATE)
|
|
386
|
+
],
|
|
387
|
+
typeArguments: [pool.suiCoinType]
|
|
388
|
+
});
|
|
389
|
+
const [borrowedCoin] = tx.moveCall({
|
|
390
|
+
target: "0x2::coin::from_balance",
|
|
391
|
+
arguments: [balance],
|
|
392
|
+
typeArguments: [pool.suiCoinType]
|
|
393
|
+
});
|
|
263
394
|
if (options.collectFee) {
|
|
264
395
|
addCollectFeeToTx(tx, borrowedCoin, "borrow");
|
|
265
396
|
}
|
|
@@ -268,31 +399,79 @@ async function buildBorrowTx(client, address, amount, options = {}) {
|
|
|
268
399
|
}
|
|
269
400
|
async function buildRepayTx(client, address, amount) {
|
|
270
401
|
const rawAmount = Number(usdcToRaw(amount));
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
402
|
+
const [config, pool] = await Promise.all([getConfig(), getUsdcPool()]);
|
|
403
|
+
const coins = await fetchCoins(client, address, USDC_TYPE);
|
|
404
|
+
if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins to repay with");
|
|
275
405
|
const tx = new transactions.Transaction();
|
|
276
406
|
tx.setSender(address);
|
|
277
|
-
const coinObj =
|
|
278
|
-
|
|
407
|
+
const coinObj = mergeCoinsPtb(tx, coins, rawAmount);
|
|
408
|
+
tx.moveCall({
|
|
409
|
+
target: `${config.package}::incentive_v3::entry_repay`,
|
|
410
|
+
arguments: [
|
|
411
|
+
tx.object(CLOCK),
|
|
412
|
+
tx.object(config.oracle.priceOracle),
|
|
413
|
+
tx.object(config.storage),
|
|
414
|
+
tx.object(pool.contract.pool),
|
|
415
|
+
tx.pure.u8(pool.id),
|
|
416
|
+
coinObj,
|
|
417
|
+
tx.pure.u64(rawAmount),
|
|
418
|
+
tx.object(config.incentiveV2),
|
|
419
|
+
tx.object(config.incentiveV3)
|
|
420
|
+
],
|
|
421
|
+
typeArguments: [pool.suiCoinType]
|
|
422
|
+
});
|
|
279
423
|
return tx;
|
|
280
424
|
}
|
|
281
425
|
async function getHealthFactor(client, addressOrKeypair) {
|
|
282
426
|
const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
|
|
283
|
-
const [
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
427
|
+
const [config, pool, states] = await Promise.all([
|
|
428
|
+
getConfig(),
|
|
429
|
+
getUsdcPool(),
|
|
430
|
+
getUserState(client, address)
|
|
287
431
|
]);
|
|
288
|
-
const
|
|
289
|
-
const supplied =
|
|
290
|
-
const borrowed =
|
|
432
|
+
const usdcState = states.find((s) => s.assetId === pool.id);
|
|
433
|
+
const supplied = usdcState ? compoundBalance(usdcState.supplyBalance, pool.currentSupplyIndex) : 0;
|
|
434
|
+
const borrowed = usdcState ? compoundBalance(usdcState.borrowBalance, pool.currentBorrowIndex) : 0;
|
|
291
435
|
const ltv = parseLtv(pool.ltv);
|
|
292
436
|
const liqThreshold = parseLiqThreshold(pool.liquidationFactor.threshold);
|
|
293
437
|
const maxBorrowVal = Math.max(0, supplied * ltv - borrowed);
|
|
438
|
+
let healthFactor;
|
|
439
|
+
if (borrowed <= 0) {
|
|
440
|
+
healthFactor = Infinity;
|
|
441
|
+
} else {
|
|
442
|
+
try {
|
|
443
|
+
const tx = new transactions.Transaction();
|
|
444
|
+
tx.moveCall({
|
|
445
|
+
target: `${config.uiGetter}::calculator_unchecked::dynamic_health_factor`,
|
|
446
|
+
arguments: [
|
|
447
|
+
tx.object(CLOCK),
|
|
448
|
+
tx.object(config.storage),
|
|
449
|
+
tx.object(config.oracle.priceOracle),
|
|
450
|
+
tx.pure.u8(pool.id),
|
|
451
|
+
tx.pure.address(address),
|
|
452
|
+
tx.pure.u8(pool.id),
|
|
453
|
+
tx.pure.u64(0),
|
|
454
|
+
tx.pure.u64(0),
|
|
455
|
+
tx.pure.bool(false)
|
|
456
|
+
],
|
|
457
|
+
typeArguments: [pool.suiCoinType]
|
|
458
|
+
});
|
|
459
|
+
const result = await client.devInspectTransactionBlock({
|
|
460
|
+
transactionBlock: tx,
|
|
461
|
+
sender: address
|
|
462
|
+
});
|
|
463
|
+
const decoded = decodeDevInspect(result, bcs.bcs.u256());
|
|
464
|
+
if (decoded !== void 0) {
|
|
465
|
+
healthFactor = normalizeHealthFactor(Number(decoded));
|
|
466
|
+
} else {
|
|
467
|
+
healthFactor = borrowed > 0 ? supplied * liqThreshold / borrowed : Infinity;
|
|
468
|
+
}
|
|
469
|
+
} catch {
|
|
470
|
+
healthFactor = borrowed > 0 ? supplied * liqThreshold / borrowed : Infinity;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
294
473
|
return {
|
|
295
|
-
healthFactor
|
|
474
|
+
healthFactor,
|
|
296
475
|
supplied,
|
|
297
476
|
borrowed,
|
|
298
477
|
maxBorrow: maxBorrowVal,
|
|
@@ -301,7 +480,7 @@ async function getHealthFactor(client, addressOrKeypair) {
|
|
|
301
480
|
}
|
|
302
481
|
async function getRates(client) {
|
|
303
482
|
try {
|
|
304
|
-
const pool = await
|
|
483
|
+
const pool = await getUsdcPool();
|
|
305
484
|
let saveApy = rateToApy(pool.currentSupplyRate);
|
|
306
485
|
let borrowApy = rateToApy(pool.currentBorrowRate);
|
|
307
486
|
if (saveApy <= 0 || saveApy > 100) saveApy = 4;
|
|
@@ -313,19 +492,21 @@ async function getRates(client) {
|
|
|
313
492
|
}
|
|
314
493
|
async function getPositions(client, addressOrKeypair) {
|
|
315
494
|
const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
|
|
316
|
-
const
|
|
495
|
+
const [states, pools] = await Promise.all([getUserState(client, address), getPools()]);
|
|
317
496
|
const positions = [];
|
|
318
|
-
for (const
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
const
|
|
497
|
+
for (const state of states) {
|
|
498
|
+
const pool = pools.find((p) => p.id === state.assetId);
|
|
499
|
+
if (!pool) continue;
|
|
500
|
+
const symbol = pool.token?.symbol ?? "UNKNOWN";
|
|
501
|
+
const supplyBal = compoundBalance(state.supplyBalance, pool.currentSupplyIndex);
|
|
502
|
+
const borrowBal = compoundBalance(state.borrowBalance, pool.currentBorrowIndex);
|
|
322
503
|
if (supplyBal > 1e-4) {
|
|
323
504
|
positions.push({
|
|
324
505
|
protocol: "navi",
|
|
325
506
|
asset: symbol,
|
|
326
507
|
type: "save",
|
|
327
508
|
amount: supplyBal,
|
|
328
|
-
apy: rateToApy(
|
|
509
|
+
apy: rateToApy(pool.currentSupplyRate)
|
|
329
510
|
});
|
|
330
511
|
}
|
|
331
512
|
if (borrowBal > 1e-4) {
|
|
@@ -334,7 +515,7 @@ async function getPositions(client, addressOrKeypair) {
|
|
|
334
515
|
asset: symbol,
|
|
335
516
|
type: "borrow",
|
|
336
517
|
amount: borrowBal,
|
|
337
|
-
apy: rateToApy(
|
|
518
|
+
apy: rateToApy(pool.currentBorrowRate)
|
|
338
519
|
});
|
|
339
520
|
}
|
|
340
521
|
}
|
|
@@ -351,21 +532,13 @@ async function maxWithdrawAmount(client, addressOrKeypair) {
|
|
|
351
532
|
}
|
|
352
533
|
const remainingSupply = hf.supplied - maxAmount;
|
|
353
534
|
const hfAfter = hf.borrowed > 0 ? remainingSupply / hf.borrowed : Infinity;
|
|
354
|
-
return {
|
|
355
|
-
maxAmount,
|
|
356
|
-
healthFactorAfter: hfAfter,
|
|
357
|
-
currentHF: hf.healthFactor
|
|
358
|
-
};
|
|
535
|
+
return { maxAmount, healthFactorAfter: hfAfter, currentHF: hf.healthFactor };
|
|
359
536
|
}
|
|
360
537
|
async function maxBorrowAmount(client, addressOrKeypair) {
|
|
361
538
|
const hf = await getHealthFactor(client, addressOrKeypair);
|
|
362
539
|
const ltv = hf.liquidationThreshold > 0 ? hf.liquidationThreshold : 0.75;
|
|
363
540
|
const maxAmount = Math.max(0, hf.supplied * ltv / MIN_HEALTH_FACTOR - hf.borrowed);
|
|
364
|
-
return {
|
|
365
|
-
maxAmount,
|
|
366
|
-
healthFactorAfter: MIN_HEALTH_FACTOR,
|
|
367
|
-
currentHF: hf.healthFactor
|
|
368
|
-
};
|
|
541
|
+
return { maxAmount, healthFactorAfter: MIN_HEALTH_FACTOR, currentHF: hf.healthFactor };
|
|
369
542
|
}
|
|
370
543
|
|
|
371
544
|
// src/adapters/navi.ts
|
|
@@ -561,6 +734,12 @@ var CetusAdapter = class {
|
|
|
561
734
|
var USDC_TYPE2 = SUPPORTED_ASSETS.USDC.type;
|
|
562
735
|
var WAD = 1e18;
|
|
563
736
|
var MIN_HEALTH_FACTOR2 = 1.5;
|
|
737
|
+
var CLOCK2 = "0x6";
|
|
738
|
+
var LENDING_MARKET_ID = "0x84030d26d85eaa7035084a057f2f11f701b7e2e4eda87551becbc7c97505ece1";
|
|
739
|
+
var LENDING_MARKET_TYPE = "0xf95b06141ed4a174f239417323bde3f209b972f5930d8521ea38a52aff3a6ddf::suilend::MAIN_POOL";
|
|
740
|
+
var SUILEND_PACKAGE = "0xf95b06141ed4a174f239417323bde3f209b972f5930d8521ea38a52aff3a6ddf";
|
|
741
|
+
var UPGRADE_CAP_ID = "0x3d4ef1859c3ee9fc72858f588b56a09da5466e64f8cc4e90a7b3b909fba8a7ae";
|
|
742
|
+
var FALLBACK_PUBLISHED_AT = "0xd2a67633ccb8de063163e25bcfca242929caf5cf1a26c2929dab519ee0b8f331";
|
|
564
743
|
function interpolateRate(utilBreakpoints, aprBreakpoints, utilizationPct) {
|
|
565
744
|
if (utilBreakpoints.length === 0) return 0;
|
|
566
745
|
if (utilizationPct <= utilBreakpoints[0]) return aprBreakpoints[0];
|
|
@@ -575,85 +754,121 @@ function interpolateRate(utilBreakpoints, aprBreakpoints, utilizationPct) {
|
|
|
575
754
|
}
|
|
576
755
|
return aprBreakpoints[aprBreakpoints.length - 1];
|
|
577
756
|
}
|
|
578
|
-
function
|
|
579
|
-
const
|
|
580
|
-
const
|
|
581
|
-
const borrowed = Number(reserve.borrowedAmount.value) / WAD / 10 ** decimals;
|
|
757
|
+
function computeRates(reserve) {
|
|
758
|
+
const available = reserve.availableAmount / 10 ** reserve.mintDecimals;
|
|
759
|
+
const borrowed = reserve.borrowedAmountWad / WAD / 10 ** reserve.mintDecimals;
|
|
582
760
|
const totalDeposited = available + borrowed;
|
|
583
761
|
const utilizationPct = totalDeposited > 0 ? borrowed / totalDeposited * 100 : 0;
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
const
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
const spreadFeeBps = Number(config.spreadFeeBps);
|
|
590
|
-
const depositAprPct = utilizationPct / 100 * (borrowAprPct / 100) * (1 - spreadFeeBps / 1e4) * 100;
|
|
591
|
-
return { borrowAprPct, depositAprPct, utilizationPct };
|
|
762
|
+
if (reserve.interestRateUtils.length === 0) return { borrowAprPct: 0, depositAprPct: 0 };
|
|
763
|
+
const aprs = reserve.interestRateAprs.map((a) => a / 100);
|
|
764
|
+
const borrowAprPct = interpolateRate(reserve.interestRateUtils, aprs, utilizationPct);
|
|
765
|
+
const depositAprPct = utilizationPct / 100 * (borrowAprPct / 100) * (1 - reserve.spreadFeeBps / 1e4) * 100;
|
|
766
|
+
return { borrowAprPct, depositAprPct };
|
|
592
767
|
}
|
|
593
768
|
function cTokenRatio(reserve) {
|
|
594
|
-
if (reserve.ctokenSupply ===
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
769
|
+
if (reserve.ctokenSupply === 0) return 1;
|
|
770
|
+
const totalSupply = reserve.availableAmount + reserve.borrowedAmountWad / WAD - reserve.unclaimedSpreadFeesWad / WAD;
|
|
771
|
+
return totalSupply / reserve.ctokenSupply;
|
|
772
|
+
}
|
|
773
|
+
function f(obj) {
|
|
774
|
+
if (obj && typeof obj === "object" && "fields" in obj) return obj.fields;
|
|
775
|
+
return obj;
|
|
776
|
+
}
|
|
777
|
+
function str(v) {
|
|
778
|
+
return String(v ?? "0");
|
|
779
|
+
}
|
|
780
|
+
function num(v) {
|
|
781
|
+
return Number(str(v));
|
|
782
|
+
}
|
|
783
|
+
function parseReserve(raw, index) {
|
|
784
|
+
const r = f(raw);
|
|
785
|
+
const coinTypeField = f(r.coin_type);
|
|
786
|
+
const config = f(f(r.config)?.element);
|
|
787
|
+
return {
|
|
788
|
+
coinType: str(coinTypeField?.name),
|
|
789
|
+
mintDecimals: num(r.mint_decimals),
|
|
790
|
+
availableAmount: num(r.available_amount),
|
|
791
|
+
borrowedAmountWad: num(f(r.borrowed_amount)?.value),
|
|
792
|
+
ctokenSupply: num(r.ctoken_supply),
|
|
793
|
+
unclaimedSpreadFeesWad: num(f(r.unclaimed_spread_fees)?.value),
|
|
794
|
+
cumulativeBorrowRateWad: num(f(r.cumulative_borrow_rate)?.value),
|
|
795
|
+
openLtvPct: num(config?.open_ltv_pct),
|
|
796
|
+
closeLtvPct: num(config?.close_ltv_pct),
|
|
797
|
+
spreadFeeBps: num(config?.spread_fee_bps),
|
|
798
|
+
interestRateUtils: Array.isArray(config?.interest_rate_utils) ? config.interest_rate_utils.map(num) : [],
|
|
799
|
+
interestRateAprs: Array.isArray(config?.interest_rate_aprs) ? config.interest_rate_aprs.map(num) : [],
|
|
800
|
+
arrayIndex: index
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
function parseObligation(raw) {
|
|
804
|
+
const deposits = Array.isArray(raw.deposits) ? raw.deposits.map((d) => {
|
|
805
|
+
const df = f(d);
|
|
806
|
+
return {
|
|
807
|
+
coinType: str(f(df.coin_type)?.name),
|
|
808
|
+
ctokenAmount: num(df.deposited_ctoken_amount),
|
|
809
|
+
reserveIdx: num(df.reserve_array_index)
|
|
810
|
+
};
|
|
811
|
+
}) : [];
|
|
812
|
+
const borrows = Array.isArray(raw.borrows) ? raw.borrows.map((b) => {
|
|
813
|
+
const bf = f(b);
|
|
814
|
+
return {
|
|
815
|
+
coinType: str(f(bf.coin_type)?.name),
|
|
816
|
+
borrowedWad: num(f(bf.borrowed_amount)?.value),
|
|
817
|
+
cumBorrowRateWad: num(f(bf.cumulative_borrow_rate)?.value),
|
|
818
|
+
reserveIdx: num(bf.reserve_array_index)
|
|
819
|
+
};
|
|
820
|
+
}) : [];
|
|
821
|
+
return { deposits, borrows };
|
|
600
822
|
}
|
|
601
823
|
var SuilendAdapter = class {
|
|
602
824
|
id = "suilend";
|
|
603
825
|
name = "Suilend";
|
|
604
|
-
version = "
|
|
826
|
+
version = "2.0.0";
|
|
605
827
|
capabilities = ["save", "withdraw"];
|
|
606
828
|
supportedAssets = ["USDC"];
|
|
607
829
|
supportsSameAssetBorrow = false;
|
|
608
830
|
client;
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
initialized = false;
|
|
612
|
-
initPromise = null;
|
|
831
|
+
publishedAt = null;
|
|
832
|
+
reserveCache = null;
|
|
613
833
|
async init(client) {
|
|
614
834
|
this.client = client;
|
|
615
|
-
await this.lazyInit();
|
|
616
835
|
}
|
|
617
836
|
initSync(client) {
|
|
618
837
|
this.client = client;
|
|
619
838
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
if (this.
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
"PROTOCOL_UNAVAILABLE",
|
|
630
|
-
"Suilend SDK not installed. Run: npm install @suilend/sdk@^1"
|
|
631
|
-
);
|
|
632
|
-
}
|
|
633
|
-
this.lendingMarketType = sdk.LENDING_MARKET_TYPE;
|
|
634
|
-
try {
|
|
635
|
-
this.suilend = await sdk.SuilendClient.initialize(
|
|
636
|
-
sdk.LENDING_MARKET_ID,
|
|
637
|
-
sdk.LENDING_MARKET_TYPE,
|
|
638
|
-
this.client
|
|
639
|
-
);
|
|
640
|
-
} catch (err) {
|
|
641
|
-
this.initPromise = null;
|
|
642
|
-
throw new T2000Error(
|
|
643
|
-
"PROTOCOL_UNAVAILABLE",
|
|
644
|
-
`Failed to initialize Suilend: ${err instanceof Error ? err.message : String(err)}`
|
|
645
|
-
);
|
|
839
|
+
// -- On-chain reads -------------------------------------------------------
|
|
840
|
+
async resolvePackage() {
|
|
841
|
+
if (this.publishedAt) return this.publishedAt;
|
|
842
|
+
try {
|
|
843
|
+
const cap = await this.client.getObject({ id: UPGRADE_CAP_ID, options: { showContent: true } });
|
|
844
|
+
if (cap.data?.content?.dataType === "moveObject") {
|
|
845
|
+
const fields = cap.data.content.fields;
|
|
846
|
+
this.publishedAt = str(fields.package);
|
|
847
|
+
return this.publishedAt;
|
|
646
848
|
}
|
|
647
|
-
|
|
648
|
-
}
|
|
649
|
-
|
|
849
|
+
} catch {
|
|
850
|
+
}
|
|
851
|
+
this.publishedAt = FALLBACK_PUBLISHED_AT;
|
|
852
|
+
return this.publishedAt;
|
|
650
853
|
}
|
|
651
|
-
async
|
|
652
|
-
if (
|
|
653
|
-
|
|
854
|
+
async loadReserves(fresh = false) {
|
|
855
|
+
if (this.reserveCache && !fresh) return this.reserveCache;
|
|
856
|
+
const market = await this.client.getObject({
|
|
857
|
+
id: LENDING_MARKET_ID,
|
|
858
|
+
options: { showContent: true }
|
|
859
|
+
});
|
|
860
|
+
if (market.data?.content?.dataType !== "moveObject") {
|
|
861
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", "Failed to read Suilend lending market");
|
|
654
862
|
}
|
|
863
|
+
const fields = market.data.content.fields;
|
|
864
|
+
const reservesRaw = fields.reserves;
|
|
865
|
+
if (!Array.isArray(reservesRaw)) {
|
|
866
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", "Failed to parse Suilend reserves");
|
|
867
|
+
}
|
|
868
|
+
this.reserveCache = reservesRaw.map((r, i) => parseReserve(r, i));
|
|
869
|
+
return this.reserveCache;
|
|
655
870
|
}
|
|
656
|
-
findReserve(asset) {
|
|
871
|
+
findReserve(reserves, asset) {
|
|
657
872
|
const upper = asset.toUpperCase();
|
|
658
873
|
let coinType;
|
|
659
874
|
if (upper === "USDC") coinType = USDC_TYPE2;
|
|
@@ -662,187 +877,229 @@ var SuilendAdapter = class {
|
|
|
662
877
|
else return void 0;
|
|
663
878
|
try {
|
|
664
879
|
const normalized = utils.normalizeStructTag(coinType);
|
|
665
|
-
return
|
|
666
|
-
|
|
667
|
-
|
|
880
|
+
return reserves.find((r) => {
|
|
881
|
+
try {
|
|
882
|
+
return utils.normalizeStructTag(r.coinType) === normalized;
|
|
883
|
+
} catch {
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
});
|
|
668
887
|
} catch {
|
|
669
888
|
return void 0;
|
|
670
889
|
}
|
|
671
890
|
}
|
|
672
|
-
async
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
891
|
+
async fetchObligationCaps(address) {
|
|
892
|
+
const capType = `${SUILEND_PACKAGE}::lending_market::ObligationOwnerCap<${LENDING_MARKET_TYPE}>`;
|
|
893
|
+
const caps = [];
|
|
894
|
+
let cursor;
|
|
895
|
+
let hasNext = true;
|
|
896
|
+
while (hasNext) {
|
|
897
|
+
const page = await this.client.getOwnedObjects({
|
|
898
|
+
owner: address,
|
|
899
|
+
filter: { StructType: capType },
|
|
900
|
+
options: { showContent: true },
|
|
901
|
+
cursor: cursor ?? void 0
|
|
902
|
+
});
|
|
903
|
+
for (const item of page.data) {
|
|
904
|
+
if (item.data?.content?.dataType !== "moveObject") continue;
|
|
905
|
+
const fields = item.data.content.fields;
|
|
906
|
+
caps.push({
|
|
907
|
+
id: item.data.objectId,
|
|
908
|
+
obligationId: str(fields.obligation_id)
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
cursor = page.nextCursor;
|
|
912
|
+
hasNext = page.hasNextPage;
|
|
913
|
+
}
|
|
914
|
+
return caps;
|
|
915
|
+
}
|
|
916
|
+
async fetchObligation(obligationId) {
|
|
917
|
+
const obj = await this.client.getObject({ id: obligationId, options: { showContent: true } });
|
|
918
|
+
if (obj.data?.content?.dataType !== "moveObject") {
|
|
919
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", "Failed to read Suilend obligation");
|
|
920
|
+
}
|
|
921
|
+
return parseObligation(obj.data.content.fields);
|
|
679
922
|
}
|
|
680
923
|
resolveSymbol(coinType) {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
924
|
+
try {
|
|
925
|
+
const normalized = utils.normalizeStructTag(coinType);
|
|
926
|
+
if (normalized === utils.normalizeStructTag(USDC_TYPE2)) return "USDC";
|
|
927
|
+
if (normalized === utils.normalizeStructTag("0x2::sui::SUI")) return "SUI";
|
|
928
|
+
} catch {
|
|
929
|
+
}
|
|
684
930
|
const parts = coinType.split("::");
|
|
685
931
|
return parts[parts.length - 1] || "UNKNOWN";
|
|
686
932
|
}
|
|
933
|
+
// -- Adapter interface ----------------------------------------------------
|
|
687
934
|
async getRates(asset) {
|
|
688
|
-
await this.
|
|
689
|
-
const reserve = this.findReserve(asset);
|
|
690
|
-
if (!reserve) {
|
|
691
|
-
|
|
692
|
-
}
|
|
693
|
-
const { borrowAprPct, depositAprPct } = computeRatesFromReserve(reserve);
|
|
694
|
-
return {
|
|
695
|
-
asset,
|
|
696
|
-
saveApy: depositAprPct,
|
|
697
|
-
borrowApy: borrowAprPct
|
|
698
|
-
};
|
|
935
|
+
const reserves = await this.loadReserves();
|
|
936
|
+
const reserve = this.findReserve(reserves, asset);
|
|
937
|
+
if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `Suilend does not support ${asset}`);
|
|
938
|
+
const { borrowAprPct, depositAprPct } = computeRates(reserve);
|
|
939
|
+
return { asset, saveApy: depositAprPct, borrowApy: borrowAprPct };
|
|
699
940
|
}
|
|
700
941
|
async getPositions(address) {
|
|
701
|
-
await this.ensureInit();
|
|
702
942
|
const supplies = [];
|
|
703
943
|
const borrows = [];
|
|
704
|
-
const caps = await this.
|
|
944
|
+
const caps = await this.fetchObligationCaps(address);
|
|
705
945
|
if (caps.length === 0) return { supplies, borrows };
|
|
706
|
-
const obligation = await
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
946
|
+
const [reserves, obligation] = await Promise.all([
|
|
947
|
+
this.loadReserves(),
|
|
948
|
+
this.fetchObligation(caps[0].obligationId)
|
|
949
|
+
]);
|
|
950
|
+
for (const dep of obligation.deposits) {
|
|
951
|
+
const reserve = reserves[dep.reserveIdx];
|
|
712
952
|
if (!reserve) continue;
|
|
713
|
-
const ctokenAmount = Number(deposit.depositedCtokenAmount.toString());
|
|
714
953
|
const ratio = cTokenRatio(reserve);
|
|
715
|
-
const amount = ctokenAmount * ratio / 10 ** reserve.mintDecimals;
|
|
716
|
-
const { depositAprPct } =
|
|
717
|
-
supplies.push({ asset: this.resolveSymbol(coinType), amount, apy: depositAprPct });
|
|
954
|
+
const amount = dep.ctokenAmount * ratio / 10 ** reserve.mintDecimals;
|
|
955
|
+
const { depositAprPct } = computeRates(reserve);
|
|
956
|
+
supplies.push({ asset: this.resolveSymbol(dep.coinType), amount, apy: depositAprPct });
|
|
718
957
|
}
|
|
719
|
-
for (const
|
|
720
|
-
const
|
|
721
|
-
const reserve = this.suilend.lendingMarket.reserves.find(
|
|
722
|
-
(r) => utils.normalizeStructTag(r.coinType.name) === coinType
|
|
723
|
-
);
|
|
958
|
+
for (const bor of obligation.borrows) {
|
|
959
|
+
const reserve = reserves[bor.reserveIdx];
|
|
724
960
|
if (!reserve) continue;
|
|
725
|
-
const
|
|
726
|
-
const
|
|
727
|
-
const
|
|
728
|
-
const
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
borrows.push({ asset: this.resolveSymbol(coinType), amount: compounded, apy: borrowAprPct });
|
|
961
|
+
const rawAmount = bor.borrowedWad / WAD / 10 ** reserve.mintDecimals;
|
|
962
|
+
const reserveRate = reserve.cumulativeBorrowRateWad / WAD;
|
|
963
|
+
const posRate = bor.cumBorrowRateWad / WAD;
|
|
964
|
+
const compounded = posRate > 0 ? rawAmount * (reserveRate / posRate) : rawAmount;
|
|
965
|
+
const { borrowAprPct } = computeRates(reserve);
|
|
966
|
+
borrows.push({ asset: this.resolveSymbol(bor.coinType), amount: compounded, apy: borrowAprPct });
|
|
732
967
|
}
|
|
733
968
|
return { supplies, borrows };
|
|
734
969
|
}
|
|
735
970
|
async getHealth(address) {
|
|
736
|
-
await this.
|
|
737
|
-
const caps = await this.getObligationCaps(address);
|
|
971
|
+
const caps = await this.fetchObligationCaps(address);
|
|
738
972
|
if (caps.length === 0) {
|
|
739
973
|
return { healthFactor: Infinity, supplied: 0, borrowed: 0, maxBorrow: 0, liquidationThreshold: 0 };
|
|
740
974
|
}
|
|
741
975
|
const positions = await this.getPositions(address);
|
|
742
976
|
const supplied = positions.supplies.reduce((s, p) => s + p.amount, 0);
|
|
743
977
|
const borrowed = positions.borrows.reduce((s, p) => s + p.amount, 0);
|
|
744
|
-
const
|
|
745
|
-
const
|
|
746
|
-
const
|
|
978
|
+
const reserves = await this.loadReserves();
|
|
979
|
+
const reserve = this.findReserve(reserves, "USDC");
|
|
980
|
+
const closeLtv = reserve?.closeLtvPct ?? 75;
|
|
981
|
+
const openLtv = reserve?.openLtvPct ?? 70;
|
|
747
982
|
const liqThreshold = closeLtv / 100;
|
|
748
983
|
const healthFactor = borrowed > 0 ? supplied * liqThreshold / borrowed : Infinity;
|
|
749
984
|
const maxBorrow = Math.max(0, supplied * (openLtv / 100) - borrowed);
|
|
750
985
|
return { healthFactor, supplied, borrowed, maxBorrow, liquidationThreshold: liqThreshold };
|
|
751
986
|
}
|
|
752
987
|
async buildSaveTx(address, amount, _asset, options) {
|
|
753
|
-
await this.
|
|
754
|
-
const
|
|
988
|
+
const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
|
|
989
|
+
const usdcReserve = this.findReserve(reserves, "USDC");
|
|
990
|
+
if (!usdcReserve) throw new T2000Error("ASSET_NOT_SUPPORTED", "USDC reserve not found on Suilend");
|
|
991
|
+
const caps = await this.fetchObligationCaps(address);
|
|
755
992
|
const tx = new transactions.Transaction();
|
|
756
993
|
tx.setSender(address);
|
|
757
|
-
const caps = await this.getObligationCaps(address);
|
|
758
994
|
let capRef;
|
|
759
995
|
if (caps.length === 0) {
|
|
760
|
-
const [newCap] =
|
|
996
|
+
const [newCap] = tx.moveCall({
|
|
997
|
+
target: `${pkg}::lending_market::create_obligation`,
|
|
998
|
+
typeArguments: [LENDING_MARKET_TYPE],
|
|
999
|
+
arguments: [tx.object(LENDING_MARKET_ID)]
|
|
1000
|
+
});
|
|
761
1001
|
capRef = newCap;
|
|
762
1002
|
} else {
|
|
763
1003
|
capRef = caps[0].id;
|
|
764
1004
|
}
|
|
765
1005
|
const allCoins = await this.fetchAllCoins(address, USDC_TYPE2);
|
|
766
|
-
if (allCoins.length === 0)
|
|
767
|
-
throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins found");
|
|
768
|
-
}
|
|
1006
|
+
if (allCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins found");
|
|
769
1007
|
const primaryCoinId = allCoins[0].coinObjectId;
|
|
770
1008
|
if (allCoins.length > 1) {
|
|
771
|
-
tx.mergeCoins(
|
|
772
|
-
tx.object(primaryCoinId),
|
|
773
|
-
allCoins.slice(1).map((c) => tx.object(c.coinObjectId))
|
|
774
|
-
);
|
|
1009
|
+
tx.mergeCoins(tx.object(primaryCoinId), allCoins.slice(1).map((c) => tx.object(c.coinObjectId)));
|
|
775
1010
|
}
|
|
1011
|
+
const rawAmount = usdcToRaw(amount).toString();
|
|
776
1012
|
const [depositCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawAmount]);
|
|
777
1013
|
if (options?.collectFee) {
|
|
778
1014
|
addCollectFeeToTx(tx, depositCoin, "save");
|
|
779
1015
|
}
|
|
780
|
-
|
|
1016
|
+
const [ctokens] = tx.moveCall({
|
|
1017
|
+
target: `${pkg}::lending_market::deposit_liquidity_and_mint_ctokens`,
|
|
1018
|
+
typeArguments: [LENDING_MARKET_TYPE, USDC_TYPE2],
|
|
1019
|
+
arguments: [
|
|
1020
|
+
tx.object(LENDING_MARKET_ID),
|
|
1021
|
+
tx.pure.u64(usdcReserve.arrayIndex),
|
|
1022
|
+
tx.object(CLOCK2),
|
|
1023
|
+
depositCoin
|
|
1024
|
+
]
|
|
1025
|
+
});
|
|
1026
|
+
tx.moveCall({
|
|
1027
|
+
target: `${pkg}::lending_market::deposit_ctokens_into_obligation`,
|
|
1028
|
+
typeArguments: [LENDING_MARKET_TYPE, USDC_TYPE2],
|
|
1029
|
+
arguments: [
|
|
1030
|
+
tx.object(LENDING_MARKET_ID),
|
|
1031
|
+
tx.pure.u64(usdcReserve.arrayIndex),
|
|
1032
|
+
typeof capRef === "string" ? tx.object(capRef) : capRef,
|
|
1033
|
+
tx.object(CLOCK2),
|
|
1034
|
+
ctokens
|
|
1035
|
+
]
|
|
1036
|
+
});
|
|
781
1037
|
return { tx };
|
|
782
1038
|
}
|
|
783
1039
|
async buildWithdrawTx(address, amount, _asset) {
|
|
784
|
-
await this.
|
|
785
|
-
const
|
|
786
|
-
if (
|
|
787
|
-
|
|
788
|
-
|
|
1040
|
+
const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves(true)]);
|
|
1041
|
+
const usdcReserve = this.findReserve(reserves, "USDC");
|
|
1042
|
+
if (!usdcReserve) throw new T2000Error("ASSET_NOT_SUPPORTED", "USDC reserve not found on Suilend");
|
|
1043
|
+
const caps = await this.fetchObligationCaps(address);
|
|
1044
|
+
if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found");
|
|
789
1045
|
const positions = await this.getPositions(address);
|
|
790
|
-
const
|
|
791
|
-
const deposited = usdcSupply?.amount ?? 0;
|
|
1046
|
+
const deposited = positions.supplies.find((s) => s.asset === "USDC")?.amount ?? 0;
|
|
792
1047
|
const effectiveAmount = Math.min(amount, deposited);
|
|
793
|
-
if (effectiveAmount <= 0)
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
const rawAmount = usdcToRaw(effectiveAmount).toString();
|
|
1048
|
+
if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", "Nothing to withdraw from Suilend");
|
|
1049
|
+
const ratio = cTokenRatio(usdcReserve);
|
|
1050
|
+
const ctokenAmount = Math.ceil(effectiveAmount * 10 ** usdcReserve.mintDecimals / ratio);
|
|
797
1051
|
const tx = new transactions.Transaction();
|
|
798
1052
|
tx.setSender(address);
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
1053
|
+
const [ctokens] = tx.moveCall({
|
|
1054
|
+
target: `${pkg}::lending_market::withdraw_ctokens`,
|
|
1055
|
+
typeArguments: [LENDING_MARKET_TYPE, USDC_TYPE2],
|
|
1056
|
+
arguments: [
|
|
1057
|
+
tx.object(LENDING_MARKET_ID),
|
|
1058
|
+
tx.pure.u64(usdcReserve.arrayIndex),
|
|
1059
|
+
tx.object(caps[0].id),
|
|
1060
|
+
tx.object(CLOCK2),
|
|
1061
|
+
tx.pure.u64(ctokenAmount)
|
|
1062
|
+
]
|
|
1063
|
+
});
|
|
1064
|
+
const exemptionType = `${SUILEND_PACKAGE}::lending_market::RateLimiterExemption<${LENDING_MARKET_TYPE}, ${USDC_TYPE2}>`;
|
|
1065
|
+
const [none] = tx.moveCall({
|
|
1066
|
+
target: "0x1::option::none",
|
|
1067
|
+
typeArguments: [exemptionType]
|
|
1068
|
+
});
|
|
1069
|
+
const [coin] = tx.moveCall({
|
|
1070
|
+
target: `${pkg}::lending_market::redeem_ctokens_and_withdraw_liquidity`,
|
|
1071
|
+
typeArguments: [LENDING_MARKET_TYPE, USDC_TYPE2],
|
|
1072
|
+
arguments: [
|
|
1073
|
+
tx.object(LENDING_MARKET_ID),
|
|
1074
|
+
tx.pure.u64(usdcReserve.arrayIndex),
|
|
1075
|
+
tx.object(CLOCK2),
|
|
1076
|
+
ctokens,
|
|
1077
|
+
none
|
|
1078
|
+
]
|
|
1079
|
+
});
|
|
1080
|
+
tx.transferObjects([coin], address);
|
|
807
1081
|
return { tx, effectiveAmount };
|
|
808
1082
|
}
|
|
809
1083
|
async buildBorrowTx(_address, _amount, _asset, _options) {
|
|
810
|
-
throw new T2000Error(
|
|
811
|
-
"ASSET_NOT_SUPPORTED",
|
|
812
|
-
"SuilendAdapter.buildBorrowTx() not available \u2014 Suilend requires different collateral/borrow assets. Deferred to Phase 10."
|
|
813
|
-
);
|
|
1084
|
+
throw new T2000Error("ASSET_NOT_SUPPORTED", "Suilend borrow requires different collateral/borrow assets. Deferred to Phase 10.");
|
|
814
1085
|
}
|
|
815
1086
|
async buildRepayTx(_address, _amount, _asset) {
|
|
816
|
-
throw new T2000Error(
|
|
817
|
-
"ASSET_NOT_SUPPORTED",
|
|
818
|
-
"SuilendAdapter.buildRepayTx() not available \u2014 deferred to Phase 10."
|
|
819
|
-
);
|
|
1087
|
+
throw new T2000Error("ASSET_NOT_SUPPORTED", "Suilend repay deferred to Phase 10.");
|
|
820
1088
|
}
|
|
821
1089
|
async maxWithdraw(address, _asset) {
|
|
822
|
-
await this.ensureInit();
|
|
823
1090
|
const health = await this.getHealth(address);
|
|
824
1091
|
let maxAmount;
|
|
825
1092
|
if (health.borrowed === 0) {
|
|
826
1093
|
maxAmount = health.supplied;
|
|
827
1094
|
} else {
|
|
828
|
-
maxAmount = Math.max(
|
|
829
|
-
0,
|
|
830
|
-
health.supplied - health.borrowed * MIN_HEALTH_FACTOR2 / health.liquidationThreshold
|
|
831
|
-
);
|
|
1095
|
+
maxAmount = Math.max(0, health.supplied - health.borrowed * MIN_HEALTH_FACTOR2 / health.liquidationThreshold);
|
|
832
1096
|
}
|
|
833
1097
|
const remainingSupply = health.supplied - maxAmount;
|
|
834
1098
|
const hfAfter = health.borrowed > 0 ? remainingSupply * health.liquidationThreshold / health.borrowed : Infinity;
|
|
835
|
-
return {
|
|
836
|
-
maxAmount,
|
|
837
|
-
healthFactorAfter: hfAfter,
|
|
838
|
-
currentHF: health.healthFactor
|
|
839
|
-
};
|
|
1099
|
+
return { maxAmount, healthFactorAfter: hfAfter, currentHF: health.healthFactor };
|
|
840
1100
|
}
|
|
841
1101
|
async maxBorrow(_address, _asset) {
|
|
842
|
-
throw new T2000Error(
|
|
843
|
-
"ASSET_NOT_SUPPORTED",
|
|
844
|
-
"SuilendAdapter.maxBorrow() not available \u2014 deferred to Phase 10."
|
|
845
|
-
);
|
|
1102
|
+
throw new T2000Error("ASSET_NOT_SUPPORTED", "Suilend maxBorrow deferred to Phase 10.");
|
|
846
1103
|
}
|
|
847
1104
|
async fetchAllCoins(owner, coinType) {
|
|
848
1105
|
const all = [];
|