@suilend/sdk 12.3.0 → 12.4.0-next.gd4b87be

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.
@@ -58,6 +58,7 @@ export interface ClosePoolRewardArgs {
58
58
  }
59
59
  export declare function closePoolReward(tx: Transaction, typeArgs: [string, string], args: ClosePoolRewardArgs): import("@mysten/sui/transactions").TransactionResult;
60
60
  export declare function reserve(tx: Transaction, typeArgs: [string, string], lendingMarket: TransactionObjectInput): import("@mysten/sui/transactions").TransactionResult;
61
+ export declare function reserveConfigBuilder(tx: Transaction, typeArgs: [string, string], lendingMarket: TransactionObjectInput): import("@mysten/sui/transactions").TransactionResult;
61
62
  export interface CompoundInterestArgs {
62
63
  lendingMarket: TransactionObjectInput;
63
64
  reserveArrayIndex: bigint | TransactionArgument;
@@ -107,6 +107,17 @@ export function reserve(tx, typeArgs, lendingMarket) {
107
107
  arguments: [obj(tx, lendingMarket)],
108
108
  });
109
109
  }
110
+ // lending_market::reserve_config_builder has no published package to
111
+ // regenerate a binding from yet, so this is hand-written to match the shape
112
+ // codegen already produces for `reserve` above. Replace it with the real
113
+ // generated output once this package publishes and the generator runs again.
114
+ export function reserveConfigBuilder(tx, typeArgs, lendingMarket) {
115
+ return tx.moveCall({
116
+ target: `${PUBLISHED_AT}::lending_market::reserve_config_builder`,
117
+ typeArguments: typeArgs,
118
+ arguments: [obj(tx, lendingMarket)],
119
+ });
120
+ }
110
121
  export function compoundInterest(tx, typeArg, args) {
111
122
  return tx.moveCall({
112
123
  target: `${PUBLISHED_AT}::lending_market::compound_interest`,
@@ -13,6 +13,11 @@ export interface SetSpreadFeeBpsArgs {
13
13
  spreadFeeBps: bigint | TransactionArgument;
14
14
  }
15
15
  export declare function setSpreadFeeBps(tx: Transaction, args: SetSpreadFeeBpsArgs): import("@mysten/sui/transactions").TransactionResult;
16
+ export interface SetConfidenceMultiplierBuilderArgs {
17
+ builder: TransactionObjectInput;
18
+ confidenceMultiplierBps: bigint | TransactionArgument;
19
+ }
20
+ export declare function setConfidenceMultiplierBuilder(tx: Transaction, args: SetConfidenceMultiplierBuilderArgs): import("@mysten/sui/transactions").TransactionResult;
16
21
  export declare function spreadFee(tx: Transaction, config: TransactionObjectInput): import("@mysten/sui/transactions").TransactionResult;
17
22
  export declare function borrowFee(tx: Transaction, config: TransactionObjectInput): import("@mysten/sui/transactions").TransactionResult;
18
23
  export declare function borrowLimit(tx: Transaction, config: TransactionObjectInput): import("@mysten/sui/transactions").TransactionResult;
@@ -29,6 +29,21 @@ export function setSpreadFeeBps(tx, args) {
29
29
  arguments: [obj(tx, args.builder), pure(tx, args.spreadFeeBps, `u64`)],
30
30
  });
31
31
  }
32
+ // reserve_config::set_confidence_multiplier_builder is published (v25), but
33
+ // this SDK's PUBLISHED_AT/PKG_V* constants haven't been bumped to v25 yet, so
34
+ // there's nothing to regenerate a binding from until that separate regen
35
+ // pass happens. Hand-written to match the shape codegen already produces for
36
+ // every other u64 builder setter in this file (e.g. setSpreadFeeBps above).
37
+ // Replace it with the real generated output once that regen lands.
38
+ export function setConfidenceMultiplierBuilder(tx, args) {
39
+ return tx.moveCall({
40
+ target: `${PUBLISHED_AT}::reserve_config::set_confidence_multiplier_builder`,
41
+ arguments: [
42
+ obj(tx, args.builder),
43
+ pure(tx, args.confidenceMultiplierBps, `u64`),
44
+ ],
45
+ });
46
+ }
32
47
  export function spreadFee(tx, config) {
33
48
  return tx.moveCall({
34
49
  target: `${PUBLISHED_AT}::reserve_config::spread_fee`,
package/client.d.ts CHANGED
@@ -1,13 +1,22 @@
1
1
  import { SuiClientTypes } from "@mysten/sui/client";
2
2
  import { SuiGrpcClient } from "@mysten/sui/grpc";
3
- import { Transaction, TransactionObjectArgument, TransactionObjectInput } from "@mysten/sui/transactions";
3
+ import { Transaction, TransactionArgument, TransactionObjectArgument, TransactionObjectInput } from "@mysten/sui/transactions";
4
4
  import { SuiPythClient } from "@pythnetwork/pyth-sui-js";
5
+ import BigNumber from "bignumber.js";
5
6
  import { LendingMarket, ObligationOwnerCap } from "./_generated/suilend/lending-market/structs";
6
7
  import { Obligation } from "./_generated/suilend/obligation/structs";
7
8
  import { NewConfigArgs as CreateRateLimiterConfigArgs } from "./_generated/suilend/rate-limiter/functions";
8
9
  import { CreateReserveConfigArgs } from "./_generated/suilend/reserve-config/functions";
9
10
  import { type PriceUpdateDataSource } from "./lib/pyth";
10
11
  import { Side } from "./lib/types";
12
+ /**
13
+ * Signed Pyth update data the caller already fetched for `priceIds`, posted
14
+ * unconditionally instead of the on-chain staleness check deciding.
15
+ */
16
+ export interface PriceUpdate {
17
+ priceIds: string[];
18
+ updateData: Buffer[];
19
+ }
11
20
  export declare function needsPriceRefresh(stalenessSeconds: number): boolean;
12
21
  export declare const ADMIN_ADDRESS: string;
13
22
  export declare const LENDING_MARKET_REGISTRY_ID: string;
@@ -41,6 +50,9 @@ export type ClaimRewardsReward = {
41
50
  rewardCoinType: string;
42
51
  side: Side;
43
52
  };
53
+ export type PatchReserveConfigArgs = Partial<CreateReserveConfigArgs> & {
54
+ confidenceMultiplierBps?: bigint | TransactionArgument;
55
+ };
44
56
  export declare class SuilendClient {
45
57
  lendingMarket: LendingMarket<string>;
46
58
  suiGrpcClient: SuiGrpcClient;
@@ -71,6 +83,8 @@ export declare class SuilendClient {
71
83
  receivers: string[];
72
84
  weights: string[];
73
85
  }>;
86
+ /** A reserve's confidence multiplier `k`, in bps; pass `parsedReserve.config.additionalFields.id`. 0 when unset. */
87
+ static getReserveConfidenceMultiplierBps(suiGrpcClient: SuiGrpcClient, additionalFieldsBagId: string): Promise<BigNumber>;
74
88
  static createNewLendingMarket(registryId: string, lendingMarketType: string, transaction: Transaction): {
75
89
  NestedResult: [number, number];
76
90
  $kind: "NestedResult";
@@ -94,12 +108,19 @@ export declare class SuilendClient {
94
108
  claimRewardsAndSendToUser(ownerId: string, obligationOwnerCap: TransactionObjectInput, rewards: ClaimRewardsReward[], transaction: Transaction): Transaction;
95
109
  claimRewardsAndDeposit(ownerId: string, obligationOwnerCap: TransactionObjectInput, rewards: ClaimRewardsReward[], transaction: Transaction): Transaction;
96
110
  findReserveArrayIndex(coinType: string): bigint;
111
+ private submitReserveConfigUpdate;
97
112
  updateReserveConfig(lendingMarketOwnerCapId: string, transaction: Transaction, coinType: string, createReserveConfigArgs: CreateReserveConfigArgs): import("@mysten/sui/transactions").TransactionResult;
113
+ /**
114
+ * Updates a subset of a reserve's config fields, leaving every unspecified
115
+ * field -- including `additionalFields` entries like the confidence
116
+ * multiplier -- unchanged from the reserve's current on-chain value.
117
+ */
118
+ patchReserveConfig(lendingMarketOwnerCapId: string, transaction: Transaction, coinType: string, overrides: PatchReserveConfigArgs): import("@mysten/sui/transactions").TransactionResult;
98
119
  newObligationOwnerCap(transaction: Transaction, lendingMarketOwnerCapId: string, destinationAddress: string, obligationId: string): void;
99
120
  updateRateLimiterConfig(lendingMarketOwnerCapId: string, transaction: Transaction, newRateLimiterConfigArgs: CreateRateLimiterConfigArgs): import("@mysten/sui/transactions").TransactionResult;
100
121
  changeReservePriceFeed(lendingMarketOwnerCapId: string, coinType: string, pythPriceId: string, transaction: Transaction): Promise<import("@mysten/sui/transactions").TransactionResult>;
101
122
  createObligation(transaction: Transaction): import("@mysten/sui/transactions").TransactionResult;
102
- refreshAll(transaction: Transaction, obligation?: Obligation<string>, coinTypes?: string[]): Promise<void>;
123
+ refreshAll(transaction: Transaction, obligation?: Obligation<string>, coinTypes?: string[], priceUpdate?: PriceUpdate): Promise<void>;
103
124
  refreshReservePrices(transaction: Transaction, priceInfoObjectId: string, reserveArrayIndex: bigint): Promise<void>;
104
125
  deposit(sendCoin: TransactionObjectInput, coinType: string, obligationOwnerCap: TransactionObjectInput, transaction: Transaction): void;
105
126
  depositIntoObligation(ownerId: string, coinType: string, value: string, transaction: Transaction, obligationOwnerCap: TransactionObjectInput): Promise<void>;
@@ -111,8 +132,8 @@ export declare class SuilendClient {
111
132
  borrowAndSendToUser(ownerId: string, obligationOwnerCap: TransactionObjectInput, obligationId: string, coinType: string, value: string, transaction: Transaction): Promise<void>;
112
133
  repay(obligationId: string, coinType: string, coin: TransactionObjectInput, transaction: Transaction): import("@mysten/sui/transactions").TransactionResult;
113
134
  repayIntoObligation(ownerId: string, obligationId: string, coinType: string, value: string, transaction: Transaction): Promise<import("@mysten/sui/transactions").TransactionResult>;
114
- liquidateAndRedeem(transaction: Transaction, obligation: Obligation<string>, repayCoinType: string, withdrawCoinType: string, repayCoinId: TransactionObjectInput): Promise<import("@mysten/sui/transactions").TransactionResult>;
115
- liquidate(transaction: Transaction, obligation: Obligation<string>, repayCoinType: string, withdrawCoinType: string, repayCoinId: TransactionObjectInput): Promise<import("@mysten/sui/transactions").TransactionResult>;
135
+ liquidateAndRedeem(transaction: Transaction, obligation: Obligation<string>, repayCoinType: string, withdrawCoinType: string, repayCoinId: TransactionObjectInput, priceUpdate?: PriceUpdate): Promise<import("@mysten/sui/transactions").TransactionResult>;
136
+ liquidate(transaction: Transaction, obligation: Obligation<string>, repayCoinType: string, withdrawCoinType: string, repayCoinId: TransactionObjectInput, priceUpdate?: PriceUpdate): Promise<import("@mysten/sui/transactions").TransactionResult>;
116
137
  migrate(transaction: Transaction, lendingMarketOwnerCapId: string): import("@mysten/sui/transactions").TransactionResult;
117
138
  claimFees(transaction: Transaction, coinType: string): import("@mysten/sui/transactions").TransactionResult;
118
139
  setFeeReceiversAndWeights(transaction: Transaction, lendingMarketOwnerCapId: string, receivers: string[], weights: bigint[]): import("@mysten/sui/transactions").TransactionResult;
package/client.js CHANGED
@@ -1,15 +1,17 @@
1
+ import { bcs } from "@mysten/sui/bcs";
1
2
  import { SUI_CLOCK_OBJECT_ID, SUI_SYSTEM_STATE_OBJECT_ID, normalizeStructTag, normalizeSuiObjectId, toHex, } from "@mysten/sui/utils";
2
3
  import { SuiPythClient } from "@pythnetwork/pyth-sui-js";
4
+ import BigNumber from "bignumber.js";
3
5
  import { extractCTokenCoinType, getAllCoins, getSpendableCoin, } from "@suilend/sui-core";
4
6
  import { phantom } from "./_generated/_framework/reified.js";
5
7
  import { PKG_V10, setPublishedAt } from "./_generated/suilend/index.js";
6
8
  import { PACKAGE_ID, PUBLISHED_AT } from "./_generated/suilend/index.js";
7
- import { addPoolReward, borrowRequest, cancelPoolReward, claimFees, claimRewards, claimRewardsAndDeposit, closePoolReward, depositCtokensIntoObligation, depositLiquidityAndMintCtokens, fulfillLiquidityRequest, liquidate, migrate, newObligationOwnerCap, rebalanceStaker, redeemCtokensAndWithdrawLiquidityRequest, repay, setFeeReceivers, unstakeSuiFromStaker, updateRateLimiterConfig, updateReserveConfig, withdrawCtokens, } from "./_generated/suilend/lending-market/functions.js";
9
+ import { addPoolReward, borrowRequest, cancelPoolReward, claimFees, claimRewards, claimRewardsAndDeposit, closePoolReward, depositCtokensIntoObligation, depositLiquidityAndMintCtokens, fulfillLiquidityRequest, liquidate, migrate, newObligationOwnerCap, rebalanceStaker, redeemCtokensAndWithdrawLiquidityRequest, repay, reserveConfigBuilder, setFeeReceivers, unstakeSuiFromStaker, updateRateLimiterConfig, updateReserveConfig, withdrawCtokens, } from "./_generated/suilend/lending-market/functions.js";
8
10
  import { LendingMarket, ObligationOwnerCap, } from "./_generated/suilend/lending-market/structs.js";
9
11
  import { createLendingMarket } from "./_generated/suilend/lending-market-registry/functions.js";
10
12
  import { Obligation } from "./_generated/suilend/obligation/structs.js";
11
13
  import { newConfig as createRateLimiterConfig, } from "./_generated/suilend/rate-limiter/functions.js";
12
- import { createReserveConfig, } from "./_generated/suilend/reserve-config/functions.js";
14
+ import { build as buildReserveConfig, createReserveConfig, setBorrowFeeBps, setBorrowLimit, setBorrowLimitUsd, setBorrowWeightBps, setCloseAttributedBorrowLimitUsd, setCloseLtvPct, setConfidenceMultiplierBuilder, setDepositLimit, setDepositLimitUsd, setInterestRateAprs, setInterestRateUtils, setIsolated, setLiquidationBonusBps, setMaxCloseLtvPct, setMaxLiquidationBonusBps, setOpenAttributedBorrowLimitUsd, setOpenLtvPct, setProtocolLiquidationFeeBps, setSpreadFeeBps, } from "./_generated/suilend/reserve-config/functions.js";
13
15
  import { addReserveProCompatible, addReserveV2ProCompatible, changeReservePriceFeedProCompatible, priceInfoPublishTime, refreshReservePriceProCompatible, } from "./lib/proCompatible.js";
14
16
  import { resolvePriceUpdateDataSource, } from "./lib/pyth.js";
15
17
  import { Side } from "./lib/types.js";
@@ -28,6 +30,7 @@ const isSui = (coinType) => normalizeStructTag(coinType) === NORMALIZED_SUI_COIN
28
30
  // refresh would come from a package the contract does not accept.
29
31
  const WORMHOLE_STATE_ID = "0xdbca52b9fb4f712e25f61f974586d93ac541bcf8389564f0323bb07215168b5c";
30
32
  const PYTH_STATE_ID = "0x03719fae774ddab3cfcaa53bbc046f0cbe21410019b6280811bf3f9f4b05839d";
33
+ const normalizePriceIdentifier = (id) => id.toLowerCase().replace(/^0x/, "");
31
34
  // Mirrors suilend::oracles's MAX_FORWARD_SECONDS (move/contracts/suilend/sources/oracles.move);
32
35
  // `refresh_reserve_price` now rejects a forward-dated price the same as a stale one.
33
36
  const MAX_FORWARD_SECONDS = 60;
@@ -153,6 +156,87 @@ export async function listAllOwnedObjects(suiGrpcClient, owner, type, include) {
153
156
  cache.set(key, promise);
154
157
  return promise;
155
158
  }
159
+ // One entry per CreateReserveConfigArgs field, each just closing over its
160
+ // generated setter -- lets patchReserveConfig apply only the caller's
161
+ // specified overrides with one loop instead of one copy-pasted
162
+ // `if (overrides.x !== undefined) setX(...)` per field.
163
+ const CREATE_RESERVE_CONFIG_FIELD_SETTERS = {
164
+ openLtvPct: (tx, builder, openLtvPct) => setOpenLtvPct(tx, { builder, openLtvPct }),
165
+ closeLtvPct: (tx, builder, closeLtvPct) => setCloseLtvPct(tx, { builder, closeLtvPct }),
166
+ maxCloseLtvPct: (tx, builder, maxCloseLtvPct) => setMaxCloseLtvPct(tx, { builder, maxCloseLtvPct }),
167
+ borrowWeightBps: (tx, builder, borrowWeightBps) => setBorrowWeightBps(tx, { builder, borrowWeightBps }),
168
+ depositLimit: (tx, builder, depositLimit) => setDepositLimit(tx, { builder, depositLimit }),
169
+ borrowLimit: (tx, builder, borrowLimit) => setBorrowLimit(tx, { builder, borrowLimit }),
170
+ liquidationBonusBps: (tx, builder, liquidationBonusBps) => setLiquidationBonusBps(tx, { builder, liquidationBonusBps }),
171
+ maxLiquidationBonusBps: (tx, builder, maxLiquidationBonusBps) => setMaxLiquidationBonusBps(tx, { builder, maxLiquidationBonusBps }),
172
+ depositLimitUsd: (tx, builder, depositLimitUsd) => setDepositLimitUsd(tx, { builder, depositLimitUsd }),
173
+ borrowLimitUsd: (tx, builder, borrowLimitUsd) => setBorrowLimitUsd(tx, { builder, borrowLimitUsd }),
174
+ borrowFeeBps: (tx, builder, borrowFeeBps) => setBorrowFeeBps(tx, { builder, borrowFeeBps }),
175
+ spreadFeeBps: (tx, builder, spreadFeeBps) => setSpreadFeeBps(tx, { builder, spreadFeeBps }),
176
+ protocolLiquidationFeeBps: (tx, builder, protocolLiquidationFeeBps) => setProtocolLiquidationFeeBps(tx, { builder, protocolLiquidationFeeBps }),
177
+ interestRateUtils: (tx, builder, interestRateUtils) => setInterestRateUtils(tx, { builder, interestRateUtils }),
178
+ interestRateAprs: (tx, builder, interestRateAprs) => setInterestRateAprs(tx, { builder, interestRateAprs }),
179
+ isolated: (tx, builder, isolated) => setIsolated(tx, { builder, isolated }),
180
+ openAttributedBorrowLimitUsd: (tx, builder, openAttributedBorrowLimitUsd) => setOpenAttributedBorrowLimitUsd(tx, {
181
+ builder,
182
+ openAttributedBorrowLimitUsd,
183
+ }),
184
+ closeAttributedBorrowLimitUsd: (tx, builder, closeAttributedBorrowLimitUsd) => setCloseAttributedBorrowLimitUsd(tx, {
185
+ builder,
186
+ closeAttributedBorrowLimitUsd,
187
+ }),
188
+ };
189
+ // A generic type parameter, unlike indexing CREATE_RESERVE_CONFIG_FIELD_SETTERS
190
+ // with the union `keyof CreateReserveConfigArgs` directly at the call site,
191
+ // keeps TS from expanding the call into one union branch per field (which
192
+ // blows up into "union type too complex to represent").
193
+ function applyCreateReserveConfigFieldOverride(tx, builder, overrides, key) {
194
+ const value = overrides[key];
195
+ if (value === undefined)
196
+ return;
197
+ // TS can't verify this call is sound (calling a mapped type indexed by a
198
+ // generic key produces a union of all 18 setter signatures, which blows up
199
+ // into "union type too complex to represent" rather than typechecking) --
200
+ // but it is: `value`'s type and CREATE_RESERVE_CONFIG_FIELD_SETTERS[key]'s
201
+ // parameter type are both CreateReserveConfigArgs[K] for the same K.
202
+ CREATE_RESERVE_CONFIG_FIELD_SETTERS[key](tx, builder, value);
203
+ }
204
+ // Only safe for a key with a fixed, known type tag; see findBagEntryByTypeSuffix
205
+ // for one that can legitimately be absent.
206
+ async function resolveDynamicFieldId(suiGrpcClient, parentId, nameType) {
207
+ const { dynamicField } = await suiGrpcClient.getDynamicField({
208
+ parentId,
209
+ name: { type: nameType, bcs: new Uint8Array([0]) },
210
+ });
211
+ return dynamicField.fieldId;
212
+ }
213
+ // For a key that's always present (getFeeReceivers), both calls' errors can
214
+ // propagate through this combined helper unguarded.
215
+ async function fetchDynamicFieldJson(suiGrpcClient, parentId, nameType) {
216
+ const fieldId = await resolveDynamicFieldId(suiGrpcClient, parentId, nameType);
217
+ const { object } = await suiGrpcClient.getObject({
218
+ objectId: fieldId,
219
+ include: { json: true },
220
+ });
221
+ return object.json;
222
+ }
223
+ // Scans by type-tag suffix, ignoring the package address: codegen doesn't
224
+ // track a struct's originally-defining address, only the current PUBLISHED_AT.
225
+ async function findBagEntryByTypeSuffix(suiGrpcClient, parentId, typeSuffix) {
226
+ let cursor = null;
227
+ while (true) {
228
+ const response = await suiGrpcClient.listDynamicFields({ parentId, cursor });
229
+ const match = response.dynamicFields.find((entry) => entry.name.type.endsWith(typeSuffix));
230
+ if (match)
231
+ return match;
232
+ if (!response.hasNextPage)
233
+ return undefined;
234
+ if (!response.cursor) {
235
+ throw new Error(`listDynamicFields for ${parentId} reported hasNextPage=true with no cursor to continue`);
236
+ }
237
+ cursor = response.cursor;
238
+ }
239
+ }
156
240
  export class SuilendClient {
157
241
  lendingMarket;
158
242
  suiGrpcClient;
@@ -249,22 +333,22 @@ export class SuilendClient {
249
333
  });
250
334
  }
251
335
  static async getFeeReceivers(suiGrpcClient, lendingMarketId) {
252
- const { dynamicField } = await suiGrpcClient.getDynamicField({
253
- parentId: lendingMarketId,
254
- name: {
255
- type: `${PKG_V10}::lending_market::FeeReceiversKey`,
256
- bcs: new Uint8Array([0]),
257
- },
258
- });
259
- const { object } = await suiGrpcClient.getObject({
260
- objectId: dynamicField.fieldId,
261
- include: { json: true },
262
- });
263
- const json = object.json;
336
+ const json = await fetchDynamicFieldJson(suiGrpcClient, lendingMarketId, `${PKG_V10}::lending_market::FeeReceiversKey`);
264
337
  const receivers = json?.value?.receivers;
265
338
  const weights = json?.value?.weights;
266
339
  return { receivers, weights };
267
340
  }
341
+ /** A reserve's confidence multiplier `k`, in bps; pass `parsedReserve.config.additionalFields.id`. 0 when unset. */
342
+ static async getReserveConfidenceMultiplierBps(suiGrpcClient, additionalFieldsBagId) {
343
+ const match = await findBagEntryByTypeSuffix(suiGrpcClient, additionalFieldsBagId, "::reserve_config::ConfidenceMultiplierKey");
344
+ if (!match)
345
+ return new BigNumber(0);
346
+ const { dynamicField } = await suiGrpcClient.getDynamicField({
347
+ parentId: additionalFieldsBagId,
348
+ name: match.name,
349
+ });
350
+ return new BigNumber(bcs.u64().parse(dynamicField.value.bcs));
351
+ }
268
352
  static createNewLendingMarket(registryId, lendingMarketType, transaction) {
269
353
  const [ownerCap, lendingMarket] = createLendingMarket(transaction, lendingMarketType, transaction.object(registryId));
270
354
  transaction.moveCall({
@@ -429,8 +513,7 @@ export class SuilendClient {
429
513
  const arrayIndex = this.lendingMarket.reserves.findIndex((r) => normalizeStructTag(r.coinType.name) === normalizeStructTag(coinType));
430
514
  return BigInt(arrayIndex);
431
515
  }
432
- updateReserveConfig(lendingMarketOwnerCapId, transaction, coinType, createReserveConfigArgs) {
433
- const [config] = createReserveConfig(transaction, createReserveConfigArgs);
516
+ submitReserveConfigUpdate(lendingMarketOwnerCapId, transaction, coinType, config) {
434
517
  return updateReserveConfig(transaction, [this.lendingMarket.$typeArgs[0], coinType], {
435
518
  lendingMarketOwnerCap: transaction.object(lendingMarketOwnerCapId),
436
519
  lendingMarket: transaction.object(this.lendingMarket.id),
@@ -438,6 +521,28 @@ export class SuilendClient {
438
521
  config: transaction.object(config),
439
522
  });
440
523
  }
524
+ updateReserveConfig(lendingMarketOwnerCapId, transaction, coinType, createReserveConfigArgs) {
525
+ const [config] = createReserveConfig(transaction, createReserveConfigArgs);
526
+ return this.submitReserveConfigUpdate(lendingMarketOwnerCapId, transaction, coinType, config);
527
+ }
528
+ /**
529
+ * Updates a subset of a reserve's config fields, leaving every unspecified
530
+ * field -- including `additionalFields` entries like the confidence
531
+ * multiplier -- unchanged from the reserve's current on-chain value.
532
+ */
533
+ patchReserveConfig(lendingMarketOwnerCapId, transaction, coinType, overrides) {
534
+ const [builder] = reserveConfigBuilder(transaction, [this.lendingMarket.$typeArgs[0], coinType], transaction.object(this.lendingMarket.id));
535
+ for (const key of Object.keys(CREATE_RESERVE_CONFIG_FIELD_SETTERS)) {
536
+ applyCreateReserveConfigFieldOverride(transaction, builder, overrides, key);
537
+ }
538
+ if (overrides.confidenceMultiplierBps !== undefined)
539
+ setConfidenceMultiplierBuilder(transaction, {
540
+ builder,
541
+ confidenceMultiplierBps: overrides.confidenceMultiplierBps,
542
+ });
543
+ const [config] = buildReserveConfig(transaction, builder);
544
+ return this.submitReserveConfigUpdate(lendingMarketOwnerCapId, transaction, coinType, config);
545
+ }
441
546
  newObligationOwnerCap(transaction, lendingMarketOwnerCapId, destinationAddress, obligationId) {
442
547
  const [obligationOwnerCap] = newObligationOwnerCap(transaction, this.lendingMarket.$typeArgs[0], {
443
548
  lendingMarketOwnerCap: transaction.object(lendingMarketOwnerCapId),
@@ -475,7 +580,7 @@ export class SuilendClient {
475
580
  typeArguments: this.lendingMarket.$typeArgs,
476
581
  });
477
582
  }
478
- async refreshAll(transaction, obligation, coinTypes) {
583
+ async refreshAll(transaction, obligation, coinTypes, priceUpdate) {
479
584
  const reserveArrayIndexToPriceId = new Map();
480
585
  if (obligation) {
481
586
  obligation.deposits.forEach((deposit) => {
@@ -505,8 +610,14 @@ export class SuilendClient {
505
610
  const priceInfoObjectId = await this.pythClient.getPriceFeedObjectId(priceIdentifiers[i]);
506
611
  priceInfoObjectIds[i] = priceInfoObjectId;
507
612
  })()));
508
- const stalePriceIdentifiers = [];
509
- await Promise.all(Object.entries(priceInfoObjectIds).map(([i, priceInfoObjectId]) => (async () => {
613
+ const suppliedPriceIdentifiers = new Set(priceUpdate?.priceIds.map(normalizePriceIdentifier));
614
+ if (priceUpdate?.priceIds.length) {
615
+ await this.pythClient.updatePriceFeeds(transaction, priceUpdate.updateData, priceUpdate.priceIds);
616
+ }
617
+ const stalePriceIdentifiers = new Set();
618
+ await Promise.all(Object.entries(priceInfoObjectIds)
619
+ .filter(([i]) => !suppliedPriceIdentifiers.has(normalizePriceIdentifier(priceIdentifiers[+i])))
620
+ .map(([i, priceInfoObjectId]) => (async () => {
510
621
  // Read the field directly: the generated parser's type check is
511
622
  // bound to one Pyth package address and would throw post-cutover.
512
623
  const { object } = await this.suiGrpcClient.getObject({
@@ -516,15 +627,16 @@ export class SuilendClient {
516
627
  const publishTime = priceInfoPublishTime(object.json, priceInfoObjectId);
517
628
  const stalenessSeconds = Date.now() / 1000 - Number(publishTime);
518
629
  if (needsPriceRefresh(stalenessSeconds)) {
519
- stalePriceIdentifiers.push(priceIdentifiers[+i]);
630
+ stalePriceIdentifiers.add(priceIdentifiers[+i]);
520
631
  }
521
632
  })()));
522
- if (stalePriceIdentifiers.length > 0) {
523
- const stalePriceUpdateData = await this.pythConnection.getPriceFeedsUpdateData(stalePriceIdentifiers);
524
- await this.pythClient.updatePriceFeeds(transaction, stalePriceUpdateData, stalePriceIdentifiers);
633
+ if (stalePriceIdentifiers.size > 0) {
634
+ const staleIds = [...stalePriceIdentifiers];
635
+ const stalePriceUpdateData = await this.pythConnection.getPriceFeedsUpdateData(staleIds);
636
+ await this.pythClient.updatePriceFeeds(transaction, stalePriceUpdateData, staleIds);
525
637
  }
526
638
  for (let i = 0; i < reserveArrayIndexes.length; i++) {
527
- this.refreshReservePrices(transaction, priceInfoObjectIds[i], reserveArrayIndexes[i]);
639
+ await this.refreshReservePrices(transaction, priceInfoObjectIds[i], reserveArrayIndexes[i]);
528
640
  }
529
641
  }
530
642
  async refreshReservePrices(transaction, priceInfoObjectId, reserveArrayIndex) {
@@ -670,8 +782,8 @@ export class SuilendClient {
670
782
  transaction.transferObjects([sendCoin], transaction.pure.address(ownerId));
671
783
  return result;
672
784
  }
673
- async liquidateAndRedeem(transaction, obligation, repayCoinType, withdrawCoinType, repayCoinId) {
674
- const [ctokens, exemption] = await this.liquidate(transaction, obligation, repayCoinType, withdrawCoinType, repayCoinId);
785
+ async liquidateAndRedeem(transaction, obligation, repayCoinType, withdrawCoinType, repayCoinId, priceUpdate) {
786
+ const [ctokens, exemption] = await this.liquidate(transaction, obligation, repayCoinType, withdrawCoinType, repayCoinId, priceUpdate);
675
787
  const [optionalExemption] = transaction.moveCall({
676
788
  target: `0x1::option::some`,
677
789
  typeArguments: [
@@ -681,8 +793,8 @@ export class SuilendClient {
681
793
  });
682
794
  return this.redeem(ctokens, withdrawCoinType, optionalExemption, transaction);
683
795
  }
684
- async liquidate(transaction, obligation, repayCoinType, withdrawCoinType, repayCoinId) {
685
- await this.refreshAll(transaction, obligation);
796
+ async liquidate(transaction, obligation, repayCoinType, withdrawCoinType, repayCoinId, priceUpdate) {
797
+ await this.refreshAll(transaction, obligation, undefined, priceUpdate);
686
798
  return liquidate(transaction, [this.lendingMarket.$typeArgs[0], repayCoinType, withdrawCoinType], {
687
799
  lendingMarket: transaction.object(this.lendingMarket.id),
688
800
  obligationId: obligation.id,
@@ -167,6 +167,7 @@ export declare const initializeSuilendAll: (suiGrpcClient: SuiGrpcClient, market
167
167
  isolated: boolean;
168
168
  openAttributedBorrowLimitUsd: number;
169
169
  closeAttributedBorrowLimitUsd: number;
170
+ additionalFields: import("../_generated/_dependencies/source/0x2/bag/structs").Bag;
170
171
  interestRate: {
171
172
  id: string;
172
173
  utilPercent: BigNumber;
@@ -306,6 +307,7 @@ export declare const initializeSuilendAll: (suiGrpcClient: SuiGrpcClient, market
306
307
  isolated: boolean;
307
308
  openAttributedBorrowLimitUsd: number;
308
309
  closeAttributedBorrowLimitUsd: number;
310
+ additionalFields: import("../_generated/_dependencies/source/0x2/bag/structs").Bag;
309
311
  interestRate: {
310
312
  id: string;
311
313
  utilPercent: BigNumber;
@@ -423,6 +425,7 @@ export declare const initializeSuilend: (suiGrpcClient: SuiGrpcClient, suilendCl
423
425
  isolated: boolean;
424
426
  openAttributedBorrowLimitUsd: number;
425
427
  closeAttributedBorrowLimitUsd: number;
428
+ additionalFields: import("../_generated/_dependencies/source/0x2/bag/structs").Bag;
426
429
  interestRate: {
427
430
  id: string;
428
431
  utilPercent: BigNumber;
@@ -562,6 +565,7 @@ export declare const initializeSuilend: (suiGrpcClient: SuiGrpcClient, suilendCl
562
565
  isolated: boolean;
563
566
  openAttributedBorrowLimitUsd: number;
564
567
  closeAttributedBorrowLimitUsd: number;
568
+ additionalFields: import("../_generated/_dependencies/source/0x2/bag/structs").Bag;
565
569
  interestRate: {
566
570
  id: string;
567
571
  utilPercent: BigNumber;
@@ -708,6 +712,7 @@ export declare const initializeObligations: (suiGrpcClient: SuiGrpcClient, suile
708
712
  isolated: boolean;
709
713
  openAttributedBorrowLimitUsd: number;
710
714
  closeAttributedBorrowLimitUsd: number;
715
+ additionalFields: import("../_generated/_dependencies/source/0x2/bag/structs").Bag;
711
716
  interestRate: {
712
717
  id: string;
713
718
  utilPercent: BigNumber;
@@ -825,6 +830,7 @@ export declare const initializeObligations: (suiGrpcClient: SuiGrpcClient, suile
825
830
  isolated: boolean;
826
831
  openAttributedBorrowLimitUsd: number;
827
832
  closeAttributedBorrowLimitUsd: number;
833
+ additionalFields: import("../_generated/_dependencies/source/0x2/bag/structs").Bag;
828
834
  interestRate: {
829
835
  id: string;
830
836
  utilPercent: BigNumber;
@@ -966,6 +972,7 @@ export declare const initializeObligations: (suiGrpcClient: SuiGrpcClient, suile
966
972
  isolated: boolean;
967
973
  openAttributedBorrowLimitUsd: number;
968
974
  closeAttributedBorrowLimitUsd: number;
975
+ additionalFields: import("../_generated/_dependencies/source/0x2/bag/structs").Bag;
969
976
  interestRate: {
970
977
  id: string;
971
978
  utilPercent: BigNumber;
@@ -1083,6 +1090,7 @@ export declare const initializeObligations: (suiGrpcClient: SuiGrpcClient, suile
1083
1090
  isolated: boolean;
1084
1091
  openAttributedBorrowLimitUsd: number;
1085
1092
  closeAttributedBorrowLimitUsd: number;
1093
+ additionalFields: import("../_generated/_dependencies/source/0x2/bag/structs").Bag;
1086
1094
  interestRate: {
1087
1095
  id: string;
1088
1096
  utilPercent: BigNumber;
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"@suilend/sdk","version":"12.3.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/obligation":"./utils/obligation.js","./utils/simulate":"./utils/simulate.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/source/0x1":"./_generated/_dependencies/source/0x1/index.js","./_generated/_dependencies/source/0x2":"./_generated/_dependencies/source/0x2/index.js","./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/index.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","./_generated/_dependencies/source/0x1/ascii/structs":"./_generated/_dependencies/source/0x1/ascii/structs.js","./_generated/_dependencies/source/0x1/option/structs":"./_generated/_dependencies/source/0x1/option/structs.js","./_generated/_dependencies/source/0x1/type-name/structs":"./_generated/_dependencies/source/0x1/type-name/structs.js","./_generated/_dependencies/source/0x2/bag/structs":"./_generated/_dependencies/source/0x2/bag/structs.js","./_generated/_dependencies/source/0x2/balance/structs":"./_generated/_dependencies/source/0x2/balance/structs.js","./_generated/_dependencies/source/0x2/object/structs":"./_generated/_dependencies/source/0x2/object/structs.js","./_generated/_dependencies/source/0x2/object-table/structs":"./_generated/_dependencies/source/0x2/object-table/structs.js","./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-identifier/structs":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-identifier/structs.js"},"types":"./index.d.ts","scripts":{"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":{"@bluefin-exchange/bluefin7k-aggregator-sdk":"7.6.3","@cetusprotocol/aggregator-sdk":"1.7.2","@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.2","uuid":"14.0.2"},"devDependencies":{"@mysten/bcs":"2.1.1","@mysten/sui":"2.29.0","@suilend/sui-core":"2.1.0","@tsconfig/recommended":"1.0.13","@types/bn.js":"5.2.0","@types/lodash":"4.17.25","@types/node":"26.5.1","fast-check":"4.10.0","prettier":"3.9.6","ts-node":"10.9.2","typescript":"6.0.3","vite":"8.3.0","vitest":"5.0.0"},"peerDependencies":{"@mysten/bcs":"^2.1.1","@mysten/sui":">=2.22.1 <3","@suilend/sui-core":"^2.0.1"},"type":"module"}
1
+ {"name":"@suilend/sdk","version":"12.4.0-next.gd4b87be","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/obligation":"./utils/obligation.js","./utils/simulate":"./utils/simulate.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/source/0x1":"./_generated/_dependencies/source/0x1/index.js","./_generated/_dependencies/source/0x2":"./_generated/_dependencies/source/0x2/index.js","./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/index.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","./_generated/_dependencies/source/0x1/ascii/structs":"./_generated/_dependencies/source/0x1/ascii/structs.js","./_generated/_dependencies/source/0x1/option/structs":"./_generated/_dependencies/source/0x1/option/structs.js","./_generated/_dependencies/source/0x1/type-name/structs":"./_generated/_dependencies/source/0x1/type-name/structs.js","./_generated/_dependencies/source/0x2/bag/structs":"./_generated/_dependencies/source/0x2/bag/structs.js","./_generated/_dependencies/source/0x2/balance/structs":"./_generated/_dependencies/source/0x2/balance/structs.js","./_generated/_dependencies/source/0x2/object/structs":"./_generated/_dependencies/source/0x2/object/structs.js","./_generated/_dependencies/source/0x2/object-table/structs":"./_generated/_dependencies/source/0x2/object-table/structs.js","./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-identifier/structs":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-identifier/structs.js"},"types":"./index.d.ts","scripts":{"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":{"@bluefin-exchange/bluefin7k-aggregator-sdk":"7.6.3","@cetusprotocol/aggregator-sdk":"1.7.2","@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.2","uuid":"14.0.2"},"devDependencies":{"@mysten/bcs":"2.1.1","@mysten/sui":"2.29.0","@suilend/sui-core":"2.1.0","@tsconfig/recommended":"1.0.13","@types/bn.js":"5.2.0","@types/lodash":"4.17.25","@types/node":"26.5.1","fast-check":"4.10.0","prettier":"3.9.6","ts-node":"10.9.2","typescript":"6.0.3","vite":"8.3.0","vitest":"5.0.0"},"peerDependencies":{"@mysten/bcs":"^2.1.1","@mysten/sui":">=2.22.1 <3","@suilend/sui-core":"^2.0.1"},"type":"module"}
@@ -25,6 +25,7 @@ export declare const parseLendingMarket: (lendingMarket: LendingMarket<string>,
25
25
  isolated: boolean;
26
26
  openAttributedBorrowLimitUsd: number;
27
27
  closeAttributedBorrowLimitUsd: number;
28
+ additionalFields: import("../_generated/_dependencies/source/0x2/bag/structs").Bag;
28
29
  interestRate: {
29
30
  id: string;
30
31
  utilPercent: BigNumber;
@@ -45,6 +45,7 @@ export declare const parseObligation: (obligation: Obligation<string>, parsedRes
45
45
  isolated: boolean;
46
46
  openAttributedBorrowLimitUsd: number;
47
47
  closeAttributedBorrowLimitUsd: number;
48
+ additionalFields: import("../_generated/_dependencies/source/0x2/bag/structs").Bag;
48
49
  interestRate: {
49
50
  id: string;
50
51
  utilPercent: BigNumber;
@@ -162,6 +163,7 @@ export declare const parseObligation: (obligation: Obligation<string>, parsedRes
162
163
  isolated: boolean;
163
164
  openAttributedBorrowLimitUsd: number;
164
165
  closeAttributedBorrowLimitUsd: number;
166
+ additionalFields: import("../_generated/_dependencies/source/0x2/bag/structs").Bag;
165
167
  interestRate: {
166
168
  id: string;
167
169
  utilPercent: BigNumber;
@@ -25,6 +25,7 @@ export declare const parseReserve: (reserve: Reserve<string>, coinMetadataMap: R
25
25
  isolated: boolean;
26
26
  openAttributedBorrowLimitUsd: number;
27
27
  closeAttributedBorrowLimitUsd: number;
28
+ additionalFields: import("../_generated/_dependencies/source/0x2/bag/structs").Bag;
28
29
  interestRate: {
29
30
  id: string;
30
31
  utilPercent: BigNumber;
@@ -147,6 +148,7 @@ export declare const parseReserveConfig: (reserve: Reserve<string>) => {
147
148
  isolated: boolean;
148
149
  openAttributedBorrowLimitUsd: number;
149
150
  closeAttributedBorrowLimitUsd: number;
151
+ additionalFields: import("../_generated/_dependencies/source/0x2/bag/structs").Bag;
150
152
  interestRate: {
151
153
  id: string;
152
154
  utilPercent: BigNumber;
@@ -122,7 +122,7 @@ export const parseReserveConfig = (reserve) => {
122
122
  const isolated = config.isolated;
123
123
  const openAttributedBorrowLimitUsd = Number(config.openAttributedBorrowLimitUsd.toString());
124
124
  const closeAttributedBorrowLimitUsd = Number(config.closeAttributedBorrowLimitUsd.toString());
125
- // additionalFields
125
+ const additionalFields = config.additionalFields;
126
126
  const interestRate = config.interestRateUtils.map((util, index) => ({
127
127
  id: uuidv4(),
128
128
  utilPercent: new BigNumber(util.toString()),
@@ -146,10 +146,12 @@ export const parseReserveConfig = (reserve) => {
146
146
  isolated,
147
147
  openAttributedBorrowLimitUsd,
148
148
  closeAttributedBorrowLimitUsd,
149
- // additionalFields,
149
+ additionalFields,
150
150
  interestRate,
151
151
  };
152
152
  };
153
+ // additionalFields is a Bag; read the confidence multiplier via
154
+ // SuilendClient.getReserveConfidenceMultiplierBps(suiGrpcClient, additionalFields.id).
153
155
  export const parsePoolRewardManager = (poolRewardManager, coinMetadataMap) => {
154
156
  const $typeName = poolRewardManager.$typeName;
155
157
  const id = poolRewardManager.id;
package/strategies.d.ts CHANGED
@@ -12,6 +12,7 @@ import { StrategyType } from "./lib/strategyOwnerCap";
12
12
  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
+ 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.";
15
16
  /**
16
17
  * Below this, a levered position is uneconomic no matter what the slippage
17
18
  * tolerance says. The aggregator's dust regime dominates: routes go multi-hop
@@ -73,6 +74,7 @@ export declare const getStrategySuiReserve: (reserveMap: Record<string, ParsedRe
73
74
  isolated: boolean;
74
75
  openAttributedBorrowLimitUsd: number;
75
76
  closeAttributedBorrowLimitUsd: number;
77
+ additionalFields: import("./_generated/_dependencies/source/0x2/bag/structs").Bag;
76
78
  interestRate: {
77
79
  id: string;
78
80
  utilPercent: BigNumber;
@@ -202,6 +204,7 @@ export declare const getStrategyDefaultCurrencyReserve: (reserveMap: Record<stri
202
204
  isolated: boolean;
203
205
  openAttributedBorrowLimitUsd: number;
204
206
  closeAttributedBorrowLimitUsd: number;
207
+ additionalFields: import("./_generated/_dependencies/source/0x2/bag/structs").Bag;
205
208
  interestRate: {
206
209
  id: string;
207
210
  utilPercent: BigNumber;
@@ -297,7 +300,8 @@ export declare const getStrategyBorrowedAmount: (reserveMap: Record<string, Pars
297
300
  export declare const getStrategyTvlAmount: (reserveMap: Record<string, ParsedReserve>, lstMap: StrategyLstMap, strategyType: StrategyType, obligation?: ParsedObligation) => BigNumber;
298
301
  export declare const getStrategyExposure: (reserveMap: Record<string, ParsedReserve>, lstMap: StrategyLstMap, strategyType: StrategyType, obligation?: ParsedObligation) => BigNumber;
299
302
  export declare const getStrategyStepMaxBorrowedAmount: (reserveMap: Record<string, ParsedReserve>, lstMap: StrategyLstMap, strategyType: StrategyType, deposits: StrategyDeposit[], borrowedAmount: BigNumber) => BigNumber;
300
- export declare const getStrategyStepMaxWithdrawnAmount: (reserveMap: Record<string, ParsedReserve>, lstMap: StrategyLstMap, strategyType: StrategyType, deposits: StrategyDeposit[], borrowedAmount: BigNumber, withdrawCoinType: string) => BigNumber;
303
+ export declare const getStrategyStepMaxWithdrawnAmount: (reserveMap: Record<string, ParsedReserve>, lstMap: StrategyLstMap, strategyType: StrategyType, deposits: StrategyDeposit[], borrowedAmount: BigNumber, withdrawCoinType: string, headroomBuffer?: number) => BigNumber;
304
+ export declare const getStrategyRebalanceDepositAmount: (reserveMap: Record<string, ParsedReserve>, lstMap: StrategyLstMap, strategyType: StrategyType, deposits: StrategyDeposit[], borrowedAmount: BigNumber) => BigNumber;
301
305
  export declare const strategySimulateLoopToExposure: (reserveMap: Record<string, ParsedReserve>, lstMap: StrategyLstMap, strategyType: StrategyType, _deposits: StrategyDeposit[], _borrowedAmount: BigNumber, _targetBorrowedAmount: BigNumber | undefined, _targetExposure: BigNumber | undefined) => {
302
306
  deposits: StrategyDeposit[];
303
307
  borrowedAmount: BigNumber;
@@ -357,7 +361,7 @@ export declare const strategyWithdrawTx: (reserveMap: Record<string, ParsedReser
357
361
  transaction: Transaction;
358
362
  withdrawnCoin?: TransactionArgument;
359
363
  }>;
360
- 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<{
364
+ 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<{
361
365
  commandResults: true;
362
366
  }>>) => Promise<{
363
367
  deposits: StrategyDeposit[];
@@ -378,7 +382,7 @@ export declare const strategyDepositAdjustWithdrawTx: (reserveMap: Record<string
378
382
  borrowedAmount: BigNumber;
379
383
  transaction: Transaction;
380
384
  }>;
381
- export declare const strategyAdjustTx: (reserveMap: Record<string, ParsedReserve>, lstMap: StrategyLstMap, strategyType: StrategyType, suiGrpcClient: SuiGrpcClient, suilendClient: SuilendClient, cetusSdk: CetusSdk, cetusPartnerId: string, _address: string, strategyOwnerCapId: TransactionObjectInput, obligation: ParsedObligation, _deposits: StrategyDeposit[], _borrowedAmount: BigNumber, targetExposure: BigNumber, transaction: Transaction, dryRunTransaction: (transaction: Transaction, setGasBudget?: boolean) => Promise<SuiClientTypes.SimulateTransactionResult<{
385
+ export declare const strategyAdjustTx: (reserveMap: Record<string, ParsedReserve>, lstMap: StrategyLstMap, strategyType: StrategyType, suiGrpcClient: SuiGrpcClient, suilendClient: SuilendClient, cetusSdk: CetusSdk, cetusPartnerId: string, _address: string, strategyOwnerCapId: TransactionObjectInput, obligation: ParsedObligation, _deposits: StrategyDeposit[], _borrowedAmount: BigNumber, targetExposure: BigNumber, transaction: Transaction, _dryRunTransaction: (transaction: Transaction, setGasBudget?: boolean) => Promise<SuiClientTypes.SimulateTransactionResult<{
382
386
  commandResults: true;
383
387
  }>>) => Promise<{
384
388
  deposits: StrategyDeposit[];
package/strategies.js CHANGED
@@ -10,9 +10,28 @@ import { getNetAprPercent } from "./lib/index.js";
10
10
  import { getRewardsMap } from "./lib/liquidityMining.js";
11
11
  import { STRATEGY_TYPE_INFO_MAP, StrategyType, strategyBorrow, strategyClaimRewardsAndMergeCoins, strategyDeposit, strategyWithdraw, } from "./lib/strategyOwnerCap.js";
12
12
  import { MMT_CONTRACT_PACKAGE_ID, MMT_VERSION_OBJECT_ID } from "./mmt.js";
13
- import { getWeightedBorrowsUsd } from "./utils/index.js";
13
+ 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
+ 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
+ const isRateLimitExceeded = (err) => /abort code: 2, in '[^']*::rate_limiter::process_qty'/.test(err instanceof Error ? err.message : String(err));
18
+ // Every step dry-runs and rolls back on failure, so the builders only see that
19
+ // no step landed. Recording why lets them name the market's outflow limit
20
+ // instead of reporting a generic failure.
21
+ const trackRateLimit = (dryRunTransaction) => {
22
+ const state = { exceeded: false };
23
+ const tracked = (async (...args) => {
24
+ try {
25
+ return await dryRunTransaction(...args);
26
+ }
27
+ catch (err) {
28
+ if (isRateLimitExceeded(err))
29
+ state.exceeded = true;
30
+ throw err;
31
+ }
32
+ });
33
+ return { rateLimit: state, dryRunTransaction: tracked };
34
+ };
16
35
  /**
17
36
  * Cetus applies its slippage check against its OWN quote, so a wrong quote yields
18
37
  * a wrong floor. Measured on a mainnet unwind of a sub-dollar position: the
@@ -591,7 +610,7 @@ export const getStrategyStepMaxWithdrawnAmount = (
591
610
  // AppContext
592
611
  reserveMap,
593
612
  // Strategy
594
- lstMap, strategyType, deposits, borrowedAmount, withdrawCoinType) => {
613
+ lstMap, strategyType, deposits, borrowedAmount, withdrawCoinType, headroomBuffer = 1) => {
595
614
  const depositReserves = getStrategyDepositReserves(reserveMap, strategyType);
596
615
  const borrowReserve = getStrategyBorrowReserve(reserveMap, strategyType);
597
616
  const defaultCurrencyReserve = getStrategyDefaultCurrencyReserve(reserveMap, strategyType);
@@ -608,10 +627,45 @@ lstMap, strategyType, deposits, borrowedAmount, withdrawCoinType) => {
608
627
  .minus(obligation.maxPriceWeightedBorrowsUsd)
609
628
  .div(withdrawReserve.minPrice)
610
629
  .div(withdrawReserve.config.openLtvPct / 100)
630
+ .decimalPlaces(withdrawReserve.token.decimals, BigNumber.ROUND_DOWN)
631
+ .times(headroomBuffer)
611
632
  : MAX_U64, // Infinity
612
633
  deposits.find((deposit) => deposit.coinType === withdrawReserve.coinType)
613
634
  ?.depositedAmount ?? new BigNumber(0)).decimalPlaces(withdrawReserve.token.decimals, BigNumber.ROUND_DOWN);
614
635
  };
636
+ export const getStrategyRebalanceDepositAmount = (
637
+ // AppContext
638
+ reserveMap,
639
+ // Strategy
640
+ lstMap, strategyType, deposits, borrowedAmount) => {
641
+ const depositReserves = getStrategyDepositReserves(reserveMap, strategyType);
642
+ const borrowReserve = getStrategyBorrowReserve(reserveMap, strategyType);
643
+ const depositReserve = (depositReserves.base ?? depositReserves.lst); // Must have LST if no base
644
+ const simulateWith = (additionalAmount) => getStrategySimulatedObligation(reserveMap, lstMap, strategyType, addOrInsertStrategyDeposit(deposits, {
645
+ coinType: depositReserve.coinType,
646
+ depositedAmount: additionalAmount,
647
+ }), borrowedAmount);
648
+ const isRebalanced = (additionalAmount) => {
649
+ const obligation = simulateWith(additionalAmount);
650
+ return (getStrategyHealthPercent(reserveMap, lstMap, strategyType, obligation).eq(100) &&
651
+ getStrategyExposure(reserveMap, lstMap, strategyType, obligation).lte(STRATEGY_TYPE_EXPOSURE_MAP[strategyType].max));
652
+ };
653
+ if (isRebalanced(new BigNumber(0)))
654
+ return new BigNumber(0);
655
+ // Both conditions only get easier as base is added, so a doubled bound
656
+ // that satisfies them brackets the smallest amount that does.
657
+ let upperBound = BigNumber.max(deposits.find((d) => d.coinType === depositReserve.coinType)
658
+ ?.depositedAmount ?? new BigNumber(0), borrowedAmount.times(borrowReserve.price).div(depositReserve.price));
659
+ let upperBoundRebalances = isRebalanced(upperBound);
660
+ for (let i = 0; i < 30 && !upperBoundRebalances; i++) {
661
+ upperBound = upperBound.times(2);
662
+ upperBoundRebalances = isRebalanced(upperBound);
663
+ }
664
+ if (!upperBoundRebalances)
665
+ throw new Error("No deposit brings the position back within its borrow limit");
666
+ const smallest = bisectionMethod(new BigNumber(0), upperBound, isRebalanced);
667
+ return (smallest.gt(0) ? smallest : upperBound).decimalPlaces(depositReserve.token.decimals, BigNumber.ROUND_UP);
668
+ };
615
669
  // Simulate
616
670
  export const strategySimulateLoopToExposure = (
617
671
  // AppContext
@@ -1236,7 +1290,7 @@ transaction, dryRunTransaction) => {
1236
1290
  _targetExposure: _targetExposure?.toFixed(20),
1237
1291
  }, null, 2));
1238
1292
  const depositReserve = (depositReserves.base ?? depositReserves.lst); // Must have LST if no base
1239
- const loopingDepositReserve = (depositReserves.lst ?? depositReserves.base); // Must have base if no LST
1293
+ let loopingDepositReserve = (depositReserves.lst ?? depositReserves.base); // Must have base if no LST
1240
1294
  //
1241
1295
  let deposits = cloneDeep(_deposits);
1242
1296
  let borrowedAmount = _borrowedAmount;
@@ -1426,7 +1480,9 @@ transaction, dryRunTransaction) => {
1426
1480
  fullRepaymentAmount: fullRepaymentAmount.toFixed(20),
1427
1481
  }));
1428
1482
  // 1) MAX withdraw LST
1429
- if (depositReserves.lst !== undefined) {
1483
+ if (depositReserves.lst !== undefined &&
1484
+ (deposits.find((d) => d.coinType === depositReserves.lst.coinType)
1485
+ ?.depositedAmount ?? new BigNumber(0)).gt(0)) {
1430
1486
  // 1.1) MAX withdraw
1431
1487
  const [withdrawnMaxLstCoin] = strategyWithdraw(strategyType, depositReserves.lst.coinType, strategyOwnerCapId, suilendClient.findReserveArrayIndex(depositReserves.lst.coinType), BigInt(MAX_U64.toString()), transaction);
1432
1488
  // 1.2) Update state
@@ -1585,9 +1641,10 @@ transaction, dryRunTransaction) => {
1585
1641
  if (pendingBorrowedAmount.lt(0))
1586
1642
  break; // Fully repaid already
1587
1643
  if (depositReserve.coinType === depositReserves.base?.coinType) {
1588
- const lstDeposit = deposits.find((d) => d.coinType === depositReserves.lst.coinType);
1644
+ const lstDepositedAmount = deposits.find((d) => d.coinType === depositReserves.lst.coinType)
1645
+ ?.depositedAmount ?? new BigNumber(0);
1589
1646
  // Ran out of LST
1590
- if (lstDeposit.depositedAmount.lte(STRATEGY_E)) {
1647
+ if (lstDepositedAmount.lte(STRATEGY_E)) {
1591
1648
  // 1. MAX withdraws LST (transferred to user as SUI)
1592
1649
  // 2. Withdraws base to cover borrows
1593
1650
  // - Leftover transferred to user as borrow coinType, e.g. SUI or suiWBTC
@@ -1643,12 +1700,17 @@ transaction, dryRunTransaction) => {
1643
1700
  else {
1644
1701
  if (pendingBorrowedAmount.lte(STRATEGY_E))
1645
1702
  break;
1703
+ // The rest of the debt is backed by base, so keep unwinding from it.
1704
+ if (depositReserves.base !== undefined &&
1705
+ (deposits.find((d) => d.coinType === depositReserves.lst.coinType)
1706
+ ?.depositedAmount ?? new BigNumber(0)).lte(STRATEGY_E)) {
1707
+ loopingDepositReserve = depositReserves.base;
1708
+ continue;
1709
+ }
1646
1710
  }
1647
1711
  // 1) Withdraw LST
1648
1712
  // 1.1) Max
1649
- const stepMaxWithdrawnAmount = getStrategyStepMaxWithdrawnAmount(reserveMap, lstMap, strategyType, deposits, borrowedAmount, loopingDepositReserve.coinType)
1650
- .times(0.9) // 10% buffer
1651
- .decimalPlaces(loopingDepositReserve.token.decimals, BigNumber.ROUND_DOWN);
1713
+ const stepMaxWithdrawnAmount = getStrategyStepMaxWithdrawnAmount(reserveMap, lstMap, strategyType, deposits, borrowedAmount, loopingDepositReserve.coinType, 0.9);
1652
1714
  const stepMaxRepaidAmount = new BigNumber(new BigNumber(stepMaxWithdrawnAmount.times(lst?.lstToSuiExchangeRate ?? 1)).minus(getStrategyLstRedeemFee(lstMap, loopingDepositReserve.coinType, stepMaxWithdrawnAmount))).decimalPlaces(borrowReserve.token.decimals, BigNumber.ROUND_DOWN);
1653
1715
  console.log(`[unloopStrategyToExposure] ${i} withdraw_lst.max |`, JSON.stringify({
1654
1716
  stepMaxWithdrawnAmount: stepMaxWithdrawnAmount.toFixed(20),
@@ -1785,9 +1847,7 @@ transaction, dryRunTransaction) => {
1785
1847
  }
1786
1848
  // 1) Withdraw base
1787
1849
  // 1.1) Max
1788
- const stepMaxWithdrawnAmount = getStrategyStepMaxWithdrawnAmount(reserveMap, lstMap, strategyType, deposits, borrowedAmount, loopingDepositReserve.coinType)
1789
- .times(0.9) // 10% buffer
1790
- .decimalPlaces(loopingDepositReserve.token.decimals, BigNumber.ROUND_DOWN);
1850
+ const stepMaxWithdrawnAmount = getStrategyStepMaxWithdrawnAmount(reserveMap, lstMap, strategyType, deposits, borrowedAmount, loopingDepositReserve.coinType, 0.9);
1791
1851
  const stepMaxRepaidAmount = new BigNumber(stepMaxWithdrawnAmount.times(baseToBorrowExchangeRate)).decimalPlaces(borrowReserve.token.decimals, BigNumber.ROUND_DOWN);
1792
1852
  console.log(`[unloopStrategyToExposure] ${i} withdraw_base.max |`, JSON.stringify({
1793
1853
  stepMaxWithdrawnAmount: stepMaxWithdrawnAmount.toFixed(20),
@@ -2325,7 +2385,8 @@ export const strategyMaxWithdrawTx = async (
2325
2385
  // AppContext
2326
2386
  reserveMap, rewardsMap,
2327
2387
  // Strategy
2328
- lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _address, strategyOwnerCapId, obligationId, _deposits, _borrowedAmount, withdrawCoinType, transaction, dryRunTransaction) => {
2388
+ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _address, strategyOwnerCapId, obligationId, _deposits, _borrowedAmount, withdrawCoinType, transaction, _dryRunTransaction) => {
2389
+ const { rateLimit, dryRunTransaction } = trackRateLimit(_dryRunTransaction);
2329
2390
  const strategyInfo = STRATEGY_TYPE_INFO_MAP[strategyType];
2330
2391
  const lst = strategyInfo.depositLstCoinType !== undefined
2331
2392
  ? lstMap[strategyInfo.depositLstCoinType]
@@ -2351,14 +2412,37 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
2351
2412
  let borrowedAmount = _borrowedAmount;
2352
2413
  // 1) Unloop to 1x (base+LST: no LST and no borrows, LST: no borrows)
2353
2414
  if (borrowedAmount.gt(0)) {
2354
- // 1.1) Unloop
2355
- const { deposits: newDeposits, borrowedAmount: newBorrowedAmount, transaction: newTransaction, } = await strategyUnloopToExposureTx(reserveMap, lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _address, strategyOwnerCapId, obligationId, deposits, borrowedAmount, undefined, // Don't pass targetBorrowedAmount
2415
+ const unloopToOne = (fromDeposits, fromBorrowedAmount, fromTransaction) => strategyUnloopToExposureTx(reserveMap, lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _address, strategyOwnerCapId, obligationId, fromDeposits, fromBorrowedAmount, undefined, // Don't pass targetBorrowedAmount
2356
2416
  new BigNumber(1), // Pass targetExposure
2357
- transaction, dryRunTransaction);
2417
+ fromTransaction, dryRunTransaction);
2418
+ // 1.1) Unloop
2419
+ let unlooped;
2420
+ let plainError;
2421
+ try {
2422
+ unlooped = await unloopToOne(deposits, borrowedAmount, Transaction.from(transaction));
2423
+ }
2424
+ catch (err) {
2425
+ plainError = err;
2426
+ }
2427
+ // 1.1b) Above the borrow limit no stepwise withdraw can land, so rebalance and retry
2428
+ if ((unlooped === undefined || unlooped.borrowedAmount.gt(STRATEGY_E)) &&
2429
+ !isStrategyWithinBorrowLimit(reserveMap, lstMap, strategyType, deposits, borrowedAmount)) {
2430
+ try {
2431
+ const rebalanced = await strategyRebalanceWithinBorrowLimitTx(reserveMap, lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _address, strategyOwnerCapId, obligationId, deposits, borrowedAmount, Transaction.from(transaction), dryRunTransaction);
2432
+ unlooped = await unloopToOne(rebalanced.deposits, rebalanced.borrowedAmount, rebalanced.transaction);
2433
+ }
2434
+ catch (err) {
2435
+ console.error(err);
2436
+ }
2437
+ }
2438
+ if (unlooped === undefined)
2439
+ throw rateLimit.exceeded
2440
+ ? new Error(STRATEGY_RATE_LIMITED_ERROR_MESSAGE)
2441
+ : plainError;
2358
2442
  // 1.2) Update state
2359
- deposits = newDeposits;
2360
- borrowedAmount = newBorrowedAmount;
2361
- transaction = newTransaction;
2443
+ deposits = unlooped.deposits;
2444
+ borrowedAmount = unlooped.borrowedAmount;
2445
+ transaction = unlooped.transaction;
2362
2446
  console.log(`[strategyMaxWithdraw] unloop.update_state |`, JSON.stringify({
2363
2447
  deposits: deposits.map((d) => ({
2364
2448
  coinType: d.coinType,
@@ -2373,6 +2457,32 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
2373
2457
  // rather than build a transaction that is certain to abort on the health check —
2374
2458
  // the caller can surface this instead of asking the user to sign it.
2375
2459
  let flashLoan;
2460
+ let repaidByAdjustRepay = false;
2461
+ if (borrowedAmount.gt(STRATEGY_E) &&
2462
+ depositReserves.base !== undefined &&
2463
+ depositReserves.lst !== undefined) {
2464
+ const baseFlashLoanAmount = borrowedAmount
2465
+ .times(1.01)
2466
+ .decimalPlaces(borrowReserve.token.decimals, BigNumber.ROUND_UP)
2467
+ .times(borrowReserve.price)
2468
+ .div(depositReserves.base.price)
2469
+ .times(1.03)
2470
+ .decimalPlaces(depositReserves.base.token.decimals, BigNumber.ROUND_UP);
2471
+ console.log(`[strategyMaxWithdraw] adjust_repay |`, JSON.stringify({
2472
+ borrowedAmount: borrowedAmount.toFixed(20),
2473
+ baseFlashLoanAmount: baseFlashLoanAmount.toFixed(20),
2474
+ }));
2475
+ try {
2476
+ ({ deposits, borrowedAmount, transaction } = await strategyAdjustRepayTx(reserveMap, lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _address, strategyOwnerCapId, obligationId, deposits, borrowedAmount, baseFlashLoanAmount, Transaction.from(transaction), dryRunTransaction));
2477
+ }
2478
+ catch (err) {
2479
+ console.error(err);
2480
+ throw new Error(rateLimit.exceeded
2481
+ ? STRATEGY_RATE_LIMITED_ERROR_MESSAGE
2482
+ : "Failed to repay borrows before withdrawing");
2483
+ }
2484
+ repaidByAdjustRepay = true;
2485
+ }
2376
2486
  if (borrowedAmount.gt(STRATEGY_E)) {
2377
2487
  const flashLoanObj = STRATEGY_TYPE_FLASH_LOAN_OBJ_MAP[strategyType];
2378
2488
  if (flashLoanObj.provider !== StrategyFlashLoanProvider.MMT ||
@@ -2385,7 +2495,9 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
2385
2495
  borrowedAmount
2386
2496
  .times(borrowReserve.price)
2387
2497
  .gte(STRATEGY_FLASH_REPAY_MAX_RESIDUAL_USD))
2388
- throw new Error("Failed to repay borrows before withdrawing");
2498
+ throw new Error(rateLimit.exceeded
2499
+ ? STRATEGY_RATE_LIMITED_ERROR_MESSAGE
2500
+ : "Failed to repay borrows before withdrawing");
2389
2501
  const flashLoanBorrowedAmount = borrowedAmount
2390
2502
  .times(1.1)
2391
2503
  .decimalPlaces(borrowReserve.token.decimals, BigNumber.ROUND_UP);
@@ -2525,13 +2637,15 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
2525
2637
  transaction = transactionBeforeClaim;
2526
2638
  }
2527
2639
  }
2528
- if (flashLoan !== undefined || borrowedAmount.gt(0)) {
2640
+ if (flashLoan !== undefined || repaidByAdjustRepay || borrowedAmount.gt(0)) {
2529
2641
  try {
2530
2642
  await dryRunTransaction(transaction);
2531
2643
  }
2532
2644
  catch (err) {
2533
2645
  console.error(err);
2534
- throw new Error("Failed to repay borrows before withdrawing");
2646
+ throw new Error(rateLimit.exceeded
2647
+ ? STRATEGY_RATE_LIMITED_ERROR_MESSAGE
2648
+ : "Failed to repay borrows before withdrawing");
2535
2649
  }
2536
2650
  }
2537
2651
  return { deposits, borrowedAmount, transaction };
@@ -2971,11 +3085,14 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
2971
3085
  }
2972
3086
  return { deposits, borrowedAmount, transaction };
2973
3087
  };
3088
+ const isStrategyWithinBorrowLimit = (reserveMap, lstMap, strategyType, deposits, borrowedAmount) => getStrategyHealthPercent(reserveMap, lstMap, strategyType, getStrategySimulatedObligation(reserveMap, lstMap, strategyType, deposits, borrowedAmount)).eq(100);
3089
+ const strategyRebalanceWithinBorrowLimitTx = (reserveMap, lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _address, strategyOwnerCapId, obligationId, deposits, borrowedAmount, transaction, dryRunTransaction) => strategyDepositAdjustWithdrawTx(reserveMap, lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _address, strategyOwnerCapId, obligationId, deposits, borrowedAmount, getStrategyRebalanceDepositAmount(reserveMap, lstMap, strategyType, deposits, borrowedAmount), transaction, dryRunTransaction);
2974
3090
  export const strategyAdjustTx = async (
2975
3091
  // AppContext
2976
3092
  reserveMap,
2977
3093
  // Strategy
2978
- lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _address, strategyOwnerCapId, obligation, _deposits, _borrowedAmount, targetExposure, transaction, dryRunTransaction) => {
3094
+ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _address, strategyOwnerCapId, obligation, _deposits, _borrowedAmount, targetExposure, transaction, _dryRunTransaction) => {
3095
+ const { rateLimit, dryRunTransaction } = trackRateLimit(_dryRunTransaction);
2979
3096
  const strategyInfo = STRATEGY_TYPE_INFO_MAP[strategyType];
2980
3097
  const lst = strategyInfo.depositLstCoinType !== undefined
2981
3098
  ? lstMap[strategyInfo.depositLstCoinType]
@@ -2998,15 +3115,54 @@ lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _a
2998
3115
  //
2999
3116
  const deposits = cloneDeep(_deposits);
3000
3117
  const borrowedAmount = _borrowedAmount;
3001
- // 1) Loop or unloop to target exposure
3002
- if (targetExposure.gt(exposure))
3003
- return strategyLoopToExposureTx(reserveMap, lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _address, strategyOwnerCapId, obligation.id, deposits, borrowedAmount, undefined, // Don't pass targetBorrowedAmount
3118
+ if (targetExposure.eq(exposure))
3119
+ return { deposits, borrowedAmount, transaction };
3120
+ const loopOrUnloop = (fromDeposits, fromBorrowedAmount, fromExposure, fromTransaction) => targetExposure.gt(fromExposure)
3121
+ ? strategyLoopToExposureTx(reserveMap, lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _address, strategyOwnerCapId, obligation.id, fromDeposits, fromBorrowedAmount, undefined, // Don't pass targetBorrowedAmount
3004
3122
  targetExposure, // Pass targetExposure
3005
- transaction);
3006
- else if (targetExposure.lt(exposure))
3007
- return strategyUnloopToExposureTx(reserveMap, lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _address, strategyOwnerCapId, obligation.id, deposits, borrowedAmount, undefined, // Don't pass targetBorrowedAmount
3123
+ fromTransaction)
3124
+ : strategyUnloopToExposureTx(reserveMap, lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _address, strategyOwnerCapId, obligation.id, fromDeposits, fromBorrowedAmount, undefined, // Don't pass targetBorrowedAmount
3008
3125
  targetExposure, // Pass targetExposure
3009
- transaction, dryRunTransaction);
3010
- else
3011
- return { deposits, borrowedAmount, transaction };
3126
+ fromTransaction, dryRunTransaction);
3127
+ // 1) Loop or unloop to target exposure
3128
+ let plainError;
3129
+ try {
3130
+ const result = await loopOrUnloop(deposits, borrowedAmount, exposure, Transaction.from(transaction));
3131
+ if (!result.borrowedAmount.eq(borrowedAmount))
3132
+ return result;
3133
+ }
3134
+ catch (err) {
3135
+ plainError = err;
3136
+ }
3137
+ // 2) Above the borrow limit no stepwise withdraw can land, so rebalance and retry
3138
+ if (borrowedAmount.gt(0) &&
3139
+ !isStrategyWithinBorrowLimit(reserveMap, lstMap, strategyType, deposits, borrowedAmount)) {
3140
+ let rebalanced;
3141
+ try {
3142
+ rebalanced = await strategyRebalanceWithinBorrowLimitTx(reserveMap, lstMap, strategyType, suiGrpcClient, suilendClient, cetusSdk, cetusPartnerId, _address, strategyOwnerCapId, obligation.id, deposits, borrowedAmount, Transaction.from(transaction), dryRunTransaction);
3143
+ }
3144
+ catch (err) {
3145
+ console.error(err);
3146
+ }
3147
+ if (rebalanced !== undefined) {
3148
+ const rebalancedExposure = getStrategyExposure(reserveMap, lstMap, strategyType, getStrategySimulatedObligation(reserveMap, lstMap, strategyType, rebalanced.deposits, rebalanced.borrowedAmount));
3149
+ if (targetExposure.eq(rebalancedExposure))
3150
+ return rebalanced;
3151
+ try {
3152
+ const result = await loopOrUnloop(rebalanced.deposits, rebalanced.borrowedAmount, rebalancedExposure, Transaction.from(rebalanced.transaction));
3153
+ return result.borrowedAmount.eq(rebalanced.borrowedAmount)
3154
+ ? rebalanced
3155
+ : result;
3156
+ }
3157
+ catch (err) {
3158
+ console.error(err);
3159
+ return rebalanced;
3160
+ }
3161
+ }
3162
+ }
3163
+ if (rateLimit.exceeded)
3164
+ throw new Error(STRATEGY_RATE_LIMITED_ERROR_MESSAGE);
3165
+ if (plainError !== undefined)
3166
+ throw plainError;
3167
+ throw new Error("Failed to adjust leverage");
3012
3168
  };
@@ -197,6 +197,42 @@ export type ObligationHealth = {
197
197
  * All math stays in BigNumber end-to-end — never construct a `Decimal` from
198
198
  * a stored quantity; `numberToDecimal`/`stringToDecimal` round through a JS
199
199
  * `number` (`Math.round`) and are lossy at WAD scale. `decimalToBigNumber` is
200
- * only ever called on a genuine on-chain `Reserve` field below.
200
+ * only ever called on a genuine on-chain `Reserve` field, in
201
+ * `buildReserveHealthTable`.
202
+ *
203
+ * Per-position operation order matches `getCTokenMarketValue` and
204
+ * `compoundBorrowedAmount` exactly, so the result is bit-identical to
205
+ * recomputing every reserve value per position; reordering the divisions
206
+ * would round differently and can flip a verdict at the threshold.
207
+ */
208
+ export declare function computeObligationHealth(deposits: ObligationHealthDepositInput[], borrows: ObligationHealthBorrowInput[], reserveHealth: ReserveHealthTable): ObligationHealth;
209
+ export type ReserveDepositHealthValues = {
210
+ price: BigNumber;
211
+ cTokenRatio: BigNumber;
212
+ mintScale: BigNumber;
213
+ closeLtv: BigNumber;
214
+ };
215
+ export type ReserveBorrowHealthValues = {
216
+ price: BigNumber;
217
+ mintScale: BigNumber;
218
+ cumulativeBorrowRate: BigNumber;
219
+ borrowWeight: BigNumber;
220
+ };
221
+ /**
222
+ * Each lookup throws, as `findReserveOrThrow` does, when no reserve has
223
+ * this coin type. The two sides are separate so a reserve is only read for
224
+ * the fields its positions actually use.
225
+ */
226
+ export type ReserveHealthTable = {
227
+ deposit: (coinType: string) => ReserveDepositHealthValues;
228
+ borrow: (coinType: string) => ReserveBorrowHealthValues;
229
+ };
230
+ /**
231
+ * The per-reserve inputs to `computeObligationHealth`, derived once per
232
+ * reserve and side on first lookup instead of once per position. Build one
233
+ * per pass over a fixed set of priced reserves and drop it after: the values
234
+ * are a snapshot, and a caller that later rewrites a reserve's price in place
235
+ * would otherwise keep reading the old one.
201
236
  */
237
+ export declare function buildReserveHealthTable(reserves: Reserve<string>[]): ReserveHealthTable;
202
238
  export declare function computeObligationHealthFromPositions(deposits: ObligationHealthDepositInput[], borrows: ObligationHealthBorrowInput[], refreshedReserves: Reserve<string>[]): ObligationHealth;
package/utils/simulate.js CHANGED
@@ -507,28 +507,32 @@ export const compoundDebt = (borrow, reserve) => {
507
507
  * All math stays in BigNumber end-to-end — never construct a `Decimal` from
508
508
  * a stored quantity; `numberToDecimal`/`stringToDecimal` round through a JS
509
509
  * `number` (`Math.round`) and are lossy at WAD scale. `decimalToBigNumber` is
510
- * only ever called on a genuine on-chain `Reserve` field below.
510
+ * only ever called on a genuine on-chain `Reserve` field, in
511
+ * `buildReserveHealthTable`.
512
+ *
513
+ * Per-position operation order matches `getCTokenMarketValue` and
514
+ * `compoundBorrowedAmount` exactly, so the result is bit-identical to
515
+ * recomputing every reserve value per position; reordering the divisions
516
+ * would round differently and can flip a verdict at the threshold.
511
517
  */
512
- export function computeObligationHealthFromPositions(deposits, borrows, refreshedReserves) {
518
+ export function computeObligationHealth(deposits, borrows, reserveHealth) {
513
519
  let depositValueUsd = new BigNumber(0);
514
520
  let unhealthyBorrowValueUsd = new BigNumber(0);
515
521
  for (const deposit of deposits) {
516
- const reserve = findReserveOrThrow(refreshedReserves, deposit.coinType);
517
- const config = reserve.config.element;
518
- const marketValue = getCTokenMarketValue(reserve, deposit.depositedCtokenAmount);
522
+ const reserve = reserveHealth.deposit(deposit.coinType);
523
+ const marketValue = reserve.price
524
+ .multipliedBy(deposit.depositedCtokenAmount.multipliedBy(reserve.cTokenRatio))
525
+ .dividedBy(reserve.mintScale);
519
526
  depositValueUsd = depositValueUsd.plus(marketValue);
520
- unhealthyBorrowValueUsd = unhealthyBorrowValueUsd.plus(marketValue.multipliedBy(computeCloseLtv(config)));
527
+ unhealthyBorrowValueUsd = unhealthyBorrowValueUsd.plus(marketValue.multipliedBy(reserve.closeLtv));
521
528
  }
522
529
  let weightedBorrowedValueUsd = new BigNumber(0);
523
530
  for (const borrow of borrows) {
524
- const reserve = findReserveOrThrow(refreshedReserves, borrow.coinType);
525
- const config = reserve.config.element;
526
- const currentBorrowedAmountUnderlying = compoundBorrowedAmount(borrow.borrowedAmountUnderlying, decimalToBigNumber(reserve.cumulativeBorrowRate), borrow.cumulativeBorrowRateAtSnapshot);
527
- const marketValue = currentBorrowedAmountUnderlying
528
- .multipliedBy(decimalToBigNumber(reserve.price))
529
- .dividedBy(new BigNumber(10 ** reserve.mintDecimals));
530
- const borrowWeight = computeBorrowWeight(config);
531
- weightedBorrowedValueUsd = weightedBorrowedValueUsd.plus(marketValue.multipliedBy(borrowWeight));
531
+ const reserve = reserveHealth.borrow(borrow.coinType);
532
+ const marketValue = compoundBorrowedAmount(borrow.borrowedAmountUnderlying, reserve.cumulativeBorrowRate, borrow.cumulativeBorrowRateAtSnapshot)
533
+ .multipliedBy(reserve.price)
534
+ .dividedBy(reserve.mintScale);
535
+ weightedBorrowedValueUsd = weightedBorrowedValueUsd.plus(marketValue.multipliedBy(reserve.borrowWeight));
532
536
  }
533
537
  return {
534
538
  depositedValueUsd: depositValueUsd,
@@ -536,3 +540,46 @@ export function computeObligationHealthFromPositions(deposits, borrows, refreshe
536
540
  weightedBorrowedValueUsd,
537
541
  };
538
542
  }
543
+ const memoizeByCoinType = (derive) => {
544
+ const byCoinType = new Map();
545
+ return (coinType) => {
546
+ let values = byCoinType.get(coinType);
547
+ if (values === undefined) {
548
+ values = derive(coinType);
549
+ byCoinType.set(coinType, values);
550
+ }
551
+ return values;
552
+ };
553
+ };
554
+ /**
555
+ * The per-reserve inputs to `computeObligationHealth`, derived once per
556
+ * reserve and side on first lookup instead of once per position. Build one
557
+ * per pass over a fixed set of priced reserves and drop it after: the values
558
+ * are a snapshot, and a caller that later rewrites a reserve's price in place
559
+ * would otherwise keep reading the old one.
560
+ */
561
+ export function buildReserveHealthTable(reserves) {
562
+ return {
563
+ deposit: memoizeByCoinType((coinType) => {
564
+ const reserve = findReserveOrThrow(reserves, coinType);
565
+ return {
566
+ price: decimalToBigNumber(reserve.price),
567
+ cTokenRatio: cTokenRatio(reserve),
568
+ mintScale: new BigNumber(10 ** reserve.mintDecimals),
569
+ closeLtv: computeCloseLtv(reserve.config.element),
570
+ };
571
+ }),
572
+ borrow: memoizeByCoinType((coinType) => {
573
+ const reserve = findReserveOrThrow(reserves, coinType);
574
+ return {
575
+ price: decimalToBigNumber(reserve.price),
576
+ mintScale: new BigNumber(10 ** reserve.mintDecimals),
577
+ cumulativeBorrowRate: decimalToBigNumber(reserve.cumulativeBorrowRate),
578
+ borrowWeight: computeBorrowWeight(reserve.config.element),
579
+ };
580
+ }),
581
+ };
582
+ }
583
+ export function computeObligationHealthFromPositions(deposits, borrows, refreshedReserves) {
584
+ return computeObligationHealth(deposits, borrows, buildReserveHealthTable(refreshedReserves));
585
+ }