From 93591ce3322125caee09323896a3f3316e52595b Mon Sep 17 00:00:00 2001 From: leifu Date: Mon, 16 Oct 2023 22:32:37 +0300 Subject: [PATCH] feat(app): restore kwenta redeem function (#1041) --- packages/app/src/constants/routes.ts | 1 + packages/app/src/pages/dashboard/redeem.tsx | 25 ++++ .../sections/dashboard/DashboardLayout.tsx | 13 +- .../src/sections/dashboard/RedemptionTab.tsx | 49 +++++++ .../Stake/InputCards/RedeempInputCard.tsx | 130 ++++++++++++++++++ packages/app/src/state/staking/actions.ts | 8 ++ packages/app/src/state/staking/reducer.ts | 8 ++ packages/app/src/state/staking/selectors.ts | 28 ++++ packages/app/src/state/staking/types.ts | 4 + .../src/state/stakingMigration/selectors.ts | 6 + packages/app/src/translations/en.json | 7 +- 11 files changed, 276 insertions(+), 3 deletions(-) create mode 100644 packages/app/src/pages/dashboard/redeem.tsx create mode 100644 packages/app/src/sections/dashboard/RedemptionTab.tsx create mode 100644 packages/app/src/sections/dashboard/Stake/InputCards/RedeempInputCard.tsx diff --git a/packages/app/src/constants/routes.ts b/packages/app/src/constants/routes.ts index 924930898..bbd0b61dd 100644 --- a/packages/app/src/constants/routes.ts +++ b/packages/app/src/constants/routes.ts @@ -26,6 +26,7 @@ export const ROUTES = { Stake: normalizeRoute('/dashboard', 'staking', 'tab'), Rewards: normalizeRoute('/dashboard', 'rewards', 'tab'), Migrate: normalizeRoute('/dashboard', 'migrate', 'tab'), + Redeem: normalizeRoute('/dashboard', 'redeem', 'tab'), TradingRewards: formatUrl('/dashboard/staking', { tab: 'trading-rewards' }), }, Exchange: { diff --git a/packages/app/src/pages/dashboard/redeem.tsx b/packages/app/src/pages/dashboard/redeem.tsx new file mode 100644 index 000000000..d4574e4a5 --- /dev/null +++ b/packages/app/src/pages/dashboard/redeem.tsx @@ -0,0 +1,25 @@ +import Head from 'next/head' +import { FC, ReactNode } from 'react' +import { useTranslation } from 'react-i18next' + +import DashboardLayout from 'sections/dashboard/DashboardLayout' +import RedemptionTab from 'sections/dashboard/RedemptionTab' + +type RedemptionComponent = FC & { getLayout: (page: ReactNode) => JSX.Element } + +const RedeemPage: RedemptionComponent = () => { + const { t } = useTranslation() + + return ( + <> + + {t('dashboard-redeem.page-title')} + + + + ) +} + +RedeemPage.getLayout = (page) => {page} + +export default RedeemPage diff --git a/packages/app/src/sections/dashboard/DashboardLayout.tsx b/packages/app/src/sections/dashboard/DashboardLayout.tsx index 21340cf5c..55bfedffb 100644 --- a/packages/app/src/sections/dashboard/DashboardLayout.tsx +++ b/packages/app/src/sections/dashboard/DashboardLayout.tsx @@ -10,7 +10,7 @@ import { EXTERNAL_LINKS } from 'constants/links' import ROUTES from 'constants/routes' import AppLayout from 'sections/shared/Layout/AppLayout' import { useAppSelector } from 'state/hooks' -import { selectStakingMigrationRequired } from 'state/staking/selectors' +import { selectRedemptionRequired, selectStakingMigrationRequired } from 'state/staking/selectors' import { selectStartMigration } from 'state/stakingMigration/selectors' import { LeftSideContent, PageContent } from 'styles/common' @@ -23,6 +23,7 @@ enum Tab { Governance = 'governance', Stake = 'staking', Migrate = 'migrate', + Redeem = 'redeem', } const Tabs = Object.values(Tab) @@ -32,6 +33,7 @@ const DashboardLayout: FC<{ children?: ReactNode }> = ({ children }) => { const router = useRouter() const stakingMigrationRequired = useAppSelector(selectStakingMigrationRequired) const startMigration = useAppSelector(selectStartMigration) + const redemptionRequired = useAppSelector(selectRedemptionRequired) const tabQuery = useMemo(() => { if (router.pathname) { @@ -79,6 +81,13 @@ const DashboardLayout: FC<{ children?: ReactNode }> = ({ children }) => { href: ROUTES.Dashboard.Migrate, hidden: !stakingMigrationRequired && !startMigration, }, + { + name: Tab.Redeem, + label: t('dashboard.tabs.redeem'), + active: activeTab === Tab.Redeem, + href: ROUTES.Dashboard.Redeem, + hidden: !redemptionRequired, + }, { name: Tab.Governance, label: t('dashboard.tabs.governance'), @@ -87,7 +96,7 @@ const DashboardLayout: FC<{ children?: ReactNode }> = ({ children }) => { external: true, }, ], - [t, activeTab, startMigration, stakingMigrationRequired] + [t, activeTab, startMigration, stakingMigrationRequired, redemptionRequired] ) const visibleTabs = TABS.filter(({ hidden }) => !hidden) diff --git a/packages/app/src/sections/dashboard/RedemptionTab.tsx b/packages/app/src/sections/dashboard/RedemptionTab.tsx new file mode 100644 index 000000000..4ab4a1253 --- /dev/null +++ b/packages/app/src/sections/dashboard/RedemptionTab.tsx @@ -0,0 +1,49 @@ +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import { FlexDivRowCentered } from 'components/layout/flex' +import { SplitContainer } from 'components/layout/grid' +import { Heading } from 'components/Text' +import media from 'styles/media' + +import RedeemInputCard from './Stake/InputCards/RedeempInputCard' + +const RedemptionTab = () => { + const { t } = useTranslation() + + return ( + + + {t('dashboard.stake.tabs.redeem.title')} + + + + + + + ) +} + +const StyledHeading = styled(Heading)` + font-weight: 400; +` + +const TitleContainer = styled(FlexDivRowCentered)` + margin: 30px 0px; + column-gap: 10%; +` + +const Container = styled.div` + ${media.lessThan('lg')` + padding: 0px 15px; + `} + margin-top: 20px; +` + +export default RedemptionTab diff --git a/packages/app/src/sections/dashboard/Stake/InputCards/RedeempInputCard.tsx b/packages/app/src/sections/dashboard/Stake/InputCards/RedeempInputCard.tsx new file mode 100644 index 000000000..24c4b8408 --- /dev/null +++ b/packages/app/src/sections/dashboard/Stake/InputCards/RedeempInputCard.tsx @@ -0,0 +1,130 @@ +import { truncateNumbers } from '@kwenta/sdk/utils' +import { FC, useCallback, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import Button from 'components/Button' +import ErrorView from 'components/ErrorView' +import { FlexDivRowCentered } from 'components/layout/flex' +import { StakingCard } from 'sections/dashboard/Stake/card' +import { useAppDispatch, useAppSelector } from 'state/hooks' +import { approveKwentaToken, redeemToken } from 'state/staking/actions' +import { + selectIsVeKwentaTokenApproved, + selectIsVKwentaTokenApproved, + selectVeKwentaBalance, + selectVKwentaBalance, +} from 'state/staking/selectors' +import { selectDisableRedeemEscrowKwenta } from 'state/stakingMigration/selectors' +import { numericValueCSS } from 'styles/common' + +type RedeemInputCardProps = { + inputLabel: string + isVKwenta: boolean +} + +const RedeemInputCard: FC = ({ inputLabel, isVKwenta }) => { + const { t } = useTranslation() + const dispatch = useAppDispatch() + + const vKwentaBalance = useAppSelector(selectVKwentaBalance) + const veKwentaBalance = useAppSelector(selectVeKwentaBalance) + const isVKwentaApproved = useAppSelector(selectIsVKwentaTokenApproved) + const isVeKwentaApproved = useAppSelector(selectIsVeKwentaTokenApproved) + const VeKwentaDisableRedeem = useAppSelector(selectDisableRedeemEscrowKwenta) + + const isApproved = useMemo( + () => (isVKwenta ? isVKwentaApproved : isVeKwentaApproved), + [isVKwenta, isVKwentaApproved, isVeKwentaApproved] + ) + + const balance = useMemo( + () => (isVKwenta ? vKwentaBalance : veKwentaBalance), + [isVKwenta, vKwentaBalance, veKwentaBalance] + ) + + const DisableRedeem = useMemo( + () => (isVKwenta ? false : VeKwentaDisableRedeem), + [isVKwenta, VeKwentaDisableRedeem] + ) + + const buttonLabel = useMemo(() => { + return isApproved + ? t('dashboard.stake.tabs.stake-table.redeem') + : t('dashboard.stake.tabs.stake-table.approve') + }, [isApproved, t]) + + const submitRedeem = useCallback(() => { + const token = isVKwenta ? 'vKwenta' : 'veKwenta' + + if (!isApproved) { + dispatch(approveKwentaToken(token)) + } else { + dispatch(redeemToken(token)) + } + }, [dispatch, isApproved, isVKwenta]) + + return ( + <> + +
+ +
{inputLabel}
+ +
{t('dashboard.stake.tabs.stake-table.balance')}
+
{truncateNumbers(balance, 4)}
+
+
+
+ {DisableRedeem ? ( + + ) : ( + + )} +
+ + ) +} + +const StyledFlexDivRowCentered = styled(FlexDivRowCentered)` + column-gap: 5px; +` + +const StakingInputCardContainer = styled(StakingCard)` + min-height: 125px; + max-height: 250px; + display: flex; + flex-direction: column; + justify-content: space-between; +` + +const StakeInputHeader = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; + color: ${(props) => props.theme.colors.selectedTheme.title}; + font-size: 14px; + .max { + color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; + ${numericValueCSS}; + } +` + +export default RedeemInputCard diff --git a/packages/app/src/state/staking/actions.ts b/packages/app/src/state/staking/actions.ts index f1ccc2b88..1b7d04b73 100644 --- a/packages/app/src/state/staking/actions.ts +++ b/packages/app/src/state/staking/actions.ts @@ -57,8 +57,12 @@ export const fetchStakingData = createAsyncThunk state.staking.vKwentaBalance, + toWei +) + +export const selectVeKwentaBalance = createSelector( + (state: RootState) => state.staking.veKwentaBalance, + toWei +) + +export const selectRedemptionRequired = createSelector( + selectVKwentaBalance, + selectVeKwentaBalance, + (vKwentaBalance, veKwentaBalance) => vKwentaBalance.gt(0) || veKwentaBalance.gt(0) +) + +export const selectIsVKwentaTokenApproved = createSelector( + selectVKwentaBalance, + (state: RootState) => state.staking.vKwentaAllowance, + (vKwentaBalance, vKwentaAllowance) => vKwentaBalance.lte(vKwentaAllowance) +) + +export const selectIsVeKwentaTokenApproved = createSelector( + selectVeKwentaBalance, + (state: RootState) => state.staking.veKwentaAllowance, + (veKwentaBalance, veKwentaAllowance) => veKwentaBalance.lte(veKwentaAllowance) +) diff --git a/packages/app/src/state/staking/types.ts b/packages/app/src/state/staking/types.ts index 2fd56adff..f7c77019b 100644 --- a/packages/app/src/state/staking/types.ts +++ b/packages/app/src/state/staking/types.ts @@ -12,9 +12,13 @@ type StakeBalance = { type StakingMiscInfo = { kwentaBalance: string + vKwentaBalance: string + veKwentaBalance: string kwentaAllowance: string epochPeriod: number weekCounter: number + vKwentaAllowance: string + veKwentaAllowance: string } type StakingMiscInfoV2 = { diff --git a/packages/app/src/state/stakingMigration/selectors.ts b/packages/app/src/state/stakingMigration/selectors.ts index b399213f7..1f52873db 100644 --- a/packages/app/src/state/stakingMigration/selectors.ts +++ b/packages/app/src/state/stakingMigration/selectors.ts @@ -172,3 +172,9 @@ export const selectStartMigration = createSelector( selectInMigrationPeriod, (totalEscrowUnmigrated, inMigrationPeriod) => totalEscrowUnmigrated.gt(0) && inMigrationPeriod ) + +export const selectDisableRedeemEscrowKwenta = createSelector( + selectIsMigrationPeriodStarted, + selectInMigrationPeriod, + (isMigrationPeriodStarted, inMigrationPeriod) => isMigrationPeriodStarted && !inMigrationPeriod +) diff --git a/packages/app/src/translations/en.json b/packages/app/src/translations/en.json index 28cce5720..80854ee7f 100644 --- a/packages/app/src/translations/en.json +++ b/packages/app/src/translations/en.json @@ -365,6 +365,7 @@ "staking": "Staking & Rewards", "earn": "Earn", "migrate": "Migrate to V2", + "redeem": "Redeem", "v1-staking": "V1 Staking" }, "overview": { @@ -659,7 +660,8 @@ "reclaimable": "Reclaimable" }, "redeem": { - "title": "Redeem" + "title": "Redeem", + "warning": "You migration window is closed. Please use an unmigrated wallet to redeem your escrowed $KWENTA." }, "stake-table": { "stake": "Stake", @@ -730,6 +732,9 @@ "dashboard-rewards": { "page-title": "Rewards | Kwenta" }, + "dashboard-redeem": { + "page-title": "Redemption | Kwenta" + }, "futures": { "page-title": "Kwenta Futures", "cta-buttons": {