diff --git a/ts/client/src/accounts/bank.ts b/ts/client/src/accounts/bank.ts index dcb1fb910..e24ae651d 100644 --- a/ts/client/src/accounts/bank.ts +++ b/ts/client/src/accounts/bank.ts @@ -1,7 +1,7 @@ import { BN } from '@coral-xyz/anchor'; import { utf8 } from '@coral-xyz/anchor/dist/cjs/utils/bytes'; import { PublicKey } from '@solana/web3.js'; -import { I80F48, I80F48Dto, ZERO_I80F48 } from '../numbers/I80F48'; +import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48'; import { As, toUiDecimals } from '../utils'; import { OracleProvider } from './oracle'; @@ -397,17 +397,11 @@ export class Bank implements BankForHealth { } uiDeposits(): number { - return toUiDecimals( - this.indexedDeposits.mul(this.depositIndex), - this.mintDecimals, - ); + return toUiDecimals(this.nativeDeposits(), this.mintDecimals); } uiBorrows(): number { - return toUiDecimals( - this.indexedBorrows.mul(this.borrowIndex), - this.mintDecimals, - ); + return toUiDecimals(this.nativeBorrows(), this.mintDecimals); } /** @@ -481,6 +475,48 @@ export class Bank implements BankForHealth { getDepositRateUi(): number { return this.getDepositRate().toNumber() * 100; } + + getNetBorrowLimitPerWindow(): I80F48 { + return I80F48.fromI64(this.netBorrowLimitPerWindowQuote).div(this.price); + } + + getBorrowLimitLeftInWindow(): I80F48 { + return this.getNetBorrowLimitPerWindow() + .sub(I80F48.fromI64(this.netBorrowsInWindow)) + .max(ZERO_I80F48()); + } + + getNetBorrowLimitPerWindowUi(): number { + return toUiDecimals(this.getNetBorrowLimitPerWindow(), this.mintDecimals); + } + + getMaxWithdraw(vaultBalance: BN, userDeposits = ZERO_I80F48()): I80F48 { + userDeposits = userDeposits.max(ZERO_I80F48()); + + // any borrow must respect the minVaultToDepositsRatio + const minVaultBalanceRequired = this.nativeDeposits().mul( + I80F48.fromNumber(this.minVaultToDepositsRatio), + ); + const maxBorrowFromVault = I80F48.fromI64(vaultBalance) + .sub(minVaultBalanceRequired) + .max(ZERO_I80F48()); + // User deposits can exceed maxWithdrawFromVault + let maxBorrow = maxBorrowFromVault.sub(userDeposits).max(ZERO_I80F48()); + // any borrow must respect the limit left in window + maxBorrow = maxBorrow.min(this.getBorrowLimitLeftInWindow()); + // borrows would be applied a fee + maxBorrow = maxBorrow.div(ONE_I80F48().add(this.loanOriginationFeeRate)); + + // user deposits can always be withdrawn + // even if vaults can be depleted + return maxBorrow.add(userDeposits).min(I80F48.fromI64(vaultBalance)); + } + + getTimeToNextBorrowLimitWindowStartsTs(): number { + return this.netBorrowLimitWindowSizeTs + .sub(new BN(Date.now() / 1000).sub(this.lastNetBorrowsWindowStartTs)) + .toNumber(); + } } export class MintInfo { diff --git a/ts/client/src/accounts/group.ts b/ts/client/src/accounts/group.ts index f1cab82c3..df3bc7b0b 100644 --- a/ts/client/src/accounts/group.ts +++ b/ts/client/src/accounts/group.ts @@ -488,12 +488,7 @@ export class Group { return this.getFirstBankByTokenIndex(0 as TokenIndex); } - /** - * - * @param mintPk - * @returns sum of ui balances of vaults for all banks for a token - */ - public getTokenVaultBalanceByMintUi(mintPk: PublicKey): number { + public getTokenVaultBalanceByMint(mintPk: PublicKey): BN { const banks = this.banksMapByMint.get(mintPk.toBase58()); if (!banks) { throw new Error(`No bank found for mint ${mintPk}!`); @@ -509,7 +504,19 @@ export class Group { totalAmount.iadd(amount); } - return toUiDecimals(totalAmount, this.getMintDecimals(mintPk)); + return totalAmount; + } + + /** + * + * @param mintPk + * @returns sum of ui balances of vaults for all banks for a token + */ + public getTokenVaultBalanceByMintUi(mintPk: PublicKey): number { + return toUiDecimals( + this.getTokenVaultBalanceByMint(mintPk), + this.getMintDecimals(mintPk), + ); } public getSerum3MarketByMarketIndex(marketIndex: MarketIndex): Serum3Market { diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index c62379169..554277c01 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -594,22 +594,11 @@ export class MangoAccount { ), ); const sourceBalance = this.getEffectiveTokenBalance(group, sourceBank); - if (maxSource.gt(sourceBalance)) { - // Cannot borrow more than the window limit - let sourceBorrow = maxSource.sub(sourceBalance); - const netBorrowLimitPerWindow = I80F48.fromI64( - sourceBank.netBorrowLimitPerWindowQuote, - ).div(sourceBank.price); - sourceBorrow = sourceBorrow.min(netBorrowLimitPerWindow); - - maxSource = sourceBalance.add( - sourceBorrow.div(ONE_I80F48().add(sourceBank.loanOriginationFeeRate)), - ); - } - - // Cannot swap more than total deposits in mango for the token - maxSource = maxSource.min(sourceBank.nativeDeposits()); - + const maxWithdrawNative = sourceBank.getMaxWithdraw( + group.getTokenVaultBalanceByMint(sourceBank.mint), + sourceBalance, + ); + maxSource = maxSource.min(maxWithdrawNative); return toUiDecimals(maxSource, group.getMintDecimals(sourceMintPk)); } @@ -740,12 +729,11 @@ export class MangoAccount { // If its a bid then the reserved fund and potential loan is in base // also keep some buffer for fees, use taker fees for worst case simulation. const quoteBalance = this.getEffectiveTokenBalance(group, quoteBank); - if (quoteAmount.gt(quoteBalance)) { - const quoteBorrow = quoteAmount.sub(quoteBalance); - quoteAmount = quoteBalance.add( - quoteBorrow.div(ONE_I80F48().add(quoteBank.loanOriginationFeeRate)), - ); - } + const maxWithdrawNative = quoteBank.getMaxWithdraw( + group.getTokenVaultBalanceByMint(quoteBank.mint), + quoteBalance, + ); + quoteAmount = quoteAmount.min(maxWithdrawNative); quoteAmount = quoteAmount.div( ONE_I80F48().add(I80F48.fromNumber(serum3Market.getFeeRates(true))), ); @@ -782,12 +770,11 @@ export class MangoAccount { // If its a ask then the reserved fund and potential loan is in base // also keep some buffer for fees, use taker fees for worst case simulation. const baseBalance = this.getEffectiveTokenBalance(group, baseBank); - if (baseAmount.gt(baseBalance)) { - const baseBorrow = baseAmount.sub(baseBalance); - baseAmount = baseBalance.add( - baseBorrow.div(ONE_I80F48().add(baseBank.loanOriginationFeeRate)), - ); - } + const maxWithdrawNative = baseBank.getMaxWithdraw( + group.getTokenVaultBalanceByMint(baseBank.mint), + baseBalance, + ); + baseAmount = baseAmount.min(maxWithdrawNative); baseAmount = baseAmount.div( ONE_I80F48().add(I80F48.fromNumber(serum3Market.getFeeRates(true))), );