Skip to content

Commit

Permalink
feat(app): restore kwenta redeem function (#1041)
Browse files Browse the repository at this point in the history
  • Loading branch information
LeifuChen authored Oct 16, 2023
1 parent 5781e26 commit 93591ce
Show file tree
Hide file tree
Showing 11 changed files with 276 additions and 3 deletions.
1 change: 1 addition & 0 deletions packages/app/src/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
25 changes: 25 additions & 0 deletions packages/app/src/pages/dashboard/redeem.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Head>
<title>{t('dashboard-redeem.page-title')}</title>
</Head>
<RedemptionTab />
</>
)
}

RedeemPage.getLayout = (page) => <DashboardLayout>{page}</DashboardLayout>

export default RedeemPage
13 changes: 11 additions & 2 deletions packages/app/src/sections/dashboard/DashboardLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -23,6 +23,7 @@ enum Tab {
Governance = 'governance',
Stake = 'staking',
Migrate = 'migrate',
Redeem = 'redeem',
}

const Tabs = Object.values(Tab)
Expand All @@ -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) {
Expand Down Expand Up @@ -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'),
Expand All @@ -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)
Expand Down
49 changes: 49 additions & 0 deletions packages/app/src/sections/dashboard/RedemptionTab.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Container>
<TitleContainer>
<StyledHeading variant="h4">{t('dashboard.stake.tabs.redeem.title')}</StyledHeading>
</TitleContainer>
<SplitContainer>
<RedeemInputCard
inputLabel={t('dashboard.stake.tabs.stake-table.vkwenta-token')}
isVKwenta
/>
<RedeemInputCard
inputLabel={t('dashboard.stake.tabs.stake-table.vekwenta-token')}
isVKwenta={false}
/>
</SplitContainer>
</Container>
)
}

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
Original file line number Diff line number Diff line change
@@ -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<RedeemInputCardProps> = ({ 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 (
<>
<StakingInputCardContainer>
<div>
<StakeInputHeader>
<div>{inputLabel}</div>
<StyledFlexDivRowCentered>
<div>{t('dashboard.stake.tabs.stake-table.balance')}</div>
<div className="max">{truncateNumbers(balance, 4)}</div>
</StyledFlexDivRowCentered>
</StakeInputHeader>
</div>
{DisableRedeem ? (
<ErrorView
messageType="warn"
message={t('dashboard.stake.tabs.redeem.warning')}
containerStyle={{
margin: '0',
marginTop: '25px',
padding: '10px 0',
}}
/>
) : (
<Button
fullWidth
variant="flat"
size="small"
disabled={balance.eq(0) || DisableRedeem}
onClick={submitRedeem}
>
{buttonLabel}
</Button>
)}
</StakingInputCardContainer>
</>
)
}

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
8 changes: 8 additions & 0 deletions packages/app/src/state/staking/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,12 @@ export const fetchStakingData = createAsyncThunk<StakingAction, void, ThunkConfi
kwentaBalance,
weekCounter,
totalStakedBalance,
vKwentaBalance,
vKwentaAllowance,
kwentaAllowance,
epochPeriod,
veKwentaBalance,
veKwentaAllowance,
} = await sdk.kwentaToken.getStakingData()

return {
Expand All @@ -69,8 +73,12 @@ export const fetchStakingData = createAsyncThunk<StakingAction, void, ThunkConfi
kwentaBalance: kwentaBalance.toString(),
weekCounter,
totalStakedBalance: totalStakedBalance.toString(),
vKwentaBalance: vKwentaBalance.toString(),
vKwentaAllowance: vKwentaAllowance.toString(),
kwentaAllowance: kwentaAllowance.toString(),
epochPeriod,
veKwentaBalance: veKwentaBalance.toString(),
veKwentaAllowance: veKwentaAllowance.toString(),
}
} catch (err) {
logError(err)
Expand Down
8 changes: 8 additions & 0 deletions packages/app/src/state/staking/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ const ZERO_VERSIONED_STAKE_DATA = {

const INITIAL_STAKE_INFO = {
kwentaBalance: '0',
vKwentaBalance: '0',
veKwentaBalance: '0',
kwentaAllowance: '0',
vKwentaAllowance: '0',
veKwentaAllowance: '0',
epochPeriod: 0,
weekCounter: 1,
}
Expand Down Expand Up @@ -173,8 +177,12 @@ const stakingSlice = createSlice({
state.v1.totalStakedBalance = action.payload.totalStakedBalance
state.kwentaBalance = action.payload.kwentaBalance
state.weekCounter = action.payload.weekCounter
state.vKwentaBalance = action.payload.vKwentaBalance
state.vKwentaAllowance = action.payload.vKwentaAllowance
state.kwentaAllowance = action.payload.kwentaAllowance
state.epochPeriod = action.payload.epochPeriod
state.veKwentaBalance = action.payload.veKwentaBalance
state.veKwentaAllowance = action.payload.veKwentaAllowance
state.stakeStatus = FetchStatus.Idle
state.unstakeStatus = FetchStatus.Idle
state.stakeEscrowedStatus = FetchStatus.Idle
Expand Down
28 changes: 28 additions & 0 deletions packages/app/src/state/staking/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -501,3 +501,31 @@ export const selectIsTransferring = createSelector(
(app.transaction?.type === 'transfer_escrow_entries' ||
app.transaction?.type === 'transfer_escrow_entry')
)

export const selectVKwentaBalance = createSelector(
(state: RootState) => 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)
)
4 changes: 4 additions & 0 deletions packages/app/src/state/staking/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
6 changes: 6 additions & 0 deletions packages/app/src/state/stakingMigration/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
7 changes: 6 additions & 1 deletion packages/app/src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@
"staking": "Staking & Rewards",
"earn": "Earn",
"migrate": "Migrate to V2",
"redeem": "Redeem",
"v1-staking": "V1 Staking"
},
"overview": {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -730,6 +732,9 @@
"dashboard-rewards": {
"page-title": "Rewards | Kwenta"
},
"dashboard-redeem": {
"page-title": "Redemption | Kwenta"
},
"futures": {
"page-title": "Kwenta Futures",
"cta-buttons": {
Expand Down

0 comments on commit 93591ce

Please sign in to comment.