Skip to content

Commit

Permalink
feat(app): transfer escrow staking v2 (#1020)
Browse files Browse the repository at this point in the history
* Added transferFrom and bulkTransferFrom functionality

* Updated the UI and added check for unstaked escrow entries

* Updated the copy

* Fixed the total transfer amount when the length of the entries over the maximum limit

* Fixed the typo
  • Loading branch information
LeifuChen authored Oct 6, 2023
1 parent 32af22a commit 4345e46
Show file tree
Hide file tree
Showing 10 changed files with 384 additions and 46 deletions.
127 changes: 97 additions & 30 deletions packages/app/src/sections/dashboard/Stake/EscrowTable.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ZERO_WEI } from '@kwenta/sdk/constants'
import { formatNumber, formatPercent } from '@kwenta/sdk/utils'
import { wei } from '@synthetixio/wei'
import { useCallback, useMemo, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'

Expand All @@ -16,33 +16,46 @@ import { TableCellHead, TableHeader } from 'components/Table'
import StakingPagination from 'components/Table/StakingPagination'
import { Body } from 'components/Text'
import { STAKING_DISABLED } from 'constants/ui'
import { setOpenModal } from 'state/app/reducer'
import { selectShowModal } from 'state/app/selectors'
import { useAppDispatch, useAppSelector } from 'state/hooks'
import { vestEscrowedRewards, vestEscrowedRewardsV2 } from 'state/staking/actions'
import {
selectCanVestBeforeMigration,
selectEscrowEntries,
selectStakingV1,
selectUnstakedEscrowedKwentaBalance,
} from 'state/staking/selectors'
import media from 'styles/media'
import common from 'styles/theme/colors/common'

import TransferInputModal from './TransferInputModal'
import VestConfirmationModal from './VestConfirmationModal'

const TRANSFER_BATCH_SIZE = 200

const EscrowTable = () => {
const { t } = useTranslation()
const dispatch = useAppDispatch()
const stakingV1 = useAppSelector(selectStakingV1)
const canVestBeforeMigration = useAppSelector(selectCanVestBeforeMigration)
const escrowData = useAppSelector(selectEscrowEntries)
const unstakedEscrowedKwentaBalance = useAppSelector(selectUnstakedEscrowedKwentaBalance)
const openModal = useAppSelector(selectShowModal)

const [checkedState, setCheckedState] = useState(escrowData.map((_) => false))
const [checkAllState, setCheckAllState] = useState(false)
const [isConfirmModalOpen, setConfirmModalOpen] = useState(false)

useEffect(() => {
setCheckedState(escrowData.map((_) => false))
setCheckAllState(false)
}, [escrowData])

const handleOnChange = useCallback(
(position: number) => {
checkedState[position] = !checkedState[position]
setCheckedState([...checkedState])
const newCheckedState = [...checkedState]
newCheckedState[position] = !newCheckedState[position]
setCheckedState(newCheckedState)
},
[checkedState]
)
Expand All @@ -63,7 +76,7 @@ const EscrowTable = () => {
() =>
checkedState.reduce(
(acc, current, index) => {
if (current) {
if (current && escrowData[index]) {
acc.totalVestable = acc.totalVestable.add(escrowData[index].vestable)
acc.totalFee = acc.totalFee.add(escrowData[index].fee)
}
Expand All @@ -75,12 +88,48 @@ const EscrowTable = () => {
[checkedState, escrowData]
)

const { ids, vestEnabled } = useMemo(() => {
const { totalTransferAmount } = useMemo(
() =>
checkedState.reduce(
(acc, current, index) => {
if (acc.totalCount >= TRANSFER_BATCH_SIZE) {
return acc
}

if (current && escrowData[index]) {
acc.totalTransferAmount = acc.totalTransferAmount.add(escrowData[index].amount)
acc.totalCount++
}

return acc
},
{ totalTransferAmount: ZERO_WEI, totalCount: 0 }
),
[checkedState, escrowData]
)
const { ids, vestEnabled, transferEnabled } = useMemo(() => {
const ids = escrowData.filter((_, i) => !!checkedState[i]).map((d) => d.id)
const vestEnabled = ids.length > 0 && !STAKING_DISABLED
const transferEnabled =
ids.length > 0 &&
!STAKING_DISABLED &&
!stakingV1 &&
unstakedEscrowedKwentaBalance.gte(totalTransferAmount)

return { ids, vestEnabled, transferEnabled }
}, [escrowData, stakingV1, unstakedEscrowedKwentaBalance, totalTransferAmount, checkedState])

const handleOpenVestModal = useCallback(() => {
dispatch(setOpenModal('vest_escrow_entries'))
}, [dispatch])

return { ids, vestEnabled }
}, [escrowData, checkedState])
const handleOpenTransferModal = useCallback(() => {
dispatch(setOpenModal('transfer_escrow_entries'))
}, [dispatch])

const handleDismissModal = useCallback(() => {
dispatch(setOpenModal(null))
}, [dispatch])

const handleVest = useCallback(async () => {
if (vestEnabled) {
Expand All @@ -93,15 +142,8 @@ const EscrowTable = () => {
} else {
await dispatch(vestEscrowedRewardsV2(ids))
}
setCheckedState(escrowData.map((_) => false))
setCheckAllState(false)
}

setConfirmModalOpen(false)
}, [canVestBeforeMigration, dispatch, escrowData, ids, stakingV1, vestEnabled])

const openConfirmModal = useCallback(() => setConfirmModalOpen(true), [])
const closeConfirmModal = useCallback(() => setConfirmModalOpen(false), [])
}, [canVestBeforeMigration, dispatch, ids, stakingV1, vestEnabled])

const EscrowStatsContainer = () => (
<StatsContainer columnGap="25px" justifyContent="flex-end">
Expand All @@ -117,15 +159,28 @@ const EscrowTable = () => {
</LabelContainer>
</LabelContainers>
</Container>
<StyledButton
variant="yellow"
size="xsmall"
isRounded
disabled={!vestEnabled}
onClick={openConfirmModal}
>
{t('dashboard.stake.tabs.escrow.vest')}
</StyledButton>
<ButtonContainer>
{!stakingV1 && (
<StyledButton
variant="flat"
size="xsmall"
isRounded
disabled={!transferEnabled}
onClick={handleOpenTransferModal}
>
{t('dashboard.stake.tabs.escrow.transfer')}
</StyledButton>
)}
<StyledButton
variant="yellow"
size="xsmall"
isRounded
disabled={!vestEnabled}
onClick={handleOpenVestModal}
>
{t('dashboard.stake.tabs.escrow.vest')}
</StyledButton>
</ButtonContainer>
</StatsContainer>
)

Expand Down Expand Up @@ -337,17 +392,32 @@ const EscrowTable = () => {
]}
/>
</DesktopSmallOnlyView>
{isConfirmModalOpen && (
{openModal === 'vest_escrow_entries' && (
<VestConfirmationModal
totalFee={totalFee}
onDismiss={closeConfirmModal}
onDismiss={handleDismissModal}
handleVest={handleVest}
/>
)}
{openModal === 'transfer_escrow_entries' && (
<TransferInputModal
onDismiss={handleDismissModal}
totalAmount={totalTransferAmount}
totalEntries={ids.slice(0, TRANSFER_BATCH_SIZE)}
/>
)}
</EscrowTableContainer>
)
}

const ButtonContainer = styled(FlexDivRowCentered)`
column-gap: 25px;
${media.lessThan('lg')`
width: 100%;
column-gap: 15px;
`}
`

const StyledButton = styled(Button)`
padding: 10px 20px;
${media.lessThan('lg')`
Expand Down Expand Up @@ -387,9 +457,6 @@ const LabelContainers = styled(FlexDivRow)`

const StatsContainer = styled(FlexDivRowCentered)`
${media.lessThan('lg')`
padding: 15px 15px;
`}
${media.lessThan('md')`
flex-direction: column;
row-gap: 25px;
padding: 15px 15px;
Expand Down
137 changes: 137 additions & 0 deletions packages/app/src/sections/dashboard/Stake/TransferInputModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { formatNumber } from '@kwenta/sdk/utils'
import { useChainModal, useConnectModal } from '@rainbow-me/rainbowkit'
import Wei from '@synthetixio/wei'
import { isAddress } from 'ethers/lib/utils.js'
import { ChangeEvent, FC, memo, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'

import BaseModal from 'components/BaseModal'
import Button from 'components/Button'
import Input from 'components/Input/Input'
import { FlexDivCol, FlexDivRowCentered } from 'components/layout/flex'
import Spacer from 'components/Spacer'
import { Body } from 'components/Text'
import useENS from 'hooks/useENS'
import useIsL2 from 'hooks/useIsL2'
import { useAppDispatch, useAppSelector } from 'state/hooks'
import { bulkTransferEscrowEntries, transferEscrowEntry } from 'state/staking/actions'
import { selectIsTransferring } from 'state/staking/selectors'
import { selectWallet } from 'state/wallet/selectors'
import media from 'styles/media'

type Props = {
onDismiss(): void
totalEntries: number[]
totalAmount: Wei
}

const TransferInputModal: FC<Props> = memo(({ onDismiss, totalEntries, totalAmount }) => {
const { t } = useTranslation()
const dispatch = useAppDispatch()
const { openChainModal } = useChainModal()
const { openConnectModal } = useConnectModal()
const isL2 = useIsL2()
const wallet = useAppSelector(selectWallet)
const isTransferring = useAppSelector(selectIsTransferring)

const [addressOrName, setAddressOrName] = useState<string>('')

const onChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => setAddressOrName(e.target.value),
[]
)
const { ensAddress } = useENS(addressOrName)
const recipient = ensAddress || addressOrName

const isRecipientValid = useMemo(() => isAddress(recipient), [recipient])

const handleTransfer = useCallback(() => {
if (isRecipientValid) {
if (totalEntries.length > 1) {
dispatch(bulkTransferEscrowEntries({ recipient, entries: totalEntries }))
} else if (totalEntries.length === 1) {
dispatch(transferEscrowEntry({ recipient, entry: totalEntries[0] }))
}
}
}, [isRecipientValid, totalEntries, dispatch, recipient])

return (
<StyledBaseModal
title={t('dashboard.stake.tabs.escrow.transfer-modal.title')}
isOpen
onDismiss={onDismiss}
>
<Body color="secondary">{t('dashboard.stake.tabs.escrow.transfer-modal.copy')}</Body>
<Spacer height={30} />
<Body color="secondary">
{t('dashboard.stake.tabs.escrow.transfer-modal.recipient-address')}
</Body>
<Spacer height={10} />
<StyledInput value={recipient} onChange={onChange} />
<Spacer height={30} />
<FlexDivCol rowGap="8px">
<FlexDivRowCentered>
<Body color="secondary">
{t('dashboard.stake.tabs.escrow.transfer-modal.entries-total')}
</Body>
<Body mono color="primary">
{totalEntries.length}
</Body>
</FlexDivRowCentered>
<FlexDivRowCentered>
<Body color="secondary">
{t('dashboard.stake.tabs.escrow.transfer-modal.amount-total')}
</Body>
<Body mono color="primary">
{formatNumber(totalAmount, { suggestDecimals: true })}
</Body>
</FlexDivRowCentered>
</FlexDivCol>
<Spacer height={30} />
<TransferButton
size="small"
variant="flat"
disabled={!isRecipientValid}
loading={isTransferring}
fullWidth
onClick={wallet ? (isL2 ? handleTransfer : openChainModal) : openConnectModal}
>
{wallet
? isL2
? t('dashboard.stake.tabs.escrow.transfer-modal.transfer-escrowed-kwenta')
: t('homepage.l2.cta-buttons.switch-l2')
: t('common.wallet.connect-wallet')}
</TransferButton>
</StyledBaseModal>
)
})

const StyledInput = styled(Input)`
font-size: 14px;
font-family: ${(props) => props.theme.fonts.mono};
${media.lessThan('md')`
font-size: 13px;
`}
`

const TransferButton = styled(Button)`
height: 50px;
`

const StyledBaseModal = styled(BaseModal)`
[data-reach-dialog-content] {
width: 440px;
margin-top: 300px;
}
${media.lessThan('md')`
[data-reach-dialog-content] {
width: 300px;
margin-top: 200px;
}
`}
`

export default TransferInputModal
Loading

0 comments on commit 4345e46

Please sign in to comment.