Skip to content

Commit d0899e6

Browse files
SOV-3945: prevent creation of "dead vestings" (#964)
* feat: custom row wrapper for table component * feat: add alert message * chore: fix review comments * chore: add changeset --------- Co-authored-by: soulBit <its.soulBit@gmail.com>
1 parent 68e9c96 commit d0899e6

17 files changed

Lines changed: 368 additions & 49 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sovryn/ui': patch
3+
---
4+
5+
feat: allow custom row wrapper in table component

.changeset/real-paws-poke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"frontend": patch
3+
---
4+
5+
SOV-3945: prevent "dead vestings" from LM rewards

apps/frontend/src/app/4_templates/PageContainer/PageContainer.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@ import { Outlet } from 'react-router-dom';
55

66
import { applyDataAttr } from '@sovryn/ui';
77

8+
import { RSK_CHAIN_ID } from '../../../config/chains';
9+
810
import { DappLocked } from '../../1_atoms/DappLocked/DappLocked';
911
import { Header, Footer } from '../../3_organisms';
12+
import { UnclaimcedVestingAlert } from '../../5_pages/RewardsPage/components/Vesting/components/UnclaimedVestingAlert/UnclaimedVestingAlert';
1013
import { useIsDappLocked } from '../../../hooks/maintenances/useIsDappLocked';
14+
import { useAccount } from '../../../hooks/useAccount';
15+
import { useCurrentChain } from '../../../hooks/useChainStore';
1116

1217
type PageContainerProps = {
1318
className?: string;
@@ -21,6 +26,8 @@ export const PageContainer: FC<PageContainerProps> = ({
2126
dataAttribute,
2227
}) => {
2328
const dappLocked = useIsDappLocked();
29+
const { account } = useAccount();
30+
const chainID = useCurrentChain();
2431
return (
2532
<div
2633
className={classNames('flex flex-col flex-grow', className)}
@@ -31,6 +38,7 @@ export const PageContainer: FC<PageContainerProps> = ({
3138
) : (
3239
<>
3340
<Header />
41+
{account && chainID === RSK_CHAIN_ID && <UnclaimcedVestingAlert />}
3442
<div
3543
className={classNames('my-2 px-4 flex flex-grow', contentClassName)}
3644
>

apps/frontend/src/app/5_pages/RewardsPage/components/Vesting/Vesting.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { translations } from '../../../../../locales/i18n';
1010
import { VestingContractType } from '../../../../../utils/graphql/rsk/generated';
1111
import { columnsConfig } from './Vestings.constants';
1212
import { generateRowTitle } from './Vestings.utils';
13+
import { VestingContextProvider } from './context/VestingContext';
1314
import { useGetVestingContracts } from './hooks/useGetVestingContracts';
1415

1516
const pageSize = DEFAULT_PAGE_SIZE;
@@ -57,6 +58,7 @@ export const Vesting: FC = () => {
5758
isLoading={loading}
5859
dataAttribute="vesting-rewards-history-table"
5960
rowTitle={generateRowTitle}
61+
rowComponent={VestingContextProvider}
6062
/>
6163
<Pagination
6264
page={page}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const MIN_ALERT_COUNT = 32;
2+
export const LOCK_CLAIM_COUNT = 43;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import React from 'react';
2+
3+
import { t } from 'i18next';
4+
import { Link } from 'react-router-dom';
5+
6+
import { Paragraph } from '@sovryn/ui';
7+
8+
import { translations } from '../../../../../../../locales/i18n';
9+
import { useGetUnclaimedUserVestingCount } from '../../hooks/useLmLimit';
10+
import {
11+
LOCK_CLAIM_COUNT,
12+
MIN_ALERT_COUNT,
13+
} from './UnclaimedVestingAlert.constants';
14+
15+
export const UnclaimcedVestingAlert = () => {
16+
const count = useGetUnclaimedUserVestingCount();
17+
18+
if (count > MIN_ALERT_COUNT && count < LOCK_CLAIM_COUNT) {
19+
return (
20+
<div className="bg-error-light bg-opacity-50 p-4 rounded-lg mt-12 my-4 mx-8">
21+
<Paragraph>
22+
{t(translations.unclaimedVestings.text, { value: count })}{' '}
23+
<Link
24+
to="/rewards"
25+
className="underline text-primary-20 hover:text-primary-10"
26+
>
27+
{t(translations.unclaimedVestings.cta)}
28+
</Link>
29+
</Paragraph>
30+
</div>
31+
);
32+
}
33+
34+
return null;
35+
};

apps/frontend/src/app/5_pages/RewardsPage/components/Vesting/components/UnlockSchedule/UnlockSchedule.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
import React, { useMemo, useState } from 'react';
1+
import React, { useEffect, useMemo, useState } from 'react';
22

33
import { Button, ButtonStyle, ButtonType } from '@sovryn/ui';
44

55
import { VestingContractTableRecord } from '../../Vesting.types';
66
import { vestingTypeToTitleMapping } from '../../Vestings.utils';
7+
import { useVestingContext } from '../../context/VestingContext';
78
import { useGetUnlockSchedule } from '../../hooks/useGetUnlockSchedule';
89
import { UnlockScheduleDialog } from '../UnlockScheduleDialog/UnlockScheduleDialog';
910

1011
export const UnlockSchedule = (item: VestingContractTableRecord) => {
12+
const update = useVestingContext().update;
1113
const unlockSchedule = useGetUnlockSchedule(item);
1214

1315
const [showDialog, setShowDialog] = useState(false);
@@ -17,6 +19,10 @@ export const UnlockSchedule = (item: VestingContractTableRecord) => {
1719
[item.type],
1820
);
1921

22+
useEffect(() => {
23+
update(state => (state.item = item));
24+
}, [item, update]);
25+
2026
return (
2127
<>
2228
<Button

apps/frontend/src/app/5_pages/RewardsPage/components/Vesting/components/WithdrawButton/WithdrawButton.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,25 @@ import { t } from 'i18next';
55
import { Button, ButtonType, ButtonStyle } from '@sovryn/ui';
66

77
import { translations } from '../../../../../../../locales/i18n';
8+
import { VestingContractType } from '../../../../../../../utils/graphql/rsk/generated';
89
import { VestingContractTableRecord } from '../../Vesting.types';
10+
import { useVestingContext } from '../../context/VestingContext';
911
import { useGetUnlockedBalance } from '../../hooks/useGetUnlockedBalance';
1012
import { useHandleWithdraw } from '../../hooks/useHandleWithdraw';
13+
import { LOCK_CLAIM_COUNT } from '../UnclaimedVestingAlert/UnclaimedVestingAlert.constants';
1114

1215
export const WithdrawButton = (item: VestingContractTableRecord) => {
16+
const { count } = useVestingContext();
1317
const handleWithdraw = useHandleWithdraw(item);
1418

1519
const { isLoading, result } = useGetUnlockedBalance(item);
16-
const isDisabled = useMemo(() => !result || result === 0, [result]);
20+
const isDisabled = useMemo(
21+
() =>
22+
!result ||
23+
result === 0 ||
24+
(count >= LOCK_CLAIM_COUNT && item.type === VestingContractType.Rewards),
25+
[count, item.type, result],
26+
);
1727

1828
return (
1929
<div className="flex justify-end w-full md:w-auto h-full pt-3">
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import React, {
2+
useContext,
3+
createContext,
4+
useState,
5+
useCallback,
6+
FC,
7+
PropsWithChildren,
8+
} from 'react';
9+
10+
import { produce } from 'immer';
11+
12+
import { VestingContractTableRecord } from '../Vesting.types';
13+
14+
export type State = {
15+
count: number;
16+
item?: VestingContractTableRecord;
17+
};
18+
19+
type Update = (state: State) => void;
20+
21+
type Actions = {
22+
update: (handler: Update) => void;
23+
};
24+
25+
const defaultValue: State & Actions = {
26+
count: 0,
27+
item: undefined,
28+
update: () => {},
29+
};
30+
31+
const VestingContext = createContext<State & Actions>(defaultValue);
32+
33+
export function useVestingContext() {
34+
const context = useContext(VestingContext);
35+
if (context === undefined) {
36+
throw new Error(
37+
'useVestingContext must be used within a VestingContextProvider',
38+
);
39+
}
40+
return context;
41+
}
42+
43+
export const VestingContextProvider: FC<PropsWithChildren> = ({ children }) => {
44+
const [state, setState] = useState<State>({
45+
count: 0,
46+
item: undefined,
47+
});
48+
49+
const handleOnChange = useCallback(
50+
(handler: (value: State) => void) => setState(produce(handler)),
51+
[],
52+
);
53+
54+
return (
55+
<VestingContext.Provider value={{ ...state, update: handleOnChange }}>
56+
{children}
57+
</VestingContext.Provider>
58+
);
59+
};

apps/frontend/src/app/5_pages/RewardsPage/components/Vesting/hooks/useGetUnlockSchedule.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useMemo } from 'react';
1+
import { useEffect, useMemo } from 'react';
22

33
import dayjs from 'dayjs';
44

@@ -8,12 +8,14 @@ import {
88
VestingContractTableRecord,
99
VestingHistoryItem,
1010
} from '../Vesting.types';
11+
import { useVestingContext } from '../context/VestingContext';
1112

1213
const MAXIMUM_UNLOCKED_DATES = 2;
1314

1415
export const useGetUnlockSchedule = (
1516
item: VestingContractTableRecord,
1617
): VestingHistoryItem[] | undefined => {
18+
const update = useVestingContext().update;
1719
const { data } = useGetVestingHistoryQuery({
1820
variables: { vestingAddress: item.address },
1921
client: rskClient,
@@ -49,11 +51,20 @@ export const useGetUnlockSchedule = (
4951
const pastDatesLength = unlockDates?.filter(item => item.isUnlocked).length;
5052

5153
if (!pastDatesLength || pastDatesLength < MAXIMUM_UNLOCKED_DATES) {
52-
return unlockDates;
54+
return { unlockDates, pastDatesLength };
5355
}
5456

55-
return unlockDates.slice(pastDatesLength - MAXIMUM_UNLOCKED_DATES);
57+
return {
58+
unlockDates: unlockDates.slice(pastDatesLength - MAXIMUM_UNLOCKED_DATES),
59+
pastDatesLength,
60+
};
5661
}, [data?.vestingContracts]);
5762

58-
return result;
63+
useEffect(() => {
64+
update(state => {
65+
state.count = result?.pastDatesLength || 0;
66+
});
67+
}, [result?.pastDatesLength, update]);
68+
69+
return result?.unlockDates;
5970
};

0 commit comments

Comments
 (0)