@t2000/sdk 0.9.1 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var eventemitter3 = require('eventemitter3');
4
+ var transactions = require('@mysten/sui/transactions');
4
5
  var jsonRpc = require('@mysten/sui/jsonRpc');
5
6
  var utils = require('@mysten/sui/utils');
6
7
  var ed25519 = require('@mysten/sui/keypairs/ed25519');
@@ -9,7 +10,6 @@ var crypto = require('crypto');
9
10
  var promises = require('fs/promises');
10
11
  var path = require('path');
11
12
  var os = require('os');
12
- var transactions = require('@mysten/sui/transactions');
13
13
  var bcs = require('@mysten/sui/bcs');
14
14
  var aggregatorSdk = require('@cetusprotocol/aggregator-sdk');
15
15
 
@@ -128,7 +128,9 @@ function mapMoveAbortCode(code) {
128
128
  // NAVI Protocol abort codes
129
129
  1502: "Oracle price is stale \u2014 try again in a moment",
130
130
  1600: "Health factor too low \u2014 withdrawal would risk liquidation",
131
- 1605: "Asset borrowing is disabled or at capacity on this protocol"
131
+ 1605: "Asset borrowing is disabled or at capacity on this protocol",
132
+ // Cetus DEX abort codes
133
+ 46001: "Swap failed \u2014 the DEX pool rejected the trade (liquidity or routing issue). Try again."
132
134
  };
133
135
  return abortMessages[code] ?? `Move abort code: ${code}`;
134
136
  }
@@ -139,7 +141,12 @@ function parseMoveAbortMessage(msg) {
139
141
  const abortMatch = msg.match(/abort code:\s*(\d+)/i) ?? msg.match(/MoveAbort[^,]*,\s*(\d+)/);
140
142
  if (abortMatch) {
141
143
  const code = parseInt(abortMatch[1], 10);
142
- return mapMoveAbortCode(code);
144
+ const mapped = mapMoveAbortCode(code);
145
+ if (mapped.startsWith("Move abort code:")) {
146
+ const moduleMatch = msg.match(/in '([^']+)'/);
147
+ if (moduleMatch) return `${mapped} (in ${moduleMatch[1]})`;
148
+ }
149
+ return mapped;
143
150
  }
144
151
  return msg;
145
152
  }
@@ -746,6 +753,94 @@ async function buildWithdrawTx(client, address, amount, options = {}) {
746
753
  tx.transferObjects([coin], address);
747
754
  return { tx, effectiveAmount };
748
755
  }
