diff --git a/src/api/incentives/getUnclaimedRewards.ts b/src/api/incentives/getUnclaimedRewards.ts new file mode 100644 index 00000000..d7aa6be6 --- /dev/null +++ b/src/api/incentives/getUnclaimedRewards.ts @@ -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 { + 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 [] + } +} diff --git a/src/components/Header/DesktopHeader.tsx b/src/components/Header/DesktopHeader.tsx index b1ec918a..0c329c15 100644 --- a/src/components/Header/DesktopHeader.tsx +++ b/src/components/Header/DesktopHeader.tsx @@ -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() { ) : (
{address && } +
diff --git a/src/components/Overlay.tsx b/src/components/Overlay.tsx index d7e1b8a5..f682b51d 100644 --- a/src/components/Overlay.tsx +++ b/src/components/Overlay.tsx @@ -19,7 +19,7 @@ export default function Overlay(props: Props) { <>
diff --git a/src/components/RewardsCenter.tsx b/src/components/RewardsCenter.tsx new file mode 100644 index 00000000..15d44c43 --- /dev/null +++ b/src/components/RewardsCenter.tsx @@ -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 ( + + You have no claimable rewards. + + ) + + return unclaimedRewards.map((reward, index) => { + const asset = ASSETS.find(byDenom(reward.denom)) + if (!asset) return null + return ( + <> + {index !== 0 && } + + + ) + }) +} + +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( + 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 ( +
+ + +
+ + Rewards Center + +
+
+ {renderIncentives(unclaimedRewards)} +
+ {hasIncentives && ( + <> +
+
+
+
+ ) +} diff --git a/src/hooks/useAccount.tsx b/src/hooks/useAccount.tsx index 7c6e940f..8b79d6c2 100644 --- a/src/hooks/useAccount.tsx +++ b/src/hooks/useAccount.tsx @@ -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, }) } diff --git a/src/hooks/usePrices.tsx b/src/hooks/usePrices.tsx index b1298be4..e816e53a 100644 --- a/src/hooks/usePrices.tsx +++ b/src/hooks/usePrices.tsx @@ -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, }) } diff --git a/src/hooks/useUnclaimedRewards.tsx b/src/hooks/useUnclaimedRewards.tsx new file mode 100644 index 00000000..ed3642df --- /dev/null +++ b/src/hooks/useUnclaimedRewards.tsx @@ -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, + }, + ) +} diff --git a/src/store/slices/broadcast.ts b/src/store/slices/broadcast.ts index 150727a9..3ddee1c6 100644 --- a/src/store/slices/broadcast.ts +++ b/src/store/slices/broadcast.ts @@ -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: { diff --git a/src/types/generated/mars-incentives/MarsIncentives.client.ts b/src/types/generated/mars-incentives/MarsIncentives.client.ts index 566f4bbc..9ac604e3 100644 --- a/src/types/generated/mars-incentives/MarsIncentives.client.ts +++ b/src/types/generated/mars-incentives/MarsIncentives.client.ts @@ -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 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 => { 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 balanceChange: ( { + accountId, denom, totalAmountScaledBefore, userAddr, userAmountScaledBefore, }: { + accountId?: string denom: string totalAmountScaledBefore: Uint128 userAddr: Addr @@ -262,10 +270,12 @@ export interface MarsIncentivesInterface extends MarsIncentivesReadOnlyInterface ) => Promise 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, diff --git a/src/types/generated/mars-incentives/MarsIncentives.react-query.ts b/src/types/generated/mars-incentives/MarsIncentives.react-query.ts index 9386101c..5dfad90f 100644 --- a/src/types/generated/mars-incentives/MarsIncentives.react-query.ts +++ b/src/types/generated/mars-incentives/MarsIncentives.react-query.ts @@ -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({ export interface MarsIncentivesUserUnclaimedRewardsQuery extends MarsIncentivesReactQuery { args: { + accountId?: string limit?: number startAfterCollateralDenom?: string startAfterIncentiveDenom?: string @@ -110,6 +112,7 @@ export function useMarsIncentivesUserUnclaimedRewardsQuery( () => 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 diff --git a/src/types/generated/mars-incentives/MarsIncentives.types.ts b/src/types/generated/mars-incentives/MarsIncentives.types.ts index f3b2ca4d..1098c5bb 100644 --- a/src/types/generated/mars-incentives/MarsIncentives.types.ts +++ b/src/types/generated/mars-incentives/MarsIncentives.types.ts @@ -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 diff --git a/src/types/interfaces/store/broadcast.d.ts b/src/types/interfaces/store/broadcast.d.ts index 549d8b74..320561ee 100644 --- a/src/types/interfaces/store/broadcast.d.ts +++ b/src/types/interfaces/store/broadcast.d.ts @@ -11,20 +11,13 @@ interface ExecutableTx { } interface BroadcastSlice { - toast: { message: string; isError?: boolean; title?: string } | null - executeMsg: (options: { messages: MsgExecuteContract[] }) => Promise borrow: (options: { accountId: string; coin: Coin; borrowToWallet: boolean }) => Promise + claimRewards: (options: { accountId: string }) => ExecutableTx createAccount: () => Promise deleteAccount: (options: { accountId: string; lends: BNCoin[] }) => Promise deposit: (options: { accountId: string; coins: BNCoin[] }) => Promise - unlock: (options: { - accountId: string - vault: DepositedVault - amount: string - }) => Promise - withdrawFromVaults: (options: { accountId: string; vaults: DepositedVault[] }) => Promise depositIntoVault: (options: { accountId: string; actions: Action[] }) => Promise - withdraw: (options: { accountId: string; coins: BNCoin[]; borrow: BNCoin[] }) => Promise + executeMsg: (options: { messages: MsgExecuteContract[] }) => Promise lend: (options: { accountId: string; coin: BNCoin; isMax?: boolean }) => Promise reclaim: (options: { accountId: string; coin: BNCoin; isMax?: boolean }) => Promise 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 + withdrawFromVaults: (options: { accountId: string; vaults: DepositedVault[] }) => Promise + withdraw: (options: { accountId: string; coins: BNCoin[]; borrow: BNCoin[] }) => Promise }