Skip to content

Commit 93591ce

Browse files
authored
feat(app): restore kwenta redeem function (#1041)
1 parent 5781e26 commit 93591ce

File tree

11 files changed

+276
-3
lines changed

11 files changed

+276
-3
lines changed

packages/app/src/constants/routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export const ROUTES = {
2626
Stake: normalizeRoute('/dashboard', 'staking', 'tab'),
2727
Rewards: normalizeRoute('/dashboard', 'rewards', 'tab'),
2828
Migrate: normalizeRoute('/dashboard', 'migrate', 'tab'),
29+
Redeem: normalizeRoute('/dashboard', 'redeem', 'tab'),
2930
TradingRewards: formatUrl('/dashboard/staking', { tab: 'trading-rewards' }),
3031
},
3132
Exchange: {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import Head from 'next/head'
2+
import { FC, ReactNode } from 'react'
3+
import { useTranslation } from 'react-i18next'
4+
5+
import DashboardLayout from 'sections/dashboard/DashboardLayout'
6+
import RedemptionTab from 'sections/dashboard/RedemptionTab'
7+
8+
type RedemptionComponent = FC & { getLayout: (page: ReactNode) => JSX.Element }
9+
10+
const RedeemPage: RedemptionComponent = () => {
11+
const { t } = useTranslation()
12+
13+
return (
14+
<>
15+
<Head>
16+
<title>{t('dashboard-redeem.page-title')}</title>
17+
</Head>
18+
<RedemptionTab />
19+
</>
20+
)
21+
}
22+
23+
RedeemPage.getLayout = (page) => <DashboardLayout>{page}</DashboardLayout>
24+
25+
export default RedeemPage

packages/app/src/sections/dashboard/DashboardLayout.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { EXTERNAL_LINKS } from 'constants/links'
1010
import ROUTES from 'constants/routes'
1111
import AppLayout from 'sections/shared/Layout/AppLayout'
1212
import { useAppSelector } from 'state/hooks'
13-
import { selectStakingMigrationRequired } from 'state/staking/selectors'
13+
import { selectRedemptionRequired, selectStakingMigrationRequired } from 'state/staking/selectors'
1414
import { selectStartMigration } from 'state/stakingMigration/selectors'
1515
import { LeftSideContent, PageContent } from 'styles/common'
1616

@@ -23,6 +23,7 @@ enum Tab {
2323
Governance = 'governance',
2424
Stake = 'staking',
2525
Migrate = 'migrate',
26+
Redeem = 'redeem',
2627
}
2728

2829
const Tabs = Object.values(Tab)
@@ -32,6 +33,7 @@ const DashboardLayout: FC<{ children?: ReactNode }> = ({ children }) => {
3233
const router = useRouter()
3334
const stakingMigrationRequired = useAppSelector(selectStakingMigrationRequired)
3435
const startMigration = useAppSelector(selectStartMigration)
36+
const redemptionRequired = useAppSelector(selectRedemptionRequired)
3537

3638
const tabQuery = useMemo(() => {
3739
if (router.pathname) {
@@ -79,6 +81,13 @@ const DashboardLayout: FC<{ children?: ReactNode }> = ({ children }) => {
7981
href: ROUTES.Dashboard.Migrate,
8082
hidden: !stakingMigrationRequired && !startMigration,
8183
},
84+
{
85+
name: Tab.Redeem,
86+
label: t('dashboard.tabs.redeem'),
87+
active: activeTab === Tab.Redeem,
88+
href: ROUTES.Dashboard.Redeem,
89+
hidden: !redemptionRequired,
90+
},
8291
{
8392
name: Tab.Governance,
8493
label: t('dashboard.tabs.governance'),
@@ -87,7 +96,7 @@ const DashboardLayout: FC<{ children?: ReactNode }> = ({ children }) => {
8796
external: true,
8897
},
8998
],
90-
[t, activeTab, startMigration, stakingMigrationRequired]
99+
[t, activeTab, startMigration, stakingMigrationRequired, redemptionRequired]
91100
)
92101

93102
const visibleTabs = TABS.filter(({ hidden }) => !hidden)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { useTranslation } from 'react-i18next'
2+
import styled from 'styled-components'
3+
4+
import { FlexDivRowCentered } from 'components/layout/flex'
5+
import { SplitContainer } from 'components/layout/grid'
6+
import { Heading } from 'components/Text'
7+
import media from 'styles/media'
8+
9+
import RedeemInputCard from './Stake/InputCards/RedeempInputCard'
10+
11+
const RedemptionTab = () => {
12+
const { t } = useTranslation()
13+
14+
return (
15+
<Container>
16+
<TitleContainer>
17+
<StyledHeading variant="h4">{t('dashboard.stake.tabs.redeem.title')}</StyledHeading>
18+
</TitleContainer>
19+
<SplitContainer>
20+
<RedeemInputCard
21+
inputLabel={t('dashboard.stake.tabs.stake-table.vkwenta-token')}
22+
isVKwenta
23+
/>
24+
<RedeemInputCard
25+
inputLabel={t('dashboard.stake.tabs.stake-table.vekwenta-token')}
26+
isVKwenta={false}
27+
/>
28+
</SplitContainer>
29+
</Container>
30+
)
31+
}
32+
33+
const StyledHeading = styled(Heading)`
34+
font-weight: 400;
35+
`
36+
37+
const TitleContainer = styled(FlexDivRowCentered)`
38+
margin: 30px 0px;
39+
column-gap: 10%;
40+
`
41+
42+
const Container = styled.div`
43+
${media.lessThan('lg')`
44+
padding: 0px 15px;
45+
`}
46+
margin-top: 20px;
47+
`
48+
49+
export default RedemptionTab
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { truncateNumbers } from '@kwenta/sdk/utils'
2+
import { FC, useCallback, useMemo } from 'react'
3+
import { useTranslation } from 'react-i18next'
4+
import styled from 'styled-components'
5+
6+
import Button from 'components/Button'
7+
import ErrorView from 'components/ErrorView'
8+
import { FlexDivRowCentered } from 'components/layout/flex'
9+
import { StakingCard } from 'sections/dashboard/Stake/card'
10+
import { useAppDispatch, useAppSelector } from 'state/hooks'
11+
import { approveKwentaToken, redeemToken } from 'state/staking/actions'
12+
import {
13+
selectIsVeKwentaTokenApproved,
14+
selectIsVKwentaTokenApproved,
15+
selectVeKwentaBalance,
16+
selectVKwentaBalance,
17+
} from 'state/staking/selectors'
18+
import { selectDisableRedeemEscrowKwenta } from 'state/stakingMigration/selectors'
19+
import { numericValueCSS } from 'styles/common'
20+
21+
type RedeemInputCardProps = {
22+
inputLabel: string
23+
isVKwenta: boolean
24+
}
25+
26+
const RedeemInputCard: FC<RedeemInputCardProps> = ({ inputLabel, isVKwenta }) => {
27+
const { t } = useTranslation()
28+
const dispatch = useAppDispatch()
29+
30+
const vKwentaBalance = useAppSelector(selectVKwentaBalance)
31+
const veKwentaBalance = useAppSelector(selectVeKwentaBalance)
32+
const isVKwentaApproved = useAppSelector(selectIsVKwentaTokenApproved)
33+
const isVeKwentaApproved = useAppSelector(selectIsVeKwentaTokenApproved)
34+
const VeKwentaDisableRedeem = useAppSelector(selectDisableRedeemEscrowKwenta)
35+
36+
const isApproved = useMemo(
37+
() => (isVKwenta ? isVKwentaApproved : isVeKwentaApproved),
38+
[isVKwenta, isVKwentaApproved, isVeKwentaApproved]
39+
)
40+
41+
const balance = useMemo(
42+
() => (isVKwenta ? vKwentaBalance : veKwentaBalance),
43+
[isVKwenta, vKwentaBalance, veKwentaBalance]
44+
)
45+
46+
const DisableRedeem = useMemo(
47+
() => (isVKwenta ? false : VeKwentaDisableRedeem),
48+
[isVKwenta, VeKwentaDisableRedeem]
49+
)
50+
51+
const buttonLabel = useMemo(() => {
52+
return isApproved
53+
? t('dashboard.stake.tabs.stake-table.redeem')
54+
: t('dashboard.stake.tabs.stake-table.approve')
55+
}, [isApproved, t])
56+
57+
const submitRedeem = useCallback(() => {
58+
const token = isVKwenta ? 'vKwenta' : 'veKwenta'
59+
60+
if (!isApproved) {
61+
dispatch(approveKwentaToken(token))
62+
} else {
63+
dispatch(redeemToken(token))
64+
}
65+
}, [dispatch, isApproved, isVKwenta])
66+
67+
return (
68+
<>
69+
<StakingInputCardContainer>
70+
<div>
71+
<StakeInputHeader>
72+
<div>{inputLabel}</div>
73+
<StyledFlexDivRowCentered>
74+
<div>{t('dashboard.stake.tabs.stake-table.balance')}</div>
75+
<div className="max">{truncateNumbers(balance, 4)}</div>
76+
</StyledFlexDivRowCentered>
77+
</StakeInputHeader>
78+
</div>
79+
{DisableRedeem ? (
80+
<ErrorView
81+
messageType="warn"
82+
message={t('dashboard.stake.tabs.redeem.warning')}
83+
containerStyle={{
84+
margin: '0',
85+
marginTop: '25px',
86+
padding: '10px 0',
87+
}}
88+
/>
89+
) : (
90+
<Button
91+
fullWidth
92+
variant="flat"
93+
size="small"
94+
disabled={balance.eq(0) || DisableRedeem}
95+
onClick={submitRedeem}
96+
>
97+
{buttonLabel}
98+
</Button>
99+
)}
100+
</StakingInputCardContainer>
101+
</>
102+
)
103+
}
104+
105+
const StyledFlexDivRowCentered = styled(FlexDivRowCentered)`
106+
column-gap: 5px;
107+
`
108+
109+
const StakingInputCardContainer = styled(StakingCard)`
110+
min-height: 125px;
111+
max-height: 250px;
112+
display: flex;
113+
flex-direction: column;
114+
justify-content: space-between;
115+
`
116+
117+
const StakeInputHeader = styled.div`
118+
display: flex;
119+
justify-content: space-between;
120+
align-items: center;
121+
margin-bottom: 10px;
122+
color: ${(props) => props.theme.colors.selectedTheme.title};
123+
font-size: 14px;
124+
.max {
125+
color: ${(props) => props.theme.colors.selectedTheme.button.text.primary};
126+
${numericValueCSS};
127+
}
128+
`
129+
130+
export default RedeemInputCard

packages/app/src/state/staking/actions.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,12 @@ export const fetchStakingData = createAsyncThunk<StakingAction, void, ThunkConfi
5757
kwentaBalance,
5858
weekCounter,
5959
totalStakedBalance,
60+
vKwentaBalance,
61+
vKwentaAllowance,
6062
kwentaAllowance,
6163
epochPeriod,
64+
veKwentaBalance,
65+
veKwentaAllowance,
6266
} = await sdk.kwentaToken.getStakingData()
6367

6468
return {
@@ -69,8 +73,12 @@ export const fetchStakingData = createAsyncThunk<StakingAction, void, ThunkConfi
6973
kwentaBalance: kwentaBalance.toString(),
7074
weekCounter,
7175
totalStakedBalance: totalStakedBalance.toString(),
76+
vKwentaBalance: vKwentaBalance.toString(),
77+
vKwentaAllowance: vKwentaAllowance.toString(),
7278
kwentaAllowance: kwentaAllowance.toString(),
7379
epochPeriod,
80+
veKwentaBalance: veKwentaBalance.toString(),
81+
veKwentaAllowance: veKwentaAllowance.toString(),
7482
}
7583
} catch (err) {
7684
logError(err)

packages/app/src/state/staking/reducer.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@ const ZERO_VERSIONED_STAKE_DATA = {
5151

5252
const INITIAL_STAKE_INFO = {
5353
kwentaBalance: '0',
54+
vKwentaBalance: '0',
55+
veKwentaBalance: '0',
5456
kwentaAllowance: '0',
57+
vKwentaAllowance: '0',
58+
veKwentaAllowance: '0',
5559
epochPeriod: 0,
5660
weekCounter: 1,
5761
}
@@ -173,8 +177,12 @@ const stakingSlice = createSlice({
173177
state.v1.totalStakedBalance = action.payload.totalStakedBalance
174178
state.kwentaBalance = action.payload.kwentaBalance
175179
state.weekCounter = action.payload.weekCounter
180+
state.vKwentaBalance = action.payload.vKwentaBalance
181+
state.vKwentaAllowance = action.payload.vKwentaAllowance
176182
state.kwentaAllowance = action.payload.kwentaAllowance
177183
state.epochPeriod = action.payload.epochPeriod
184+
state.veKwentaBalance = action.payload.veKwentaBalance
185+
state.veKwentaAllowance = action.payload.veKwentaAllowance
178186
state.stakeStatus = FetchStatus.Idle
179187
state.unstakeStatus = FetchStatus.Idle
180188
state.stakeEscrowedStatus = FetchStatus.Idle

packages/app/src/state/staking/selectors.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,3 +501,31 @@ export const selectIsTransferring = createSelector(
501501
(app.transaction?.type === 'transfer_escrow_entries' ||
502502
app.transaction?.type === 'transfer_escrow_entry')
503503
)
504+
505+
export const selectVKwentaBalance = createSelector(
506+
(state: RootState) => state.staking.vKwentaBalance,
507+
toWei
508+
)
509+
510+
export const selectVeKwentaBalance = createSelector(
511+
(state: RootState) => state.staking.veKwentaBalance,
512+
toWei
513+
)
514+
515+
export const selectRedemptionRequired = createSelector(
516+
selectVKwentaBalance,
517+
selectVeKwentaBalance,
518+
(vKwentaBalance, veKwentaBalance) => vKwentaBalance.gt(0) || veKwentaBalance.gt(0)
519+
)
520+
521+
export const selectIsVKwentaTokenApproved = createSelector(
522+
selectVKwentaBalance,
523+
(state: RootState) => state.staking.vKwentaAllowance,
524+
(vKwentaBalance, vKwentaAllowance) => vKwentaBalance.lte(vKwentaAllowance)
525+
)
526+
527+
export const selectIsVeKwentaTokenApproved = createSelector(
528+
selectVeKwentaBalance,
529+
(state: RootState) => state.staking.veKwentaAllowance,
530+
(veKwentaBalance, veKwentaAllowance) => veKwentaBalance.lte(veKwentaAllowance)
531+
)

packages/app/src/state/staking/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@ type StakeBalance = {
1212

1313
type StakingMiscInfo = {
1414
kwentaBalance: string
15+
vKwentaBalance: string
16+
veKwentaBalance: string
1517
kwentaAllowance: string
1618
epochPeriod: number
1719
weekCounter: number
20+
vKwentaAllowance: string
21+
veKwentaAllowance: string
1822
}
1923

2024
type StakingMiscInfoV2 = {

packages/app/src/state/stakingMigration/selectors.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,9 @@ export const selectStartMigration = createSelector(
172172
selectInMigrationPeriod,
173173
(totalEscrowUnmigrated, inMigrationPeriod) => totalEscrowUnmigrated.gt(0) && inMigrationPeriod
174174
)
175+
176+
export const selectDisableRedeemEscrowKwenta = createSelector(
177+
selectIsMigrationPeriodStarted,
178+
selectInMigrationPeriod,
179+
(isMigrationPeriodStarted, inMigrationPeriod) => isMigrationPeriodStarted && !inMigrationPeriod
180+
)

0 commit comments

Comments
 (0)