@t2000/sdk 0.18.10 → 0.18.11

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.
@@ -1,7 +1,11 @@
1
1
  import { Transaction } from '@mysten/sui/transactions';
2
- import { bcs } from '@mysten/sui/bcs';
2
+ import { getPools, getLendingPositions, getHealthFactor as getHealthFactor$1, depositCoinPTB, withdrawCoinPTB, borrowCoinPTB, repayCoinPTB, getUserAvailableLendingRewards, summaryLendingRewards, claimLendingRewardsPTB } from '@naviprotocol/lending';
3
3
  import { AggregatorClient, Env } from '@cetusprotocol/aggregator-sdk';
4
4
  import { normalizeStructTag } from '@mysten/sui/utils';
5
+ import { SuilendClient, LENDING_MARKET_ID, LENDING_MARKET_TYPE } from '@suilend/sdk/client';
6
+ import { initializeSuilend, initializeObligations } from '@suilend/sdk/lib/initialize';
7
+ import { Side } from '@suilend/sdk/lib/types';
8
+ import '@mysten/sui/bcs';
5
9
 
6
10
  // src/constants.ts
7
11
  var SAVE_FEE_BPS = 10n;
@@ -280,79 +284,34 @@ function addCollectFeeToTx(tx, paymentCoin, operation) {
280
284
  ]
281
285
  });
282
286
  }
283
- var RATE_DECIMALS = 27;
284
- var LTV_DECIMALS = 27;
287
+
288
+ // src/protocols/navi.ts
285
289
  var MIN_HEALTH_FACTOR = 1.5;
