void;
+ showBorrowLimitTooltip: boolean;
+ showWeightedBorrowTooltip: boolean;
+ showLiquidationThresholdTooltip: boolean;
+ showBreakdown: boolean;
+ stateOpen: boolean;
+}): ReactElement {
const [obligation] = useAtom(selectedObligationAtom);
- if (!obligation) return
;
+ const usedObligation = obligation ?? {
+ totalSupplyValue: new BigNumber(0),
+ totalBorrowValue: new BigNumber(0),
+ borrowLimit: new BigNumber(0),
+ liquidationThreshold: new BigNumber(0),
+ borrowOverSupply: new BigNumber(0),
+ borrowLimitOverSupply: new BigNumber(0),
+ liquidationThresholdFactor: new BigNumber(0),
+ weightedTotalBorrowValue: new BigNumber(0),
+ weightedBorrowUtilization: new BigNumber(0),
+ };
+
+ const borrowLimitOverSupply = usedObligation.totalSupplyValue.isZero()
+ ? new BigNumber(0)
+ : usedObligation.borrowLimit.dividedBy(usedObligation.totalSupplyValue);
+
+ const weightedBorrowOverSupply = usedObligation.totalSupplyValue.isZero()
+ ? new BigNumber(0)
+ : usedObligation.weightedTotalBorrowValue.dividedBy(
+ usedObligation.totalSupplyValue,
+ );
const passedLimit =
- obligation.totalSupplyValue.isZero() ||
- (!obligation.totalBorrowValue.isZero() &&
- obligation.totalBorrowValue.isGreaterThanOrEqualTo(
- obligation.borrowLimit,
+ usedObligation.totalSupplyValue.isZero() ||
+ (!usedObligation.weightedTotalBorrowValue.isZero() &&
+ usedObligation.weightedTotalBorrowValue.isGreaterThanOrEqualTo(
+ usedObligation.borrowLimit,
));
const passedThreshold =
- obligation.totalSupplyValue.isZero() ||
- (!obligation.totalBorrowValue.isZero() &&
- obligation.totalBorrowValue.isGreaterThanOrEqualTo(
- obligation.liquidationThreshold,
+ usedObligation.totalSupplyValue.isZero() ||
+ (!usedObligation.weightedTotalBorrowValue.isZero() &&
+ usedObligation.weightedTotalBorrowValue.isGreaterThanOrEqualTo(
+ usedObligation.liquidationThreshold,
));
- // 2% reserved for the bars
- const denominator = 97 + (passedLimit ? 1 : 0) + (passedThreshold ? 1 : 0);
+ // 3% reserved for the bars
+ const denominator =
+ 97 + (passedLimit ? 1.5 : 0) + (passedThreshold ? 1.5 : 0);
const borrowWidth = Math.min(
100,
- Number(Number(obligation.borrowOverSupply.toString()).toFixed(4)) *
+ Number(Number(usedObligation.borrowOverSupply.toString()).toFixed(4)) *
denominator,
);
+
+ const liquidationThresholdFactor = usedObligation.totalSupplyValue.isZero()
+ ? BigNumber(0)
+ : usedObligation.liquidationThreshold.dividedBy(
+ usedObligation.totalSupplyValue,
+ );
+
+ const weightedBorrowWidth =
+ Math.min(
+ 100,
+ Number(Number(weightedBorrowOverSupply.toString()).toFixed(4)) *
+ denominator,
+ ) - borrowWidth;
+
+ const totalBorrowWidth = borrowWidth + weightedBorrowWidth;
+
const unborrowedWidth =
Number(
Number(
- obligation.totalSupplyValue.isZero()
+ usedObligation.totalSupplyValue.isZero()
? BigNumber(0)
: BigNumber.max(
- obligation.borrowLimit.minus(obligation.totalBorrowValue),
+ usedObligation.borrowLimit.minus(
+ usedObligation.weightedTotalBorrowValue,
+ ),
BigNumber(0),
)
- .dividedBy(obligation.totalSupplyValue)
+ .dividedBy(usedObligation.totalSupplyValue)
.toString(),
).toFixed(4),
) * denominator;
const unliquidatedWidth =
Number(
Number(
- obligation.totalSupplyValue.isZero()
+ usedObligation.totalSupplyValue.isZero()
? BigNumber(0)
: BigNumber.max(
- obligation.liquidationThreshold.minus(
+ usedObligation.liquidationThreshold.minus(
BigNumber.max(
- obligation.borrowLimit,
- obligation.totalBorrowValue,
+ usedObligation.borrowLimit,
+ usedObligation.weightedTotalBorrowValue,
),
),
BigNumber(0),
)
- .dividedBy(obligation.totalSupplyValue)
+ .dividedBy(usedObligation.totalSupplyValue)
.toString(),
).toFixed(4),
) * denominator;
const unusedSupply =
- denominator - borrowWidth - unborrowedWidth - unliquidatedWidth;
+ denominator - totalBorrowWidth - unborrowedWidth - unliquidatedWidth;
- let borrowToolTip = `You are borrowing ${formatPercent(
- obligation.borrowOverSupply.toString(),
+ let borrowToolTip = `Your weighted borrow balance is ${formatPercent(
+ weightedBorrowOverSupply.toString(),
)} of your total supply, or ${formatPercent(
- obligation.borrowUtilization.toString(),
+ usedObligation.weightedBorrowUtilization.toString(),
)} of your borrow limit.`;
+
if (passedLimit) {
borrowToolTip =
- 'Your borrow balance is past the borrow limit and could be at risk of liquidation. Please repay your borrow balance or supply more assets.';
+ 'Your weighted borrow balance is past the borrow limit and could be at risk of liquidation. Please repay your borrows or supply more assets.';
}
+
if (passedThreshold) {
borrowToolTip =
- 'Your borrow balance is past the liquidation threshold and could be liquidated.';
+ 'Your weighted borrow balance is past the liquidation threshold and could be liquidated.';
}
+ const unweightedBorrowTooltip = `This portion represents the actual value of your borrows. However, certain assets have a borrow weight that changes their value during liquidation or borrow limit calculations.`;
+
return (
-
-
+
+ {showBreakdown && (
+
+ )}
+ {showBreakdown && (
+
+ )}
+ {!showBreakdown && (
+
+ )}
{!passedLimit && (
)}
{!passedLimit && (
)}
@@ -142,10 +243,12 @@ function UtilizationBar(): ReactElement {
{!passedThreshold && (
)}
diff --git a/solend-lite/src/stores/obligations.ts b/solend-lite/src/stores/obligations.ts
index 35370620..df72533a 100644
--- a/solend-lite/src/stores/obligations.ts
+++ b/solend-lite/src/stores/obligations.ts
@@ -1,6 +1,6 @@
import BigNumber from 'bignumber.js';
import { atom } from 'jotai';
-import { atomFamily, loadable } from 'jotai/utils';
+import { atomFamily, atomWithDefault, loadable } from 'jotai/utils';
import { poolsFamily, poolsWithMetaDataAtom, selectedPoolAtom } from './pools';
import { publicKeyAtom } from './wallet';
import { connectionAtom } from './settings';
@@ -86,6 +86,7 @@ const obligationsFamily = atomFamily((address: string) =>
(get) => {
const rawObligation = get(rawObligationsAtom)[address];
if (!rawObligation?.info) return null;
+
const pool = get(
poolsFamily(rawObligation.info.lendingMarket.toBase58()),
);
@@ -105,7 +106,14 @@ const obligationsFamily = atomFamily((address: string) =>
),
);
-export const selectedObligationAddressAtom = atom
(null);
+export const selectedObligationAddressAtom = atomWithDefault(
+ () => {
+ const queryParams = new URLSearchParams(window.location.search);
+ const poolParam = queryParams.get('obligation');
+
+ return poolParam;
+ },
+);
export const selectedObligationAtom = atom(
(get) => {
diff --git a/solend-lite/src/stores/pools.ts b/solend-lite/src/stores/pools.ts
index 8151e904..ed079a59 100644
--- a/solend-lite/src/stores/pools.ts
+++ b/solend-lite/src/stores/pools.ts
@@ -17,8 +17,11 @@ import {
ReserveType,
fetchPools,
getReservesOfPool,
+ parseLendingMarket,
+ parseRateLimiter,
} from '@solendprotocol/solend-sdk';
import { DEBUG_MODE, PROGRAM_ID } from 'common/config';
+import { atomWithRefresh } from './shared';
export type ReserveWithMetadataType = ReserveType & {
symbol: string;
@@ -78,6 +81,7 @@ export const loadPoolsAtom = atom(
async (get, set) => {
const [connection, pools] = get(waitForAll([connectionAtom, poolsAtom]));
const switchboardProgram = get(switchboardAtom);
+ const currentSlot = get(currentSlotAtom);
set(
poolsAtom,
@@ -86,6 +90,7 @@ export const loadPoolsAtom = atom(
connection,
switchboardProgram,
PROGRAM_ID,
+ currentSlot,
DEBUG_MODE,
),
);
@@ -137,6 +142,31 @@ export const poolsWithMetaDataAtom = atom((get) => {
);
});
+export const currentSlotAtom = atomWithRefresh(async (get) => {
+ const connection = get(connectionAtom);
+ return connection.getSlot();
+});
+
+export const rateLimiterAtom = atom(async (get) => {
+ const selectedPoolAddress = get(selectedPoolAddressAtom);
+ const connection = get(connectionAtom);
+ if (!selectedPoolAddress) return null;
+ const currentSlot = get(currentSlotAtom);
+ const pool = await connection.getAccountInfo(
+ new PublicKey(selectedPoolAddress),
+ );
+ if (pool) {
+ const raterLimiter = parseLendingMarket(
+ new PublicKey(selectedPoolAddress),
+ pool,
+ ).info.rateLimiter;
+
+ return parseRateLimiter(raterLimiter, currentSlot);
+ }
+
+ return null;
+});
+
export const selectedPoolAddressAtom = atomWithDefault((get) => {
const config = get(configAtom);
const queryParams = new URLSearchParams(window.location.search);
@@ -186,12 +216,14 @@ export const selectedPoolAtom = atom(
PROGRAM_ID,
);
}
+ const currentSlot = get(currentSlotAtom);
getReservesOfPool(
new PublicKey(newSelectedPoolAddress),
connection,
switchboardProgram,
PROGRAM_ID,
+ currentSlot,
DEBUG_MODE,
);
diff --git a/solend-lite/src/utils/numberFormatter.tsx b/solend-lite/src/utils/numberFormatter.tsx
index 363ef840..90da7fba 100644
--- a/solend-lite/src/utils/numberFormatter.tsx
+++ b/solend-lite/src/utils/numberFormatter.tsx
@@ -2,6 +2,43 @@ import React, { ReactNode } from 'react';
import BigNumber from 'bignumber.js';
import { Tooltip, Text } from '@chakra-ui/react';
+export function collapsableUsd(value: string, maxLength: number) {
+ const bn = new BigNumber(value);
+
+ if (bn.isLessThan(0.01) && !bn.isLessThanOrEqualTo(new BigNumber(0))) {
+ return '< $0.01';
+ }
+
+ const usdString = formatUsd(value);
+
+ return (
+
+ {usdString.length > maxLength ? `$${formatCompact(bn)}` : usdString}
+
+ );
+}
+
+export function collapsableToken(
+ value: string,
+ decimals: number,
+ maxLength: number,
+) {
+ const bn = new BigNumber(value);
+ if (bn.isLessThan(0.0001) && !bn.isLessThanOrEqualTo(new BigNumber(0))) {
+ return '< 0.0001';
+ }
+
+ const valString = formatToken(value, decimals) as string;
+
+ return (
+
+ {valString.length > maxLength
+ ? formatCompact(new BigNumber(valString))
+ : valString}
+
+ );
+}
+
export function formatExact(value: string | number | BigNumber) {
const bignum = new BigNumber(value);
return bignum.isNaN() ? '0' : bignum.toFormat();
@@ -11,7 +48,7 @@ export function formatToken(
value: string | number | BigNumber,
digits = 4,
exactTip?: boolean,
- noTrim?: boolean,
+ trim?: boolean,
// by default we truncate for tokens
round?: boolean,
exact?: boolean,
@@ -31,7 +68,9 @@ export function formatToken(
);
}
- const contents = bn.toFormat(digits, round ? 4 : 1);
+ const contents = trim
+ ? `${Number.parseFloat(bn.toFixed(digits))}`
+ : bn.toFormat(digits, round ? 4 : 1);
if (bn.eq(0)) return '0';
diff --git a/solend-lite/src/utils/utils.ts b/solend-lite/src/utils/utils.ts
index de0b7537..8a2489f0 100644
--- a/solend-lite/src/utils/utils.ts
+++ b/solend-lite/src/utils/utils.ts
@@ -8,13 +8,15 @@ export function runIfFn(
return isFunction(valueOrFn) ? valueOrFn(...args) : valueOrFn;
}
+export const SLOT_RATE = 2;
+
const errorList = [
'Failed to unpack instruction data',
'Account is already initialized',
'Lamport balance below rent-exempt threshold',
'Market authority is invalid',
'Market owner is invalid',
- 'Input account owner is not the program address',
+ 'Input account owner is not the program plusress',
'Input token account is not owned by the correct token program id',
'Input token account is not valid',
'Input token mint account is not valid',
diff --git a/solend-sdk/src/core/utils/obligations.ts b/solend-sdk/src/core/utils/obligations.ts
index 436126f5..1b9f325b 100644
--- a/solend-sdk/src/core/utils/obligations.ts
+++ b/solend-sdk/src/core/utils/obligations.ts
@@ -3,12 +3,16 @@ import BigNumber from "bignumber.js";
import { Obligation, parseObligation } from "../../state";
import { PoolType } from "../types";
import { getBatchMultipleAccountsInfo } from "./utils";
+import { U64_MAX } from "../../classes/constants";
export function formatObligation(
obligation: { pubkey: PublicKey; info: Obligation },
pool: PoolType
) {
const poolAddress = obligation.info.lendingMarket.toBase58();
+ let minPriceUserTotalSupply = new BigNumber(0);
+ let minPriceBorrowLimit = new BigNumber(0);
+ let maxPriceUserTotalWeightedBorrow = new BigNumber(0);
const deposits = obligation.info.deposits
.filter((d) => d.depositedAmount.toString() !== "0")
@@ -24,6 +28,16 @@ export function formatObligation(
.times(reserve.cTokenExchangeRate);
const amountUsd = amount.times(reserve.price);
+ minPriceUserTotalSupply = minPriceUserTotalSupply.plus(
+ amount.times(reserve.minPrice),
+ );
+
+ minPriceBorrowLimit = minPriceBorrowLimit.plus(
+ amount
+ .times(reserve.minPrice)
+ .times(reserve.loanToValueRatio),
+ );
+
return {
liquidationThreshold: reserve.liquidationThreshold,
loanToValueRatio: reserve.loanToValueRatio,
@@ -51,6 +65,18 @@ export function formatObligation(
);
const amountUsd = amount.times(reserve.price);
+ const maxPrice = reserve.emaPrice ? BigNumber.max(reserve.emaPrice, reserve.price) : reserve.price;
+
+ maxPriceUserTotalWeightedBorrow = maxPriceUserTotalWeightedBorrow.plus(
+ amount
+ .times(maxPrice)
+ .times(
+ reserve.borrowWeight
+ ? reserve.borrowWeight
+ : U64_MAX,
+ ),
+ );
+
return {
liquidationThreshold: reserve.liquidationThreshold,
loanToValueRatio: reserve.loanToValueRatio,
@@ -59,9 +85,10 @@ export function formatObligation(
reserveAddress,
amount,
amountUsd,
+ weightedAmountUsd: reserve.borrowWeight.multipliedBy(amountUsd)
};
});
-
+
const totalSupplyValue = deposits.reduce(
(acc, d) => acc.plus(d.amountUsd),
new BigNumber(0)
@@ -70,6 +97,10 @@ export function formatObligation(
(acc, b) => acc.plus(b.amountUsd),
new BigNumber(0)
);
+ const weightedTotalBorrowValue = borrows.reduce(
+ (acc, b) => acc.plus(b.weightedAmountUsd),
+ new BigNumber(0)
+ );
const borrowLimit = deposits.reduce(
(acc, d) => d.amountUsd.times(d.loanToValueRatio).plus(acc),
@@ -89,21 +120,26 @@ export function formatObligation(
const borrowUtilization = borrowLimit.isZero()
? new BigNumber(0)
: totalBorrowValue.dividedBy(borrowLimit);
+ const weightedBorrowUtilization = minPriceBorrowLimit.isZero()
+ ? new BigNumber(0)
+ : weightedTotalBorrowValue.dividedBy(borrowLimit);
const isBorrowLimitReached = borrowUtilization.isGreaterThanOrEqualTo(
new BigNumber("1")
);
const borrowOverSupply = totalSupplyValue.isZero()
? new BigNumber(0)
: totalBorrowValue.dividedBy(totalSupplyValue);
- const borrowLimitOverSupply = totalSupplyValue.isZero()
- ? new BigNumber(0)
- : borrowLimit.dividedBy(totalSupplyValue);
const positions =
obligation.info.deposits.filter((d) => !d.depositedAmount.isZero()).length +
obligation.info.borrows.filter((b) => !b.borrowedAmountWads.isZero())
.length;
+
+ const weightedConservativeBorrowUtilization = minPriceBorrowLimit.isZero()
+ ? new BigNumber(0)
+ : maxPriceUserTotalWeightedBorrow.dividedBy(minPriceBorrowLimit);
+
return {
address: obligation.pubkey.toBase58(),
positions,
@@ -118,9 +154,14 @@ export function formatObligation(
liquidationThresholdFactor,
borrowLimitFactor,
borrowUtilization,
+ weightedConservativeBorrowUtilization,
+ weightedBorrowUtilization,
isBorrowLimitReached,
borrowOverSupply,
- borrowLimitOverSupply,
+ weightedTotalBorrowValue,
+ minPriceUserTotalSupply,
+ minPriceBorrowLimit,
+ maxPriceUserTotalWeightedBorrow,
};
}
diff --git a/solend-sdk/src/core/utils/pools.ts b/solend-sdk/src/core/utils/pools.ts
index 27df309c..f89b0c81 100644
--- a/solend-sdk/src/core/utils/pools.ts
+++ b/solend-sdk/src/core/utils/pools.ts
@@ -5,16 +5,18 @@ import { fetchPrices } from "./prices";
import { calculateBorrowInterest, calculateSupplyInterest } from "./rates";
import { PoolType, ReserveType } from "../types";
import { parseReserve, Reserve } from "../../state";
+import { parseRateLimiter, remainingOutflow } from "./utils";
export async function fetchPools(
oldPools: Array,
connection: Connection,
switchboardProgram: SwitchboardProgram,
programId: string,
+ currentSlot: number,
debug?: boolean
) {
const reserves = (
- await getReservesFromChain(connection, switchboardProgram, programId, debug)
+ await getReservesFromChain(connection, switchboardProgram, programId, currentSlot, debug)
).sort((a, b) => (a.totalSupply.isGreaterThan(b.totalSupply) ? -1 : 1));
const pools = Object.fromEntries(
@@ -45,7 +47,11 @@ export function formatReserve(
pubkey: PublicKey;
info: Reserve;
},
- price: number | null
+ price: {
+ spotPrice: number,
+ emaPrice: number,
+ } | undefined,
+ currentSlot: number,
) {
const decimals = reserve.info.liquidity.mintDecimals;
const availableAmount = new BigNumber(
@@ -62,7 +68,7 @@ export function formatReserve(
.minus(accumulatedProtocolFees);
const address = reserve.pubkey.toBase58();
const priceResolved = price
- ? BigNumber(price)
+ ? BigNumber(price.spotPrice)
: new BigNumber(reserve.info.liquidity.marketPrice.toString()).shiftedBy(
-18
);
@@ -114,6 +120,7 @@ export function formatReserve(
totalSupply,
totalBorrow,
availableAmount,
+ rateLimiter: parseRateLimiter(reserve.info.rateLimiter, currentSlot),
totalSupplyUsd: totalSupply.times(priceResolved),
totalBorrowUsd: totalBorrow.times(priceResolved),
availableAmountUsd: availableAmount.times(priceResolved),
@@ -131,6 +138,11 @@ export function formatReserve(
poolAddress: reserve.info.lendingMarket.toBase58(),
pythOracle: reserve.info.liquidity.pythOracle.toBase58(),
switchboardOracle: reserve.info.liquidity.switchboardOracle.toBase58(),
+ addedBorrowWeightBPS: reserve.info.config.addedBorrowWeightBPS,
+ borrowWeight: reserve.info.config.borrowWeight,
+ emaPrice: price?.emaPrice,
+ minPrice: ((price?.emaPrice && price?.spotPrice) ? BigNumber.min(price.emaPrice, price.spotPrice) : new BigNumber(price?.spotPrice ?? priceResolved)),
+ maxPrice: ((price?.emaPrice && price?.spotPrice) ? BigNumber.max(price.emaPrice, price.spotPrice) : new BigNumber(price?.spotPrice ?? priceResolved)) ,
};
}
@@ -139,6 +151,7 @@ export const getReservesOfPool = async (
connection: Connection,
switchboardProgram: SwitchboardProgram,
programId: string,
+ currentSlot: number,
debug?: boolean
) => {
if (debug) console.log("getReservesOfPool");
@@ -171,7 +184,7 @@ export const getReservesOfPool = async (
);
return parsedReserves.map((r) =>
- formatReserve(r, prices[r.pubkey.toBase58()])
+ formatReserve(r, prices[r.pubkey.toBase58()], currentSlot)
);
};
@@ -179,6 +192,7 @@ export const getReservesFromChain = async (
connection: Connection,
switchboardProgram: SwitchboardProgram,
programId: string,
+ currentSlot: number,
debug?: boolean
) => {
if (debug) console.log("getReservesFromChain");
@@ -208,6 +222,6 @@ export const getReservesFromChain = async (
);
return parsedReserves.map((r) =>
- formatReserve(r, prices[r.pubkey.toBase58()])
+ formatReserve(r, prices[r.pubkey.toBase58()], currentSlot)
);
};
diff --git a/solend-sdk/src/core/utils/prices.ts b/solend-sdk/src/core/utils/prices.ts
index 12631cb9..d1e4c8f7 100644
--- a/solend-sdk/src/core/utils/prices.ts
+++ b/solend-sdk/src/core/utils/prices.ts
@@ -25,16 +25,22 @@ export async function fetchPrices(
const pythOracleData = priceAccounts[i];
const switchboardOracleData = priceAccounts[parsedReserves.length + i];
- let priceData: number | undefined;
+ let priceData: {
+ spotPrice: number,
+ emaPrice: number,
+ } | undefined;
if (pythOracleData) {
- const { price, previousPrice } = parsePriceData(
+ const { price, previousPrice, emaPrice } = parsePriceData(
pythOracleData.data as Buffer
);
if (price || previousPrice) {
- // use latest price if available otherwise fallback to previoius
- priceData = price || previousPrice;
+ // use latest price if available otherwise fallback to previous
+ priceData = {
+ spotPrice: (price || previousPrice),
+ emaPrice: emaPrice?.value ?? (price || previousPrice),
+ };
}
}
@@ -47,7 +53,10 @@ export async function fetchPrices(
if (owner === SBV2_MAINNET) {
const result = switchboardProgram.decodeLatestAggregatorValue(rawSb!);
- priceData = result?.toNumber();
+ priceData = {
+ spotPrice: result?.toNumber() ?? 0,
+ emaPrice: result?.toNumber() ?? 0,
+ };
}
}
}
@@ -56,5 +65,8 @@ export async function fetchPrices(
...acc,
[reserve.pubkey.toBase58()]: priceData,
};
- }, {}) as { [address: string]: number };
+ }, {}) as { [address: string]: {
+ spotPrice: number,
+ emaPrice: number,
+ } | undefined };
}
diff --git a/solend-sdk/src/core/utils/utils.ts b/solend-sdk/src/core/utils/utils.ts
index dda38729..aef5676e 100644
--- a/solend-sdk/src/core/utils/utils.ts
+++ b/solend-sdk/src/core/utils/utils.ts
@@ -1,7 +1,84 @@
import { Connection, PublicKey } from "@solana/web3.js";
+import { ParsedRateLimiter, RateLimiter, RateLimiterConfig } from "../../state/rateLimiter";
+import BigNumber from "bignumber.js";
+import BN from "bn.js";
const ADDRESS_PREFIX_SUFFIX_LENGTH = 6;
+export const OUTFLOW_BUFFER = 0.985;
+
+export const parseRateLimiter = (rateLimiter: RateLimiter, currentSlot: number) => {
+ const convertedRateLimiter = {
+ config: {
+ windowDuration: new BigNumber(
+ rateLimiter.config.windowDuration.toString(),
+ ),
+ maxOutflow: new BigNumber(
+ rateLimiter.config.maxOutflow.toString(),
+ ),
+ },
+ windowStart: new BigNumber(rateLimiter.windowStart.toString()),
+ previousQuantity: new BigNumber(
+ rateLimiter.previousQuantity.toString(),
+ ).shiftedBy(-18),
+ currentQuantity: new BigNumber(
+ rateLimiter.currentQuantity.toString(),
+ ).shiftedBy(-18),
+ }
+ return {
+ ...convertedRateLimiter,
+ remainingOutflow: remainingOutflow(currentSlot, convertedRateLimiter)
+}};
+
+
+export const remainingOutflow = (
+ currentSlot: number,
+ rateLimiter: {
+ config: {
+ windowDuration: BigNumber,
+ maxOutflow: BigNumber,
+ },
+ windowStart: BigNumber,
+ previousQuantity: BigNumber,
+ currentQuantity: BigNumber,
+ },
+) => {
+ if (rateLimiter.config.windowDuration.eq(new BigNumber(0))) {
+ return null;
+ }
+
+ const curSlot = new BigNumber(currentSlot);
+ const windowDuration = rateLimiter.config.windowDuration
+ const previousQuantity = rateLimiter.previousQuantity;
+ const currentQuantity = rateLimiter.currentQuantity;
+ const maxOutflow = rateLimiter.config.maxOutflow;
+ const windowStart = rateLimiter.windowStart;
+
+ const curSlotStart = curSlot
+ .dividedBy(windowDuration)
+ .integerValue(BigNumber.ROUND_FLOOR)
+ .times(windowDuration);
+
+ const prevWeight = windowDuration
+ .minus(curSlot.minus(curSlotStart).plus(new BigNumber(1)))
+ .dividedBy(windowDuration);
+ let outflow = new BigNumber(0);
+
+ if (windowStart.isEqualTo(curSlotStart)) {
+ const curOutflow = prevWeight.times(
+ previousQuantity.plus(currentQuantity),
+ );
+ outflow = maxOutflow.plus(curOutflow);
+ } else if (windowStart.plus(windowDuration).isEqualTo(curSlotStart)) {
+ const curOutflow = prevWeight.times(currentQuantity);
+ outflow = maxOutflow.minus(curOutflow);
+ } else {
+ outflow = maxOutflow;
+ }
+
+ return outflow.times(new BigNumber(OUTFLOW_BUFFER));
+};
+
export const formatAddress = (address: string, length?: number) => {
return `${address.slice(
0,
diff --git a/solend-sdk/src/state/rateLimiter.ts b/solend-sdk/src/state/rateLimiter.ts
index 7e56b1e3..519b2444 100644
--- a/solend-sdk/src/state/rateLimiter.ts
+++ b/solend-sdk/src/state/rateLimiter.ts
@@ -1,5 +1,7 @@
+import BigNumber from "bignumber.js";
import * as Layout from "../utils/layout";
import BN from "bn.js";
+import { remainingOutflow } from "../core";
const BufferLayout = require("buffer-layout");
export const RATE_LIMITER_LEN = 56;
@@ -10,6 +12,17 @@ export interface RateLimiter {
currentQuantity: BN;
}
+export type ParsedRateLimiter = {
+ config: {
+ windowDuration: BigNumber,
+ maxOutflow: BigNumber,
+ },
+ windowStart: BigNumber,
+ previousQuantity: BigNumber,
+ currentQuantity: BigNumber,
+ remainingOutflow: BigNumber | null
+};
+
export interface RateLimiterConfig {
windowDuration: BN;
maxOutflow: BN;
diff --git a/solend-sdk/src/state/reserve.ts b/solend-sdk/src/state/reserve.ts
index 8e9adc2b..3c2ecb71 100644
--- a/solend-sdk/src/state/reserve.ts
+++ b/solend-sdk/src/state/reserve.ts
@@ -3,9 +3,9 @@ import BigNumber from "bignumber.js";
import BN from "bn.js";
import { Buffer } from "buffer";
import * as fzstd from "fzstd";
+import { RateLimiterLayout, RateLimiter } from "./rateLimiter";
import * as Layout from "../utils/layout";
import { LastUpdate, LastUpdateLayout } from "./lastUpdate";
-import { RateLimiter, RateLimiterLayout } from "./rateLimiter";
const BufferLayout = require("buffer-layout");
From 93fe5325e2170eb2f265c0db0a61c0e5770315d4 Mon Sep 17 00:00:00 2001
From: 0xodia <0xodia@solend.fi>
Date: Wed, 12 Jul 2023 19:11:32 -0400
Subject: [PATCH 2/3] fix account metrics
---
solend-lite/src/components/AccountMetrics/AccountMetrics.tsx | 2 +-
.../src/components/UtilizationBar/UtilizationBar.module.scss | 2 +-
solend-sdk/src/state/rateLimiter.ts | 1 -
3 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/solend-lite/src/components/AccountMetrics/AccountMetrics.tsx b/solend-lite/src/components/AccountMetrics/AccountMetrics.tsx
index 82275e8f..9b09cf3d 100644
--- a/solend-lite/src/components/AccountMetrics/AccountMetrics.tsx
+++ b/solend-lite/src/components/AccountMetrics/AccountMetrics.tsx
@@ -54,7 +54,7 @@ function AccountMetrics(): ReactElement {
label='Weighted borrow'
value={
obligation
- ? `${formatUsd(obligation.totalBorrowValue.toString())}`
+ ? `${formatUsd(obligation.weightedTotalBorrowValue.toString())}`
: '-'
}
tooltip='Borrow balance is the sum of all assets borrowed.'
diff --git a/solend-lite/src/components/UtilizationBar/UtilizationBar.module.scss b/solend-lite/src/components/UtilizationBar/UtilizationBar.module.scss
index c57cf341..5aea92c9 100644
--- a/solend-lite/src/components/UtilizationBar/UtilizationBar.module.scss
+++ b/solend-lite/src/components/UtilizationBar/UtilizationBar.module.scss
@@ -36,7 +36,7 @@
}
.borrowed2 {
- background-color: var(--brandAlt);
+ background-color: var(--chakra-colors-brandAlt);
opacity: 0.5;
}
diff --git a/solend-sdk/src/state/rateLimiter.ts b/solend-sdk/src/state/rateLimiter.ts
index 519b2444..09bcdcc6 100644
--- a/solend-sdk/src/state/rateLimiter.ts
+++ b/solend-sdk/src/state/rateLimiter.ts
@@ -1,7 +1,6 @@
import BigNumber from "bignumber.js";
import * as Layout from "../utils/layout";
import BN from "bn.js";
-import { remainingOutflow } from "../core";
const BufferLayout = require("buffer-layout");
export const RATE_LIMITER_LEN = 56;
From 4966dfabde4c5518ce7c1e1d22c4f1b8bff7ff56 Mon Sep 17 00:00:00 2001
From: 0xodia <0xodia@solend.fi>
Date: Wed, 12 Jul 2023 19:17:03 -0400
Subject: [PATCH 3/3] fix lint issues
---
solend-sdk/src/core/utils/pools.ts | 2 +-
solend-sdk/src/core/utils/utils.ts | 3 +--
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/solend-sdk/src/core/utils/pools.ts b/solend-sdk/src/core/utils/pools.ts
index f89b0c81..9ab6eecc 100644
--- a/solend-sdk/src/core/utils/pools.ts
+++ b/solend-sdk/src/core/utils/pools.ts
@@ -5,7 +5,7 @@ import { fetchPrices } from "./prices";
import { calculateBorrowInterest, calculateSupplyInterest } from "./rates";
import { PoolType, ReserveType } from "../types";
import { parseReserve, Reserve } from "../../state";
-import { parseRateLimiter, remainingOutflow } from "./utils";
+import { parseRateLimiter } from "./utils";
export async function fetchPools(
oldPools: Array,
diff --git a/solend-sdk/src/core/utils/utils.ts b/solend-sdk/src/core/utils/utils.ts
index aef5676e..ca34fba2 100644
--- a/solend-sdk/src/core/utils/utils.ts
+++ b/solend-sdk/src/core/utils/utils.ts
@@ -1,7 +1,6 @@
import { Connection, PublicKey } from "@solana/web3.js";
-import { ParsedRateLimiter, RateLimiter, RateLimiterConfig } from "../../state/rateLimiter";
+import { RateLimiter } from "../../state/rateLimiter";
import BigNumber from "bignumber.js";
-import BN from "bn.js";
const ADDRESS_PREFIX_SUFFIX_LENGTH = 6;