-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
71e048d
commit d122485
Showing
37 changed files
with
1,432 additions
and
204 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
import { dataSlice, getAddress } from 'ethers' | ||
import { FC, useMemo, useState } from 'react' | ||
import { useTranslation } from 'react-i18next' | ||
import { useAccount, useSendTransaction, useEstimateGas, useGasPrice } from 'wagmi' | ||
import displayToast from '../../../utilities/displayToast' | ||
import { CONSOLIDATION_CONTRACT } from '../../constants/constants' | ||
import useHasSufficientBalance from '../../hooks/useHasSufficientBalance' | ||
import { Address, ConsolidationTx, ToastType } from '../../types' | ||
import { ValidatorInfo } from '../../types/validator' | ||
import Button, { ButtonFace } from '../Button/Button' | ||
import WalletActionGuard from '../WalletActionGuard/WalletActionGuard' | ||
|
||
export interface ConsolidateViewProps { | ||
targetPubKey: string | ||
sourceValidator: ValidatorInfo | ||
queueLength?: BigInt | undefined | ||
chainId: number | ||
bufferPercentage: bigint | ||
onSubmitRequest: (request: ConsolidationTx) => void | ||
} | ||
|
||
const Consolidate: FC<ConsolidateViewProps> = ({ | ||
targetPubKey, | ||
sourceValidator, | ||
chainId, | ||
queueLength, | ||
bufferPercentage, | ||
onSubmitRequest, | ||
}) => { | ||
const { t } = useTranslation() | ||
const { index, pubKey: sourcePubKey, withdrawalAddress } = sourceValidator | ||
const txData = '0x' + sourcePubKey.substring(2) + targetPubKey.substring(2) | ||
const { address } = useAccount() | ||
const [isLoading, setIsLoading] = useState(false) | ||
const submitRequest = useSendTransaction() | ||
const getRequiredFee = (numerator: bigint, percentage = 0n): bigint => { | ||
// https://eips.ethereum.org/EIPS/eip-7251#fee-calculation | ||
let i = 1n | ||
let output = 0n | ||
let numeratorAccum = 1n * 17n | ||
|
||
while (numeratorAccum > 0n) { | ||
output += numeratorAccum | ||
numeratorAccum = (numeratorAccum * numerator) / (17n * i) | ||
i += 1n | ||
} | ||
|
||
const baseFee = output / 17n | ||
|
||
return (baseFee * (100n + BigInt(percentage)) + (100n - 1n)) / 100n | ||
} | ||
const requestFee = useMemo(() => { | ||
if (!queueLength) return 0n | ||
|
||
return getRequiredFee(BigInt(queueLength), bufferPercentage) | ||
}, [queueLength, bufferPercentage]) | ||
|
||
const { data: gasPrice } = useGasPrice({ chainId }) | ||
|
||
const { data: estimatedGasData } = useEstimateGas({ | ||
to: CONSOLIDATION_CONTRACT, | ||
value: requestFee, | ||
data: txData, | ||
}) | ||
|
||
const estimatedGasLimit = estimatedGasData | ||
? (BigInt(estimatedGasData.toString()) * 110n) / 100n | ||
: null | ||
const gasFee = estimatedGasLimit ? estimatedGasLimit * gasPrice : 0n | ||
const totalRequiredFunds = requestFee + gasFee | ||
const { isSufficient } = useHasSufficientBalance(totalRequiredFunds) | ||
|
||
const formattedWithdrawalAddress = getAddress( | ||
dataSlice(withdrawalAddress as string, 12), | ||
) as Address | ||
|
||
const handleTxError = (e: any) => { | ||
const error = (e as Error).message | ||
let errorMessage = 'error.unexpectedConsolidationError' | ||
if (error.toLowerCase().includes('user rejected the request')) { | ||
errorMessage = 'error.userRejectedTransaction' | ||
} | ||
|
||
displayToast(t(errorMessage), ToastType.ERROR) | ||
} | ||
|
||
const submitConsolidation = async () => { | ||
setIsLoading(true) | ||
try { | ||
const txHash = await submitRequest.sendTransactionAsync({ | ||
to: CONSOLIDATION_CONTRACT, | ||
account: address, | ||
chainId, | ||
value: requestFee, | ||
data: txData, | ||
gas: estimatedGasLimit, | ||
}) | ||
onSubmitRequest({ | ||
index, | ||
pubKey: sourcePubKey, | ||
txHash, | ||
status: 'pending', | ||
}) | ||
} catch (error) { | ||
handleTxError(error) | ||
} finally { | ||
setIsLoading(false) | ||
} | ||
} | ||
|
||
return ( | ||
<WalletActionGuard | ||
isSufficientBalance={isSufficient} | ||
targetAddress={formattedWithdrawalAddress} | ||
> | ||
<Button isLoading={isLoading} type={ButtonFace.SECONDARY} onClick={submitConsolidation}> | ||
{t('validatorManagement.consolidateView.signAndSubmit.submitRequest')} | ||
</Button> | ||
</WalletActionGuard> | ||
) | ||
} | ||
|
||
export default Consolidate |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import { motion } from 'framer-motion' | ||
import Carousel from 'nuka-carousel' | ||
import React, { | ||
FC, | ||
ReactElement, | ||
useState, | ||
useCallback, | ||
useMemo, | ||
Fragment, | ||
Children, | ||
isValidElement, | ||
} from 'react' | ||
import ProgressBar from '../ProgressBar/ProgressBar' | ||
import Typography from '../Typography/Typography' | ||
|
||
export interface StepperRenderProps { | ||
incrementStep: () => void | ||
decrementStep: () => void | ||
step: number | ||
} | ||
|
||
export interface HorizontalStepperProps { | ||
children: (props: StepperRenderProps) => ReactElement | ReactElement[] | ||
steps: string[] | ||
} | ||
|
||
const HorizontalStepper: FC<HorizontalStepperProps> = ({ children, steps }) => { | ||
const totalSteps = steps.length | ||
const [currentStep, setCurrentStep] = useState(0) | ||
|
||
const incrementStep = useCallback(() => { | ||
setCurrentStep((prev) => Math.min(prev + 1, totalSteps - 1)) | ||
}, [totalSteps]) | ||
|
||
const decrementStep = useCallback(() => { | ||
setCurrentStep((prev) => Math.max(prev - 1, 0)) | ||
}, []) | ||
|
||
const stepperProps = useMemo( | ||
() => ({ | ||
incrementStep, | ||
decrementStep, | ||
step: currentStep, | ||
}), | ||
[incrementStep, decrementStep, currentStep], | ||
) | ||
|
||
const slides = useMemo(() => { | ||
return Children.toArray(children(stepperProps)).flatMap((child) => { | ||
if (isValidElement(child) && child.type === Fragment) { | ||
return Children.toArray(child.props.children) | ||
} | ||
return child | ||
}) | ||
}, [children, stepperProps]) | ||
|
||
return ( | ||
<> | ||
<div className='w-full'> | ||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className='w-full flex'> | ||
{steps.map((label, index) => ( | ||
<div | ||
key={index} | ||
className='flex-1 h-11 bg-dark25 dark:bg-dark750 flex items-center justify-center border-r dark:border-r-dark600 last:border-r-0' | ||
> | ||
<div className='flex space-x-2 items-center'> | ||
<div className='w-6 h-6 lg:w-3 lg:h-3 flex items-center justify-center border dark:border-dark300 text-dark900 rounded-full'> | ||
<Typography type='text-caption' className='lg:text-xTiny'> | ||
{index + 1} | ||
</Typography> | ||
</div> | ||
<Typography className='hidden lg:block' type='text-caption1'> | ||
{label} | ||
</Typography> | ||
</div> | ||
</div> | ||
))} | ||
</motion.div> | ||
<ProgressBar total={totalSteps} position={currentStep + 1} /> | ||
</div> | ||
<div className='w-full h-full relative createSlide'> | ||
<Carousel swiping={false} slideIndex={currentStep} dragging={false} withoutControls> | ||
{slides.map((child, index) => ( | ||
<div key={index} className='h-full w-full'> | ||
{child} | ||
</div> | ||
))} | ||
</Carousel> | ||
</div> | ||
</> | ||
) | ||
} | ||
|
||
export default HorizontalStepper |
Oops, something went wrong.