Mp 3105 build rewards center for v 2 (#389)

* MP-3105: finished rewards center

* tidy: refactor

* fix: rerolled generated types

* fix: revert types to before

* refactor: addressed feedback
This commit is contained in:
Linkie Link 2023-08-21 17:31:47 +02:00 committed by GitHub
parent 78bf3068b4
commit 160f8da6aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 286 additions and 48 deletions

View File

@ -0,0 +1,24 @@
import { getIncentivesQueryClient } from 'api/cosmwasm-client'
import { BNCoin } from 'types/classes/BNCoin'
export default async function getUnclaimedRewards(
user: string,
accountId: string,
): Promise<BNCoin[]> {
try {
const client = await getIncentivesQueryClient()
const unclaimedRewards = await client.userUnclaimedRewards({
user,
accountId,
limit: 100,
})
if (unclaimedRewards.length === 0) return []
return await Promise.all(
unclaimedRewards.map((reward) => new BNCoin({ denom: reward.denom, amount: reward.amount })),
)
} catch (ex) {
return []
}
}

View File

@ -4,6 +4,7 @@ import { isDesktop } from 'react-device-detect'
import AccountMenu from 'components/Account/AccountMenu'
import EscButton from 'components/Button/EscButton'
import DesktopNavigation from 'components/Navigation/DesktopNavigation'
import RewardsCenter from 'components/RewardsCenter'
import Settings from 'components/Settings'
import Wallet from 'components/Wallet'
import useStore from 'store'
@ -50,6 +51,7 @@ export default function DesktopHeader() {
) : (
<div className='flex gap-4'>
{address && <AccountMenu />}
<RewardsCenter />
<Wallet />
<Settings />
</div>

View File

@ -19,7 +19,7 @@ export default function Overlay(props: Props) {
<>
<div
className={classNames(
'max-w-screen absolute isolate z-50 rounded-sm shadow-overlay backdrop-blur-lg',
'max-w-screen absolute isolate z-50 rounded-base shadow-overlay backdrop-blur-lg',
props.hasBackdropIsolation ? 'bg-body' : 'gradient-popover',
'before:content-[" "] before:absolute before:inset-0 before:-z-1 before:rounded-base before:p-[1px] before:border-glas',
props.className,
@ -28,7 +28,7 @@ export default function Overlay(props: Props) {
{props.children ? props.children : props.content}
</div>
<div
className='fixed left-0 top-0 z-40 block h-full w-full hover:cursor-pointer'
className='fixed top-0 left-0 z-40 block w-full h-full hover:cursor-pointer'
onClick={onClickAway}
role='button'
/>

View File

@ -0,0 +1,135 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import AssetBalanceRow from 'components/AssetBalanceRow'
import Button from 'components/Button'
import DisplayCurrency from 'components/DisplayCurrency'
import Divider from 'components/Divider'
import { Logo } from 'components/Icons'
import Overlay from 'components/Overlay'
import Text from 'components/Text'
import { ASSETS } from 'constants/assets'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { DISPLAY_CURRENCY_KEY } from 'constants/localStore'
import useCurrentAccount from 'hooks/useCurrentAccount'
import useLocalStorage from 'hooks/useLocalStorage'
import usePrices from 'hooks/usePrices'
import useToggle from 'hooks/useToggle'
import useUnclaimedRewards from 'hooks/useUnclaimedRewards'
import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
import { byDenom } from 'utils/array'
import { defaultFee } from 'utils/constants'
import { convertToDisplayAmount, formatAmountWithSymbol } from 'utils/formatters'
const renderIncentives = (unclaimedRewards: BNCoin[]) => {
if (unclaimedRewards.length === 0)
return (
<Text className='w-full px-4 text-center' size='sm'>
You have no claimable rewards.
</Text>
)
return unclaimedRewards.map((reward, index) => {
const asset = ASSETS.find(byDenom(reward.denom))
if (!asset) return null
return (
<>
{index !== 0 && <Divider />}
<AssetBalanceRow key={reward.denom} coin={reward} asset={asset} />
</>
)
})
}
export default function RewardsCenter() {
const account = useCurrentAccount()
const accountId = account?.id
const [isConfirming, setIsConfirming] = useState(false)
const [estimatedFee, setEstimatedFee] = useState(defaultFee)
const [showRewardsCenter, setShowRewardsCenter] = useToggle()
const [displayCurrency] = useLocalStorage<string>(
DISPLAY_CURRENCY_KEY,
DEFAULT_SETTINGS.displayCurrency,
)
const claimRewards = useStore((s) => s.claimRewards)
const { data: prices } = usePrices()
const { data: unclaimedRewards } = useUnclaimedRewards()
const totalRewardsCoin = useMemo(() => {
let total = 0
unclaimedRewards.forEach((reward) => {
total = total + convertToDisplayAmount(reward, displayCurrency, prices).toNumber()
})
return new BNCoin({
denom: displayCurrency,
amount: total.toString(),
})
}, [displayCurrency, prices, unclaimedRewards])
const hasIncentives = unclaimedRewards.length > 0
const claimTx = useMemo(() => {
return claimRewards({
accountId: accountId || '',
})
}, [accountId, claimRewards])
useEffect(() => {
claimTx.estimateFee().then(setEstimatedFee)
}, [claimTx])
const handleClaim = useCallback(async () => {
if (accountId) {
setIsConfirming(true)
await claimTx.execute()
setIsConfirming(false)
}
}, [accountId, claimTx])
return (
<div className={'relative'}>
<Button
variant='solid'
color='tertiary'
leftIcon={<Logo />}
onClick={() => {
setShowRewardsCenter(!showRewardsCenter)
}}
hasFocus={showRewardsCenter}
>
<div className='relative flex items-center h-fullx'>
<DisplayCurrency coin={totalRewardsCoin} />
</div>
</Button>
<Overlay className={'mt-2 right-0'} show={showRewardsCenter} setShow={setShowRewardsCenter}>
<div className='flex w-[402px] flex-wrap'>
<Text size='lg' className='w-full px-4 py-5 rounded-t-base bg-white/10'>
Rewards Center
</Text>
<div className='w-full p-4'>
<div className='flex flex-wrap w-full gap-4 pb-4'>
{renderIncentives(unclaimedRewards)}
</div>
{hasIncentives && (
<>
<Button
variant='solid'
color='secondary'
className='w-full py-2'
showProgressIndicator={isConfirming}
text={'Claim total account rewards'}
onClick={handleClaim}
/>
<Text className='w-full py-4 text-center text-white/50' size='sm'>
Tx Fee: {formatAmountWithSymbol(estimatedFee.amount[0])}
</Text>
</>
)}
</div>
</div>
</Overlay>
</div>
)
}

View File

@ -4,7 +4,7 @@ import getAccount from 'api/accounts/getAccount'
export default function useAccounts(accountId?: string) {
return useSWR(`account${accountId}`, () => getAccount(accountId || ''), {
refreshInterval: 30000,
refreshInterval: 30_000,
revalidateOnFocus: false,
})
}

View File

@ -5,7 +5,7 @@ import getPrices from 'api/prices/getPrices'
export default function usePrices() {
return useSWR('prices', getPrices, {
fallbackData: [],
refreshInterval: 30000,
refreshInterval: 30_000,
revalidateOnFocus: false,
})
}

View File

@ -0,0 +1,22 @@
import useSWR from 'swr'
import getUnclaimedRewards from 'api/incentives/getUnclaimedRewards'
import useCurrentAccount from 'hooks/useCurrentAccount'
import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
export default function useUserUnclaimedRewards() {
const user = useStore((s) => s.address)
const account = useCurrentAccount()
return useSWR(
`userUnclaimedRewards-${account?.id}`,
() => getUnclaimedRewards(user ?? '', account?.id ?? ''),
{
fallbackData: [] as BNCoin[],
refreshInterval: 10_000,
isPaused: () => !account?.id || !user,
revalidateOnFocus: false,
},
)
}

View File

@ -11,13 +11,13 @@ import {
Action as CreditManagerAction,
ExecuteMsg as CreditManagerExecuteMsg,
} from 'types/generated/mars-credit-manager/MarsCreditManager.types'
import { getAssetByDenom } from 'utils/assets'
import { getSingleValueFromBroadcastResult } from 'utils/broadcast'
import checkAutoLendEnabled from 'utils/checkAutoLendEnabled'
import { defaultFee } from 'utils/constants'
import { formatAmountWithSymbol } from 'utils/formatters'
import checkAutoLendEnabled from 'utils/checkAutoLendEnabled'
import getTokenOutFromSwapResponse from 'utils/getTokenOutFromSwapResponse'
import { BN } from 'utils/helpers'
import { getAssetByDenom } from 'utils/assets'
function generateExecutionMessage(
sender: string | undefined = '',
@ -178,6 +178,36 @@ export default function createBroadcastSlice(
return !!response.result
},
claimRewards: (options: { accountId: string }) => {
const msg: CreditManagerExecuteMsg = {
update_credit_account: {
account_id: options.accountId,
actions: [
{
claim_rewards: {},
},
],
},
}
const messages = [
generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, []),
]
const estimateFee = () => getEstimatedFee(messages)
const execute = async () => {
const response = await get().executeMsg({
messages,
})
const successMessage = `Claimed rewards for, ${options.accountId}`
handleResponseMessages(response, successMessage)
return !!response.result
}
return { estimateFee, execute }
},
deposit: async (options: { accountId: string; coins: BNCoin[] }) => {
const msg: CreditManagerExecuteMsg = {
update_credit_account: {

View File

@ -5,27 +5,28 @@
* and run the @cosmwasm/ts-codegen generate command to regenerate this file.
*/
import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult } from '@cosmjs/cosmwasm-stargate'
import { CosmWasmClient, ExecuteResult, SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
import { StdFee } from '@cosmjs/amino'
import {
InstantiateMsg,
ExecuteMsg,
Uint128,
Addr,
OwnerUpdate,
WhitelistEntry,
QueryMsg,
ArrayOfActiveEmission,
ActiveEmission,
ConfigResponse,
ArrayOfEmissionResponse,
EmissionResponse,
Decimal,
IncentiveStateResponse,
ArrayOfIncentiveStateResponse,
Addr,
ArrayOfActiveEmission,
ArrayOfCoin,
Coin,
ArrayOfEmissionResponse,
ArrayOfIncentiveStateResponse,
ArrayOfWhitelistEntry,
Coin,
ConfigResponse,
Decimal,
EmissionResponse,
ExecuteMsg,
IncentiveStateResponse,
InstantiateMsg,
OwnerUpdate,
QueryMsg,
Uint128,
WhitelistEntry,
} from './MarsIncentives.types'
export interface MarsIncentivesReadOnlyInterface {
contractAddress: string
@ -72,11 +73,13 @@ export interface MarsIncentivesReadOnlyInterface {
startAfterTimestamp?: number
}) => Promise<ArrayOfEmissionResponse>
userUnclaimedRewards: ({
accountId,
limit,
startAfterCollateralDenom,
startAfterIncentiveDenom,
user,
}: {
accountId?: string
limit?: number
startAfterCollateralDenom?: string
startAfterIncentiveDenom?: string
@ -186,11 +189,13 @@ export class MarsIncentivesQueryClient implements MarsIncentivesReadOnlyInterfac
})
}
userUnclaimedRewards = async ({
accountId,
limit,
startAfterCollateralDenom,
startAfterIncentiveDenom,
user,
}: {
accountId?: string
limit?: number
startAfterCollateralDenom?: string
startAfterIncentiveDenom?: string
@ -198,6 +203,7 @@ export class MarsIncentivesQueryClient implements MarsIncentivesReadOnlyInterfac
}): Promise<ArrayOfCoin> => {
return this.client.queryContractSmart(this.contractAddress, {
user_unclaimed_rewards: {
account_id: accountId,
limit,
start_after_collateral_denom: startAfterCollateralDenom,
start_after_incentive_denom: startAfterIncentiveDenom,
@ -246,11 +252,13 @@ export interface MarsIncentivesInterface extends MarsIncentivesReadOnlyInterface
) => Promise<ExecuteResult>
balanceChange: (
{
accountId,
denom,
totalAmountScaledBefore,
userAddr,
userAmountScaledBefore,
}: {
accountId?: string
denom: string
totalAmountScaledBefore: Uint128
userAddr: Addr
@ -262,10 +270,12 @@ export interface MarsIncentivesInterface extends MarsIncentivesReadOnlyInterface
) => Promise<ExecuteResult>
claimRewards: (
{
accountId,
limit,
startAfterCollateralDenom,
startAfterIncentiveDenom,
}: {
accountId?: string
limit?: number
startAfterCollateralDenom?: string
startAfterIncentiveDenom?: string
@ -377,11 +387,13 @@ export class MarsIncentivesClient
}
balanceChange = async (
{
accountId,
denom,
totalAmountScaledBefore,
userAddr,
userAmountScaledBefore,
}: {
accountId?: string
denom: string
totalAmountScaledBefore: Uint128
userAddr: Addr
@ -396,6 +408,7 @@ export class MarsIncentivesClient
this.contractAddress,
{
balance_change: {
account_id: accountId,
denom,
total_amount_scaled_before: totalAmountScaledBefore,
user_addr: userAddr,
@ -409,10 +422,12 @@ export class MarsIncentivesClient
}
claimRewards = async (
{
accountId,
limit,
startAfterCollateralDenom,
startAfterIncentiveDenom,
}: {
accountId?: string
limit?: number
startAfterCollateralDenom?: string
startAfterIncentiveDenom?: string
@ -426,6 +441,7 @@ export class MarsIncentivesClient
this.contractAddress,
{
claim_rewards: {
account_id: accountId,
limit,
start_after_collateral_denom: startAfterCollateralDenom,
start_after_incentive_denom: startAfterIncentiveDenom,

View File

@ -5,30 +5,31 @@
* and run the @cosmwasm/ts-codegen generate command to regenerate this file.
*/
import { UseQueryOptions, useQuery, useMutation, UseMutationOptions } from '@tanstack/react-query'
import { useMutation, UseMutationOptions, useQuery, UseQueryOptions } from '@tanstack/react-query'
import { ExecuteResult } from '@cosmjs/cosmwasm-stargate'
import { StdFee } from '@cosmjs/amino'
import {
InstantiateMsg,
ExecuteMsg,
Uint128,
Addr,
OwnerUpdate,
WhitelistEntry,
QueryMsg,
ArrayOfActiveEmission,
ActiveEmission,
ConfigResponse,
ArrayOfEmissionResponse,
EmissionResponse,
Decimal,
IncentiveStateResponse,
ArrayOfIncentiveStateResponse,
Addr,
ArrayOfActiveEmission,
ArrayOfCoin,
Coin,
ArrayOfEmissionResponse,
ArrayOfIncentiveStateResponse,
ArrayOfWhitelistEntry,
Coin,
ConfigResponse,
Decimal,
EmissionResponse,
ExecuteMsg,
IncentiveStateResponse,
InstantiateMsg,
OwnerUpdate,
QueryMsg,
Uint128,
WhitelistEntry,
} from './MarsIncentives.types'
import { MarsIncentivesQueryClient, MarsIncentivesClient } from './MarsIncentives.client'
import { MarsIncentivesClient, MarsIncentivesQueryClient } from './MarsIncentives.client'
export const marsIncentivesQueryKeys = {
contract: [
{
@ -94,6 +95,7 @@ export function useMarsIncentivesWhitelistQuery<TData = ArrayOfWhitelistEntry>({
export interface MarsIncentivesUserUnclaimedRewardsQuery<TData>
extends MarsIncentivesReactQuery<ArrayOfCoin, TData> {
args: {
accountId?: string
limit?: number
startAfterCollateralDenom?: string
startAfterIncentiveDenom?: string
@ -110,6 +112,7 @@ export function useMarsIncentivesUserUnclaimedRewardsQuery<TData = ArrayOfCoin>(
() =>
client
? client.userUnclaimedRewards({
accountId: args.accountId,
limit: args.limit,
startAfterCollateralDenom: args.startAfterCollateralDenom,
startAfterIncentiveDenom: args.startAfterIncentiveDenom,
@ -304,6 +307,7 @@ export function useMarsIncentivesUpdateConfigMutation(
export interface MarsIncentivesClaimRewardsMutation {
client: MarsIncentivesClient
msg: {
accountId?: string
limit?: number
startAfterCollateralDenom?: string
startAfterIncentiveDenom?: string
@ -329,6 +333,7 @@ export function useMarsIncentivesClaimRewardsMutation(
export interface MarsIncentivesBalanceChangeMutation {
client: MarsIncentivesClient
msg: {
accountId?: string
denom: string
totalAmountScaledBefore: Uint128
userAddr: Addr

View File

@ -29,6 +29,7 @@ export type ExecuteMsg =
}
| {
balance_change: {
account_id?: string | null
denom: string
total_amount_scaled_before: Uint128
user_addr: Addr
@ -37,6 +38,7 @@ export type ExecuteMsg =
}
| {
claim_rewards: {
account_id?: string | null
limit?: number | null
start_after_collateral_denom?: string | null
start_after_incentive_denom?: string | null
@ -111,6 +113,7 @@ export type QueryMsg =
}
| {
user_unclaimed_rewards: {
account_id?: string | null
limit?: number | null
start_after_collateral_denom?: string | null
start_after_incentive_denom?: string | null

View File

@ -11,20 +11,13 @@ interface ExecutableTx {
}
interface BroadcastSlice {
toast: { message: string; isError?: boolean; title?: string } | null
executeMsg: (options: { messages: MsgExecuteContract[] }) => Promise<BroadcastResult>
borrow: (options: { accountId: string; coin: Coin; borrowToWallet: boolean }) => Promise<boolean>
claimRewards: (options: { accountId: string }) => ExecutableTx
createAccount: () => Promise<string | null>
deleteAccount: (options: { accountId: string; lends: BNCoin[] }) => Promise<boolean>
deposit: (options: { accountId: string; coins: BNCoin[] }) => Promise<boolean>
unlock: (options: {
accountId: string
vault: DepositedVault
amount: string
}) => Promise<boolean>
withdrawFromVaults: (options: { accountId: string; vaults: DepositedVault[] }) => Promise<boolean>
depositIntoVault: (options: { accountId: string; actions: Action[] }) => Promise<boolean>
withdraw: (options: { accountId: string; coins: BNCoin[]; borrow: BNCoin[] }) => Promise<boolean>
executeMsg: (options: { messages: MsgExecuteContract[] }) => Promise<BroadcastResult>
lend: (options: { accountId: string; coin: BNCoin; isMax?: boolean }) => Promise<boolean>
reclaim: (options: { accountId: string; coin: BNCoin; isMax?: boolean }) => Promise<boolean>
repay: (options: {
@ -39,4 +32,12 @@ interface BroadcastSlice {
denomOut: string
slippage: number
}) => ExecutableTx
toast: { message: string; isError?: boolean; title?: string } | null
unlock: (options: {
accountId: string
vault: DepositedVault
amount: string
}) => Promise<boolean>
withdrawFromVaults: (options: { accountId: string; vaults: DepositedVault[] }) => Promise<boolean>
withdraw: (options: { accountId: string; coins: BNCoin[]; borrow: BNCoin[] }) => Promise<boolean>
}