Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
220 changes: 220 additions & 0 deletions src/pages/stakingPage/components/StakeUnstakeModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import {
Box,
Button,
Flex,
HStack,
Input,
Modal,
ModalCloseButton,
ModalContent,
ModalOverlay,
Text,
VStack,
} from '@chakra-ui/react';
import React, { useMemo } from 'react';

import { GrixLogo } from '@/components/commons/Logo';

import { formatBalance } from '../utils/formatters';

type StakeUnstakeModalProps = {
isOpen: boolean;
onClose: () => void;
mode: 'stake' | 'unstake';
title: string;
amount: string;
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
needsApproval: boolean;
isApproving: boolean;
handleApprove: () => void;
isAmountValid: () => boolean;
isLoading: boolean;
onSubmit: () => void;
availableAmount: string;
tokenType: 'gx' | 'esgx';
};

export const StakeUnstakeModal: React.FC<StakeUnstakeModalProps> = ({
isOpen,
onClose,
mode,
title,
amount,
handleInputChange,
needsApproval,
isApproving,
handleApprove,
isAmountValid,
isLoading,
onSubmit,
availableAmount,
tokenType,
}) => {
const isStakeMode = mode === 'stake';
const isEsGrix = tokenType === 'esgx';

const hasBalance = useMemo(() => {
try {
const balance = Number(availableAmount);
return balance > 0.000001; // Consider very small amounts as no balance
} catch {
return false;
}
}, [availableAmount]);

const handlePercentageClick = (percentage: number) => {
if (!hasBalance) return;

if (percentage === 100) {
// Use exact available amount without any calculations
const event = {
target: { value: availableAmount },
} as React.ChangeEvent<HTMLInputElement>;
handleInputChange(event);
return;
}

// For other percentages, use string manipulation to maintain precision

const baseAmount = availableAmount.replace('.', '');
const decimals = availableAmount.split('.')[1]?.length || 0;
const multiplier = BigInt(percentage);
const result = (BigInt(baseAmount) * multiplier) / BigInt(100);

let valueStr = result.toString();
if (decimals > 0) {
const padded = valueStr.padStart(decimals + 1, '0');
valueStr = `${padded.slice(0, -decimals)}.${padded.slice(-decimals)}`;
}

const event = {
target: { value: valueStr },
} as React.ChangeEvent<HTMLInputElement>;
handleInputChange(event);
};

return (
<Modal isOpen={isOpen} onClose={onClose} isCentered>
<ModalOverlay bg="blackAlpha.900" backdropFilter="blur(2px)" />
<ModalContent bg="#0D0F12" maxW="400px">
<Box px={6} py={4}>
<Flex justify="space-between" align="center" mb={4}>
<Text color="white" fontSize="xl" fontWeight="600">
{title}
</Text>
<ModalCloseButton position="static" color="gray.400" />
</Flex>

<VStack spacing={4} align="stretch">
<Text color="gray.500" fontSize="md" mb={1}>
{isStakeMode ? 'Stake Amount' : 'Unstake Amount'}
</Text>

<Box bg="#161A1E" borderRadius="xl" p={4}>
<Flex align="center" justify="space-between">
<HStack spacing={3}>
<Box bg="#1E2328" p={2} borderRadius="lg">
<GrixLogo boxSize="24px" />
</Box>
<Text color="white" fontSize="lg" fontWeight="600">
{isEsGrix ? 'esGRIX' : 'GRIX'}
</Text>
</HStack>
<Input
placeholder="Enter Amount"
value={Number(amount) ? Number(amount).toFixed(3) : amount}
onChange={handleInputChange}
variant="unstyled"
textAlign="right"
color="white"
fontSize="lg"
width="auto"
/>
</Flex>
</Box>

<Flex justify="space-between" align="center">
<Text color="gray.500" fontSize="md">
{isStakeMode ? 'Available balance' : 'Staked balance'}
</Text>
<Text color="white" fontSize="md">
{formatBalance(availableAmount)} {isEsGrix ? 'esGRIX' : 'GRIX'}
</Text>
</Flex>

<HStack spacing={2}>
{[25, 50, 75, 100].map((percentage) => (
<Button
key={percentage}
onClick={() => handlePercentageClick(percentage)}
bg="#1E2328"
color="white"
size="md"
flex={1}
isDisabled={!hasBalance}
opacity={hasBalance ? 1 : 0.5}
_hover={{ bg: hasBalance ? '#2A3038' : '#1E2328' }}
_active={{ bg: hasBalance ? '#2A3038' : '#1E2328' }}
>
{percentage}%
</Button>
))}
</HStack>

{!isStakeMode && (
<Box bg="#161A1E" p={4} borderRadius="xl">
<Flex align="center" gap={2}>
<Box as="span" color="#3B82F6" fontSize="lg">
</Box>
<Text color="#3B82F6" fontSize="sm">
You might not be able to unstake your desired {isEsGrix ? 'esGRIX' : 'GRIX'} if there is an active
vesting position.
</Text>
</Flex>
</Box>
)}

<Box mt={2}>
{amount && needsApproval && isStakeMode ? (
<Button
isLoading={isApproving}
loadingText="Approving"
onClick={handleApprove}
bg="#2F6C60"
color="white"
size="lg"
width="full"
height="48px"
fontSize="sm"
isDisabled={!isAmountValid()}
_hover={{ bg: '#2A5F54' }}
_active={{ bg: '#264F46' }}
>
Approve
</Button>
) : (
<Button
isLoading={isLoading}
loadingText={isStakeMode ? 'Staking' : 'Unstaking'}
onClick={onSubmit}
bg="#2F6C60"
color="white"
size="lg"
width="full"
height="48px"
fontSize="sm"
isDisabled={!isAmountValid() || !amount}
_hover={{ bg: '#2A5F54' }}
_active={{ bg: '#264F46' }}
>
{isStakeMode ? 'Stake' : 'Unstake'}
</Button>
)}
</Box>
</VStack>
</Box>
</ModalContent>
</Modal>
);
};
52 changes: 14 additions & 38 deletions src/pages/stakingPage/components/StakingCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import {
getEsGrixStakedAmount,
getStakedAmount,
getTokenBalance,
stakeEsGs,
stakeGs,
unstakeEsGs,
stakeEsGRIX,
stakeGRIX,
unstakeEsGRIX,
unstakeGs,
} from '@/web3Config/staking/hooks';