286
- function withdrawDustBuffer(decimals) {
287
- return 1e3 / 10 ** decimals;
288
- }
289
- var CLOCK = "0x06";
290
- var SUI_SYSTEM_STATE = "0x05";
291
- var NAVI_BALANCE_DECIMALS = 9;
292
- var CONFIG_API = "https://open-api.naviprotocol.io/api/navi/config?env=prod";
293
- var POOLS_API = "https://open-api.naviprotocol.io/api/navi/pools?env=prod";
294
- var PACKAGE_API = "https://open-api.naviprotocol.io/api/package";
295
- var packageCache = null;
296
- function toBigInt(v) {
297
- if (typeof v === "bigint") return v;
298
- return BigInt(String(v));
299
- }
300
- var UserStateInfo = bcs.struct("UserStateInfo", {
301
- asset_id: bcs.u8(),
302
- borrow_balance: bcs.u256(),
303
- supply_balance: bcs.u256()
304
- });
305
- function decodeDevInspect(result, schema) {
306
- const rv = result.results?.[0]?.returnValues?.[0];
307
- if (result.error || !rv) return void 0;
308
- const bytes = Uint8Array.from(rv[0]);
309
- return schema.parse(bytes);
310
- }
311
- var configCache = null;
312
- var poolsCache = null;
313
- var CACHE_TTL = 5 * 6e4;
314
- async function fetchJson(url) {
315
- const res = await fetch(url);
316
- if (!res.ok) throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI API error: ${res.status}`);
317
- const json = await res.json();
318
- return json.data ?? json;
319
- }
320
- async function getLatestPackageId() {
321
- if (packageCache && Date.now() - packageCache.ts < CACHE_TTL) return packageCache.id;
322
- const res = await fetch(PACKAGE_API);
323
- if (!res.ok) throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI package API error: ${res.status}`);
324
- const json = await res.json();
325
- if (!json.packageId) throw new T2000Error("PROTOCOL_UNAVAILABLE", "NAVI package API returned no packageId");
326
- packageCache = { id: json.packageId, ts: Date.now() };
327
- return json.packageId;
328
- }
329
- async function getConfig(fresh = false) {
330
- if (configCache && !fresh && Date.now() - configCache.ts < CACHE_TTL) return configCache.data;
331
- const [data, latestPkg] = await Promise.all([
332
- fetchJson(CONFIG_API),
333
- getLatestPackageId()
334
- ]);
335
- data.package = latestPkg;
336
- configCache = { data, ts: Date.now() };
337
- return data;
338
- }
339
- async function getPools(fresh = false) {
340
- if (poolsCache && !fresh && Date.now() - poolsCache.ts < CACHE_TTL) return poolsCache.data;
341
- const data = await fetchJson(POOLS_API);
342
- poolsCache = { data, ts: Date.now() };
343
- return data;
344
- }
345
- function matchesCoinType(poolType, targetType) {
346
- const poolSuffix = poolType.split("::").slice(1).join("::").toLowerCase();
347
- const targetSuffix = targetType.split("::").slice(1).join("::").toLowerCase();
348
- return poolSuffix === targetSuffix;
290
+ var NAVI_SUPPORTED_ASSETS = [...STABLE_ASSETS, "SUI", "ETH", "GOLD"];
291
+ function sdkOptions(client) {
292
+ return { env: "prod", client };
349
293
  }
350
- function resolvePoolSymbol(pool) {
351
- const coinType = pool.suiCoinType || pool.coinType || "";
294
+ var NAVI_SYMBOL_MAP = {
295
+ nUSDC: "USDC",
296
+ suiUSDT: "USDT",
297
+ suiUSDe: "USDe",
298
+ XAUM: "GOLD",
299
+ WBTC: "BTC",
300
+ suiETH: "ETH",
301
+ WETH: "ETH",
302
+ SUI: "SUI",
303
+ USDC: "USDC",
304
+ USDT: "USDT",
305
+ USDe: "USDe",
306
+ USDsui: "USDsui"
307
+ };
308
+ function resolveNaviSymbol(sdkSymbol, coinType) {
352
309
  for (const [key, info] of Object.entries(SUPPORTED_ASSETS)) {
353
- if (matchesCoinType(coinType, info.type)) return key;
310
+ const poolSuffix = coinType.split("::").slice(1).join("::").toLowerCase();
311
+ const targetSuffix = info.type.split("::").slice(1).join("::").toLowerCase();
312
+ if (poolSuffix === targetSuffix) return key;
354
313
  }
355
- return pool.token?.symbol ?? "UNKNOWN";
314
+ return NAVI_SYMBOL_MAP[sdkSymbol] ?? sdkSymbol;
356
315
  }
357
316
  function resolveAssetInfo(asset) {
358
317
  if (asset in SUPPORTED_ASSETS) {
@@ -361,109 +320,6 @@ function resolveAssetInfo(asset) {
361
320
  }
362
321
  throw new T2000Error("ASSET_NOT_SUPPORTED", `Unknown asset: ${asset}`);
363
322
  }
364
- async function getPool(asset = "USDC") {
365
- const pools = await getPools();
366
- const { type: targetType, displayName } = resolveAssetInfo(asset);
367
- const pool = pools.find(
368
- (p) => matchesCoinType(p.suiCoinType || p.coinType || "", targetType)
369
- );
370
- if (!pool) {
371
- throw new T2000Error(
372
- "ASSET_NOT_SUPPORTED",
373
- `${displayName} pool not found on NAVI`
374
- );
375
- }
376
- return pool;
377
- }
378
- function addOracleUpdate(tx, config, pool) {
379
- const feed = config.oracle.feeds?.find((f2) => f2.assetId === pool.id);
380
- if (!feed) {
381
- throw new T2000Error("PROTOCOL_UNAVAILABLE", `Oracle feed not found for asset ${pool.token?.symbol ?? pool.id}`);
382
- }
383
- tx.moveCall({
384
- target: `${config.oracle.packageId}::oracle_pro::update_single_price_v2`,
385
- arguments: [
386
- tx.object(CLOCK),
387
- tx.object(config.oracle.oracleConfig),
388
- tx.object(config.oracle.priceOracle),
389
- tx.object(config.oracle.supraOracleHolder),
390
- tx.object(feed.pythPriceInfoObject),
391
- tx.object(config.oracle.switchboardAggregator),
392
- tx.pure.address(feed.feedId)
393
- ]
394
- });
395
- }
396
- function refreshOracles(tx, config, pools, opts) {
397
- const assetsToRefresh = NAVI_SUPPORTED_ASSETS;
398
- const targetTypes = assetsToRefresh.map((a) => SUPPORTED_ASSETS[a].type);
399
- const matchedPools = pools.filter((p) => {
400
- const ct = p.suiCoinType || p.coinType || "";
401
- return targetTypes.some((t) => matchesCoinType(ct, t));
402
- });
403
- for (const pool of matchedPools) {
404
- addOracleUpdate(tx, config, pool);
405
- }
406
- }
407
- function rateToApy(rawRate) {
408
- if (!rawRate || rawRate === "0") return 0;
409
- return Number(BigInt(rawRate)) / 10 ** RATE_DECIMALS * 100;
410
- }
411
- function poolSaveApy(pool) {
412
- const incentive = parseFloat(pool.supplyIncentiveApyInfo?.apy ?? "0");
413
- if (incentive > 0) return incentive;
414
- return rateToApy(pool.currentSupplyRate);
415
- }
416
- function poolBorrowApy(pool) {
417
- const incentive = parseFloat(pool.borrowIncentiveApyInfo?.apy ?? "0");
418
- if (incentive > 0) return incentive;
419
- return rateToApy(pool.currentBorrowRate);
420
- }
421
- function parseLtv(rawLtv) {
422
- if (!rawLtv || rawLtv === "0") return 0.75;
423
- return Number(BigInt(rawLtv)) / 10 ** LTV_DECIMALS;
424
- }
425
- function parseLiqThreshold(val) {
426
- if (typeof val === "number") return val;
427
- const n = Number(val);
428
- if (n > 1) return Number(BigInt(val)) / 10 ** LTV_DECIMALS;
429
- return n;
430
- }
431
- function normalizeHealthFactor(raw) {
432
- const v = raw / 10 ** RATE_DECIMALS;
433
- return v > 1e5 ? Infinity : v;
434
- }
435
- function naviStorageDecimals(poolId, tokenDecimals) {
436
- if (poolId <= 10) return NAVI_BALANCE_DECIMALS;
437
- return tokenDecimals;
438
- }
439
- function compoundBalance(rawBalance, currentIndex, pool) {
440
- if (!rawBalance || !currentIndex || currentIndex === "0") return 0;
441
- const scale = BigInt("1" + "0".repeat(RATE_DECIMALS));
442
- const half = scale / 2n;
443
- const result = (rawBalance * BigInt(currentIndex) + half) / scale;
444
- const decimals = pool ? naviStorageDecimals(pool.id, pool.token.decimals) : NAVI_BALANCE_DECIMALS;
445
- return Number(result) / 10 ** decimals;
446
- }
447
- async function getUserState(client, address) {
448
- const config = await getConfig();
449
- const tx = new Transaction();
450
- tx.moveCall({
451
- target: `${config.uiGetter}::getter_unchecked::get_user_state`,
452
- arguments: [tx.object(config.storage), tx.pure.address(address)]
453
- });
454
- const result = await client.devInspectTransactionBlock({
455
- transactionBlock: tx,
456
- sender: address
457
- });
458
- const decoded = decodeDevInspect(result, bcs.vector(UserStateInfo));
459
- if (!decoded) return [];
460
- const mapped = decoded.map((s) => ({
461
- assetId: s.asset_id,
462
- supplyBalance: toBigInt(s.supply_balance),
463
- borrowBalance: toBigInt(s.borrow_balance)
464
- }));
465
- return mapped.filter((s) => s.supplyBalance !== 0n || s.borrowBalance !== 0n);
466
- }
467
323
  async function fetchCoins(client, owner, coinType) {
468
324
  const all = [];
469
325
  let cursor;
@@ -484,14 +340,98 @@ function mergeCoins(tx, coins) {
484
340
  }
485
341
  return primary;
486
342
  }
343
+ async function getPositions(client, addressOrKeypair) {
344
+ const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
345
+ try {
346
+ const naviPositions = await getLendingPositions(address, {
347
+ ...sdkOptions(client),
348
+ markets: ["main"]
349
+ });
350
+ const positions = [];
351
+ for (const pos of naviPositions) {
352
+ const data = pos["navi-lending-supply"] ?? pos["navi-lending-emode-supply"] ?? pos["navi-lending-borrow"] ?? pos["navi-lending-emode-borrow"];
353
+ if (!data) continue;
354
+ const isBorrow = pos.type.includes("borrow");
355
+ const symbol = resolveNaviSymbol(data.token.symbol, data.token.coinType);
356
+ const amount = parseFloat(data.amount);
357
+ const amountUsd = parseFloat(data.valueUSD);
358
+ const pool = data.pool;
359
+ const apy = isBorrow ? parseFloat(pool.borrowIncentiveApyInfo?.apy ?? "0") : parseFloat(pool.supplyIncentiveApyInfo?.apy ?? "0");
360
+ if (amount > 1e-4 || amountUsd > 1e-3) {
361
+ positions.push({
362
+ protocol: "navi",
363
+ asset: symbol,
364
+ type: isBorrow ? "borrow" : "save",
365
+ amount,
366
+ amountUsd,
367
+ apy
368
+ });
369
+ }
370
+ }
371
+ return { positions };
372
+ } catch (err) {
373
+ const msg = err instanceof Error ? err.message : String(err);
374
+ if (msg.includes("not found") || msg.includes("404")) return { positions: [] };
375
+ throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI getPositions failed: ${msg}`);
376
+ }
377
+ }
378
+ async function getRates(client) {
379
+ try {
380
+ const pools = await getPools(sdkOptions(client));
381
+ const result = {};
382
+ for (const asset of NAVI_SUPPORTED_ASSETS) {
383
+ const targetType = SUPPORTED_ASSETS[asset].type;
384
+ const pool = pools.find((p) => {
385
+ const poolSuffix = (p.suiCoinType || p.coinType || "").split("::").slice(1).join("::").toLowerCase();
386
+ const targetSuffix = targetType.split("::").slice(1).join("::").toLowerCase();
387
+ return poolSuffix === targetSuffix;
388
+ });
389
+ if (!pool) continue;
390
+ const saveApy = parseFloat(pool.supplyIncentiveApyInfo?.apy ?? "0");
391
+ const borrowApy = parseFloat(pool.borrowIncentiveApyInfo?.apy ?? "0");
392
+ if (saveApy >= 0 && saveApy < 200) {
393
+ result[asset] = { saveApy, borrowApy: borrowApy >= 0 && borrowApy < 200 ? borrowApy : 0 };
394
+ }
395
+ }
396
+ if (!result.USDC) result.USDC = { saveApy: 4, borrowApy: 6 };
397
+ return result;
398
+ } catch {
399
+ return { USDC: { saveApy: 4, borrowApy: 6 } };
400
+ }
401
+ }
402
+ async function getHealthFactor(client, addressOrKeypair) {
403
+ const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
404
+ const posResult = await getPositions(client, address);
405
+ let supplied = 0;
406
+ let borrowed = 0;
407
+ for (const pos of posResult.positions) {
408
+ const usd = pos.amountUsd ?? pos.amount;
409
+ if (pos.type === "save") supplied += usd;
410
+ else if (pos.type === "borrow") borrowed += usd;
411
+ }
412
+ let healthFactor;
413
+ try {
414
+ const hf = await getHealthFactor$1(address, sdkOptions(client));
415
+ healthFactor = hf > 1e5 ? Infinity : hf;
416
+ } catch {
417
+ healthFactor = borrowed > 0 ? supplied * 0.75 / borrowed : Infinity;
418
+ }
419
+ const ltv = 0.75;
420
+ const maxBorrow = Math.max(0, supplied * ltv - borrowed);
421
+ return {
422
+ healthFactor,
423
+ supplied,
424
+ borrowed,
425
+ maxBorrow,
426
+ liquidationThreshold: ltv
427
+ };
428
+ }
487
429
  async function buildSaveTx(client, address, amount, options = {}) {
488
430
  if (!amount || amount <= 0 || !Number.isFinite(amount)) {
489
431
  throw new T2000Error("INVALID_AMOUNT", "Save amount must be a positive number");
490
432
  }
491
433
  const asset = options.asset ?? "USDC";
492
434
  const assetInfo = resolveAssetInfo(asset);
493
- const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
494
- const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
495
435
  const coins = await fetchCoins(client, address, assetInfo.type);
496
436
  if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins found`);
497
437
  const tx = new Transaction();
@@ -500,159 +440,103 @@ async function buildSaveTx(client, address, amount, options = {}) {
500
440
  if (options.collectFee) {
501
441
  addCollectFeeToTx(tx, coinObj, "save");
502
442
  }
503
- tx.moveCall({
504
- target: `${config.package}::incentive_v3::entry_deposit`,
505
- arguments: [
506
- tx.object(CLOCK),
507
- tx.object(config.storage),
508
- tx.object(pool.contract.pool),
509
- tx.pure.u8(pool.id),
510
- coinObj,
511
- tx.pure.u64(rawAmount),
512
- tx.object(config.incentiveV2),
513
- tx.object(config.incentiveV3)
514
- ],
515
- typeArguments: [pool.suiCoinType]
516
- });
443
+ const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
444
+ try {
445
+ await depositCoinPTB(tx, assetInfo.type, coinObj, {
446
+ ...sdkOptions(client),
447
+ amount: rawAmount
448
+ });
449
+ } catch (err) {
450
+ const msg = err instanceof Error ? err.message : String(err);
451
+ throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI deposit failed: ${msg}`);
452
+ }
517
453
  return tx;
518
454
  }
519
455
  async function buildWithdrawTx(client, address, amount, options = {}) {
520
456
  const asset = options.asset ?? "USDC";
521
457
  const assetInfo = resolveAssetInfo(asset);
522
- const [config, pool, pools, states] = await Promise.all([
523
- getConfig(),
524
- getPool(asset),
525
- getPools(),
526
- getUserState(client, address)
527
- ]);
528
- const assetState = states.find((s) => s.assetId === pool.id);
529
- const deposited = assetState ? compoundBalance(assetState.supplyBalance, pool.currentSupplyIndex, pool) : 0;
530
- const effectiveAmount = Math.min(amount, Math.max(0, deposited - withdrawDustBuffer(assetInfo.decimals)));
458
+ const posResult = await getPositions(client, address);
459
+ const supply = posResult.positions.find(
460
+ (p) => p.type === "save" && p.asset === asset
461
+ );
462
+ const deposited = supply?.amount ?? 0;
463
+ const dustBuffer = 1e3 / 10 ** assetInfo.decimals;
464
+ const effectiveAmount = Math.min(amount, Math.max(0, deposited - dustBuffer));
531
465
  if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on NAVI`);
532
466
  const rawAmount = Number(stableToRaw(effectiveAmount, assetInfo.decimals));
533
467
  if (rawAmount <= 0) {
534
- throw new T2000Error("INVALID_AMOUNT", `Withdrawal amount rounds to zero \u2014 balance is dust`);
468
+ throw new T2000Error("INVALID_AMOUNT", "Withdrawal amount rounds to zero \u2014 balance is dust");
535
469
  }
536
470
  const tx = new Transaction();
537
471
  tx.setSender(address);
538
- refreshOracles(tx, config, pools);
539
- const [balance] = tx.moveCall({
540
- target: `${config.package}::incentive_v3::withdraw_v2`,
541
- arguments: [
542
- tx.object(CLOCK),
543
- tx.object(config.oracle.priceOracle),
544
- tx.object(config.storage),
545
- tx.object(pool.contract.pool),
546
- tx.pure.u8(pool.id),
547
- tx.pure.u64(rawAmount),
548
- tx.object(config.incentiveV2),
549
- tx.object(config.incentiveV3),
550
- tx.object(SUI_SYSTEM_STATE)
551
- ],
552
- typeArguments: [pool.suiCoinType]
553
- });
554
- const [coin] = tx.moveCall({
555
- target: "0x2::coin::from_balance",
556
- arguments: [balance],
557
- typeArguments: [pool.suiCoinType]
558
- });
559
- tx.transferObjects([coin], address);
472
+ try {
473
+ const coinResult = await withdrawCoinPTB(tx, assetInfo.type, rawAmount, sdkOptions(client));
474
+ const [coin] = tx.moveCall({
475
+ target: "0x2::coin::from_balance",
476
+ arguments: [coinResult],
477
+ typeArguments: [assetInfo.type]
478
+ });
479
+ tx.transferObjects([coin], address);
480
+ } catch (err) {
481
+ const msg = err instanceof Error ? err.message : String(err);
482
+ throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI withdraw failed: ${msg}`);
483
+ }
560
484
  return { tx, effectiveAmount };
561
485
  }
562
486
  async function addWithdrawToTx(tx, client, address, amount, options = {}) {
563
487
  const asset = options.asset ?? "USDC";
564
488
  const assetInfo = resolveAssetInfo(asset);
565
- const [config, pool, pools, states] = await Promise.all([
566
- getConfig(),
567
- getPool(asset),
568
- getPools(),
569
- getUserState(client, address)
570
- ]);
571
- const assetState = states.find((s) => s.assetId === pool.id);
572
- const deposited = assetState ? compoundBalance(assetState.supplyBalance, pool.currentSupplyIndex, pool) : 0;
573
- const effectiveAmount = Math.min(amount, Math.max(0, deposited - withdrawDustBuffer(assetInfo.decimals)));
489
+ const posResult = await getPositions(client, address);
490
+ const supply = posResult.positions.find(
491
+ (p) => p.type === "save" && p.asset === asset
492
+ );
493
+ const deposited = supply?.amount ?? 0;
494
+ const dustBuffer = 1e3 / 10 ** assetInfo.decimals;
495
+ const effectiveAmount = Math.min(amount, Math.max(0, deposited - dustBuffer));
574
496
  if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on NAVI`);
575
497
  const rawAmount = Number(stableToRaw(effectiveAmount, assetInfo.decimals));
576
498
  if (rawAmount <= 0) {
577
- const [coin2] = tx.moveCall({
499
+ const [coin] = tx.moveCall({
578
500
  target: "0x2::coin::zero",
579
- typeArguments: [pool.suiCoinType]
501
+ typeArguments: [assetInfo.type]
580
502
  });
581
- return { coin: coin2, effectiveAmount: 0 };
503
+ return { coin, effectiveAmount: 0 };
504
+ }
505
+ try {
506
+ const coinResult = await withdrawCoinPTB(tx, assetInfo.type, rawAmount, sdkOptions(client));
507
+ const [coin] = tx.moveCall({
508
+ target: "0x2::coin::from_balance",
509
+ arguments: [coinResult],
510
+ typeArguments: [assetInfo.type]
511
+ });
512
+ return { coin, effectiveAmount };
513
+ } catch (err) {
514
+ const msg = err instanceof Error ? err.message : String(err);
515
+ throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI withdraw failed: ${msg}`);
582
516
  }
583
- refreshOracles(tx, config, pools);
584
- const [balance] = tx.moveCall({
585
- target: `${config.package}::incentive_v3::withdraw_v2`,
586
- arguments: [
587
- tx.object(CLOCK),
588
- tx.object(config.oracle.priceOracle),
589
- tx.object(config.storage),
590
- tx.object(pool.contract.pool),
591
- tx.pure.u8(pool.id),
592
- tx.pure.u64(rawAmount),
593
- tx.object(config.incentiveV2),
594
- tx.object(config.incentiveV3),
595
- tx.object(SUI_SYSTEM_STATE)
596
- ],
597
- typeArguments: [pool.suiCoinType]
598
- });
599
- const [coin] = tx.moveCall({
600
- target: "0x2::coin::from_balance",
601
- arguments: [balance],
602
- typeArguments: [pool.suiCoinType]
603
- });
604
- return { coin, effectiveAmount };
605
517
  }
606
518
  async function addSaveToTx(tx, _client, _address, coin, options = {}) {
607
519
  const asset = options.asset ?? "USDC";
608
- const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
520
+ const assetInfo = resolveAssetInfo(asset);
609
521
  if (options.collectFee) {
610
522
  addCollectFeeToTx(tx, coin, "save");
611
523
  }
612
- const [coinValue] = tx.moveCall({
613
- target: "0x2::coin::value",
614
- typeArguments: [pool.suiCoinType],
615
- arguments: [coin]
616
- });
617
- tx.moveCall({
618
- target: `${config.package}::incentive_v3::entry_deposit`,
619
- arguments: [
620
- tx.object(CLOCK),
621
- tx.object(config.storage),
622
- tx.object(pool.contract.pool),
623
- tx.pure.u8(pool.id),
624
- coin,
625
- coinValue,
626
- tx.object(config.incentiveV2),
627
- tx.object(config.incentiveV3)
628
- ],
629
- typeArguments: [pool.suiCoinType]
630
- });
524
+ try {
525
+ await depositCoinPTB(tx, assetInfo.type, coin, { env: "prod" });
526
+ } catch (err) {
527
+ const msg = err instanceof Error ? err.message : String(err);
528
+ throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI deposit failed: ${msg}`);
529
+ }
631
530
  }
632
531
  async function addRepayToTx(tx, _client, _address, coin, options = {}) {
633
532
  const asset = options.asset ?? "USDC";
634
- const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
635
- addOracleUpdate(tx, config, pool);
636
- const [coinValue] = tx.moveCall({
637
- target: "0x2::coin::value",
638
- typeArguments: [pool.suiCoinType],
639
- arguments: [coin]
640
- });
641
- tx.moveCall({
642
- target: `${config.package}::incentive_v3::entry_repay`,
643
- arguments: [
644
- tx.object(CLOCK),
645
- tx.object(config.oracle.priceOracle),
646
- tx.object(config.storage),
647
- tx.object(pool.contract.pool),
648
- tx.pure.u8(pool.id),
649
- coin,
650
- coinValue,
651
- tx.object(config.incentiveV2),
652
- tx.object(config.incentiveV3)
653
- ],
654
- typeArguments: [pool.suiCoinType]
655
- });
533
+ const assetInfo = resolveAssetInfo(asset);
534
+ try {
535
+ await repayCoinPTB(tx, assetInfo.type, coin, { env: "prod" });
536
+ } catch (err) {
537
+ const msg = err instanceof Error ? err.message : String(err);
538
+ throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI repay failed: ${msg}`);
539
+ }
656
540
  }
657
541
  async function buildBorrowTx(client, address, amount, options = {}) {
658
542
  if (!amount || amount <= 0 || !Number.isFinite(amount)) {
@@ -661,38 +545,23 @@ async function buildBorrowTx(client, address, amount, options = {}) {
661
545
  const asset = options.asset ?? "USDC";
662
546
  const assetInfo = resolveAssetInfo(asset);
663
547
  const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
664
- const [config, pool, pools] = await Promise.all([
665
- getConfig(),
666
- getPool(asset),
667
- getPools()
668
- ]);
669
548
  const tx = new Transaction();
670
549
  tx.setSender(address);
671
- refreshOracles(tx, config, pools);
672
- const [balance] = tx.moveCall({
673
- target: `${config.package}::incentive_v3::borrow_v2`,
674
- arguments: [
675
- tx.object(CLOCK),
676
- tx.object(config.oracle.priceOracle),
677
- tx.object(config.storage),
678
- tx.object(pool.contract.pool),
679
- tx.pure.u8(pool.id),
680
- tx.pure.u64(rawAmount),
681
- tx.object(config.incentiveV2),
682
- tx.object(config.incentiveV3),
683
- tx.object(SUI_SYSTEM_STATE)
684
- ],
685
- typeArguments: [pool.suiCoinType]
686
- });
687
- const [borrowedCoin] = tx.moveCall({
688
- target: "0x2::coin::from_balance",
689
- arguments: [balance],
690
- typeArguments: [pool.suiCoinType]
691
- });
692
- if (options.collectFee) {
693
- addCollectFeeToTx(tx, borrowedCoin, "borrow");
550
+ try {
551
+ const coinResult = await borrowCoinPTB(tx, assetInfo.type, rawAmount, sdkOptions(client));
552
+ const [borrowedCoin] = tx.moveCall({
553
+ target: "0x2::coin::from_balance",
554
+ arguments: [coinResult],
555
+ typeArguments: [assetInfo.type]
556
+ });
557
+ if (options.collectFee) {
558
+ addCollectFeeToTx(tx, borrowedCoin, "borrow");
559
+ }
560
+ tx.transferObjects([borrowedCoin], address);
561
+ } catch (err) {
562
+ const msg = err instanceof Error ? err.message : String(err);
563
+ throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI borrow failed: ${msg}`);
694
564
  }
695
- tx.transferObjects([borrowedCoin], address);
696
565
  return tx;
697
566
  }
698
567
  async function buildRepayTx(client, address, amount, options = {}) {
@@ -701,163 +570,23 @@ async function buildRepayTx(client, address, amount, options = {}) {
701
570
  }
702
571
  const asset = options.asset ?? "USDC";
703
572
  const assetInfo = resolveAssetInfo(asset);
704
- const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
705
- const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
706
573
  const coins = await fetchCoins(client, address, assetInfo.type);
707
574
  if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins to repay with`);
708
575
  const tx = new Transaction();
709
576
  tx.setSender(address);
710
- addOracleUpdate(tx, config, pool);
711
577
  const coinObj = mergeCoins(tx, coins);
712
- tx.moveCall({
713
- target: `${config.package}::incentive_v3::entry_repay`,
714
- arguments: [
715
- tx.object(CLOCK),
716
- tx.object(config.oracle.priceOracle),
717
- tx.object(config.storage),
718
- tx.object(pool.contract.pool),
719
- tx.pure.u8(pool.id),
720
- coinObj,
721
- tx.pure.u64(rawAmount),
722
- tx.object(config.incentiveV2),
723
- tx.object(config.incentiveV3)
724
- ],
725
- typeArguments: [pool.suiCoinType]
726
- });
727
- return tx;
728
- }
729
- async function getHealthFactor(client, addressOrKeypair) {
730
- const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
731
- const [config, pools, states] = await Promise.all([
732
- getConfig(),
733
- getPools(),
734
- getUserState(client, address)
735
- ]);
736
- let supplied = 0;
737
- let borrowed = 0;
738
- let weightedLtv = 0;
739
- let weightedLiqThreshold = 0;
740
- for (const state of states) {
741
- const pool = pools.find((p) => p.id === state.assetId);
742
- if (!pool) continue;
743
- const supplyBal = compoundBalance(state.supplyBalance, pool.currentSupplyIndex, pool);
744
- const borrowBal = compoundBalance(state.borrowBalance, pool.currentBorrowIndex, pool);
745
- const price = pool.token?.price ?? 1;
746
- supplied += supplyBal * price;
747
- borrowed += borrowBal * price;
748
- if (supplyBal > 0) {
749
- weightedLtv += supplyBal * price * parseLtv(pool.ltv);
750
- weightedLiqThreshold += supplyBal * price * parseLiqThreshold(pool.liquidationFactor.threshold);
751
- }
752
- }
753
- const ltv = supplied > 0 ? weightedLtv / supplied : 0.75;
754
- const liqThreshold = supplied > 0 ? weightedLiqThreshold / supplied : 0.75;
755
- const maxBorrowVal = Math.max(0, supplied * ltv - borrowed);
756
- const usdcPool = pools.find((p) => matchesCoinType(p.suiCoinType || p.coinType || "", SUPPORTED_ASSETS.USDC.type));
757
- let healthFactor;
758
- if (borrowed <= 0) {
759
- healthFactor = Infinity;
760
- } else if (usdcPool) {
761
- try {
762
- const tx = new Transaction();
763
- tx.moveCall({
764
- target: `${config.uiGetter}::calculator_unchecked::dynamic_health_factor`,
765
- arguments: [
766
- tx.object(CLOCK),
767
- tx.object(config.storage),
768
- tx.object(config.oracle.priceOracle),
769
- tx.pure.u8(usdcPool.id),
770
- tx.pure.address(address),
771
- tx.pure.u8(usdcPool.id),
772
- tx.pure.u64(0),
773
- tx.pure.u64(0),
774
- tx.pure.bool(false)
775
- ],
776
- typeArguments: [usdcPool.suiCoinType]
777
- });
778
- const result = await client.devInspectTransactionBlock({
779
- transactionBlock: tx,
780
- sender: address
781
- });
782
- const decoded = decodeDevInspect(result, bcs.u256());
783
- if (decoded !== void 0) {
784
- healthFactor = normalizeHealthFactor(Number(decoded));
785
- } else {
786
- healthFactor = supplied * liqThreshold / borrowed;
787
- }
788
- } catch {
789
- healthFactor = supplied * liqThreshold / borrowed;
790
- }
791
- } else {
792
- healthFactor = supplied * liqThreshold / borrowed;
793
- }
794
- return {
795
- healthFactor,
796
- supplied,
797
- borrowed,
798
- maxBorrow: maxBorrowVal,
799
- liquidationThreshold: liqThreshold
800
- };
801
- }
802
- var NAVI_SUPPORTED_ASSETS = [...STABLE_ASSETS, "SUI", "ETH", "GOLD"];
803
- async function getRates(client) {
578
+ const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
579
+ const [repayCoin] = tx.splitCoins(coinObj, [rawAmount]);
804
580
  try {
805
- const pools = await getPools();
806
- const result = {};
807
- for (const asset of NAVI_SUPPORTED_ASSETS) {
808
- const targetType = SUPPORTED_ASSETS[asset].type;
809
- const pool = pools.find((p) => matchesCoinType(p.suiCoinType || p.coinType || "", targetType));
810
- if (!pool) continue;
811
- let saveApy = poolSaveApy(pool);
812
- let borrowApy = poolBorrowApy(pool);
813
- if (saveApy <= 0 || saveApy > 200) saveApy = 0;
814
- if (borrowApy <= 0 || borrowApy > 200) borrowApy = 0;
815
- result[asset] = { saveApy, borrowApy };
816
- }
817
- if (!result.USDC) result.USDC = { saveApy: 4, borrowApy: 6 };
818
- return result;
819
- } catch {
820
- return { USDC: { saveApy: 4, borrowApy: 6 } };
821
- }
822
- }
823
- async function getPositions(client, addressOrKeypair) {
824
- const address = typeof addressOrKeypair === "string" ? addressOrKeypair : addressOrKeypair.getPublicKey().toSuiAddress();
825
- const [states, cachedPools] = await Promise.all([getUserState(client, address), getPools()]);
826
- let pools = cachedPools;
827
- const unmatchedIds = states.filter((s) => !pools.find((p) => p.id === s.assetId)).map((s) => s.assetId);
828
- if (unmatchedIds.length > 0) {
829
- pools = await getPools(true);
830
- }
831
- const positions = [];
832
- for (const state of states) {
833
- const pool = pools.find((p) => p.id === state.assetId);
834
- if (!pool) {
835
- console.warn(`[NAVI] No pool found for assetId=${state.assetId} (supply=${state.supplyBalance}, borrow=${state.borrowBalance}) \u2014 funds may be invisible`);
836
- continue;
837
- }
838
- const symbol = resolvePoolSymbol(pool);
839
- const supplyBal = compoundBalance(state.supplyBalance, pool.currentSupplyIndex, pool);
840
- const borrowBal = compoundBalance(state.borrowBalance, pool.currentBorrowIndex, pool);
841
- if (supplyBal > 1e-4) {
842
- positions.push({
843
- protocol: "navi",
844
- asset: symbol,
845
- type: "save",
846
- amount: supplyBal,
847
- apy: poolSaveApy(pool)
848
- });
849
- }
850
- if (borrowBal > 1e-4) {
851
- positions.push({
852
- protocol: "navi",
853
- asset: symbol,
854
- type: "borrow",
855
- amount: borrowBal,
856
- apy: poolBorrowApy(pool)
857
- });
858
- }
581
+ await repayCoinPTB(tx, assetInfo.type, repayCoin, {
582
+ ...sdkOptions(client),
583
+ amount: rawAmount
584
+ });
585
+ } catch (err) {
586
+ const msg = err instanceof Error ? err.message : String(err);
587
+ throw new T2000Error("PROTOCOL_UNAVAILABLE", `NAVI repay failed: ${msg}`);
859
588
  }
860
- return { positions };
589
+ return tx;
861
590
  }
862
591
  async function maxWithdrawAmount(client, addressOrKeypair) {
863
592
  const hf = await getHealthFactor(client, addressOrKeypair);
@@ -878,164 +607,61 @@ async function maxBorrowAmount(client, addressOrKeypair) {
878
607
  const maxAmount = Math.max(0, hf.supplied * ltv / MIN_HEALTH_FACTOR - hf.borrowed);
879
608
  return { maxAmount, healthFactorAfter: MIN_HEALTH_FACTOR, currentHF: hf.healthFactor };
880
609
  }
881
- var CERT_TYPE = "0x549e8b69270defbfafd4f94e17ec44cdbdd99820b33bda2278dea3b9a32d3f55::cert::CERT";
882
- var DEEP_TYPE = "0xdeeb7a4662eec9f2f3def03fb937a663dddaa2e215b8078a284d026b7946c270::deep::DEEP";
883
- var REWARD_FUNDS = {
884
- [CERT_TYPE]: "0x7093cf7549d5e5b35bfde2177223d1050f71655c7f676a5e610ee70eb4d93b5c",
885
- [DEEP_TYPE]: "0xc889d78b634f954979e80e622a2ae0fece824c0f6d9590044378a2563035f32f"
886
- };
887
- var REWARD_SYMBOLS = {
888
- [CERT_TYPE]: "vSUI",
889
- [DEEP_TYPE]: "DEEP"
890
- };
891
- var incentiveRulesCache = null;
892
- async function getIncentiveRules(client) {
893
- if (incentiveRulesCache && Date.now() - incentiveRulesCache.ts < CACHE_TTL) {
894
- return incentiveRulesCache.data;
895
- }
896
- const [pools, obj] = await Promise.all([
897
- getPools(),
898
- client.getObject({
899
- id: "0x62982dad27fb10bb314b3384d5de8d2ac2d72ab2dbeae5d801dbdb9efa816c80",
900
- options: { showContent: true }
901
- })
902
- ]);
903
- const rewardCoinMap = /* @__PURE__ */ new Map();
904
- for (const pool of pools) {
905
- const ct = (pool.suiCoinType || pool.coinType || "").toLowerCase();
906
- const suffix = ct.split("::").slice(1).join("::");
907
- const coins = pool.supplyIncentiveApyInfo?.rewardCoin;
908
- if (Array.isArray(coins) && coins.length > 0) {
909
- rewardCoinMap.set(suffix, coins[0]);
910
- }
911
- }
912
- const result = /* @__PURE__ */ new Map();
913
- if (obj.data?.content?.dataType !== "moveObject") {
914
- incentiveRulesCache = { data: result, ts: Date.now() };
915
- return result;
916
- }
917
- const fields = obj.data.content.fields;
918
- const poolsObj = fields.pools;
919
- const entries = poolsObj?.fields?.contents;
920
- if (!Array.isArray(entries)) {
921
- incentiveRulesCache = { data: result, ts: Date.now() };
922
- return result;
923
- }
924
- for (const entry of entries) {
925
- const ef = entry?.fields;
926
- if (!ef) continue;
927
- const key = String(ef.key ?? "");
928
- const value = ef.value;
929
- const rules = value?.fields?.rules;
930
- const ruleEntries = rules?.fields?.contents;
931
- if (!Array.isArray(ruleEntries)) continue;
932
- const ruleIds = ruleEntries.map((re) => {
933
- const rf = re?.fields;
934
- return String(rf?.key ?? "");
935
- }).filter(Boolean);
936
- const suffix = key.split("::").slice(1).join("::").toLowerCase();
937
- const full = key.toLowerCase();
938
- const rewardCoin = rewardCoinMap.get(suffix) ?? rewardCoinMap.get(full) ?? null;
939
- result.set(key, { ruleIds, rewardCoinType: rewardCoin });
940
- }
941
- incentiveRulesCache = { data: result, ts: Date.now() };
942
- return result;
943
- }
944
- function stripPrefix(coinType) {
945
- return coinType.replace(/^0x0*/, "");
946
- }
947
610
  async function getPendingRewards(client, address) {
948
- const [pools, states] = await Promise.all([
949
- getPools(),
950
- getUserState(client, address)
951
- ]);
952
- const rewards = [];
953
- const deposited = states.filter((s) => s.supplyBalance > 0n);
954
- if (deposited.length === 0) return rewards;
955
- for (const state of deposited) {
956
- const pool = pools.find((p) => p.id === state.assetId);
957
- if (!pool) continue;
958
- const boostedApr = parseFloat(pool.supplyIncentiveApyInfo?.boostedApr ?? "0");
959
- if (boostedApr <= 0) continue;
960
- const rewardCoins = pool.supplyIncentiveApyInfo?.rewardCoin;
961
- if (!Array.isArray(rewardCoins) || rewardCoins.length === 0) continue;
962
- const rewardType = rewardCoins[0];
963
- const assetSymbol = resolvePoolSymbol(pool);
964
- rewards.push({
965
- protocol: "navi",
966
- asset: assetSymbol,
967
- coinType: rewardType,
968
- symbol: REWARD_SYMBOLS[rewardType] ?? rewardType.split("::").pop() ?? "UNKNOWN",
969
- amount: 0,
970
- estimatedValueUsd: 0
611
+ try {
612
+ const rewards = await getUserAvailableLendingRewards(address, {
613
+ ...sdkOptions(client),
614
+ markets: ["main"]
971
615
  });
616
+ if (!rewards || rewards.length === 0) return [];
617
+ const summary = summaryLendingRewards(rewards);
618
+ const result = [];
619
+ for (const s of summary) {
620
+ for (const rw of s.rewards) {
621
+ const available = Number(rw.available);
622
+ if (available <= 0) continue;
623
+ const symbol = rw.coinType.split("::").pop() ?? "UNKNOWN";
624
+ result.push({
625
+ protocol: "navi",
626
+ asset: String(s.assetId),
627
+ coinType: rw.coinType,
628
+ symbol,
629
+ amount: available,
630
+ estimatedValueUsd: 0
631
+ });
632
+ }
633
+ }
634
+ return result;
635
+ } catch {
636
+ return [];
972
637
  }
973
- return rewards;
974
638
  }
975
639
  async function addClaimRewardsToTx(tx, client, address) {
976
- const [config, pools, states, rules] = await Promise.all([
977
- getConfig(),
978
- getPools(),
979
- getUserState(client, address),
980
- getIncentiveRules(client)
981
- ]);
982
- const deposited = states.filter((s) => s.supplyBalance > 0n);
983
- if (deposited.length === 0) return [];
984
- const claimGroups = /* @__PURE__ */ new Map();
985
- for (const state of deposited) {
986
- const pool = pools.find((p) => p.id === state.assetId);
987
- if (!pool) continue;
988
- const boostedApr = parseFloat(pool.supplyIncentiveApyInfo?.boostedApr ?? "0");
989
- if (boostedApr <= 0) continue;
990
- const rewardCoins = pool.supplyIncentiveApyInfo?.rewardCoin;
991
- if (!Array.isArray(rewardCoins) || rewardCoins.length === 0) continue;
992
- const rewardType = rewardCoins[0];
993
- const fundId = REWARD_FUNDS[rewardType];
994
- if (!fundId) continue;
995
- const coinType = pool.suiCoinType || pool.coinType || "";
996
- const strippedType = stripPrefix(coinType);
997
- const ruleData = Array.from(rules.entries()).find(
998
- ([key]) => stripPrefix(key) === strippedType || key.split("::").slice(1).join("::").toLowerCase() === coinType.split("::").slice(1).join("::").toLowerCase()
999
- );
1000
- if (!ruleData || ruleData[1].ruleIds.length === 0) continue;
1001
- const group = claimGroups.get(rewardType) ?? { assets: [], ruleIds: [] };
1002
- for (const ruleId of ruleData[1].ruleIds) {
1003
- group.assets.push(strippedType);
1004
- group.ruleIds.push(ruleId);
1005
- }
1006
- claimGroups.set(rewardType, group);
1007
- }
1008
- const claimed = [];
1009
- for (const [rewardType, { assets, ruleIds }] of claimGroups) {
1010
- const fundId = REWARD_FUNDS[rewardType];
1011
- const [balance] = tx.moveCall({
1012
- target: `${config.package}::incentive_v3::claim_reward`,
1013
- typeArguments: [rewardType],
1014
- arguments: [
1015
- tx.object(CLOCK),
1016
- tx.object(config.incentiveV3),
1017
- tx.object(config.storage),
1018
- tx.object(fundId),
1019
- tx.pure(bcs.vector(bcs.string()).serialize(assets)),
1020
- tx.pure(bcs.vector(bcs.Address).serialize(ruleIds))
1021
- ]
640
+ try {
641
+ const rewards = await getUserAvailableLendingRewards(address, {
642
+ ...sdkOptions(client),
643
+ markets: ["main"]
1022
644
  });
1023
- const [coin] = tx.moveCall({
1024
- target: "0x2::coin::from_balance",
1025
- typeArguments: [rewardType],
1026
- arguments: [balance]
645
+ if (!rewards || rewards.length === 0) return [];
646
+ const claimable = rewards.filter(
647
+ (r) => Number(r.userClaimableReward) > 0
648
+ );
649
+ if (claimable.length === 0) return [];
650
+ const claimed = await claimLendingRewardsPTB(tx, claimable, {
651
+ env: "prod",
652
+ customCoinReceive: { type: "transfer", transfer: address }
1027
653
  });
1028
- tx.transferObjects([coin], address);
1029
- claimed.push({
654
+ return claimed.map((c) => ({
1030
655
  protocol: "navi",
1031
- asset: assets.join(", "),
1032
- coinType: rewardType,
1033
- symbol: REWARD_SYMBOLS[rewardType] ?? "UNKNOWN",
656
+ asset: "",
657
+ coinType: "",
658
+ symbol: "REWARD",
1034
659
  amount: 0,
1035
660
  estimatedValueUsd: 0
1036
- });
661
+ }));
662
+ } catch {
663
+ return [];
1037
664
  }
1038
- return claimed;
1039
665
  }
1040
666
 
1041
667
  // src/adapters/navi.ts
@@ -1079,8 +705,8 @@ var NaviAdapter = class {
1079
705
  async getPositions(address) {
1080
706
  const result = await getPositions(this.client, address);
1081
707
  return {
1082
- supplies: result.positions.filter((p) => p.type === "save").map((p) => ({ asset: p.asset, amount: p.amount, apy: p.apy })),
1083
- borrows: result.positions.filter((p) => p.type === "borrow").map((p) => ({ asset: p.asset, amount: p.amount, apy: p.apy }))
708
+ supplies: result.positions.filter((p) => p.type === "save").map((p) => ({ asset: p.asset, amount: p.amount, amountUsd: p.amountUsd, apy: p.apy })),
709
+ borrows: result.positions.filter((p) => p.type === "borrow").map((p) => ({ asset: p.asset, amount: p.amount, amountUsd: p.amountUsd, apy: p.apy }))
1084
710
  };
1085
711
  }
1086
712
  async getHealth(address) {
@@ -1361,15 +987,8 @@ var CetusAdapter = class {
1361
987
  });
1362
988
  }
1363
989
  };
1364
- var WAD = 1e18;
1365
- var MIN_HEALTH_FACTOR2 = 1.5;
1366
- var CLOCK2 = "0x6";
1367
- var SUI_SYSTEM_STATE2 = "0x5";
1368
- var LENDING_MARKET_ID = "0x84030d26d85eaa7035084a057f2f11f701b7e2e4eda87551becbc7c97505ece1";
1369
- var LENDING_MARKET_TYPE = "0xf95b06141ed4a174f239417323bde3f209b972f5930d8521ea38a52aff3a6ddf::suilend::MAIN_POOL";
1370
990
  var SUILEND_PACKAGE = "0xf95b06141ed4a174f239417323bde3f209b972f5930d8521ea38a52aff3a6ddf";
1371
- var UPGRADE_CAP_ID = "0x3d4ef1859c3ee9fc72858f588b56a09da5466e64f8cc4e90a7b3b909fba8a7ae";
1372
- var FALLBACK_PUBLISHED_AT = "0x3d4353f3bd3565329655e6b77bc2abfd31e558b86662ebd078ae453d416bc10f";
991
+ var MIN_HEALTH_FACTOR2 = 1.5;
1373
992
  var descriptor3 = {
1374
993
  id: "suilend",
1375
994
  name: "Suilend",
@@ -1387,224 +1006,31 @@ var descriptor3 = {
1387
1006
  "lending_market::repay": "repay"
1388
1007
  }
1389
1008
  };
1390
- function interpolateRate(utilBreakpoints, aprBreakpoints, utilizationPct) {
1391
- if (utilBreakpoints.length === 0) return 0;
1392
- if (utilizationPct <= utilBreakpoints[0]) return aprBreakpoints[0];
1393
- if (utilizationPct >= utilBreakpoints[utilBreakpoints.length - 1]) {
1394
- return aprBreakpoints[aprBreakpoints.length - 1];
1395
- }
1396
- for (let i = 1; i < utilBreakpoints.length; i++) {
1397
- if (utilizationPct <= utilBreakpoints[i]) {
1398
- const t = (utilizationPct - utilBreakpoints[i - 1]) / (utilBreakpoints[i] - utilBreakpoints[i - 1]);
1399
- return aprBreakpoints[i - 1] + t * (aprBreakpoints[i] - aprBreakpoints[i - 1]);
1400
- }
1401
- }
1402
- return aprBreakpoints[aprBreakpoints.length - 1];
1403
- }
1404
- function computeRates(reserve) {
1405
- const available = reserve.availableAmount / 10 ** reserve.mintDecimals;
1406
- const borrowed = reserve.borrowedAmountWad / WAD / 10 ** reserve.mintDecimals;
1407
- const totalDeposited = available + borrowed;
1408
- const utilizationPct = totalDeposited > 0 ? borrowed / totalDeposited * 100 : 0;
1409
- if (reserve.interestRateUtils.length === 0) return { borrowAprPct: 0, depositAprPct: 0 };
1410
- const aprs = reserve.interestRateAprs.map((a) => a / 100);
1411
- const borrowAprPct = interpolateRate(reserve.interestRateUtils, aprs, utilizationPct);
1412
- const depositAprPct = utilizationPct / 100 * (borrowAprPct / 100) * (1 - reserve.spreadFeeBps / 1e4) * 100;
1413
- return { borrowAprPct, depositAprPct };
1414
- }
1415
- var MS_PER_YEAR = 365.25 * 24 * 3600 * 1e3;
1416
- function computeDepositRewardApr(reserve, allReserves) {
1417
- if (reserve.depositTotalShares <= 0 || reserve.price <= 0) return 0;
1418
- const totalDepositValue = reserve.depositTotalShares / 10 ** reserve.mintDecimals * reserve.price;
1419
- if (totalDepositValue <= 0) return 0;
1420
- const priceMap = /* @__PURE__ */ new Map();
1421
- for (const r of allReserves) {
1422
- if (r.price > 0) priceMap.set(r.coinType, { price: r.price, decimals: r.mintDecimals });
1423
- }
1424
- let rewardApr = 0;
1425
- for (const rw of reserve.depositPoolRewards) {
1426
- const info = priceMap.get(rw.coinType);
1427
- if (!info || info.price <= 0) continue;
1428
- const durationMs = rw.endTimeMs - rw.startTimeMs;
1429
- if (durationMs <= 0) continue;
1430
- const annualTokens = rw.totalRewards / 10 ** info.decimals * (MS_PER_YEAR / durationMs);
1431
- rewardApr += annualTokens * info.price / totalDepositValue * 100;
1432
- }
1433
- return rewardApr;
1434
- }
1435
- function cTokenRatio(reserve) {
1436
- if (reserve.ctokenSupply === 0) return 1;
1437
- const totalSupply = reserve.availableAmount + reserve.borrowedAmountWad / WAD - reserve.unclaimedSpreadFeesWad / WAD;
1438
- return totalSupply / reserve.ctokenSupply;
1439
- }
1440
- function f(obj) {
1441
- if (obj && typeof obj === "object" && "fields" in obj) return obj.fields;
1442
- return obj;
1443
- }
1444
- function str(v) {
1445
- return String(v ?? "0");
1446
- }
1447
- function num(v) {
1448
- return Number(str(v));
1449
- }
1450
- function parseReserve(raw, index) {
1451
- const r = f(raw);
1452
- const coinTypeField = f(r.coin_type);
1453
- const config = f(f(r.config)?.element);
1454
- const dMgr = f(r.deposits_pool_reward_manager);
1455
- const rawRewards = Array.isArray(dMgr?.pool_rewards) ? dMgr.pool_rewards : [];
1456
- const now = Date.now();
1457
- const depositPoolRewards = rawRewards.map((rw, idx) => {
1458
- if (rw === null) return null;
1459
- const rwf = f(rw);
1460
- return {
1461
- coinType: str(f(rwf.coin_type)?.name),
1462
- totalRewards: num(rwf.total_rewards),
1463
- startTimeMs: num(rwf.start_time_ms),
1464
- endTimeMs: num(rwf.end_time_ms),
1465
- rewardIndex: idx
1466
- };
1467
- }).filter((rw) => rw !== null && rw.endTimeMs > now && rw.totalRewards > 0);
1468
- return {
1469
- coinType: str(coinTypeField?.name),
1470
- mintDecimals: num(r.mint_decimals),
1471
- availableAmount: num(r.available_amount),
1472
- borrowedAmountWad: num(f(r.borrowed_amount)?.value),
1473
- ctokenSupply: num(r.ctoken_supply),
1474
- unclaimedSpreadFeesWad: num(f(r.unclaimed_spread_fees)?.value),
1475
- cumulativeBorrowRateWad: num(f(r.cumulative_borrow_rate)?.value),
1476
- openLtvPct: num(config?.open_ltv_pct),
1477
- closeLtvPct: num(config?.close_ltv_pct),
1478
- spreadFeeBps: num(config?.spread_fee_bps),
1479
- interestRateUtils: Array.isArray(config?.interest_rate_utils) ? config.interest_rate_utils.map(num) : [],
1480
- interestRateAprs: Array.isArray(config?.interest_rate_aprs) ? config.interest_rate_aprs.map(num) : [],
1481
- arrayIndex: index,
1482
- price: num(f(r.price)?.value) / WAD,
1483
- depositTotalShares: num(dMgr?.total_shares),
1484
- depositPoolRewards
1485
- };
1486
- }
1487
- function parseObligation(raw) {
1488
- const deposits = Array.isArray(raw.deposits) ? raw.deposits.map((d) => {
1489
- const df = f(d);
1490
- return {
1491
- coinType: str(f(df.coin_type)?.name),
1492
- ctokenAmount: num(df.deposited_ctoken_amount),
1493
- reserveIdx: num(df.reserve_array_index)
1494
- };
1495
- }) : [];
1496
- const borrows = Array.isArray(raw.borrows) ? raw.borrows.map((b) => {
1497
- const bf = f(b);
1498
- return {
1499
- coinType: str(f(bf.coin_type)?.name),
1500
- borrowedWad: num(f(bf.borrowed_amount)?.value),
1501
- cumBorrowRateWad: num(f(bf.cumulative_borrow_rate)?.value),
1502
- reserveIdx: num(bf.reserve_array_index)
1503
- };
1504
- }) : [];
1505
- return { deposits, borrows };
1506
- }
1507
1009
  var SuilendAdapter = class {
1508
1010
  id = "suilend";
1509
1011
  name = "Suilend";
1510
- version = "2.0.0";
1012
+ version = "3.0.0";
1511
1013
  capabilities = ["save", "withdraw", "borrow", "repay"];
1512
1014
  supportedAssets = [...STABLE_ASSETS, "SUI", "ETH", "BTC", "GOLD"];
1513
1015
  supportsSameAssetBorrow = false;
1514
1016
  client;
1515
- publishedAt = null;
1516
- reserveCache = null;
1017
+ sdkClient = null;
1517
1018
  async init(client) {
1518
1019
  this.client = client;
1519
1020
  }
1520
1021
  initSync(client) {
1521
1022
  this.client = client;
1522
1023
  }
1523
- // -- On-chain reads -------------------------------------------------------
1524
- async resolvePackage() {
1525
- if (this.publishedAt) return this.publishedAt;
1526
- try {
1527
- const cap = await this.client.getObject({ id: UPGRADE_CAP_ID, options: { showContent: true } });
1528
- if (cap.data?.content?.dataType === "moveObject") {
1529
- const fields = cap.data.content.fields;
1530
- this.publishedAt = str(fields.package);
1531
- return this.publishedAt;
1532
- }
1533
- } catch {
1024
+ async getSdkClient() {
1025
+ if (!this.sdkClient) {
1026
+ this.sdkClient = await SuilendClient.initialize(
1027
+ LENDING_MARKET_ID,
1028
+ LENDING_MARKET_TYPE,
1029
+ this.client,
1030
+ false
1031
+ );
1534
1032
  }
1535
- this.publishedAt = FALLBACK_PUBLISHED_AT;
1536
- return this.publishedAt;
1537
- }
1538
- async loadReserves(fresh = false) {
1539
- if (this.reserveCache && !fresh) return this.reserveCache;
1540
- const market = await this.client.getObject({
1541
- id: LENDING_MARKET_ID,
1542
- options: { showContent: true }
1543
- });
1544
- if (market.data?.content?.dataType !== "moveObject") {
1545
- throw new T2000Error("PROTOCOL_UNAVAILABLE", "Failed to read Suilend lending market");
1546
- }
1547
- const fields = market.data.content.fields;
1548
- const reservesRaw = fields.reserves;
1549
- if (!Array.isArray(reservesRaw)) {
1550
- throw new T2000Error("PROTOCOL_UNAVAILABLE", "Failed to parse Suilend reserves");
1551
- }
1552
- this.reserveCache = reservesRaw.map((r, i) => parseReserve(r, i));
1553
- return this.reserveCache;
1554
- }
1555
- findReserve(reserves, asset) {
1556
- let coinType;
1557
- if (asset in SUPPORTED_ASSETS) {
1558
- coinType = SUPPORTED_ASSETS[asset].type;
1559
- } else if (asset.includes("::")) {
1560
- coinType = asset;
1561
- } else {
1562
- return void 0;
1563
- }
1564
- try {
1565
- const normalized = normalizeStructTag(coinType);
1566
- return reserves.find((r) => {
1567
- try {
1568
- return normalizeStructTag(r.coinType) === normalized;
1569
- } catch {
1570
- return false;
1571
- }
1572
- });
1573
- } catch {
1574
- return void 0;
1575
- }
1576
- }
1577
- async fetchObligationCaps(address) {
1578
- const capType = `${SUILEND_PACKAGE}::lending_market::ObligationOwnerCap<${LENDING_MARKET_TYPE}>`;
1579
- const caps = [];
1580
- let cursor;
1581
- let hasNext = true;
1582
- while (hasNext) {
1583
- const page = await this.client.getOwnedObjects({
1584
- owner: address,
1585
- filter: { StructType: capType },
1586
- options: { showContent: true },
1587
- cursor: cursor ?? void 0
1588
- });
1589
- for (const item of page.data) {
1590
- if (item.data?.content?.dataType !== "moveObject") continue;
1591
- const fields = item.data.content.fields;
1592
- caps.push({
1593
- id: item.data.objectId,
1594
- obligationId: str(fields.obligation_id)
1595
- });
1596
- }
1597
- cursor = page.nextCursor;
1598
- hasNext = page.hasNextPage;
1599
- }
1600
- return caps;
1601
- }
1602
- async fetchObligation(obligationId) {
1603
- const obj = await this.client.getObject({ id: obligationId, options: { showContent: true } });
1604
- if (obj.data?.content?.dataType !== "moveObject") {
1605
- throw new T2000Error("PROTOCOL_UNAVAILABLE", "Failed to read Suilend obligation");
1606
- }
1607
- return parseObligation(obj.data.content.fields);
1033
+ return this.sdkClient;
1608
1034
  }
1609
1035
  resolveSymbol(coinType) {
1610
1036
  try {
@@ -1620,371 +1046,217 @@ var SuilendAdapter = class {
1620
1046
  const parts = coinType.split("::");
1621
1047
  return parts[parts.length - 1] || "UNKNOWN";
1622
1048
  }
1623
- // -- Adapter interface ----------------------------------------------------
1624
1049
  async getRates(asset) {
1625
- const reserves = await this.loadReserves();
1626
- const reserve = this.findReserve(reserves, asset);
1627
- if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `Suilend does not support ${asset}`);
1628
- const { borrowAprPct, depositAprPct } = computeRates(reserve);
1629
- const rewardApr = computeDepositRewardApr(reserve, reserves);
1630
- return { asset, saveApy: depositAprPct + rewardApr, borrowApy: borrowAprPct };
1050
+ try {
1051
+ const sdk = await this.getSdkClient();
1052
+ const { reserveMap } = await initializeSuilend(this.client, sdk);
1053
+ const assetInfo = SUPPORTED_ASSETS[asset];
1054
+ if (!assetInfo) throw new T2000Error("ASSET_NOT_SUPPORTED", `Suilend does not support ${asset}`);
1055
+ const normalized = normalizeStructTag(assetInfo.type);
1056
+ const reserve = Object.values(reserveMap).find((r) => {
1057
+ try {
1058
+ return normalizeStructTag(r.coinType) === normalized;
1059
+ } catch {
1060
+ return false;
1061
+ }
1062
+ });
1063
+ if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `Suilend does not support ${asset}`);
1064
+ return {
1065
+ asset,
1066
+ saveApy: reserve.depositAprPercent.toNumber(),
1067
+ borrowApy: reserve.borrowAprPercent.toNumber()
1068
+ };
1069
+ } catch (err) {
1070
+ if (err instanceof T2000Error) throw err;
1071
+ const msg = err instanceof Error ? err.message : String(err);
1072
+ throw new T2000Error("PROTOCOL_UNAVAILABLE", `Suilend getRates failed: ${msg}`);
1073
+ }
1631
1074
  }
1632
1075
  async getPositions(address) {
1633
1076
  const supplies = [];
1634
1077
  const borrows = [];
1635
- const caps = await this.fetchObligationCaps(address);
1636
- if (caps.length === 0) return { supplies, borrows };
1637
- const [reserves, obligation] = await Promise.all([
1638
- this.loadReserves(),
1639
- this.fetchObligation(caps[0].obligationId)
1640
- ]);
1641
- for (const dep of obligation.deposits) {
1642
- const reserve = reserves[dep.reserveIdx];
1643
- if (!reserve) continue;
1644
- const ratio = cTokenRatio(reserve);
1645
- const amount = dep.ctokenAmount * ratio / 10 ** reserve.mintDecimals;
1646
- const { depositAprPct } = computeRates(reserve);
1647
- const rewardApr = computeDepositRewardApr(reserve, reserves);
1648
- supplies.push({ asset: this.resolveSymbol(dep.coinType), amount, apy: depositAprPct + rewardApr });
1649
- }
1650
- for (const bor of obligation.borrows) {
1651
- const reserve = reserves[bor.reserveIdx];
1652
- if (!reserve) continue;
1653
- const rawAmount = bor.borrowedWad / WAD / 10 ** reserve.mintDecimals;
1654
- const reserveRate = reserve.cumulativeBorrowRateWad / WAD;
1655
- const posRate = bor.cumBorrowRateWad / WAD;
1656
- const compounded = posRate > 0 ? rawAmount * (reserveRate / posRate) : rawAmount;
1657
- const { borrowAprPct } = computeRates(reserve);
1658
- borrows.push({ asset: this.resolveSymbol(bor.coinType), amount: compounded, apy: borrowAprPct });
1078
+ try {
1079
+ const sdk = await this.getSdkClient();
1080
+ const { reserveMap, refreshedRawReserves } = await initializeSuilend(this.client, sdk);
1081
+ const { obligations, obligationOwnerCaps } = await initializeObligations(
1082
+ this.client,
1083
+ sdk,
1084
+ refreshedRawReserves,
1085
+ reserveMap,
1086
+ address
1087
+ );
1088
+ if (obligationOwnerCaps.length === 0 || obligations.length === 0) {
1089
+ return { supplies, borrows };
1090
+ }
1091
+ const obligation = obligations[0];
1092
+ for (const dep of obligation.deposits) {
1093
+ const symbol = this.resolveSymbol(dep.coinType);
1094
+ const amount = dep.depositedAmount.toNumber();
1095
+ const amountUsd = dep.depositedAmountUsd.toNumber();
1096
+ const apy = dep.reserve.depositAprPercent.toNumber();
1097
+ if (amount > 1e-4) {
1098
+ supplies.push({ asset: symbol, amount, amountUsd, apy });
1099
+ }
1100
+ }
1101
+ for (const bor of obligation.borrows) {
1102
+ const symbol = this.resolveSymbol(bor.coinType);
1103
+ const amount = bor.borrowedAmount.toNumber();
1104
+ const amountUsd = bor.borrowedAmountUsd.toNumber();
1105
+ const apy = bor.reserve.borrowAprPercent.toNumber();
1106
+ if (amount > 1e-4) {
1107
+ borrows.push({ asset: symbol, amount, amountUsd, apy });
1108
+ }
1109
+ }
1110
+ } catch (err) {
1111
+ if (err instanceof T2000Error) throw err;
1112
+ const msg = err instanceof Error ? err.message : String(err);
1113
+ throw new T2000Error("PROTOCOL_UNAVAILABLE", `Suilend getPositions failed: ${msg}`);
1659
1114
  }
1660
1115
  return { supplies, borrows };
1661
1116
  }
1662
1117
  async getHealth(address) {
1663
- const caps = await this.fetchObligationCaps(address);
1664
- if (caps.length === 0) {
1118
+ try {
1119
+ const sdk = await this.getSdkClient();
1120
+ const { reserveMap, refreshedRawReserves } = await initializeSuilend(this.client, sdk);
1121
+ const { obligations, obligationOwnerCaps } = await initializeObligations(
1122
+ this.client,
1123
+ sdk,
1124
+ refreshedRawReserves,
1125
+ reserveMap,
1126
+ address
1127
+ );
1128
+ if (obligationOwnerCaps.length === 0 || obligations.length === 0) {
1129
+ return { healthFactor: Infinity, supplied: 0, borrowed: 0, maxBorrow: 0, liquidationThreshold: 0 };
1130
+ }
1131
+ const ob = obligations[0];
1132
+ const supplied = ob.depositedAmountUsd.toNumber();
1133
+ const borrowed = ob.borrowedAmountUsd.toNumber();
1134
+ const borrowLimit = ob.borrowLimitUsd.toNumber();
1135
+ const unhealthy = ob.unhealthyBorrowValueUsd.toNumber();
1136
+ const liqThreshold = supplied > 0 ? unhealthy / supplied : 0.75;
1137
+ const healthFactor = borrowed > 0 ? unhealthy / borrowed : Infinity;
1138
+ const maxBorrow = Math.max(0, borrowLimit - borrowed);
1139
+ return { healthFactor, supplied, borrowed, maxBorrow, liquidationThreshold: liqThreshold };
1140
+ } catch {
1665
1141
  return { healthFactor: Infinity, supplied: 0, borrowed: 0, maxBorrow: 0, liquidationThreshold: 0 };
1666
1142
  }
1667
- const [reserves, obligation] = await Promise.all([
1668
- this.loadReserves(),
1669
- this.fetchObligation(caps[0].obligationId)
1670
- ]);
1671
- let supplied = 0;
1672
- let borrowed = 0;
1673
- let weightedCloseLtv = 0;
1674
- let weightedOpenLtv = 0;
1675
- for (const dep of obligation.deposits) {
1676
- const reserve = reserves[dep.reserveIdx];
1677
- if (!reserve) continue;
1678
- const ratio = cTokenRatio(reserve);
1679
- const amount = dep.ctokenAmount * ratio / 10 ** reserve.mintDecimals;
1680
- supplied += amount;
1681
- weightedCloseLtv += amount * (reserve.closeLtvPct / 100);
1682
- weightedOpenLtv += amount * (reserve.openLtvPct / 100);
1683
- }
1684
- for (const bor of obligation.borrows) {
1685
- const reserve = reserves[bor.reserveIdx];
1686
- if (!reserve) continue;
1687
- const rawAmount = bor.borrowedWad / WAD / 10 ** reserve.mintDecimals;
1688
- const reserveRate = reserve.cumulativeBorrowRateWad / WAD;
1689
- const posRate = bor.cumBorrowRateWad / WAD;
1690
- borrowed += posRate > 0 ? rawAmount * (reserveRate / posRate) : rawAmount;
1691
- }
1692
- const liqThreshold = supplied > 0 ? weightedCloseLtv / supplied : 0.75;
1693
- const openLtv = supplied > 0 ? weightedOpenLtv / supplied : 0.7;
1694
- const healthFactor = borrowed > 0 ? supplied * liqThreshold / borrowed : Infinity;
1695
- const maxBorrow = Math.max(0, supplied * openLtv - borrowed);
1696
- return { healthFactor, supplied, borrowed, maxBorrow, liquidationThreshold: liqThreshold };
1697
1143
  }
1698
1144
  async buildSaveTx(address, amount, asset, options) {
1699
1145
  const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
1700
1146
  const assetInfo = SUPPORTED_ASSETS[assetKey];
1701
- const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
1702
- const reserve = this.findReserve(reserves, assetKey);
1703
- if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend. Try: NAVI or a different asset.`);
1704
- const caps = await this.fetchObligationCaps(address);
1147
+ const sdk = await this.getSdkClient();
1148
+ const caps = await SuilendClient.getObligationOwnerCaps(address, [LENDING_MARKET_TYPE], this.client);
1705
1149
  const tx = new Transaction();
1706
1150
  tx.setSender(address);
1707
- let capRef;
1708
1151
  if (caps.length === 0) {
1709
- const [newCap] = tx.moveCall({
1710
- target: `${pkg}::lending_market::create_obligation`,
1711
- typeArguments: [LENDING_MARKET_TYPE],
1712
- arguments: [tx.object(LENDING_MARKET_ID)]
1713
- });
1714
- capRef = newCap;
1715
- } else {
1716
- capRef = caps[0].id;
1152
+ const newCap = sdk.createObligation(tx);
1153
+ tx.transferObjects([newCap], address);
1717
1154
  }
1718
- const allCoins = await this.fetchAllCoins(address, assetInfo.type);
1719
- if (allCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins found`);
1720
- const primaryCoinId = allCoins[0].coinObjectId;
1721
- if (allCoins.length > 1) {
1722
- tx.mergeCoins(tx.object(primaryCoinId), allCoins.slice(1).map((c) => tx.object(c.coinObjectId)));
1723
- }
1724
- const rawAmount = stableToRaw(amount, assetInfo.decimals).toString();
1725
- const [depositCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawAmount]);
1155
+ const rawValue = stableToRaw(amount, assetInfo.decimals).toString();
1726
1156
  if (options?.collectFee) {
1157
+ const allCoins = await this.fetchAllCoins(address, assetInfo.type);
1158
+ if (allCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins found`);
1159
+ const primaryCoinId = allCoins[0].coinObjectId;
1160
+ if (allCoins.length > 1) {
1161
+ tx.mergeCoins(tx.object(primaryCoinId), allCoins.slice(1).map((c) => tx.object(c.coinObjectId)));
1162
+ }
1163
+ const [depositCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawValue]);
1727
1164
  addCollectFeeToTx(tx, depositCoin, "save");
1728
1165
  }
1729
- const [ctokens] = tx.moveCall({
1730
- target: `${pkg}::lending_market::deposit_liquidity_and_mint_ctokens`,
1731
- typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1732
- arguments: [
1733
- tx.object(LENDING_MARKET_ID),
1734
- tx.pure.u64(reserve.arrayIndex),
1735
- tx.object(CLOCK2),
1736
- depositCoin
1737
- ]
1738
- });
1739
- tx.moveCall({
1740
- target: `${pkg}::lending_market::deposit_ctokens_into_obligation`,
1741
- typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1742
- arguments: [
1743
- tx.object(LENDING_MARKET_ID),
1744
- tx.pure.u64(reserve.arrayIndex),
1745
- typeof capRef === "string" ? tx.object(capRef) : capRef,
1746
- tx.object(CLOCK2),
1747
- ctokens
1748
- ]
1749
- });
1750
- if (typeof capRef !== "string") {
1751
- tx.transferObjects([capRef], address);
1166
+ if (caps.length > 0) {
1167
+ await sdk.depositIntoObligation(address, assetInfo.type, rawValue, tx, caps[0].id);
1168
+ } else {
1169
+ await sdk.depositIntoObligation(address, assetInfo.type, rawValue, tx, tx.object(caps[0]?.id ?? ""));
1752
1170
  }
1753
1171
  return { tx };
1754
1172
  }
1755
1173
  async buildWithdrawTx(address, amount, asset) {
1756
1174
  const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
1757
1175
  const assetInfo = SUPPORTED_ASSETS[assetKey];
1758
- const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves(true)]);
1759
- const reserve = this.findReserve(reserves, assetKey);
1760
- if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
1761
- const caps = await this.fetchObligationCaps(address);
1176
+ const sdk = await this.getSdkClient();
1177
+ const caps = await SuilendClient.getObligationOwnerCaps(address, [LENDING_MARKET_TYPE], this.client);
1762
1178
  if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found");
1763
- const obligation = await this.fetchObligation(caps[0].obligationId);
1764
- const dep = obligation.deposits.find((d) => d.reserveIdx === reserve.arrayIndex);
1765
- const ratio = cTokenRatio(reserve);
1766
- const deposited = dep ? dep.ctokenAmount * ratio / 10 ** reserve.mintDecimals : 0;
1179
+ const positions = await this.getPositions(address);
1180
+ const dep = positions.supplies.find((s) => s.asset === assetKey);
1181
+ const deposited = dep?.amount ?? 0;
1767
1182
  const effectiveAmount = Math.min(amount, deposited);
1768
1183
  if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on Suilend`);
1769
- const U64_MAX = "18446744073709551615";
1770
- const isFullWithdraw = dep && effectiveAmount >= deposited * 0.999;
1771
- const withdrawArg = isFullWithdraw ? U64_MAX : String(Math.floor(effectiveAmount * 10 ** reserve.mintDecimals / ratio));
1184
+ const rawValue = stableToRaw(effectiveAmount, assetInfo.decimals).toString();
1772
1185
  const tx = new Transaction();
1773
1186
  tx.setSender(address);
1774
- const [ctokens] = tx.moveCall({
1775
- target: `${pkg}::lending_market::withdraw_ctokens`,
1776
- typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1777
- arguments: [
1778
- tx.object(LENDING_MARKET_ID),
1779
- tx.pure.u64(reserve.arrayIndex),
1780
- tx.object(caps[0].id),
1781
- tx.object(CLOCK2),
1782
- tx.pure("u64", BigInt(withdrawArg))
1783
- ]
1784
- });
1785
- const coin = this.redeemCtokens(tx, pkg, reserve, assetInfo.type, assetKey, ctokens);
1786
- tx.transferObjects([coin], address);
1187
+ await sdk.withdrawAndSendToUser(address, caps[0].id, caps[0].obligationId, assetInfo.type, rawValue, tx);
1787
1188
  return { tx, effectiveAmount };
1788
1189
  }
1789
1190
  async addWithdrawToTx(tx, address, amount, asset) {
1790
1191
  const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
1791
1192
  const assetInfo = SUPPORTED_ASSETS[assetKey];
1792
- const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves(true)]);
1793
- const reserve = this.findReserve(reserves, assetKey);
1794
- if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
1795
- const caps = await this.fetchObligationCaps(address);
1193
+ const sdk = await this.getSdkClient();
1194
+ const caps = await SuilendClient.getObligationOwnerCaps(address, [LENDING_MARKET_TYPE], this.client);
1796
1195
  if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found");
1797
- const obligation = await this.fetchObligation(caps[0].obligationId);
1798
- const dep = obligation.deposits.find((d) => d.reserveIdx === reserve.arrayIndex);
1799
- const ratio = cTokenRatio(reserve);
1800
- const deposited = dep ? dep.ctokenAmount * ratio / 10 ** reserve.mintDecimals : 0;
1196
+ const positions = await this.getPositions(address);
1197
+ const dep = positions.supplies.find((s) => s.asset === assetKey);
1198
+ const deposited = dep?.amount ?? 0;
1801
1199
  const effectiveAmount = Math.min(amount, deposited);
1802
1200
  if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on Suilend`);
