@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/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from 'eventemitter3';
|
|
2
|
-
import {
|
|
2
|
+
import { SuiJsonRpcClient } from '@mysten/sui/jsonRpc';
|
|
3
3
|
import { normalizeSuiAddress, isValidSuiAddress, normalizeStructTag } from '@mysten/sui/utils';
|
|
4
4
|
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
|
|
5
5
|
import { decodeSuiPrivateKey } from '@mysten/sui/cryptography';
|
|
@@ -8,7 +8,6 @@ import { access, mkdir, writeFile, readFile } from 'fs/promises';
|
|
|
8
8
|
import { dirname, resolve } from 'path';
|
|
9
9
|
import { homedir } from 'os';
|
|
10
10
|
import { Transaction } from '@mysten/sui/transactions';
|
|
11
|
-
import { getPool, getLendingState, getHealthFactor as getHealthFactor$1, getCoins, mergeCoinsPTB, depositCoinPTB, withdrawCoinPTB, borrowCoinPTB, repayCoinPTB, getPriceFeeds, filterPriceFeeds, updateOraclePricesPTB } from '@naviprotocol/lending';
|
|
12
11
|
import { bcs } from '@mysten/sui/bcs';
|
|
13
12
|
import { AggregatorClient, Env } from '@cetusprotocol/aggregator-sdk';
|
|
14
13
|
|
|
@@ -111,7 +110,7 @@ var cachedClient = null;
|
|
|
111
110
|
function getSuiClient(rpcUrl) {
|
|
112
111
|
const url = rpcUrl ?? DEFAULT_RPC_URL;
|
|
113
112
|
if (cachedClient) return cachedClient;
|
|
114
|
-
cachedClient = new
|
|
113
|
+
cachedClient = new SuiJsonRpcClient({ url, network: "mainnet" });
|
|
115
114
|
return cachedClient;
|
|
116
115
|
}
|
|
117
116
|
function validateAddress(address) {
|
|
@@ -427,15 +426,59 @@ async function reportFee(agentAddress, operation, feeAmount, feeRate, txDigest)
|
|
|
427
426
|
}
|
|
428
427
|
|
|
429
428
|
// src/protocols/navi.ts
|
|
430
|
-
var ENV = { env: "prod" };
|
|
431
429
|
var USDC_TYPE = SUPPORTED_ASSETS.USDC.type;
|
|
432
430
|
var RATE_DECIMALS = 27;
|
|
433
431
|
var LTV_DECIMALS = 27;
|
|
434
432
|
var MIN_HEALTH_FACTOR = 1.5;
|
|
435
433
|
var WITHDRAW_DUST_BUFFER = 1e-3;
|
|
434
|
+
var CLOCK = "0x06";
|
|
435
|
+
var SUI_SYSTEM_STATE = "0x05";
|
|
436
436
|
var NAVI_BALANCE_DECIMALS = 9;
|
|
437
|
-
|
|
438
|
-
|
|
437
|
+
var CONFIG_API = "https://open-api.naviprotocol.io/api/navi/config?env=prod";
|
|
438
|
+
var POOLS_API = "https://open-api.naviprotocol.io/api/navi/pools?env=prod";
|
|
439
|
+
function toBigInt(v) {
|
|
440
|
+
if (typeof v === "bigint") return v;
|
|
441
|
+
return BigInt(String(v));
|
|
442
|
+
}
|
|
443
|
+
var UserStateInfo = bcs.struct("UserStateInfo", {
|
|
444
|
+
asset_id: bcs.u8(),
|
|
445
|
+
borrow_balance: bcs.u256(),
|
|
446
|
+
supply_balance: bcs.u256()
|
|
447
|
+
});
|
|
448
|
+
function decodeDevInspect(result, schema) {
|
|
449
|
+
const rv = result.results?.[0]?.returnValues?.[0];
|
|
450
|
+
if (result.error || !rv) return void 0;
|
|
451
|
+
const bytes = Uint8Array.from(rv[0]);
|
|
452
|
+
return schema.parse(bytes);
|
|
453
|
+
}
|
|
454
|
+
var configCache = null;
|
|
455
|
+
var poolsCache = null;
|
|
456
|
+
var CACHE_TTL = 5 * 6e4;
|
|
457
|
+
async function fetchJson(url) {
|
|
458
|
+
const res = await fetch(url);
|
|
459
|
+
if (!res.ok) throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI API error: ${res.status}`);
|
|
460
|
+
const json = await res.json();
|
|
461
|
+
return json.data ?? json;
|
|
462
|
+
}
|
|
463
|
+
async function getConfig(fresh = false) {
|
|
464
|
+
if (configCache && !fresh && Date.now() - configCache.ts < CACHE_TTL) return configCache.data;
|
|
465
|
+
const data = await fetchJson(CONFIG_API);
|
|
466
|
+
configCache = { data, ts: Date.now() };
|
|
467
|
+
return data;
|
|
468
|
+
}
|
|
469
|
+
async function getPools(fresh = false) {
|
|
470
|
+
if (poolsCache && !fresh && Date.now() - poolsCache.ts < CACHE_TTL) return poolsCache.data;
|
|
471
|
+
const data = await fetchJson(POOLS_API);
|
|
472
|
+
poolsCache = { data, ts: Date.now() };
|
|
473
|
+
return data;
|
|
474
|
+
}
|
|
475
|
+
async function getUsdcPool() {
|
|
476
|
+
const pools = await getPools();
|
|
477
|
+
const usdc = pools.find(
|
|
478
|
+
(p) => p.token?.symbol === "USDC" || p.coinType?.toLowerCase().includes("usdc")
|
|
479
|
+
);
|
|
480
|
+
if (!usdc) throw new T2000Error("PROTOCOL_UNAVAILABLE", "USDC pool not found on NAVI");
|
|
481
|
+
return usdc;
|
|
439
482
|
}
|
|
440
483
|
function rateToApy(rawRate) {
|
|
441
484
|
if (!rawRate || rawRate === "0") return 0;
|
|
@@ -451,59 +494,146 @@ function parseLiqThreshold(val) {
|
|
|
451
494
|
if (n > 1) return Number(BigInt(val)) / 10 ** LTV_DECIMALS;
|
|
452
495
|
return n;
|
|
453
496
|
}
|
|
454
|
-
function
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
);
|
|
497
|
+
function normalizeHealthFactor(raw) {
|
|
498
|
+
const v = raw / 10 ** RATE_DECIMALS;
|
|
499
|
+
return v > 1e5 ? Infinity : v;
|
|
458
500
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
501
|
+
function compoundBalance(rawBalance, currentIndex) {
|
|
502
|
+
if (!rawBalance || !currentIndex || currentIndex === "0") return 0;
|
|
503
|
+
const scale = BigInt("1" + "0".repeat(RATE_DECIMALS));
|
|
504
|
+
const half = scale / 2n;
|
|
505
|
+
const result = (rawBalance * scale + half) / BigInt(currentIndex);
|
|
506
|
+
return Number(result) / 10 ** NAVI_BALANCE_DECIMALS;
|
|
507
|
+
}
|
|
508
|
+
async function getUserState(client, address) {
|
|
509
|
+
const config = await getConfig();
|
|
510
|
+
const tx = new Transaction();
|
|
511
|
+
tx.moveCall({
|
|
512
|
+
target: `${config.uiGetter}::getter_unchecked::get_user_state`,
|
|
513
|
+
arguments: [tx.object(config.storage), tx.pure.address(address)]
|
|
514
|
+
});
|
|
515
|
+
const result = await client.devInspectTransactionBlock({
|
|
516
|
+
transactionBlock: tx,
|
|
517
|
+
sender: address
|
|
518
|
+
});
|
|
519
|
+
const decoded = decodeDevInspect(result, bcs.vector(UserStateInfo));
|
|
520
|
+
if (!decoded) return [];
|
|
521
|
+
return decoded.map((s) => ({
|
|
522
|
+
assetId: s.asset_id,
|
|
523
|
+
supplyBalance: toBigInt(s.supply_balance),
|
|
524
|
+
borrowBalance: toBigInt(s.borrow_balance)
|
|
525
|
+
})).filter((s) => s.supplyBalance !== 0n || s.borrowBalance !== 0n);
|
|
526
|
+
}
|
|
527
|
+
async function fetchCoins(client, owner, coinType) {
|
|
528
|
+
const all = [];
|
|
529
|
+
let cursor;
|
|
530
|
+
let hasNext = true;
|
|
531
|
+
while (hasNext) {
|
|
532
|
+
const page = await client.getCoins({ owner, coinType, cursor: cursor ?? void 0 });
|
|
533
|
+
all.push(...page.data.map((c) => ({ coinObjectId: c.coinObjectId, balance: c.balance })));
|
|
534
|
+
cursor = page.nextCursor;
|
|
535
|
+
hasNext = page.hasNextPage;
|
|
536
|
+
}
|
|
537
|
+
return all;
|
|
538
|
+
}
|
|
539
|
+
function mergeCoinsPtb(tx, coins, amount) {
|
|
540
|
+
if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No coins to merge");
|
|
541
|
+
const primary = tx.object(coins[0].coinObjectId);
|
|
542
|
+
if (coins.length > 1) {
|
|
543
|
+
tx.mergeCoins(primary, coins.slice(1).map((c) => tx.object(c.coinObjectId)));
|
|
544
|
+
}
|
|
545
|
+
const [split] = tx.splitCoins(primary, [amount]);
|
|
546
|
+
return split;
|
|
471
547
|
}
|
|
472
548
|
async function buildSaveTx(client, address, amount, options = {}) {
|
|
473
549
|
const rawAmount = Number(usdcToRaw(amount));
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
550
|
+
const [config, pool] = await Promise.all([getConfig(), getUsdcPool()]);
|
|
551
|
+
const coins = await fetchCoins(client, address, USDC_TYPE);
|
|
552
|
+
if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins found");
|
|
478
553
|
const tx = new Transaction();
|
|
479
554
|
tx.setSender(address);
|
|
480
|
-
const coinObj =
|
|
555
|
+
const coinObj = mergeCoinsPtb(tx, coins, rawAmount);
|
|
481
556
|
if (options.collectFee) {
|
|
482
557
|
addCollectFeeToTx(tx, coinObj, "save");
|
|
483
558
|
}
|
|
484
|
-
|
|
559
|
+
tx.moveCall({
|
|
560
|
+
target: `${config.package}::incentive_v3::entry_deposit`,
|
|
561
|
+
arguments: [
|
|
562
|
+
tx.object(CLOCK),
|
|
563
|
+
tx.object(config.storage),
|
|
564
|
+
tx.object(pool.contract.pool),
|
|
565
|
+
tx.pure.u8(pool.id),
|
|
566
|
+
coinObj,
|
|
567
|
+
tx.pure.u64(rawAmount),
|
|
568
|
+
tx.object(config.incentiveV2),
|
|
569
|
+
tx.object(config.incentiveV3)
|
|
570
|
+
],
|
|
571
|
+
typeArguments: [pool.suiCoinType]
|
|
572
|
+
});
|
|
485
573
|
return tx;
|
|
486
574
|
}
|
|
487
575
|
async function buildWithdrawTx(client, address, amount) {
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
576
|
+
const [config, pool, pools, states] = await Promise.all([
|
|
577
|
+
getConfig(),
|
|
578
|
+
getUsdcPool(),
|
|
579
|
+
getPools(),
|
|
580
|
+
getUserState(client, address)
|
|
581
|
+
]);
|
|
582
|
+
const usdcState = states.find((s) => s.assetId === pool.id);
|
|
583
|
+
const deposited = usdcState ? compoundBalance(usdcState.supplyBalance, pool.currentSupplyIndex) : 0;
|
|
491
584
|
const effectiveAmount = Math.min(amount, Math.max(0, deposited - WITHDRAW_DUST_BUFFER));
|
|
492
585
|
if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", "Nothing to withdraw");
|
|
493
586
|
const rawAmount = Number(usdcToRaw(effectiveAmount));
|
|
494
587
|
const tx = new Transaction();
|
|
495
588
|
tx.setSender(address);
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
589
|
+
const [balance] = tx.moveCall({
|
|
590
|
+
target: `${config.package}::incentive_v3::withdraw_v2`,
|
|
591
|
+
arguments: [
|
|
592
|
+
tx.object(CLOCK),
|
|
593
|
+
tx.object(config.oracle.priceOracle),
|
|
594
|
+
tx.object(config.storage),
|
|
595
|
+
tx.object(pool.contract.pool),
|
|
596
|
+
tx.pure.u8(pool.id),
|
|
597
|
+
tx.pure.u64(rawAmount),
|
|
598
|
+
tx.object(config.incentiveV2),
|
|
599
|
+
tx.object(config.incentiveV3),
|
|
600
|
+
tx.object(SUI_SYSTEM_STATE)
|
|
601
|
+
],
|
|
602
|
+
typeArguments: [pool.suiCoinType]
|
|
603
|
+
});
|
|
604
|
+
const [coin] = tx.moveCall({
|
|
605
|
+
target: "0x2::coin::from_balance",
|
|
606
|
+
arguments: [balance],
|
|
607
|
+
typeArguments: [pool.suiCoinType]
|
|
608
|
+
});
|
|
609
|
+
tx.transferObjects([coin], address);
|
|
499
610
|
return { tx, effectiveAmount };
|
|
500
611
|
}
|
|
501
612
|
async function buildBorrowTx(client, address, amount, options = {}) {
|
|
502
613
|
const rawAmount = Number(usdcToRaw(amount));
|
|
614
|
+
const [config, pool] = await Promise.all([getConfig(), getUsdcPool()]);
|
|
503
615
|
const tx = new Transaction();
|
|
504
616
|
tx.setSender(address);
|
|
505
|
-
|
|
506
|
-
|
|
617
|
+
const [balance] = tx.moveCall({
|
|
618
|
+
target: `${config.package}::incentive_v3::borrow_v2`,
|
|
619
|
+
arguments: [
|
|
620
|
+
tx.object(CLOCK),
|
|
621
|
+
tx.object(config.oracle.priceOracle),
|
|
622
|
+
tx.object(config.storage),
|
|
623
|
+
tx.object(pool.contract.pool),
|
|
624
|
+
tx.pure.u8(pool.id),
|
|
625
|
+
tx.pure.u64(rawAmount),
|
|
626
|
+
tx.object(config.incentiveV2),
|
|
627
|
+
tx.object(config.incentiveV3),
|
|
628
|
+
tx.object(SUI_SYSTEM_STATE)
|
|
629
|
+
],
|
|
630
|
+
typeArguments: [pool.suiCoinType]
|
|
631
|
+
});
|
|
632
|
+
const [borrowedCoin] = tx.moveCall({
|
|
633
|
+
target: "0x2::coin::from_balance",
|
|
634
|
+
arguments: [balance],
|
|
635
|
+
typeArguments: [pool.suiCoinType]
|
|
636
|
+
});
|
|
507
637
|
if (options.collectFee) {
|
|
508
638
|
addCollectFeeToTx(tx, borrowedCoin, "borrow");
|
|
509
639
|
}
|
|
@@ -512,31 +642,79 @@ async function buildBorrowTx(client, address, amount, options = {}) {
|
|
|
512
642
|
}
|
|
513
643
|
async function buildRepayTx(client, address, amount) {
|
|
514
644
|
const rawAmount = Number(usdcToRaw(amount));
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
}
|
|
645
|
+
const [config, pool] = await Promise.all([getConfig(), getUsdcPool()]);
|
|
646
|
+
const coins = await fetchCoins(client, address, USDC_TYPE);
|
|
647
|
+
if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins to repay with");
|
|
519
648
|
const tx = new Transaction();
|
|
520
649
|
tx.setSender(address);
|
|
521
|
-
const coinObj =
|
|
522
|
-
|
|
650
|
+
const coinObj = mergeCoinsPtb(tx, coins, rawAmount);
|
|
651
|
+
tx.moveCall({
|
|
652
|
+
target: `${config.package}::incentive_v3::entry_repay`,
|
|
653
|
+
arguments: [
|
|
654
|
+
tx.object(CLOCK),
|
|
655
|
+
tx.object(config.oracle.priceOracle),
|
|
656
|
+
tx.object(config.storage),
|
|
657
|
+
tx.object(pool.contract.pool),
|
|
658
|
+
tx.pure.u8(pool.id),
|
|
659
|
+
coinObj,
|
|
660
|
+
tx.pure.u64(rawAmount),
|
|
661
|
+
tx.object(config.incentiveV2),
|
|
662
|
+
tx.object(config.incentiveV3)
|
|
663
|
+
],
|
|
664
|
+
typeArguments: [pool.suiCoinType]
|
|
665
|
+
});
|
|
523
666
|
return tx;
|
|
524
667
|
}
|
|
525
668
|
async function getHealthFactor(client, addressOrKeypair) {
|
|
526
669
|
const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
|
|
527
|
-
const [
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
670
|
+
const [config, pool, states] = await Promise.all([
|
|
671
|
+
getConfig(),
|
|
672
|
+
getUsdcPool(),
|
|
673
|
+
getUserState(client, address)
|
|
531
674
|
]);
|
|
532
|
-
const
|
|
533
|
-
const supplied =
|
|
534
|
-
const borrowed =
|
|
675
|
+
const usdcState = states.find((s) => s.assetId === pool.id);
|
|
676
|
+
const supplied = usdcState ? compoundBalance(usdcState.supplyBalance, pool.currentSupplyIndex) : 0;
|
|
677
|
+
const borrowed = usdcState ? compoundBalance(usdcState.borrowBalance, pool.currentBorrowIndex) : 0;
|
|
535
678
|
const ltv = parseLtv(pool.ltv);
|
|
536
679
|
const liqThreshold = parseLiqThreshold(pool.liquidationFactor.threshold);
|
|
537
680
|
const maxBorrowVal = Math.max(0, supplied * ltv - borrowed);
|
|
681
|
+
let healthFactor;
|
|
682
|
+
if (borrowed <= 0) {
|
|
683
|
+
healthFactor = Infinity;
|
|
684
|
+
} else {
|
|
685
|
+
try {
|
|
686
|
+
const tx = new Transaction();
|
|
687
|
+
tx.moveCall({
|
|
688
|
+
target: `${config.uiGetter}::calculator_unchecked::dynamic_health_factor`,
|
|
689
|
+
arguments: [
|
|
690
|
+
tx.object(CLOCK),
|
|
691
|
+
tx.object(config.storage),
|
|
692
|
+
tx.object(config.oracle.priceOracle),
|
|
693
|
+
tx.pure.u8(pool.id),
|
|
694
|
+
tx.pure.address(address),
|
|
695
|
+
tx.pure.u8(pool.id),
|
|
696
|
+
tx.pure.u64(0),
|
|
697
|
+
tx.pure.u64(0),
|
|
698
|
+
tx.pure.bool(false)
|
|
699
|
+
],
|
|
700
|
+
typeArguments: [pool.suiCoinType]
|
|
701
|
+
});
|
|
702
|
+
const result = await client.devInspectTransactionBlock({
|
|
703
|
+
transactionBlock: tx,
|
|
704
|
+
sender: address
|
|
705
|
+
});
|
|
706
|
+
const decoded = decodeDevInspect(result, bcs.u256());
|
|
707
|
+
if (decoded !== void 0) {
|
|
708
|
+
healthFactor = normalizeHealthFactor(Number(decoded));
|
|
709
|
+
} else {
|
|
710
|
+
healthFactor = borrowed > 0 ? supplied * liqThreshold / borrowed : Infinity;
|
|
711
|
+
}
|
|
712
|
+
} catch {
|
|
713
|
+
healthFactor = borrowed > 0 ? supplied * liqThreshold / borrowed : Infinity;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
538
716
|
return {
|
|
539
|
-
healthFactor
|
|
717
|
+
healthFactor,
|
|
540
718
|
supplied,
|
|
541
719
|
borrowed,
|
|
542
720
|
maxBorrow: maxBorrowVal,
|
|
@@ -545,7 +723,7 @@ async function getHealthFactor(client, addressOrKeypair) {
|
|
|
545
723
|
}
|
|
546
724
|
async function getRates(client) {
|
|
547
725
|
try {
|
|
548
|
-
const pool = await
|
|
726
|
+
const pool = await getUsdcPool();
|
|
549
727
|
let saveApy = rateToApy(pool.currentSupplyRate);
|
|
550
728
|
let borrowApy = rateToApy(pool.currentBorrowRate);
|
|
551
729
|
if (saveApy <= 0 || saveApy > 100) saveApy = 4;
|
|
@@ -557,19 +735,21 @@ async function getRates(client) {
|
|
|
557
735
|
}
|
|
558
736
|
async function getPositions(client, addressOrKeypair) {
|
|
559
737
|
const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
|
|
560
|
-
const
|
|
738
|
+
const [states, pools] = await Promise.all([getUserState(client, address), getPools()]);
|
|
561
739
|
const positions = [];
|
|
562
|
-
for (const
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
const
|
|
740
|
+
for (const state of states) {
|
|
741
|
+
const pool = pools.find((p) => p.id === state.assetId);
|
|
742
|
+
if (!pool) continue;
|
|
743
|
+
const symbol = pool.token?.symbol ?? "UNKNOWN";
|
|
744
|
+
const supplyBal = compoundBalance(state.supplyBalance, pool.currentSupplyIndex);
|
|
745
|
+
const borrowBal = compoundBalance(state.borrowBalance, pool.currentBorrowIndex);
|
|
566
746
|
if (supplyBal > 1e-4) {
|
|
567
747
|
positions.push({
|
|
568
748
|
protocol: "navi",
|
|
569
749
|
asset: symbol,
|
|
570
750
|
type: "save",
|
|
571
751
|
amount: supplyBal,
|
|
572
|
-
apy: rateToApy(
|
|
752
|
+
apy: rateToApy(pool.currentSupplyRate)
|
|
573
753
|
});
|
|
574
754
|
}
|
|
575
755
|
if (borrowBal > 1e-4) {
|
|
@@ -578,7 +758,7 @@ async function getPositions(client, addressOrKeypair) {
|
|
|
578
758
|
asset: symbol,
|
|
579
759
|
type: "borrow",
|
|
580
760
|
amount: borrowBal,
|
|
581
|
-
apy: rateToApy(
|
|
761
|
+
apy: rateToApy(pool.currentBorrowRate)
|
|
582
762
|
});
|
|
583
763
|
}
|
|
584
764
|
}
|
|
@@ -595,21 +775,13 @@ async function maxWithdrawAmount(client, addressOrKeypair) {
|
|
|
595
775
|
}
|
|
596
776
|
const remainingSupply = hf.supplied - maxAmount;
|
|
597
777
|
const hfAfter = hf.borrowed > 0 ? remainingSupply / hf.borrowed : Infinity;
|
|
598
|
-
return {
|
|
599
|
-
maxAmount,
|
|
600
|
-
healthFactorAfter: hfAfter,
|
|
601
|
-
currentHF: hf.healthFactor
|
|
602
|
-
};
|
|
778
|
+
return { maxAmount, healthFactorAfter: hfAfter, currentHF: hf.healthFactor };
|
|
603
779
|
}
|
|
604
780
|
async function maxBorrowAmount(client, addressOrKeypair) {
|
|
605
781
|
const hf = await getHealthFactor(client, addressOrKeypair);
|
|
606
782
|
const ltv = hf.liquidationThreshold > 0 ? hf.liquidationThreshold : 0.75;
|
|
607
783
|
const maxAmount = Math.max(0, hf.supplied * ltv / MIN_HEALTH_FACTOR - hf.borrowed);
|
|
608
|
-
return {
|
|
609
|
-
maxAmount,
|
|
610
|
-
healthFactorAfter: MIN_HEALTH_FACTOR,
|
|
611
|
-
currentHF: hf.healthFactor
|
|
612
|
-
};
|
|
784
|
+
return { maxAmount, healthFactorAfter: MIN_HEALTH_FACTOR, currentHF: hf.healthFactor };
|
|
613
785
|
}
|
|
614
786
|
|
|
615
787
|
// src/protocols/yieldTracker.ts
|
|
@@ -1115,9 +1287,14 @@ var CetusAdapter = class {
|
|
|
1115
1287
|
}
|
|
1116
1288
|
};
|
|
1117
1289
|
var USDC_TYPE2 = SUPPORTED_ASSETS.USDC.type;
|
|
1118
|
-
SUPPORTED_ASSETS.USDC.decimals;
|
|
1119
1290
|
var WAD = 1e18;
|
|
1120
1291
|
var MIN_HEALTH_FACTOR2 = 1.5;
|
|
1292
|
+
var CLOCK2 = "0x6";
|
|
1293
|
+
var LENDING_MARKET_ID = "0x84030d26d85eaa7035084a057f2f11f701b7e2e4eda87551becbc7c97505ece1";
|
|
1294
|
+
var LENDING_MARKET_TYPE = "0xf95b06141ed4a174f239417323bde3f209b972f5930d8521ea38a52aff3a6ddf::suilend::MAIN_POOL";
|
|
1295
|
+
var SUILEND_PACKAGE = "0xf95b06141ed4a174f239417323bde3f209b972f5930d8521ea38a52aff3a6ddf";
|
|
1296
|
+
var UPGRADE_CAP_ID = "0x3d4ef1859c3ee9fc72858f588b56a09da5466e64f8cc4e90a7b3b909fba8a7ae";
|
|
1297
|
+
var FALLBACK_PUBLISHED_AT = "0xd2a67633ccb8de063163e25bcfca242929caf5cf1a26c2929dab519ee0b8f331";
|
|
1121
1298
|
function interpolateRate(utilBreakpoints, aprBreakpoints, utilizationPct) {
|
|
1122
1299
|
if (utilBreakpoints.length === 0) return 0;
|
|
1123
1300
|
if (utilizationPct <= utilBreakpoints[0]) return aprBreakpoints[0];
|
|
@@ -1132,85 +1309,121 @@ function interpolateRate(utilBreakpoints, aprBreakpoints, utilizationPct) {
|
|
|
1132
1309
|
}
|
|
1133
1310
|
return aprBreakpoints[aprBreakpoints.length - 1];
|
|
1134
1311
|
}
|
|
1135
|
-
function
|
|
1136
|
-
const
|
|
1137
|
-
const
|
|
1138
|
-
const borrowed = Number(reserve.borrowedAmount.value) / WAD / 10 ** decimals;
|
|
1312
|
+
function computeRates(reserve) {
|
|
1313
|
+
const available = reserve.availableAmount / 10 ** reserve.mintDecimals;
|
|
1314
|
+
const borrowed = reserve.borrowedAmountWad / WAD / 10 ** reserve.mintDecimals;
|
|
1139
1315
|
const totalDeposited = available + borrowed;
|
|
1140
1316
|
const utilizationPct = totalDeposited > 0 ? borrowed / totalDeposited * 100 : 0;
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
const
|
|
1144
|
-
const
|
|
1145
|
-
|
|
1146
|
-
const spreadFeeBps = Number(config.spreadFeeBps);
|
|
1147
|
-
const depositAprPct = utilizationPct / 100 * (borrowAprPct / 100) * (1 - spreadFeeBps / 1e4) * 100;
|
|
1148
|
-
return { borrowAprPct, depositAprPct, utilizationPct };
|
|
1317
|
+
if (reserve.interestRateUtils.length === 0) return { borrowAprPct: 0, depositAprPct: 0 };
|
|
1318
|
+
const aprs = reserve.interestRateAprs.map((a) => a / 100);
|
|
1319
|
+
const borrowAprPct = interpolateRate(reserve.interestRateUtils, aprs, utilizationPct);
|
|
1320
|
+
const depositAprPct = utilizationPct / 100 * (borrowAprPct / 100) * (1 - reserve.spreadFeeBps / 1e4) * 100;
|
|
1321
|
+
return { borrowAprPct, depositAprPct };
|
|
1149
1322
|
}
|
|
1150
1323
|
function cTokenRatio(reserve) {
|
|
1151
|
-
if (reserve.ctokenSupply ===
|
|
1152
|
-
const
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1324
|
+
if (reserve.ctokenSupply === 0) return 1;
|
|
1325
|
+
const totalSupply = reserve.availableAmount + reserve.borrowedAmountWad / WAD - reserve.unclaimedSpreadFeesWad / WAD;
|
|
1326
|
+
return totalSupply / reserve.ctokenSupply;
|
|
1327
|
+
}
|
|
1328
|
+
function f(obj) {
|
|
1329
|
+
if (obj && typeof obj === "object" && "fields" in obj) return obj.fields;
|
|
1330
|
+
return obj;
|
|
1331
|
+
}
|
|
1332
|
+
function str(v) {
|
|
1333
|
+
return String(v ?? "0");
|
|
1334
|
+
}
|
|
1335
|
+
function num(v) {
|
|
1336
|
+
return Number(str(v));
|
|
1337
|
+
}
|
|
1338
|
+
function parseReserve(raw, index) {
|
|
1339
|
+
const r = f(raw);
|
|
1340
|
+
const coinTypeField = f(r.coin_type);
|
|
1341
|
+
const config = f(f(r.config)?.element);
|
|
1342
|
+
return {
|
|
1343
|
+
coinType: str(coinTypeField?.name),
|
|
1344
|
+
mintDecimals: num(r.mint_decimals),
|
|
1345
|
+
availableAmount: num(r.available_amount),
|
|
1346
|
+
borrowedAmountWad: num(f(r.borrowed_amount)?.value),
|
|
1347
|
+
ctokenSupply: num(r.ctoken_supply),
|
|
1348
|
+
unclaimedSpreadFeesWad: num(f(r.unclaimed_spread_fees)?.value),
|
|
1349
|
+
cumulativeBorrowRateWad: num(f(r.cumulative_borrow_rate)?.value),
|
|
1350
|
+
openLtvPct: num(config?.open_ltv_pct),
|
|
1351
|
+
closeLtvPct: num(config?.close_ltv_pct),
|
|
1352
|
+
spreadFeeBps: num(config?.spread_fee_bps),
|
|
1353
|
+
interestRateUtils: Array.isArray(config?.interest_rate_utils) ? config.interest_rate_utils.map(num) : [],
|
|
1354
|
+
interestRateAprs: Array.isArray(config?.interest_rate_aprs) ? config.interest_rate_aprs.map(num) : [],
|
|
1355
|
+
arrayIndex: index
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
function parseObligation(raw) {
|
|
1359
|
+
const deposits = Array.isArray(raw.deposits) ? raw.deposits.map((d) => {
|
|
1360
|
+
const df = f(d);
|
|
1361
|
+
return {
|
|
1362
|
+
coinType: str(f(df.coin_type)?.name),
|
|
1363
|
+
ctokenAmount: num(df.deposited_ctoken_amount),
|
|
1364
|
+
reserveIdx: num(df.reserve_array_index)
|
|
1365
|
+
};
|
|
1366
|
+
}) : [];
|
|
1367
|
+
const borrows = Array.isArray(raw.borrows) ? raw.borrows.map((b) => {
|
|
1368
|
+
const bf = f(b);
|
|
1369
|
+
return {
|
|
1370
|
+
coinType: str(f(bf.coin_type)?.name),
|
|
1371
|
+
borrowedWad: num(f(bf.borrowed_amount)?.value),
|
|
1372
|
+
cumBorrowRateWad: num(f(bf.cumulative_borrow_rate)?.value),
|
|
1373
|
+
reserveIdx: num(bf.reserve_array_index)
|
|
1374
|
+
};
|
|
1375
|
+
}) : [];
|
|
1376
|
+
return { deposits, borrows };
|
|
1157
1377
|
}
|
|
1158
1378
|
var SuilendAdapter = class {
|
|
1159
1379
|
id = "suilend";
|
|
1160
1380
|
name = "Suilend";
|
|
1161
|
-
version = "
|
|
1381
|
+
version = "2.0.0";
|
|
1162
1382
|
capabilities = ["save", "withdraw"];
|
|
1163
1383
|
supportedAssets = ["USDC"];
|
|
1164
1384
|
supportsSameAssetBorrow = false;
|
|
1165
1385
|
client;
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
initialized = false;
|
|
1169
|
-
initPromise = null;
|
|
1386
|
+
publishedAt = null;
|
|
1387
|
+
reserveCache = null;
|
|
1170
1388
|
async init(client) {
|
|
1171
1389
|
this.client = client;
|
|
1172
|
-
await this.lazyInit();
|
|
1173
1390
|
}
|
|
1174
1391
|
initSync(client) {
|
|
1175
1392
|
this.client = client;
|
|
1176
1393
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
if (this.
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
"PROTOCOL_UNAVAILABLE",
|
|
1187
|
-
"Suilend SDK not installed. Run: npm install @suilend/sdk@^1"
|
|
1188
|
-
);
|
|
1189
|
-
}
|
|
1190
|
-
this.lendingMarketType = sdk.LENDING_MARKET_TYPE;
|
|
1191
|
-
try {
|
|
1192
|
-
this.suilend = await sdk.SuilendClient.initialize(
|
|
1193
|
-
sdk.LENDING_MARKET_ID,
|
|
1194
|
-
sdk.LENDING_MARKET_TYPE,
|
|
1195
|
-
this.client
|
|
1196
|
-
);
|
|
1197
|
-
} catch (err) {
|
|
1198
|
-
this.initPromise = null;
|
|
1199
|
-
throw new T2000Error(
|
|
1200
|
-
"PROTOCOL_UNAVAILABLE",
|
|
1201
|
-
`Failed to initialize Suilend: ${err instanceof Error ? err.message : String(err)}`
|
|
1202
|
-
);
|
|
1394
|
+
// -- On-chain reads -------------------------------------------------------
|
|
1395
|
+
async resolvePackage() {
|
|
1396
|
+
if (this.publishedAt) return this.publishedAt;
|
|
1397
|
+
try {
|
|
1398
|
+
const cap = await this.client.getObject({ id: UPGRADE_CAP_ID, options: { showContent: true } });
|
|
1399
|
+
if (cap.data?.content?.dataType === "moveObject") {
|
|
1400
|
+
const fields = cap.data.content.fields;
|
|
1401
|
+
this.publishedAt = str(fields.package);
|
|
1402
|
+
return this.publishedAt;
|
|
1203
1403
|
}
|
|
1204
|
-
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1404
|
+
} catch {
|
|
1405
|
+
}
|
|
1406
|
+
this.publishedAt = FALLBACK_PUBLISHED_AT;
|
|
1407
|
+
return this.publishedAt;
|
|
1207
1408
|
}
|
|
1208
|
-
async
|
|
1209
|
-
if (
|
|
1210
|
-
|
|
1409
|
+
async loadReserves(fresh = false) {
|
|
1410
|
+
if (this.reserveCache && !fresh) return this.reserveCache;
|
|
1411
|
+
const market = await this.client.getObject({
|
|
1412
|
+
id: LENDING_MARKET_ID,
|
|
1413
|
+
options: { showContent: true }
|
|
1414
|
+
});
|
|
1415
|
+
if (market.data?.content?.dataType !== "moveObject") {
|
|
1416
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", "Failed to read Suilend lending market");
|
|
1417
|
+
}
|
|
1418
|
+
const fields = market.data.content.fields;
|
|
1419
|
+
const reservesRaw = fields.reserves;
|
|
1420
|
+
if (!Array.isArray(reservesRaw)) {
|
|
1421
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", "Failed to parse Suilend reserves");
|
|
1211
1422
|
}
|
|
1423
|
+
this.reserveCache = reservesRaw.map((r, i) => parseReserve(r, i));
|
|
1424
|
+
return this.reserveCache;
|
|
1212
1425
|
}
|
|
1213
|
-
findReserve(asset) {
|
|
1426
|
+
findReserve(reserves, asset) {
|
|
1214
1427
|
const upper = asset.toUpperCase();
|
|
1215
1428
|
let coinType;
|
|
1216
1429
|
if (upper === "USDC") coinType = USDC_TYPE2;
|
|
@@ -1219,187 +1432,229 @@ var SuilendAdapter = class {
|
|
|
1219
1432
|
else return void 0;
|
|
1220
1433
|
try {
|
|
1221
1434
|
const normalized = normalizeStructTag(coinType);
|
|
1222
|
-
return
|
|
1223
|
-
|
|
1224
|
-
|
|
1435
|
+
return reserves.find((r) => {
|
|
1436
|
+
try {
|
|
1437
|
+
return normalizeStructTag(r.coinType) === normalized;
|
|
1438
|
+
} catch {
|
|
1439
|
+
return false;
|
|
1440
|
+
}
|
|
1441
|
+
});
|
|
1225
1442
|
} catch {
|
|
1226
1443
|
return void 0;
|
|
1227
1444
|
}
|
|
1228
1445
|
}
|
|
1229
|
-
async
|
|
1230
|
-
const
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1446
|
+
async fetchObligationCaps(address) {
|
|
1447
|
+
const capType = `${SUILEND_PACKAGE}::lending_market::ObligationOwnerCap<${LENDING_MARKET_TYPE}>`;
|
|
1448
|
+
const caps = [];
|
|
1449
|
+
let cursor;
|
|
1450
|
+
let hasNext = true;
|
|
1451
|
+
while (hasNext) {
|
|
1452
|
+
const page = await this.client.getOwnedObjects({
|
|
1453
|
+
owner: address,
|
|
1454
|
+
filter: { StructType: capType },
|
|
1455
|
+
options: { showContent: true },
|
|
1456
|
+
cursor: cursor ?? void 0
|
|
1457
|
+
});
|
|
1458
|
+
for (const item of page.data) {
|
|
1459
|
+
if (item.data?.content?.dataType !== "moveObject") continue;
|
|
1460
|
+
const fields = item.data.content.fields;
|
|
1461
|
+
caps.push({
|
|
1462
|
+
id: item.data.objectId,
|
|
1463
|
+
obligationId: str(fields.obligation_id)
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
cursor = page.nextCursor;
|
|
1467
|
+
hasNext = page.hasNextPage;
|
|
1468
|
+
}
|
|
1469
|
+
return caps;
|
|
1470
|
+
}
|
|
1471
|
+
async fetchObligation(obligationId) {
|
|
1472
|
+
const obj = await this.client.getObject({ id: obligationId, options: { showContent: true } });
|
|
1473
|
+
if (obj.data?.content?.dataType !== "moveObject") {
|
|
1474
|
+
throw new T2000Error("PROTOCOL_UNAVAILABLE", "Failed to read Suilend obligation");
|
|
1475
|
+
}
|
|
1476
|
+
return parseObligation(obj.data.content.fields);
|
|
1236
1477
|
}
|
|
1237
1478
|
resolveSymbol(coinType) {
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1479
|
+
try {
|
|
1480
|
+
const normalized = normalizeStructTag(coinType);
|
|
1481
|
+
if (normalized === normalizeStructTag(USDC_TYPE2)) return "USDC";
|
|
1482
|
+
if (normalized === normalizeStructTag("0x2::sui::SUI")) return "SUI";
|
|
1483
|
+
} catch {
|
|
1484
|
+
}
|
|
1241
1485
|
const parts = coinType.split("::");
|
|
1242
1486
|
return parts[parts.length - 1] || "UNKNOWN";
|
|
1243
1487
|
}
|
|
1488
|
+
// -- Adapter interface ----------------------------------------------------
|
|
1244
1489
|
async getRates(asset) {
|
|
1245
|
-
await this.
|
|
1246
|
-
const reserve = this.findReserve(asset);
|
|
1247
|
-
if (!reserve) {
|
|
1248
|
-
|
|
1249
|
-
}
|
|
1250
|
-
const { borrowAprPct, depositAprPct } = computeRatesFromReserve(reserve);
|
|
1251
|
-
return {
|
|
1252
|
-
asset,
|
|
1253
|
-
saveApy: depositAprPct,
|
|
1254
|
-
borrowApy: borrowAprPct
|
|
1255
|
-
};
|
|
1490
|
+
const reserves = await this.loadReserves();
|
|
1491
|
+
const reserve = this.findReserve(reserves, asset);
|
|
1492
|
+
if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `Suilend does not support ${asset}`);
|
|
1493
|
+
const { borrowAprPct, depositAprPct } = computeRates(reserve);
|
|
1494
|
+
return { asset, saveApy: depositAprPct, borrowApy: borrowAprPct };
|
|
1256
1495
|
}
|
|
1257
1496
|
async getPositions(address) {
|
|
1258
|
-
await this.ensureInit();
|
|
1259
1497
|
const supplies = [];
|
|
1260
1498
|
const borrows = [];
|
|
1261
|
-
const caps = await this.
|
|
1499
|
+
const caps = await this.fetchObligationCaps(address);
|
|
1262
1500
|
if (caps.length === 0) return { supplies, borrows };
|
|
1263
|
-
const obligation = await
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1501
|
+
const [reserves, obligation] = await Promise.all([
|
|
1502
|
+
this.loadReserves(),
|
|
1503
|
+
this.fetchObligation(caps[0].obligationId)
|
|
1504
|
+
]);
|
|
1505
|
+
for (const dep of obligation.deposits) {
|
|
1506
|
+
const reserve = reserves[dep.reserveIdx];
|
|
1269
1507
|
if (!reserve) continue;
|
|
1270
|
-
const ctokenAmount = Number(deposit.depositedCtokenAmount.toString());
|
|
1271
1508
|
const ratio = cTokenRatio(reserve);
|
|
1272
|
-
const amount = ctokenAmount * ratio / 10 ** reserve.mintDecimals;
|
|
1273
|
-
const { depositAprPct } =
|
|
1274
|
-
supplies.push({ asset: this.resolveSymbol(coinType), amount, apy: depositAprPct });
|
|
1509
|
+
const amount = dep.ctokenAmount * ratio / 10 ** reserve.mintDecimals;
|
|
1510
|
+
const { depositAprPct } = computeRates(reserve);
|
|
1511
|
+
supplies.push({ asset: this.resolveSymbol(dep.coinType), amount, apy: depositAprPct });
|
|
1275
1512
|
}
|
|
1276
|
-
for (const
|
|
1277
|
-
const
|
|
1278
|
-
const reserve = this.suilend.lendingMarket.reserves.find(
|
|
1279
|
-
(r) => normalizeStructTag(r.coinType.name) === coinType
|
|
1280
|
-
);
|
|
1513
|
+
for (const bor of obligation.borrows) {
|
|
1514
|
+
const reserve = reserves[bor.reserveIdx];
|
|
1281
1515
|
if (!reserve) continue;
|
|
1282
|
-
const
|
|
1283
|
-
const
|
|
1284
|
-
const
|
|
1285
|
-
const
|
|
1286
|
-
const
|
|
1287
|
-
|
|
1288
|
-
borrows.push({ asset: this.resolveSymbol(coinType), amount: compounded, apy: borrowAprPct });
|
|
1516
|
+
const rawAmount = bor.borrowedWad / WAD / 10 ** reserve.mintDecimals;
|
|
1517
|
+
const reserveRate = reserve.cumulativeBorrowRateWad / WAD;
|
|
1518
|
+
const posRate = bor.cumBorrowRateWad / WAD;
|
|
1519
|
+
const compounded = posRate > 0 ? rawAmount * (reserveRate / posRate) : rawAmount;
|
|
1520
|
+
const { borrowAprPct } = computeRates(reserve);
|
|
1521
|
+
borrows.push({ asset: this.resolveSymbol(bor.coinType), amount: compounded, apy: borrowAprPct });
|
|
1289
1522
|
}
|
|
1290
1523
|
return { supplies, borrows };
|
|
1291
1524
|
}
|
|
1292
1525
|
async getHealth(address) {
|
|
1293
|
-
await this.
|
|
1294
|
-
const caps = await this.getObligationCaps(address);
|
|
1526
|
+
const caps = await this.fetchObligationCaps(address);
|
|
1295
1527
|
if (caps.length === 0) {
|
|
1296
1528
|
return { healthFactor: Infinity, supplied: 0, borrowed: 0, maxBorrow: 0, liquidationThreshold: 0 };
|
|
1297
1529
|
}
|
|
1298
1530
|
const positions = await this.getPositions(address);
|
|
1299
1531
|
const supplied = positions.supplies.reduce((s, p) => s + p.amount, 0);
|
|
1300
1532
|
const borrowed = positions.borrows.reduce((s, p) => s + p.amount, 0);
|
|
1301
|
-
const
|
|
1302
|
-
const
|
|
1303
|
-
const
|
|
1533
|
+
const reserves = await this.loadReserves();
|
|
1534
|
+
const reserve = this.findReserve(reserves, "USDC");
|
|
1535
|
+
const closeLtv = reserve?.closeLtvPct ?? 75;
|
|
1536
|
+
const openLtv = reserve?.openLtvPct ?? 70;
|
|
1304
1537
|
const liqThreshold = closeLtv / 100;
|
|
1305
1538
|
const healthFactor = borrowed > 0 ? supplied * liqThreshold / borrowed : Infinity;
|
|
1306
1539
|
const maxBorrow = Math.max(0, supplied * (openLtv / 100) - borrowed);
|
|
1307
1540
|
return { healthFactor, supplied, borrowed, maxBorrow, liquidationThreshold: liqThreshold };
|
|
1308
1541
|
}
|
|
1309
1542
|
async buildSaveTx(address, amount, _asset, options) {
|
|
1310
|
-
await this.
|
|
1311
|
-
const
|
|
1543
|
+
const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
|
|
1544
|
+
const usdcReserve = this.findReserve(reserves, "USDC");
|
|
1545
|
+
if (!usdcReserve) throw new T2000Error("ASSET_NOT_SUPPORTED", "USDC reserve not found on Suilend");
|
|
1546
|
+
const caps = await this.fetchObligationCaps(address);
|
|
1312
1547
|
const tx = new Transaction();
|
|
1313
1548
|
tx.setSender(address);
|
|
1314
|
-
const caps = await this.getObligationCaps(address);
|
|
1315
1549
|
let capRef;
|
|
1316
1550
|
if (caps.length === 0) {
|
|
1317
|
-
const [newCap] =
|
|
1551
|
+
const [newCap] = tx.moveCall({
|
|
1552
|
+
target: `${pkg}::lending_market::create_obligation`,
|
|
1553
|
+
typeArguments: [LENDING_MARKET_TYPE],
|
|
1554
|
+
arguments: [tx.object(LENDING_MARKET_ID)]
|
|
1555
|
+
});
|
|
1318
1556
|
capRef = newCap;
|
|
1319
1557
|
} else {
|
|
1320
1558
|
capRef = caps[0].id;
|
|
1321
1559
|
}
|
|
1322
1560
|
const allCoins = await this.fetchAllCoins(address, USDC_TYPE2);
|
|
1323
|
-
if (allCoins.length === 0)
|
|
1324
|
-
throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins found");
|
|
1325
|
-
}
|
|
1561
|
+
if (allCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins found");
|
|
1326
1562
|
const primaryCoinId = allCoins[0].coinObjectId;
|
|
1327
1563
|
if (allCoins.length > 1) {
|
|
1328
|
-
tx.mergeCoins(
|
|
1329
|
-
tx.object(primaryCoinId),
|
|
1330
|
-
allCoins.slice(1).map((c) => tx.object(c.coinObjectId))
|
|
1331
|
-
);
|
|
1564
|
+
tx.mergeCoins(tx.object(primaryCoinId), allCoins.slice(1).map((c) => tx.object(c.coinObjectId)));
|
|
1332
1565
|
}
|
|
1566
|
+
const rawAmount = usdcToRaw(amount).toString();
|
|
1333
1567
|
const [depositCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawAmount]);
|
|
1334
1568
|
if (options?.collectFee) {
|
|
1335
1569
|
addCollectFeeToTx(tx, depositCoin, "save");
|
|
1336
1570
|
}
|
|
1337
|
-
|
|
1571
|
+
const [ctokens] = tx.moveCall({
|
|
1572
|
+
target: `${pkg}::lending_market::deposit_liquidity_and_mint_ctokens`,
|
|
1573
|
+
typeArguments: [LENDING_MARKET_TYPE, USDC_TYPE2],
|
|
1574
|
+
arguments: [
|
|
1575
|
+
tx.object(LENDING_MARKET_ID),
|
|
1576
|
+
tx.pure.u64(usdcReserve.arrayIndex),
|
|
1577
|
+
tx.object(CLOCK2),
|
|
1578
|
+
depositCoin
|
|
1579
|
+
]
|
|
1580
|
+
});
|
|
1581
|
+
tx.moveCall({
|
|
1582
|
+
target: `${pkg}::lending_market::deposit_ctokens_into_obligation`,
|
|
1583
|
+
typeArguments: [LENDING_MARKET_TYPE, USDC_TYPE2],
|
|
1584
|
+
arguments: [
|
|
1585
|
+
tx.object(LENDING_MARKET_ID),
|
|
1586
|
+
tx.pure.u64(usdcReserve.arrayIndex),
|
|
1587
|
+
typeof capRef === "string" ? tx.object(capRef) : capRef,
|
|
1588
|
+
tx.object(CLOCK2),
|
|
1589
|
+
ctokens
|
|
1590
|
+
]
|
|
1591
|
+
});
|
|
1338
1592
|
return { tx };
|
|
1339
1593
|
}
|
|
1340
1594
|
async buildWithdrawTx(address, amount, _asset) {
|
|
1341
|
-
await this.
|
|
1342
|
-
const
|
|
1343
|
-
if (
|
|
1344
|
-
|
|
1345
|
-
|
|
1595
|
+
const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves(true)]);
|
|
1596
|
+
const usdcReserve = this.findReserve(reserves, "USDC");
|
|
1597
|
+
if (!usdcReserve) throw new T2000Error("ASSET_NOT_SUPPORTED", "USDC reserve not found on Suilend");
|
|
1598
|
+
const caps = await this.fetchObligationCaps(address);
|
|
1599
|
+
if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found");
|
|
1346
1600
|
const positions = await this.getPositions(address);
|
|
1347
|
-
const
|
|
1348
|
-
const deposited = usdcSupply?.amount ?? 0;
|
|
1601
|
+
const deposited = positions.supplies.find((s) => s.asset === "USDC")?.amount ?? 0;
|
|
1349
1602
|
const effectiveAmount = Math.min(amount, deposited);
|
|
1350
|
-
if (effectiveAmount <= 0)
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
const rawAmount = usdcToRaw(effectiveAmount).toString();
|
|
1603
|
+
if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", "Nothing to withdraw from Suilend");
|
|
1604
|
+
const ratio = cTokenRatio(usdcReserve);
|
|
1605
|
+
const ctokenAmount = Math.ceil(effectiveAmount * 10 ** usdcReserve.mintDecimals / ratio);
|
|
1354
1606
|
const tx = new Transaction();
|
|
1355
1607
|
tx.setSender(address);
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1608
|
+
const [ctokens] = tx.moveCall({
|
|
1609
|
+
target: `${pkg}::lending_market::withdraw_ctokens`,
|
|
1610
|
+
typeArguments: [LENDING_MARKET_TYPE, USDC_TYPE2],
|
|
1611
|
+
arguments: [
|
|
1612
|
+
tx.object(LENDING_MARKET_ID),
|
|
1613
|
+
tx.pure.u64(usdcReserve.arrayIndex),
|
|
1614
|
+
tx.object(caps[0].id),
|
|
1615
|
+
tx.object(CLOCK2),
|
|
1616
|
+
tx.pure.u64(ctokenAmount)
|
|
1617
|
+
]
|
|
1618
|
+
});
|
|
1619
|
+
const exemptionType = `${SUILEND_PACKAGE}::lending_market::RateLimiterExemption<${LENDING_MARKET_TYPE}, ${USDC_TYPE2}>`;
|
|
1620
|
+
const [none] = tx.moveCall({
|
|
1621
|
+
target: "0x1::option::none",
|
|
1622
|
+
typeArguments: [exemptionType]
|
|
1623
|
+
});
|
|
1624
|
+
const [coin] = tx.moveCall({
|
|
1625
|
+
target: `${pkg}::lending_market::redeem_ctokens_and_withdraw_liquidity`,
|
|
1626
|
+
typeArguments: [LENDING_MARKET_TYPE, USDC_TYPE2],
|
|
1627
|
+
arguments: [
|
|
1628
|
+
tx.object(LENDING_MARKET_ID),
|
|
1629
|
+
tx.pure.u64(usdcReserve.arrayIndex),
|
|
1630
|
+
tx.object(CLOCK2),
|
|
1631
|
+
ctokens,
|
|
1632
|
+
none
|
|
1633
|
+
]
|
|
1634
|
+
});
|
|
1635
|
+
tx.transferObjects([coin], address);
|
|
1364
1636
|
return { tx, effectiveAmount };
|
|
1365
1637
|
}
|
|
1366
1638
|
async buildBorrowTx(_address, _amount, _asset, _options) {
|
|
1367
|
-
throw new T2000Error(
|
|
1368
|
-
"ASSET_NOT_SUPPORTED",
|
|
1369
|
-
"SuilendAdapter.buildBorrowTx() not available \u2014 Suilend requires different collateral/borrow assets. Deferred to Phase 10."
|
|
1370
|
-
);
|
|
1639
|
+
throw new T2000Error("ASSET_NOT_SUPPORTED", "Suilend borrow requires different collateral/borrow assets. Deferred to Phase 10.");
|
|
1371
1640
|
}
|
|
1372
1641
|
async buildRepayTx(_address, _amount, _asset) {
|
|
1373
|
-
throw new T2000Error(
|
|
1374
|
-
"ASSET_NOT_SUPPORTED",
|
|
1375
|
-
"SuilendAdapter.buildRepayTx() not available \u2014 deferred to Phase 10."
|
|
1376
|
-
);
|
|
1642
|
+
throw new T2000Error("ASSET_NOT_SUPPORTED", "Suilend repay deferred to Phase 10.");
|
|
1377
1643
|
}
|
|
1378
1644
|
async maxWithdraw(address, _asset) {
|
|
1379
|
-
await this.ensureInit();
|
|
1380
1645
|
const health = await this.getHealth(address);
|
|
1381
1646
|
let maxAmount;
|
|
1382
1647
|
if (health.borrowed === 0) {
|
|
1383
1648
|
maxAmount = health.supplied;
|
|
1384
1649
|
} else {
|
|
1385
|
-
maxAmount = Math.max(
|
|
1386
|
-
0,
|
|
1387
|
-
health.supplied - health.borrowed * MIN_HEALTH_FACTOR2 / health.liquidationThreshold
|
|
1388
|
-
);
|
|
1650
|
+
maxAmount = Math.max(0, health.supplied - health.borrowed * MIN_HEALTH_FACTOR2 / health.liquidationThreshold);
|
|
1389
1651
|
}
|
|
1390
1652
|
const remainingSupply = health.supplied - maxAmount;
|
|
1391
1653
|
const hfAfter = health.borrowed > 0 ? remainingSupply * health.liquidationThreshold / health.borrowed : Infinity;
|
|
1392
|
-
return {
|
|
1393
|
-
maxAmount,
|
|
1394
|
-
healthFactorAfter: hfAfter,
|
|
1395
|
-
currentHF: health.healthFactor
|
|
1396
|
-
};
|
|
1654
|
+
return { maxAmount, healthFactorAfter: hfAfter, currentHF: health.healthFactor };
|
|
1397
1655
|
}
|
|
1398
1656
|
async maxBorrow(_address, _asset) {
|
|
1399
|
-
throw new T2000Error(
|
|
1400
|
-
"ASSET_NOT_SUPPORTED",
|
|
1401
|
-
"SuilendAdapter.maxBorrow() not available \u2014 deferred to Phase 10."
|
|
1402
|
-
);
|
|
1657
|
+
throw new T2000Error("ASSET_NOT_SUPPORTED", "Suilend maxBorrow deferred to Phase 10.");
|
|
1403
1658
|
}
|
|
1404
1659
|
async fetchAllCoins(owner, coinType) {
|
|
1405
1660
|
const all = [];
|
|
@@ -1714,7 +1969,7 @@ var T2000 = class _T2000 extends EventEmitter {
|
|
|
1714
1969
|
return { agent, address, sponsored };
|
|
1715
1970
|
}
|
|
1716
1971
|
// -- Gas --
|
|
1717
|
-
/**
|
|
1972
|
+
/** SuiJsonRpcClient used by this agent — exposed for x402 and other integrations. */
|
|
1718
1973
|
get suiClient() {
|
|
1719
1974
|
return this.client;
|
|
1720
1975
|
}
|