@suilend/sdk 14.2.0 → 14.2.1

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.
@@ -31,7 +31,7 @@ export declare const resolveCoinMetadataForInitialize: (miscCoinTypes: string[],
31
31
  reserveCoinMetadataMap: Record<string, import("@suilend/sui-core").Token>;
32
32
  rewardCoinMetadataMap: Record<string, import("@suilend/sui-core").Token>;
33
33
  }>;
34
- export declare const refreshReservesForInitializeAll: (reservesByMarket: Reserve<string>[][], pythConnection: simulate.PriceFeedSource, tolerateMissingPriceFeeds?: boolean) => Promise<{
34
+ export declare const refreshReservesForInitializeAll: (reservesByMarket: Reserve<string>[][], pythConnection: simulate.PriceFeedSource, tolerateMissingPriceFeeds?: boolean, marketVersions?: bigint[]) => Promise<{
35
35
  reserves: Reserve<string>[];
36
36
  unpricedCoinTypes: string[];
37
37
  }[]>;
@@ -42,7 +42,7 @@ export declare const refreshReservesForInitializeAll: (reservesByMarket: Reserve
42
42
  * them is a throw, which is the whole reason the flag exists and is exactly
43
43
  * what a regression here would silently change.
44
44
  */
45
- export declare const refreshReservesForInitialize: (reserves: Reserve<string>[], pythConnection: simulate.PriceFeedSource, tolerateMissingPriceFeeds?: boolean) => Promise<{
45
+ export declare const refreshReservesForInitialize: (reserves: Reserve<string>[], pythConnection: simulate.PriceFeedSource, tolerateMissingPriceFeeds?: boolean, marketVersion?: bigint) => Promise<{
46
46
  reserves: Reserve<string>[];
47
47
  unpricedCoinTypes: string[];
48
48
  }>;
package/lib/initialize.js CHANGED
@@ -162,7 +162,7 @@ export const resolveCoinMetadataForInitialize = async (miscCoinTypes, reserveCoi
162
162
  ]);
163
163
  return resolved;
164
164
  };
165
- export const refreshReservesForInitializeAll = async (reservesByMarket, pythConnection, tolerateMissingPriceFeeds) => {
165
+ export const refreshReservesForInitializeAll = async (reservesByMarket, pythConnection, tolerateMissingPriceFeeds, marketVersions) => {
166
166
  const allReserves = reservesByMarket.flat();
167
167
  if (tolerateMissingPriceFeeds) {
168
168
  // Tolerance is only expressible over a connection that can report WHICH
@@ -184,13 +184,13 @@ export const refreshReservesForInitializeAll = async (reservesByMarket, pythConn
184
184
  throw new Error(`tolerateMissingPriceFeeds requires a connection that can report per-feed gaps. The connection in use is all-or-nothing, so an endpoint that rejects the whole batch over one unknown feed would fail initialization regardless of this flag — ${simulate.PARTIAL_FETCH_REQUIRED_HINT}. Note that patching a client prototype does NOT satisfy this: it can reroute getLatestPriceFeeds but cannot add a per-feed gap report.`);
185
185
  }
186
186
  const priceFeeds = await simulate.fetchReservePriceFeedsTolerant(allReserves, pythConnection);
187
- return reservesByMarket.map((reserves) => simulate.applyReservePriceFeedsTolerant(reserves, priceFeeds));
187
+ return reservesByMarket.map((reserves, i) => simulate.applyReservePriceFeedsTolerant(reserves, priceFeeds, marketVersions?.[i]));
188
188
  }
189
189
  // Default path, behaviourally identical to what this function did before the
190
190
  // flag existed: throws if any requested feed is absent from the response.
191
191
  const priceFeeds = await pythConnection.getLatestPriceFeeds(simulate.reservePriceFeedIds(allReserves));
192
- return reservesByMarket.map((reserves) => ({
193
- reserves: simulate.applyReservePriceFeeds(reserves, priceFeeds),
192
+ return reservesByMarket.map((reserves, i) => ({
193
+ reserves: simulate.applyReservePriceFeeds(reserves, priceFeeds, marketVersions?.[i]),
194
194
  unpricedCoinTypes: [],
195
195
  }));
196
196
  };
@@ -201,8 +201,8 @@ export const refreshReservesForInitializeAll = async (reservesByMarket, pythConn
201
201
  * them is a throw, which is the whole reason the flag exists and is exactly
202
202
  * what a regression here would silently change.
203
203
  */
204
- export const refreshReservesForInitialize = async (reserves, pythConnection, tolerateMissingPriceFeeds) => {
205
- const [refreshed] = await refreshReservesForInitializeAll([reserves], pythConnection, tolerateMissingPriceFeeds);
204
+ export const refreshReservesForInitialize = async (reserves, pythConnection, tolerateMissingPriceFeeds, marketVersion) => {
205
+ const [refreshed] = await refreshReservesForInitializeAll([reserves], pythConnection, tolerateMissingPriceFeeds, marketVersion === undefined ? undefined : [marketVersion]);
206
206
  return refreshed;
207
207
  };
208
208
  /**
@@ -360,7 +360,7 @@ const initializeSuilendMarkets = async (suiGrpcClient, markets, fallbackPythEndp
360
360
  const reservelessActiveRewardCoinTypesByMarket = coinTypesByMarket.map(reservelessActiveRewardCoinTypes);
361
361
  const [[refreshedWithoutTemporaryByMarket, fabricatedPriceCoinTypesByMarket], coinMetadataByMarket, rewardUsdPrices, kBpsByMarket,] = await Promise.all([
362
362
  Promise.all([
363
- resolvePythConnection(options?.pythConnection, fallbackPythEndpoint, options?.pythConnectionConfig).then((pythConnection) => refreshReservesForInitializeAll(reservesByMarket.map((r) => r.withoutTemporaryPythPriceFeeds), pythConnection, options?.tolerateMissingPriceFeeds)),
363
+ resolvePythConnection(options?.pythConnection, fallbackPythEndpoint, options?.pythConnectionConfig).then((pythConnection) => refreshReservesForInitializeAll(reservesByMarket.map((r) => r.withoutTemporaryPythPriceFeeds), pythConnection, options?.tolerateMissingPriceFeeds, markets.map(({ suilendClient }) => suilendClient.lendingMarket.version))),
364
364
  priceTemporaryPythFeedReservesAll(reservesByMarket.map((r) => r.withTemporaryPythPriceFeeds), fetchPrices),
365
365
  ]),
366
366
  resolveCoinMetadataForInitializeAll(coinTypesByMarket.map(({ uniqueMiscCoinTypes, uniqueReserveCoinTypes, uniqueRewardCoinTypes, }) => ({
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"@suilend/sdk","version":"14.2.0","private":false,"description":"A TypeScript SDK for interacting with the Suilend program","author":"Suilend","license":"MIT","main":"./index.js","exports":{".":"./index.js","./client":"./client.js","./mmt":"./mmt.js","./strategies":"./strategies.js","./api/events":"./api/events.js","./api":"./api/index.js","./lib/constants":"./lib/constants.js","./lib":"./lib/index.js","./lib/initialize":"./lib/initialize.js","./lib/liquidityMining":"./lib/liquidityMining.js","./lib/proCompatible":"./lib/proCompatible.js","./lib/pyth":"./lib/pyth.js","./lib/strategyOwnerCap":"./lib/strategyOwnerCap.js","./lib/transactions":"./lib/transactions.js","./lib/types":"./lib/types.js","./margin":"./margin/index.js","./parsers/apiReserveAssetDataEvent":"./parsers/apiReserveAssetDataEvent.js","./parsers":"./parsers/index.js","./parsers/lendingMarket":"./parsers/lendingMarket.js","./parsers/obligation":"./parsers/obligation.js","./parsers/rateLimiter":"./parsers/rateLimiter.js","./parsers/reserve":"./parsers/reserve.js","./swap":"./swap/index.js","./swap/quote":"./swap/quote.js","./swap/transaction":"./swap/transaction.js","./utils/events":"./utils/events.js","./utils/feedId":"./utils/feedId.js","./utils":"./utils/index.js","./utils/interpolate":"./utils/interpolate.js","./utils/obligation":"./utils/obligation.js","./utils/priceGuard":"./utils/priceGuard.js","./utils/simulate":"./utils/simulate.js","./_generated/_envs":"./_generated/_envs/index.js","./_generated/_envs/mainnet":"./_generated/_envs/mainnet.js","./_generated/_framework/env":"./_generated/_framework/env.js","./_generated/_framework/reified":"./_generated/_framework/reified.js","./_generated/_framework/util":"./_generated/_framework/util.js","./_generated/_framework/vector":"./_generated/_framework/vector.js","./_generated/suilend":"./_generated/suilend/index.js","./margin/margin/admin_cap":"./margin/margin/admin_cap.js","./margin/margin/market":"./margin/margin/market.js","./margin/margin/permissions":"./margin/margin/permissions.js","./margin/margin/position":"./margin/margin/position.js","./margin/margin/router":"./margin/margin/router.js","./margin/margin/version":"./margin/margin/version.js","./margin/utils":"./margin/utils/index.js","./_generated/suilend/cell/structs":"./_generated/suilend/cell/structs.js","./_generated/suilend/decimal/structs":"./_generated/suilend/decimal/structs.js","./_generated/suilend/lending-market/functions":"./_generated/suilend/lending-market/functions.js","./_generated/suilend/lending-market/structs":"./_generated/suilend/lending-market/structs.js","./_generated/suilend/lending-market-registry/functions":"./_generated/suilend/lending-market-registry/functions.js","./_generated/suilend/liquidity-mining/structs":"./_generated/suilend/liquidity-mining/structs.js","./_generated/suilend/obligation/structs":"./_generated/suilend/obligation/structs.js","./_generated/suilend/rate-limiter/functions":"./_generated/suilend/rate-limiter/functions.js","./_generated/suilend/rate-limiter/structs":"./_generated/suilend/rate-limiter/structs.js","./_generated/suilend/reserve/structs":"./_generated/suilend/reserve/structs.js","./_generated/suilend/reserve-config/functions":"./_generated/suilend/reserve-config/functions.js","./_generated/suilend/reserve-config/structs":"./_generated/suilend/reserve-config/structs.js","./_generated/_dependencies/pyth-2/price-identifier/structs":"./_generated/_dependencies/pyth-2/price-identifier/structs.js","./_generated/_dependencies/std/ascii/structs":"./_generated/_dependencies/std/ascii/structs.js","./_generated/_dependencies/std/option/structs":"./_generated/_dependencies/std/option/structs.js","./_generated/_dependencies/std/type-name/structs":"./_generated/_dependencies/std/type-name/structs.js","./_generated/_dependencies/sui/bag/structs":"./_generated/_dependencies/sui/bag/structs.js","./_generated/_dependencies/sui/balance/structs":"./_generated/_dependencies/sui/balance/structs.js","./_generated/_dependencies/sui/object/structs":"./_generated/_dependencies/sui/object/structs.js","./_generated/_dependencies/sui/object-table/structs":"./_generated/_dependencies/sui/object-table/structs.js","./margin/margin/deps/std/type_name":"./margin/margin/deps/std/type_name.js","./margin/margin/deps/sui/vec_set":"./margin/margin/deps/sui/vec_set.js","./margin/margin/deps/suilend/lending_market":"./margin/margin/deps/suilend/lending_market.js"},"types":"./index.d.ts","scripts":{"codegen":"node ./codegen.js","codegen:install":"node ./install-codegen.js","build":"rm -rf ./dist && tsc && node ./fix-esm-imports.js","typecheck":"tsc --noEmit && tsc --noEmit -p tsconfig.test.json","test":"vitest run","lint:ci":"yarn run typecheck","prettier":"prettier --write src/ tests/","release":"yarn run build && node ./release.js && cd ./dist && npm publish --access public"},"repository":{"type":"git","url":"git+https://github.com/fireflyprotocol/lending-mono.git","directory":"ts/sdks/sdk"},"dependencies":{"@cetusprotocol/aggregator-sdk":"1.7.3","@flowx-finance/sdk":"2.1.0","@pythnetwork/hermes-client":"3.1.0","@pythnetwork/pyth-sui-js":"4.0.0","@suilend/springsui-sdk":"4.1.0","bignumber.js":"11.1.5","bn.js":"5.2.5","crypto-js":"4.2.0","lodash":"4.18.1","p-limit":"7.3.3","uuid":"14.0.2"},"devDependencies":{"@mysten/bcs":"2.1.2","@mysten/sui":"2.35.0","@suilend/sui-core":"2.2.0","@tsconfig/recommended":"1.0.13","@types/bn.js":"5.2.0","@types/lodash":"4.17.26","@types/node":"26.6.5","fast-check":"4.10.2","prettier":"3.9.10","ts-node":"10.9.2","typescript":"6.0.3","vite":"8.3.4","vitest":"5.0.4"},"peerDependencies":{"@mysten/bcs":"^2.1.1","@mysten/sui":">=2.31.3 <3","@suilend/sui-core":"^2.0.1"},"type":"module"}
1
+ {"name":"@suilend/sdk","version":"14.2.1","private":false,"description":"A TypeScript SDK for interacting with the Suilend program","author":"Suilend","license":"MIT","main":"./index.js","exports":{".":"./index.js","./client":"./client.js","./mmt":"./mmt.js","./strategies":"./strategies.js","./api/events":"./api/events.js","./api":"./api/index.js","./lib/constants":"./lib/constants.js","./lib":"./lib/index.js","./lib/initialize":"./lib/initialize.js","./lib/liquidityMining":"./lib/liquidityMining.js","./lib/proCompatible":"./lib/proCompatible.js","./lib/pyth":"./lib/pyth.js","./lib/strategyOwnerCap":"./lib/strategyOwnerCap.js","./lib/transactions":"./lib/transactions.js","./lib/types":"./lib/types.js","./margin":"./margin/index.js","./parsers/apiReserveAssetDataEvent":"./parsers/apiReserveAssetDataEvent.js","./parsers":"./parsers/index.js","./parsers/lendingMarket":"./parsers/lendingMarket.js","./parsers/obligation":"./parsers/obligation.js","./parsers/rateLimiter":"./parsers/rateLimiter.js","./parsers/reserve":"./parsers/reserve.js","./swap":"./swap/index.js","./swap/quote":"./swap/quote.js","./swap/transaction":"./swap/transaction.js","./utils/events":"./utils/events.js","./utils/feedId":"./utils/feedId.js","./utils":"./utils/index.js","./utils/interpolate":"./utils/interpolate.js","./utils/obligation":"./utils/obligation.js","./utils/priceGuard":"./utils/priceGuard.js","./utils/simulate":"./utils/simulate.js","./_generated/_envs":"./_generated/_envs/index.js","./_generated/_envs/mainnet":"./_generated/_envs/mainnet.js","./_generated/_framework/env":"./_generated/_framework/env.js","./_generated/_framework/reified":"./_generated/_framework/reified.js","./_generated/_framework/util":"./_generated/_framework/util.js","./_generated/_framework/vector":"./_generated/_framework/vector.js","./_generated/suilend":"./_generated/suilend/index.js","./margin/margin/admin_cap":"./margin/margin/admin_cap.js","./margin/margin/market":"./margin/margin/market.js","./margin/margin/permissions":"./margin/margin/permissions.js","./margin/margin/position":"./margin/margin/position.js","./margin/margin/router":"./margin/margin/router.js","./margin/margin/version":"./margin/margin/version.js","./margin/utils":"./margin/utils/index.js","./_generated/suilend/cell/structs":"./_generated/suilend/cell/structs.js","./_generated/suilend/decimal/structs":"./_generated/suilend/decimal/structs.js","./_generated/suilend/lending-market/functions":"./_generated/suilend/lending-market/functions.js","./_generated/suilend/lending-market/structs":"./_generated/suilend/lending-market/structs.js","./_generated/suilend/lending-market-registry/functions":"./_generated/suilend/lending-market-registry/functions.js","./_generated/suilend/liquidity-mining/structs":"./_generated/suilend/liquidity-mining/structs.js","./_generated/suilend/obligation/structs":"./_generated/suilend/obligation/structs.js","./_generated/suilend/rate-limiter/functions":"./_generated/suilend/rate-limiter/functions.js","./_generated/suilend/rate-limiter/structs":"./_generated/suilend/rate-limiter/structs.js","./_generated/suilend/reserve/structs":"./_generated/suilend/reserve/structs.js","./_generated/suilend/reserve-config/functions":"./_generated/suilend/reserve-config/functions.js","./_generated/suilend/reserve-config/structs":"./_generated/suilend/reserve-config/structs.js","./_generated/_dependencies/pyth-2/price-identifier/structs":"./_generated/_dependencies/pyth-2/price-identifier/structs.js","./_generated/_dependencies/std/ascii/structs":"./_generated/_dependencies/std/ascii/structs.js","./_generated/_dependencies/std/option/structs":"./_generated/_dependencies/std/option/structs.js","./_generated/_dependencies/std/type-name/structs":"./_generated/_dependencies/std/type-name/structs.js","./_generated/_dependencies/sui/bag/structs":"./_generated/_dependencies/sui/bag/structs.js","./_generated/_dependencies/sui/balance/structs":"./_generated/_dependencies/sui/balance/structs.js","./_generated/_dependencies/sui/object/structs":"./_generated/_dependencies/sui/object/structs.js","./_generated/_dependencies/sui/object-table/structs":"./_generated/_dependencies/sui/object-table/structs.js","./margin/margin/deps/std/type_name":"./margin/margin/deps/std/type_name.js","./margin/margin/deps/sui/vec_set":"./margin/margin/deps/sui/vec_set.js","./margin/margin/deps/suilend/lending_market":"./margin/margin/deps/suilend/lending_market.js"},"types":"./index.d.ts","scripts":{"codegen":"node ./codegen.js","codegen:install":"node ./install-codegen.js","build":"rm -rf ./dist && tsc && node ./fix-esm-imports.js","typecheck":"tsc --noEmit && tsc --noEmit -p tsconfig.test.json","test":"vitest run","lint:ci":"yarn run typecheck","prettier":"prettier --write src/ tests/","release":"yarn run build && node ./release.js && cd ./dist && npm publish --access public"},"repository":{"type":"git","url":"git+https://github.com/fireflyprotocol/lending-mono.git","directory":"ts/sdks/sdk"},"dependencies":{"@cetusprotocol/aggregator-sdk":"1.7.5","@flowx-finance/sdk":"2.1.0","@pythnetwork/hermes-client":"3.1.0","@pythnetwork/pyth-sui-js":"4.0.0","@suilend/springsui-sdk":"4.1.0","bignumber.js":"11.1.5","bn.js":"5.2.5","crypto-js":"4.2.0","lodash":"4.18.1","p-limit":"7.3.3","uuid":"14.0.3"},"devDependencies":{"@mysten/bcs":"2.1.3","@mysten/sui":"2.37.0","@suilend/sui-core":"2.2.0","@tsconfig/recommended":"1.0.13","@types/bn.js":"5.2.0","@types/lodash":"4.17.27","@types/node":"26.6.5","fast-check":"4.10.2","prettier":"3.9.10","ts-node":"10.9.2","typescript":"6.0.3","vite":"8.3.5","vitest":"5.0.5"},"peerDependencies":{"@mysten/bcs":"^2.1.1","@mysten/sui":">=2.31.3 <3","@suilend/sui-core":"^2.0.1"},"type":"module"}
package/strategies.js CHANGED
@@ -274,6 +274,37 @@ export const getStrategySuiReserve = (
274
274
  // AppContext
275
275
  reserveMap) => reserveMap[NORMALIZED_SUI_COINTYPE] ??
276
276
  { price: new BigNumber(1) };
277
+ const getStrategyDepositUnitValueUsd = (reserveMap, lstMap, coinType) => {
278
+ const suiPrice = getStrategySuiReserve(reserveMap).price;
279
+ const lstToSuiExchangeRate = lstMap[coinType]?.lstToSuiExchangeRate;
280
+ if (isSui(coinType))
281
+ return suiPrice;
282
+ if (lstToSuiExchangeRate !== undefined)
283
+ return new BigNumber(lstToSuiExchangeRate).times(suiPrice);
284
+ return reserveMap[coinType].price;
285
+ };
286
+ const getStrategyExtraDeposits = (reserveMap, lstMap, strategyType, deposits) => {
287
+ const { depositBaseCoinType, depositLstCoinType } = STRATEGY_TYPE_INFO_MAP[strategyType];
288
+ const getValueUsd = (deposit) => deposit.depositedAmount.times(getStrategyDepositUnitValueUsd(reserveMap, lstMap, deposit.coinType));
289
+ return deposits
290
+ .filter((deposit) => {
291
+ const reserve = reserveMap[deposit.coinType];
292
+ return (deposit.coinType !== depositBaseCoinType &&
293
+ deposit.coinType !== depositLstCoinType &&
294
+ reserve !== undefined &&
295
+ deposit.depositedAmount
296
+ .decimalPlaces(reserve.token.decimals, BigNumber.ROUND_DOWN)
297
+ .gt(0));
298
+ })
299
+ .sort((a, b) => getValueUsd(b).comparedTo(getValueUsd(a)) ?? 0);
300
+ };
301
+ const getStrategyWithdrawnCtokenAmount = (reserve, amount) => BigInt(new BigNumber(amount
302
+ .times(10 ** reserve.token.decimals)
303
+ .integerValue(BigNumber.ROUND_DOWN)
304
+ .toString())
305
+ .div(reserve.cTokenExchangeRate)
306
+ .integerValue(BigNumber.ROUND_UP)
307
+ .toString());
277
308
  export const fetchStrategyLstMap = async (suiGrpcClient) => {
278
309
  try {
279
310
  const lstCoinTypes = Array.from(new Set([
@@ -1310,6 +1341,87 @@ dryRunTransaction) => {
1310
1341
  }));
1311
1342
  if (borrowedAmount.eq(targetBorrowedAmount))
1312
1343
  return { deposits, borrowedAmount, transaction };
1344
+ const repayBorrowsUsingExtraDeposits = async () => {
1345
+ const skippedCoinTypes = new Set();
1346
+ for (let i = 0; i < 30; i++) {
1347
+ const pendingBorrowedAmount = borrowedAmount.minus(targetBorrowedAmount);
1348
+ if (pendingBorrowedAmount.lte(STRATEGY_E))
1349
+ return false;
1350
+ const extraDeposit = getStrategyExtraDeposits(reserveMap, lstMap, strategyType, deposits).find((deposit) => !skippedCoinTypes.has(deposit.coinType));
1351
+ if (extraDeposit === undefined)
1352
+ return false;
1353
+ const extraReserve = reserveMap[extraDeposit.coinType];
1354
+ const extraToBorrowExchangeRate = extraReserve.minPrice.div(borrowReserve.maxPrice);
1355
+ const stepMaxWithdrawnAmount = getStrategyStepMaxWithdrawnAmount(reserveMap, lstMap, strategyType, deposits, borrowedAmount, extraReserve.coinType, 0.9);
1356
+ const stepMaxRepaidAmount = stepMaxWithdrawnAmount
1357
+ .times(extraToBorrowExchangeRate)
1358
+ .decimalPlaces(borrowReserve.token.decimals, BigNumber.ROUND_DOWN);
1359
+ const stepTargetRepaidAmount = pendingBorrowedAmount.div(1 - SWAP_MAX_DEVIATION_PERCENT / 100);
1360
+ const isFinalStep = stepTargetRepaidAmount.lte(stepMaxRepaidAmount);
1361
+ const stepWithdrawnAmount = BigNumber.min(stepTargetRepaidAmount, stepMaxRepaidAmount)
1362
+ .div(extraToBorrowExchangeRate)
1363
+ .decimalPlaces(extraReserve.token.decimals, BigNumber.ROUND_DOWN);
1364
+ console.log(`[unloopStrategyToExposure] ${i} repay_using_extra_deposit |`, JSON.stringify({
1365
+ coinType: extraReserve.coinType,
1366
+ pendingBorrowedAmount: pendingBorrowedAmount.toFixed(20),
1367
+ stepMaxWithdrawnAmount: stepMaxWithdrawnAmount.toFixed(20),
1368
+ stepWithdrawnAmount: stepWithdrawnAmount.toFixed(20),
1369
+ isFinalStep,
1370
+ }, null, 2));
1371
+ if (!extraToBorrowExchangeRate.gt(0) || !stepWithdrawnAmount.gt(0)) {
1372
+ skippedCoinTypes.add(extraReserve.coinType);
1373
+ continue;
1374
+ }
1375
+ const transactionBeforeStep = Transaction.from(transaction);
1376
+ const depositsBeforeStep = cloneDeep(deposits);
1377
+ try {
1378
+ const [stepWithdrawnCoin] = strategyWithdraw(strategyType, extraReserve.coinType, strategyOwnerCapId, suilendClient.findReserveArrayIndex(extraReserve.coinType), getStrategyWithdrawnCtokenAmount(extraReserve, stepWithdrawnAmount), transaction);
1379
+ deposits = addOrInsertStrategyDeposit(deposits, {
1380
+ coinType: extraReserve.coinType,
1381
+ depositedAmount: stepWithdrawnAmount.times(-1),
1382
+ });
1383
+ const routers = await cetusSdk.findRouters({
1384
+ from: extraReserve.coinType,
1385
+ target: borrowReserve.coinType,
1386
+ amount: new BN(stepWithdrawnAmount
1387
+ .times(10 ** extraReserve.token.decimals)
1388
+ .integerValue(BigNumber.ROUND_DOWN)
1389
+ .toString()),
1390
+ byAmountIn: true,
1391
+ splitCount: 0,
1392
+ });
1393
+ if (!routers)
1394
+ throw new Error("No swap quote found");
1395
+ const { slippagePercent, guaranteedAmountOut } = resolveSwapSlippage(stepWithdrawnAmount, new BigNumber(routers.amountOut.toString()).div(10 ** borrowReserve.token.decimals), extraReserve, borrowReserve);
1396
+ const stepBorrowCoin = (await cetusSdk.fixableRouterSwapV3({
1397
+ router: routers,
1398
+ inputCoin: stepWithdrawnCoin,
1399
+ slippage: slippagePercent / 100,
1400
+ txb: transaction,
1401
+ partner: cetusPartnerId,
1402
+ }));
1403
+ const txCopy = Transaction.from(transaction);
1404
+ suilendClient.repay(obligationId, borrowReserve.coinType, stepBorrowCoin, txCopy);
1405
+ txCopy.transferObjects([stepBorrowCoin], _address);
1406
+ await dryRunTransaction(txCopy);
1407
+ transaction = txCopy;
1408
+ if (isFinalStep) {
1409
+ borrowedAmount = targetBorrowedAmount;
1410
+ return true;
1411
+ }
1412
+ borrowedAmount = borrowedAmount.minus(guaranteedAmountOut.decimalPlaces(borrowReserve.token.decimals, BigNumber.ROUND_DOWN));
1413
+ }
1414
+ catch (err) {
1415
+ console.error(err);
1416
+ transaction = transactionBeforeStep;
1417
+ deposits = depositsBeforeStep;
1418
+ skippedCoinTypes.add(extraReserve.coinType);
1419
+ }
1420
+ }
1421
+ return false;
1422
+ };
1423
+ if (await repayBorrowsUsingExtraDeposits())
1424
+ return { deposits, borrowedAmount, transaction };
1313
1425
  const fullyRepayBorrowsUsingLst = async (maxWithdrawRemainingLstAndRedepositAsBase) => {
1314
1426
  if (depositReserves.lst === undefined)
1315
1427
  throw new Error("LST reserve not found");
@@ -2288,6 +2400,100 @@ const sendStrategyWithdrawnCoin = async (transaction, coin, estimatedAmount, add
2288
2400
  const getRedeemedSuiAmount = (lstAmount, lst) => lstAmount
2289
2401
  .times(new BigNumber(lst?.lstToSuiExchangeRate ?? 1).times(1 - +(lst?.redeemFeePercent ?? 0) / 100))
2290
2402
  .decimalPlaces(SUI_DECIMALS, BigNumber.ROUND_DOWN);
2403
+ const swapStrategyWithdrawnCoin = async (cetusSdk, cetusPartnerId, transaction, coin, amount, fromReserve, toReserve) => {
2404
+ const swapTransaction = Transaction.from(transaction);
2405
+ try {
2406
+ const routers = await cetusSdk.findRouters({
2407
+ from: fromReserve.coinType,
2408
+ target: toReserve.coinType,
2409
+ amount: new BN(amount
2410
+ .times(10 ** fromReserve.token.decimals)
2411
+ .integerValue(BigNumber.ROUND_DOWN)
2412
+ .toString()),
2413
+ byAmountIn: true,
2414
+ splitCount: 0,
2415
+ });
2416
+ if (!routers)
2417
+ return undefined;
2418
+ const swappedAmount = new BigNumber(routers.amountOut.toString()).div(10 ** toReserve.token.decimals);
2419
+ const { slippagePercent } = resolveSwapSlippage(amount, swappedAmount, fromReserve, toReserve);
2420
+ const swappedCoin = (await cetusSdk.fixableRouterSwapV3({
2421
+ router: routers,
2422
+ inputCoin: coin,
2423
+ slippage: slippagePercent / 100,
2424
+ txb: swapTransaction,
2425
+ partner: cetusPartnerId,
2426
+ }));
2427
+ return {
2428
+ transaction: swapTransaction,
2429
+ withdrawnCoin: { coin: swappedCoin, estimatedAmount: swappedAmount },
2430
+ };
2431
+ }
2432
+ catch (err) {
2433
+ console.error(err);
2434
+ return undefined;
2435
+ }
2436
+ };
2437
+ const withdrawStrategyExtraDepositsTx = async (strategyType, suilendClient, cetusSdk, cetusPartnerId, address, strategyOwnerCapId, extraWithdrawals, withdrawReserve, withdrawnCoin, transaction, dryRunTransaction) => {
2438
+ if (extraWithdrawals.length === 0)
2439
+ return { transaction, withdrawnCoin, isWithdrawn: false };
2440
+ console.log(`[strategyWithdraw] extra_deposits |`, JSON.stringify(extraWithdrawals.map(({ reserve, amount, isMax }) => ({
2441
+ coinType: reserve.coinType,
2442
+ amount: amount.toFixed(20),
2443
+ isMax,
2444
+ })), null, 2));
2445
+ const build = async (swap) => {
2446
+ let extraTransaction = Transaction.from(transaction);
2447
+ const coins = withdrawnCoin !== undefined ? [withdrawnCoin] : [];
2448
+ for (const { reserve, amount, isMax } of extraWithdrawals) {
2449
+ const [coin] = strategyWithdraw(strategyType, reserve.coinType, strategyOwnerCapId, suilendClient.findReserveArrayIndex(reserve.coinType), isMax
2450
+ ? BigInt(MAX_U64.toString())
2451
+ : getStrategyWithdrawnCtokenAmount(reserve, amount), extraTransaction);
2452
+ if (reserve.coinType === withdrawReserve.coinType) {
2453
+ coins.push({ coin, estimatedAmount: amount });
2454
+ continue;
2455
+ }
2456
+ const swapped = swap
2457
+ ? await swapStrategyWithdrawnCoin(cetusSdk, cetusPartnerId, extraTransaction, coin, amount, reserve, withdrawReserve)
2458
+ : undefined;
2459
+ if (swapped === undefined) {
2460
+ extraTransaction.transferObjects([coin], address);
2461
+ continue;
2462
+ }
2463
+ extraTransaction = swapped.transaction;
2464
+ coins.push(swapped.withdrawnCoin);
2465
+ }
2466
+ const [mergedCoin, ...otherCoins] = coins;
2467
+ if (mergedCoin !== undefined && otherCoins.length > 0)
2468
+ extraTransaction.mergeCoins(mergedCoin.coin, otherCoins.map(({ coin }) => coin));
2469
+ return {
2470
+ transaction: extraTransaction,
2471
+ withdrawnCoin: mergedCoin === undefined
2472
+ ? undefined
2473
+ : {
2474
+ coin: mergedCoin.coin,
2475
+ estimatedAmount: coins.reduce((acc, { estimatedAmount }) => acc.plus(estimatedAmount), new BigNumber(0)),
2476
+ },
2477
+ };
2478
+ };
2479
+ const needsSwap = extraWithdrawals.some(({ reserve }) => reserve.coinType !== withdrawReserve.coinType);
2480
+ let error;
2481
+ for (const swap of needsSwap ? [true, false] : [false]) {
2482
+ try {
2483
+ const built = await build(swap);
2484
+ const probe = Transaction.from(built.transaction);
2485
+ if (built.withdrawnCoin !== undefined)
2486
+ probe.transferObjects([built.withdrawnCoin.coin], address);
2487
+ await dryRunTransaction(probe);
2488
+ return { ...built, isWithdrawn: true };
2489
+ }
2490
+ catch (err) {
2491
+ console.error(err);
2492
+ error = err;
2493
+ }
2494
+ }
2495
+ return { transaction, withdrawnCoin, isWithdrawn: false, error };
2496
+ };
2291
2497
  const tryClaimStrategyRewardsToUser = async (strategyType, rewardsMap, strategyOwnerCapId, address, transaction, dryRunTransaction) => {
2292
2498
  const transactionBeforeClaim = Transaction.from(transaction);
2293
2499
  try {
@@ -2400,18 +2606,51 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
2400
2606
  remainingDepositedAmount: remainingDepositedAmount.toFixed(20),
2401
2607
  finalWithdrawnAmount: finalWithdrawnAmount.toFixed(20),
2402
2608
  }, null, 2));
2403
- const [withdrawnCoin] = strategyWithdraw(strategyType, depositReserve.coinType, strategyOwnerCapId, suilendClient.findReserveArrayIndex(depositReserve.coinType), BigInt(new BigNumber(finalWithdrawnAmount
2404
- .times(10 ** depositReserve.token.decimals)
2405
- .integerValue(BigNumber.ROUND_DOWN)
2406
- .toString())
2407
- .div(depositReserve.cTokenExchangeRate)
2408
- .integerValue(BigNumber.ROUND_UP)
2409
- .toString()), transaction);
2410
- // 2.2) Update state
2411
- deposits = addOrInsertStrategyDeposit(deposits, {
2412
- coinType: depositReserve.coinType,
2413
- depositedAmount: finalWithdrawnAmount.times(-1),
2414
- });
2609
+ const finalWithdrawnCtokenAmount = getStrategyWithdrawnCtokenAmount(depositReserve, finalWithdrawnAmount);
2610
+ let withdrawnCoin;
2611
+ if (finalWithdrawnCtokenAmount > BigInt(0)) {
2612
+ [withdrawnCoin] = strategyWithdraw(strategyType, depositReserve.coinType, strategyOwnerCapId, suilendClient.findReserveArrayIndex(depositReserve.coinType), finalWithdrawnCtokenAmount, transaction);
2613
+ // 2.2) Update state
2614
+ deposits = addOrInsertStrategyDeposit(deposits, {
2615
+ coinType: depositReserve.coinType,
2616
+ depositedAmount: finalWithdrawnAmount.times(-1),
2617
+ });
2618
+ }
2619
+ const extraWithdrawals = [];
2620
+ if (!returnWithdrawnCoin) {
2621
+ let shortfallUsd = withdrawnAmount.gt(0)
2622
+ ? withdrawnAmountUsd
2623
+ .times(withdrawnAmount.minus(finalWithdrawnAmount))
2624
+ .div(withdrawnAmount)
2625
+ : new BigNumber(0);
2626
+ let remainingDeposits = deposits;
2627
+ for (const extraDeposit of getStrategyExtraDeposits(reserveMap, lstMap, strategyType, deposits)) {
2628
+ if (!shortfallUsd.gt(0))
2629
+ break;
2630
+ const reserve = reserveMap[extraDeposit.coinType];
2631
+ const unitValueUsd = getStrategyDepositUnitValueUsd(reserveMap, lstMap, reserve.coinType);
2632
+ if (!unitValueUsd.gt(0))
2633
+ continue;
2634
+ const amount = BigNumber.min(shortfallUsd
2635
+ .div(unitValueUsd)
2636
+ .decimalPlaces(reserve.token.decimals, BigNumber.ROUND_UP), getStrategyStepMaxWithdrawnAmount(reserveMap, lstMap, strategyType, remainingDeposits, borrowedAmount, reserve.coinType, borrowedAmount.gt(0) ? 0.9 : 1));
2637
+ if (!amount.gt(0))
2638
+ continue;
2639
+ extraWithdrawals.push({
2640
+ reserve,
2641
+ amount,
2642
+ isMax: borrowedAmount.lte(0) &&
2643
+ amount.gte(extraDeposit.depositedAmount.decimalPlaces(reserve.token.decimals, BigNumber.ROUND_DOWN)),
2644
+ });
2645
+ remainingDeposits = addOrInsertStrategyDeposit(remainingDeposits, {
2646
+ coinType: reserve.coinType,
2647
+ depositedAmount: amount.times(-1),
2648
+ });
2649
+ shortfallUsd = shortfallUsd.minus(amount.times(unitValueUsd));
2650
+ }
2651
+ }
2652
+ if (withdrawnCoin === undefined && extraWithdrawals.length === 0)
2653
+ throw new Error("Nothing left to withdraw");
2415
2654
  const newExposure = getStrategyExposure(reserveMap, lstMap, strategyType, getStrategySimulatedObligation(reserveMap, lstMap, strategyType, deposits, borrowedAmount));
2416
2655
  const newTvlAmountUsd = getStrategyTvlAmount(reserveMap, lstMap, strategyType, getStrategySimulatedObligation(reserveMap, lstMap, strategyType, deposits, borrowedAmount)).times(defaultCurrencyReserve.price);
2417
2656
  console.log(`[withdraw] withdraw.update_state |`, JSON.stringify({
@@ -2430,7 +2669,8 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
2430
2669
  const sendWithdrawnCoin = (coin, estimatedAmount) => sendStrategyWithdrawnCoin(transaction, coin, estimatedAmount, _address, consumeWithdrawnCoin, dryRunTransaction);
2431
2670
  if (returnWithdrawnCoin)
2432
2671
  return { deposits, borrowedAmount, transaction, withdrawnCoin };
2433
- else {
2672
+ let finalWithdrawnCoin;
2673
+ if (withdrawnCoin !== undefined) {
2434
2674
  if (depositReserve.coinType === depositReserves.base?.coinType) {
2435
2675
  if (withdraw.coinType !== depositReserve.coinType) {
2436
2676
  const routers = await cetusSdk.findRouters({
@@ -2460,25 +2700,47 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
2460
2700
  catch (err) {
2461
2701
  throw new Error("No swap quote found");
2462
2702
  }
2463
- await sendWithdrawnCoin(swappedCoin, swappedAmount);
2703
+ finalWithdrawnCoin = {
2704
+ coin: swappedCoin,
2705
+ estimatedAmount: swappedAmount,
2706
+ };
2464
2707
  }
2465
2708
  else {
2466
- await sendWithdrawnCoin(withdrawnCoin, finalWithdrawnAmount);
2709
+ finalWithdrawnCoin = {
2710
+ coin: withdrawnCoin,
2711
+ estimatedAmount: finalWithdrawnAmount,
2712
+ };
2467
2713
  }
2468
2714
  }
2469
2715
  else {
2470
2716
  if (isSui(withdraw.coinType)) {
2471
2717
  // 3.1) Unstake LST for SUI
2472
2718
  const suiWithdrawnCoin = lst.client.redeem(transaction, withdrawnCoin);
2473
- // 3.2) Transfer SUI to user
2474
- await sendWithdrawnCoin(suiWithdrawnCoin, getRedeemedSuiAmount(finalWithdrawnAmount, lst));
2719
+ finalWithdrawnCoin = {
2720
+ coin: suiWithdrawnCoin,
2721
+ estimatedAmount: getRedeemedSuiAmount(finalWithdrawnAmount, lst),
2722
+ };
2475
2723
  }
2476
2724
  else {
2477
- // 3.1) Transfer LST to user
2478
- await sendWithdrawnCoin(withdrawnCoin, finalWithdrawnAmount);
2725
+ finalWithdrawnCoin = {
2726
+ coin: withdrawnCoin,
2727
+ estimatedAmount: finalWithdrawnAmount,
2728
+ };
2479
2729
  }
2480
2730
  }
2481
2731
  }
2732
+ const extras = await withdrawStrategyExtraDepositsTx(strategyType, suilendClient, cetusSdk, cetusPartnerId, _address, strategyOwnerCapId, extraWithdrawals, reserveMap[withdraw.coinType], finalWithdrawnCoin, transaction, dryRunTransaction);
2733
+ if (extras.withdrawnCoin === undefined && !extras.isWithdrawn)
2734
+ throw extras.error ?? new Error("Nothing left to withdraw");
2735
+ transaction = extras.transaction;
2736
+ if (extras.isWithdrawn)
2737
+ for (const { reserve, amount } of extraWithdrawals)
2738
+ deposits = addOrInsertStrategyDeposit(deposits, {
2739
+ coinType: reserve.coinType,
2740
+ depositedAmount: amount.times(-1),
2741
+ });
2742
+ if (extras.withdrawnCoin !== undefined)
2743
+ await sendWithdrawnCoin(extras.withdrawnCoin.coin, extras.withdrawnCoin.estimatedAmount);
2482
2744
  if (hasStrategyRewardsToClaim(rewardsMap) &&
2483
2745
  isStrategyPositionClosed(reserveMap, deposits))
2484
2746
  transaction = await tryClaimStrategyRewardsToUser(strategyType, rewardsMap, strategyOwnerCapId, _address, transaction, dryRunTransaction);
@@ -2639,7 +2901,14 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
2639
2901
  }
2640
2902
  const estimatedWithdrawnAmount = deposits.find((d) => d.coinType === depositReserve.coinType)
2641
2903
  ?.depositedAmount ?? new BigNumber(0);
2642
- const [withdrawnCoin] = strategyWithdraw(strategyType, depositReserve.coinType, strategyOwnerCapId, suilendClient.findReserveArrayIndex(depositReserve.coinType), BigInt(MAX_U64.toString()), transaction);
2904
+ const extraDeposits = getStrategyExtraDeposits(reserveMap, lstMap, strategyType, deposits);
2905
+ const withdrawsDepositReserve = _borrowedAmount.gt(0) ||
2906
+ deposits.some((d) => d.coinType === depositReserve.coinType);
2907
+ if (!withdrawsDepositReserve && extraDeposits.length === 0)
2908
+ throw new Error("Nothing left to withdraw");
2909
+ let withdrawnCoin;
2910
+ if (withdrawsDepositReserve)
2911
+ [withdrawnCoin] = strategyWithdraw(strategyType, depositReserve.coinType, strategyOwnerCapId, suilendClient.findReserveArrayIndex(depositReserve.coinType), BigInt(MAX_U64.toString()), transaction);
2643
2912
  // 2.2) Update state
2644
2913
  deposits = [];
2645
2914
  console.log(`[strategyMaxWithdraw] max_withdraw.update_state |`, JSON.stringify({
@@ -2651,81 +2920,107 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
2651
2920
  }, null, 2));
2652
2921
  const sendWithdrawnCoin = (coin, estimatedAmount) => sendStrategyWithdrawnCoin(transaction, coin, estimatedAmount, _address, consumeWithdrawnCoin, dryRunTransaction);
2653
2922
  // 3) Transfer coin to user
2654
- if (depositReserve.coinType === depositReserves.base?.coinType) {
2655
- if (withdrawCoinType !== depositReserve.coinType) {
2656
- const routers = await cetusSdk.findRouters({
2657
- from: depositReserve.coinType,
2658
- target: withdrawCoinType,
2659
- amount: new BN(estimatedWithdrawnAmount
2660
- .times(10 ** depositReserve.token.decimals)
2661
- .integerValue(BigNumber.ROUND_DOWN)
2662
- .toString()),
2663
- byAmountIn: true,
2664
- splitCount: 0,
2665
- });
2666
- if (!routers)
2667
- throw new Error("No swap quote found");
2668
- const swappedAmount = new BigNumber(routers.amountOut.toString()).div(10 ** reserveMap[withdrawCoinType].token.decimals);
2669
- const { slippagePercent } = resolveSwapSlippage(estimatedWithdrawnAmount, swappedAmount, depositReserve, reserveMap[withdrawCoinType]);
2670
- let swappedCoin;
2671
- try {
2672
- swappedCoin = (await cetusSdk.fixableRouterSwapV3({
2673
- router: routers,
2674
- inputCoin: withdrawnCoin,
2675
- slippage: slippagePercent / 100,
2676
- txb: transaction,
2677
- partner: cetusPartnerId,
2678
- }));
2923
+ let finalWithdrawnCoin;
2924
+ if (withdrawnCoin !== undefined) {
2925
+ if (depositReserve.coinType === depositReserves.base?.coinType) {
2926
+ if (withdrawCoinType !== depositReserve.coinType) {
2927
+ const routers = await cetusSdk.findRouters({
2928
+ from: depositReserve.coinType,
2929
+ target: withdrawCoinType,
2930
+ amount: new BN(estimatedWithdrawnAmount
2931
+ .times(10 ** depositReserve.token.decimals)
2932
+ .integerValue(BigNumber.ROUND_DOWN)
2933
+ .toString()),
2934
+ byAmountIn: true,
2935
+ splitCount: 0,
2936
+ });
2937
+ if (!routers)
2938
+ throw new Error("No swap quote found");
2939
+ const swappedAmount = new BigNumber(routers.amountOut.toString()).div(10 ** reserveMap[withdrawCoinType].token.decimals);
2940
+ const { slippagePercent } = resolveSwapSlippage(estimatedWithdrawnAmount, swappedAmount, depositReserve, reserveMap[withdrawCoinType]);
2941
+ let swappedCoin;
2942
+ try {
2943
+ swappedCoin = (await cetusSdk.fixableRouterSwapV3({
2944
+ router: routers,
2945
+ inputCoin: withdrawnCoin,
2946
+ slippage: slippagePercent / 100,
2947
+ txb: transaction,
2948
+ partner: cetusPartnerId,
2949
+ }));
2950
+ }
2951
+ catch (err) {
2952
+ throw new Error("No swap quote found");
2953
+ }
2954
+ finalWithdrawnCoin = {
2955
+ coin: swappedCoin,
2956
+ estimatedAmount: swappedAmount,
2957
+ };
2679
2958
  }
2680
- catch (err) {
2681
- throw new Error("No swap quote found");
2959
+ else {
2960
+ finalWithdrawnCoin = {
2961
+ coin: withdrawnCoin,
2962
+ estimatedAmount: estimatedWithdrawnAmount,
2963
+ };
2682
2964
  }
2683
- await sendWithdrawnCoin(swappedCoin, swappedAmount);
2684
2965
  }
2685
2966
  else {
2686
- await sendWithdrawnCoin(withdrawnCoin, estimatedWithdrawnAmount);
2687
- }
2688
- }
2689
- else {
2690
- if (isSui(withdrawCoinType)) {
2691
- // 3.1) Unstake LST for SUI
2692
- const suiWithdrawnCoin = lst.client.redeem(transaction, withdrawnCoin);
2693
- // 3.2) Repay flash loan + fee
2694
- if (flashLoan !== undefined) {
2695
- transaction.mergeCoins(suiWithdrawnCoin, [flashLoan.remainingCoin]);
2696
- const receiptDebts = transaction.moveCall({
2697
- target: `${MMT_CONTRACT_PACKAGE_ID}::trade::flash_receipt_debts`,
2698
- typeArguments: [],
2699
- arguments: [flashLoan.receipt],
2700
- });
2701
- const [flashLoanRepayCoin] = transaction.splitCoins(suiWithdrawnCoin, [
2702
- receiptDebts[1],
2703
- ]);
2704
- const flashLoanRepayBalance = transaction.moveCall({
2705
- target: "0x2::coin::into_balance",
2706
- typeArguments: [flashLoan.obj.coinTypeB],
2707
- arguments: [flashLoanRepayCoin],
2708
- });
2709
- transaction.moveCall({
2710
- target: `${MMT_CONTRACT_PACKAGE_ID}::trade::repay_flash_loan`,
2711
- typeArguments: [flashLoan.obj.coinTypeA, flashLoan.obj.coinTypeB],
2712
- arguments: [
2713
- transaction.object(flashLoan.obj.poolId),
2714
- flashLoan.receipt,
2715
- flashLoan.borrowedBalanceA,
2716
- flashLoanRepayBalance,
2717
- transaction.object(MMT_VERSION_OBJECT_ID),
2718
- ],
2719
- });
2967
+ if (isSui(withdrawCoinType)) {
2968
+ // 3.1) Unstake LST for SUI
2969
+ const suiWithdrawnCoin = lst.client.redeem(transaction, withdrawnCoin);
2970
+ // 3.2) Repay flash loan + fee
2971
+ if (flashLoan !== undefined) {
2972
+ transaction.mergeCoins(suiWithdrawnCoin, [flashLoan.remainingCoin]);
2973
+ const receiptDebts = transaction.moveCall({
2974
+ target: `${MMT_CONTRACT_PACKAGE_ID}::trade::flash_receipt_debts`,
2975
+ typeArguments: [],
2976
+ arguments: [flashLoan.receipt],
2977
+ });
2978
+ const [flashLoanRepayCoin] = transaction.splitCoins(suiWithdrawnCoin, [receiptDebts[1]]);
2979
+ const flashLoanRepayBalance = transaction.moveCall({
2980
+ target: "0x2::coin::into_balance",
2981
+ typeArguments: [flashLoan.obj.coinTypeB],
2982
+ arguments: [flashLoanRepayCoin],
2983
+ });
2984
+ transaction.moveCall({
2985
+ target: `${MMT_CONTRACT_PACKAGE_ID}::trade::repay_flash_loan`,
2986
+ typeArguments: [flashLoan.obj.coinTypeA, flashLoan.obj.coinTypeB],
2987
+ arguments: [
2988
+ transaction.object(flashLoan.obj.poolId),
2989
+ flashLoan.receipt,
2990
+ flashLoan.borrowedBalanceA,
2991
+ flashLoanRepayBalance,
2992
+ transaction.object(MMT_VERSION_OBJECT_ID),
2993
+ ],
2994
+ });
2995
+ }
2996
+ finalWithdrawnCoin = {
2997
+ coin: suiWithdrawnCoin,
2998
+ estimatedAmount: BigNumber.max(0, getRedeemedSuiAmount(estimatedWithdrawnAmount, lst).minus(flashLoan?.borrowedAmount ?? 0)),
2999
+ };
3000
+ }
3001
+ else {
3002
+ finalWithdrawnCoin = {
3003
+ coin: withdrawnCoin,
3004
+ estimatedAmount: estimatedWithdrawnAmount,
3005
+ };
2720
3006
  }
2721
- // 3.3) Transfer SUI to user
2722
- await sendWithdrawnCoin(suiWithdrawnCoin, BigNumber.max(0, getRedeemedSuiAmount(estimatedWithdrawnAmount, lst).minus(flashLoan?.borrowedAmount ?? 0)));
2723
- }
2724
- else {
2725
- // 3.1) Transfer LST to user
2726
- await sendWithdrawnCoin(withdrawnCoin, estimatedWithdrawnAmount);
2727
3007
  }
2728
3008
  }
3009
+ const extras = await withdrawStrategyExtraDepositsTx(strategyType, suilendClient, cetusSdk, cetusPartnerId, _address, strategyOwnerCapId, extraDeposits.map((deposit) => {
3010
+ const reserve = reserveMap[deposit.coinType];
3011
+ return {
3012
+ reserve,
3013
+ amount: deposit.depositedAmount.decimalPlaces(reserve.token.decimals, BigNumber.ROUND_DOWN),
3014
+ isMax: true,
3015
+ };
3016
+ }), reserveMap[withdrawCoinType], finalWithdrawnCoin, transaction, dryRunTransaction);
3017
+ if (extras.withdrawnCoin === undefined && !extras.isWithdrawn)
3018
+ throw extras.error ?? new Error("Nothing left to withdraw");
3019
+ transaction = extras.transaction;
3020
+ if (!extras.isWithdrawn)
3021
+ deposits = extraDeposits;
3022
+ if (extras.withdrawnCoin !== undefined)
3023
+ await sendWithdrawnCoin(extras.withdrawnCoin.coin, extras.withdrawnCoin.estimatedAmount);
2729
3024
  // 4) Claim rewards and transfer to user
2730
3025
  if (hasClaimableRewards) {
2731
3026
  // Claiming is a bonus, so a failure must not block the withdraw — but the calls
@@ -56,9 +56,17 @@ export interface PriceFeedSource {
56
56
  getLatestPriceFeeds(ids: string[]): Promise<FeedPrice[] | undefined>;
57
57
  }
58
58
  export { normalizeFeedId };
59
+ /**
60
+ * The first lending market version whose refresh aborts `EInvalidPrice` on a
61
+ * spot that parses to a zero `Decimal`, instead of storing it. Earlier versions
62
+ * store the zero, so the model follows the market's own version. The chain
63
+ * builds that `Decimal` with flooring division, so any spot below 1e-18 is
64
+ * zero there, while `stringToDecimal` rounds 7e-19 up to 1e-18.
65
+ */
66
+ export declare const ZERO_PRICE_ABORT_MARKET_VERSION: bigint;
59
67
  export declare const reservePriceFeedIds: (reserves: Reserve<string>[]) => string[];
60
- export declare const applyReservePriceFeeds: (reserves: Reserve<string>[], priceFeeds: FeedPrice[] | undefined) => Reserve<string>[];
61
- export declare const refreshReservePrice: (reserves: Reserve<string>[], pythConnection: PriceFeedSource) => Promise<Reserve<string>[]>;
68
+ export declare const applyReservePriceFeeds: (reserves: Reserve<string>[], priceFeeds: FeedPrice[] | undefined, marketVersion?: bigint) => Reserve<string>[];
69
+ export declare const refreshReservePrice: (reserves: Reserve<string>[], pythConnection: PriceFeedSource, marketVersion?: bigint) => Promise<Reserve<string>[]>;
62
70
  /**
63
71
  * A connection that can report which of the requested feeds it could not serve,
64
72
  * instead of failing the whole batch. Structural on purpose: any object with
@@ -123,11 +131,11 @@ export declare const PARTIAL_FETCH_REQUIRED_HINT = "inject a connection implemen
123
131
  * error. `refreshReservesForInitialize` rejects the combination outright.
124
132
  */
125
133
  export declare const fetchReservePriceFeedsTolerant: (reserves: Reserve<string>[], pythConnection: PriceFeedSource | PartialPriceFeedSource) => Promise<FeedPrice[] | undefined>;
126
- export declare const applyReservePriceFeedsTolerant: (reserves: Reserve<string>[], priceFeeds: FeedPrice[] | undefined) => {
134
+ export declare const applyReservePriceFeedsTolerant: (reserves: Reserve<string>[], priceFeeds: FeedPrice[] | undefined, marketVersion?: bigint) => {
127
135
  reserves: Reserve<string>[];
128
136
  unpricedCoinTypes: string[];
129
137
  };
130
- export declare const refreshReservePriceTolerant: (reserves: Reserve<string>[], pythConnection: PriceFeedSource | PartialPriceFeedSource) => Promise<{
138
+ export declare const refreshReservePriceTolerant: (reserves: Reserve<string>[], pythConnection: PriceFeedSource | PartialPriceFeedSource, marketVersion?: bigint) => Promise<{
131
139
  reserves: Reserve<string>[];
132
140
  unpricedCoinTypes: string[];
133
141
  }>;
package/utils/simulate.js CHANGED
@@ -180,8 +180,19 @@ const withFeedPrice = (reserve, priceFeed) => {
180
180
  newReserve.unclampedPrices = undefined;
181
181
  return newReserve;
182
182
  };
183
+ /**
184
+ * The first lending market version whose refresh aborts `EInvalidPrice` on a
185
+ * spot that parses to a zero `Decimal`, instead of storing it. Earlier versions
186
+ * store the zero, so the model follows the market's own version. The chain
187
+ * builds that `Decimal` with flooring division, so any spot below 1e-18 is
188
+ * zero there, while `stringToDecimal` rounds 7e-19 up to 1e-18.
189
+ */
190
+ export const ZERO_PRICE_ABORT_MARKET_VERSION = BigInt(8);
191
+ const refreshAbortsOnZeroPrice = (marketVersion, priceFeed) => marketVersion !== undefined &&
192
+ marketVersion >= ZERO_PRICE_ABORT_MARKET_VERSION &&
193
+ new BigNumber(priceFeed.price).shiftedBy(18).lt(1);
183
194
  export const reservePriceFeedIds = (reserves) => Array.from(new Set(reserves.map(reserveFeedId)));
184
- export const applyReservePriceFeeds = (reserves, priceFeeds) => {
195
+ export const applyReservePriceFeeds = (reserves, priceFeeds, marketVersion) => {
185
196
  // Deliberately NOT a throw, and deliberately not expressed over the tolerant
186
197
  // function below: consumers outside this repo depend on `undefined` meaning
187
198
  // "keep every reserve's existing price". Hazardous (see the tolerant
@@ -194,10 +205,12 @@ export const applyReservePriceFeeds = (reserves, priceFeeds) => {
194
205
  const priceFeed = byId.get(reserveFeedId(reserve));
195
206
  if (!priceFeed)
196
207
  throw new Error(`Price feed not found for reserve ${reserve.coinType}`);
208
+ if (refreshAbortsOnZeroPrice(marketVersion, priceFeed))
209
+ throw new Error(`Price feed for reserve ${reserve.coinType} reads a zero price, which a market at version ${marketVersion} rejects with EInvalidPrice`);
197
210
  return withFeedPrice(reserve, priceFeed);
198
211
  });
199
212
  };
200
- export const refreshReservePrice = async (reserves, pythConnection) => applyReservePriceFeeds(reserves, await pythConnection.getLatestPriceFeeds(reservePriceFeedIds(reserves)));
213
+ export const refreshReservePrice = async (reserves, pythConnection, marketVersion) => applyReservePriceFeeds(reserves, await pythConnection.getLatestPriceFeeds(reservePriceFeedIds(reserves)), marketVersion);
201
214
  /**
202
215
  * Whether this connection can report WHICH requested feeds it could not serve.
203
216
  *
@@ -269,12 +282,12 @@ export const fetchReservePriceFeedsTolerant = async (reserves, pythConnection) =
269
282
  throw new Error(`Tolerant price refresh failed on an all-or-nothing connection: ${error instanceof Error ? error.message : String(error)}. This connection cannot report which feeds are missing, so an endpoint that rejects the whole batch over one unknown id (Hermes v2) defeats tolerance entirely — ${PARTIAL_FETCH_REQUIRED_HINT}.`, { cause: error });
270
283
  }
271
284
  };
272
- export const applyReservePriceFeedsTolerant = (reserves, priceFeeds) => {
285
+ export const applyReservePriceFeedsTolerant = (reserves, priceFeeds, marketVersion) => {
273
286
  const byId = indexFeedsById(priceFeeds ?? []);
274
287
  const unpricedCoinTypes = [];
275
288
  const updatedReserves = reserves.map((reserve) => {
276
289
  const priceFeed = byId.get(reserveFeedId(reserve));
277
- if (!priceFeed) {
290
+ if (!priceFeed || refreshAbortsOnZeroPrice(marketVersion, priceFeed)) {
278
291
  // Normalized to match every other coin type this SDK returns
279
292
  // (`reserveMap`, `reserveCoinTypes`, and the parsers are all
280
293
  // normalizeStructTag'd). The raw on-chain form never compares equal to
@@ -287,7 +300,7 @@ export const applyReservePriceFeedsTolerant = (reserves, priceFeeds) => {
287
300
  });
288
301
  return { reserves: updatedReserves, unpricedCoinTypes };
289
302
  };
290
- export const refreshReservePriceTolerant = async (reserves, pythConnection) => applyReservePriceFeedsTolerant(reserves, await fetchReservePriceFeedsTolerant(reserves, pythConnection));
303
+ export const refreshReservePriceTolerant = async (reserves, pythConnection, marketVersion) => applyReservePriceFeedsTolerant(reserves, await fetchReservePriceFeedsTolerant(reserves, pythConnection), marketVersion);
291
304
  const warnedStaleRewardSnapshots = new Set();
292
305
  const warnStaleRewardSnapshot = (poolRewardId, poolCumulative, userCumulative) => {
293
306
  if (warnedStaleRewardSnapshots.has(poolRewardId))