From 7fc6bea7f648bb82055751d44574d700be176f79 Mon Sep 17 00:00:00 2001 From: Linkie Link Date: Fri, 21 Apr 2023 08:45:21 +0800 Subject: [PATCH] release v1.4.4 --- .../redbank/RedbankAction/RedbankAction.tsx | 783 ++++++++++++------ src/i18n.ts | 2 +- 2 files changed, 543 insertions(+), 242 deletions(-) diff --git a/src/components/redbank/RedbankAction/RedbankAction.tsx b/src/components/redbank/RedbankAction/RedbankAction.tsx index ad9e615..affbf91 100644 --- a/src/components/redbank/RedbankAction/RedbankAction.tsx +++ b/src/components/redbank/RedbankAction/RedbankAction.tsx @@ -1,280 +1,581 @@ -import { TxBroadcastResult } from '@marsprotocol/wallet-connector' -import { useQueryClient } from '@tanstack/react-query' -import { Action, Notification, TxResponse } from 'components/common' -import { findByDenom } from 'functions' +import 'chart.js/auto' + +import classNames from 'classnames' import { - getRedbankBorrowMsgOptions, - getRedbankDepositMsgOptions, - getRedbankRepayMsgOptions, - getRedbankWithdrawMsgOptions, -} from 'functions/messages' -import { useEstimateFee } from 'hooks/queries' -import { ltvWeightedDepositValue, maintainanceMarginWeightedDepositValue } from 'libs/assetInfo' -import { lookup, lookupDecimals } from 'libs/parse' -import isEqual from 'lodash.isequal' -import { useRouter } from 'next/router' -import React, { useMemo, useState } from 'react' + BorrowCapacity, + Button, + Card, + ConnectButton, + DisplayCurrency, + ErrorMessage, + InputSection, +} from 'components/common' +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 { NotificationType, ViewType } from 'types/enums' -import { QUERY_KEYS } from 'types/enums/queryKeys' +import colors from 'styles/_assets.module.scss' +import { ViewType } from 'types/enums' -import styles from './RedbankAction.module.scss' +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 - id: string + denom: string + decimals: number + handleClose: () => void } -export const RedbankAction = React.memo( - ({ activeView, id }: Props) => { - // ------------------ - // EXTERNAL HOOKS - // ------------------ - const { t } = useTranslation() - const router = useRouter() - const queryClient = useQueryClient() +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) - // ------------------ - // STORE STATE - // ------------------ - const client = useStore((s) => s.client) - const marketInfo = useStore((s) => s.marketInfo) - const networkConfig = useStore((s) => s.networkConfig) - const otherAssets = useStore((s) => s.otherAssets) - const redBankAssets = useStore((s) => s.redBankAssets) - const userBalances = useStore((s) => s.userBalances) - const userCollateral = useStore((s) => s.userCollateral) - const whitelistedAssets = useStore((s) => s.whitelistedAssets) - const executeMsg = useStore((s) => s.executeMsg) + // ------------------ + // LOCAL STATE + // ------------------ + const [currentAssetPrice, setCurrentAssetPrice] = useState(0) + const [portfolioVisible, setPortfolioVisible] = useState(false) + const [chartsDataLoaded, setChartsDataLoaded] = useState(false) - // ------------------ - // LOCAL STATE - // ------------------ - const [amount, setAmount] = useState(0) - const [submitted, setSubmitted] = useState(false) - const [response, setResponse] = useState() - const [error, setError] = useState() - const [isMax, setIsMax] = useState(false) - const [capHit, setCapHit] = useState(false) + const { data: userBalances } = useUserBalance() - // ------------------ - // VARIABLES - // ------------------ - const assets = [...whitelistedAssets, ...otherAssets] - const denom = assets.find((asset) => asset.id === id)?.denom || '' - const decimals = lookupDecimals(denom, whitelistedAssets || []) || 6 - const symbol = assets.find((asset) => asset.id === id)?.symbol || '' - const walletBallance = Number(findByDenom(userBalances, denom)?.amount.toString()) + /// ------------------ + // 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) - // Read only states - const borrowAssetName = redBankAssets.find((asset) => asset.denom === denom) - const redBankContractAddress = networkConfig?.contracts.redBank - const totalScaledDepositbaseCurrencyBalance = useMemo(() => { - if (!userCollateral) return 0 - return ltvWeightedDepositValue( + // ------------------------- + // 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, - marketInfo, - userCollateral, - 'depositBalanceBaseCurrency', - ) - }, [redBankAssets, marketInfo, userCollateral]) + [...relevantAssetData], + denom, + amount * currentAssetPrice, // amount in display currency + activeView, + relevantBalanceKey, + baseCurrencyDecimals, + ), + [ + activeView, + amount, + relevantAssetData, + currentAssetPrice, + denom, + redBankAssets, + relevantBalanceKey, + baseCurrencyDecimals, + ], + ) - const totalMMScaledDepositbaseCurrencyBalance = useMemo(() => { - if (!userCollateral) return 0 - return maintainanceMarginWeightedDepositValue( - redBankAssets, - marketInfo, - userCollateral, - 'depositBalanceBaseCurrency', - ) - }, [redBankAssets, marketInfo, userCollateral]) + const percentData = producePercentData( + produceUpdatedAssetData( + redBankAssets, + [...relevantAssetData], + denom, + 0.0, + activeView, + relevantBalanceKey, + baseCurrencyDecimals, + ), + relevantBalanceKey, + ) + const updatedData = producePercentData(amountAdjustedAssetData, relevantBalanceKey) - const totalBorrowBaseCurrencyAmount = redBankAssets.reduce( - (total, asset) => total + (Number(asset.borrowBalanceBaseCurrency) || 0), - 0, - ) + // --------------------- + // 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 - // -------------------------------- - // Transaction objects - // -------------------------------- + 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 txMsgOptions = useMemo(() => { - if (!redBankContractAddress || amount <= 0 || !denom) return + 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 - switch (activeView) { - case ViewType.Deposit: - return getRedbankDepositMsgOptions(amount, denom) - case ViewType.Withdraw: - return getRedbankWithdrawMsgOptions(amount, denom) - case ViewType.Repay: - return getRedbankRepayMsgOptions( - amount, - denom, - Number(findByDenom(userBalances, denom)?.amount) || 0, - isMax, - ) - case ViewType.Borrow: - return getRedbankBorrowMsgOptions(amount, denom) - default: - return undefined - } - }, [activeView, amount, redBankContractAddress, denom, isMax, userBalances]) + 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 { data: fee, error: feeError } = useEstimateFee({ - msg: txMsgOptions?.msg, - funds: - activeView === ViewType.Deposit || activeView === ViewType.Repay - ? [{ denom, amount: amount > 0 ? amount.toFixed(0) : '1' }] - : undefined, - contract: redBankContractAddress, - }) + const debtValue = + activeView === ViewType.Borrow || activeView === ViewType.Repay + ? balanceSum(amountAdjustedAssetData, relevantBalanceKey) + : totalBorrowBaseCurrencyAmount - const produceActionButtonSpec = (): ModalActionButton => { - return { - disabled: amount === 0 || capHit, - fetching: (amount > 0 && typeof fee === 'undefined') || submitted, - text: t(`redbank.${activeView.toLowerCase()}`), - clickHandler: handleAction, - color: 'primary', - } + const calculateMaxBorrowableAmount = useMemo((): number => { + const assetLiquidity = Number(findByDenom(marketAssetLiquidity, denom)?.amount || 0) + + return maxBorrowableAmount(assetLiquidity, availableBalanceBaseCurrency, currentAssetPrice) + }, [denom, availableBalanceBaseCurrency, currentAssetPrice, marketAssetLiquidity]) + + const repayMax = useMemo((): number => { + return Math.min(assetBorrowBalance, walletBalance) + }, [assetBorrowBalance, walletBalance, denom, baseCurrency.denom]) + + 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 } - const handleAction = async () => { - if (!redBankContractAddress || !client) { - alert('Uh oh, operation failed') - return + // If we did not receive a usable asset there is nothing more to do. + if (!asset || !asset.depositBalance || !asset.denom) return 0 + + const withdrawableAmountOfAsset = + availableBalanceBaseCurrency / (currentAssetPrice * assetLtvRatio) + + return withdrawableAmountOfAsset < assetBalanceOrAvailableLiquidity + ? withdrawableAmountOfAsset + : assetBalanceOrAvailableLiquidity + }, [ + denom, + currentAssetPrice, + depositAssets, + availableBalanceBaseCurrency, + totalBorrowBaseCurrencyAmount, + marketInfo, + marketAssetLiquidity, + ]) + + 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) } - setSubmitted(true) + setAmountCallback(Number(formatValue(inputAmount, 0, 0, false, false, false, false, false))) + }, + [maxUsableAmount, setIsMax, setAmountCallback], + ) - if (!fee || !txMsgOptions) { - return - } + if (!currentAsset) return <> - try { - const res = await executeMsg({ - msg: txMsgOptions.msg, - // @ts-ignore - funds: txMsgOptions.funds || [], - contract: redBankContractAddress, - fee: fee, + 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 ( + <> +