756
+ async function addWithdrawToTx(tx, client, address, amount, options = {}) {
757
+ const asset = options.asset ?? "USDC";
758
+ const assetInfo = SUPPORTED_ASSETS[asset];
759
+ const [config, pool, pools, states] = await Promise.all([
760
+ getConfig(),
761
+ getPool(asset),
762
+ getPools(),
763
+ getUserState(client, address)
764
+ ]);
765
+ const assetState = states.find((s) => s.assetId === pool.id);
766
+ const deposited = assetState ? compoundBalance(assetState.supplyBalance, pool.currentSupplyIndex) : 0;
767
+ const effectiveAmount = Math.min(amount, Math.max(0, deposited - WITHDRAW_DUST_BUFFER));
768
+ if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on NAVI`);
769
+ const rawAmount = Number(stableToRaw(effectiveAmount, assetInfo.decimals));
770
+ addOracleUpdatesForPositions(tx, config, pools, states, pool);
771
+ const [balance] = tx.moveCall({
772
+ target: `${config.package}::incentive_v3::withdraw_v2`,
773
+ arguments: [
774
+ tx.object(CLOCK),
775
+ tx.object(config.oracle.priceOracle),
776
+ tx.object(config.storage),
777
+ tx.object(pool.contract.pool),
778
+ tx.pure.u8(pool.id),
779
+ tx.pure.u64(rawAmount),
780
+ tx.object(config.incentiveV2),
781
+ tx.object(config.incentiveV3),
782
+ tx.object(SUI_SYSTEM_STATE)
783
+ ],
784
+ typeArguments: [pool.suiCoinType]
785
+ });
786
+ const [coin] = tx.moveCall({
787
+ target: "0x2::coin::from_balance",
788
+ arguments: [balance],
789
+ typeArguments: [pool.suiCoinType]
790
+ });
791
+ return { coin, effectiveAmount };
792
+ }
793
+ async function addSaveToTx(tx, _client, _address, coin, options = {}) {
794
+ const asset = options.asset ?? "USDC";
795
+ const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
796
+ if (options.collectFee) {
797
+ addCollectFeeToTx(tx, coin, "save");
798
+ }
799
+ const [coinValue] = tx.moveCall({
800
+ target: "0x2::coin::value",
801
+ typeArguments: [pool.suiCoinType],
802
+ arguments: [coin]
803
+ });
804
+ tx.moveCall({
805
+ target: `${config.package}::incentive_v3::entry_deposit`,
806
+ arguments: [
807
+ tx.object(CLOCK),
808
+ tx.object(config.storage),
809
+ tx.object(pool.contract.pool),
810
+ tx.pure.u8(pool.id),
811
+ coin,
812
+ coinValue,
813
+ tx.object(config.incentiveV2),
814
+ tx.object(config.incentiveV3)
815
+ ],
816
+ typeArguments: [pool.suiCoinType]
817
+ });
818
+ }
819
+ async function addRepayToTx(tx, client, _address, coin, options = {}) {
820
+ const asset = options.asset ?? "USDC";
821
+ const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
822
+ addOracleUpdate(tx, config, pool);
823
+ const [coinValue] = tx.moveCall({
824
+ target: "0x2::coin::value",
825
+ typeArguments: [pool.suiCoinType],
826
+ arguments: [coin]
827
+ });
828
+ tx.moveCall({
829
+ target: `${config.package}::incentive_v3::entry_repay`,
830
+ arguments: [
831
+ tx.object(CLOCK),
832
+ tx.object(config.oracle.priceOracle),
833
+ tx.object(config.storage),
834
+ tx.object(pool.contract.pool),
835
+ tx.pure.u8(pool.id),
836
+ coin,
837
+ coinValue,
838
+ tx.object(config.incentiveV2),
839
+ tx.object(config.incentiveV3)
840
+ ],
841
+ typeArguments: [pool.suiCoinType]
842
+ });
843
+ }
749
844
  async function buildBorrowTx(client, address, amount, options = {}) {
750
845
  if (!amount || amount <= 0 || !Number.isFinite(amount)) {
751
846
  throw new T2000Error("INVALID_AMOUNT", "Borrow amount must be a positive number");
@@ -1393,6 +1488,18 @@ var NaviAdapter = class {
1393
1488
  async maxBorrow(address, _asset) {
1394
1489
  return maxBorrowAmount(this.client, address);
1395
1490
  }
1491
+ async addWithdrawToTx(tx, address, amount, asset) {
1492
+ const stableAsset = normalizeAsset(asset);
1493
+ return addWithdrawToTx(tx, this.client, address, amount, { asset: stableAsset });
1494
+ }
1495
+ async addSaveToTx(tx, address, coin, asset, options) {
1496
+ const stableAsset = normalizeAsset(asset);
1497
+ return addSaveToTx(tx, this.client, address, coin, { ...options, asset: stableAsset });
1498
+ }
1499
+ async addRepayToTx(tx, address, coin, asset) {
1500
+ const stableAsset = normalizeAsset(asset);
1501
+ return addRepayToTx(tx, this.client, address, coin, { asset: stableAsset });
1502
+ }
1396
1503
  };
1397
1504
  var DEFAULT_SLIPPAGE_BPS = 300;
1398
1505
  function createAggregatorClient(client, signer) {
@@ -1437,6 +1544,41 @@ async function buildSwapTx(params) {
1437
1544
  toDecimals: toInfo.decimals
1438
1545
  };
1439
1546
  }
1547
+ async function addSwapToTx(params) {
1548
+ const { tx, client, address, inputCoin, fromAsset, toAsset, amount, maxSlippageBps = DEFAULT_SLIPPAGE_BPS } = params;
1549
+ const fromInfo = SUPPORTED_ASSETS[fromAsset];
1550
+ const toInfo = SUPPORTED_ASSETS[toAsset];
1551
+ if (!fromInfo || !toInfo) {
1552
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `Swap pair ${fromAsset}/${toAsset} is not supported`);
1553
+ }
1554
+ const rawAmount = BigInt(Math.floor(amount * 10 ** fromInfo.decimals));
1555
+ const aggClient = createAggregatorClient(client, address);
1556
+ const result = await aggClient.findRouters({
1557
+ from: fromInfo.type,
1558
+ target: toInfo.type,
1559
+ amount: rawAmount,
1560
+ byAmountIn: true
1561
+ });
1562
+ if (!result || result.insufficientLiquidity) {
1563
+ throw new T2000Error(
1564
+ "ASSET_NOT_SUPPORTED",
1565
+ `No swap route found for ${fromAsset} \u2192 ${toAsset}`
1566
+ );
1567
+ }
1568
+ const slippage = maxSlippageBps / 1e4;
1569
+ const outputCoin = await aggClient.routerSwap({
1570
+ router: result,
1571
+ txb: tx,
1572
+ inputCoin,
1573
+ slippage
1574
+ });
1575
+ const estimatedOut = Number(result.amountOut.toString());
1576
+ return {
1577
+ outputCoin,
1578
+ estimatedOut,
1579
+ toDecimals: toInfo.decimals
1580
+ };
1581
+ }
1440
1582
  async function getPoolPrice(client) {
1441
1583
  try {
1442
1584
  const pool = await client.getObject({
@@ -1547,6 +1689,18 @@ var CetusAdapter = class {
1547
1689
  async getPoolPrice() {
1548
1690
  return getPoolPrice(this.client);
1549
1691
  }
1692
+ async addSwapToTx(tx, address, inputCoin, from, to, amount, maxSlippageBps) {
1693
+ return addSwapToTx({
1694
+ tx,
1695
+ client: this.client,
1696
+ address,
1697
+ inputCoin,
1698
+ fromAsset: from,
1699
+ toAsset: to,
1700
+ amount,
1701
+ maxSlippageBps
1702
+ });
1703
+ }
1550
1704
  };
1551
1705
  SUPPORTED_ASSETS.USDC.type;
1552
1706
  var WAD = 1e18;
@@ -1943,6 +2097,95 @@ var SuilendAdapter = class {
1943
2097
  tx.transferObjects([coin], address);
1944
2098
  return { tx, effectiveAmount };
1945
2099
  }
2100
+ async addWithdrawToTx(tx, address, amount, asset) {
2101
+ const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
2102
+ const assetInfo = SUPPORTED_ASSETS[assetKey];
2103
+ const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves(true)]);
2104
+ const reserve = this.findReserve(reserves, assetKey);
2105
+ if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
2106
+ const caps = await this.fetchObligationCaps(address);
2107
+ if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend position found");
2108
+ const positions = await this.getPositions(address);
2109
+ const deposited = positions.supplies.find((s) => s.asset === assetKey)?.amount ?? 0;
2110
+ const effectiveAmount = Math.min(amount, deposited);
2111
+ if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", `Nothing to withdraw for ${assetInfo.displayName} on Suilend`);
2112
+ const ratio = cTokenRatio(reserve);
2113
+ const ctokenAmount = Math.ceil(effectiveAmount * 10 ** reserve.mintDecimals / ratio);
2114
+ const [ctokens] = tx.moveCall({
2115
+ target: `${pkg}::lending_market::withdraw_ctokens`,
2116
+ typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
2117
+ arguments: [
2118
+ tx.object(LENDING_MARKET_ID),
2119
+ tx.pure.u64(reserve.arrayIndex),
2120
+ tx.object(caps[0].id),
2121
+ tx.object(CLOCK2),
2122
+ tx.pure.u64(ctokenAmount)
2123
+ ]
2124
+ });
2125
+ const exemptionType = `${SUILEND_PACKAGE}::lending_market::RateLimiterExemption<${LENDING_MARKET_TYPE}, ${assetInfo.type}>`;
2126
+ const [none] = tx.moveCall({
2127
+ target: "0x1::option::none",
2128
+ typeArguments: [exemptionType]
2129
+ });
2130
+ const [coin] = tx.moveCall({
2131
+ target: `${pkg}::lending_market::redeem_ctokens_and_withdraw_liquidity`,
2132
+ typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
2133
+ arguments: [
2134
+ tx.object(LENDING_MARKET_ID),
2135
+ tx.pure.u64(reserve.arrayIndex),
2136
+ tx.object(CLOCK2),
2137
+ ctokens,
2138
+ none
2139
+ ]
2140
+ });
2141
+ return { coin, effectiveAmount };
2142
+ }
2143
+ async addSaveToTx(tx, address, coin, asset, options) {
2144
+ const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
2145
+ const assetInfo = SUPPORTED_ASSETS[assetKey];
2146
+ const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
2147
+ const reserve = this.findReserve(reserves, assetKey);
2148
+ if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
2149
+ const caps = await this.fetchObligationCaps(address);
2150
+ let capRef;
2151
+ if (caps.length === 0) {
2152
+ const [newCap] = tx.moveCall({
2153
+ target: `${pkg}::lending_market::create_obligation`,
2154
+ typeArguments: [LENDING_MARKET_TYPE],
2155
+ arguments: [tx.object(LENDING_MARKET_ID)]
2156
+ });
2157
+ capRef = newCap;
2158
+ } else {
2159
+ capRef = caps[0].id;
2160
+ }
2161
+ if (options?.collectFee) {
2162
+ addCollectFeeToTx(tx, coin, "save");
2163
+ }
2164
+ const [ctokens] = tx.moveCall({
2165
+ target: `${pkg}::lending_market::deposit_liquidity_and_mint_ctokens`,
2166
+ typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
2167
+ arguments: [
2168
+ tx.object(LENDING_MARKET_ID),
2169
+ tx.pure.u64(reserve.arrayIndex),
2170
+ tx.object(CLOCK2),
2171
+ coin
2172
+ ]
2173
+ });
2174
+ tx.moveCall({
2175
+ target: `${pkg}::lending_market::deposit_ctokens_into_obligation`,
2176
+ typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
2177
+ arguments: [
2178
+ tx.object(LENDING_MARKET_ID),
2179
+ tx.pure.u64(reserve.arrayIndex),
2180
+ typeof capRef === "string" ? tx.object(capRef) : capRef,
2181
+ tx.object(CLOCK2),
2182
+ ctokens
2183
+ ]
2184
+ });
2185
+ if (typeof capRef !== "string") {
2186
+ tx.transferObjects([capRef], address);
2187
+ }
2188
+ }
1946
2189
  async buildBorrowTx(address, amount, asset, options) {
1947
2190
  const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
1948
2191
  const assetInfo = SUPPORTED_ASSETS[assetKey];
@@ -2002,6 +2245,26 @@ var SuilendAdapter = class {
2002
2245
  });
2003
2246
  return { tx };
2004
2247
  }
2248
+ async addRepayToTx(tx, address, coin, asset) {
2249
+ const assetKey = asset in SUPPORTED_ASSETS ? asset : "USDC";
2250
+ const assetInfo = SUPPORTED_ASSETS[assetKey];
2251
+ const [pkg, reserves] = await Promise.all([this.resolvePackage(), this.loadReserves()]);
2252
+ const reserve = this.findReserve(reserves, assetKey);
2253
+ if (!reserve) throw new T2000Error("ASSET_NOT_SUPPORTED", `${assetInfo.displayName} reserve not found on Suilend`);
2254
+ const caps = await this.fetchObligationCaps(address);
2255
+ if (caps.length === 0) throw new T2000Error("NO_COLLATERAL", "No Suilend obligation found");
2256
+ tx.moveCall({
2257
+ target: `${pkg}::lending_market::repay`,
2258
+ typeArguments: [LENDING_MARKET_TYPE, assetInfo.type],
2259
+ arguments: [
2260
+ tx.object(LENDING_MARKET_ID),
2261
+ tx.pure.u64(reserve.arrayIndex),
2262
+ tx.object(caps[0].id),
2263
+ tx.object(CLOCK2),
2264
+ coin
2265
+ ]
2266
+ });
2267
+ }
2005
2268
  async maxWithdraw(address, _asset) {
2006
2269
  const health = await this.getHealth(address);
2007
2270
  let maxAmount;
@@ -2418,31 +2681,73 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
2418
2681
  const asset = "USDC";
2419
2682
  const bal = await queryBalance(this.client, this._address);
2420
2683
  const usdcBalance = bal.stables.USDC ?? 0;
2684
+ const needsAutoConvert = params.amount === "all" ? Object.entries(bal.stables).some(([k, v]) => k !== "USDC" && v > 0.01) : typeof params.amount === "number" && params.amount > usdcBalance;
2421
2685
  let amount;
2422
2686
  if (params.amount === "all") {
2423
- await this._convertWalletStablesToUsdc(bal);
2424
- const refreshedBal = await queryBalance(this.client, this._address);
2425
- amount = (refreshedBal.stables.USDC ?? 0) - 1;
2687
+ amount = (bal.available ?? 0) - 1;
2426
2688
  if (amount <= 0) {
2427
2689
  throw new T2000Error("INSUFFICIENT_BALANCE", "Balance too low to save after $1 gas reserve", {
2428
2690
  reason: "gas_reserve_required",
2429
- available: refreshedBal.stables.USDC ?? 0
2691
+ available: bal.available ?? 0
2430
2692
  });
2431
2693
  }
2432
2694
  } else {
2433
2695
  amount = params.amount;
2434
- if (amount > usdcBalance) {
2435
- const totalStables = bal.available;
2436
- if (amount > totalStables) {
2437
- throw new T2000Error("INSUFFICIENT_BALANCE", `Insufficient balance. Available: $${totalStables.toFixed(2)}, requested: $${amount.toFixed(2)}`);
2438
- }
2439
- await this._convertWalletStablesToUsdc(bal, amount - usdcBalance);
2696
+ if (amount > (bal.available ?? 0)) {
2697
+ throw new T2000Error("INSUFFICIENT_BALANCE", `Insufficient balance. Available: $${(bal.available ?? 0).toFixed(2)}, requested: $${amount.toFixed(2)}`);
2440
2698
  }
2441
2699
  }
2442
2700
  const fee = calculateFee("save", amount);
2443
2701
  const saveAmount = amount;
2444
2702
  const adapter = await this.resolveLending(params.protocol, asset, "save");
2703
+ const swapAdapter = this.registry.listSwap()[0];
2704
+ const canPTB = adapter.addSaveToTx && (!needsAutoConvert || swapAdapter?.addSwapToTx);
2445
2705
  const gasResult = await executeWithGas(this.client, this.keypair, async () => {
2706
+ if (canPTB && needsAutoConvert) {
2707
+ const tx2 = new transactions.Transaction();
2708
+ tx2.setSender(this._address);
2709
+ const usdcCoins = [];
2710
+ for (const [stableAsset, stableAmount] of Object.entries(bal.stables)) {
2711
+ if (stableAsset === "USDC" || stableAmount <= 0.01) continue;
2712
+ const assetInfo = SUPPORTED_ASSETS[stableAsset];
2713
+ if (!assetInfo) continue;
2714
+ const coins = await this._fetchCoins(assetInfo.type);
2715
+ if (coins.length === 0) continue;
2716
+ const merged = this._mergeCoinsInTx(tx2, coins);
2717
+ const { outputCoin } = await swapAdapter.addSwapToTx(
2718
+ tx2,
2719
+ this._address,
2720
+ merged,
2721
+ stableAsset,
2722
+ "USDC",
2723
+ stableAmount
2724
+ );
2725
+ usdcCoins.push(outputCoin);
2726
+ }
2727
+ const existingUsdc = await this._fetchCoins(SUPPORTED_ASSETS.USDC.type);
2728
+ if (existingUsdc.length > 0) {
2729
+ usdcCoins.push(this._mergeCoinsInTx(tx2, existingUsdc));
2730
+ }
2731
+ if (usdcCoins.length > 1) {
2732
+ tx2.mergeCoins(usdcCoins[0], usdcCoins.slice(1));
2733
+ }
2734
+ await adapter.addSaveToTx(tx2, this._address, usdcCoins[0], asset, { collectFee: true });
2735
+ return tx2;
2736
+ }
2737
+ if (canPTB && !needsAutoConvert) {
2738
+ const tx2 = new transactions.Transaction();
2739
+ tx2.setSender(this._address);
2740
+ const existingUsdc = await this._fetchCoins(SUPPORTED_ASSETS.USDC.type);
2741
+ if (existingUsdc.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins found");
2742
+ const merged = this._mergeCoinsInTx(tx2, existingUsdc);
2743
+ const rawAmount = BigInt(Math.floor(saveAmount * 10 ** SUPPORTED_ASSETS.USDC.decimals));
2744
+ const [depositCoin] = tx2.splitCoins(merged, [rawAmount]);
2745
+ await adapter.addSaveToTx(tx2, this._address, depositCoin, asset, { collectFee: true });
2746
+ return tx2;
2747
+ }
2748
+ if (needsAutoConvert) {
2749
+ await this._convertWalletStablesToUsdc(bal, params.amount === "all" ? void 0 : amount - usdcBalance);
2750
+ }
2446
2751
  const { tx } = await adapter.buildSaveTx(this._address, saveAmount, asset, { collectFee: true });
2447
2752
  return tx;
2448
2753
  });
@@ -2511,35 +2816,41 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
2511
2816
  }
2512
2817
  }
2513
2818
  const withdrawAmount = amount;
2514
- let effectiveAmount = withdrawAmount;
2819
+ let finalAmount = withdrawAmount;
2820
+ const swapAdapter = target.asset !== "USDC" ? this.registry.listSwap()[0] : void 0;
2821
+ const canPTB = adapter.addWithdrawToTx && (!swapAdapter || swapAdapter.addSwapToTx);
2515
2822
  const gasResult = await executeWithGas(this.client, this.keypair, async () => {
2823
+ if (canPTB) {
2824
+ const tx = new transactions.Transaction();
2825
+ tx.setSender(this._address);
2826
+ const { coin, effectiveAmount } = await adapter.addWithdrawToTx(tx, this._address, withdrawAmount, target.asset);
2827
+ finalAmount = effectiveAmount;
2828
+ if (target.asset !== "USDC" && swapAdapter?.addSwapToTx) {
2829
+ const { outputCoin, estimatedOut, toDecimals } = await swapAdapter.addSwapToTx(
2830
+ tx,
2831
+ this._address,
2832
+ coin,
2833
+ target.asset,
2834
+ "USDC",
2835
+ effectiveAmount
2836
+ );
2837
+ finalAmount = estimatedOut / 10 ** toDecimals;
2838
+ tx.transferObjects([outputCoin], this._address);
2839
+ } else {
2840
+ tx.transferObjects([coin], this._address);
2841
+ }
2842
+ return tx;
2843
+ }
2516
2844
  const built = await adapter.buildWithdrawTx(this._address, withdrawAmount, target.asset);
2517
- effectiveAmount = built.effectiveAmount;
2845
+ finalAmount = built.effectiveAmount;
2518
2846
  return built.tx;
2519
2847
  });
2520
- let totalGasCost = gasResult.gasCostSui;
2521
- let finalAmount = effectiveAmount;
2522
- let lastDigest = gasResult.digest;
2523
- if (target.asset !== "USDC") {
2524
- try {
2525
- const swapResult = await this._swapToUsdc(target.asset, effectiveAmount);
2526
- finalAmount = swapResult.usdcReceived;
2527
- lastDigest = swapResult.digest;
2528
- totalGasCost += swapResult.gasCost;
2529
- } catch (err) {
2530
- throw new T2000Error(
2531
- "SWAP_FAILED",
2532
- `Withdrew $${effectiveAmount.toFixed(2)} ${target.asset} but swap to USDC failed. Your ${target.asset} is safe in your wallet.`,
2533
- { withdrawDigest: gasResult.digest, originalError: err instanceof Error ? err.message : String(err) }
2534
- );
2535
- }
2536
- }
2537
- this.emitBalanceChange("USDC", finalAmount, "withdraw", lastDigest);
2848
+ this.emitBalanceChange("USDC", finalAmount, "withdraw", gasResult.digest);
2538
2849
  return {
2539
2850
  success: true,
2540
- tx: lastDigest,
2851
+ tx: gasResult.digest,
2541
2852
  amount: finalAmount,
2542
- gasCost: totalGasCost,
2853
+ gasCost: gasResult.gasCostSui,
2543
2854
  gasMethod: gasResult.gasMethod
2544
2855
  };
2545
2856
  }
@@ -2556,49 +2867,94 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
2556
2867
  if (withdrawable.length === 0) {
2557
2868
  throw new T2000Error("NO_COLLATERAL", "No savings to withdraw across any protocol");
2558
2869
  }
2559
- let totalUsdcReceived = 0;
2560
- let lastDigest = "";
2561
- let totalGasCost = 0;
2562
- let lastGasMethod = "self-funded";
2870
+ const entries = [];
2563
2871
  for (const entry of withdrawable) {
2564
2872
  const adapter = this.registry.getLending(entry.protocolId);
2565
2873
  if (!adapter) continue;
2566
2874
  const maxResult = await adapter.maxWithdraw(this._address, entry.asset);
2567
- if (maxResult.maxAmount <= 1e-3) continue;
2568
- let effectiveAmount = maxResult.maxAmount;
2569
- const gasResult = await executeWithGas(this.client, this.keypair, async () => {
2570
- const built = await adapter.buildWithdrawTx(this._address, maxResult.maxAmount, entry.asset);
2571
- effectiveAmount = built.effectiveAmount;
2572
- return built.tx;
2573
- });
2574
- lastDigest = gasResult.digest;
2575
- totalGasCost += gasResult.gasCostSui;
2576
- lastGasMethod = gasResult.gasMethod;
2577
- this.emitBalanceChange(entry.asset, effectiveAmount, "withdraw", gasResult.digest);
2578
- if (entry.asset !== "USDC") {
2579
- try {
2580
- const swapResult = await this._swapToUsdc(entry.asset, effectiveAmount);
2581
- totalUsdcReceived += swapResult.usdcReceived;
2582
- lastDigest = swapResult.digest;
2583
- totalGasCost += swapResult.gasCost;
2584
- } catch {
2585
- totalUsdcReceived += effectiveAmount;
2586
- }
2587
- } else {
2588
- totalUsdcReceived += effectiveAmount;
2875
+ if (maxResult.maxAmount > 1e-3) {
2876
+ entries.push({ ...entry, maxAmount: maxResult.maxAmount, adapter });
2589
2877
  }
2590
2878
  }
2879
+ if (entries.length === 0) {
2880
+ throw new T2000Error("NO_COLLATERAL", "No savings to withdraw across any protocol");
2881
+ }
2882
+ const swapAdapter = this.registry.listSwap()[0];
2883
+ const canPTB = entries.every((e) => e.adapter.addWithdrawToTx) && (!swapAdapter || swapAdapter.addSwapToTx);
2884
+ let totalUsdcReceived = 0;
2885
+ const gasResult = await executeWithGas(this.client, this.keypair, async () => {
2886
+ if (canPTB) {
2887
+ const tx = new transactions.Transaction();
2888
+ tx.setSender(this._address);
2889
+ const usdcCoins = [];
2890
+ for (const entry of entries) {
2891
+ const { coin, effectiveAmount } = await entry.adapter.addWithdrawToTx(
2892
+ tx,
2893
+ this._address,
2894
+ entry.maxAmount,
2895
+ entry.asset
2896
+ );
2897
+ if (entry.asset !== "USDC" && swapAdapter?.addSwapToTx) {
2898
+ const { outputCoin, estimatedOut, toDecimals } = await swapAdapter.addSwapToTx(
2899
+ tx,
2900
+ this._address,
2901
+ coin,
2902
+ entry.asset,
2903
+ "USDC",
2904
+ effectiveAmount
2905
+ );
2906
+ totalUsdcReceived += estimatedOut / 10 ** toDecimals;
2907
+ usdcCoins.push(outputCoin);
2908
+ } else {
2909
+ totalUsdcReceived += effectiveAmount;
2910
+ usdcCoins.push(coin);
2911
+ }
2912
+ }
2913
+ if (usdcCoins.length > 1) {
2914
+ tx.mergeCoins(usdcCoins[0], usdcCoins.slice(1));
2915
+ }
2916
+ tx.transferObjects([usdcCoins[0]], this._address);
2917
+ return tx;
2918
+ }
2919
+ let lastTx;
2920
+ for (const entry of entries) {
2921
+ const built = await entry.adapter.buildWithdrawTx(this._address, entry.maxAmount, entry.asset);
2922
+ totalUsdcReceived += built.effectiveAmount;
2923
+ lastTx = built.tx;
2924
+ }
2925
+ return lastTx;
2926
+ });
2591
2927
  if (totalUsdcReceived <= 0) {
2592
2928
  throw new T2000Error("NO_COLLATERAL", "No savings to withdraw across any protocol");
2593
2929
  }
2594
2930
  return {
2595
2931
  success: true,
2596
- tx: lastDigest,
2932
+ tx: gasResult.digest,
2597
2933
  amount: totalUsdcReceived,
2598
- gasCost: totalGasCost,
2599
- gasMethod: lastGasMethod
2934
+ gasCost: gasResult.gasCostSui,
2935
+ gasMethod: gasResult.gasMethod
2600
2936
  };
2601
2937
  }
2938
+ async _fetchCoins(coinType) {
2939
+ const all = [];
2940
+ let cursor;
2941
+ let hasNext = true;
2942
+ while (hasNext) {
2943
+ const page = await this.client.getCoins({ owner: this._address, coinType, cursor: cursor ?? void 0 });
2944
+ all.push(...page.data.map((c) => ({ coinObjectId: c.coinObjectId, balance: c.balance })));
2945
+ cursor = page.nextCursor;
2946
+ hasNext = page.hasNextPage;
2947
+ }
2948
+ return all;
2949
+ }
2950
+ _mergeCoinsInTx(tx, coins) {
2951
+ if (coins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No coins to merge");
2952
+ const primary = tx.object(coins[0].coinObjectId);
2953
+ if (coins.length > 1) {
2954
+ tx.mergeCoins(primary, coins.slice(1).map((c) => tx.object(c.coinObjectId)));
2955
+ }
2956
+ return primary;
2957
+ }
2602
2958
  async _swapToUsdc(asset, amount) {
2603
2959
  const swapAdapter = this.registry.listSwap()[0];
2604
2960
  if (!swapAdapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", "No swap adapter available");
@@ -2703,72 +3059,122 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
2703
3059
  const adapter = this.registry.getLending(target.protocolId);
2704
3060
  if (!adapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", `Protocol ${target.protocolId} not found`);
2705
3061
  const repayAmount = Math.min(params.amount, target.amount);
2706
- let totalGasCost = 0;
2707
- let lastDigest = "";
2708
- if (target.asset !== "USDC") {
2709
- const buffer = repayAmount * 1.005;
2710
- const swapResult = await this._swapFromUsdc(target.asset, buffer);
2711
- totalGasCost += swapResult.gasCost;
2712
- lastDigest = swapResult.digest;
2713
- }
3062
+ const swapAdapter = target.asset !== "USDC" ? this.registry.listSwap()[0] : void 0;
3063
+ const canPTB = adapter.addRepayToTx && (!swapAdapter || swapAdapter.addSwapToTx);
2714
3064
  const gasResult = await executeWithGas(this.client, this.keypair, async () => {
3065
+ if (canPTB && target.asset !== "USDC" && swapAdapter?.addSwapToTx) {
3066
+ const tx2 = new transactions.Transaction();
3067
+ tx2.setSender(this._address);
3068
+ const buffer = repayAmount * 1.005;
3069
+ const usdcCoins = await this._fetchCoins(SUPPORTED_ASSETS.USDC.type);
3070
+ if (usdcCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins for swap");
3071
+ const merged = this._mergeCoinsInTx(tx2, usdcCoins);
3072
+ const rawSwap = BigInt(Math.floor(buffer * 10 ** SUPPORTED_ASSETS.USDC.decimals));
3073
+ const [splitCoin] = tx2.splitCoins(merged, [rawSwap]);
3074
+ const { outputCoin } = await swapAdapter.addSwapToTx(
3075
+ tx2,
3076
+ this._address,
3077
+ splitCoin,
3078
+ "USDC",
3079
+ target.asset,
3080
+ buffer
3081
+ );
3082
+ await adapter.addRepayToTx(tx2, this._address, outputCoin, target.asset);
3083
+ return tx2;
3084
+ }
3085
+ if (canPTB && target.asset === "USDC") {
3086
+ const tx2 = new transactions.Transaction();
3087
+ tx2.setSender(this._address);
3088
+ const usdcCoins = await this._fetchCoins(SUPPORTED_ASSETS.USDC.type);
3089
+ if (usdcCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins");
3090
+ const merged = this._mergeCoinsInTx(tx2, usdcCoins);
3091
+ const raw = BigInt(Math.floor(repayAmount * 10 ** SUPPORTED_ASSETS.USDC.decimals));
3092
+ const [repayCoin] = tx2.splitCoins(merged, [raw]);
3093
+ await adapter.addRepayToTx(tx2, this._address, repayCoin, target.asset);
3094
+ return tx2;
3095
+ }
3096
+ if (target.asset !== "USDC") {
3097
+ await this._swapFromUsdc(target.asset, repayAmount * 1.005);
3098
+ }
2715
3099
  const { tx } = await adapter.buildRepayTx(this._address, repayAmount, target.asset);
2716
3100
  return tx;
2717
3101
  });
2718
- totalGasCost += gasResult.gasCostSui;
2719
- lastDigest = gasResult.digest;
2720
3102
  const hf = await adapter.getHealth(this._address);
2721
- this.emitBalanceChange("USDC", repayAmount, "repay", lastDigest);
3103
+ this.emitBalanceChange("USDC", repayAmount, "repay", gasResult.digest);
2722
3104
  return {
2723
3105
  success: true,
2724
- tx: lastDigest,
3106
+ tx: gasResult.digest,
2725
3107
  amount: repayAmount,
2726
3108
  remainingDebt: hf.borrowed,
2727
- gasCost: totalGasCost,
3109
+ gasCost: gasResult.gasCostSui,
2728
3110
  gasMethod: gasResult.gasMethod
2729
3111
  };
2730
3112
  }
2731
3113
  async _repayAllBorrows(borrows) {
2732
- let totalRepaid = 0;
2733
- let totalGasCost = 0;
2734
- let lastDigest = "";
2735
- let lastGasMethod = "self-funded";
2736
3114
  borrows.sort((a, b) => b.apy - a.apy);
3115
+ const entries = [];
2737
3116
  for (const borrow of borrows) {
2738
3117
  const adapter = this.registry.getLending(borrow.protocolId);
2739
- if (!adapter) continue;
2740
- if (borrow.asset !== "USDC") {
2741
- try {
2742
- const buffer = borrow.amount * 1.005;
2743
- const swapResult = await this._swapFromUsdc(borrow.asset, buffer);
2744
- totalGasCost += swapResult.gasCost;
2745
- } catch (err) {
2746
- throw new T2000Error(
2747
- "SWAP_FAILED",
2748
- `Could not convert USDC to ${borrow.asset} for repayment. Try again later.`,
2749
- { originalError: err instanceof Error ? err.message : String(err) }
2750
- );
3118
+ if (adapter) entries.push({ borrow, adapter });
3119
+ }
3120
+ const swapAdapter = this.registry.listSwap()[0];
3121
+ const canPTB = entries.every((e) => e.adapter.addRepayToTx) && (entries.every((e) => e.borrow.asset === "USDC") || swapAdapter?.addSwapToTx);
3122
+ let totalRepaid = 0;
3123
+ const gasResult = await executeWithGas(this.client, this.keypair, async () => {
3124
+ if (canPTB) {
3125
+ const tx = new transactions.Transaction();
3126
+ tx.setSender(this._address);
3127
+ const usdcCoins = await this._fetchCoins(SUPPORTED_ASSETS.USDC.type);
3128
+ let usdcMerged;
3129
+ if (usdcCoins.length > 0) {
3130
+ usdcMerged = this._mergeCoinsInTx(tx, usdcCoins);
3131
+ }
3132
+ for (const { borrow, adapter } of entries) {
3133
+ if (borrow.asset !== "USDC" && swapAdapter?.addSwapToTx) {
3134
+ const buffer = borrow.amount * 1.005;
3135
+ const rawSwap = BigInt(Math.floor(buffer * 10 ** SUPPORTED_ASSETS.USDC.decimals));
3136
+ if (!usdcMerged) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC for swap");
3137
+ const [splitCoin] = tx.splitCoins(usdcMerged, [rawSwap]);
3138
+ const { outputCoin } = await swapAdapter.addSwapToTx(
3139
+ tx,
3140
+ this._address,
3141
+ splitCoin,
3142
+ "USDC",
3143
+ borrow.asset,
3144
+ buffer
3145
+ );
3146
+ await adapter.addRepayToTx(tx, this._address, outputCoin, borrow.asset);
3147
+ } else {
3148
+ const raw = BigInt(Math.floor(borrow.amount * 10 ** SUPPORTED_ASSETS.USDC.decimals));
3149
+ if (!usdcMerged) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC for repayment");
3150
+ const [repayCoin] = tx.splitCoins(usdcMerged, [raw]);
3151
+ await adapter.addRepayToTx(tx, this._address, repayCoin, borrow.asset);
3152
+ }
3153
+ totalRepaid += borrow.amount;
2751
3154
  }
3155
+ return tx;
2752
3156
  }
2753
- const gasResult = await executeWithGas(this.client, this.keypair, async () => {
3157
+ let lastTx;
3158
+ for (const { borrow, adapter } of entries) {
3159
+ if (borrow.asset !== "USDC") {
3160
+ await this._swapFromUsdc(borrow.asset, borrow.amount * 1.005);
3161
+ }
2754
3162
  const { tx } = await adapter.buildRepayTx(this._address, borrow.amount, borrow.asset);
2755
- return tx;
2756
- });
2757
- totalRepaid += borrow.amount;
2758
- totalGasCost += gasResult.gasCostSui;
2759
- lastDigest = gasResult.digest;
2760
- lastGasMethod = gasResult.gasMethod;
2761
- }
2762
- const firstAdapter = this.registry.getLending(borrows[0].protocolId);
3163
+ lastTx = tx;
3164
+ totalRepaid += borrow.amount;
3165
+ }
3166
+ return lastTx;
3167
+ });
3168
+ const firstAdapter = entries[0]?.adapter;
2763
3169
  const hf = firstAdapter ? await firstAdapter.getHealth(this._address) : { borrowed: 0 };
2764
- this.emitBalanceChange("USDC", totalRepaid, "repay", lastDigest);
3170
+ this.emitBalanceChange("USDC", totalRepaid, "repay", gasResult.digest);
2765
3171
  return {
2766
3172
  success: true,
2767
- tx: lastDigest,
3173
+ tx: gasResult.digest,
2768
3174
  amount: totalRepaid,
2769
3175
  remainingDebt: hf.borrowed,
2770
- gasCost: totalGasCost,
2771
- gasMethod: lastGasMethod
3176
+ gasCost: gasResult.gasCostSui,
3177
+ gasMethod: gasResult.gasMethod
2772
3178
  };
2773
3179
  }
2774
3180
  async maxBorrow() {
@@ -2851,14 +3257,14 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
2851
3257
  const allPositions = await this.registry.allPositions(this._address);
2852
3258
  const positions = allPositions.flatMap(
2853
3259
  (p) => [
2854
- ...p.positions.supplies.map((s) => ({
3260
+ ...p.positions.supplies.filter((s) => s.amount > 5e-3).map((s) => ({
2855
3261
  protocol: p.protocolId,
2856
3262
  asset: s.asset,
2857
3263
  type: "save",
2858
3264
  amount: s.amount,
2859
3265
  apy: s.apy
2860
3266
  })),
2861
- ...p.positions.borrows.map((b) => ({
3267
+ ...p.positions.borrows.filter((b) => b.amount > 5e-3).map((b) => ({
2862
3268
  protocol: p.protocolId,
2863
3269
  asset: b.asset,
2864
3270
  type: "borrow",
@@ -3058,35 +3464,76 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
3058
3464
  totalGasCost: 0
3059
3465
  };
3060
3466
  }
3061
- const txDigests = [];
3062
- let totalGasCost = 0;
3063
3467
  if (!withdrawAdapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", `Protocol ${current.protocolId} not found`);
3064
- const withdrawResult = await executeWithGas(this.client, this.keypair, async () => {
3065
- const built = await withdrawAdapter.buildWithdrawTx(this._address, current.amount, current.asset);
3066
- amountToDeposit = isSameAsset ? built.effectiveAmount : built.effectiveAmount;
3067
- return built.tx;
3068
- });
3069
- txDigests.push(withdrawResult.digest);
3070
- totalGasCost += withdrawResult.gasCostSui;
3071
- if (!isSameAsset) {
3072
- const swapAdapter = this.registry.listSwap()[0];
3073
- if (!swapAdapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", "No swap adapter available");
3074
- const swapResult = await executeWithGas(this.client, this.keypair, async () => {
3075
- const built = await swapAdapter.buildSwapTx(this._address, current.asset, bestRate.asset, amountToDeposit);
3076
- amountToDeposit = built.estimatedOut / 10 ** built.toDecimals;
3468
+ const depositAdapter = this.registry.getLending(bestRate.protocolId);
3469
+ if (!depositAdapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", `Protocol ${bestRate.protocolId} not found`);
3470
+ const canComposePTB = withdrawAdapter.addWithdrawToTx && depositAdapter.addSaveToTx && (isSameAsset || this.registry.listSwap()[0]?.addSwapToTx);
3471
+ let txDigests;
3472
+ let totalGasCost;
3473
+ if (canComposePTB) {
3474
+ const result = await executeWithGas(this.client, this.keypair, async () => {
3475
+ const tx = new transactions.Transaction();
3476
+ tx.setSender(this._address);
3477
+ const { coin: withdrawnCoin, effectiveAmount } = await withdrawAdapter.addWithdrawToTx(
3478
+ tx,
3479
+ this._address,
3480
+ current.amount,
3481
+ current.asset
3482
+ );
3483
+ amountToDeposit = effectiveAmount;
3484
+ let depositCoin = withdrawnCoin;
3485
+ if (!isSameAsset) {
3486
+ const swapAdapter = this.registry.listSwap()[0];
3487
+ const { outputCoin, estimatedOut, toDecimals } = await swapAdapter.addSwapToTx(
3488
+ tx,
3489
+ this._address,
3490
+ withdrawnCoin,
3491
+ current.asset,
3492
+ bestRate.asset,
3493
+ amountToDeposit
3494
+ );
3495
+ depositCoin = outputCoin;
3496
+ amountToDeposit = estimatedOut / 10 ** toDecimals;
3497
+ }
3498
+ await depositAdapter.addSaveToTx(
3499
+ tx,
3500
+ this._address,
3501
+ depositCoin,
3502
+ bestRate.asset,
3503
+ { collectFee: bestRate.asset === "USDC" }
3504
+ );
3505
+ return tx;
3506
+ });
3507
+ txDigests = [result.digest];
3508
+ totalGasCost = result.gasCostSui;
3509
+ } else {
3510
+ txDigests = [];
3511
+ totalGasCost = 0;
3512
+ const withdrawResult = await executeWithGas(this.client, this.keypair, async () => {
3513
+ const built = await withdrawAdapter.buildWithdrawTx(this._address, current.amount, current.asset);
3514
+ amountToDeposit = built.effectiveAmount;
3077
3515
  return built.tx;
3078
3516
  });
3079
- txDigests.push(swapResult.digest);
3080
- totalGasCost += swapResult.gasCostSui;
3517
+ txDigests.push(withdrawResult.digest);
3518
+ totalGasCost += withdrawResult.gasCostSui;
3519
+ if (!isSameAsset) {
3520
+ const swapAdapter = this.registry.listSwap()[0];
3521
+ if (!swapAdapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", "No swap adapter available");
3522
+ const swapResult = await executeWithGas(this.client, this.keypair, async () => {
3523
+ const built = await swapAdapter.buildSwapTx(this._address, current.asset, bestRate.asset, amountToDeposit);
3524
+ amountToDeposit = built.estimatedOut / 10 ** built.toDecimals;
3525
+ return built.tx;
3526
+ });
3527
+ txDigests.push(swapResult.digest);
3528
+ totalGasCost += swapResult.gasCostSui;
3529
+ }
3530
+ const depositResult = await executeWithGas(this.client, this.keypair, async () => {
3531
+ const { tx } = await depositAdapter.buildSaveTx(this._address, amountToDeposit, bestRate.asset, { collectFee: bestRate.asset === "USDC" });
3532
+ return tx;
3533
+ });
3534
+ txDigests.push(depositResult.digest);
3535
+ totalGasCost += depositResult.gasCostSui;
3081
3536
  }
3082
- const depositAdapter = this.registry.getLending(bestRate.protocolId);
3083
- if (!depositAdapter) throw new T2000Error("PROTOCOL_UNAVAILABLE", `Protocol ${bestRate.protocolId} not found`);
3084
- const depositResult = await executeWithGas(this.client, this.keypair, async () => {
3085
- const { tx } = await depositAdapter.buildSaveTx(this._address, amountToDeposit, bestRate.asset, { collectFee: bestRate.asset === "USDC" });
3086
- return tx;
3087
- });
3088
- txDigests.push(depositResult.digest);
3089
- totalGasCost += depositResult.gasCostSui;
3090
3537
  return {
3091
3538
  executed: true,
3092
3539
  steps,