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