@suilend/sdk 7.0.0 → 7.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client.js CHANGED
@@ -115,6 +115,10 @@ export async function listAllOwnedObjects(suiGrpcClient, owner, type, include) {
115
115
  const objs = await suiGrpcClient.listOwnedObjects({
116
116
  owner,
117
117
  cursor,
118
+ // The server defaults to 50/page and caps at 1000; this walk runs to
119
+ // exhaustion with no retry, so the default multiplied round-trips
120
+ // (and per-page transient-failure exposure) ~15x for no reason.
121
+ limit: 1000,
118
122
  type,
119
123
  include,
120
124
  });
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"@suilend/sdk","version":"7.0.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/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/i64/structs":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/i64/structs.js","./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price/structs":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price/structs.js","./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-feed/structs":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-feed/structs.js","./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-identifier/structs":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-identifier/structs.js","./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-info/structs":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-info/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.5.0","@cetusprotocol/aggregator-sdk":"^1.5.7","@flowx-finance/sdk":"^2.1.0","@pythnetwork/hermes-client":"3.1.0","@pythnetwork/pyth-sui-js":"4.0.0","@suilend/springsui-sdk":"^4.0.0","bignumber.js":"^9.1.2","bn.js":"^5.2.2","crypto-js":"^4.2.0","lodash":"^4.17.21","p-limit":"3.1.0","uuid":"^11.0.3"},"devDependencies":{"@mysten/bcs":"^2.0.5","@mysten/sui":"2.17.0","@suilend/sui-core":"^1.0.0","@tsconfig/recommended":"^1.0.8","@types/bn.js":"^5.2.0","@types/lodash":"^4.17.20","@types/node":"^22.9.0","prettier":"^3.3.3","ts-node":"^10.9.2","typescript":"^6.0.3","vitest":"4.1.0"},"peerDependencies":{"@mysten/bcs":"^2.0.5","@mysten/sui":"2.17.0","@suilend/sui-core":"^1.0.0"},"type":"module"}
1
+ {"name":"@suilend/sdk","version":"7.0.1","private":false,"description":"A TypeScript SDK for interacting with the Suilend program","author":"Suilend","license":"MIT","main":"./index.js","exports":{".":"./index.js","./client":"./client.js","./mmt":"./mmt.js","./strategies":"./strategies.js","./api/events":"./api/events.js","./api":"./api/index.js","./lib/constants":"./lib/constants.js","./lib":"./lib/index.js","./lib/initialize":"./lib/initialize.js","./lib/liquidityMining":"./lib/liquidityMining.js","./lib/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/i64/structs":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/i64/structs.js","./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price/structs":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price/structs.js","./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-feed/structs":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-feed/structs.js","./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-identifier/structs":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-identifier/structs.js","./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-info/structs":"./_generated/_dependencies/source/0x8d97f1cd6ac663735be08d1d2b6d02a159e711586461306ce60a2b7a6a565a9e/price-info/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.5.0","@cetusprotocol/aggregator-sdk":"^1.5.7","@flowx-finance/sdk":"^2.1.0","@pythnetwork/hermes-client":"3.1.0","@pythnetwork/pyth-sui-js":"4.0.0","@suilend/springsui-sdk":"^4.0.0","bignumber.js":"^11.1.5","bn.js":"^5.2.2","crypto-js":"^4.2.0","lodash":"^4.17.21","p-limit":"7.3.1","uuid":"^14.0.1"},"devDependencies":{"@mysten/bcs":"^2.0.5","@mysten/sui":"2.23.1","@suilend/sui-core":"^1.0.0","@tsconfig/recommended":"^1.0.8","@types/bn.js":"^5.2.0","@types/lodash":"^4.17.20","@types/node":"^26.1.2","fast-check":"^4.9.0","prettier":"^3.3.3","ts-node":"^10.9.2","typescript":"^6.0.3","vitest":"4.1.10"},"peerDependencies":{"@mysten/bcs":"^2.0.5","@mysten/sui":">=2.22.1 <3","@suilend/sui-core":"^1.0.0"},"type":"module"}
@@ -1,6 +1,7 @@
1
1
  import { SuiClientTypes } from "@mysten/sui/client";
2
2
  import { SuiGraphQLClient } from "@mysten/sui/graphql";
3
3
  import { SuiGrpcClient } from "@mysten/sui/grpc";
4
+ import { BigNumber } from "bignumber.js";
4
5
  import { Obligation } from "../_generated/suilend/obligation/structs";
5
6
  import { ParsedObligation } from "../parsers";
6
7
  export declare function fetchAllObligationsForMarketWithHandler(suiGrpcClient: SuiGrpcClient, lendingMarketId: string, lendingMarketType: string, chunkHandler: (obligations: Obligation<string>[]) => Promise<void>): Promise<void>;
@@ -1,8 +1,9 @@
1
1
  import BigNumber from "bignumber.js";
2
2
  import { Decimal } from "../_generated/suilend/decimal/structs";
3
3
  import { PoolRewardManager, UserRewardManager } from "../_generated/suilend/liquidity-mining/structs";
4
- import { Borrow, Obligation } from "../_generated/suilend/obligation/structs";
4
+ import { Borrow, Deposit, Obligation } from "../_generated/suilend/obligation/structs";
5
5
  import { Reserve } from "../_generated/suilend/reserve/structs";
6
+ import { ReserveConfig } from "../_generated/suilend/reserve-config/structs";
6
7
  import { normalizeFeedId } from "./feedId";
7
8
  /**
8
9
  * @deprecated since version 1.0.8. Use `calculateUtilizationPercent` instead.
@@ -113,6 +114,30 @@ export declare const refreshReservePriceTolerant: (reserves: Reserve<string>[],
113
114
  unpricedCoinTypes: string[];
114
115
  }>;
115
116
  export declare const updateUserRewardManager: (poolManager: PoolRewardManager, userRewardManager: UserRewardManager, nowMs: number) => UserRewardManager;
117
+ export declare const findReserveOrThrow: (reserves: Reserve<string>[], coinType: string) => Reserve<string>;
118
+ export declare const computeBorrowWeight: (config: ReserveConfig) => BigNumber;
119
+ export declare const computeCloseLtv: (config: ReserveConfig) => BigNumber;
120
+ export type ObligationRefresh = {
121
+ deposits: Deposit[];
122
+ borrows: Borrow[];
123
+ userRewardManagers: UserRewardManager[];
124
+ depositedValueUsd: BigNumber;
125
+ allowedBorrowValueUsd: BigNumber;
126
+ unhealthyBorrowValueUsd: BigNumber;
127
+ unweightedBorrowedValueUsd: BigNumber;
128
+ weightedBorrowedValueUsd: BigNumber;
129
+ weightedBorrowedValueUpperBoundUsd: BigNumber;
130
+ };
131
+ /**
132
+ * Pure computation half of `refreshObligation`: same math, but returns fresh
133
+ * deposits/borrows/reward-manager arrays and the five USD aggregates instead
134
+ * of writing them onto `obligation`. Never touches `obligation` or any of its
135
+ * nested arrays — `refreshObligation`'s in-place `obligation.deposits[i] = ...`
136
+ * mutated the caller's original array too (only the top-level object was
137
+ * shallow-copied), silently corrupting `unrefreshedObligation` for anyone still
138
+ * holding a reference to it.
139
+ */
140
+ export declare function computeObligationRefresh(obligation: Obligation<string>, refreshedReserves: Reserve<string>[], nowMs: number): ObligationRefresh;
116
141
  export declare const refreshObligation: (unrefreshedObligation: Obligation<string>, refreshedReserves: Reserve<string>[]) => Obligation<string>;
117
142
  export declare const numberToDecimal: (value: number) => Decimal;
118
143
  export declare const stringToDecimal: (value: string) => Decimal;
@@ -121,4 +146,42 @@ export declare const getCTokenMarketValue: (reserve: Reserve<string>, depositedC
121
146
  export declare const getCTokenMarketValueLowerBound: (reserve: Reserve<string>, depositedCTokenAmount: BigNumber) => BigNumber;
122
147
  export declare const cTokenRatio: (reserve: Reserve<string>) => BigNumber;
123
148
  export declare const totalSupply: (reserve: Reserve<string>) => BigNumber;
149
+ export declare const compoundBorrowedAmount: (snapshotAmount: BigNumber, currentCumulativeBorrowRate: BigNumber, snapshotCumulativeBorrowRate: BigNumber) => BigNumber;
124
150
  export declare const compoundDebt: (borrow: Borrow, reserve: Reserve<string>) => Borrow;
151
+ export type ObligationHealthDepositInput = {
152
+ /** Matches `reserve.coinType.name` (unprefixed struct tag) — same field `refreshObligation` compares deposits against. */
153
+ coinType: string;
154
+ /** Raw ctoken unit count, i.e. `deposit.depositedCtokenAmount` as stored (Positions.quantity for a deposit row). */
155
+ depositedCtokenAmount: BigNumber;
156
+ };
157
+ export type ObligationHealthBorrowInput = {
158
+ coinType: string;
159
+ /** Human-scale underlying amount as of the stored snapshot (Positions.quantity for a borrow row — already ÷ WAD at write time, see dbClient.ts writeCachedObligations). */
160
+ borrowedAmountUnderlying: BigNumber;
161
+ /** Human-scale cumulative borrow rate factor as of the stored snapshot (Positions.cumulativeBorrowRate, a raw WAD integer, ÷ WAD). */
162
+ cumulativeBorrowRateAtSnapshot: BigNumber;
163
+ };
164
+ export type ObligationHealth = {
165
+ depositedValueUsd: BigNumber;
166
+ unhealthyBorrowValueUsd: BigNumber;
167
+ weightedBorrowedValueUsd: BigNumber;
168
+ };
169
+ /**
170
+ * The health-relevant subset of `refreshObligation` (deposit/borrow valuation
171
+ * and the two USD thresholds `shouldAttemptLiquidations` compares), computed
172
+ * from stored position state rather than a live on-chain `Obligation`.
173
+ *
174
+ * Deliberately does NOT touch reward-manager state (`refreshObligation`'s
175
+ * `updateUserRewardManager` calls) — that state isn't stored in `Positions`
176
+ * and doesn't participate in either threshold (LEN-576). Deliberately does
177
+ * NOT compute `allowedBorrowValueUsd`/`unweightedBorrowedValueUsd`/
178
+ * `weightedBorrowedValueUpperBoundUsd` — those exist for the risky-subset
179
+ * re-scan optimization in `obligationIsRisky`, which has no equivalent here
180
+ * (this function is cheap enough to run over every obligation every cycle).
181
+ *
182
+ * All math stays in BigNumber end-to-end — never construct a `Decimal` from
183
+ * a stored quantity; `numberToDecimal`/`stringToDecimal` round through a JS
184
+ * `number` (`Math.round`) and are lossy at WAD scale. `decimalToBigNumber` is
185
+ * only ever called on a genuine on-chain `Reserve` field below.
186
+ */
187
+ export declare function computeObligationHealthFromPositions(deposits: ObligationHealthDepositInput[], borrows: ObligationHealthBorrowInput[], refreshedReserves: Reserve<string>[]): ObligationHealth;
package/utils/simulate.js CHANGED
@@ -271,14 +271,17 @@ export const refreshReservePriceTolerant = async (reserves, pythConnection) => {
271
271
  return { reserves: updatedReserves, unpricedCoinTypes };
272
272
  };
273
273
  export const updateUserRewardManager = (poolManager, userRewardManager, nowMs) => {
274
- const updatedUserRewardManager = { ...userRewardManager };
274
+ const updatedUserRewardManager = {
275
+ ...userRewardManager,
276
+ rewards: [...userRewardManager.rewards],
277
+ };
275
278
  for (let i = 0; i < poolManager.poolRewards.length; i++) {
276
279
  const poolReward = poolManager.poolRewards[i];
277
280
  if (poolReward == null) {
278
281
  continue;
279
282
  }
280
- if (i >= userRewardManager.rewards.length) {
281
- userRewardManager.rewards.push(null);
283
+ if (i >= updatedUserRewardManager.rewards.length) {
284
+ updatedUserRewardManager.rewards.push(null);
282
285
  }
283
286
  const oldReward = updatedUserRewardManager.rewards[i];
284
287
  const reward = { ...oldReward };
@@ -305,66 +308,105 @@ export const updateUserRewardManager = (poolManager, userRewardManager, nowMs) =
305
308
  updatedUserRewardManager.lastUpdateTimeMs = BigInt(nowMs);
306
309
  return updatedUserRewardManager;
307
310
  };
308
- export const refreshObligation = (unrefreshedObligation, refreshedReserves) => {
309
- const obligation = { ...unrefreshedObligation };
310
- // Refresh Deposits
311
+ export const findReserveOrThrow = (reserves, coinType) => {
312
+ const reserve = reserves.find((r) => r.coinType.name === coinType);
313
+ if (!reserve) {
314
+ throw new Error(`Unable to find reserve for coin type ${coinType}`);
315
+ }
316
+ return reserve;
317
+ };
318
+ // `borrowWeightBps` is a raw u64, so `bps / BigInt(10000)` is integer
319
+ // division and truncates a fractional weight (e.g. DEEP/WAL at 1.6x, IKA at
320
+ // 1.8x) down to 1.0x. On-chain `decimal::from_bps` scales first
321
+ // (`v * WAD / 10_000`); dividing in BigNumber here matches that.
322
+ export const computeBorrowWeight = (config) => new BigNumber(config.borrowWeightBps.toString()).div(10000);
323
+ export const computeCloseLtv = (config) => new BigNumber(config.closeLtvPct / 100);
324
+ /**
325
+ * Pure computation half of `refreshObligation`: same math, but returns fresh
326
+ * deposits/borrows/reward-manager arrays and the five USD aggregates instead
327
+ * of writing them onto `obligation`. Never touches `obligation` or any of its
328
+ * nested arrays — `refreshObligation`'s in-place `obligation.deposits[i] = ...`
329
+ * mutated the caller's original array too (only the top-level object was
330
+ * shallow-copied), silently corrupting `unrefreshedObligation` for anyone still
331
+ * holding a reference to it.
332
+ */
333
+ export function computeObligationRefresh(obligation, refreshedReserves, nowMs) {
334
+ const userRewardManagers = [...obligation.userRewardManagers];
311
335
  let depositValueUsd = new BigNumber(0);
312
336
  let allowedBorrowValueUsd = new BigNumber(0);
313
337
  let unhealthyBorrowValueUsd = new BigNumber(0);
314
- for (let i = 0; i < obligation.deposits.length; i++) {
315
- const deposit = { ...obligation.deposits[i] };
316
- const reserve = refreshedReserves.find((r) => r.coinType.name === deposit.coinType.name);
317
- if (!reserve) {
318
- throw new Error(`Unable to find reserve for coin type ${deposit.coinType.name}`);
319
- }
338
+ const deposits = obligation.deposits.map((unrefreshedDeposit) => {
339
+ const deposit = { ...unrefreshedDeposit };
340
+ const reserve = findReserveOrThrow(refreshedReserves, deposit.coinType.name);
320
341
  const config = reserve.config.element;
321
- obligation.userRewardManagers[Number(deposit.userRewardManagerIndex)] =
322
- updateUserRewardManager(reserve.depositsPoolRewardManager, obligation.userRewardManagers[Number(deposit.userRewardManagerIndex)], Date.now());
342
+ userRewardManagers[Number(deposit.userRewardManagerIndex)] =
343
+ updateUserRewardManager(reserve.depositsPoolRewardManager, userRewardManagers[Number(deposit.userRewardManagerIndex)], nowMs);
323
344
  const marketValue = getCTokenMarketValue(reserve, new BigNumber(deposit.depositedCtokenAmount.toString()));
324
345
  const marketValueLowerBound = getCTokenMarketValueLowerBound(reserve, new BigNumber(deposit.depositedCtokenAmount.toString()));
325
346
  deposit.marketValue = stringToDecimal(marketValue.toString());
326
347
  depositValueUsd = depositValueUsd.plus(new BigNumber(marketValue.toString()));
327
348
  allowedBorrowValueUsd = allowedBorrowValueUsd.plus(marketValueLowerBound.multipliedBy(new BigNumber(config.openLtvPct / 100)));
328
- unhealthyBorrowValueUsd = unhealthyBorrowValueUsd.plus(marketValue.multipliedBy(new BigNumber(config.closeLtvPct / 100)));
329
- obligation.deposits[i] = deposit;
330
- }
331
- obligation.unhealthyBorrowValueUsd = stringToDecimal(unhealthyBorrowValueUsd.toString());
332
- obligation.allowedBorrowValueUsd = stringToDecimal(allowedBorrowValueUsd.toString());
333
- obligation.depositedValueUsd = stringToDecimal(depositValueUsd.toString());
334
- // Refresh borrows
349
+ unhealthyBorrowValueUsd = unhealthyBorrowValueUsd.plus(marketValue.multipliedBy(computeCloseLtv(config)));
350
+ return deposit;
351
+ });
335
352
  let unweightedBorrowedValueUsd = new BigNumber(0);
336
353
  let weightedBorrowedValueUsd = new BigNumber(0);
337
354
  let weightedBorrowedValueUpperBoundUsd = new BigNumber(0);
338
- for (let i = 0; i < obligation.borrows.length; i++) {
339
- const unrefreshedBorrow = obligation.borrows[i];
340
- const reserve = refreshedReserves.find((r) => r.coinType.name == unrefreshedBorrow.coinType.name);
341
- if (!reserve) {
342
- throw new Error(`Unable to find reserve for coin type ${unrefreshedBorrow.coinType.name}`);
343
- }
355
+ const borrows = obligation.borrows.map((unrefreshedBorrow) => {
356
+ const reserve = findReserveOrThrow(refreshedReserves, unrefreshedBorrow.coinType.name);
344
357
  const config = reserve.config.element;
345
358
  const borrow = {
346
359
  ...compoundDebt(unrefreshedBorrow, reserve),
347
360
  };
348
- obligation.userRewardManagers[Number(borrow.userRewardManagerIndex)] =
349
- updateUserRewardManager(reserve.borrowsPoolRewardManager, obligation.userRewardManagers[Number(borrow.userRewardManagerIndex)], Date.now());
361
+ userRewardManagers[Number(borrow.userRewardManagerIndex)] =
362
+ updateUserRewardManager(reserve.borrowsPoolRewardManager, userRewardManagers[Number(borrow.userRewardManagerIndex)], nowMs);
350
363
  const marketValue = decimalToBigNumber(borrow.borrowedAmount)
351
364
  .multipliedBy(decimalToBigNumber(reserve.price))
352
365
  .dividedBy(new BigNumber(10 ** reserve.mintDecimals));
353
366
  const upperBoundPrice = BigNumber.max(decimalToBigNumber(reserve.price), decimalToBigNumber(reserve.smoothedPrice));
354
- const marketValueUpperBound = decimalToBigNumber(reserve.borrowedAmount)
367
+ // `borrow.borrowedAmount`, not `reserve.borrowedAmount`: the reserve field
368
+ // is the whole reserve's debt, so using it scaled every borrower's upper
369
+ // bound by total reserve borrows. `obligation.move` passes
370
+ // `borrow.borrowed_amount` to both `market_value` and
371
+ // `market_value_upper_bound`, and this is what `is_healthy` tests — an
372
+ // obligation owing $100 of a reserve with $22M borrowed read as $22M.
373
+ const marketValueUpperBound = decimalToBigNumber(borrow.borrowedAmount)
355
374
  .multipliedBy(upperBoundPrice)
356
375
  .dividedBy(new BigNumber(10 ** reserve.mintDecimals));
357
376
  borrow.marketValue = stringToDecimal(marketValue.toString());
358
377
  unweightedBorrowedValueUsd = unweightedBorrowedValueUsd.plus(marketValue);
359
- const borrowWeight = new BigNumber((config.borrowWeightBps / BigInt(10000)).toString());
378
+ const borrowWeight = computeBorrowWeight(config);
360
379
  weightedBorrowedValueUsd = weightedBorrowedValueUsd.plus(marketValue.multipliedBy(borrowWeight));
361
380
  weightedBorrowedValueUpperBoundUsd =
362
381
  weightedBorrowedValueUpperBoundUsd.plus(marketValueUpperBound.multipliedBy(borrowWeight));
363
- }
364
- obligation.unweightedBorrowedValueUsd = stringToDecimal(unweightedBorrowedValueUsd.toString());
365
- obligation.weightedBorrowedValueUpperBoundUsd = stringToDecimal(weightedBorrowedValueUpperBoundUsd.toString());
366
- obligation.weightedBorrowedValueUsd = stringToDecimal(weightedBorrowedValueUsd.toString());
367
- return obligation;
382
+ return borrow;
383
+ });
384
+ return {
385
+ deposits,
386
+ borrows,
387
+ userRewardManagers: userRewardManagers,
388
+ depositedValueUsd: depositValueUsd,
389
+ allowedBorrowValueUsd,
390
+ unhealthyBorrowValueUsd,
391
+ unweightedBorrowedValueUsd,
392
+ weightedBorrowedValueUsd,
393
+ weightedBorrowedValueUpperBoundUsd,
394
+ };
395
+ }
396
+ export const refreshObligation = (unrefreshedObligation, refreshedReserves) => {
397
+ const refresh = computeObligationRefresh(unrefreshedObligation, refreshedReserves, Date.now());
398
+ return {
399
+ ...unrefreshedObligation,
400
+ deposits: refresh.deposits,
401
+ borrows: refresh.borrows,
402
+ userRewardManagers: refresh.userRewardManagers,
403
+ depositedValueUsd: stringToDecimal(refresh.depositedValueUsd.toString()),
404
+ allowedBorrowValueUsd: stringToDecimal(refresh.allowedBorrowValueUsd.toString()),
405
+ unhealthyBorrowValueUsd: stringToDecimal(refresh.unhealthyBorrowValueUsd.toString()),
406
+ unweightedBorrowedValueUsd: stringToDecimal(refresh.unweightedBorrowedValueUsd.toString()),
407
+ weightedBorrowedValueUsd: stringToDecimal(refresh.weightedBorrowedValueUsd.toString()),
408
+ weightedBorrowedValueUpperBoundUsd: stringToDecimal(refresh.weightedBorrowedValueUpperBoundUsd.toString()),
409
+ };
368
410
  };
369
411
  export const numberToDecimal = (value) => {
370
412
  const adjustedValue = Math.round(value * +WAD);
@@ -400,13 +442,62 @@ export const totalSupply = (reserve) => {
400
442
  .plus(decimalToBigNumber(reserve.borrowedAmount))
401
443
  .minus(decimalToBigNumber(reserve.unclaimedSpreadFees));
402
444
  };
445
+ // A snapshot's cumulative borrow rate is only ever a divisor here because
446
+ // on-chain it's initialized to 1 and monotonically increases — never 0. That
447
+ // invariant is enforced on-chain, not by this function's callers (one of
448
+ // them reads a Postgres snapshot, not a live `Reserve`), so it's checked
449
+ // here rather than assumed.
450
+ export const compoundBorrowedAmount = (snapshotAmount, currentCumulativeBorrowRate, snapshotCumulativeBorrowRate) => {
451
+ if (snapshotCumulativeBorrowRate.isZero()) {
452
+ throw new Error("Cannot compound a borrow whose snapshot cumulativeBorrowRate is 0");
453
+ }
454
+ return snapshotAmount.multipliedBy(currentCumulativeBorrowRate.dividedBy(snapshotCumulativeBorrowRate));
455
+ };
403
456
  export const compoundDebt = (borrow, reserve) => {
404
457
  const borrowCopy = { ...borrow };
405
458
  const newCumulativeBorrowRate = decimalToBigNumber(reserve.cumulativeBorrowRate);
406
- const compoundedInterestRate = newCumulativeBorrowRate.dividedBy(decimalToBigNumber(borrow.cumulativeBorrowRate));
407
- borrowCopy.borrowedAmount = stringToDecimal(decimalToBigNumber(borrow.borrowedAmount)
408
- .multipliedBy(compoundedInterestRate)
409
- .toString());
459
+ borrowCopy.borrowedAmount = stringToDecimal(compoundBorrowedAmount(decimalToBigNumber(borrow.borrowedAmount), newCumulativeBorrowRate, decimalToBigNumber(borrow.cumulativeBorrowRate)).toString());
410
460
  borrowCopy.cumulativeBorrowRate = stringToDecimal(newCumulativeBorrowRate.toString());
411
461
  return borrowCopy;
412
462
  };
463
+ /**
464
+ * The health-relevant subset of `refreshObligation` (deposit/borrow valuation
465
+ * and the two USD thresholds `shouldAttemptLiquidations` compares), computed
466
+ * from stored position state rather than a live on-chain `Obligation`.
467
+ *
468
+ * Deliberately does NOT touch reward-manager state (`refreshObligation`'s
469
+ * `updateUserRewardManager` calls) — that state isn't stored in `Positions`
470
+ * and doesn't participate in either threshold (LEN-576). Deliberately does
471
+ * NOT compute `allowedBorrowValueUsd`/`unweightedBorrowedValueUsd`/
472
+ * `weightedBorrowedValueUpperBoundUsd` — those exist for the risky-subset
473
+ * re-scan optimization in `obligationIsRisky`, which has no equivalent here
474
+ * (this function is cheap enough to run over every obligation every cycle).
475
+ *
476
+ * All math stays in BigNumber end-to-end — never construct a `Decimal` from
477
+ * a stored quantity; `numberToDecimal`/`stringToDecimal` round through a JS
478
+ * `number` (`Math.round`) and are lossy at WAD scale. `decimalToBigNumber` is
479
+ * only ever called on a genuine on-chain `Reserve` field below.
480
+ */
481
+ export function computeObligationHealthFromPositions(deposits, borrows, refreshedReserves) {
482
+ let depositValueUsd = new BigNumber(0);
483
+ let unhealthyBorrowValueUsd = new BigNumber(0);
484
+ for (const deposit of deposits) {
485
+ const reserve = findReserveOrThrow(refreshedReserves, deposit.coinType);
486
+ const config = reserve.config.element;
487
+ const marketValue = getCTokenMarketValue(reserve, deposit.depositedCtokenAmount);
488
+ depositValueUsd = depositValueUsd.plus(marketValue);
489
+ unhealthyBorrowValueUsd = unhealthyBorrowValueUsd.plus(marketValue.multipliedBy(computeCloseLtv(config)));
490
+ }
491
+ let weightedBorrowedValueUsd = new BigNumber(0);
492
+ for (const borrow of borrows) {
493
+ const reserve = findReserveOrThrow(refreshedReserves, borrow.coinType);
494
+ const config = reserve.config.element;
495
+ const currentBorrowedAmountUnderlying = compoundBorrowedAmount(borrow.borrowedAmountUnderlying, decimalToBigNumber(reserve.cumulativeBorrowRate), borrow.cumulativeBorrowRateAtSnapshot);
496
+ const marketValue = currentBorrowedAmountUnderlying
497
+ .multipliedBy(decimalToBigNumber(reserve.price))
498
+ .dividedBy(new BigNumber(10 ** reserve.mintDecimals));
499
+ const borrowWeight = computeBorrowWeight(config);
500
+ weightedBorrowedValueUsd = weightedBorrowedValueUsd.plus(marketValue.multipliedBy(borrowWeight));
501
+ }
502
+ return { depositedValueUsd: depositValueUsd, unhealthyBorrowValueUsd, weightedBorrowedValueUsd };
503
+ }