diff --git a/src/components/pages/Fuse/Modals/CVXMigrateModal.tsx b/src/components/pages/Fuse/Modals/CVXMigrateModal.tsx new file mode 100644 index 00000000..ecf103ff --- /dev/null +++ b/src/components/pages/Fuse/Modals/CVXMigrateModal.tsx @@ -0,0 +1,368 @@ +import { + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalFooter, + ModalBody, + ModalCloseButton, + Text, + Flex, + VStack, + Spacer, + Button, + HStack, + useToast, + Avatar, + Box, + Image, +} from "@chakra-ui/react" +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 { 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 { useEffect, useMemo, useState } from "react" +import { useQuery } from "react-query" +import { TokensDataMap } from "types/tokens" +import { 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 { fuse, address } = useRari() + const cvxBalances = useStakedConvexBalances() + const curveLPBalances = useCurveLPBalances() + + // Steppers + const toast = useToast() + const [step, setStep] = useState<1 | 2 | 3 | 4 | 5 | undefined>() + const [activeStep, setActiveStep] = useState<1 | 2 | 3 | 4 | 5 | undefined>() + + // Fuse pool Data + const fusePoolData = useFusePoolData("156") + 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)) + + // 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 marketBalanceForAsset = marketsBalancesMap[assets[assetIndex]?.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) + + console.log({ assetToBeUpdated, amount }) + + 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) + } + } + + + // If you've already approved this market, skip + const hasApproval = useHasApproval(assets[assetIndex], marketBalanceForAsset?.total.toString() ?? "0") + const showApproval = !hasApproval + // We show enable as Collateral only if this asset has not yet been anabled + const showEnableAsCollateral = !assets[assetIndex]?.membership + // If you dont have any staked, you dont need to unstake to enter this market + const showUnstake = !marketBalanceForAsset?.stakedBalance?.isZero() ?? true + + const activeSymbol = tokenData[assets[assetIndex]?.underlyingToken]?.symbol + + // Skip to step conditionally + useEffect(() => { + if (!showUnstake) setStep(2) + else if (hasApproval) setStep(3) + else setStep(undefined) + }, [assetIndex]) + + + return ( + <> + + + + + + + + Migrate Staked CVX Positions to Fuse + + + + + + + + {/* */} + + + We detected {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 borrow up to {smallStringUsdFormatter(borrowLimit.toString())} by migrating them to Fuse. + {/* Select from available markets */} + + {Object.keys(marketsBalancesMap).map((market, i) => + + )} + + + + Migrate {activeSymbol} in 3 clicks + + + {showUnstake && ( + + + 1.) + + + + )} + + {showApproval && ( + + + 2.) + + + + )} + + + + 3.) + + + + + {showEnableAsCollateral && step === 4 && + + 4.) + + + } + + {step === 5 && Done!} + + + + + + + + + + + ) +} + +const Market = ({ + setAssetIndex, + assetIndex, + market, + i, + tokensData, + marketsUnderlyingMap, + marketsBalancesMap +}: { + setAssetIndex: any, + assetIndex: number, + market: string, + i: number; + tokensData: TokensDataMap, + marketsUnderlyingMap: { [underlying: string]: string }, + marketsBalancesMap: { + [cToken: string]: { + stakedBalance: BigNumber, + curveBalance: BigNumber, + total: BigNumber + } + } +}) => { + return ( + setAssetIndex(i)} bg={assetIndex === i ? "aqua" : "white"} border="1px solid red"> + + + + + {tokensData[marketsUnderlyingMap[market]]?.symbol} + + + + {commify(parseFloat(formatEther(marketsBalancesMap[market].stakedBalance)).toFixed(2))} staked + + + {commify(parseFloat(formatEther(marketsBalancesMap[market].curveBalance)).toFixed(2))} in Curve + + + + ) +} + + + +export default CVXMigrateModal \ No newline at end of file diff --git a/src/components/pages/Fuse/Modals/PluginModal/PluginRewardsModal.tsx b/src/components/pages/Fuse/Modals/PluginModal/PluginRewardsModal.tsx index 4e31b450..6d06cfda 100644 --- a/src/components/pages/Fuse/Modals/PluginModal/PluginRewardsModal.tsx +++ b/src/components/pages/Fuse/Modals/PluginModal/PluginRewardsModal.tsx @@ -21,7 +21,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, @@ -61,10 +61,10 @@ 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 */} + 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 */} {/* */} @@ -73,9 +73,9 @@ export const PluginRewardsModal = ({ Info - - - + + + } @@ -83,7 +83,7 @@ export const PluginRewardsModal = ({ diff --git a/src/components/shared/Layout/Layout.tsx b/src/components/shared/Layout/Layout.tsx index 9d2be4b8..24e0abba 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) onOpen() + }, [hasCvxBalances]) return ( { {children} + {!!hasCvxBalances && } + {!!hasCvxBalances && }