Skip to content

Commit

Permalink
feat: claim all rewards with single tx
Browse files Browse the repository at this point in the history
  • Loading branch information
creed-victor committed Aug 31, 2023
1 parent 25c289b commit ca1f2d9
Show file tree
Hide file tree
Showing 9 changed files with 273 additions and 71 deletions.
5 changes: 5 additions & 0 deletions .changeset/friendly-laws-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sovryn/ui': patch
---

feat: allow to hide table header row
5 changes: 5 additions & 0 deletions .changeset/nice-crabs-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'frontend': patch
---

feat: claim all rewards with single tx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand All @@ -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: (
<AmountRenderer
value={formatUnits(earnedFee.value, 18)}
suffix={getTokenDisplayName(earnedFee.token)}
precision={BTC_RENDER_PRECISION}
dataAttribute={`${earnedFee.token}-rewards-amount`}
/>
<div className="flex flex-col gap-1 my-4 text-left">
{earnedFees.map(fee => (
<AmountRenderer
key={fee.token}
value={formatUnits(fee.value, 18)}
suffix={getTokenDisplayName(fee.token)}
precision={BTC_RENDER_PRECISION}
dataAttribute={`${fee.token}-rewards-amount`}
/>
))}
</div>
),
action: <WithdrawFee {...earnedFee} refetch={refetch} />,
key: `${earnedFee.token}-fee`,
})),
action: <WithdrawAllFees fees={earnedFees} refetch={refetch} />,
key: `all-fee`,
},
{
type: t(translations.rewardPage.staking.stakingSubsidies),
amount: (
Expand All @@ -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 (
<div className="flex flex-col items-center w-full">
<div className="flex flex-col items-center w-full gap-y-8">
<div className="lg:bg-gray-80 lg:py-4 lg:px-4 rounded w-full">
<Table
columns={columns}
rows={rows}
rows={noRewards ? [] : rows1}
isLoading={!!account ? loading : false}
rowKey={row => row.key}
noData={
Expand Down
Original file line number Diff line number Diff line change
@@ -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<WithdrawFeeProps> = ({ 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 (
<Button
type={ButtonType.button}
style={ButtonStyle.secondary}
text={t(translations.rewardPage.stabilityPool.actions.withdrawAll)}
onClick={onSubmit}
disabled={isClaimDisabled}
className="w-full lg:w-auto whitespace-nowrap"
dataAttribute="rewards-withdraw"
/>
);
};

type UserCheckpoint = {
token: SupportedTokens;
checkpointNum: number;
hasFees: boolean;
hasSkippedCheckpoints: boolean;
};

let feeSharingContract: Contract;
const getFeeSharingContract = async () => {
if (!feeSharingContract) {
feeSharingContract = (
await getProtocolContract('feeSharing', defaultChainId)
).contract(getProvider(defaultChainId));
}
return feeSharingContract;
};

async function getNextPositiveCheckpoint(
owner: string,
fee: EarnedFee,
): Promise<UserCheckpoint> {
let userNextUnprocessedCheckpoint = fee.startFrom || 0;
while (userNextUnprocessedCheckpoint < (fee.maxCheckpoints || 0)) {
const { hasFees, checkpointNum, hasSkippedCheckpoints } = await (
await getFeeSharingContract()
).getNextPositiveUserCheckpoint(
owner,
fee.contractAddress,
userNextUnprocessedCheckpoint,
MAX_NEXT_POSITIVE_CHECKPOINT,
);

userNextUnprocessedCheckpoint = Number(checkpointNum);

if (!!hasFees) {
return {
token: fee.token,
checkpointNum: Number(checkpointNum),
hasFees,
hasSkippedCheckpoints,
};
}
}

return {
token: fee.token,
checkpointNum: 0,
hasFees: false,
hasSkippedCheckpoints: false,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type WithdrawFeeProps = EarnedFee & {
refetch: () => void;
};

/** @deprecated */
export const WithdrawFee: FC<WithdrawFeeProps> = ({
token,
value,
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/src/constants/gasLimits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const GAS_LIMIT = {
STABILITY_POOL_DLLR: 600_000,
STABILITY_POOL_DLLR_INC_WITHDRAW: 690_000,
REWARDS: 240_000,
REWARDS_CLAIM: 5_000_000,
TRANSFER_LOC: 900_000,
LENDING_MINT: 350_000,
LENDING_BURN: 450_000,
Expand Down
3 changes: 2 additions & 1 deletion apps/frontend/src/locales/en/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,8 @@
"stakingRevenue": "Staking revenue",
"actions": {
"transferToLOC": "Transfer to LOC",
"withdraw": "Withdraw"
"withdraw": "Withdraw",
"withdrawAll": "Withdraw All"
},
"tx": {
"withdraw": "Withdraw rewards to your address",
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/2_molecules/Table/Table.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export type TableProps<RowType extends RowObject> = {
expandedClassNames?: string;
preventExpandOnClickClass?: string;
mobileRenderer?: (row: RowType) => ReactNode;
hideHeader?: boolean;
};

export enum OrderDirection {
Expand Down
Loading

0 comments on commit ca1f2d9

Please sign in to comment.