import 'chart.js/auto' import BigNumber from 'bignumber.js' import classNames from 'classnames' import { BorrowCapacity, Button, Card, ConnectButton, DisplayCurrency, ErrorMessage, InputSection, } from 'components/common' import { DEFAULT_SLIPPAGE } from 'constants/appConstants' import { findByDenom } from 'functions' import { maxBorrowableAmount } from 'functions/redbank/maxBorrowableAmount' import { produceBarChartConfig } from 'functions/redbank/produceBarChartConfig' import { produceUpdatedAssetData } from 'functions/redbank/produceUpdatedAssetData' import { useUserBalance } from 'hooks/queries' import { balanceSum, ltvWeightedDepositValue, maintainanceMarginWeightedDepositValue, producePercentData, } from 'libs/assetInfo' import { formatValue, lookup, lookupSymbol } from 'libs/parse' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Bar } from 'react-chartjs-2' import { useTranslation } from 'react-i18next' import useStore from 'store' import colors from 'styles/_assets.module.scss' import { ViewType } from 'types/enums' import styles from './Action.module.scss' interface Props { amount: number redBankAssets: RedBankAsset[] depositAssets: RedBankAsset[] borrowAssets: RedBankAsset[] setIsMax: (isMax: boolean) => void setCapHit: (capHit: boolean) => void setAmountCallback: (amount: number) => void mmScaledDepositAmount: number ltvScaledDepositAmount: number totalBorrowBaseCurrencyAmount: number actionButtonSpec: ModalActionButton submitted: boolean feeError?: string activeView: ViewType denom: string decimals: number handleClose: () => void } export const Action = ({ amount, redBankAssets, depositAssets, borrowAssets, setIsMax, setCapHit, setAmountCallback, mmScaledDepositAmount, ltvScaledDepositAmount, totalBorrowBaseCurrencyAmount, actionButtonSpec, submitted, feeError, activeView, denom, decimals, handleClose, }: Props) => { const { t } = useTranslation() // ------------------ // STORE STATE // ------------------ const baseCurrency = useStore((s) => s.baseCurrency) const marketInfo = useStore((s) => s.marketInfo) const marketAssetLiquidity = useStore((s) => s.marketAssetLiquidity) const userCollateral = useStore((s) => s.userCollateral) const userWalletAddress = useStore((s) => s.userWalletAddress) const whitelistedAssets = useStore((s) => s.whitelistedAssets) const convertToBaseCurrency = useStore((s) => s.convertToBaseCurrency) const findUserDebt = useStore((s) => s.findUserDebt) const enableAnimations = useStore((s) => s.enableAnimations) const baseCurrencyDecimals = useStore((s) => s.baseCurrency.decimals) // ------------------ // LOCAL STATE // ------------------ const [currentAssetPrice, setCurrentAssetPrice] = useState(0) const [portfolioVisible, setPortfolioVisible] = useState(false) const [chartsDataLoaded, setChartsDataLoaded] = useState(false) const { data: userBalances } = useUserBalance() /// ------------------ // VARIABLES // ------------------ const walletBalance = Number(findByDenom(userBalances || [], denom)?.amount.toString()) || 0 const assetBorrowBalance = findUserDebt(denom) const availableBalanceBaseCurrency = Math.max( ltvScaledDepositAmount - totalBorrowBaseCurrencyAmount, 0, ) const currentAsset = redBankAssets.find((asset) => asset.denom === denom) // ------------------------- // calculate // ------------------------- const relevantAssetData = useMemo( () => activeView === ViewType.Deposit || activeView === ViewType.Withdraw ? depositAssets : borrowAssets, [depositAssets, borrowAssets, activeView], ) const relevantBalanceKey = useMemo( () => activeView === ViewType.Deposit || activeView === ViewType.Withdraw ? 'depositBalanceBaseCurrency' : 'borrowBalanceBaseCurrency', [activeView], ) const amountAdjustedAssetData = useMemo( () => produceUpdatedAssetData( redBankAssets, [...relevantAssetData], denom, amount * currentAssetPrice, // amount in display currency activeView, relevantBalanceKey, baseCurrencyDecimals, ), [ activeView, amount, relevantAssetData, currentAssetPrice, denom, redBankAssets, relevantBalanceKey, baseCurrencyDecimals, ], ) const percentData = producePercentData( produceUpdatedAssetData( redBankAssets, [...relevantAssetData], denom, 0.0, activeView, relevantBalanceKey, baseCurrencyDecimals, ), relevantBalanceKey, ) const updatedData = producePercentData(amountAdjustedAssetData, relevantBalanceKey) // --------------------- // logic // --------------------- const newTotalMMScaledSupplyBalance = useMemo( () => // For deposits and withdraws, we need to recalculate the loan limit { if (!userCollateral) return 0 // On first deposit of asset, SC does not hold state of collateral.enabled // Therefore, we need to emulate this state const isFirstDeposit = !relevantAssetData.find((asset) => asset.denom === denom) && activeView === ViewType.Deposit return activeView === ViewType.Deposit || activeView === ViewType.Withdraw ? maintainanceMarginWeightedDepositValue( amountAdjustedAssetData, marketInfo, userCollateral, relevantBalanceKey, isFirstDeposit ? denom : '', ) : mmScaledDepositAmount }, // eslint-disable-next-line react-hooks/exhaustive-deps [activeView, amountAdjustedAssetData, mmScaledDepositAmount], ) const newTotalLTVScaledSupplyBalance = useMemo( () => // For deposits and withdraws, we need to recalculate the loan limit { if (!userCollateral) return 0 // On first deposit of asset, SC does not hold state of collateral.enabled // Therefore, we need to emulate this state const isFirstDeposit = !relevantAssetData.find((asset) => asset.denom === denom) && activeView === ViewType.Deposit return activeView === ViewType.Deposit || activeView === ViewType.Withdraw ? ltvWeightedDepositValue( amountAdjustedAssetData, marketInfo, userCollateral, relevantBalanceKey, isFirstDeposit ? denom : '', ) : ltvScaledDepositAmount }, // eslint-disable-next-line react-hooks/exhaustive-deps [activeView, amountAdjustedAssetData, ltvScaledDepositAmount], ) const debtValue = activeView === ViewType.Borrow || activeView === ViewType.Repay ? balanceSum(amountAdjustedAssetData, relevantBalanceKey) : totalBorrowBaseCurrencyAmount const calculateMaxBorrowableAmount = useMemo((): number => { const assetLiquidity = Number(findByDenom(marketAssetLiquidity, denom)?.amount || 0) return maxBorrowableAmount( assetLiquidity, availableBalanceBaseCurrency, new BigNumber(currentAssetPrice) .shiftedBy(baseCurrency.decimals - (currentAsset?.decimals || 0)) .toNumber(), ) }, [ denom, availableBalanceBaseCurrency, currentAssetPrice, marketAssetLiquidity, baseCurrency.decimals, currentAsset?.decimals, ]) const repayMax = Math.min(assetBorrowBalance, walletBalance) const maxWithdrawableAmount = useMemo((): number => { const assetLtvRatio = findByDenom(marketInfo, denom)?.max_loan_to_value || 0 const assetLiquidity = Number(findByDenom(marketAssetLiquidity, denom)?.amount || 0) const asset = depositAssets.find((asset) => asset.denom === denom) const assetBalanceOrAvailableLiquidity = Math.min(Number(asset?.depositBalance), assetLiquidity) if (totalBorrowBaseCurrencyAmount === 0) { return assetBalanceOrAvailableLiquidity } // If we did not receive a usable asset there is nothing more to do. if (!asset || !asset.depositBalance || !asset.denom) return 0 // When withdrawing, we have to remove the slippage, otherwise we can't actually hit the borrow limit. const withdrawableAmountOfAsset = new BigNumber( availableBalanceBaseCurrency / (1 - DEFAULT_SLIPPAGE) / (currentAssetPrice * assetLtvRatio), ) .shiftedBy(asset.decimals - baseCurrency.decimals) .toNumber() return withdrawableAmountOfAsset < assetBalanceOrAvailableLiquidity ? withdrawableAmountOfAsset : assetBalanceOrAvailableLiquidity }, [ denom, currentAssetPrice, depositAssets, availableBalanceBaseCurrency, totalBorrowBaseCurrencyAmount, marketInfo, marketAssetLiquidity, baseCurrency.decimals, ]) const maxUsableAmount = useMemo(() => { if (!currentAsset) return 0 return activeView === ViewType.Deposit ? walletBalance : activeView === ViewType.Withdraw ? maxWithdrawableAmount : activeView === ViewType.Borrow ? calculateMaxBorrowableAmount : repayMax }, [ walletBalance, maxWithdrawableAmount, calculateMaxBorrowableAmount, repayMax, activeView, currentAsset, ]) useEffect(() => { setCurrentAssetPrice(convertToBaseCurrency({ denom: denom || '', amount: '1' })) }, [denom, convertToBaseCurrency]) useEffect(() => { if (!chartsDataLoaded && percentData[0] != 0) { setChartsDataLoaded(true) } }, [percentData, chartsDataLoaded]) const chartRefBefore = useRef(null) const chartRefAfter = useRef(null) // ----------- // callbacks // ----------- const handleInputAmount = useCallback( (inputAmount: number) => { if (inputAmount >= maxUsableAmount * 0.99) { setIsMax(true) } setAmountCallback(Number(formatValue(inputAmount, 0, 0, false, false, false, false, false))) }, [maxUsableAmount, setIsMax, setAmountCallback], ) if (!currentAsset) return <> const amountUntilDepositCap = currentAsset.depositCap - Number(currentAsset.depositLiquidity) const onValueEntered = (microValue: number) => { if (microValue >= maxUsableAmount) microValue = maxUsableAmount setAmountCallback(Number(formatValue(microValue, 0, 0, false, false, false, false, false))) setCapHit(amount > amountUntilDepositCap && activeView === ViewType.Deposit) } const produceTabActionButton = () => { return ( <>