diff --git a/src/components/menus/Modal.tsx b/src/components/menus/Modal.tsx index bc7b225..746ab5e 100644 --- a/src/components/menus/Modal.tsx +++ b/src/components/menus/Modal.tsx @@ -56,7 +56,7 @@ export function Modal({ @@ -73,6 +73,8 @@ export function Modal({ onClick={close} title="Close" className="hover:rotate-90" + width={20} + height={20} /> )} diff --git a/src/features/account/hooks.ts b/src/features/account/hooks.ts index 13bc133..6791eac 100644 --- a/src/features/account/hooks.ts +++ b/src/features/account/hooks.ts @@ -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; diff --git a/src/features/staking/StakeForm.tsx b/src/features/staking/StakeForm.tsx index 5a0a542..1fad032 100644 --- a/src/features/staking/StakeForm.tsx +++ b/src/features/staking/StakeForm.tsx @@ -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 { @@ -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 ( + + initialValues={{ + ...initialValues, + group: defaultGroup || initialValues.group, + }} + onSubmit={onSubmit} + validate={validate} + validateOnChange={false} + validateOnBlur={false} + > +
+

Stake with a validator

+ + + Stake + + + ); +} + +function AmountField({ availableLockedWei }: { availableLockedWei: bigint }) { + const { setFieldValue } = useFormikContext(); + + const availableLocked = useMemo(() => fromWeiRounded(availableLockedWei), [availableLockedWei]); + + const onClickMax = async () => { + await setFieldValue('amount', availableLocked); + }; + + return ( +
+
+ + + {`${formatNumberString(availableLocked, 2)} Locked CELO available`} + +
+
+ +
+ + Max + +
+
+
+ ); +} + +function GroupField({ + groups, + defaultGroup, +}: { + groups?: ValidatorGroup[]; + defaultGroup?: Address; +}) { + const [field, , helpers] = useField
('group'); + + useEffect(() => { + helpers.setValue(defaultGroup || ZERO_ADDRESS).catch((e) => logger.error(e)); + }, [defaultGroup, helpers]); + return (
-

Stake with a validator

- - initialValues={initialValues} - onSubmit={onSubmit} - validate={validate} - validateOnChange={false} - validateOnBlur={false} - > -
-
-
- - - {`${formatNumberString(lockedBalance?.value, 2)} Locked CELO available`} - -
-
- -
- alert('TODO')}>Max -
-
-
-
- + +
+ +
); } diff --git a/src/features/staking/rewards/computeRewards.test.ts b/src/features/staking/rewards/computeRewards.test.ts index c96aa04..5018699 100644 --- a/src/features/staking/rewards/computeRewards.test.ts +++ b/src/features/staking/rewards/computeRewards.test.ts @@ -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 }, }; diff --git a/src/features/staking/rewards/computeRewards.ts b/src/features/staking/rewards/computeRewards.ts index 3af63f4..96f9a9f 100644 --- a/src/features/staking/rewards/computeRewards.ts +++ b/src/features/staking/rewards/computeRewards.ts @@ -1,4 +1,4 @@ -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'; @@ -6,7 +6,7 @@ import { getDaysBetween } from 'src/utils/time'; export function computeStakingRewards( stakeEvents: StakeEvent[], - stakes: StakingBalances, + stakes: GroupToStake, mode: 'amount' | 'apy' = 'amount', ): Record { return mode === 'amount' @@ -14,7 +14,7 @@ export function computeStakingRewards( : computeRewardApy(stakeEvents, stakes); } -function computeRewardAmount(stakeEvents: StakeEvent[], stakes: StakingBalances) { +function computeRewardAmount(stakeEvents: StakeEvent[], stakes: GroupToStake) { const groupTotals: Record = {}; // group addr to sum votes for (const event of stakeEvents) { const { group, type, value } = event; @@ -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); diff --git a/src/features/staking/rewards/useStakingRewards.ts b/src/features/staking/rewards/useStakingRewards.ts index d328e48..d94239f 100644 --- a/src/features/staking/rewards/useStakingRewards.ts +++ b/src/features/staking/rewards/useStakingRewards.ts @@ -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 () => { diff --git a/src/features/staking/types.ts b/src/features/staking/types.ts index 01a557a..a15f596 100644 --- a/src/features/staking/types.ts +++ b/src/features/staking/types.ts @@ -1,3 +1,6 @@ +export type GroupToStake = Record; +export type StakingBalances = { active: bigint; pending: bigint; total: bigint }; + export enum StakeActionType { Vote = 'vote', Activate = 'activate', @@ -16,8 +19,6 @@ export function stakeActionLabel(type: StakeActionType, activeTense = false) { } } -export type StakingBalances = Record; - export enum StakeEventType { Activate = 'activate', Revoke = 'revoke', // Revoke of active votes (i.e. not pending) diff --git a/src/features/staking/useStakingBalances.ts b/src/features/staking/useStakingBalances.ts index bfbd17c..22be80a 100644 --- a/src/features/staking/useStakingBalances.ts +++ b/src/features/staking/useStakingBalances.ts @@ -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'; @@ -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 @@ -26,7 +32,8 @@ export function useStakingBalances(address?: Address) { return { isLoading, isError, - stakes: data || undefined, + groupToStake: data?.groupToStake, + stakeBalances: data?.stakeBalances, }; } @@ -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]; @@ -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); diff --git a/src/features/validators/ValidatorGroupTable.tsx b/src/features/validators/ValidatorGroupTable.tsx index 60925ae..6dedc6c 100644 --- a/src/features/validators/ValidatorGroupTable.tsx +++ b/src/features/validators/ValidatorGroupTable.tsx @@ -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'; @@ -147,6 +149,8 @@ export function ValidatorGroupTable({ } function useTableColumns(totalVotes: bigint) { + const setTxModal = useStore((state) => state.setTransactionModal); + return useMemo(() => { const columnHelper = createColumnHelper(); return [ @@ -195,7 +199,7 @@ function useTableColumns(totalVotes: bigint) { { e.preventDefault(); - alert(props.row.original.address); + setTxModal({ type: TxModalType.Stake, props: { defaultGroup: props.row.original } }); }} >
@@ -206,7 +210,7 @@ function useTableColumns(totalVotes: bigint) { ), }), ]; - }, [totalVotes]); + }, [totalVotes, setTxModal]); } function useTableRows({ diff --git a/src/features/wallet/WalletDropdown.tsx b/src/features/wallet/WalletDropdown.tsx index a421239..8a64296 100644 --- a/src/features/wallet/WalletDropdown.tsx +++ b/src/features/wallet/WalletDropdown.tsx @@ -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);