import { StakingCardContent } from './StakingCardContent.js';
import { StakingCardContent } from './StakingCardContent';

type StakingCardProps = {
title: string;
Expand All @@ -43,7 +43,6 @@ export const StakingCard: React.FC<StakingCardProps> = ({
const [availableBalance, setAvailableBalance] = useState('0');
const [stakedAmount, setStakedAmount] = useState('0');
const [apr, setApr] = useState(0);
const [_showError, setShowError] = useState(false);
const toast = useToast();
const tokenAddress = type === 'gx' ? stakingContracts.grixToken.address : stakingContracts.esGRIXToken.address;

Expand Down Expand Up @@ -105,14 +104,6 @@ export const StakingCard: React.FC<StakingCardProps> = ({
return () => clearInterval(interval);
}, [fetchBalance, fetchStakedAmount, fetchAPR, refreshTrigger]);

const handleMaxClick = () => {
if (type === 'esgx') {
setAmount(stakedAmount);
} else {
setAmount(availableBalance);
}
};

const isAmountValid = useCallback(() => {
if (!amount) return false;
try {
Expand Down Expand Up @@ -143,7 +134,7 @@ export const StakingCard: React.FC<StakingCardProps> = ({
const amountBigInt = amount ? parseEther(amount) : 0n;
setNeedsApproval(allowance < amountBigInt);
} catch (error) {
setNeedsApproval(true); // Default to needing approval on error
setNeedsApproval(true);
}
}, [address, amount, tokenAddress]);

Expand All @@ -158,6 +149,10 @@ export const StakingCard: React.FC<StakingCardProps> = ({
}
};

const clearAmount = () => {
setAmount('');
};

const refreshAllData = useCallback(
async (triggerParentRefresh = true) => {
await Promise.all([fetchBalance(), fetchStakedAmount(), fetchAPR()]);
Expand All @@ -181,9 +176,7 @@ export const StakingCard: React.FC<StakingCardProps> = ({
setIsApproving(true);
const amountBigInt = parseEther(amount);
await approveStaking(tokenAddress, amountBigInt);

showToast('Approval Successful', 'You can now stake your tokens', 'success');

setNeedsApproval(false);
await refreshAllData();
} catch (error) {
Expand All @@ -196,26 +189,18 @@ export const StakingCard: React.FC<StakingCardProps> = ({
const handleStake = async () => {
if (!amount || !address) return;

if (!isAmountValid()) {
showToast('Invalid Amount', 'Amount exceeds available balance', 'error');
setShowError(true);
return;
}

try {
setIsStaking(true);
const amountBigInt = parseEther(amount);

if (type === 'gx') {
await stakeGs(amountBigInt);
await stakeGRIX(amountBigInt);
} else {
await stakeEsGs(amountBigInt);
await stakeEsGRIX(amountBigInt);
}

showToast('Staking Successful', 'Your tokens have been staked', 'success');

setAmount('');
setShowError(false);
await refreshAllData();
} catch (error) {
showToast('Staking Failed', 'There was an error staking your tokens', 'error');
Expand All @@ -228,27 +213,17 @@ export const StakingCard: React.FC<StakingCardProps> = ({
if (!amount || !address) return;

try {
const currentStaked = type === 'gx' ? await getStakedAmount(address) : await getEsGrixStakedAmount(address);

if (Number(amount) > Number(currentStaked)) {
showToast('Invalid Amount', 'Amount exceeds staked balance', 'error');
setShowError(true);
return;
}

setIsUnstaking(true);
const amountBigInt = parseEther(amount);

if (type === 'gx') {
await unstakeGs(amountBigInt);
} else {
await unstakeEsGs(amountBigInt);
await unstakeEsGRIX(amountBigInt);
}

showToast('Unstaking Successful', 'Your tokens have been unstaked', 'success');

setAmount('');
setShowError(false);
await refreshAllData();
} catch (error) {
showToast('Unstaking Failed', 'There was an error unstaking your tokens', 'error');
Expand All @@ -266,7 +241,6 @@ export const StakingCard: React.FC<StakingCardProps> = ({
apr={apr}
amount={amount}
handleInputChange={handleInputChange}
handleMaxClick={handleMaxClick}
needsApproval={needsApproval}
isApproving={isApproving}
handleApprove={() => void handleApprove()}
Expand All @@ -276,6 +250,8 @@ export const StakingCard: React.FC<StakingCardProps> = ({
handleStake={() => void handleStake()}
isUnstaking={isUnstaking}
handleUnstake={() => void handleUnstake()}
type={type}
clearAmount={clearAmount}
/>
);
};
Loading