diff --git a/src/custom/hooks/useCowBalanceAndSubsidy.ts b/src/custom/hooks/useCowBalanceAndSubsidy.ts index b44931bab7..1470bfa1e1 100644 --- a/src/custom/hooks/useCowBalanceAndSubsidy.ts +++ b/src/custom/hooks/useCowBalanceAndSubsidy.ts @@ -4,7 +4,7 @@ import { JSBI } from '@uniswap/sdk' import { CurrencyAmount } from '@uniswap/sdk-core' import { getDiscountFromBalance } from 'components/CowSubsidyModal/utils' -import { useVCowData } from 'state/claim/hooks' +import { useVCowData } from 'state/swapVCow/hooks' import { useTokenBalance } from 'state/wallet/hooks' import { useActiveWeb3React } from '.' diff --git a/src/custom/pages/App/index.tsx b/src/custom/pages/App/index.tsx index 7299c5264f..aac12b79fc 100644 --- a/src/custom/pages/App/index.tsx +++ b/src/custom/pages/App/index.tsx @@ -2,7 +2,7 @@ import AppMod from './AppMod' import styled from 'styled-components/macro' import { RedirectPathToSwapOnly, RedirectToSwap } from 'pages/Swap/redirects' import { Suspense, lazy } from 'react' -import { Route, Switch } from 'react-router-dom' +import { Redirect, Route, Switch } from 'react-router-dom' import AnySwapAffectedUsers from 'pages/error/AnySwapAffectedUsers' import * as Sentry from '@sentry/react' @@ -17,7 +17,6 @@ const SENTRY_DSN = process.env.REACT_APP_SENTRY_DSN const SENTRY_TRACES_SAMPLE_RATE = process.env.REACT_APP_SENTRY_TRACES_SAMPLE_RATE const Swap = lazy(() => import(/* webpackPrefetch: true, webpackChunkName: "swap" */ 'pages/Swap')) -const Claim = lazy(() => import(/* webpackChunkName: "claim" */ 'pages/Claim')) const PrivacyPolicy = lazy(() => import(/* webpackChunkName: "privacy_policy" */ 'pages/PrivacyPolicy')) const CookiePolicy = lazy(() => import(/* webpackChunkName: "cookie_policy" */ 'pages/CookiePolicy')) const TermsAndConditions = lazy(() => import(/* webpackChunkName: "terms" */ 'pages/TermsAndConditions')) @@ -99,10 +98,10 @@ export default function App() { + - diff --git a/src/custom/pages/Profile/index.tsx b/src/custom/pages/Profile/index.tsx index 80fe448ad8..e4e2cb7515 100644 --- a/src/custom/pages/Profile/index.tsx +++ b/src/custom/pages/Profile/index.tsx @@ -42,15 +42,13 @@ import ArrowIcon from 'assets/cow-swap/arrow.svg' import CowImage from 'assets/cow-swap/cow_v2.svg' import CowProtocolImage from 'assets/cow-swap/cowprotocol.svg' import { useTokenBalance } from 'state/wallet/hooks' -import { useVCowData } from 'state/claim/hooks' +import { useVCowData, useSwapVCowCallback, useSetSwapVCowStatus, useSwapVCowStatus } from 'state/swapVCow/hooks' import { AMOUNT_PRECISION } from 'constants/index' import { COW } from 'constants/tokens' import { useErrorModal } from 'hooks/useErrorMessageAndModal' import { OperationType } from 'components/TransactionConfirmationModal' import useTransactionConfirmationModal from 'hooks/useTransactionConfirmationModal' -import { useClaimDispatchers, useClaimState } from 'state/claim/hooks' -import { SwapVCowStatus } from 'state/claim/actions' -import { useSwapVCowCallback } from 'state/claim/hooks' +import { SwapVCowStatus } from 'state/swapVCow/actions' const COW_DECIMALS = COW[ChainId.MAINNET].decimals @@ -62,8 +60,8 @@ export default function Profile() { const isTradesTooltipVisible = account && chainId == 1 && !!profileData?.totalTrades const hasOrders = useHasOrders(account) - const { setSwapVCowStatus } = useClaimDispatchers() - const { swapVCowStatus } = useClaimState() + const setSwapVCowStatus = useSetSwapVCowStatus() + const swapVCowStatus = useSwapVCowStatus() // Cow balance const cow = useTokenBalance(account || undefined, chainId ? COW[chainId] : undefined) diff --git a/src/custom/state/index.ts b/src/custom/state/index.ts index c82096b1b1..415f1eb84d 100644 --- a/src/custom/state/index.ts +++ b/src/custom/state/index.ts @@ -24,9 +24,10 @@ import { updateVersion } from 'state/global/actions' import affiliate from 'state/affiliate/reducer' import enhancedTransactions from 'state/enhancedTransactions/reducer' import claim from 'state/claim/reducer' +import swapVCow from 'state/swapVCow/reducer' import { popupMiddleware, soundMiddleware } from './orders/middleware' -import { claimMinedMiddleware } from './claim/middleware' +import { swapVCowMiddleware } from './swapVCow/middleware' import { DEFAULT_NETWORK_FOR_LISTS } from 'constants/lists' const UNISWAP_REDUCERS = { @@ -54,6 +55,7 @@ const reducers = { affiliate, profile, claim, + swapVCow, } const PERSISTED_KEYS: string[] = ['user', 'transactions', 'orders', 'lists', 'gas', 'affiliate', 'profile'] @@ -66,7 +68,7 @@ const store = configureStore({ // .concat(routingApi.middleware) .concat(save({ states: PERSISTED_KEYS, debounce: 1000 })) .concat(popupMiddleware) - .concat(claimMinedMiddleware) + .concat(swapVCowMiddleware) .concat(soundMiddleware), preloadedState: load({ states: PERSISTED_KEYS, disableWarnings: process.env.NODE_ENV === 'test' }), }) diff --git a/src/custom/state/swapVCow/actions.ts b/src/custom/state/swapVCow/actions.ts new file mode 100644 index 0000000000..45eb51e852 --- /dev/null +++ b/src/custom/state/swapVCow/actions.ts @@ -0,0 +1,13 @@ +import { createAction } from '@reduxjs/toolkit' + +export enum SwapVCowStatus { + INITIAL = 'INITIAL', + ATTEMPTING = 'ATTEMPTING', + SUBMITTED = 'SUBMITTED', +} + +export type SwapVCowActions = { + setStatus: (payload: SwapVCowStatus) => void +} + +export const setStatus = createAction('swapVCow/setStatus') diff --git a/src/custom/state/swapVCow/hooks.ts b/src/custom/state/swapVCow/hooks.ts new file mode 100644 index 0000000000..942ea03dac --- /dev/null +++ b/src/custom/state/swapVCow/hooks.ts @@ -0,0 +1,152 @@ +import { useCallback, useMemo } from 'react' + +import { Currency, CurrencyAmount } from '@uniswap/sdk-core' +import { TransactionResponse } from '@ethersproject/providers' + +import { useVCowContract } from 'hooks/useContract' +import { useActiveWeb3React } from 'hooks/web3' +import { useSingleCallResult, Result } from 'state/multicall/hooks' +import { useTransactionAdder } from 'state/enhancedTransactions/hooks' +import { V_COW } from 'constants/tokens' +import { AppState } from 'state' +import { useAppDispatch, useAppSelector } from 'state/hooks' +import { setStatus, SwapVCowStatus } from './actions' +import { OperationType } from 'components/TransactionConfirmationModal' +import { APPROVE_GAS_LIMIT_DEFAULT } from 'hooks/useApproveCallback/useApproveCallbackMod' + +export type SetSwapVCowStatusCallback = (payload: SwapVCowStatus) => void + +type VCowData = { + isLoading: boolean + total: CurrencyAmount | undefined | null + unvested: CurrencyAmount | undefined | null + vested: CurrencyAmount | undefined | null +} + +interface SwapVCowCallbackParams { + openModal: (message: string, operationType: OperationType) => void + closeModal: () => void +} + +/** + * Hook that parses the result input with BigNumber value to CurrencyAmount + */ +function useParseVCowResult(result: Result | undefined) { + const { chainId } = useActiveWeb3React() + + const vCowToken = chainId ? V_COW[chainId] : undefined + + return useMemo(() => { + if (!chainId || !vCowToken || !result) { + return + } + + return CurrencyAmount.fromRawAmount(vCowToken, result[0].toString()) + }, [chainId, result, vCowToken]) +} + +/** + * Hook that fetches the needed vCow data and returns it in VCowData type + */ +export function useVCowData(): VCowData { + const vCowContract = useVCowContract() + const { account } = useActiveWeb3React() + + const { loading: isVestedLoading, result: vestedResult } = useSingleCallResult(vCowContract, 'swappableBalanceOf', [ + account ?? undefined, + ]) + const { loading: isTotalLoading, result: totalResult } = useSingleCallResult(vCowContract, 'balanceOf', [ + account ?? undefined, + ]) + + const vested = useParseVCowResult(vestedResult) + const total = useParseVCowResult(totalResult) + + const unvested = useMemo(() => { + if (!total || !vested) { + return null + } + + // Check if total < vested, if it is something is probably wrong and we return null + if (total.lessThan(vested)) { + return null + } + + return total.subtract(vested) + }, [total, vested]) + + const isLoading = isVestedLoading || isTotalLoading + + return { isLoading, vested, unvested, total } +} + +/** + * Hook used to swap vCow to Cow token + */ +export function useSwapVCowCallback({ openModal, closeModal }: SwapVCowCallbackParams) { + const { chainId, account } = useActiveWeb3React() + const vCowContract = useVCowContract() + + const addTransaction = useTransactionAdder() + const vCowToken = chainId ? V_COW[chainId] : undefined + + const swapCallback = useCallback(async () => { + if (!account) { + throw new Error('Not connected') + } + if (!chainId) { + throw new Error('No chainId') + } + if (!vCowContract) { + throw new Error('vCOW contract not present') + } + if (!vCowToken) { + throw new Error('vCOW token not present') + } + + const estimatedGas = await vCowContract.estimateGas.swapAll({ from: account }).catch(() => { + // general fallback for tokens who restrict approval amounts + return vCowContract.estimateGas.swapAll().catch((error) => { + console.log( + '[useSwapVCowCallback] Error estimating gas for swapAll. Using default gas limit ' + + APPROVE_GAS_LIMIT_DEFAULT.toString(), + error + ) + return APPROVE_GAS_LIMIT_DEFAULT + }) + }) + + const summary = `Convert vCOW to COW` + openModal(summary, OperationType.CONVERT_VCOW) + + return vCowContract + .swapAll({ from: account, gasLimit: estimatedGas }) + .then((tx: TransactionResponse) => { + addTransaction({ + swapVCow: true, + hash: tx.hash, + summary, + }) + }) + .finally(closeModal) + }, [account, addTransaction, chainId, closeModal, openModal, vCowContract, vCowToken]) + + return { + swapCallback, + } +} + +/** + * Hook that sets the swap vCow->Cow status + */ +export function useSetSwapVCowStatus(): SetSwapVCowStatusCallback { + const dispatch = useAppDispatch() + return useCallback((payload: SwapVCowStatus) => dispatch(setStatus(payload)), [dispatch]) +} + +/** + * Hook that gets swap vCow->Cow status + */ +export function useSwapVCowStatus() { + return useAppSelector((state: AppState) => state.swapVCow.status) +} diff --git a/src/custom/state/swapVCow/middleware.ts b/src/custom/state/swapVCow/middleware.ts new file mode 100644 index 0000000000..a2db7eeee7 --- /dev/null +++ b/src/custom/state/swapVCow/middleware.ts @@ -0,0 +1,39 @@ +import { isAnyOf, Middleware } from '@reduxjs/toolkit' +import { AppState } from 'state' +import { finalizeTransaction } from '../enhancedTransactions/actions' +import { setStatus, SwapVCowStatus } from './actions' +import { getCowSoundSuccess } from 'utils/sound' + +const isFinalizeTransaction = isAnyOf(finalizeTransaction) + +// Watch for swapVCow tx being finalized and triggers a change of status +export const swapVCowMiddleware: Middleware, AppState> = (store) => (next) => (action) => { + const result = next(action) + + let cowSound + + if (isFinalizeTransaction(action)) { + const { chainId, hash } = action.payload + const transaction = store.getState().transactions[chainId][hash] + + if (transaction.swapVCow) { + const status = transaction.receipt?.status + + console.debug( + `[stat:swapVCow:middleware] Convert vCOW to COW transaction finalized with status ${status}`, + transaction.hash + ) + + store.dispatch(setStatus(SwapVCowStatus.INITIAL)) + cowSound = getCowSoundSuccess() + } + } + + if (cowSound) { + cowSound.play().catch((e) => { + console.error('🐮 [middleware::swapVCow] Moooooo cannot be played', e) + }) + } + + return result +} diff --git a/src/custom/state/swapVCow/reducer.ts b/src/custom/state/swapVCow/reducer.ts new file mode 100644 index 0000000000..5bb4ab0f79 --- /dev/null +++ b/src/custom/state/swapVCow/reducer.ts @@ -0,0 +1,16 @@ +import { createReducer } from '@reduxjs/toolkit' +import { SwapVCowStatus, setStatus } from './actions' + +export type SwapVCowState = { + status: SwapVCowStatus +} + +export const initialState: SwapVCowState = { + status: SwapVCowStatus.INITIAL, +} + +export default createReducer(initialState, (builder) => + builder.addCase(setStatus, (state, { payload }) => { + state.status = payload + }) +)