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 && }
);
@@ -69,4 +87,5 @@ const Layout = ({ children }) => {
+
export default Layout;
diff --git a/src/components/shared/Layout/PluginMigratorButton.tsx b/src/components/shared/Layout/PluginMigratorButton.tsx
new file mode 100644
index 00000000..540ec9b4
--- /dev/null
+++ b/src/components/shared/Layout/PluginMigratorButton.tsx
@@ -0,0 +1,32 @@
+
+
+import { Button, Image, keyframes, usePrefersReducedMotion } from "@chakra-ui/react"
+
+
+const spin = keyframes`
+ from { transform: rotateY(0deg); }
+ to { transform: rotateY(360deg); }
+`
+
+export const PluginMigratorButton = ({ onOpen }: { onOpen: any }) => {
+ const prefersReducedMotion = usePrefersReducedMotion()
+
+ const animation = prefersReducedMotion
+ ? undefined
+ : `${spin} infinite 5s linear`
+ return (
+
+ )
+}
+
+export default PluginMigratorButton
\ No newline at end of file
diff --git a/src/components/shared/SwitchNetworkMenu.tsx b/src/components/shared/SwitchNetworkMenu.tsx
index 407670ee..932e78c4 100644
--- a/src/components/shared/SwitchNetworkMenu.tsx
+++ b/src/components/shared/SwitchNetworkMenu.tsx
@@ -86,7 +86,6 @@ const SwitchNetworkMenu: React.FC = () => {
// If user presses meta key or control key + slash they will toggle the private allocation claim mode.
useEffect(() => {
const handler = (e: KeyboardEvent) => {
- console.log(e.code)
if (e.code === "Slash") {
e.preventDefault();
setDevMode(true);
diff --git a/src/constants/convex.ts b/src/constants/convex.ts
index ce99792c..6f70fa7a 100644
--- a/src/constants/convex.ts
+++ b/src/constants/convex.ts
@@ -1,6 +1,6 @@
export const eligibleTokens = ["FRAX3CRV-f", "steCRV", "UST_whv23CRV-f", "crv3crypto", "D3-f", "FEI3CRV3CRV-f", "alUSD3CRV-f"]
-export const tokenInfo: PluginCTokenInfoType = {
+export const CONVEX_CTOKEN_INFO: PluginCTokenInfoType = {
"FRAX3CRV-f": {
"cToken": "0x2ec70d3Ff3FD7ac5c2a72AAA64A398b6CA7428A5",
"plugin": "0xd88e89ac6a0859e9b91078cb2a183a36ba6c8933",
@@ -89,3 +89,8 @@ type PluginCTokenInfoType = {
}
export const POOL_156_COMPTROLLER = "0x07cd53380FE9B2a5E64099591b498c73F0EfaA66"
+
+
+export const SUPPORTED_CONVEX_LPS = [
+
+]
\ No newline at end of file
diff --git a/src/context/BalancesContext.tsx b/src/context/BalancesContext.tsx
index 1d06711c..d2ffd938 100644
--- a/src/context/BalancesContext.tsx
+++ b/src/context/BalancesContext.tsx
@@ -1,9 +1,9 @@
+
import { useTokenBalances } from "hooks/useTokenBalance";
import { SubgraphUnderlyingAsset } from "pages/api/explore";
-import { createContext, useContext, ReactNode, useMemo } from "react";
+import { createContext, useContext, ReactNode, useMemo, useEffect } from "react";
import { queryAllUnderlyingAssets } from "services/gql";
import useSWR from "swr";
-import { UnderlyingAsset } from "types/tokens";
import { useRari } from "./RariContext";
export const BalancesContext = createContext(undefined);
@@ -54,15 +54,16 @@ export const BalancesContextProvider = ({
return ret;
}, [tokenBalances, isAuthed]);
+
return (
-
+
{children}
);
};
export const useAccountBalances = (): [any, string[]] => {
- const balances = useContext(BalancesContext);
+ const { balances} = useContext(BalancesContext);
const significantTokens: string[] = useMemo(
() =>
diff --git a/src/context/RariContext.tsx b/src/context/RariContext.tsx
index 4a02abf0..a747859e 100644
--- a/src/context/RariContext.tsx
+++ b/src/context/RariContext.tsx
@@ -16,7 +16,7 @@ import { DASHBOARD_BOX_PROPS } from "../components/shared/DashboardBox";
import { Fuse } from "../esm";
-import LogRocket, { init } from "logrocket";
+import LogRocket from "logrocket";
import { useToast } from "@chakra-ui/react";
import {
@@ -131,6 +131,10 @@ export const RariProvider = ({ children }: { children: ReactNode }) => {
const queryClient = useQueryClient();
const { t } = useTranslation();
+ useEffect(() => {
+ // if (!!requestedAddress) setAddress(requestedAddress as string)
+ }, [requestedAddress])
+
useEffect(() => {
//toast on brave users
if(typeof navigator !== 'object') return
diff --git a/src/contracts/abi/ConvexBaseRewardPool.json b/src/contracts/abi/ConvexBaseRewardPool.json
new file mode 100644
index 00000000..35189fec
--- /dev/null
+++ b/src/contracts/abi/ConvexBaseRewardPool.json
@@ -0,0 +1 @@
+[{"inputs":[{"internalType":"uint256","name":"pid_","type":"uint256"},{"internalType":"address","name":"stakingToken_","type":"address"},{"internalType":"address","name":"rewardToken_","type":"address"},{"internalType":"address","name":"operator_","type":"address"},{"internalType":"address","name":"rewardManager_","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"reward","type":"uint256"}],"name":"RewardAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"reward","type":"uint256"}],"name":"RewardPaid","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Staked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Withdrawn","type":"event"},{"inputs":[{"internalType":"address","name":"_reward","type":"address"}],"name":"addExtraReward","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"clearExtraRewards","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"currentRewards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"donate","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"duration","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"earned","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"extraRewards","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"extraRewardsLength","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getReward","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_account","type":"address"},{"internalType":"bool","name":"_claimExtras","type":"bool"}],"name":"getReward","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"historicalRewards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"lastTimeRewardApplicable","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"lastUpdateTime","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"newRewardRatio","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"operator","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"periodFinish","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pid","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_rewards","type":"uint256"}],"name":"queueNewRewards","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"queuedRewards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"rewardManager","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"rewardPerToken","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"rewardPerTokenStored","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"rewardRate","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"rewardToken","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"rewards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"stake","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"stakeAll","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_for","type":"address"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"stakeFor","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"stakingToken","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"userRewardPerTokenPaid","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"bool","name":"claim","type":"bool"}],"name":"withdraw","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bool","name":"claim","type":"bool"}],"name":"withdrawAll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bool","name":"claim","type":"bool"}],"name":"withdrawAllAndUnwrap","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"bool","name":"claim","type":"bool"}],"name":"withdrawAndUnwrap","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}]
\ No newline at end of file
diff --git a/src/esm/utils/networks.js b/src/esm/utils/networks.js
index 4929a457..bfe6ead8 100644
--- a/src/esm/utils/networks.js
+++ b/src/esm/utils/networks.js
@@ -81,15 +81,15 @@ export const chainMetadata = {
blockExplorerURL: "https://optimistic.etherscan.io",
color: "#FE0521",
},
- // [ChainID.HARDHAT]: {
- // chainId: ChainID.HARDHAT,
- // name: "Hardhat",
- // imageUrl: "/static/networks/optimism.svg", // no logo
- // supported: true,
- // rpcUrl: "http://localhost:8545",
- // blockExplorerURL: "",
- // color: "#BC6C6C"
- // }
+ [ChainID.HARDHAT]: {
+ chainId: ChainID.HARDHAT,
+ name: "Hardhat",
+ imageUrl: "/static/networks/optimism.svg", // no logo
+ supported: true,
+ rpcUrl: "http://localhost:8545",
+ blockExplorerURL: "",
+ color: "#BC6C6C"
+ }
};
export const isSupportedChainId = (chainId) => Object.values(ChainID).includes(chainId);
export function getSupportedChains() {
diff --git a/src/hooks/convex/useConvexPoolSuppliedCTokens.ts b/src/hooks/convex/useConvexPoolSuppliedCTokens.ts
index 4578bb92..2edee266 100644
--- a/src/hooks/convex/useConvexPoolSuppliedCTokens.ts
+++ b/src/hooks/convex/useConvexPoolSuppliedCTokens.ts
@@ -1,4 +1,4 @@
-import { tokenInfo } from 'constants/convex';
+import { CONVEX_CTOKEN_INFO } from 'constants/convex';
import { useRari } from 'context/RariContext'
import { BigNumber } from 'ethers';
import { Interface } from 'ethers/lib/utils';
@@ -11,7 +11,7 @@ export const useConvexPoolSuppliedCTokens = (comptrollerAddress: string) => {
const { data: suppliedMarkets } = useQuery(`Pool ${comptrollerAddress} CTokens User ${address}`, async () => {
- const markets = Object.values(tokenInfo).map((value => value.cToken))
+ const markets = Object.values(CONVEX_CTOKEN_INFO).map((value => value.cToken))
const IComptroller = new Interface(JSON.parse(
fuse.compoundContracts["contracts/CErc20Delegate.sol:CErc20Delegate"].abi
diff --git a/src/hooks/convex/useStakedConvexBalances.ts b/src/hooks/convex/useStakedConvexBalances.ts
new file mode 100644
index 00000000..a2f5e6c2
--- /dev/null
+++ b/src/hooks/convex/useStakedConvexBalances.ts
@@ -0,0 +1,121 @@
+import { providers } from "@0xsequence/multicall";
+import { useRari } from "context/RariContext";
+import { useQuery } from "react-query";
+
+import BaseRewardPoolABI from "contracts/abi/ConvexBaseRewardPool.json"
+import { CONVEX_CTOKEN_INFO } from "constants/convex";
+import { Contract } from "ethers";
+import { BigNumber } from "ethers";
+import { Interface } from "ethers/lib/utils";
+
+export type StakedConvexBalancesMap = {
+ [cToken: string]: {
+ balance: BigNumber,
+ baseRewardsPool: string
+ }
+}
+
+
+/* For Staked CVX positions - there's no ERC20 to query balance for */
+export const useStakedConvexBalances = (): StakedConvexBalancesMap => {
+ const { fuse, address, isAuthed } = useRari();
+ const multiCallProvider = new providers.MulticallProvider(fuse.provider)
+ const { data: stakedConvexBalances } = useQuery(
+ ' staked convex balances for ' + address,
+ async () => {
+ if (!isAuthed) return undefined
+
+ let map: StakedConvexBalancesMap = {}
+
+ await Promise.all(Object.values(CONVEX_CTOKEN_INFO)
+ .map(c => ({ baseRewardsPool: c.rewardsContract, cToken: c.cToken }))
+ .map(({ baseRewardsPool, cToken }) => {
+ let contract = new Contract(baseRewardsPool, BaseRewardPoolABI as any, multiCallProvider)
+ return contract.balanceOf(address)
+ .then((balance: BigNumber) => {
+ if (!balance.isZero()) {
+ map[cToken] = {
+ balance,
+ baseRewardsPool,
+ }
+ }
+ })
+ .catch((_err: any) => { })
+ }
+ ))
+
+ console.log({ map })
+
+ return map
+ },
+ {
+ enabled: !!address ? true : false,
+ refetchOnMount: false,
+ refetchOnWindowFocus: false,
+ }
+ )
+
+ // const hasBalances =
+
+ return stakedConvexBalances ?? {}
+
+}
+
+
+type BalancesMap = {
+ [token: string]: BigNumber
+}
+
+const IERC20 = new Interface([
+ 'function name() public view returns (string)',
+ 'function symbol() public view returns (string)',
+ 'function decimals() public view returns (uint8)',
+ 'function balanceOf(address _owner) public view returns (uint256 balance)',
+])
+
+export const useCurveLPBalances = (): BalancesMap => {
+
+
+ const { fuse, address, isAuthed } = useRari();
+ const multiCallProvider = new providers.MulticallProvider(fuse.provider)
+
+ const { data: curveLPBalances } = useQuery(
+ ' curve convex balances for ' + address,
+ async () => {
+
+ if (!isAuthed) return undefined
+
+ let map: BalancesMap = {}
+
+ await Promise.all(
+ Object.values(CONVEX_CTOKEN_INFO)
+ .map(c => ({ curveLPToken: c.lpToken }))
+ .map(({ curveLPToken }) => {
+ let contract = new Contract(curveLPToken, IERC20, multiCallProvider)
+ return contract.balanceOf(address)
+ .then((balance: BigNumber) => {
+ if (!balance.isZero()) {
+ map[curveLPToken] = balance
+ }
+ })
+ .catch((_err: any) => { })
+ }
+ ))
+ return map
+ },
+ {
+ enabled: !!address ? true : false,
+ refetchOnMount: false,
+ refetchOnWindowFocus: false,
+ }
+ )
+
+
+ console.log({curveLPBalances})
+ // const hasBalances =
+
+ return curveLPBalances ?? {}
+
+}
+
+
diff --git a/src/hooks/useBorrowLimit.ts b/src/hooks/useBorrowLimit.ts
index 764ca825..4220620d 100644
--- a/src/hooks/useBorrowLimit.ts
+++ b/src/hooks/useBorrowLimit.ts
@@ -30,6 +30,26 @@ export const useBorrowLimit = (
return maxBorrow;
};
+export const useTotalSupply = (
+ assets: USDPricedFuseAsset[],
+): BigNumber => {
+ const totalSupply = useMemo(() => {
+ let _totalSupplyUSD = constants.Zero;
+
+ for (let i = 0; i < assets.length; i++) {
+ let asset = assets[i];
+ _totalSupplyUSD = _totalSupplyUSD.add(
+ asset.supplyBalanceUSD
+ )
+ }
+
+ // console.log({result, _maxBorrow, id})
+ return _totalSupplyUSD
+ }, [assets]);
+
+ return totalSupply;
+};
+
export const useBorrowLimits = (
assetsArray: USDPricedFuseAsset[][] | null,
options?: { ignoreIsEnabledCheckFor?: string }
diff --git a/src/hooks/useHasApproval.ts b/src/hooks/useHasApproval.ts
new file mode 100644
index 00000000..29b6f646
--- /dev/null
+++ b/src/hooks/useHasApproval.ts
@@ -0,0 +1,49 @@
+import { useQuery } from "react-query"
+import { useRari } from "context/RariContext"
+import { BigNumber, Contract } from "ethers"
+import { Interface, parseUnits } from "ethers/lib/utils"
+import { USDPricedFuseAsset } from "utils/fetchFusePoolData"
+import { isAssetETH } from "utils/tokenUtils"
+
+
+const useHasApproval = (market: USDPricedFuseAsset, amount: string) => {
+ const { fuse, address } = useRari()
+ const { provider } = fuse
+
+ const { data } = useQuery(`${address} has approval for ${market?.underlyingSymbol}`, async () => {
+ if (!address || !market) return false
+ let _amount = parseUnits(amount, market.underlyingDecimals)
+ return await checkAllowance(address, market.cToken, market.underlyingToken, _amount, provider)
+ })
+ return data ?? false
+}
+
+export default useHasApproval
+
+export async function checkAllowance(
+ userAddress: string,
+ marketAddress: string,
+ underlyingAddress: string,
+ amount: BigNumber,
+ provider: any
+) {
+ if (isAssetETH(underlyingAddress)) return
+
+ const erc20Interface = new Interface([
+ 'function allowance(address owner, address spender) public view returns (uint256 remaining)',
+ 'function approve(address spender, uint256 value) public returns (bool success)',
+ ])
+
+ const erc20Contract = new Contract(
+ underlyingAddress,
+ erc20Interface,
+ provider.getSigner(userAddress)
+ )
+
+ const hasApproval = (
+ await erc20Contract.callStatic.allowance(userAddress, marketAddress)
+ ).gte(amount);
+
+ return hasApproval
+
+}
\ No newline at end of file
diff --git a/src/utils/convex/migratePositions.ts b/src/utils/convex/migratePositions.ts
new file mode 100644
index 00000000..7ad97eb3
--- /dev/null
+++ b/src/utils/convex/migratePositions.ts
@@ -0,0 +1,194 @@
+import { testForCTokenErrorAndSend } from "components/pages/Fuse/Modals/PoolModal/AmountSelect";
+import BaseRewardPoolABI from "contracts/abi/ConvexBaseRewardPool.json"
+import { Contract } from "ethers";
+import { constants } from "ethers";
+import { BigNumber } from "ethers";
+import { Interface } from "ethers/lib/utils"
+import { USDPricedFuseAsset } from "utils/fetchFusePoolData";
+import { callStaticWithMultiCall, encodeCall, estimateGasWithMultiCall, sendWithMultiCall } from "utils/multicall"
+import { Fuse } from "../../esm";
+
+
+export const unstakeAndWithdrawCVXPool = async (fuse: Fuse, baseRewardPool: string) => {
+ const IBaseRewardPool = new Interface(JSON.stringify(BaseRewardPoolABI))
+
+ const BaseRewardPool = new Contract(baseRewardPool, IBaseRewardPool, fuse.provider.getSigner())
+
+ try {
+ let estimatedGas = await BaseRewardPool.estimateGas.withdrawAllAndUnwrap(true)
+ console.log({ estimatedGas })
+
+ let result = await BaseRewardPool.callStatic.withdrawAllAndUnwrap(true)
+ console.log({ result })
+
+ let tx = await BaseRewardPool.withdrawAllAndUnwrap(true)
+ console.log({ tx })
+
+ const txConfirmed = await tx.wait(1)
+ return txConfirmed;
+
+ } catch (err) {
+ console.error("Could not unstake and claim CVX Rewards")
+ throw err
+ }
+}
+
+
+/**
+ * @param marketAddress - Market/ctoken to give approval to.
+ * @param underlyingAddress - The token to approve.
+ * @param amount - Amount user is supplying.
+ * @param provider - An initiated ethers provider.
+ */
+export async function checkAllowanceAndApprove(
+ fuse: Fuse,
+ userAddress: string,
+ marketAddress: string,
+ underlyingAddress: string,
+ amount: BigNumber,
+) {
+ const erc20Interface = new Interface([
+ 'function allowance(address owner, address spender) public view returns (uint256 remaining)',
+ 'function approve(address spender, uint256 value) public returns (bool success)',
+ ])
+
+ const erc20Contract = new Contract(
+ underlyingAddress,
+ erc20Interface,
+ fuse.provider.getSigner(userAddress)
+ )
+
+ const hasApprovedEnough = (
+ await erc20Contract.callStatic.allowance(userAddress, marketAddress)
+ ).gte(amount);
+
+ if (!hasApprovedEnough) {
+ const max = BigNumber.from(2).pow(BigNumber.from(256)).sub(constants.One); //big fucking #
+ let tx = await erc20Contract.approve(marketAddress, max);
+ const txConfirmed = await tx.wait(1)
+ return txConfirmed;
+ }
+}
+
+// Todo - remove. Will be replaced by SDK
+export const deposit = async (fuse: Fuse, marketAddress: string, amount: BigNumber) => {
+ const market = new Contract(
+ marketAddress,
+ JSON.parse(
+ fuse.compoundContracts[
+ "contracts/CErc20Delegate.sol:CErc20Delegate"
+ ].abi),
+ fuse.provider.getSigner()
+ );
+
+ let tx = await testForCTokenErrorAndSend(
+ market.callStatic.mint,
+ amount,
+ market.mint,
+ "Cannot deposit this amount right now!"
+ );
+
+ const txConfirmed = await tx.wait(1)
+ console.log({txConfirmed})
+
+ return txConfirmed
+}
+
+export const collateralize = async (fuse: Fuse, comptrollerAddress: string, marketAddresses: string[]) => {
+ const comptroller = new Contract(
+ comptrollerAddress,
+ JSON.parse(
+ fuse.compoundContracts["contracts/Comptroller.sol:Comptroller"].abi
+ ),
+ fuse.provider.getSigner()
+ );
+
+ let tx = await comptroller.enterMarkets(marketAddresses);
+ const txConfirmed = await tx.wait(1)
+ return txConfirmed;
+}
+
+/*
+Can't Multicall these :/
+
+export const unstakeAndWithdrawStakedCVXMulti = async (fuse: Fuse, baseRewardPools: string[]) => {
+ const IBaseRewardPool = new Interface(JSON.stringify(BaseRewardPoolABI))
+
+ const encodedCalls = baseRewardPools.map(baseRewardPool => encodeCall(IBaseRewardPool, baseRewardPool, "withdrawAllAndUnwrap", [true]))
+ try {
+ let estimatedGas = await estimateGasWithMultiCall(fuse.provider, encodedCalls)
+ console.log({ estimatedGas })
+
+ let result = await callStaticWithMultiCall(fuse.provider, encodedCalls)
+ console.log({ result })
+
+ result = await sendWithMultiCall(fuse, encodedCalls)
+ console.log({ result })
+
+ return result
+
+ } catch (err) {
+ console.error("Could not unstake and claim CVX Rewards")
+ }
+}
+
+// Todo - move elswhere
+export const approveAllMarketsMulti = async (fuse: Fuse, assets: USDPricedFuseAsset[]) => {
+ const MAX_APPROVAL = BigNumber.from(2).pow(BigNumber.from(256)).sub(constants.One); //big fucking #
+
+ const IERC20 = new Interface(JSON.parse(
+ fuse.compoundContracts[
+ "contracts/EIP20Interface.sol:EIP20Interface"
+ ].abi
+ ))
+
+ const encodedCalls = assets.map(asset => encodeCall(IERC20, asset.underlyingToken, "approve", [asset.cToken, MAX_APPROVAL]))
+ try {
+ let estimatedGas = await estimateGasWithMultiCall(fuse.provider, encodedCalls)
+ console.log({ estimatedGas })
+
+ let result = await callStaticWithMultiCall(fuse.provider, encodedCalls)
+ console.log({ result })
+
+ result = await sendWithMultiCall(fuse, encodedCalls)
+ console.log({ result })
+
+ return result
+
+ } catch (err) {
+ console.error("Could not unstake and claim CVX Rewards")
+ }
+
+}
+
+
+export const unstakeAndWithdrawStakedCVXAndApproveCtokens = async (fuse: Fuse, baseRewardPools: string[], assets: USDPricedFuseAsset[]) => {
+ const IBaseRewardPool = new Interface(JSON.stringify(BaseRewardPoolABI))
+ const IERC20 = new Interface(JSON.parse(
+ fuse.compoundContracts[
+ "contracts/EIP20Interface.sol:EIP20Interface"
+ ].abi
+ ))
+ const MAX_APPROVAL = BigNumber.from(2).pow(BigNumber.from(256)).sub(constants.One); //big fucking #
+
+ let cvxEncodedCalls = baseRewardPools.map(baseRewardPool => encodeCall(IBaseRewardPool, baseRewardPool, "withdrawAllAndUnwrap", [true]))
+ let approvalEncodedCalls = assets.map(asset => encodeCall(IERC20, asset.underlyingToken, "approve", [asset.cToken, MAX_APPROVAL]))
+ const encodedCalls = [...cvxEncodedCalls, ...approvalEncodedCalls]
+
+ try {
+ let estimatedGas = await estimateGasWithMultiCall(fuse.provider, encodedCalls)
+ console.log({ estimatedGas })
+
+ let result = await callStaticWithMultiCall(fuse.provider, encodedCalls)
+ console.log({ result })
+
+ result = await sendWithMultiCall(fuse, encodedCalls)
+ console.log({ result })
+
+ return result
+
+ } catch (err) {
+ console.error("Could not unstake and claim CVX Rewards and Approve CTokens :( :(")
+ }
+}
+*/
\ No newline at end of file
diff --git a/src/utils/multicall.ts b/src/utils/multicall.ts
index 39efc34d..acd021fb 100644
--- a/src/utils/multicall.ts
+++ b/src/utils/multicall.ts
@@ -18,16 +18,16 @@ export const createMultiCall = (provider: JsonRpcProvider | Web3Provider,) => {
export const sendWithMultiCall = async (
fuse: Fuse,
encodedCalls: any,
- address: string
+ address?: string
) => {
// @ts-ignore
const multicall = createMultiCall(fuse.provider);
- console.log("sendWithMultiCall", { encodedCalls, multicall });
+ console.log("sendWithMultiCall", { encodedCalls, multicall });
+ let options: any = {}
+ if (!!address) options.address = address
- const returnDatas = await multicall.aggregate(encodedCalls, {
- from: address,
- });
+ const returnDatas = await multicall.aggregate(encodedCalls, options);
return returnDatas;
};
@@ -48,6 +48,21 @@ export const callStaticWithMultiCall = async (
};
+export const estimateGasWithMultiCall = async (
+ provider: JsonRpcProvider | Web3Provider,
+ encodedCalls: EncodedCall[],
+ address?: string
+) => {
+ const multicall = createMultiCall(provider);
+ let options: any = {}
+ if (!!address) options.address = address
+
+ const estimatedGas = await multicall.estimateGas.aggregate(encodedCalls, options)
+
+ return estimatedGas;
+};
+
+
export const callInterfaceWithMulticall = async (
provider: JsonRpcProvider | Web3Provider,