@suilend/sdk 14.1.0 → 14.2.0-next.g8500491
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.d.ts +4 -2
- package/strategies.js +45 -12
- 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.
|
|
1
|
+
{"name":"@suilend/sdk","version":"14.2.0-next.g8500491","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.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { ParsedObligation, ParsedReserve } from "./parsers";
|
|
|
13
13
|
export declare const STRATEGY_E: number;
|
|
14
14
|
export declare const STRATEGY_FLASH_REPAY_MAX_RESIDUAL_USD = 0.1;
|
|
15
15
|
export declare const STRATEGY_RATE_LIMITED_ERROR_MESSAGE = "This withdrawal exceeds the lending market's outflow limit for now. Withdraw a smaller amount, or wait and try again.";
|
|
16
|
+
export declare const STRATEGY_WITHDRAWN_COIN_CONSUMER_ERROR_MESSAGE = "Failed to use the withdrawn coin";
|
|
16
17
|
/**
|
|
17
18
|
* Below this, a levered position is uneconomic no matter what the slippage
|
|
18
19
|
* tolerance says. The aggregator's dust regime dominates: routes go multi-hop
|
|
@@ -356,9 +357,10 @@ export declare const strategyDepositAndLoopToExposureTx: (reserveMap: Record<str
|
|
|
356
357
|
borrowedAmount: BigNumber;
|
|
357
358
|
transaction: Transaction;
|
|
358
359
|
}>;
|
|
360
|
+
export type StrategyWithdrawnCoinConsumer = (transaction: Transaction, coin: TransactionObjectArgument, estimatedAmount: BigNumber) => Promise<void>;
|
|
359
361
|
export declare const strategyWithdrawTx: (reserveMap: Record<string, ParsedReserve>, lstMap: StrategyLstMap, strategyType: StrategyType, suiGrpcClient: SuiGrpcClient, suilendClient: SuilendClient, cetusSdk: CetusSdk, cetusPartnerId: string, _address: string, strategyOwnerCapId: TransactionObjectInput, obligationId: string, _deposits: StrategyDeposit[], _borrowedAmount: BigNumber, withdraw: StrategyWithdraw, transaction: Transaction, dryRunTransaction: (transaction: Transaction, setGasBudget?: boolean) => Promise<SuiClientTypes.SimulateTransactionResult<{
|
|
360
362
|
commandResults: true;
|
|
361
|
-
}>>, returnWithdrawnCoin?: boolean, rewardsMap?: RewardsMap) => Promise<{
|
|
363
|
+
}>>, returnWithdrawnCoin?: boolean, rewardsMap?: RewardsMap, consumeWithdrawnCoin?: StrategyWithdrawnCoinConsumer) => Promise<{
|
|
362
364
|
deposits: StrategyDeposit[];
|
|
363
365
|
borrowedAmount: BigNumber;
|
|
364
366
|
transaction: Transaction;
|
|
@@ -366,7 +368,7 @@ export declare const strategyWithdrawTx: (reserveMap: Record<string, ParsedReser
|
|
|
366
368
|
}>;
|
|
367
369
|
export declare const strategyMaxWithdrawTx: (reserveMap: Record<string, ParsedReserve>, rewardsMap: RewardsMap, lstMap: StrategyLstMap, strategyType: StrategyType, suiGrpcClient: SuiGrpcClient, suilendClient: SuilendClient, cetusSdk: CetusSdk, cetusPartnerId: string, _address: string, strategyOwnerCapId: TransactionObjectInput, obligationId: string, _deposits: StrategyDeposit[], _borrowedAmount: BigNumber, withdrawCoinType: string, transaction: Transaction, _dryRunTransaction: (transaction: Transaction, setGasBudget?: boolean) => Promise<SuiClientTypes.SimulateTransactionResult<{
|
|
368
370
|
commandResults: true;
|
|
369
|
-
}
|
|
371
|
+
}>>, consumeWithdrawnCoin?: StrategyWithdrawnCoinConsumer) => Promise<{
|
|
370
372
|
deposits: StrategyDeposit[];
|
|
371
373
|
borrowedAmount: BigNumber;
|
|
372
374
|
transaction: Transaction;
|
package/strategies.js
CHANGED
|
@@ -14,6 +14,7 @@ import { bisectionMethod, getWeightedBorrowsUsd } from "./utils/index.js";
|
|
|
14
14
|
export const STRATEGY_E = 10 ** -7;
|
|
15
15
|
export const STRATEGY_FLASH_REPAY_MAX_RESIDUAL_USD = 0.1;
|
|
16
16
|
export const STRATEGY_RATE_LIMITED_ERROR_MESSAGE = "This withdrawal exceeds the lending market's outflow limit for now. Withdraw a smaller amount, or wait and try again.";
|
|
17
|
+
export const STRATEGY_WITHDRAWN_COIN_CONSUMER_ERROR_MESSAGE = "Failed to use the withdrawn coin";
|
|
17
18
|
const isRateLimitExceeded = (err) => /abort code: 2, in '[^']*::rate_limiter::process_qty'/.test(err instanceof Error ? err.message : String(err));
|
|
18
19
|
// Every step dry-runs and rolls back on failure, so the builders only see that
|
|
19
20
|
// no step landed. Recording why lets them name the market's outflow limit
|
|
@@ -2262,6 +2263,31 @@ const isStrategyPositionClosed = (reserveMap, deposits) => deposits.every((d) =>
|
|
|
2262
2263
|
.decimalPlaces(reserve.token.decimals, BigNumber.ROUND_DOWN)
|
|
2263
2264
|
.lte(0));
|
|
2264
2265
|
});
|
|
2266
|
+
const sendStrategyWithdrawnCoin = async (transaction, coin, estimatedAmount, address, consumeWithdrawnCoin, dryRunTransaction) => {
|
|
2267
|
+
if (!consumeWithdrawnCoin) {
|
|
2268
|
+
transaction.transferObjects([coin], address);
|
|
2269
|
+
return;
|
|
2270
|
+
}
|
|
2271
|
+
const transactionBeforeConsumer = Transaction.from(transaction);
|
|
2272
|
+
await consumeWithdrawnCoin(transaction, coin, estimatedAmount);
|
|
2273
|
+
try {
|
|
2274
|
+
await dryRunTransaction(transaction);
|
|
2275
|
+
}
|
|
2276
|
+
catch (err) {
|
|
2277
|
+
transactionBeforeConsumer.transferObjects([coin], address);
|
|
2278
|
+
try {
|
|
2279
|
+
await dryRunTransaction(transactionBeforeConsumer);
|
|
2280
|
+
}
|
|
2281
|
+
catch {
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
console.error(err);
|
|
2285
|
+
throw new Error(STRATEGY_WITHDRAWN_COIN_CONSUMER_ERROR_MESSAGE);
|
|
2286
|
+
}
|
|
2287
|
+
};
|
|
2288
|
+
const getRedeemedSuiAmount = (lstAmount, lst) => lstAmount
|
|
2289
|
+
.times(new BigNumber(lst?.lstToSuiExchangeRate ?? 1).times(1 - +(lst?.redeemFeePercent ?? 0) / 100))
|
|
2290
|
+
.decimalPlaces(SUI_DECIMALS, BigNumber.ROUND_DOWN);
|
|
2265
2291
|
const tryClaimStrategyRewardsToUser = async (strategyType, rewardsMap, strategyOwnerCapId, address, transaction, dryRunTransaction) => {
|
|
2266
2292
|
const transactionBeforeClaim = Transaction.from(transaction);
|
|
2267
2293
|
try {
|
|
@@ -2281,7 +2307,9 @@ export const strategyWithdrawTx = async (
|
|
|
2281
2307
|
// AppContext
|
|
2282
2308
|
reserveMap,
|
|
2283
2309
|
// Strategy
|
|
2284
|
-
lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _address, strategyOwnerCapId, obligationId, _deposits, _borrowedAmount, withdraw, transaction, dryRunTransaction, returnWithdrawnCoin, rewardsMap = {}) => {
|
|
2310
|
+
lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _address, strategyOwnerCapId, obligationId, _deposits, _borrowedAmount, withdraw, transaction, dryRunTransaction, returnWithdrawnCoin, rewardsMap = {}, consumeWithdrawnCoin) => {
|
|
2311
|
+
if (returnWithdrawnCoin && consumeWithdrawnCoin)
|
|
2312
|
+
throw new Error("Pass returnWithdrawnCoin or consumeWithdrawnCoin, not both");
|
|
2285
2313
|
const strategyInfo = STRATEGY_TYPE_INFO_MAP[strategyType];
|
|
2286
2314
|
const lst = strategyInfo.depositLstCoinType !== undefined
|
|
2287
2315
|
? lstMap[strategyInfo.depositLstCoinType]
|
|
@@ -2399,6 +2427,7 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
|
|
|
2399
2427
|
newTvlAmountUsd: newTvlAmountUsd.toFixed(20),
|
|
2400
2428
|
}, null, 2));
|
|
2401
2429
|
// 3) Transfer coin to user, or return coin
|
|
2430
|
+
const sendWithdrawnCoin = (coin, estimatedAmount) => sendStrategyWithdrawnCoin(transaction, coin, estimatedAmount, _address, consumeWithdrawnCoin, dryRunTransaction);
|
|
2402
2431
|
if (returnWithdrawnCoin)
|
|
2403
2432
|
return { deposits, borrowedAmount, transaction, withdrawnCoin };
|
|
2404
2433
|
else {
|
|
@@ -2416,7 +2445,8 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
|
|
|
2416
2445
|
});
|
|
2417
2446
|
if (!routers)
|
|
2418
2447
|
throw new Error("No swap quote found");
|
|
2419
|
-
const
|
|
2448
|
+
const swappedAmount = new BigNumber(routers.amountOut.toString()).div(10 ** reserveMap[withdraw.coinType].token.decimals);
|
|
2449
|
+
const { slippagePercent } = resolveSwapSlippage(finalWithdrawnAmount, swappedAmount, depositReserve, reserveMap[withdraw.coinType]);
|
|
2420
2450
|
let swappedCoin;
|
|
2421
2451
|
try {
|
|
2422
2452
|
swappedCoin = (await cetusSdk.fixableRouterSwapV3({
|
|
@@ -2430,10 +2460,10 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
|
|
|
2430
2460
|
catch (err) {
|
|
2431
2461
|
throw new Error("No swap quote found");
|
|
2432
2462
|
}
|
|
2433
|
-
|
|
2463
|
+
await sendWithdrawnCoin(swappedCoin, swappedAmount);
|
|
2434
2464
|
}
|
|
2435
2465
|
else {
|
|
2436
|
-
|
|
2466
|
+
await sendWithdrawnCoin(withdrawnCoin, finalWithdrawnAmount);
|
|
2437
2467
|
}
|
|
2438
2468
|
}
|
|
2439
2469
|
else {
|
|
@@ -2441,11 +2471,11 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
|
|
|
2441
2471
|
// 3.1) Unstake LST for SUI
|
|
2442
2472
|
const suiWithdrawnCoin = lst.client.redeem(transaction, withdrawnCoin);
|
|
2443
2473
|
// 3.2) Transfer SUI to user
|
|
2444
|
-
|
|
2474
|
+
await sendWithdrawnCoin(suiWithdrawnCoin, getRedeemedSuiAmount(finalWithdrawnAmount, lst));
|
|
2445
2475
|
}
|
|
2446
2476
|
else {
|
|
2447
2477
|
// 3.1) Transfer LST to user
|
|
2448
|
-
|
|
2478
|
+
await sendWithdrawnCoin(withdrawnCoin, finalWithdrawnAmount);
|
|
2449
2479
|
}
|
|
2450
2480
|
}
|
|
2451
2481
|
}
|
|
@@ -2458,7 +2488,7 @@ export const strategyMaxWithdrawTx = async (
|
|
|
2458
2488
|
// AppContext
|
|
2459
2489
|
reserveMap, rewardsMap,
|
|
2460
2490
|
// Strategy
|
|
2461
|
-
lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _address, strategyOwnerCapId, obligationId, _deposits, _borrowedAmount, withdrawCoinType, transaction, _dryRunTransaction) => {
|
|
2491
|
+
lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _address, strategyOwnerCapId, obligationId, _deposits, _borrowedAmount, withdrawCoinType, transaction, _dryRunTransaction, consumeWithdrawnCoin) => {
|
|
2462
2492
|
const { rateLimit, dryRunTransaction } = trackRateLimit(_dryRunTransaction);
|
|
2463
2493
|
const strategyInfo = STRATEGY_TYPE_INFO_MAP[strategyType];
|
|
2464
2494
|
const lst = strategyInfo.depositLstCoinType !== undefined
|
|
@@ -2604,6 +2634,7 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
|
|
|
2604
2634
|
receipt,
|
|
2605
2635
|
borrowedBalanceA,
|
|
2606
2636
|
remainingCoin: flashLoanBorrowedCoin,
|
|
2637
|
+
borrowedAmount: flashLoanBorrowedAmount,
|
|
2607
2638
|
};
|
|
2608
2639
|
}
|
|
2609
2640
|
const estimatedWithdrawnAmount = deposits.find((d) => d.coinType === depositReserve.coinType)
|
|
@@ -2618,6 +2649,7 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
|
|
|
2618
2649
|
})),
|
|
2619
2650
|
borrowedAmount: borrowedAmount.toFixed(20),
|
|
2620
2651
|
}, null, 2));
|
|
2652
|
+
const sendWithdrawnCoin = (coin, estimatedAmount) => sendStrategyWithdrawnCoin(transaction, coin, estimatedAmount, _address, consumeWithdrawnCoin, dryRunTransaction);
|
|
2621
2653
|
// 3) Transfer coin to user
|
|
2622
2654
|
if (depositReserve.coinType === depositReserves.base?.coinType) {
|
|
2623
2655
|
if (withdrawCoinType !== depositReserve.coinType) {
|
|
@@ -2633,7 +2665,8 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
|
|
|
2633
2665
|
});
|
|
2634
2666
|
if (!routers)
|
|
2635
2667
|
throw new Error("No swap quote found");
|
|
2636
|
-
const
|
|
2668
|
+
const swappedAmount = new BigNumber(routers.amountOut.toString()).div(10 ** reserveMap[withdrawCoinType].token.decimals);
|
|
2669
|
+
const { slippagePercent } = resolveSwapSlippage(estimatedWithdrawnAmount, swappedAmount, depositReserve, reserveMap[withdrawCoinType]);
|
|
2637
2670
|
let swappedCoin;
|
|
2638
2671
|
try {
|
|
2639
2672
|
swappedCoin = (await cetusSdk.fixableRouterSwapV3({
|
|
@@ -2647,10 +2680,10 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
|
|
|
2647
2680
|
catch (err) {
|
|
2648
2681
|
throw new Error("No swap quote found");
|
|
2649
2682
|
}
|
|
2650
|
-
|
|
2683
|
+
await sendWithdrawnCoin(swappedCoin, swappedAmount);
|
|
2651
2684
|
}
|
|
2652
2685
|
else {
|
|
2653
|
-
|
|
2686
|
+
await sendWithdrawnCoin(withdrawnCoin, estimatedWithdrawnAmount);
|
|
2654
2687
|
}
|
|
2655
2688
|
}
|
|
2656
2689
|
else {
|
|
@@ -2686,11 +2719,11 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
|
|
|
2686
2719
|
});
|
|
2687
2720
|
}
|
|
2688
2721
|
// 3.3) Transfer SUI to user
|
|
2689
|
-
|
|
2722
|
+
await sendWithdrawnCoin(suiWithdrawnCoin, BigNumber.max(0, getRedeemedSuiAmount(estimatedWithdrawnAmount, lst).minus(flashLoan?.borrowedAmount ?? 0)));
|
|
2690
2723
|
}
|
|
2691
2724
|
else {
|
|
2692
2725
|
// 3.1) Transfer LST to user
|
|
2693
|
-
|
|
2726
|
+
await sendWithdrawnCoin(withdrawnCoin, estimatedWithdrawnAmount);
|
|
2694
2727
|
}
|
|
2695
2728
|
}
|
|
2696
2729
|
// 4) Claim rewards and transfer to user
|
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))
|