diff --git a/components/SideNav.tsx b/components/SideNav.tsx index 9452d7ac..c57c20da 100644 --- a/components/SideNav.tsx +++ b/components/SideNav.tsx @@ -12,6 +12,7 @@ import { ArrowTrendingUpIcon, XMarkIcon, MagnifyingGlassIcon, + BanknotesIcon, } from '@heroicons/react/20/solid' import { useRouter } from 'next/router' import { useTranslation } from 'next-i18next' @@ -94,6 +95,13 @@ const SideNav = ({ collapsed }: { collapsed: boolean }) => { title={t('trade')} pagePath="/trade" /> + } + title={t('borrow')} + pagePath="/borrow" + /> { + const { t } = useTranslation(['common', 'token']) + const [showBorrowModal, setShowBorrowModal] = useState(false) + const [selectedToken, setSelectedToken] = useState('') + const actions = mangoStore.getState().actions + const initialStatsLoad = mangoStore((s) => s.tokenStats.initialLoad) + const { group } = useMangoGroup() + const { mangoAccount } = useMangoAccount() + const { mangoTokens } = useJupiterMints() + const { width } = useViewport() + const showTableView = width ? width > breakpoints.md : false + + const handleShowBorrowModal = useCallback((token: string) => { + setSelectedToken(token) + setShowBorrowModal(true) + }, []) + + useEffect(() => { + if (group && !initialStatsLoad) { + actions.fetchTokenStats() + } + }, [group]) + + const banks = useMemo(() => { + if (group) { + const rawBanks = Array.from(group?.banksMapByName, ([key, value]) => ({ + key, + value, + })) + return rawBanks.sort((a, b) => a.key.localeCompare(b.key)) + } + return [] + }, [group]) + + return ( + <> + {showTableView ? ( + + + + + + + + + + {banks.map(({ key, value }) => { + const bank = value[0] + + let logoURI + if (mangoTokens?.length) { + logoURI = mangoTokens.find( + (t) => t.address === bank.mint.toString() + )?.logoURI + } + const borrows = bank.uiBorrows() + const price = bank.uiPrice + + const available = + group && mangoAccount + ? getMaxWithdrawForBank( + group, + bank, + mangoAccount, + true + ).toNumber() + : 0 + + return ( + + + + + + + + ) + })} + +
{t('token')}{t('total-borrows')} +
+ + {t('available')} + +
+
+
{t('rate')}
+
+ +
+
+
+ {logoURI ? ( + + ) : ( + + )} +
+

{bank.name}

+
+
+
+

{formatFixedDecimals(borrows)}

+

+ {formatFixedDecimals(borrows * price, true, true)} +

+
+
+
+

+ {available > 0 ? formatFixedDecimals(available) : '0'} +

+

+ {available > 0 + ? formatFixedDecimals(available * price, false, true) + : '$0.00'} +

+
+
+

+ {formatDecimal(bank.getBorrowRateUi(), 2, { + fixed: true, + })} + % +

+
+
+ + handleShowBorrowModal(bank.name)} + size="small" + > + + + +
+
+ ) : ( +
+ {banks.map(({ key, value }) => { + const bank = value[0] + let logoURI + if (mangoTokens?.length) { + logoURI = mangoTokens.find( + (t) => t.address === bank.mint.toString() + )?.logoURI + } + const price = bank.uiPrice + + const available = + group && mangoAccount + ? getMaxWithdrawForBank( + group, + bank, + mangoAccount, + true + ).toNumber() + : 0 + + return ( +
+
+
+
+ {logoURI ? ( + + ) : ( + + )} +
+

{bank.name}

+
+
+
+

+ {t('available')} +

+ +
+
+

{t('rate')}

+

+ {formatDecimal(bank.getBorrowRateUi(), 2, { + fixed: true, + })} + % +

+
+ handleShowBorrowModal(bank.name)} + size="medium" + > + + +
+
+
+ ) + })} +
+ )} + {showBorrowModal ? ( + setShowBorrowModal(false)} + token={selectedToken} + /> + ) : null} + + ) +} + +export default AssetsBorrowsTable diff --git a/components/borrow/BorrowPage.tsx b/components/borrow/BorrowPage.tsx new file mode 100644 index 00000000..98ec392f --- /dev/null +++ b/components/borrow/BorrowPage.tsx @@ -0,0 +1,231 @@ +import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettings' +import Tooltip from '@components/shared/Tooltip' +import useLocalStorageState from 'hooks/useLocalStorageState' +import useMangoAccount from 'hooks/useMangoAccount' +import useMangoGroup from 'hooks/useMangoGroup' +import { useTranslation } from 'next-i18next' +import { ANIMATION_SETTINGS_KEY } from 'utils/constants' +import FlipNumbers from 'react-flip-numbers' +import Button from '@components/shared/Button' +import mangoStore from '@store/mangoStore' +import { formatFixedDecimals } from 'utils/numbers' +import { useEffect, useMemo, useState } from 'react' +import YourBorrowsTable from './YourBorrowsTable' +import AssetsBorrowsTable from './AssetsBorrowsTable' +import { ArrowDownRightIcon, ArrowUpLeftIcon } from '@heroicons/react/20/solid' +import { useWallet } from '@solana/wallet-adapter-react' +import BorrowRepayModal from '@components/modals/BorrowRepayModal' +import CreateAccountModal from '@components/modals/CreateAccountModal' +import { toUiDecimalsForQuote } from '@blockworks-foundation/mango-v4' +import TabButtons from '@components/shared/TabButtons' +import { useViewport } from 'hooks/useViewport' +import { breakpoints } from 'utils/theme' + +const BorrowPage = () => { + const { t } = useTranslation(['common', 'borrow']) + const { group } = useMangoGroup() + const { mangoAccount, mangoAccountAddress } = useMangoAccount() + const [activeTab, setActiveTab] = useState('borrow:your-borrows') + const [showBorrowModal, setShowBorrowModal] = useState(false) + const [showRepayModal, setShowRepayModal] = useState(false) + const [showCreateAccountModal, setShowCreateAccountModal] = useState(false) + const { connected } = useWallet() + const { width } = useViewport() + const fullWidthTabs = width ? width < breakpoints.sm : false + + const handleBorrowModal = () => { + if (!connected || mangoAccount) { + setShowBorrowModal(true) + } else { + setShowCreateAccountModal(true) + } + } + const [animationSettings] = useLocalStorageState( + ANIMATION_SETTINGS_KEY, + INITIAL_ANIMATION_SETTINGS + ) + const actions = mangoStore((s) => s.actions) + + useEffect(() => { + if (mangoAccountAddress) { + const set = mangoStore.getState().set + set((s) => { + s.mangoAccount.performance.initialLoad = false + }) + actions.fetchAccountPerformance(mangoAccountAddress, 1) + } + }, [actions, mangoAccountAddress]) + + const banks = useMemo(() => { + if (group && mangoAccount) { + const borrowBanks = Array.from(group?.banksMapByName, ([key, value]) => ({ + key, + value, + })).filter((b) => { + const bank = b.value[0] + return mangoAccount.getTokenBalanceUi(bank) < 0 + }) + return borrowBanks + .map((b) => { + return { + balance: mangoAccount.getTokenBalanceUi(b.value[0]), + bank: b.value[0], + } + }) + .sort((a, b) => { + const aBalance = Math.abs(a.balance * a.bank.uiPrice) + const bBalance = Math.abs(b.balance * b.bank.uiPrice) + return aBalance > bBalance ? -1 : 1 + }) + } + return [] + }, [group, mangoAccount]) + + const borrowValue = useMemo(() => { + if (!banks.length) return 0 + return banks.reduce((a, c) => a + Math.abs(c.balance) * c.bank.uiPrice, 0) + }, [banks]) + + useEffect(() => { + if (mangoAccountAddress && !borrowValue) { + setActiveTab('borrow:assets-to-borrow') + } + }, [borrowValue, mangoAccountAddress]) + + const [collateralRemaining, collateralRemainingRatio] = useMemo(() => { + if (mangoAccount && group) { + const remaining = toUiDecimalsForQuote( + mangoAccount.getCollateralValue(group).toNumber() + ) + if (borrowValue) { + const total = borrowValue + remaining + const ratio = (remaining / total) * 100 + return [remaining, ratio] + } + return [remaining, 100] + } + return [0, 0] + }, [borrowValue, mangoAccount, group]) + + return ( + <> +
+
+
+ +

