Skip to content

Commit

Permalink
[Staking] feat: add stake button, help tooltip; fix withdraw status d…
Browse files Browse the repository at this point in the history
…isplay issues (#4186)

* fix: hide active in if stake validation has started

* fix: hide request withdraw status if ready to withdraw

* feat: add help tooltip for withdraw in

* feat: add stake button on assets page

fix: add area-label to stake button
  • Loading branch information
compojoom authored Sep 19, 2024
1 parent 20f0625 commit ac04fba
Show file tree
Hide file tree
Showing 13 changed files with 187 additions and 36 deletions.
2 changes: 1 addition & 1 deletion public/images/common/stake.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 9 additions & 1 deletion src/components/balances/AssetsTable/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import CheckBalance from '@/features/counterfactual/CheckBalance'
import { type ReactElement } from 'react'
import { Tooltip, Typography, SvgIcon, IconButton, Box, Checkbox, Skeleton } from '@mui/material'
import { Box, IconButton, Checkbox, Skeleton, SvgIcon, Tooltip, Typography } from '@mui/material'
import type { TokenInfo } from '@safe-global/safe-gateway-typescript-sdk'
import { TokenType } from '@safe-global/safe-gateway-typescript-sdk'
import css from './styles.module.css'
Expand All @@ -21,6 +21,9 @@ import SwapButton from '@/features/swap/components/SwapButton'
import { SWAP_LABELS } from '@/services/analytics/events/swaps'
import SendButton from './SendButton'
import useIsSwapFeatureEnabled from '@/features/swap/hooks/useIsSwapFeatureEnabled'
import useIsStakingFeatureEnabled from '@/features/stake/hooks/useIsSwapFeatureEnabled'
import { STAKE_LABELS } from '@/services/analytics/events/stake'
import StakeButton from '@/features/stake/components/StakeButton'

const skeletonCells: EnhancedTableProps['rows'][0]['cells'] = {
asset: {
Expand Down Expand Up @@ -97,6 +100,7 @@ const AssetsTable = ({
}): ReactElement => {
const { balances, loading } = useBalances()
const isSwapFeatureEnabled = useIsSwapFeatureEnabled()
const isStakingFeatureEnabled = useIsStakingFeatureEnabled()

const { isAssetSelected, toggleAsset, hidingAsset, hideAsset, cancel, deselectAll, saveChanges } = useHideAssets(() =>
setShowHiddenAssets(false),
Expand Down Expand Up @@ -130,6 +134,10 @@ const AssetsTable = ({

<Typography>{item.tokenInfo.name}</Typography>

{isStakingFeatureEnabled && item.tokenInfo.type === TokenType.NATIVE_TOKEN && (
<StakeButton tokenInfo={item.tokenInfo} trackingLabel={STAKE_LABELS.asset} />
)}

{!isNative && <TokenExplorerLink address={item.tokenInfo.address} />}
</div>
),
Expand Down
2 changes: 1 addition & 1 deletion src/components/tx/FieldsGrid/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Grid, Typography } from '@mui/material'
const minWidth = { xl: '25%', lg: '100px' }
const wrap = { flexWrap: { xl: 'nowrap' } }

const FieldsGrid = ({ title, children }: { title: string; children: ReactNode }) => {
const FieldsGrid = ({ title, children }: { title: string | ReactNode; children: ReactNode }) => {
return (
<Grid container alignItems="center" gap={1} sx={wrap}>
<Grid item minWidth={minWidth}>
Expand Down
58 changes: 58 additions & 0 deletions src/features/stake/components/StakeButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import CheckWallet from '@/components/common/CheckWallet'
import Track from '@/components/common/Track'
import { AppRoutes } from '@/config/routes'
import useSpendingLimit from '@/hooks/useSpendingLimit'
import { Button } from '@mui/material'
import type { TokenInfo } from '@safe-global/safe-gateway-typescript-sdk'
import { TokenType } from '@safe-global/safe-gateway-typescript-sdk'
import { useRouter } from 'next/router'
import type { ReactElement } from 'react'
import StakeIcon from '@/public/images/common/stake.svg'
import type { STAKE_LABELS } from '@/services/analytics/events/stake'
import { STAKE_EVENTS } from '@/services/analytics/events/stake'
import { useCurrentChain } from '@/hooks/useChains'

const StakeButton = ({
tokenInfo,
trackingLabel,
}: {
tokenInfo: TokenInfo
trackingLabel: STAKE_LABELS
}): ReactElement => {
const spendingLimit = useSpendingLimit(tokenInfo)
const chain = useCurrentChain()
const router = useRouter()

return (
<CheckWallet allowSpendingLimit={!!spendingLimit}>
{(isOk) => (
<Track {...STAKE_EVENTS.OPEN_STAKE} label={trackingLabel}>
<Button
data-testid="stake-btn"
aria-label="Stake"
variant="text"
color="info"
size="small"
startIcon={<StakeIcon />}
onClick={() => {
router.push({
pathname: AppRoutes.stake,
query: {
...router.query,
asset: `${chain?.shortName}_${
tokenInfo.type === TokenType.NATIVE_TOKEN ? 'NATIVE_TOKEN' : tokenInfo.address
}`,
},
})
}}
disabled={!isOk}
>
Stake
</Button>
</Track>
)}
</CheckWallet>
)
}

export default StakeButton
5 changes: 4 additions & 1 deletion src/features/stake/components/StakePage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ import Disclaimer from '@/components/common/Disclaimer'
import WidgetDisclaimer from '@/components/common/WidgetDisclaimer'
import useStakeConsent from '@/features/stake/useStakeConsent'
import StakingWidget from '../StakingWidget'
import { useRouter } from 'next/router'

const StakePage = () => {
const { isConsentAccepted, onAccept } = useStakeConsent()
const router = useRouter()
const { asset } = router.query

return (
<>
{isConsentAccepted === undefined ? null : isConsentAccepted ? (
<StakingWidget />
<StakingWidget asset={String(asset)} />
) : (
<Stack direction="column" alignItems="center" justifyContent="center" flex={1}>
<Disclaimer
Expand Down
12 changes: 8 additions & 4 deletions src/features/stake/components/StakingConfirmationTx/Deposit.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Typography, Stack, Box } from '@mui/material'
import { Box, Stack, Typography } from '@mui/material'
import FieldsGrid from '@/components/tx/FieldsGrid'
import type { StakingTxDepositInfo } from '@safe-global/safe-gateway-typescript-sdk'
import {
ConfirmationViewTypes,
type NativeStakingDepositConfirmationView,
NativeStakingStatus,
} from '@safe-global/safe-gateway-typescript-sdk'
import ConfirmationOrderHeader from '@/components/tx/ConfirmationOrder/ConfirmationOrderHeader'
import { formatVisualAmount, formatDurationFromSeconds } from '@/utils/formatters'
import { formatDurationFromSeconds, formatVisualAmount } from '@/utils/formatters'
import { formatCurrency } from '@/utils/formatNumber'
import StakingStatus from '@/features/stake/components/StakingStatus'

Expand Down Expand Up @@ -68,8 +69,11 @@ const StakingConfirmationTxDeposit = ({ order }: StakingOrderConfirmationViewPro
<FieldsGrid title="Validators">{order.numValidators}</FieldsGrid>
)}

<FieldsGrid title="Active in">{formatDurationFromSeconds(order.estimatedEntryTime)}</FieldsGrid>
<FieldsGrid title="Rewards">Approx. every 5 days after 4 days from activation</FieldsGrid>
{!isOrder && order.status === NativeStakingStatus.VALIDATION_STARTED ? null : (
<FieldsGrid title="Active in">{formatDurationFromSeconds(order.estimatedEntryTime)}</FieldsGrid>
)}

<FieldsGrid title="Rewards">Approx. every 5 days after activation</FieldsGrid>

{!isOrder && (
<FieldsGrid title="Status">
Expand Down
39 changes: 37 additions & 2 deletions src/features/stake/components/StakingConfirmationTx/Exit.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Typography, Stack, Alert } from '@mui/material'
import { Typography, Stack, Alert, Tooltip, SvgIcon } from '@mui/material'
import FieldsGrid from '@/components/tx/FieldsGrid'
import type { StakingTxExitInfo } from '@safe-global/safe-gateway-typescript-sdk'
import { formatDurationFromSeconds } from '@/utils/formatters'
import { type NativeStakingValidatorsExitConfirmationView } from '@safe-global/safe-gateway-typescript-sdk/dist/types/decoded-data'
import ConfirmationOrderHeader from '@/components/tx/ConfirmationOrder/ConfirmationOrderHeader'
import InfoIcon from '@/public/images/notifications/info.svg'

type StakingOrderConfirmationViewProps = {
order: NativeStakingValidatorsExitConfirmationView | StakingTxExitInfo
Expand Down Expand Up @@ -31,7 +32,41 @@ const StakingConfirmationTxExit = ({ order }: StakingOrderConfirmationViewProps)
]}
/>

<FieldsGrid title="Withdraw in">Up to {withdrawIn}</FieldsGrid>
<FieldsGrid
title={
<>
Withdraw in
<Tooltip
title={
<>
Withdrawal time is the sum of:
<ul>
<li>Time until your validator is successfully exited after the withdraw request</li>
<li>Time for a stake to receive Consensus rewards on the execution layer</li>
</ul>
</>
}
arrow
placement="top"
>
<span>
<SvgIcon
component={InfoIcon}
inheritViewBox
color="border"
fontSize="small"
sx={{
verticalAlign: 'middle',
ml: 0.5,
}}
/>
</span>
</Tooltip>
</>
}
>
Up to {withdrawIn}
</FieldsGrid>

<Typography variant="body2" color="text.secondary" mt={2}>
The selected amount and any rewards will be withdrawn from Dedicated Staking for ETH after the validator exit.
Expand Down
5 changes: 4 additions & 1 deletion src/features/stake/components/StakingTxExitDetails/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Box } from '@mui/material'
import type { StakingTxExitInfo, TransactionData } from '@safe-global/safe-gateway-typescript-sdk'
import { NativeStakingExitStatus } from '@safe-global/safe-gateway-typescript-sdk'
import FieldsGrid from '@/components/tx/FieldsGrid'
import TokenAmount from '@/components/common/TokenAmount'
import StakingStatus from '@/features/stake/components/StakingStatus'
Expand All @@ -22,7 +23,9 @@ const StakingTxExitDetails = ({ info }: { info: StakingTxExitInfo; txData?: Tran
{info.numValidators} Validator{info.numValidators > 1 ? 's' : ''}
</FieldsGrid>

<FieldsGrid title="Est. exit time">Up to {withdrawIn}</FieldsGrid>
{info.status !== NativeStakingExitStatus.READY_TO_WITHDRAW && (
<FieldsGrid title="Est. exit time">Up to {withdrawIn}</FieldsGrid>
)}

<FieldsGrid title="Exit">
<StakingStatus status={info.status} />
Expand Down
31 changes: 6 additions & 25 deletions src/features/stake/components/StakingWidget/index.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,19 @@
import { useMemo } from 'react'
import { useDarkMode } from '@/hooks/useDarkMode'
import AppFrame from '@/components/safe-apps/AppFrame'
import { getEmptySafeApp } from '@/components/safe-apps/utils'
import useChainId from '@/hooks/useChainId'
import useChains from '@/hooks/useChains'
import { useGetStakeWidgetUrl } from '@/features/stake/hooks/useGetStakeWidgetUrl'
import { widgetAppData } from '@/features/stake/constants'

const WIDGET_PRODUCTION_URL = 'https://safe.widget.kiln.fi/earn'
const WIDGET_TESTNET_URL = 'https://safe.widget.testnet.kiln.fi/earn'
const widgetAppData = {
url: WIDGET_PRODUCTION_URL,
name: 'Stake',
iconUrl: '/images/common/stake.svg',
chainIds: ['17000', '11155111', '1', '42161', '137', '56', '8453', '10'],
}

const StakingWidget = () => {
const isDarkMode = useDarkMode()
let url = widgetAppData.url
const currentChainId = useChainId()
const { configs } = useChains()
const testChains = useMemo(() => configs.filter((chain) => chain.isTestnet), [configs])

// if currentChainId is in testChains, then set the url to the testnet version
if (testChains.some((chain) => chain.chainId === currentChainId)) {
url = WIDGET_TESTNET_URL
}
const StakingWidget = ({ asset }: { asset?: string }) => {
const url = useGetStakeWidgetUrl(asset)

const appData = useMemo(
() => ({
...getEmptySafeApp(),
...widgetAppData,
url: url + `?theme=${isDarkMode ? 'dark' : 'light'}`,
url,
}),
[isDarkMode, url],
[url],
)

return (
Expand Down
10 changes: 10 additions & 0 deletions src/features/stake/constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
export const STAKE_TITLE = 'Stake'

export const WIDGET_PRODUCTION_URL = 'https://safe.widget.kiln.fi/earn'
export const WIDGET_TESTNET_URL = 'https://safe.widget.testnet.kiln.fi/earn'

export const widgetAppData = {
url: WIDGET_PRODUCTION_URL,
name: STAKE_TITLE,
iconUrl: '/images/common/stake.svg',
chainIds: ['17000', '11155111', '1', '42161', '137', '56', '8453', '10'],
}
24 changes: 24 additions & 0 deletions src/features/stake/hooks/useGetStakeWidgetUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useDarkMode } from '@/hooks/useDarkMode'
import useChainId from '@/hooks/useChainId'
import useChains from '@/hooks/useChains'
import { useMemo } from 'react'
import { WIDGET_PRODUCTION_URL, WIDGET_TESTNET_URL } from '@/features/stake/constants'

export const useGetStakeWidgetUrl = (asset?: string) => {
let url = WIDGET_PRODUCTION_URL
const isDarkMode = useDarkMode()
const currentChainId = useChainId()
const { configs } = useChains()
const testChains = useMemo(() => configs.filter((chain) => chain.isTestnet), [configs])
if (testChains.some((chain) => chain.chainId === currentChainId)) {
url = WIDGET_TESTNET_URL
}
const params = new URLSearchParams()
params.append('theme', isDarkMode ? 'dark' : 'light')

if (asset) {
params.append('asset', asset)
}

return url + '?' + params.toString()
}
11 changes: 11 additions & 0 deletions src/features/stake/hooks/useIsSwapFeatureEnabled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { GeoblockingContext } from '@/components/common/GeoblockingProvider'
import { useHasFeature } from '@/hooks/useChains'
import { FEATURES } from '@/utils/chains'
import { useContext } from 'react'

const useIsStakingFeatureEnabled = () => {
const isBlockedCountry = useContext(GeoblockingContext)
return useHasFeature(FEATURES.STAKING) && !isBlockedCountry
}

export default useIsStakingFeatureEnabled
14 changes: 14 additions & 0 deletions src/services/analytics/events/stake.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const STAKE_CATEGORY = 'stake'

export const STAKE_EVENTS = {
OPEN_STAKE: {
action: 'Open stake',
category: STAKE_CATEGORY,
},
}

export enum STAKE_LABELS {
dashboard = 'dashboard',
sidebar = 'sidebar',
asset = 'asset',
}

0 comments on commit ac04fba

Please sign in to comment.