From 0ab8999f5c5038fdcb5e351cc22b8578fba74112 Mon Sep 17 00:00:00 2001 From: brightiron <95196612+brightiron@users.noreply.github.com> Date: Mon, 24 Jul 2023 07:33:54 -0500 Subject: [PATCH] Lend and Borrow (#2891) * lending page * sort by borrow amount * lend and borrow improvements * add back midas --- src/App.tsx | 4 + src/assets/icons/lendAndBorrow.svg | 20 ++ src/components/Sidebar/NavContent.tsx | 18 +- src/components/library/NavItem.tsx | 173 ++++++++++++ src/hooks/useGetLPStats.ts | 57 ++-- src/hooks/useGetLendBorrowStats.ts | 41 +++ src/views/Lending/LendingMarkets.tsx | 375 ++++++++++++++++++++++++++ src/views/Lending/index.tsx | 79 ++++++ 8 files changed, 746 insertions(+), 21 deletions(-) create mode 100644 src/assets/icons/lendAndBorrow.svg create mode 100644 src/components/library/NavItem.tsx create mode 100644 src/hooks/useGetLendBorrowStats.ts create mode 100644 src/views/Lending/LendingMarkets.tsx create mode 100644 src/views/Lending/index.tsx diff --git a/src/App.tsx b/src/App.tsx index fb1ff673a1..3c01cd6c4b 100755 --- a/src/App.tsx +++ b/src/App.tsx @@ -33,6 +33,8 @@ import { girth as gTheme } from "src/themes/girth.js"; import { light as lightTheme } from "src/themes/light.js"; import { BondModalContainer } from "src/views/Bond/components/BondModal/BondModal"; import { BondModalContainerV3 } from "src/views/Bond/components/BondModal/BondModalContainerV3"; +import { Lending } from "src/views/Lending"; +import { LendingMarkets } from "src/views/Lending/LendingMarkets"; import { Liquidity } from "src/views/Liquidity"; import { ExternalStakePools } from "src/views/Liquidity/ExternalStakePools/ExternalStakePools"; import { Vault } from "src/views/Liquidity/Vault"; @@ -246,6 +248,8 @@ function App() { } /> } /> } /> + } /> + } /> } /> } /> } /> diff --git a/src/assets/icons/lendAndBorrow.svg b/src/assets/icons/lendAndBorrow.svg new file mode 100644 index 0000000000..a3f405cf30 --- /dev/null +++ b/src/assets/icons/lendAndBorrow.svg @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/src/components/Sidebar/NavContent.tsx b/src/components/Sidebar/NavContent.tsx index cd18262c81..e7939676c8 100644 --- a/src/components/Sidebar/NavContent.tsx +++ b/src/components/Sidebar/NavContent.tsx @@ -1,9 +1,11 @@ import { Box, Divider, Link, Paper, SvgIcon, Typography, useTheme } from "@mui/material"; import { styled } from "@mui/material/styles"; -import { Icon, NavItem } from "@olympusdao/component-library"; +import { Icon } from "@olympusdao/component-library"; import React from "react"; import { NavLink } from "react-router-dom"; +import { ReactComponent as lendAndBorrowIcon } from "src/assets/icons/lendAndBorrow.svg"; import { ReactComponent as OlympusIcon } from "src/assets/icons/olympus-nav-header.svg"; +import NavItem from "src/components/library/NavItem"; import { DecimalBigNumber } from "src/helpers/DecimalBigNumber/DecimalBigNumber"; import { useTestableNetworks } from "src/hooks/useTestableNetworks"; import { BondDiscount } from "src/views/Bond/components/BondDiscount"; @@ -54,6 +56,11 @@ const NavContent: React.VFC = () => { + } + label={`Lend & Borrow`} + to="/lending" + /> @@ -62,7 +69,14 @@ const NavContent: React.VFC = () => { ) : ( - + <> + } + label={`Lend & Borrow`} + to="/lending" + /> + + )} diff --git a/src/components/library/NavItem.tsx b/src/components/library/NavItem.tsx new file mode 100644 index 0000000000..7fa7e36ca8 --- /dev/null +++ b/src/components/library/NavItem.tsx @@ -0,0 +1,173 @@ +import { Box, Link, LinkProps } from "@mui/material"; +import { styled } from "@mui/material/styles"; +import { Accordion, Chip, Icon, OHMChipProps } from "@olympusdao/component-library"; +import { IconName } from "@olympusdao/component-library/lib/components/Icon"; +import { FC, ReactNode } from "react"; +import { NavLink, useLocation } from "react-router-dom"; + +const PREFIX = "NavItem"; + +const classes = { + root: `${PREFIX}-root`, + title: `${PREFIX}-title`, +}; + +const Root = styled("div", { shouldForwardProp: prop => prop !== "match" })(({ theme, match }) => ({ + [`&.${classes.root}`]: { + alignItems: "center", + marginBottom: "12px", + "& .link-container": { + paddingRight: "12px", + }, + "& a.active": { + "& .link-container": { + backgroundColor: theme.colors.gray[600], + }, + textDecoration: "none", + }, + "& .MuiAccordion-root": { + background: "transparent", + "& .MuiAccordionDetails-root a.active .activePill": { + marginLeft: "-35px", + marginRight: "35px", + }, + "& .MuiAccordionSummary-expandIconWrapper": { + padding: "0px 18px", + }, + }, + "& .MuiAccordion-root &:last-child": { + paddingBottom: "0px", + }, + "& .MuiAccordionSummary-root": { + "&.Mui-expanded": { + marginBottom: "6px", + }, + "& a.active .link-container": { + backgroundColor: theme.colors.gray[600], + marginRight: "-48px", + }, + }, + + "& .MuiAccordionDetails-root": { + "& .link-container .title": { + fontSize: "13px", + lineHeight: 1, + }, + "& a.active .link-container": { + backgroundColor: theme.colors.gray[600], + }, + paddingLeft: "20px", + display: "block", + "& .nav-item-container": { + paddingTop: "3px", + paddingBottom: "3px", + paddingRight: "0px", + }, + }, + + "& svg": { + marginRight: "12px", + }, + "& svg.accordion-arrow": { + marginRight: "0px", + }, + "& .external-site-link": { + "& .external-site-link-icon": { + opacity: "0", + }, + "&:hover .external-site-link-icon": { + marginLeft: "5px", + opacity: "1", + }, + }, + }, + + [`& .${classes.title}`]: { + lineHeight: "33px", + paddingLeft: "12px", + paddingTop: "3px", + paddingBottom: "3px", + fontSize: "15px", + }, +})); + +interface MatchProps { + match: boolean; +} + +export interface OHMNavItemProps extends LinkProps { + label: string; + customIcon?: ReactNode; + icon?: IconName; + chip?: string | ReactNode; + className?: string; + to?: any; + /**Will Override to prop. Used for External Links */ + href?: string; + children?: ReactNode; + defaultExpanded?: boolean; + chipColor?: OHMChipProps["template"]; +} + +/** + * Primary NavItem Component for UI. + */ +const NavItem: FC = ({ + chip, + className = "", + customIcon, + icon, + label, + to, + children, + defaultExpanded = true, + chipColor, + ...props +}) => { + const currentLocation = useLocation(); + const match = currentLocation.pathname === to || currentLocation.pathname === `/${to}`; + + const linkProps = props.href + ? { + href: props.href, + target: "_blank", + className: `external-site-link ${className}`, + } + : { + component: NavLink, + to: to, + className: `button-dapp-menu ${className}`, + }; + const LinkItem = () => ( + + + + {customIcon ? customIcon : icon && } + {label} + {props.href && } + + {chip && } + + + ); + return ( + + {children ? ( + }> + {children} + + ) : ( + + )} + + ); +}; + +export default NavItem; diff --git a/src/hooks/useGetLPStats.ts b/src/hooks/useGetLPStats.ts index 70afb5186a..e3e19b3b90 100644 --- a/src/hooks/useGetLPStats.ts +++ b/src/hooks/useGetLPStats.ts @@ -1,30 +1,36 @@ import { useQuery } from "@tanstack/react-query"; import axios from "axios"; -export const useGetLPStats = () => { - const defillamaAPI = "https://yields.llama.fi/pools"; - const { data, isFetched, isLoading } = useQuery(["GetLPStats"], async () => { - return await axios.get(defillamaAPI).then(res => { - return res.data.data - .filter( - pool => - pool.symbol.split("-")[0] === "OHM" || - pool.symbol.split("-")[0] === "GOHM" || - pool.symbol.split("-")[1] === "GOHM" || - pool.symbol.split("-")[1] === "OHM" || - pool.symbol.split("-")[0] === "OHMFRAXBP", - ) - .filter(pool => pool.apy !== 0 && pool.exposure !== "single") - .map(pool => { - return { ...pool, ...mapProjectToName(pool.project), id: pool.pool }; - }); - }); +export const useGetLPStats = (exposure?: string) => { + const { data, isFetched, isLoading } = useQuery(["GetLPStats", exposure], async () => { + const ohmPools = await getOhmPools(); + + const ohmPoolsFilters = ohmPools.filter(pool => pool.exposure === exposure || "multi"); + return ohmPoolsFilters; }); return { data, isFetched, isLoading }; }; -const mapProjectToName = (project: string) => { +export const getOhmPools = async () => { + const defillamaAPI = "https://yields.llama.fi/pools"; + return await axios.get(defillamaAPI).then(res => { + return res.data.data + .filter( + pool => + pool.symbol.split("-")[0] === "OHM" || + pool.symbol.split("-")[0] === "GOHM" || + pool.symbol.split("-")[1] === "GOHM" || + pool.symbol.split("-")[1] === "OHM" || + pool.symbol.split("-")[0] === "OHMFRAXBP", + ) + .map(pool => { + return { ...pool, ...mapProjectToName(pool.project), id: pool.pool }; + }); + }); +}; + +export const mapProjectToName = (project: string) => { switch (project) { case "balancer-v2": return { projectName: "Balancer", projectLink: "https://app.balancer.fi/" }; @@ -67,6 +73,19 @@ const mapProjectToName = (project: string) => { return { projectName: "Ramses", projectLink: "https://app.ramses.exchange/liquidity" }; case "chronos": return { projectName: "Chronos", projectLink: "https://app.chronos.exchange/liquidity" }; + case "sentiment": + return { projectName: "Sentiment", projectLink: "https://arbitrum.sentiment.xyz/borrow/OHM?symbol=USDC.e" }; + case "midas-capital": + return { projectName: "Midas Capital", projectLink: "https://app.midascapital.xyz/42161/pool/1" }; + case "silo-finance": + return { projectName: "Silo Finance", projectLink: "https://app.silo.finance/" }; + case "inverse-finance-firm": + return { projectName: "Inverse Finance", projectLink: "https://www.inverse.finance/firm" }; + case "fraxlend": + return { + projectName: "Frax", + projectLink: "https://app.frax.finance/fraxlend/pair?address=0x66bf36dBa79d4606039f04b32946A260BCd3FF52", + }; default: return { projectName: project, projectLink: "" }; } diff --git a/src/hooks/useGetLendBorrowStats.ts b/src/hooks/useGetLendBorrowStats.ts new file mode 100644 index 0000000000..bbbf75a3eb --- /dev/null +++ b/src/hooks/useGetLendBorrowStats.ts @@ -0,0 +1,41 @@ +import { useQuery } from "@tanstack/react-query"; +import axios from "axios"; +import { DefiLlamaPool, getOhmPools } from "src/hooks/useGetLPStats"; + +type LendAndBorrow = { + apyBaseBorrow: number; + apyRewardBorrow: number; + borrowFactor: number | null; + debtCeilingUsd: number | null; + ltv: number; + mintedCoin: string | null; + pool: string; + rewardTokens: string[]; + totalBorrowUsd: number; + totalSupplyUsd: number; + underlyingTokens: string[]; +}; +export const useGetLendAndBorrowStats = () => { + const defillamaAPI = "https://yields.llama.fi/lendBorrow"; + const { data, isFetched, isLoading } = useQuery(["GetLendBorrowStats"], async () => { + const ohmPools = await getOhmPools(); + console.log(ohmPools, "ohmPools"); + const lendAndBorrowPools = await axios.get(defillamaAPI).then(res => { + return res.data; + }); + const ohmLendAndBorrowPools = ohmPools.filter(pool => + lendAndBorrowPools.some(lendAndBorrowPool => lendAndBorrowPool.pool === pool.id), + ); + const poolsAndLendBorrowStats = ohmLendAndBorrowPools.map(pool => { + const lendBorrowPool = lendAndBorrowPools.find(lendAndBorrowPool => lendAndBorrowPool.pool === pool.id); + return { ...pool, lendAndBorrow: lendBorrowPool }; + }); + return poolsAndLendBorrowStats; + }); + + return { data, isFetched, isLoading }; +}; + +export interface LendAndBorrowPool extends DefiLlamaPool { + lendAndBorrow: LendAndBorrow; +} diff --git a/src/views/Lending/LendingMarkets.tsx b/src/views/Lending/LendingMarkets.tsx new file mode 100644 index 0000000000..e0655e45a9 --- /dev/null +++ b/src/views/Lending/LendingMarkets.tsx @@ -0,0 +1,375 @@ +import { Check } from "@mui/icons-material"; +import { Box, FormControl, InputLabel, MenuItem, Select, SvgIcon, Typography, useTheme } from "@mui/material"; +import { styled } from "@mui/material/styles"; +import { DataGrid, GridColDef, GridRenderCellParams, GridValueFormatterParams } from "@mui/x-data-grid"; +import { + Chip, + OHMTokenProps, + OHMTokenStackProps, + TextButton, + Token, + TokenStack, + Tooltip, +} from "@olympusdao/component-library"; +import { useState } from "react"; +import PageTitle from "src/components/PageTitle"; +import { formatCurrency, formatNumber } from "src/helpers"; +import { defiLlamaChainToNetwork } from "src/helpers/defiLlamaChainToNetwork"; +import { normalizeSymbol } from "src/helpers/normalizeSymbol"; +import { LendAndBorrowPool, useGetLendAndBorrowStats } from "src/hooks/useGetLendBorrowStats"; +import { DefiLlamaPool } from "src/hooks/useGetLPStats"; + +const PREFIX = "ExternalStakePools"; + +const classes = { + stakePoolsWrapper: `${PREFIX}-stakePoolsWrapper`, + stakePoolHeaderText: `${PREFIX}-stakePoolHeaderText`, + poolPair: `${PREFIX}-poolPair`, + poolName: `${PREFIX}-poolName`, +}; + +const StyledPoolInfo = styled("div")(() => ({ + [`&.${classes.poolPair}`]: { + display: "flex", + alignItems: "center", + justifyContent: "left", + }, + + [`& .${classes.poolName}`]: { + marginLeft: "10px", + }, +})); + +export const LendingMarkets = () => { + const { data: defiLlamaPools } = useGetLendAndBorrowStats(); + console.log(defiLlamaPools, "defiLlamaPools"); + const [poolFilter, setPoolFilter] = useState("all"); + const theme = useTheme(); + const networks = [...new Set(defiLlamaPools?.map(pool => pool.chain))]; + const [networkFilter, setNetworkFilter] = useState(undefined); + const stablePools = + defiLlamaPools && + defiLlamaPools.filter(pool => { + const symbols = pool.symbol.split("-"); + const stable = + symbols.includes("DAI") || + symbols.includes("USDC") || + symbols.includes("FRAXBP") || + symbols.includes("OHMFRAXBP"); + return stable; + }); + + const volatilePools = + defiLlamaPools && + defiLlamaPools.filter(pool => { + const symbols = pool.symbol.split("-"); + const volatile = + !symbols.includes("DAI") && + !symbols.includes("USDC") && + !symbols.includes("FRAXBP") && + !symbols.includes("OHMFRAXBP"); + return volatile; + }); + + const gOHMPools = + defiLlamaPools && + defiLlamaPools.filter(pool => { + const symbols = pool.symbol.split("-"); + const stable = symbols.includes("GOHM"); + return stable; + }); + + const ohmPools = defiLlamaPools?.filter(pool => pool.symbol === "OHM") || []; + + const poolList = + poolFilter === "stable" + ? stablePools + : poolFilter === "volatile" + ? volatilePools + : poolFilter === "gohm" + ? gOHMPools + : poolFilter === "ohm" + ? ohmPools + : defiLlamaPools; + + const poolListByNetwork = networkFilter ? poolList?.filter(pool => pool.chain === networkFilter) : poolList; + const PoolChip = ({ label }: { label: string }) => ( + + {poolFilter === label.toLowerCase() && } + {label} + + } + template={poolFilter === label.toLowerCase() ? undefined : "gray"} + onClick={() => (poolFilter === label.toLowerCase() ? setPoolFilter("all") : setPoolFilter(label.toLowerCase()))} + /> + ); + + const columns: GridColDef[] = [ + { + field: "symbol", + headerName: "Lend", + renderCell: params => { + const symbols = + params.row.symbol !== "OHMFRAXBP-F" + ? params.row.symbol.split("-").filter(s => s !== "") + : ["OHM", "FRAX", "CRV"]; + return ( + + + +
+ {params.row.symbol} +
+
+ ); + }, + minWidth: 120, + }, + { + field: "mintAsset", + headerName: "Borrow", + valueGetter: params => { + return params.row.lendAndBorrow.mintedCoin || "OHM"; + }, + renderCell: params => { + const symbol = normalizeSymbol([params.row.lendAndBorrow.mintedCoin || "OHM"]) as OHMTokenStackProps["tokens"]; + return ( + + {params.row.lendAndBorrow.mintedCoin === "DOLA" ? ( + + + + + + + ) : ( + + )} + +
+ {params.row.lendAndBorrow.mintedCoin || "OHM"} +
+
+ ); + }, + minWidth: 110, + }, + { + field: "tvlUsd", + headerName: "TVL", + valueFormatter: (params: GridValueFormatterParams) => formatCurrency(params.value, 0), + minWidth: 110, + }, + { + field: "apy", + headerName: "Supply APY", + renderCell: params => ( + <> + {params.row.apyBase || params.row.apyReward ? ( + +

Base APY: {formatNumber(params.row.apyBase || 0, 2)}%

+

Reward APY: {formatNumber(params.row.apyReward || 0, 2)}%

+ + } + > + {formatNumber(params.row.apy || 0, 2)}% +
+ ) : ( + <>{formatNumber(params.row.apy || 0, 2)}% + )} + + ), + minWidth: 110, + }, + { + field: "borrowApy", + headerName: "Borrow APY", + valueGetter: params => { + return params.row.lendAndBorrow.apyBaseBorrow - params.row.lendAndBorrow.apyRewardBorrow; + }, + renderCell: params => ( + <> + {params.row.lendAndBorrow.apyBaseBorrow || params.row.lendAndBorrow.apyRewardBorrow ? ( + +

Base APY: {formatNumber(params.row.lendAndBorrow.apyBaseBorrow || 0, 2)}%

+

Reward APY: {formatNumber(params.row.lendAndBorrow.apyRewardBorrow || 0, 2)}%

+ + } + > + {formatNumber(params.row.lendAndBorrow.apyBaseBorrow - params.row.lendAndBorrow.apyRewardBorrow || 0, 2)}% +
+ ) : ( + <>{formatNumber(params.row.lendAndBorrow.apyBaseBorrow || 0, 2)}% + )} + + ), + }, + { + field: "ltv", + headerName: "LTV", + valueGetter: params => { + return params.row.lendAndBorrow.ltv; + }, + renderCell: params => <>{formatNumber(params.row.lendAndBorrow.ltv * 100)}%, + minWidth: 30, + }, + { + field: "available", + headerName: "Available to Borrow", + valueGetter: params => { + return ( + (params.row.lendAndBorrow.debtCeilingUsd || params.row.lendAndBorrow.totalSupplyUsd) - + params.row.lendAndBorrow.totalBorrowUsd + ); + }, + renderCell: params => ( + <> + {formatCurrency( + (params.row.lendAndBorrow.debtCeilingUsd || params.row.lendAndBorrow.totalSupplyUsd) - + params.row.lendAndBorrow.totalBorrowUsd, + )} + + ), + minWidth: 150, + }, + { + field: "projectName", + headerName: "", + renderCell: (params: GridRenderCellParams) => ( + + {params.row.projectName} + + ), + minWidth: 100, + flex: 1, + }, + ]; + + return ( +
+ + + + Lend & Borrow Markets + + + + } + > + + + Borrow & Lend against OHM or gOHM with our trusted partners + + + + + + + + + Filter by Network + + + + + +
+ ); +}; diff --git a/src/views/Lending/index.tsx b/src/views/Lending/index.tsx new file mode 100644 index 0000000000..7666a54c67 --- /dev/null +++ b/src/views/Lending/index.tsx @@ -0,0 +1,79 @@ +import { ArrowForward } from "@mui/icons-material"; +import { Box, Link, Skeleton, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { Chip, Metric, PrimaryButton } from "@olympusdao/component-library"; +import { Link as RouterLink } from "react-router-dom"; +import PageTitle from "src/components/PageTitle"; +import { formatCurrency } from "src/helpers"; +import { useOhmPrice } from "src/hooks/usePrices"; +import { LiquidityCTA } from "src/views/Liquidity/LiquidityCTA"; + +export const Lending = () => { + const theme = useTheme(); + const { data: ohmPrice } = useOhmPrice(); + const isMobileScreen = useMediaQuery("(max-width: 513px)"); + + return ( +
+ + + + } /> + + + + + Coming Soon} /> + + + + Cooler Loans + + + Borrow DAI against your gOHM at a fixed rate + + + + + Coming Soon + + + + + + + + New} /> + + + + Lending Markets + + + Borrow OHM or leverage OHM holdings + + + + + + View Lending Markets + + + + + + + + + + + +
+ ); +};