1803
- const ctokenAmount = dep && effectiveAmount >= deposited * 0.999 ? dep.ctokenAmount : Math.floor(effectiveAmount * 10 ** reserve.mintDecimals / ratio);
1804
- const [ctokens] = tx.moveCall({
1805
- target: `${pkg}::lending_market::withdraw_ctokens`,
1806
- typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1807
- arguments: [
1808
- tx.object(LENDING_MARKET_ID),
1809
- tx.pure.u64(reserve.arrayIndex),
1810
- tx.object(caps[0].id),
1811
- tx.object(CLOCK2),
1812
- tx.pure.u64(ctokenAmount)
1813
- ]
1814
- });
1815
- const coin = this.redeemCtokens(tx, pkg, reserve, assetInfo.type, assetKey, ctokens);
1201
+ const rawValue = stableToRaw(effectiveAmount, assetInfo.decimals).toString();
1202
+ const coin = await sdk.withdraw(caps[0].id, caps[0].obligationId, assetInfo.type, rawValue, tx);
1816
1203
  return { coin, effectiveAmount };
1817
1204
  }
1818
- /**
1819
- * 3-step cToken redemption matching the official Suilend SDK flow:
1820
- * 1. redeem_ctokens_and_withdraw_liquidity_request — creates a LiquidityRequest
1821
- * 2. unstake_sui_from_staker — (SUI only) unstakes from validators to replenish available_liquidity
1822
- * 3. fulfill_liquidity_request — splits underlying tokens from the reserve
1823
- */
1824
- redeemCtokens(tx, pkg, reserve, coinType, assetKey, ctokens) {
1825
- const exemptionType = `${SUILEND_PACKAGE}::lending_market::RateLimiterExemption<${LENDING_MARKET_TYPE}, ${coinType}>`;
1826
- const [none] = tx.moveCall({
1827
- target: "0x1::option::none",
1828
- typeArguments: [exemptionType]
1829
- });
1830
- const [liquidityRequest] = tx.moveCall({
1831
- target: `${pkg}::lending_market::redeem_ctokens_and_withdraw_liquidity_request`,
1832
- typeArguments: [LENDING_MARKET_TYPE, coinType],
1833
- arguments: [
1834
- tx.object(LENDING_MARKET_ID),
1835
- tx.pure.u64(reserve.arrayIndex),
1836
- tx.object(CLOCK2),
1837
- ctokens,
1838
- none
1839
- ]
1840
- });
1841
- if (assetKey === "SUI") {
1842
- tx.moveCall({
1843
- target: `${pkg}::lending_market::unstake_sui_from_staker`,
1844
- typeArguments: [LENDING_MARKET_TYPE],
1845
- arguments: [
1846
- tx.object(LENDING_MARKET_ID),
1847
- tx.pure.u64(reserve.arrayIndex),
1848
- liquidityRequest,
1849
- tx.object(SUI_SYSTEM_STATE2)
1850
- ]
1851
- });
1852
- }
1853
- const [coin] = tx.moveCall({
1854
- target: `${pkg}::lending_market::fulfill_liquidity_request`,
1855
- typeArguments: [LENDING_MARKET_TYPE, coinType],
1856
- arguments: [
1857
- tx.object(LENDING_MARKET_ID),
1858
- tx.pure.u64(reserve.arrayIndex),
1859
- liquidityRequest
1860
- ]
1861
- });
1862
- return coin;
1863
- }
1864
1205
  async addSaveToTx(tx, address, coin, asset, options) {
1865
1206
  const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
1866
1207
  const assetInfo = SUPPORTED_ASSETS[assetKey];
1867
- const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
1868
- const reserve = this.findReserve(reserves, assetKey);
1869
- if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
1870
- const caps = await this.fetchObligationCaps(address);
1208
+ const sdk = await this.getSdkClient();
1209
+ const caps = await SuilendClient.getObligationOwnerCaps(address, [LENDING_MARKET_TYPE], this.client);
1871
1210
  let capRef;
1872
1211
  if (caps.length === 0) {
1873
- const [newCap] = tx.moveCall({
1874
- target: `${pkg}::lending_market::create_obligation`,
1875
- typeArguments: [LENDING_MARKET_TYPE],
1876
- arguments: [tx.object(LENDING_MARKET_ID)]
1877
- });
1212
+ const newCap = sdk.createObligation(tx);
1878
1213
  capRef = newCap;
1214
+ tx.transferObjects([newCap], address);
1879
1215
  } else {
1880
1216
  capRef = caps[0].id;
1881
1217
  }
1882
1218
  if (options?.collectFee) {
1883
1219
  addCollectFeeToTx(tx, coin, "save");
1884
1220
  }
1885
- const [ctokens] = tx.moveCall({
1886
- target: `${pkg}::lending_market::deposit_liquidity_and_mint_ctokens`,
1887
- typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1888
- arguments: [
1889
- tx.object(LENDING_MARKET_ID),
1890
- tx.pure.u64(reserve.arrayIndex),
1891
- tx.object(CLOCK2),
1892
- coin
1893
- ]
1894
- });
1895
- tx.moveCall({
1896
- target: `${pkg}::lending_market::deposit_ctokens_into_obligation`,
1897
- typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1898
- arguments: [
1899
- tx.object(LENDING_MARKET_ID),
1900
- tx.pure.u64(reserve.arrayIndex),
1901
- typeof capRef === "string" ? tx.object(capRef) : capRef,
1902
- tx.object(CLOCK2),
1903
- ctokens
1904
- ]
1905
- });
1906
- if (typeof capRef !== "string") {
1907
- tx.transferObjects([capRef], address);
1908
- }
1221
+ sdk.deposit(coin, assetInfo.type, capRef, tx);
1909
1222
  }
