Skip to content

Commit

Permalink
feat: Update designs for Updating and Claiming staking rewards (#336)
Browse files Browse the repository at this point in the history
* staking rewards design changes

* use our Amount component for claiming rewards

* color tweaks for amplitude
  • Loading branch information
gonzamontiel authored Feb 14, 2024
1 parent e765993 commit 2ebc5d0
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 91 deletions.
86 changes: 52 additions & 34 deletions src/pages/collators/CollatorRewards.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ExclamationCircleIcon } from '@heroicons/react/24/outline';
import { useCallback, useEffect, useMemo, useState } from 'preact/compat';
import { Button } from 'react-daisyui';
import { toast } from 'react-toastify';
Expand All @@ -12,6 +13,8 @@ import { nativeToFormat } from '../../shared/parseNumbers';
import { UserStaking } from './CollatorColumns';
import ClaimRewardsDialog from './dialogs/ClaimRewardsDialog';

const WAIT_15_MINUTES = 15 * 60 * 1000;

function CollatorRewards() {
const { api, tokenSymbol, ss58Format } = useNodeInfoState().state;
const { walletAccount } = useGlobalState();
Expand All @@ -22,6 +25,7 @@ function CollatorRewards() {
const [claimDialogOpen, setClaimDialogOpen] = useState<boolean>(false);
const [submissionPending, setSubmissionPending] = useState(false);
const [unstaking, setUnstaking] = useState<string>('0.00');
const [updateEnabled, setUpdateEnabled] = useState<boolean>(true);

const userAccountAddress = useMemo(() => {
return walletAccount && ss58Format ? getAddressForFormat(walletAccount?.address, ss58Format) : '';
Expand Down Expand Up @@ -68,7 +72,7 @@ function CollatorRewards() {
return createUpdateDelegatorRewardsExtrinsic();
}, [api, createUpdateDelegatorRewardsExtrinsic]);

const submitUpdateExtrinsic = useCallback(() => {
const submitUpdate = useCallback(() => {
if (!updateRewardsExtrinsic || !api || !walletAccount) {
return;
}
Expand All @@ -89,6 +93,10 @@ function CollatorRewards() {
refreshRewards();
if (errors.length === 0) {
toast('Delegator rewards updated', { type: 'success' });
setUpdateEnabled(false);
setTimeout(() => {
setUpdateEnabled(true);
}, WAIT_15_MINUTES);
}
}
})
Expand All @@ -98,15 +106,15 @@ function CollatorRewards() {
});
setSubmissionPending(false);
});
}, [api, refreshRewards, updateRewardsExtrinsic, walletAccount]);
}, [api, refreshRewards, updateRewardsExtrinsic, walletAccount, setUpdateEnabled]);

