@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 +4 -0
- package/package.json +1 -1
- package/utils/obligation.d.ts +1 -0
- package/utils/simulate.d.ts +64 -1
- package/utils/simulate.js +131 -40
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.
|
|
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"}
|
package/utils/obligation.d.ts
CHANGED
|
@@ -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>;
|
package/utils/simulate.d.ts
CHANGED
|
@@ -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 = {
|
|
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 >=
|
|
281
|
-
|
|
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
|
|
309
|
-
const
|
|
310
|
-
|
|
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
|
-
|
|
315
|
-
const deposit = { ...
|
|
316
|
-
const reserve = refreshedReserves
|
|
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
|
-
|
|
322
|
-
updateUserRewardManager(reserve.depositsPoolRewardManager,
|
|
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(
|
|
329
|
-
|
|
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
|
-
|
|
339
|
-
const
|
|
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
|
-
|
|
349
|
-
updateUserRewardManager(reserve.borrowsPoolRewardManager,
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
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
|
+
}
|