1910
1223
  async buildBorrowTx(address, amount, asset, options) {
1911
1224
  const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
1912
1225
  const assetInfo = SUPPORTED_ASSETS[assetKey];
1913
- const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
1914
- const reserve = this.findReserve(reserves, assetKey);
1915
- if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend. Try: NAVI or a different asset.`);
1916
- const caps = await this.fetchObligationCaps(address);
1226
+ const sdk = await this.getSdkClient();
1227
+ const caps = await SuilendClient.getObligationOwnerCaps(address, [LENDING_MARKET_TYPE], this.client);
1917
1228
  if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found. Deposit collateral first with: t2000 save <amount>");
1918
- const rawAmount = stableToRaw(amount, assetInfo.decimals);
1229
+ const rawValue = stableToRaw(amount, assetInfo.decimals).toString();
1919
1230
  const tx = new Transaction();
1920
1231
  tx.setSender(address);
1921
- const [coin] = tx.moveCall({
1922
- target: `${pkg}::lending_market::borrow`,
1923
- typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1924
- arguments: [
1925
- tx.object(LENDING_MARKET_ID),
1926
- tx.pure.u64(reserve.arrayIndex),
1927
- tx.object(caps[0].id),
1928
- tx.object(CLOCK2),
1929
- tx.pure.u64(rawAmount)
1930
- ]
1931
- });
1932
1232
  if (options?.collectFee) {
1233
+ const coin = await sdk.borrow(caps[0].id, caps[0].obligationId, assetInfo.type, rawValue, tx);
1933
1234
  addCollectFeeToTx(tx, coin, "borrow");
1235
+ tx.transferObjects([coin], address);
1236
+ } else {
1237
+ await sdk.borrowAndSendToUser(address, caps[0].id, caps[0].obligationId, assetInfo.type, rawValue, tx);
1934
1238
  }
1935
- tx.transferObjects([coin], address);
1936
1239
  return { tx };
1937
1240
  }
1938
1241
  async buildRepayTx(address, amount, asset) {
1939
1242
  const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
1940
1243
  const assetInfo = SUPPORTED_ASSETS[assetKey];
1941
- const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
1942
- const reserve = this.findReserve(reserves, assetKey);
1943
- if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
1944
- const caps = await this.fetchObligationCaps(address);
1244
+ const sdk = await this.getSdkClient();
1245
+ const caps = await SuilendClient.getObligationOwnerCaps(address, [LENDING_MARKET_TYPE], this.client);
1945
1246
  if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend obligation found");
1946
- const allCoins = await this.fetchAllCoins(address, assetInfo.type);
1947
- if (allCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", `No ${assetInfo.displayName} coins to repay with`);
1948
- const rawAmount = stableToRaw(amount, assetInfo.decimals);
1247
+ const rawValue = stableToRaw(amount, assetInfo.decimals).toString();
1949
1248
  const tx = new Transaction();
1950
1249
  tx.setSender(address);
1951
- const primaryCoinId = allCoins[0].coinObjectId;
1952
- if (allCoins.length > 1) {
1953
- tx.mergeCoins(tx.object(primaryCoinId), allCoins.slice(1).map((c) => tx.object(c.coinObjectId)));
1954
- }
1955
- const [repayCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawAmount.toString()]);
1956
- tx.moveCall({
1957
- target: `${pkg}::lending_market::repay`,
1958
- typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1959
- arguments: [
1960
- tx.object(LENDING_MARKET_ID),
1961
- tx.pure.u64(reserve.arrayIndex),
1962
- tx.object(caps[0].id),
1963
- tx.object(CLOCK2),
1964
- repayCoin
1965
- ]
1966
- });
1250
+ await sdk.repayIntoObligation(address, caps[0].obligationId, assetInfo.type, rawValue, tx);
1967
1251
  return { tx };
1968
1252
  }
1969
1253
  async addRepayToTx(tx, address, coin, asset) {
1970
1254
  const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
1971
1255
  const assetInfo = SUPPORTED_ASSETS[assetKey];
1972
- const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
1973
- const reserve = this.findReserve(reserves, assetKey);
1974
- if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
1975
- const caps = await this.fetchObligationCaps(address);
1256
+ const sdk = await this.getSdkClient();
1257
+ const caps = await SuilendClient.getObligationOwnerCaps(address, [LENDING_MARKET_TYPE], this.client);
1976
1258
  if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend obligation found");
1977
- tx.moveCall({
1978
- target: `${pkg}::lending_market::repay`,
1979
- typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
1980
- arguments: [
1981
- tx.object(LENDING_MARKET_ID),
1982
- tx.pure.u64(reserve.arrayIndex),
1983
- tx.object(caps[0].id),
1984
- tx.object(CLOCK2),
1985
- coin
1986
- ]
1987
- });
1259
+ sdk.repay(caps[0].obligationId, assetInfo.type, coin, tx);
1988
1260
  }
1989
1261
  async maxWithdraw(address, _asset) {
1990
1262
  const health = await this.getHealth(address);
@@ -2000,8 +1272,7 @@ var SuilendAdapter = class {
2000
1272
  }
2001
1273
  async maxBorrow(address, _asset) {
2002
1274
  const health = await this.getHealth(address);
2003
- const maxAmount = health.maxBorrow;
2004
- return { maxAmount, healthFactorAfter: MIN_HEALTH_FACTOR2, currentHF: health.healthFactor };
1275
+ return { maxAmount: health.maxBorrow, healthFactorAfter: MIN_HEALTH_FACTOR2, currentHF: health.healthFactor };
2005
1276
  }
2006
1277
  async fetchAllCoins(owner, coinType) {
2007
1278
  const all = [];
@@ -2015,88 +1286,79 @@ var SuilendAdapter = class {
2015
1286
  }
2016
1287
  return all;
2017
1288
  }
2018
- // -- Claim Rewards --------------------------------------------------------
2019
- isClaimableReward(coinType) {
2020
- const ct = coinType.toLowerCase();
2021
- return ct.includes("spring_sui") || ct.includes("deep::deep") || ct.includes("cert::cert");
2022
- }
2023
1289
  async getPendingRewards(address) {
2024
- const caps = await this.fetchObligationCaps(address);
2025
- if (caps.length === 0) return [];
2026
- const [reserves, obligation] = await Promise.all([
2027
- this.loadReserves(true),
2028
- this.fetchObligation(caps[0].obligationId)
2029
- ]);
2030
- const rewards = [];
2031
- for (const dep of obligation.deposits) {
2032
- const reserve = reserves[dep.reserveIdx];
2033
- if (!reserve) continue;
2034
- for (const rw of reserve.depositPoolRewards) {
2035
- if (!this.isClaimableReward(rw.coinType)) continue;
2036
- const durationMs = rw.endTimeMs - rw.startTimeMs;
2037
- if (durationMs <= 0) continue;
2038
- const assetSymbol = this.resolveSymbol(dep.coinType);
2039
- if (!(assetSymbol in SUPPORTED_ASSETS)) continue;
2040
- rewards.push({
2041
- protocol: "suilend",
2042
- asset: assetSymbol,
2043
- coinType: rw.coinType,
2044
- symbol: rw.coinType.includes("spring_sui") ? "sSUI" : rw.coinType.includes("deep::") ? "DEEP" : rw.coinType.split("::").pop() ?? "UNKNOWN",
2045
- amount: 0,
2046
- estimatedValueUsd: 0
2047
- });
1290
+ try {
1291
+ const sdk = await this.getSdkClient();
1292
+ const { reserveMap, refreshedRawReserves } = await initializeSuilend(this.client, sdk);
1293
+ const { obligations, obligationOwnerCaps } = await initializeObligations(
1294
+ this.client,
1295
+ sdk,
1296
+ refreshedRawReserves,
1297
+ reserveMap,
1298
+ address
1299
+ );
1300
+ if (obligationOwnerCaps.length === 0 || obligations.length === 0) return [];
1301
+ const ob = obligations[0];
1302
+ const rewards = [];
1303
+ for (const dep of ob.deposits) {
1304
+ for (const rw of dep.reserve.depositsPoolRewardManager.poolRewards) {
1305
+ if (rw.endTimeMs <= Date.now()) continue;
1306
+ const symbol = rw.symbol || rw.coinType.split("::").pop() || "UNKNOWN";
1307
+ rewards.push({
1308
+ protocol: "suilend",
1309
+ asset: this.resolveSymbol(dep.coinType),
1310
+ coinType: rw.coinType,
1311
+ symbol,
1312
+ amount: 0,
1313
+ estimatedValueUsd: 0
1314
+ });
1315
+ }
2048
1316
  }
1317
+ return rewards;
1318
+ } catch {
1319
+ return [];
2049
1320
  }
2050
- return rewards;
2051
1321
  }
2052
1322
  async addClaimRewardsToTx(tx, address) {
2053
- const caps = await this.fetchObligationCaps(address);
2054
- if (caps.length === 0) return [];
2055
- const [pkg, reserves, obligation] = await Promise.all([
2056
- this.resolvePackage(),
2057
- this.loadReserves(true),
2058
- this.fetchObligation(caps[0].obligationId)
2059
- ]);
2060
- const claimsByToken = /* @__PURE__ */ new Map();
2061
- const claimed = [];
2062
- for (const dep of obligation.deposits) {
2063
- const reserve = reserves[dep.reserveIdx];
2064
- if (!reserve) continue;
2065
- for (const rw of reserve.depositPoolRewards) {
2066
- if (!this.isClaimableReward(rw.coinType)) continue;
2067
- const [coin] = tx.moveCall({
2068
- target: `${pkg}::lending_market::claim_rewards`,
2069
- typeArguments: [LENDING_MARKET_TYPE, rw.coinType],
2070
- arguments: [
2071
- tx.object(LENDING_MARKET_ID),
2072
- tx.object(caps[0].id),
2073
- tx.object(CLOCK2),
2074
- tx.pure.u64(reserve.arrayIndex),
2075
- tx.pure.u64(rw.rewardIndex),
2076
- tx.pure.bool(true)
2077
- ]
2078
- });
2079
- const existing = claimsByToken.get(rw.coinType) ?? [];
2080
- existing.push(coin);
2081
- claimsByToken.set(rw.coinType, existing);
2082
- }
2083
- }
2084
- for (const [coinType, coins] of claimsByToken) {
2085
- if (coins.length > 1) {
2086
- tx.mergeCoins(coins[0], coins.slice(1));
1323
+ try {
1324
+ const sdk = await this.getSdkClient();
1325
+ const caps = await SuilendClient.getObligationOwnerCaps(address, [LENDING_MARKET_TYPE], this.client);
1326
+ if (caps.length === 0) return [];
1327
+ const { reserveMap, refreshedRawReserves } = await initializeSuilend(this.client, sdk);
1328
+ const { obligations } = await initializeObligations(
1329
+ this.client,
1330
+ sdk,
1331
+ refreshedRawReserves,
1332
+ reserveMap,
1333
+ address
1334
+ );
1335
+ if (obligations.length === 0) return [];
1336
+ const ob = obligations[0];
1337
+ const claimRewards = [];
1338
+ for (const dep of ob.deposits) {
1339
+ for (const rw of dep.reserve.depositsPoolRewardManager.poolRewards) {
1340
+ if (rw.endTimeMs <= Date.now()) continue;
1341
+ claimRewards.push({
1342
+ reserveArrayIndex: dep.reserveArrayIndex,
1343
+ rewardIndex: BigInt(rw.rewardIndex),
1344
+ rewardCoinType: rw.coinType,
1345
+ side: Side.DEPOSIT
1346
+ });
1347
+ }
2087
1348
  }
2088
- tx.transferObjects([coins[0]], address);
2089
- const symbol = coinType.includes("spring_sui") ? "SPRING_SUI" : coinType.includes("deep::") ? "DEEP" : coinType.split("::").pop() ?? "UNKNOWN";
2090
- claimed.push({
1349
+ if (claimRewards.length === 0) return [];
1350
+ sdk.claimRewardsAndSendToUser(address, caps[0].id, claimRewards, tx);
1351
+ return claimRewards.map((r) => ({
2091
1352
  protocol: "suilend",
2092
1353
  asset: "",
2093
- coinType,
2094
- symbol,
1354
+ coinType: r.rewardCoinType,
1355
+ symbol: r.rewardCoinType.split("::").pop() ?? "UNKNOWN",
2095
1356
  amount: 0,
2096
1357
  estimatedValueUsd: 0
2097
- });
1358
+ }));
1359
+ } catch {
1360
+ return [];
2098
1361
  }
2099
- return claimed;
2100
1362
  }
2101
1363
  };
2102
1364
  var descriptor4 = {