@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.
- package/lib/initialize.d.ts +2 -2
- package/lib/initialize.js +7 -7
- package/package.json +1 -1
- package/strategies.js +383 -88
- package/utils/simulate.d.ts +12 -4
- package/utils/simulate.js +18 -5
package/lib/initialize.d.ts
CHANGED
|
@@ -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.
|
|
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
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
.
|
|
2407
|
-
.
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2703
|
+
finalWithdrawnCoin = {
|
|
2704
|
+
coin: swappedCoin,
|
|
2705
|
+
estimatedAmount: swappedAmount,
|
|
2706
|
+
};
|
|
2464
2707
|
}
|
|
2465
2708
|
else {
|
|
2466
|
-
|
|
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
|
-
|
|
2474
|
-
|
|
2719
|
+
finalWithdrawnCoin = {
|
|
2720
|
+
coin: suiWithdrawnCoin,
|
|
2721
|
+
estimatedAmount: getRedeemedSuiAmount(finalWithdrawnAmount, lst),
|
|
2722
|
+
};
|
|
2475
2723
|
}
|
|
2476
2724
|
else {
|
|
2477
|
-
|
|
2478
|
-
|
|
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
|
|
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
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
swappedCoin
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
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
|
-
|
|
2681
|
-
|
|
2959
|
+
else {
|
|
2960
|
+
finalWithdrawnCoin = {
|
|
2961
|
+
coin: withdrawnCoin,
|
|
2962
|
+
estimatedAmount: estimatedWithdrawnAmount,
|
|
2963
|
+
};
|
|
2682
2964
|
}
|
|
2683
|
-
await sendWithdrawnCoin(swappedCoin, swappedAmount);
|
|
2684
2965
|
}
|
|
2685
2966
|
else {
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
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
|
package/utils/simulate.d.ts
CHANGED
|
@@ -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))
|