diff --git a/package-lock.json b/package-lock.json index 77d213d5..b2ab9991 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,7 +41,7 @@ "node-fetch": "^2.6.1", "node-vibrant": "^3.1.6", "polished": "^4.1.3", - "rari-components": "git+https://github.com/Rari-Capital/rari-components#9541f72a093da5969155d53324d281a90f7c6b1e", + "rari-components": "git+https://github.com/Rari-Capital/rari-components#7ddc0eb58507b28e8b4be026cabb26294f179d0b", "rari-tokens-generator": "^2.0.0", "react": "^17.0.2", "react-apexcharts": "1.3.7", @@ -15788,8 +15788,8 @@ }, "node_modules/rari-components": { "version": "0.0.1", - "resolved": "git+ssh://git@github.com/Rari-Capital/rari-components.git#9541f72a093da5969155d53324d281a90f7c6b1e", - "integrity": "sha512-4bPXZTlG+RbAyrhoZzVSaUNIibshxfv0Sj8ta5Ni1xraUKrTDLCJ+FGQ0q6M34okz4bHNOz8yaT6PHdO+FvU4Q==", + "resolved": "git+ssh://git@github.com/Rari-Capital/rari-components.git#7ddc0eb58507b28e8b4be026cabb26294f179d0b", + "integrity": "sha512-27en+sZhntPvTW1XKaZ1jt6+NN1RmZcxgdn9/7tt34rBAohRRbZIhh/pkAt7Z5pjwLoBvscT0PSyXWCmXUqmjg==", "peerDependencies": { "@chakra-ui/icons": "1.x", "@chakra-ui/react": "^1.8", @@ -32578,9 +32578,9 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "rari-components": { - "version": "git+ssh://git@github.com/Rari-Capital/rari-components.git#9541f72a093da5969155d53324d281a90f7c6b1e", - "integrity": "sha512-4bPXZTlG+RbAyrhoZzVSaUNIibshxfv0Sj8ta5Ni1xraUKrTDLCJ+FGQ0q6M34okz4bHNOz8yaT6PHdO+FvU4Q==", - "from": "rari-components@git+https://github.com/Rari-Capital/rari-components#9541f72a093da5969155d53324d281a90f7c6b1e", + "version": "git+ssh://git@github.com/Rari-Capital/rari-components.git#7ddc0eb58507b28e8b4be026cabb26294f179d0b", + "integrity": "sha512-27en+sZhntPvTW1XKaZ1jt6+NN1RmZcxgdn9/7tt34rBAohRRbZIhh/pkAt7Z5pjwLoBvscT0PSyXWCmXUqmjg==", + "from": "rari-components@git+https://github.com/Rari-Capital/rari-components#7ddc0eb58507b28e8b4be026cabb26294f179d0b", "requires": {} }, "rari-tokens-generator": { diff --git a/package.json b/package.json index 8beec3e9..b904132a 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "node-fetch": "^2.6.1", "node-vibrant": "^3.1.6", "polished": "^4.1.3", - "rari-components": "git+https://github.com/Rari-Capital/rari-components#9541f72a093da5969155d53324d281a90f7c6b1e", + "rari-components": "git+https://github.com/Rari-Capital/rari-components#7ddc0eb58507b28e8b4be026cabb26294f179d0b", "rari-tokens-generator": "^2.0.0", "react": "^17.0.2", "react-apexcharts": "1.3.7", diff --git a/src/components/pages/Fuse/FusePoolPage/IncentivesRows.tsx b/src/components/pages/Fuse/FusePoolPage/IncentivesRows.tsx index b90ee714..a082e4f5 100644 --- a/src/components/pages/Fuse/FusePoolPage/IncentivesRows.tsx +++ b/src/components/pages/Fuse/FusePoolPage/IncentivesRows.tsx @@ -4,6 +4,7 @@ import { Text, useDisclosure, AvatarGroup, + HStack, } from "@chakra-ui/react"; import { Row } from "lib/chakraUtils"; import { SimpleTooltip } from "components/shared/SimpleTooltip"; @@ -77,8 +78,8 @@ export const PluginIncentivesRow: React.FC<{ }> = ({ incentives, market, tokenData }) => { const { isOpen, onOpen, onClose } = useDisclosure() - const rewardTokens = Object.keys(incentives).map((flywheel, i) => incentives[flywheel].rewardToken) + const apr = Object.values(incentives).reduce((number, value) => value.formattedAPR + number, 0) return ( <> @@ -88,35 +89,42 @@ export const PluginIncentivesRow: React.FC<{ // mb={.5} crossAxisAlignment="center" mainAxisAlignment="flex-end" - py={2} + py={4} zIndex={10} onClick={(e) => { e.stopPropagation(); onOpen(); }} > - - + 🔌 - + + + {`+`} + + - - {rewardTokens.map((rewardToken, i) => { - return ( - handleMouseEnter(i)} - // onMouseLeave={() => handleMouseLeave()} - _hover={{ - zIndex: 9, - border: ".5px solid white", - transform: "scale(1.3);", - }} - /> - ); - })} - + + + {rewardTokens.map((rewardToken, i) => { + return ( + handleMouseEnter(i)} + // onMouseLeave={() => handleMouseLeave()} + _hover={{ + zIndex: 9, + border: ".5px solid white", + transform: "scale(1.3);", + }} + /> + ); + })} + + + {apr.toFixed(2)}% APR + + diff --git a/src/components/pages/Fuse/Modals/CVXMigrateModal.tsx b/src/components/pages/Fuse/Modals/CVXMigrateModal.tsx new file mode 100644 index 00000000..de9622c7 --- /dev/null +++ b/src/components/pages/Fuse/Modals/CVXMigrateModal.tsx @@ -0,0 +1,437 @@ +import { + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalFooter, + ModalBody, + ModalCloseButton, + Text, + Flex, + VStack, + HStack, + useToast, + Avatar, + Image, + Box, + Accordion, + AvatarGroup, +} from "@chakra-ui/react" +import { CTokenAvatarGroup, CTokenIcon } from "components/shared/Icons/CTokenIcon" +import { POOL_156_COMPTROLLER } from "constants/convex" +import { useRari } from "context/RariContext" +import { getEthUsdPriceBN } from "esm/utils/getUSDPriceBN" +import { BigNumber, constants } from "ethers" +import { commify, parseEther } from "ethers/lib/utils" +import { formatEther } from "ethers/lib/utils" +import { FlywheelPluginRewardsMap, useConvexPoolIncentives } from "hooks/convex/useConvexRewards" +import { useCurveLPBalances, useStakedConvexBalances } from "hooks/convex/useStakedConvexBalances" +import { useBorrowLimit, useTotalSupply } from "hooks/useBorrowLimit" +import { useFusePoolData } from "hooks/useFusePoolData" +import useHasApproval from "hooks/useHasApproval" +import { useTokensDataAsMap } from "hooks/useTokenData" +import { Button, ExpandableCard, Heading } from "rari-components/standalone"; +import { useEffect, useMemo, useState } from "react" +import { useQuery } from "react-query" +import { TokensDataMap } from "types/tokens" +import { shortUsdFormatter, smallStringUsdFormatter } from "utils/bigUtils" +import { checkAllowanceAndApprove, collateralize, deposit, unstakeAndWithdrawCVXPool } from "utils/convex/migratePositions" +import { handleGenericError } from "utils/errorHandling" +import { USDPricedFuseAsset } from "utils/fetchFusePoolData" + +export const CVXMigrateModal = ({ + isOpen, + onClose, +}: { + isOpen: boolean, + onClose: () => void, +}) => { + + const { address, fuse } = useRari() + const cvxBalances = useStakedConvexBalances() + const curveLPBalances = useCurveLPBalances() + + // Fuse pool Data + const fusePoolData = useFusePoolData("156") + const { incentives } = useConvexPoolIncentives(fusePoolData?.comptroller) ?? { incentives: {} }; + + const assets = !fusePoolData ? [] : fusePoolData.assets.filter(a => Object.keys(cvxBalances).includes(a.cToken) || Object.keys(curveLPBalances).includes(a.underlyingToken)) + const [assetIndex, setAssetIndex] = useState(0) + const tokenData = useTokensDataAsMap(assets.map(a => a.underlyingToken)) + + // Steppers + const toast = useToast() + const [step, setStep] = useState() + const [activeStep, setActiveStep] = useState() + + + // marketAddr -> underlying + const marketsUnderlyingMap: { [underlying: string]: string } = assets.reduce((obj, asset) => ({ + ...obj, + [asset.cToken]: asset.underlyingToken + }), {}) + + // marketAddr -> migratable balances data + const marketsBalancesMap: { + [marketAddr: string]: { + stakedBalance: BigNumber, + curveBalance: BigNumber, + total: BigNumber + } + } = assets.reduce((obj, asset) => { + const stakedBalance = cvxBalances[asset.cToken]?.balance ?? constants.Zero + const curveBalance = curveLPBalances[asset.underlyingToken] ?? constants.Zero + const total = stakedBalance.add(curveBalance) + + if (total.isZero()) return { ...obj } + return { + ...obj, + [asset.cToken]: { + stakedBalance, + curveBalance, + total + } + } + }, {}) + + const market = assets[assetIndex] + const marketBalanceForAsset = marketsBalancesMap[market?.cToken] + const pluginIncentivesForAsset = incentives[market?.cToken] + + // Simulates you depositing all your CVX positions into us - to get projected totalSupply & projected borrowLimit + const { data: updatedUserAssets } = useQuery('updated assets for convex user ' + address, async () => { + + const ethPrice = await getEthUsdPriceBN() + + const updatedUserAssets: USDPricedFuseAsset[] = fusePoolData?.assets.reduce((arr: USDPricedFuseAsset[], asset) => { + if (Object.keys(marketsBalancesMap).includes(asset.cToken)) { + const assetToBeUpdated = asset + const amount = (marketsBalancesMap[asset.cToken].total) + + const supplyBalance = assetToBeUpdated.supplyBalance.add(amount); + const totalSupply = assetToBeUpdated.totalSupply.add(amount); + + // Todo - figure out where to better div by 1e18 + const updatedAsset: USDPricedFuseAsset = { + ...assetToBeUpdated, + supplyBalance, + supplyBalanceUSD: supplyBalance + .mul(assetToBeUpdated.underlyingPrice) + .mul(parseEther(ethPrice.toString())) + .div(constants.WeiPerEther) + .div(constants.WeiPerEther) + .div(constants.WeiPerEther) + .div(constants.WeiPerEther), + totalSupply, + membership: true + } + return [...arr, updatedAsset] + } + return [...arr, asset] + }, []) ?? [] + + return updatedUserAssets + }) + + // Simulated position + const borrowLimit = useBorrowLimit(updatedUserAssets ?? []) + const newTotalSupply = useTotalSupply(updatedUserAssets ?? []) + + /* Unstakes and Claims all CVX Staked Positions supported by Fuse */ + const handleUnstake = async () => { + const { stakedBalance } = marketBalanceForAsset + try { + setActiveStep(1) + if (stakedBalance.gt(0)) { + const baseRewardPool = cvxBalances[assets[assetIndex].cToken].baseRewardsPool + const res = await unstakeAndWithdrawCVXPool(fuse, baseRewardPool) + setStep(2) + + } + } catch (err) { + setActiveStep(undefined) + + handleGenericError(err, toast) + } + } + + // Approve for stakedBalance + curveBalance + const handleApproveMarket = async () => { + const { cToken, underlyingToken } = assets[assetIndex] + const { total } = marketBalanceForAsset + try { + setActiveStep(2) + const res = await checkAllowanceAndApprove(fuse, address, cToken, underlyingToken, total) + console.log({ res }) + setStep(3) + } catch (err) { + setActiveStep(undefined) + + handleGenericError(err, toast) + } + } + + // Deposit curveBalance + const handleDeposit = async () => { + const { cToken } = assets[assetIndex] + const { curveBalance } = marketBalanceForAsset + try { + setActiveStep(3) + const res = await deposit(fuse, cToken, curveBalance) + console.log({ res }) + setStep(4) + } catch (err) { + setActiveStep(undefined) + + handleGenericError(err, toast) + } + } + + // Collateralize all + const handleCollateralize = async () => { + const markets = Object.keys(marketsBalancesMap) + try { + setActiveStep(4) + const res = await collateralize(fuse, POOL_156_COMPTROLLER, markets) + console.log({ res }) + setStep(5) + } catch (err) { + setActiveStep(undefined) + + handleGenericError(err, toast) + } + } + + return ( + <> + + + + + + + + Migrate Staked CVX Positions to Fuse + + + + + + + + + {/* */} + + + We detected {newTotalSupply.isZero() ? '?' : smallStringUsdFormatter(newTotalSupply.toString())} from {Object.keys(cvxBalances).length}{' '} + staked Convex positions + {!!(Object.keys(curveLPBalances)).length && ` and ${Object.keys(curveLPBalances).length} Curve LP tokens`}. + You can keep earning CVX + CRV rewards and borrow up to {borrowLimit.isZero() ? '?' : smallStringUsdFormatter(borrowLimit.toString())} by migrating them to Fuse. + {/* Select from available markets */} + setAssetIndex(i)}> + + {Object.keys(marketsBalancesMap).map((market, i) => + + )} + + + + + + + {!localStorage.RARI_HIDE_MIGRATOR_POPUP && ( + + )} + + + + + + ) +} + +const Market = ({ + assetIndex, + asset, + i, + tokensData, + marketsUnderlyingMap, + marketBalanceForAsset, + step, + activeStep, + setStep, + handleUnstake, + handleApproveMarket, + handleDeposit, + handleCollateralize, + updatedAssets, + pluginIncentivesForAsset +}: { + assetIndex: number, + setAssetIndex: (step: number) => void, + asset: USDPricedFuseAsset, + i: number; + tokensData: TokensDataMap, + marketsUnderlyingMap: { [underlying: string]: string }, + marketBalanceForAsset: { + stakedBalance: BigNumber, + curveBalance: BigNumber, + total: BigNumber + }, + step: number | undefined, + activeStep: number | undefined, + setStep: (step: number | undefined) => void, + handleUnstake: any, + handleApproveMarket: any, + handleDeposit: any, + handleCollateralize: any, + updatedAssets: USDPricedFuseAsset[] | undefined, + pluginIncentivesForAsset: FlywheelPluginRewardsMap | undefined, +}) => { + + const hasApproval = useHasApproval(asset, marketBalanceForAsset?.total.toString() ?? "0") + const showApproval = !hasApproval + // We show enable as Collateral only if this asset has not yet been anabled + const showEnableAsCollateral = !asset?.membership + // If you dont have any staked, you dont need to unstake to enter this market + const showUnstake = !marketBalanceForAsset?.stakedBalance?.isZero() ?? true + + const tokenData = tokensData[asset?.underlyingToken] + const activeSymbol = tokenData?.symbol ?? asset?.underlyingSymbol + + const [numClicks, setNumClicks] = useState(3) + + const updatedAsset = useMemo(() => { + if (!!updatedAssets && !!asset) { + return updatedAssets.find(a => a.cToken === asset.cToken) + } + }, [updatedAssets, asset]) + + const rewardTokens = Object.keys(pluginIncentivesForAsset ?? {})?.map((flywheel, i) => pluginIncentivesForAsset![flywheel].rewardToken) ?? [] + const apr = Object.values(pluginIncentivesForAsset ?? {})?.reduce((number, value) => value.formattedAPR + number, 0) ?? 0 + + // Skip to step conditionally + useEffect(() => { + let clicks = 3 + if (!showUnstake) { + setStep(2) + clicks -= 1 + } + if (hasApproval) { + setStep(3) + clicks -= 1 + } + else setStep(undefined) + setNumClicks(clicks) + }, [assetIndex]) + + + return ( + + + + Migrate {activeSymbol} in {numClicks} click{numClicks !== 1 && 's'} and earn + + + + + {apr.toFixed(2)}% APR + + + + + + + {showUnstake && ( + + )} + + {showApproval && ( + + )} + + + + {showEnableAsCollateral && step === 4 && + + } + + {step === 5 && Done!} + + + } + > + + + + + {activeSymbol} + + + + + {commify(parseFloat(formatEther(marketBalanceForAsset.stakedBalance)).toFixed(2))} staked + + + · + + + {commify(parseFloat(formatEther(marketBalanceForAsset.curveBalance)).toFixed(2))} unstaked + + + + + ) +} + + + + +export default CVXMigrateModal diff --git a/src/components/pages/Fuse/Modals/PluginModal/PluginRewardsModal.tsx b/src/components/pages/Fuse/Modals/PluginModal/PluginRewardsModal.tsx index 4e31b450..448f0cbc 100644 --- a/src/components/pages/Fuse/Modals/PluginModal/PluginRewardsModal.tsx +++ b/src/components/pages/Fuse/Modals/PluginModal/PluginRewardsModal.tsx @@ -13,7 +13,8 @@ import { Link, Button, Heading, -VStack, + VStack, + HStack, } from "@chakra-ui/react" import { TokenData } from "hooks/useTokenData" import { USDPricedFuseAsset } from "utils/fetchFusePoolData" @@ -21,7 +22,7 @@ import { USDPricedFuseAsset } from "utils/fetchFusePoolData" import { InfoIcon } from "@chakra-ui/icons" import AppLink from "components/shared/AppLink" import { CTokenAvatarGroup } from "components/shared/Icons/CTokenIcon" -import { eligibleTokens, tokenInfo } from "constants/convex" +import { eligibleTokens, CONVEX_CTOKEN_INFO } from "constants/convex" export const PluginRewardsModal = ({ market, @@ -59,31 +60,31 @@ export const PluginRewardsModal = ({ {/* */} - This market streams - rewards - from the {tokenInfo[market.underlyingSymbol].convexPoolName} Convex pool - to suppliers of {tokenInfo[market.underlyingSymbol].curvePoolName} Curve LPs. - {/* Deposit your {tokenInfo[market.underlyingSymbol].curvePoolName} Curve LP tokens into Fuse to borrow against it while earning all the same rewards from Convex. */} - {/* View reward rates for {tokenInfo[market.underlyingSymbol].convexPoolName} on Convex */} + + This market streams rewards + + from the {CONVEX_CTOKEN_INFO[market.underlyingSymbol].convexPoolName} Convex pool + to suppliers of {CONVEX_CTOKEN_INFO[market.underlyingSymbol].curvePoolName} Curve LPs. + {/* Deposit your {CONVEX_CTOKEN_INFO[market.underlyingSymbol].curvePoolName} Curve LP tokens into Fuse to borrow against it while earning all the same rewards from Convex. */} + {/* View reward rates for {CONVEX_CTOKEN_INFO[market.underlyingSymbol].convexPoolName} on Convex */} + * Rates shown do not include the base Curve vAPRs, but you get those too {/* */} - Info - - - - + + + } - diff --git a/src/components/pages/Fuse/Modals/PoolModal/AmountSelect.tsx b/src/components/pages/Fuse/Modals/PoolModal/AmountSelect.tsx index 420c7f66..747a1831 100644 --- a/src/components/pages/Fuse/Modals/PoolModal/AmountSelect.tsx +++ b/src/components/pages/Fuse/Modals/PoolModal/AmountSelect.tsx @@ -704,6 +704,9 @@ const StatsColumn = ({ ignoreIsEnabledCheckFor: enableAsCollateral ? asset.cToken : undefined, }, `new limit`); + + console.log({}) + // console.log({ assets, updatedAssets }) const isSupplyingOrWithdrawing = diff --git a/src/components/shared/Layout/Layout.tsx b/src/components/shared/Layout/Layout.tsx index 9d2be4b8..0bb005ac 100644 --- a/src/components/shared/Layout/Layout.tsx +++ b/src/components/shared/Layout/Layout.tsx @@ -7,6 +7,12 @@ import { useMemo, useState, useEffect } from "react"; import NewHeader from "../Header2/NewHeader"; import Footer from "./Footer"; +//CVX +import { useDisclosure } from "@chakra-ui/react"; +import CVXMigrateModal from "components/pages/Fuse/Modals/CVXMigrateModal"; +import { useCurveLPBalances, useStakedConvexBalances } from "hooks/convex/useStakedConvexBalances"; +import PluginMigratorButton from "./PluginMigratorButton"; + const Layout = ({ children }) => { const { chainId } = useRari() @@ -37,6 +43,16 @@ const Layout = ({ children }) => { return () => document.removeEventListener("keydown", handler); }, []); + const cvxBalances = useStakedConvexBalances() + const curveLPBalances = useCurveLPBalances() + const hasCvxBalances = !!Object.keys(cvxBalances ?? {}).length || !!Object.keys(curveLPBalances ?? {}).length + const { isOpen, onOpen, onClose } = useDisclosure() + + console.log({ cvxBalances, curveLPBalances }) + + useEffect(() => { + if (!!hasCvxBalances && (!localStorage.RARI_HIDE_MIGRATOR_POPUP)) onOpen() + }, [hasCvxBalances]) return ( { {children} + {!!hasCvxBalances && } + {!!hasCvxBalances && }