Skip to content

Commit

Permalink
progressive ui and claim rewards (#20)
Browse files Browse the repository at this point in the history
* fix: progressive ui

* feat: claim rewards

* chore: prettier

---------

Co-authored-by: swkatmask <[email protected]>
  • Loading branch information
swkatmask and swkatmask authored May 17, 2024
1 parent a100fc2 commit e8ddc4e
Show file tree
Hide file tree
Showing 18 changed files with 198 additions and 88 deletions.
2 changes: 1 addition & 1 deletion cspell.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"version": "0.1",
"language": "en",
"words": ["immer", "Merkle", "sepolia", "urlcat", "zustand", "masknet", "unstake"],
"words": ["immer", "masknet", "Merkle", "sepolia", "unstake", "urlcat", "webp", "zustand"],
"ignoreWords": [
"Lingui",
"lingui",
Expand Down
7 changes: 5 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { createRouter, RouterProvider } from '@tanstack/react-router'
import { StrictMode } from 'react'

import { ChakraBaseProvider } from '@chakra-ui/react'
import { ChakraBaseProvider, ToastProviderProps } from '@chakra-ui/react'
import { i18n } from '@lingui/core'
import { I18nProvider } from '@lingui/react'
import { QueryClientProvider } from '@tanstack/react-query'
Expand All @@ -20,11 +20,14 @@ declare module '@tanstack/react-router' {
}
}

const toastOptions = {
defaultOptions: { position: 'top-right' },
} satisfies ToastProviderProps
export function App() {
return (
<StrictMode>
<I18nProvider i18n={i18n}>
<ChakraBaseProvider theme={theme}>
<ChakraBaseProvider theme={theme} toastOptions={toastOptions}>
<QueryClientProvider client={queryClient}>
<WagmiProvider>
<RouterProvider router={router} />
Expand Down
31 changes: 31 additions & 0 deletions src/components/ProgressiveText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Text, TextProps, Skeleton } from '@chakra-ui/react'
import { forwardRef, memo, type ReactNode } from 'react'

export interface ProgressiveTextProps extends TextProps {
loading?: boolean
skeletonWidth?: string | number
skeletonHeight?: string | number
fallback?: ReactNode
component?: string
}

export const ProgressiveText = memo(
forwardRef<HTMLDivElement | HTMLParagraphElement | HTMLSpanElement, ProgressiveTextProps>(function ProgressiveText(
{ loading, skeletonWidth, skeletonHeight, children, fallback = '--', ...props },
ref,
) {
if (loading) {
return (
<Text {...props}>
<Skeleton height={skeletonHeight ?? '1.5em'} width={skeletonWidth ?? '100%'} />
</Text>
)
}

return (
<Text {...props} ref={ref}>
{children ?? fallback}
</Text>
)
}),
)
2 changes: 1 addition & 1 deletion src/components/StakeMaskStatusCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export const StakeMaskStatusCard: FC<StakeMaskStatusCardProps> = ({ ...props })
spacing={6}
>
{pool?.apr ? (
<Tooltip label={`${formatNumber(+pool.apr * 100, 18)}%`}>
<Tooltip label={`${formatNumber(+pool.apr * 100, 18)}%`} hasArrow placement="top">
<Box
h="56px"
fontSize="32px"
Expand Down
69 changes: 55 additions & 14 deletions src/components/UserStatus/RewardCard.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import { Box, Button, HStack, Stack, Text } from '@chakra-ui/react'
import { Box, Button, HStack, Spinner, Stack, useToast } from '@chakra-ui/react'
import { t } from '@lingui/macro'
import { ActionCard, ActionCardProps } from './ActionCard'

import { mainnet } from 'viem/chains'
import { useWriteContract } from 'wagmi'
import { rewardABI } from '../../abis/reward'
import { formatNumber } from '../../helpers/formatNumber'
import { useUserInfo } from '../../hooks/useUserInfo'
import { UserInfo } from '../../types/api'
import { ProgressiveText } from '../ProgressiveText'
import { TokenIcon } from '../TokenIcon'
import { ActionCard, ActionCardProps } from './ActionCard'

interface Props extends ActionCardProps {}
console.log({ mainnet })
interface Props extends ActionCardProps {
reward?: UserInfo['reward_pool'][number]
}

export function RewardCard(props: Props) {
export function RewardCard({ reward, ...props }: Props) {
const { writeContractAsync, isPending } = useWriteContract()
const { data: userInfo, isLoading: loadingUserInfo } = useUserInfo()
const toast = useToast()
return (
<ActionCard display="flex" flexDir="column" {...props}>
<Stack alignItems="center" flexGrow={1}>
Expand All @@ -17,12 +25,26 @@ export function RewardCard(props: Props) {
<TokenIcon />
</Box>
<Stack ml="10px">
<Text fontSize={24} fontWeight={700} lineHeight="24px">
70000.00
</Text>
<Text fontSize={16} fontWeight={700} lineHeight="16px">
RSS3
</Text>
<ProgressiveText
loading={!reward}
fontSize={24}
fontWeight={700}
lineHeight="24px"
skeletonHeight="24px"
skeletonWidth="50px"
>
{formatNumber(reward?.amount ? +reward.amount : 0)}
</ProgressiveText>
<ProgressiveText
fontSize={16}
loading={!reward}
fontWeight={700}
lineHeight="16px"
textTransform="uppercase"
skeletonWidth="30px"
>
{reward?.name}
</ProgressiveText>
</Stack>
</HStack>
<Button
Expand All @@ -32,8 +54,27 @@ export function RewardCard(props: Props) {
bg="gradient.purple"
_hover={{ bg: 'gradient.purple', transform: 'scale(1.01)' }}
_active={{ transform: 'scale(0.9)' }}
disabled={loadingUserInfo}
onClick={async () => {
if (!reward || !userInfo) return
const rewardPool = userInfo.reward_pool.find((x) => x.reward_pool_id === reward.reward_pool_id)
if (!rewardPool) {
toast({
status: 'error',
title: t`No match pool found.`,
})
return
}
const res = await writeContractAsync({
abi: rewardABI,
address: import.meta.env.REWARD_CONTRACT_ADDRESS,
functionName: 'claim',
args: [reward.reward_pool_id, BigInt(reward.big_amount), rewardPool.proof],
})
console.log('claim result', res)
}}
>
{t`Claim`}
{isPending ? <Spinner size="sm" /> : t`Claim`}
</Button>
</Stack>
</ActionCard>
Expand Down
39 changes: 33 additions & 6 deletions src/components/UserStatus/StakedMask.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,44 @@
import { Box, BoxProps, Button, HStack, Icon, Stack, Text } from '@chakra-ui/react'
import { ActionCard } from './ActionCard'
import { Box, BoxProps, Button, HStack, Icon, Stack } from '@chakra-ui/react'
import { t } from '@lingui/macro'
import { ActionCard } from './ActionCard'

import { useAccount, useReadContract } from 'wagmi'
import { StakeManagerABI } from '../../abis/stakeManager.ts'
import MaskLogo from '../../assets/mask-logo.svg?react'
import Question from '../../assets/question.svg?react'
import { formatNumber } from '../../helpers/formatNumber.ts'
import { useUserInfo } from '../../hooks/useUserInfo.ts'
import { ProgressiveText } from '../ProgressiveText.tsx'
import { Tooltip } from '../Tooltip.tsx'
import { stakeModal } from '../../modals/index.tsx'

export function StakedMask(props: BoxProps) {
const account = useAccount()
const { data: userInfo, isLoading: loadingUserInfo } = useUserInfo()
const { isLoading, data: chainData } = useReadContract({
abi: StakeManagerABI,
functionName: 'userInfos',
address: import.meta.env.STAKE_MANAGER_CONTRACT_ADDRESS,
args: account.address ? [account.address] : undefined,
})
return (
<ActionCard title={t`Stake Mask`} {...props}>
<Stack alignItems="center">
<Text fontSize={48} lineHeight="56px" fontWeight={700}>
2.343
</Text>
<ProgressiveText
loading={isLoading && loadingUserInfo}
fontSize={48}
lineHeight="56px"
fontWeight={700}
skeletonWidth="100px"
skeletonHeight="56px"
>
{chainData ? chainData[0].toLocaleString() : formatNumber(userInfo?.amount)}
</ProgressiveText>
<HStack alignItems="center" my="auto">
<Text>2.343</Text>
<ProgressiveText
loading={loadingUserInfo}
skeletonWidth="50px"
>{t`+${userInfo?.score_per_hour} Points/h`}</ProgressiveText>
<Tooltip label={t`1 staked MASK will generate 1 point per hour.`} placement="top" hasArrow>
<Box as="span" w={6} h={6}>
<Icon as={Question} w="initial" h="initial" />
Expand All @@ -30,6 +54,9 @@ export function StakedMask(props: BoxProps) {
_hover={{ bg: 'gradient.purple', transform: 'scale(1.01)' }}
_active={{ transform: 'scale(0.9)' }}
leftIcon={<Icon as={MaskLogo} width={6} height={6} />}
onClick={() => {
stakeModal.show()
}}
>
{t`Stake Mask`}
</Button>
Expand Down
20 changes: 9 additions & 11 deletions src/components/UserStatus/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import { Grid, GridProps, Skeleton } from '@chakra-ui/react'
import { useAccount } from 'wagmi'
import { UserTotalPoints } from './UserTotalPoint'
import { useUserInfo } from '../../hooks/useUserInfo'
import { usePoolStore } from '../../store/poolStore'
import { t } from '@lingui/macro'
import { StakedMask } from './StakedMask'
import { useUserInfo } from '../../hooks/useUserInfo'
import { RewardCard } from './RewardCard'
import { StakedMask } from './StakedMask'
import { UserTotalPoints } from './UserTotalPoint'

export interface UserStatusProps extends GridProps {}

export function UserStatus(props: UserStatusProps) {
const { address } = useAccount()
const store = usePoolStore()
const { data: userInfo } = useUserInfo(address, store.poolId)
const { data: userInfo } = useUserInfo()
const rss3 = userInfo?.reward_pool.find((x) => x.name === 'rss3')
const ton = userInfo?.reward_pool.find((x) => x.name === 'ton')

if (!userInfo)
return (
Expand Down Expand Up @@ -47,9 +45,9 @@ export function UserStatus(props: UserStatusProps) {
{...props}
>
<UserTotalPoints user={userInfo} />
<StakedMask alignSelf="stretch" />
<RewardCard alignSelf="stretch" title={t`Estimated Rewards`} />
<RewardCard alignSelf="stretch" title={t`Estimated Rewards`} />
<StakedMask />
<RewardCard title={t`Estimated Rewards`} reward={rss3} />
<RewardCard title={t`Estimated Rewards`} reward={ton} />
</Grid>
)
}
3 changes: 2 additions & 1 deletion src/helpers/formatNumber.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export function formatNumber(num: number, digits = 2) {
export function formatNumber(num: number | undefined, digits = 2) {
if (!num) return num
return num.toLocaleString('en-US', {
maximumFractionDigits: digits,
})
Expand Down
27 changes: 13 additions & 14 deletions src/hooks/useLinkTwitter.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,23 @@
import { useToast } from '@chakra-ui/react'
import { useAsyncFn } from 'react-use'
import urlcat from 'urlcat'
import { useAccount, useClient } from 'wagmi'
import { signMessage } from 'wagmi/actions'
import { config } from '../configs/wagmiClient'
import { UserRejectedRequestError } from 'viem'
import { useAccount, useSignMessage } from 'wagmi'
import { FIREFLY_API_ROOT } from '../constants/api'
import { fetchJSON } from '../helpers/fetchJSON'
import { TwitterAuthorizeResponse } from '../types/api'
import { useToast } from '@chakra-ui/react'
import { UserRejectedRequestError } from 'viem'

// Any message is ok.
const message = 'Hello, world!'
const message = 'Stake $MASK'
export function useLinkTwitter() {
const account = useAccount()
const client = useClient()
const toast = useToast()
const { signMessageAsync } = useSignMessage()

return useAsyncFn(async () => {
if (!account.address || !client) return
if (!account.address) return
try {
const signed = await signMessage(config, {
account: account.address,
message: message,
})
const signed = await signMessageAsync({ message })
const url = urlcat(FIREFLY_API_ROOT, '/v1/mask_stake/twitter/authorize', {
original_message: message,
signature_message: signed.slice(2), // omit 0x
Expand All @@ -31,19 +26,23 @@ export function useLinkTwitter() {
const res = await fetchJSON<TwitterAuthorizeResponse>(url)
if (res.code !== 200) {
console.error('Failed to get twitter authorize', res.message, res.reason)
toast({
status: 'error',
title: 'Failed to get twitter authorize',
description: res.message,
})
return
}
location.href = res.data.url
} catch (err) {
if (err instanceof UserRejectedRequestError) {
toast({
status: 'error',
position: 'top-right',
title: err.details,
})
return
}
throw err
}
}, [account.address, client])
}, [account.address, signMessageAsync])
}
13 changes: 9 additions & 4 deletions src/hooks/useUserInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@ import { fetchJSON } from '../helpers/fetchJSON'
import { FIREFLY_API_ROOT } from '../constants/api'
import { UserInfo, UserInfoResponse } from '../types/api'
import urlcat from 'urlcat'
import { useAccount } from 'wagmi'
import { usePoolStore } from '../store/poolStore'

export function useUserInfo(address: string | undefined, pool_id: number | null) {
export function useUserInfo() {
const { address } = useAccount()
const store = usePoolStore()
const poolId = store.poolId
return useQuery({
enabled: !!pool_id && !!address,
queryKey: ['user-info', address, pool_id],
enabled: !!poolId && !!address,
queryKey: ['user-info', address, poolId],
queryFn: async () => {
const url = urlcat(FIREFLY_API_ROOT, '/v1/mask_stake/user_info', {
address,
pool_id: pool_id,
pool_id: poolId,
})
return fetchJSON<UserInfoResponse>(url)
},
Expand Down
Loading

0 comments on commit e8ddc4e

Please sign in to comment.