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