From ca1f2d98410be330f8848c690556ec4ef6490a40 Mon Sep 17 00:00:00 2001 From: Victor Creed Date: Thu, 31 Aug 2023 15:49:50 +0300 Subject: [PATCH] feat: claim all rewards with single tx --- .changeset/friendly-laws-type.md | 5 + .changeset/nice-crabs-jam.md | 5 + .../components/Staking/Staking.tsx | 71 +++---- .../WithdrawAllFees/WithdrawAllFees.tsx | 182 ++++++++++++++++++ .../components/WithdrawFee/WithdrawFee.tsx | 1 + apps/frontend/src/constants/gasLimits.ts | 1 + .../frontend/src/locales/en/translations.json | 3 +- .../ui/src/2_molecules/Table/Table.types.ts | 1 + .../components/TableDesktop/TableDesktop.tsx | 75 ++++---- 9 files changed, 273 insertions(+), 71 deletions(-) create mode 100644 .changeset/friendly-laws-type.md create mode 100644 .changeset/nice-crabs-jam.md create mode 100644 apps/frontend/src/app/5_pages/RewardsPage/components/Staking/components/WithdrawAllFees/WithdrawAllFees.tsx diff --git a/.changeset/friendly-laws-type.md b/.changeset/friendly-laws-type.md new file mode 100644 index 0000000000..ffbaf6aa2c --- /dev/null +++ b/.changeset/friendly-laws-type.md @@ -0,0 +1,5 @@ +--- +'@sovryn/ui': patch +--- + +feat: allow to hide table header row diff --git a/.changeset/nice-crabs-jam.md b/.changeset/nice-crabs-jam.md new file mode 100644 index 0000000000..e1bf6fbac5 --- /dev/null +++ b/.changeset/nice-crabs-jam.md @@ -0,0 +1,5 @@ +--- +'frontend': patch +--- + +feat: claim all rewards with single tx diff --git a/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/Staking.tsx b/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/Staking.tsx index 7c39460439..863fb214db 100644 --- a/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/Staking.tsx +++ b/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/Staking.tsx @@ -15,8 +15,7 @@ import { decimalic } from '../../../../../utils/math'; import { useGetFeesEarned } from '../../hooks/useGetFeesEarned'; import { useGetLiquidSovClaimAmount } from '../../hooks/useGetLiquidSovClaimAmount'; import { columns } from './Staking.constants'; -import { getStakingRevenueType } from './Staking.utils'; -import { WithdrawFee } from './components/WithdrawFee/WithdrawFee'; +import { WithdrawAllFees } from './components/WithdrawAllFees/WithdrawAllFees'; import { WithdrawLiquidFee } from './components/WithdrawLiquidFee/WithdrawLiquidFee'; export const Staking: FC = () => { @@ -29,29 +28,34 @@ export const Staking: FC = () => { refetch: refetchLiquidSovClaim, } = useGetLiquidSovClaimAmount(); - const rows = useMemo(() => { - const noRewards = - !earnedFees.some(earnedFee => decimalic(earnedFee.value).gt(0)) && - !decimalic(liquidSovClaimAmount).gt(0); - - if (!account || loading || noRewards) { - return []; - } + const noRewards = useMemo( + () => + (!earnedFees.some(earnedFee => decimalic(earnedFee.value).gt(0)) && + !decimalic(liquidSovClaimAmount).gt(0)) || + !account, + [account, earnedFees, liquidSovClaimAmount], + ); - return [ - ...earnedFees.map(earnedFee => ({ - type: getStakingRevenueType(earnedFee.token), + const rows1 = useMemo( + () => [ + { + type: t(translations.rewardPage.staking.stakingRevenue), amount: ( - +
+ {earnedFees.map(fee => ( + + ))} +
), - action: , - key: `${earnedFee.token}-fee`, - })), + action: , + key: `all-fee`, + }, { type: t(translations.rewardPage.staking.stakingSubsidies), amount: ( @@ -71,23 +75,22 @@ export const Staking: FC = () => { ), key: `${SupportedTokens.sov}-liquid-fee`, }, - ]; - }, [ - account, - earnedFees, - lastWithdrawalInterval, - liquidSovClaimAmount, - loading, - refetch, - refetchLiquidSovClaim, - ]); + ], + [ + earnedFees, + lastWithdrawalInterval, + liquidSovClaimAmount, + refetch, + refetchLiquidSovClaim, + ], + ); return ( -
+
row.key} noData={ diff --git a/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/components/WithdrawAllFees/WithdrawAllFees.tsx b/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/components/WithdrawAllFees/WithdrawAllFees.tsx new file mode 100644 index 0000000000..ef62b76a25 --- /dev/null +++ b/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/components/WithdrawAllFees/WithdrawAllFees.tsx @@ -0,0 +1,182 @@ +import React, { FC, useCallback, useMemo } from 'react'; + +import { Contract } from 'ethers'; +import { t } from 'i18next'; + +import { SupportedTokens, getProtocolContract } from '@sovryn/contracts'; +import { getProvider } from '@sovryn/ethers-provider'; +import { Button, ButtonType, ButtonStyle } from '@sovryn/ui'; + +import { defaultChainId } from '../../../../../../../config/chains'; + +import { + Transaction, + TransactionType, +} from '../../../../../../3_organisms/TransactionStepDialog/TransactionStepDialog.types'; +import { GAS_LIMIT } from '../../../../../../../constants/gasLimits'; +import { useTransactionContext } from '../../../../../../../contexts/TransactionContext'; +import { useAccount } from '../../../../../../../hooks/useAccount'; +import { useGetProtocolContract } from '../../../../../../../hooks/useGetContract'; +import { useMaintenance } from '../../../../../../../hooks/useMaintenance'; +import { translations } from '../../../../../../../locales/i18n'; +import { decimalic } from '../../../../../../../utils/math'; +import { EarnedFee } from '../../../../RewardsPage.types'; + +type WithdrawFeeProps = { + fees: EarnedFee[]; + refetch: () => void; +}; + +const MAX_CHECKPOINTS = 10; +const MAX_NEXT_POSITIVE_CHECKPOINT = 75; + +export const WithdrawAllFees: FC = ({ fees, refetch }) => { + const { account } = useAccount(); + const { setTransactions, setIsOpen, setTitle } = useTransactionContext(); + + const { checkMaintenance, States } = useMaintenance(); + const claimFeesEarnedLocked = checkMaintenance(States.CLAIM_FEES_EARNED); + const rewardsLocked = checkMaintenance(States.REWARDS_FULL); + + const feeSharing = useGetProtocolContract('feeSharing'); + + const isClaimDisabled = useMemo( + () => + claimFeesEarnedLocked || + rewardsLocked || + fees.every(({ value }) => decimalic(value).lte(0)), + [claimFeesEarnedLocked, fees, rewardsLocked], + ); + + const onComplete = useCallback(() => { + refetch(); + }, [refetch]); + + const onSubmit = useCallback(async () => { + if (!feeSharing) { + return; + } + + const claimable = fees.filter(fee => decimalic(fee.value).gt(0)); + + // TODO: it might be not needed to fetch checkpoints when SC is updated. + // START: Fetch checkpoints + const checkpoints = await Promise.all( + claimable.map(fee => + getNextPositiveCheckpoint(account, fee).then(result => ({ + ...fee, + startFrom: result.checkpointNum, + hasSkippedCheckpoints: result.hasSkippedCheckpoints, + hasFees: result.hasFees, + })), + ), + ).then(result => result.filter(fee => fee.hasSkippedCheckpoints)); + + console.log({ checkpoints }); + + if (checkpoints.length === 0) { + // todo: show error message about impossibility to withdraw + console.warn('No checkpoints to withdraw'); + return; + } + + // END: Fetch checkpoints + + const transactions: Transaction[] = []; + const title = t(translations.rewardPage.stabilityPool.tx.withdrawGains); + const txTitle = t(translations.rewardPage.stabilityPool.tx.withdraw); + + transactions.push({ + title, + request: { + type: TransactionType.signTransaction, + contract: feeSharing, + fnName: 'withdrawStartingFromCheckpoints', + args: [ + claimable.map(({ contractAddress }) => contractAddress), + claimable.map(({ startFrom }) => startFrom), + MAX_CHECKPOINTS, + account, + ], + gasLimit: GAS_LIMIT.REWARDS_CLAIM, + }, + onComplete, + }); + + setTransactions(transactions); + setTitle(txTitle); + setIsOpen(true); + }, [ + account, + feeSharing, + fees, + onComplete, + setIsOpen, + setTitle, + setTransactions, + ]); + + return ( + - - {columns.map(column => ( - + + {columns.map(column => ( + - ))} - - + {isValidElement(column.filter) && column.filter} + + + ))} + + + )} {rows && rows.length >= 1 &&
- - onHeaderClick(column)} - className={classNames(styles.title, { - [styles.sortable]: column.sortable, - })} - > - <> - {column.title || column.id} - {column.sortable && ( - - )} - - + {hideHeader === false && ( +
+ + onHeaderClick(column)} + className={classNames(styles.title, { + [styles.sortable]: column.sortable, + })} + > + <> + {column.title || column.id} + {column.sortable && ( + + )} + + - {isValidElement(column.filter) && column.filter} - -