return (
<>
<div className="flex flex-col mb-8 justify-between md:flex-row ">
<div className="card rounded-lg bg-base-200 mb-3 md:w-1/2 md:mb-0 md:mr-5 collators-box">
<div className="card-body px-4 xs:px-8">
<h2 className="card-title">Staking</h2>
<div className="flex flex-row flex-wrap gap-4">
<div className="card-body py-6 px-4 xs:px-8">
<h2 className="card-title font-normal">Staking</h2>
<div className="flex flex-row flex-wrap gap-4 items-center">
<div className="flex-initial">
<StakedIcon className="staked-icon mt-1" />
</div>
Expand All @@ -118,47 +126,32 @@ function CollatorRewards() {
<h3>{nativeToFormat(userAvailableBalance, tokenSymbol)}</h3>
<p>Free balance</p>
</div>
<div className="flex flex-auto place-content-end">
<button className="btn btn-secondary w-full" disabled>
{unstaking} unstaking
</button>
<div className="flex flex-auto flex-col items-center">
<div className="flex flex-row items-center mb-1">
<h3>{nativeToFormat(unstaking, tokenSymbol)}</h3>
<div className="tooltip tooltip-primary" data-tip="Locked for 7 days.">
<ExclamationCircleIcon className="w-5 h-5 ml-2 text-gray-400" />
</div>
</div>
<button className="btn btn-primary btn-unlock min-h-fit max-h-10 w-full m-auto px-8">Unlock</button>
</div>
</div>
<div className="flex flex-none flex-col items-center">
<h3 className="font-semibold">{nativeToFormat(parseInt(unstaking), tokenSymbol)}</h3>
<button className="btn btn-primary btn-unlock w-full m-auto px-8" disabled>
Unlock
</button>
</div>
</div>
</div>
<div className="card rounded-lg bg-base-200 md:w-1/2 collators-box">
<div className="card-body px-4 xs:px-8">
<h2 className="card-title">Staking Rewards</h2>
<div className="card-body py-6 px-4 xs:px-8">
<h2 className="card-title font-normal mb-2">Staking Rewards</h2>
<div className="flex flex-row">
<div className="flex-initial pt-1 pr-5 pb-0">
<RewardsIcon className="rewards-icon" />
</div>
<div className="flex-auto">
<h3 className="font-semibold">{nativeToFormat(estimatedRewards, tokenSymbol)}</h3>
<h3 className="font-semibold primary">{nativeToFormat(estimatedRewards, tokenSymbol)}</h3>
<p>Estimated reward</p>
</div>
<div className="flex flex-auto place-content-end">
<Button
loading={submissionPending}
onClick={() => submitUpdateExtrinsic()}
className="btn btn-primary lg:w-1/3 btn-outline mr-2 rounded-md px-2 py-0 leading-3"
disabled={!walletAccount}
>
{submissionPending ? '' : 'Update'}
</Button>
<Button
onClick={() => setClaimDialogOpen(true)}
className="btn btn-primary rounded-md lg:w-1/3 leading-3"
disabled={!walletAccount || parseFloat(estimatedRewards) <= 0}
>
Claim
</Button>
<div className="flex flex-auto flex-col xs:flex-row place-content-end">
{UpdateButton()}
{ClaimButton()}
</div>
</div>
</div>
Expand All @@ -172,6 +165,31 @@ function CollatorRewards() {
/>
</>
);

function ClaimButton() {
return (
<Button
onClick={() => setClaimDialogOpen(true)}
className="btn btn-primary btn-outline rounded-md w-full xs:w-1/2 leading-3 p-0 min-h-fit max-h-10"
disabled={!walletAccount || parseFloat(estimatedRewards) <= 0}
>
Claim
</Button>
);
}

function UpdateButton() {
return (
<Button
loading={submissionPending}
onClick={() => submitUpdate()}
className="btn btn-primary w-full xs:w-1/2 xs:mr-2 mb-2 rounded-md p-0 leading-3 min-h-fit max-h-10 min-w-20"
disabled={!updateEnabled || !walletAccount}
>
{submissionPending ? '' : 'Update'}
</Button>
);
}
}

export default CollatorRewards;
2 changes: 1 addition & 1 deletion src/pages/collators/Collators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import './styles.css';

function Collators() {
return (
<div className="overflow-x-hidden xl:mx-20 lg:mx-5 collators-list-container">
<div className="overflow-x-hidden xl:mx-40 lg:mx-10 collators-list-container">
<CollatorRewards />
<CollatorsTable />
</div>
Expand Down
111 changes: 64 additions & 47 deletions src/pages/collators/dialogs/ClaimRewardsDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { yupResolver } from '@hookform/resolvers/yup';
import { useCallback, useMemo, useState } from 'preact/hooks';
import { Button, Modal } from 'react-daisyui';
import { useForm } from 'react-hook-form';
import { toast } from 'react-toastify';
import { useGlobalState } from '../../../GlobalStateProvider';
import { useNodeInfoState } from '../../../NodeInfoProvider';
import SuccessDialogIcon from '../../../assets/dialog-status-success';
import { CloseButton } from '../../../components/CloseButton';
import Amount from '../../../components/Form/Amount';
import { getErrors } from '../../../helpers/substrate';
import { ParachainStakingInflationInflationInfo, useStakingPallet } from '../../../hooks/staking/staking';
import { format, nativeToDecimal } from '../../../shared/parseNumbers';
import { nativeToDecimal } from '../../../shared/parseNumbers';
import { getClaimingValidationSchema } from './ValidationSchema';

interface Props {
userRewardsBalance?: string;
Expand All @@ -23,14 +27,27 @@ enum ClaimStep {
Success = 1,
}

export type ClaimFormValues = {
amount: number;
};

function ClaimRewardsDialog(props: Props) {
const { userRewardsBalance = '0', tokenSymbol, visible, onClose } = props;
const { createClaimRewardExtrinsic } = useStakingPallet();
const { api } = useNodeInfoState().state;
const { walletAccount } = useGlobalState();
const [loading, setLoading] = useState<boolean>(false);
const [step, setStep] = useState<ClaimStep>(ClaimStep.Confirm);
const amount = nativeToDecimal(userRewardsBalance);
const balance = nativeToDecimal(userRewardsBalance).toNumber();

const form = useForm<ClaimFormValues>({
resolver: yupResolver(getClaimingValidationSchema(balance)),
defaultValues: {
amount: parseFloat(balance.toFixed(2)),
},
});

const { handleSubmit, formState, register, setValue, getValues, control } = form;

useMemo(() => {
if (!visible)
Expand All @@ -39,46 +56,55 @@ function ClaimRewardsDialog(props: Props) {
}, 500);
}, [visible]);

const submitExtrinsic = useCallback(() => {
if (!walletAccount || !api || !amount) return;
const submitExtrinsic = useCallback(
(selectedAmount: number) => {
if (!walletAccount || !api || formState.errors.amount) return;

setLoading(true);
setLoading(true);

const extrinsic = createClaimRewardExtrinsic(userRewardsBalance);
const extrinsic = createClaimRewardExtrinsic(selectedAmount.toString());

extrinsic
// eslint-disable-next-line @typescript-eslint/no-explicit-any
?.signAndSend(walletAccount.address, { signer: walletAccount.signer as any }, (result) => {
const { status, events } = result;
extrinsic
// eslint-disable-next-line @typescript-eslint/no-explicit-any
?.signAndSend(walletAccount.address, { signer: walletAccount.signer as any }, (result) => {
const { status, events } = result;

const errors = getErrors(events, api);
if (status.isInBlock) {
if (errors.length > 0) {
const errorMessage = `Transaction failed with errors: ${errors.join('\n')}`;
console.error(errorMessage);
toast(errorMessage, { type: 'error' });
const errors = getErrors(events, api);
if (status.isInBlock) {
if (errors.length > 0) {
const errorMessage = `Transaction failed with errors: ${errors.join('\n')}`;
console.error(errorMessage);
toast(errorMessage, { type: 'error' });
}
} else if (status.isFinalized) {
setLoading(false);
if (errors.length === 0) {
setStep(ClaimStep.Success);
}
}
} else if (status.isFinalized) {
})
.catch((error) => {
console.error('Transaction submission failed', error);
toast('Transaction submission failed', { type: 'error' });
setLoading(false);
if (errors.length === 0) {
setStep(ClaimStep.Success);
}
}
})
.catch((error) => {
console.error('Transaction submission failed', error);
toast('Transaction submission failed', { type: 'error' });
setLoading(false);
});
}, [api, amount, createClaimRewardExtrinsic, walletAccount, setLoading, setStep, userRewardsBalance]);
});
},
[api, formState, createClaimRewardExtrinsic, walletAccount, setLoading, setStep, userRewardsBalance],
);

const content = useMemo(() => {
switch (step) {
case ClaimStep.Confirm:
return (
<div className="rounded-lg bg-base-200 flex flex-col p-8 items-center w-fit center m-auto">
<p className="flex">Amount</p>
<h1 className="flex text-4xl">{format(amount.toNumber(), tokenSymbol)}</h1>
<div className="rounded-lg flex flex-col items-center w-full">
<form className="flex flex-col">
<Amount
register={register('amount')}
max={nativeToDecimal(userRewardsBalance).toNumber()}
setValue={(n: number) => setValue('amount', n)}
error={formState.errors.amount?.message?.toString()}
/>
</form>
</div>
);
case ClaimStep.Success:
Expand All @@ -90,7 +116,7 @@ function ClaimRewardsDialog(props: Props) {
</div>
);
}
}, [amount, step, tokenSymbol]);
}, [step, tokenSymbol, formState]);

const getButtonText = (step: ClaimStep) => {
switch (step) {
Expand All @@ -105,29 +131,20 @@ function ClaimRewardsDialog(props: Props) {
switch (step) {
case ClaimStep.Confirm:
return () => {
submitExtrinsic();
submitExtrinsic(getValues().amount);
};
case ClaimStep.Success:
return onClose;
}
};

return (
<Modal open={visible}>
<Modal.Header className="font-bold">Claim Rewards</Modal.Header>
<Modal className="bg-base-200" open={visible}>
<Modal.Header className="text-2xl claim-title">Claim Rewards</Modal.Header>
<CloseButton onClick={onClose} />
<Modal.Body>
<div className="mt-4" />
{content}
</Modal.Body>
<Modal.Actions className="justify-center mt-10">
<Button
className="px-12 text-thin"
color="primary"
loading={loading}
onClick={getButtonAction(step)}
disabled={!walletAccount || amount.lt(0)}
>
<Modal.Body>{content}</Modal.Body>
<Modal.Actions className="justify-center">
<Button color="primary" loading={loading} onClick={getButtonAction(step)} disabled={!walletAccount}>
{getButtonText(step)}
</Button>
</Modal.Actions>
Expand Down
11 changes: 11 additions & 0 deletions src/pages/collators/dialogs/ValidationSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,14 @@ export function getStakingValidationSchema(max: number) {
.max(max, 'Amount is higher than available.'),
});
}

export function getClaimingValidationSchema(max: number) {
return Yup.object<FormValues>().shape({
amount: Yup.number()
.typeError('Value is invalid.')
.transform(transformNumber)
.positive('You need to enter a positive number.')
.required('This field is required.')
.max(max, 'Amount is higher than available.'),
});
}
Loading

0 comments on commit 2ebc5d0

Please sign in to comment.