Skip to content

Commit

Permalink
Progress on stake form
Browse files Browse the repository at this point in the history
  • Loading branch information
jmrossy committed Dec 28, 2023
1 parent 388611e commit c8ddcc8
Show file tree
Hide file tree
Showing 10 changed files with 126 additions and 54 deletions.
4 changes: 3 additions & 1 deletion src/components/menus/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export function Modal({
<Dialog.Panel
className={`w-full ${
width || 'max-w-xs'
} max-h-[90vh] transform overflow-auto rounded-2xl bg-white ${
} max-h-[90vh] transform overflow-auto bg-white ${
padding || 'px-4 py-4'
} text-left shadow-lg transition-all`}
>
Expand All @@ -73,6 +73,8 @@ export function Modal({
onClick={close}
title="Close"
className="hover:rotate-90"
width={20}
height={20}
/>
</div>
)}
Expand Down
2 changes: 1 addition & 1 deletion src/features/account/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function useLockedBalance(address?: Address) {
decimals: CELO.decimals,
formatted: formatUnits(data, CELO.decimals),
symbol: CELO.symbol,
value: data,
value: BigInt(data),
}
: undefined;

Expand Down
122 changes: 90 additions & 32 deletions src/features/staking/StakeForm.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { Form, Formik } from 'formik';
import { Form, Formik, useField, useFormikContext } from 'formik';
import { useEffect, useMemo } from 'react';
import { OutlineButton } from 'src/components/buttons/OutlineButton';
import { SolidButton } from 'src/components/buttons/SolidButton';
import { NumberField } from 'src/components/input/NumberField';
import { formatNumberString } from 'src/components/numbers/Amount';
import { ZERO_ADDRESS } from 'src/config/consts';
import { useLockedBalance } from 'src/features/account/hooks';
import { useStakingBalances } from 'src/features/staking/useStakingBalances';
import { ValidatorGroup } from 'src/features/validators/types';
import { useValidatorGroups } from 'src/features/validators/useValidatorGroups';
import { fromWeiRounded } from 'src/utils/amount';
import { logger } from 'src/utils/logger';
import { bigIntMax } from 'src/utils/math';
import { useAccount } from 'wagmi';

interface StakeFormValues {
Expand All @@ -19,51 +24,104 @@ const initialValues: StakeFormValues = {
group: ZERO_ADDRESS,
};

export function StakeForm() {
export function StakeForm({ defaultGroup }: { defaultGroup?: Address }) {
const { address } = useAccount();
const { groups } = useValidatorGroups();
const { lockedBalance } = useLockedBalance(address);
const { stakes } = useStakingBalances(address);
const { stakeBalances } = useStakingBalances(address);

const availableLockedWei = bigIntMax(
(lockedBalance?.value || 0n) - (stakeBalances?.total || 0n),
0n,
);

const onSubmit = (values: StakeFormValues) => {
alert(values);
logger.debug(groups);
logger.debug(stakes);
};

const validate = (values: StakeFormValues) => {
alert(values);
};

return (
<Formik<StakeFormValues>
initialValues={{
...initialValues,
group: defaultGroup || initialValues.group,
}}
onSubmit={onSubmit}
validate={validate}
validateOnChange={false}
validateOnBlur={false}
>
<Form className="mt-2 flex w-full flex-col items-stretch space-y-4">
<h2 className="font-serif text-2xl">Stake with a validator</h2>
<AmountField availableLockedWei={availableLockedWei} />
<GroupField groups={groups} defaultGroup={defaultGroup} />
<SolidButton type="submit">Stake</SolidButton>
</Form>
</Formik>
);
}

function AmountField({ availableLockedWei }: { availableLockedWei: bigint }) {
const { setFieldValue } = useFormikContext();

const availableLocked = useMemo(() => fromWeiRounded(availableLockedWei), [availableLockedWei]);

const onClickMax = async () => {
await setFieldValue('amount', availableLocked);
};

return (
<div>
<div className="flex items-center justify-between">
<label htmlFor="amount" className="pl-0.5 text-sm">
Amount
</label>
<span className="text-xs">
{`${formatNumberString(availableLocked, 2)} Locked CELO available`}
</span>
</div>
<div className="relative mt-2">
<NumberField name="amount" className="w-full all:py-3" />
<div className="absolute right-1 top-2 z-10">
<OutlineButton onClick={onClickMax} type="button">
Max
</OutlineButton>
</div>
</div>
</div>
);
}

function GroupField({
groups,
defaultGroup,
}: {
groups?: ValidatorGroup[];
defaultGroup?: Address;
}) {
const [field, , helpers] = useField<Address>('group');

useEffect(() => {
helpers.setValue(defaultGroup || ZERO_ADDRESS).catch((e) => logger.error(e));
}, [defaultGroup, helpers]);

return (
<div>
<h2>Stake with a validator</h2>
<Formik<StakeFormValues>
initialValues={initialValues}
onSubmit={onSubmit}
validate={validate}
validateOnChange={false}
validateOnBlur={false}
>
<Form className="mt-2 flex w-full flex-col items-stretch">
<div>
<div className="flex justify-between pr-1">
<label htmlFor="amount" className="pl-0.5 text-sm">
Amount
</label>
<span className="text-xs">
{`${formatNumberString(lockedBalance?.value, 2)} Locked CELO available`}
</span>
</div>
<div className="relative">
<NumberField name="amount" />
<div className="absolute right-0 top-1/3 z-10">
<OutlineButton onClick={() => alert('TODO')}>Max</OutlineButton>
</div>
</div>
</div>
</Form>
</Formik>
<label htmlFor="group" className="pl-0.5 text-sm">
Group
</label>
<div>
<select name="group" className="w-full" value={field.value}>
{(groups || []).map((group) => (
<option key={group.address} value={group.address}>
{group.name}
</option>
))}
</select>
</div>
</div>
);
}
4 changes: 2 additions & 2 deletions src/features/staking/rewards/computeRewards.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import {
computeStakingRewards,
getTimeWeightedAverageActive,
} from 'src/features/staking/rewards/computeRewards';
import { StakeEvent, StakeEventType, StakingBalances } from 'src/features/staking/types';
import { GroupToStake, StakeEvent, StakeEventType } from 'src/features/staking/types';
import { nowMinusDays } from 'src/test/time';
import { toWei } from 'src/utils/amount';

function stakes(activeAmount: number, group = '0x1'): StakingBalances {
function stakes(activeAmount: number, group = '0x1'): GroupToStake {
return {
[group]: { active: toWei(activeAmount), pending: 0n },
};
Expand Down
8 changes: 4 additions & 4 deletions src/features/staking/rewards/computeRewards.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { StakeEvent, StakeEventType, StakingBalances } from 'src/features/staking/types';
import { GroupToStake, StakeEvent, StakeEventType } from 'src/features/staking/types';
import { fromWei } from 'src/utils/amount';
import { logger } from 'src/utils/logger';
import { objKeys } from 'src/utils/objects';
import { getDaysBetween } from 'src/utils/time';

export function computeStakingRewards(
stakeEvents: StakeEvent[],
stakes: StakingBalances,
stakes: GroupToStake,
mode: 'amount' | 'apy' = 'amount',
): Record<Address, number> {
return mode === 'amount'
? computeRewardAmount(stakeEvents, stakes)
: computeRewardApy(stakeEvents, stakes);
}

function computeRewardAmount(stakeEvents: StakeEvent[], stakes: StakingBalances) {
function computeRewardAmount(stakeEvents: StakeEvent[], stakes: GroupToStake) {
const groupTotals: Record<Address, bigint> = {}; // group addr to sum votes
for (const event of stakeEvents) {
const { group, type, value } = event;
Expand All @@ -41,7 +41,7 @@ function computeRewardAmount(stakeEvents: StakeEvent[], stakes: StakingBalances)
return groupRewards;
}

function computeRewardApy(stakeEvents: StakeEvent[], stakes: StakingBalances) {
function computeRewardApy(stakeEvents: StakeEvent[], stakes: GroupToStake) {
// First get total reward amounts per group
const groupRewardAmounts = computeRewardAmount(stakeEvents, stakes);

Expand Down
4 changes: 2 additions & 2 deletions src/features/staking/rewards/useStakingRewards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { useQuery } from '@tanstack/react-query';
import { useToastError } from 'src/components/notifications/useToastError';
import { computeStakingRewards } from 'src/features/staking/rewards/computeRewards';
import { fetchStakeEvents } from 'src/features/staking/rewards/fetchStakeHistory';
import { StakingBalances } from 'src/features/staking/types';
import { GroupToStake } from 'src/features/staking/types';
import { logger } from 'src/utils/logger';

export function useStakingRewards(address?: Address, stakes?: StakingBalances) {
export function useStakingRewards(address?: Address, stakes?: GroupToStake) {
const { isLoading, isError, error, data } = useQuery({
queryKey: ['useStakingRewards', address, stakes],
queryFn: async () => {
Expand Down
5 changes: 3 additions & 2 deletions src/features/staking/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export type GroupToStake = Record<Address, { active: bigint; pending: bigint }>;
export type StakingBalances = { active: bigint; pending: bigint; total: bigint };

export enum StakeActionType {
Vote = 'vote',
Activate = 'activate',
Expand All @@ -16,8 +19,6 @@ export function stakeActionLabel(type: StakeActionType, activeTense = false) {
}
}

export type StakingBalances = Record<Address, { active: bigint; pending: bigint }>;

export enum StakeEventType {
Activate = 'activate',
Revoke = 'revoke', // Revoke of active votes (i.e. not pending)
Expand Down
19 changes: 13 additions & 6 deletions src/features/staking/useStakingBalances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { electionABI } from '@celo/abis';
import { useQuery } from '@tanstack/react-query';
import { useToastError } from 'src/components/notifications/useToastError';
import { Addresses } from 'src/config/contracts';
import { StakingBalances } from 'src/features/staking/types';
import { GroupToStake } from 'src/features/staking/types';
import { logger } from 'src/utils/logger';
import { objKeys } from 'src/utils/objects';
import { PublicClient, usePublicClient } from 'wagmi';
Expand All @@ -12,10 +12,16 @@ export function useStakingBalances(address?: Address) {

const { isLoading, isError, error, data } = useQuery({
queryKey: ['useStakingBalances', publicClient, address],
queryFn: () => {
queryFn: async () => {
if (!address) return null;
logger.debug('Fetching staking balances');
return fetchStakingBalances(publicClient, address);
const groupToStake = await fetchStakingBalances(publicClient, address);
const stakes = Object.values(groupToStake);
const active = stakes.reduce((acc, stake) => acc + stake.active, 0n);
const pending = stakes.reduce((acc, stake) => acc + stake.pending, 0n);
const total = active + pending;
const stakeBalances = { active, pending, total };
return { groupToStake, stakeBalances };
},
gcTime: 10 * 60 * 1000, // 10 minutes
staleTime: 1 * 60 * 1000, // 1 minute
Expand All @@ -26,7 +32,8 @@ export function useStakingBalances(address?: Address) {
return {
isLoading,
isError,
stakes: data || undefined,
groupToStake: data?.groupToStake,
stakeBalances: data?.stakeBalances,
};
}

Expand Down Expand Up @@ -60,7 +67,7 @@ export async function fetchStakingBalances(publicClient: PublicClient, address:
})),
});

const votes: StakingBalances = {};
const votes: GroupToStake = {};
for (let i = 0; i < groupAddrs.length; i++) {
const groupAddr = groupAddrs[i];
const pending = pendingVotes[i];
Expand All @@ -75,7 +82,7 @@ export async function fetchStakingBalances(publicClient: PublicClient, address:

export async function checkHasActivatable(
publicClient: PublicClient,
stakes: StakingBalances,
stakes: GroupToStake,
address: Address,
) {
const groupsWithPending = objKeys(stakes).filter((groupAddr) => stakes[groupAddr].pending > 0);
Expand Down
8 changes: 6 additions & 2 deletions src/features/validators/ValidatorGroupTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { config } from 'src/config/config';

import Link from 'next/link';
import { TabHeaderButton } from 'src/components/buttons/TabHeaderButton';
import { useStore } from 'src/features/store';
import { TxModalType } from 'src/features/transactions/types';
import { ValidatorGroupLogo } from 'src/features/validators/ValidatorGroupLogo';
import { cleanGroupName, isElected } from 'src/features/validators/utils';
import { useIsMobile } from 'src/styles/mediaQueries';
Expand Down Expand Up @@ -147,6 +149,8 @@ export function ValidatorGroupTable({
}

function useTableColumns(totalVotes: bigint) {
const setTxModal = useStore((state) => state.setTransactionModal);

return useMemo(() => {
const columnHelper = createColumnHelper<ValidatorGroupRow>();
return [
Expand Down Expand Up @@ -195,7 +199,7 @@ function useTableColumns(totalVotes: bigint) {
<OutlineButton
onClick={(e) => {
e.preventDefault();
alert(props.row.original.address);
setTxModal({ type: TxModalType.Stake, props: { defaultGroup: props.row.original } });
}}
>
<div className="flex items-center space-x-1.5">
Expand All @@ -206,7 +210,7 @@ function useTableColumns(totalVotes: bigint) {
),
}),
];
}, [totalVotes]);
}, [totalVotes, setTxModal]);
}

function useTableRows({
Expand Down
4 changes: 2 additions & 2 deletions src/features/wallet/WalletDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ function DropdownContent({
// TODO update these hooks with a refetch interval after upgrading to wagmi v2
const { balance: walletBalance } = useBalance(address);
const { lockedBalance } = useLockedBalance(address);
const { stakes } = useStakingBalances(address);
const { totalRewards } = useStakingRewards(address, stakes);
const { groupToStake } = useStakingBalances(address);
const { totalRewards } = useStakingRewards(address, groupToStake);

const totalBalance = (walletBalance?.value || 0n) + (lockedBalance?.value || 0n);

Expand Down

0 comments on commit c8ddcc8

Please sign in to comment.