+ {t('borrow:current-borrow-value')} +

+
+
+ {animationSettings['number-scroll'] ? ( + group && mangoAccount ? ( + + ) : ( + + ) + ) : ( + {formatFixedDecimals(borrowValue, true, true)} + )} +
+
+
+

+ {t('borrow:available-to-borrow')} +

+

+ {formatFixedDecimals(collateralRemaining, true, true)} +

+
+
+
+
+
+
+
+ + +
+
+
+
+ setActiveTab(v)} + showBorders + values={[ + ['borrow:your-borrows', 0], + ['borrow:assets-to-borrow', 0], + ]} + /> +
+ {activeTab === 'borrow:your-borrows' ? ( + + ) : ( + + )} + {showBorrowModal ? ( + setShowBorrowModal(false)} + /> + ) : null} + {showRepayModal ? ( + setShowRepayModal(false)} + /> + ) : null} + {showCreateAccountModal ? ( + setShowCreateAccountModal(false)} + /> + ) : null} + + ) +} + +export default BorrowPage diff --git a/components/borrow/YourBorrowsTable.tsx b/components/borrow/YourBorrowsTable.tsx new file mode 100644 index 00000000..37449e4a --- /dev/null +++ b/components/borrow/YourBorrowsTable.tsx @@ -0,0 +1,303 @@ +import { Bank } from '@blockworks-foundation/mango-v4' +import useJupiterMints from 'hooks/useJupiterMints' +import { + ArrowDownRightIcon, + ArrowUpLeftIcon, + NoSymbolIcon, + QuestionMarkCircleIcon, +} from '@heroicons/react/20/solid' +import useMangoAccount from 'hooks/useMangoAccount' +import { useViewport } from 'hooks/useViewport' +import { useTranslation } from 'next-i18next' +import Image from 'next/legacy/image' +import { formatDecimal, formatFixedDecimals } from 'utils/numbers' +import { breakpoints } from 'utils/theme' +import { Table, Td, Th, TrBody, TrHead } from '../shared/TableElements' +import useMangoGroup from 'hooks/useMangoGroup' +import AmountWithValue from '../shared/AmountWithValue' +import ConnectEmptyState from '../shared/ConnectEmptyState' +import { useWallet } from '@solana/wallet-adapter-react' +import Decimal from 'decimal.js' +import { getMaxWithdrawForBank } from '@components/swap/useTokenMax' +import { IconButton } from '@components/shared/Button' +import { useCallback, useState } from 'react' +import BorrowRepayModal from '@components/modals/BorrowRepayModal' +import Tooltip from '@components/shared/Tooltip' + +interface BankWithBalance { + balance: number + bank: Bank +} + +const YourBorrowsTable = ({ banks }: { banks: BankWithBalance[] }) => { + const { t } = useTranslation(['common', 'trade']) + const [showBorrowModal, setShowBorrowModal] = useState(false) + const [showRepayModal, setShowRepayModal] = useState(false) + const [selectedToken, setSelectedToken] = useState('') + const { mangoAccount, mangoAccountAddress } = useMangoAccount() + const { group } = useMangoGroup() + const { mangoTokens } = useJupiterMints() + const { width } = useViewport() + const { connected } = useWallet() + const showTableView = width ? width > breakpoints.md : false + + const handleShowActionModals = useCallback( + (token: string, action: 'borrow' | 'repay') => { + setSelectedToken(token) + action === 'borrow' ? setShowBorrowModal(true) : setShowRepayModal(true) + }, + [] + ) + + return ( + <> + {banks?.length ? ( + showTableView ? ( + + + + + + + + + + {banks.map((b) => { + const bank: Bank = b.bank + + let logoURI + if (mangoTokens.length) { + logoURI = mangoTokens.find( + (t) => t.address === bank.mint.toString() + )?.logoURI + } + + const available = + group && mangoAccount + ? getMaxWithdrawForBank(group, bank, mangoAccount, true) + : new Decimal(0) + + const borrowedAmount = mangoAccount + ? Math.abs(mangoAccount.getTokenBalanceUi(bank)) + : 0 + + return ( + + + + + + + + ) + })} + +
{t('token')}{t('borrow:borrowed-amount')} +
+ + + {t('available')} + + +
+
{t('rate')} + +
+
+
+ {logoURI ? ( + + ) : ( + + )} +
+ {bank.name} +
+
+ {mangoAccount ? ( + + ) : ( + + )} + + + + {formatDecimal(bank.getBorrowRateUi(), 2, { + fixed: true, + })} + % + +
+ + + handleShowActionModals(bank.name, 'repay') + } + size="small" + > + + + + + + handleShowActionModals(bank.name, 'borrow') + } + size="small" + > + + + +
+
+ ) : ( + banks.map((b) => { + const bank: Bank = b.bank + + let logoURI + if (mangoTokens.length) { + logoURI = mangoTokens.find( + (t) => t.address === bank.mint.toString() + )?.logoURI + } + + const available = + group && mangoAccount + ? getMaxWithdrawForBank(group, bank, mangoAccount, true) + : new Decimal(0) + + const borrowedAmount = mangoAccount + ? Math.abs(mangoAccount.getTokenBalanceUi(bank)) + : 0 + + return ( +
+
+
+
+ {logoURI ? ( + + ) : ( + + )} +
+

{bank.name}

+
+
+
+

+ {t('borrow:borrowed-amount')} +

+ +
+
+

{t('rate')}

+

+ {formatDecimal(bank.getBorrowRateUi(), 2, { + fixed: true, + })} + % +

+
+
+ + + handleShowActionModals(bank.name, 'repay') + } + size="medium" + > + + + + + + handleShowActionModals(bank.name, 'borrow') + } + size="medium" + > + + + +
+
+
+
+ ) + }) + ) + ) : mangoAccountAddress || connected ? ( +
+
+ +

{t('borrow:no-borrows')}

+
+
+ ) : ( +
+
+ +
+
+ )} + {showBorrowModal ? ( + setShowBorrowModal(false)} + token={selectedToken} + /> + ) : null} + {showRepayModal ? ( + setShowRepayModal(false)} + token={selectedToken} + /> + ) : null} + + ) +} + +export default YourBorrowsTable diff --git a/components/mobile/BottomBar.tsx b/components/mobile/BottomBar.tsx index 4f3a86bf..eb4b7a52 100644 --- a/components/mobile/BottomBar.tsx +++ b/components/mobile/BottomBar.tsx @@ -15,6 +15,7 @@ import { BuildingLibraryIcon, ArrowTrendingUpIcon, MagnifyingGlassIcon, + BanknotesIcon, } from '@heroicons/react/20/solid' import SolanaTps from '@components/SolanaTps' @@ -73,9 +74,9 @@ const BottomBar = () => { {t('trade')} - - - {t('settings')} + + + {t('borrow')}