From 489503be355db01da24ba90530da488a971ee1f0 Mon Sep 17 00:00:00 2001 From: Dmitrii Podlesnyi Date: Mon, 21 Aug 2023 22:49:29 +0400 Subject: [PATCH 01/22] chore: wrap form refactor --- .../request/form/inputs/amount-input.tsx | 8 +- .../request/form/inputs/token-input.tsx | 8 +- .../request/form/transaction-info.tsx | 7 +- .../request-form-context.tsx | 38 +-- .../request/request-form-context/types.ts | 1 + .../request-form-context/validators.ts | 128 +++------ .../wrap/features/unwrap-form/unwrap-form.tsx | 40 +-- .../features/unwrap-form/unwrap-stats.tsx | 39 +++ .../unwrap-form/utils/unwrap-processing.ts | 102 +++++++ .../compute-wrap-form-context-values.ts | 21 ++ .../wrap/features/wrap-form-context/index.ts | 2 + .../wrap/features/wrap-form-context/types.ts | 27 ++ .../use-wrap-form-network-data.tsx | 88 ++++++ .../wrap-form-context/wrap-form-context.tsx | 110 ++++++++ .../wrap-form-context/wrap-form-validators.ts | 52 ++++ .../wrap-form/controls/amount-input.tsx | 30 ++ .../controls/error-message-input-group.tsx | 15 + .../wrap-form/controls/submit-button.tsx | 30 ++ .../wrap-form/controls/token-input.tsx | 61 ++++ features/wrap/features/wrap-form/form.tsx | 265 ------------------ features/wrap/features/wrap-form/hooks.tsx | 112 -------- .../wrap-form/hooks/use-approve-gas-limit.tsx | 41 +++ .../hooks/use-wrap-form-processing.ts | 94 +++++++ .../wrap-form/hooks/use-wrap-gas-limit.ts | 75 +++++ .../wrap-form/hooks/use-wrap-tx-approve.ts | 58 ++++ .../wrap-form/hooks/use-wrap-tx-processing.ts | 63 +++++ .../features/wrap-form/wrap-form-tx-modal.tsx | 66 +++++ .../wrap-form/wrap-form-with-providers.tsx | 13 + .../wrap/features/wrap-form/wrap-form.tsx | 251 ++--------------- .../wrap/features/wrap-form/wrap-stats.tsx | 69 +++++ features/wrap/index.ts | 1 - features/wrap/styles.tsx | 22 +- features/wrap/types.ts | 9 + features/wrap/utils.ts | 257 ----------------- pages/wrap/[[...mode]].tsx | 5 +- .../tx-link-etherscan/tx-link-etherscan.tsx | 2 +- .../tx-stage-modal/tx-stage-modal.tsx | 4 +- .../components/input-amount/input-amount.tsx | 14 +- .../forms/hooks/useCurrencyAmountValidator.ts | 6 +- shared/forms/hooks/useCurrencyInput.ts | 12 +- shared/forms/hooks/useCurrencyMaxAmount.ts | 6 +- shared/hook-form/validate-bignumber-max.ts | 11 + shared/hook-form/validate-bignumber-min.ts | 11 + shared/hook-form/validate-ether-amount.ts | 33 +++ shared/hooks/useApprove.ts | 36 ++- shared/hooks/useWstethBySteth.ts | 34 +-- utils/await-with-timeout.ts | 7 + 47 files changed, 1277 insertions(+), 1107 deletions(-) create mode 100644 features/wrap/features/unwrap-form/unwrap-stats.tsx create mode 100644 features/wrap/features/unwrap-form/utils/unwrap-processing.ts create mode 100644 features/wrap/features/wrap-form-context/compute-wrap-form-context-values.ts create mode 100644 features/wrap/features/wrap-form-context/index.ts create mode 100644 features/wrap/features/wrap-form-context/types.ts create mode 100644 features/wrap/features/wrap-form-context/use-wrap-form-network-data.tsx create mode 100644 features/wrap/features/wrap-form-context/wrap-form-context.tsx create mode 100644 features/wrap/features/wrap-form-context/wrap-form-validators.ts create mode 100644 features/wrap/features/wrap-form/controls/amount-input.tsx create mode 100644 features/wrap/features/wrap-form/controls/error-message-input-group.tsx create mode 100644 features/wrap/features/wrap-form/controls/submit-button.tsx create mode 100644 features/wrap/features/wrap-form/controls/token-input.tsx delete mode 100644 features/wrap/features/wrap-form/form.tsx delete mode 100644 features/wrap/features/wrap-form/hooks.tsx create mode 100644 features/wrap/features/wrap-form/hooks/use-approve-gas-limit.tsx create mode 100644 features/wrap/features/wrap-form/hooks/use-wrap-form-processing.ts create mode 100644 features/wrap/features/wrap-form/hooks/use-wrap-gas-limit.ts create mode 100644 features/wrap/features/wrap-form/hooks/use-wrap-tx-approve.ts create mode 100644 features/wrap/features/wrap-form/hooks/use-wrap-tx-processing.ts create mode 100644 features/wrap/features/wrap-form/wrap-form-tx-modal.tsx create mode 100644 features/wrap/features/wrap-form/wrap-form-with-providers.tsx create mode 100644 features/wrap/features/wrap-form/wrap-stats.tsx create mode 100644 features/wrap/types.ts delete mode 100644 features/wrap/utils.ts create mode 100644 shared/hook-form/validate-bignumber-max.ts create mode 100644 shared/hook-form/validate-bignumber-min.ts create mode 100644 shared/hook-form/validate-ether-amount.ts create mode 100644 utils/await-with-timeout.ts diff --git a/features/withdrawals/request/form/inputs/amount-input.tsx b/features/withdrawals/request/form/inputs/amount-input.tsx index 3adcee996..2c86123ed 100644 --- a/features/withdrawals/request/form/inputs/amount-input.tsx +++ b/features/withdrawals/request/form/inputs/amount-input.tsx @@ -1,4 +1,3 @@ -import { TOKENS } from '@lido-sdk/constants'; import { InputDecoratorTvlStake } from 'features/withdrawals/shared/input-decorator-tvl-stake'; import { useController, useWatch } from 'react-hook-form'; import { InputAmount } from 'shared/forms/components/input-amount'; @@ -11,7 +10,7 @@ import { import { useTvlMessage } from 'features/withdrawals/hooks/useTvlMessage'; export const AmountInput = () => { - const { balanceSteth, balanceWSteth, isTokenLocked } = useRequestFormData(); + const { maxAmount, isTokenLocked } = useRequestFormData(); const token = useWatch({ name: 'token' }); const { @@ -23,15 +22,12 @@ export const AmountInput = () => { const { balanceDiff } = useTvlMessage(error); - const balance = token === TOKENS.STETH ? balanceSteth : balanceWSteth; - return ( } diff --git a/features/withdrawals/request/form/inputs/token-input.tsx b/features/withdrawals/request/form/inputs/token-input.tsx index cf661bb02..9a942abdf 100644 --- a/features/withdrawals/request/form/inputs/token-input.tsx +++ b/features/withdrawals/request/form/inputs/token-input.tsx @@ -10,10 +10,10 @@ import { TokensWithdrawable } from 'features/withdrawals/types/tokens-withdrawab const iconsMap = { [TOKENS.WSTETH]: , [TOKENS.STETH]: , -}; +} as const; export const TokenInput = () => { - const { setValue, getFieldState } = useFormContext(); + const { setValue } = useFormContext(); const { field } = useController({ name: 'token', }); @@ -26,8 +26,6 @@ export const TokenInput = () => { icon={iconsMap[field.value]} error={errors.amount?.type === 'validate'} onChange={(value: TokensWithdrawable) => { - // this softly changes token state, resets amount and only validates if it was touched - const { isDirty } = getFieldState('amount'); setValue('token', value, { shouldDirty: false, shouldTouch: false, @@ -36,7 +34,7 @@ export const TokenInput = () => { setValue('amount', null, { shouldDirty: false, shouldTouch: false, - shouldValidate: isDirty, + shouldValidate: false, }); }} > diff --git a/features/withdrawals/request/form/transaction-info.tsx b/features/withdrawals/request/form/transaction-info.tsx index 6327e4269..531d109c2 100644 --- a/features/withdrawals/request/form/transaction-info.tsx +++ b/features/withdrawals/request/form/transaction-info.tsx @@ -1,7 +1,7 @@ import { TOKENS } from '@lido-sdk/constants'; import { DataTableRow } from '@lidofinance/lido-ui'; import { useRequestTxPrice } from 'features/withdrawals/hooks/useWithdrawTxPrice'; -import { useApproveGasLimit } from 'features/wrap/features/wrap-form/hooks'; +import { useApproveGasLimit } from 'features/wrap/features/wrap-form/hooks/use-approve-gas-limit'; import { useWatch } from 'react-hook-form'; import { DataTableRowStethByWsteth } from 'shared/components/data-table-row-steth-by-wsteth'; import { FormatToken } from 'shared/formatters'; @@ -29,7 +29,10 @@ export const TransactionInfo = () => { isApprovalFlow, requestCount: requests?.length, }); - const approveTxCostInUsd = useTxCostInUsd(useApproveGasLimit()); + const approveGasLimit = useApproveGasLimit(); + const approveTxCostInUsd = useTxCostInUsd( + approveGasLimit && Number(approveGasLimit), + ); const isInfiniteAllowance = useMemo(() => { return allowance.eq(MaxUint256); diff --git a/features/withdrawals/request/request-form-context/request-form-context.tsx b/features/withdrawals/request/request-form-context/request-form-context.tsx index 5f002b32e..3c596c8ce 100644 --- a/features/withdrawals/request/request-form-context/request-form-context.tsx +++ b/features/withdrawals/request/request-form-context/request-form-context.tsx @@ -1,5 +1,5 @@ import { useMemo, useState, createContext, useContext, useEffect } from 'react'; -import { FormProvider, useForm, useWatch } from 'react-hook-form'; +import { FormProvider, useForm } from 'react-hook-form'; import invariant from 'tiny-invariant'; import { TOKENS } from '@lido-sdk/constants'; @@ -52,7 +52,7 @@ export const RequestFormProvider: React.FC = ({ children }) => { useState({ requests: null }); const requestFormData = useRequestFormDataContextValue(); - const { onSuccessRequest } = requestFormData; + const { balanceSteth, balanceWSteth, onSuccessRequest } = requestFormData; const validationContext = useValidationContext( requestFormData, setIntermediateValidationResults, @@ -74,11 +74,8 @@ export const RequestFormProvider: React.FC = ({ children }) => { }); // TODO refactor this part as part of TX flow - const { control, handleSubmit, reset } = formObject; - const [token, amount] = useWatch({ - control: control, - name: ['token', 'amount'], - }); + const { handleSubmit, reset, watch } = formObject; + const [token, amount] = watch(['token', 'amount']); const { allowance, request, @@ -106,23 +103,28 @@ export const RequestFormProvider: React.FC = ({ children }) => { dispatchModalState({ type: 'set_on_retry', callback: onSubmit }); }, [dispatchModalState, onSubmit]); - const value = useMemo(() => { - return { + const maxAmount = token === TOKENS.STETH ? balanceSteth : balanceWSteth; + + const value = useMemo( + (): RequestFormDataContextValueType => ({ ...requestFormData, isApprovalFlow, isApprovalFlowLoading, isTokenLocked, allowance, + maxAmount, onSubmit, - }; - }, [ - requestFormData, - isApprovalFlow, - isApprovalFlowLoading, - isTokenLocked, - allowance, - onSubmit, - ]); + }), + [ + requestFormData, + isApprovalFlow, + isApprovalFlowLoading, + isTokenLocked, + allowance, + maxAmount, + onSubmit, + ], + ); return ( diff --git a/features/withdrawals/request/request-form-context/types.ts b/features/withdrawals/request/request-form-context/types.ts index 802ce3f7e..8bf7847ee 100644 --- a/features/withdrawals/request/request-form-context/types.ts +++ b/features/withdrawals/request/request-form-context/types.ts @@ -33,6 +33,7 @@ export type ExtraRequestFormDataType = { isApprovalFlowLoading: boolean; isTokenLocked: boolean; allowance: BigNumber; + maxAmount?: BigNumber; onSubmit: NonNullable['onSubmit']>; }; diff --git a/features/withdrawals/request/request-form-context/validators.ts b/features/withdrawals/request/request-form-context/validators.ts index 67f7b4cbe..66ee784c1 100644 --- a/features/withdrawals/request/request-form-context/validators.ts +++ b/features/withdrawals/request/request-form-context/validators.ts @@ -1,4 +1,3 @@ -import { MaxUint256, Zero } from '@ethersproject/constants'; import { formatEther } from '@ethersproject/units'; import { TOKENS } from '@lido-sdk/constants'; import { BigNumber } from 'ethers'; @@ -13,19 +12,18 @@ import { } from '.'; import { VALIDATION_CONTEXT_TIMEOUT } from 'features/withdrawals/withdrawals-constants'; -import { ValidationError } from 'shared/hook-form/validation-error'; +import { + ValidationError, + handleResolverValidationError, +} from 'shared/hook-form/validation-error'; import { getTokenDisplayName } from 'utils/getTokenDisplayName'; +import { awaitWithTimeout } from 'utils/await-with-timeout'; +import { validateEtherAmount } from 'shared/hook-form/validate-ether-amount'; +import { validateBignumberMin } from 'shared/hook-form/validate-bignumber-min'; +import { validateBignumberMax } from 'shared/hook-form/validate-bignumber-max'; // helpers that should be shared when adding next hook-form -export const withTimeout = (toWait: Promise, timeout: number) => - Promise.race([ - toWait, - new Promise((_, reject) => - setTimeout(() => reject(new Error('promise timeout')), timeout), - ), - ]); - export type TvlErrorPayload = { balanceDiffSteth: BigNumber; }; @@ -51,62 +49,13 @@ export class ValidationSplitRequest extends ValidationError { } } -// asserts only work with function declaration -// eslint-disable-next-line func-style -function validateEtherAmount( - field: string, - amount: BigNumber | null, - token: TokensWithdrawable, -): asserts amount is BigNumber { - if (!amount) - throw new ValidationError( - field, - `${getTokenDisplayName(token)} ${field} is required`, - ); - - if (amount.lte(Zero)) - throw new ValidationError( - field, - `${getTokenDisplayName(token)} ${field} must be greater than 0`, - ); - - if (amount.gt(MaxUint256)) - throw new ValidationError( - field, - `${getTokenDisplayName(token)} ${field} is not valid`, - ); -} - -const validateMinUnstake = ( - field: string, - value: BigNumber, - min: BigNumber, - token: TokensWithdrawable, -) => { - if (value.lt(min)) - throw new ValidationError( - field, - `Minimum unstake amount is ${formatEther(min)} ${getTokenDisplayName( - token, - )}`, - ); - return value; -}; +const messageMinUnstake = (min: BigNumber, token: TokensWithdrawable) => + `Minimum unstake amount is ${formatEther(min)} ${getTokenDisplayName(token)}`; -const validateMaxAmount = ( - field: string, - value: BigNumber, - max: BigNumber, - token: TokensWithdrawable, -) => { - if (value.gt(max)) - throw new ValidationError( - field, - `${getTokenDisplayName( - token, - )} ${field} must not be greater than ${formatEther(max)}`, - ); -}; +const messageMaxAmount = (max: BigNumber, token: TokensWithdrawable) => + `${getTokenDisplayName(token)} amount must not be greater than ${formatEther( + max, + )}`; // TODO!: write tests for this validation function const validateSplitRequests = ( @@ -195,7 +144,7 @@ export const RequestFormValidationResolver: Resolver< // validation function only waits limited time for data and fails validation otherwise // most of the time data will already be available invariant(contextPromise, 'must have context promise'); - const context = await withTimeout( + const context = await awaitWithTimeout( contextPromise, VALIDATION_CONTEXT_TIMEOUT, ); @@ -209,7 +158,9 @@ export const RequestFormValidationResolver: Resolver< stethTotalSupply, } = transformContext(context, values); - if (isSteth) tvlJokeValidate('amount', amount, stethTotalSupply, balance); + if (isSteth) { + tvlJokeValidate('amount', amount, stethTotalSupply, balance); + } // early validation exit for dex option if (mode === 'dex') { @@ -224,39 +175,30 @@ export const RequestFormValidationResolver: Resolver< ); validationResults.requests = requests; - validateMinUnstake('amount', amount, minAmountPerRequest, token); + validateBignumberMin( + 'amount', + amount, + minAmountPerRequest, + messageMinUnstake(minAmountPerRequest, token), + ); - validateMaxAmount('amount', amount, balance, token); + validateBignumberMax( + 'amount', + amount, + balance, + messageMaxAmount(balance, token), + ); return { values: { ...values, requests }, errors: {}, }; } catch (error) { - if (error instanceof ValidationError) { - return { - values: {}, - errors: { - [error.field]: { - message: error.message, - type: error.type, - payload: error.payload, - }, - }, - }; - } - console.warn('[RequestForm] Unhandled validation error in resolver', error); - return { - values: {}, - errors: { - // for general errors we use 'requests' field - // cause non-fields get ignored and form is still considerate valid - requests: { - type: 'validate', - message: 'unknown validation error', - }, - }, - }; + return handleResolverValidationError( + error, + 'WithdrawalRequestForm', + 'amount', + ); } finally { // no matter validation result save results for the UI to show setResults?.(validationResults); diff --git a/features/wrap/features/unwrap-form/unwrap-form.tsx b/features/wrap/features/unwrap-form/unwrap-form.tsx index f1fe58826..edb70e9f9 100644 --- a/features/wrap/features/unwrap-form/unwrap-form.tsx +++ b/features/wrap/features/unwrap-form/unwrap-form.tsx @@ -8,13 +8,7 @@ import { useRef, } from 'react'; import { parseEther } from '@ethersproject/units'; -import { - Block, - DataTable, - DataTableRow, - Wsteth, - Button, -} from '@lidofinance/lido-ui'; +import { Block, Wsteth, Button } from '@lidofinance/lido-ui'; import { TOKENS } from '@lido-sdk/constants'; import { useWeb3 } from 'reef-knot/web3-react'; import { @@ -23,21 +17,19 @@ import { useWSTETHBalance, useWSTETHContractWeb3, } from '@lido-sdk/react'; +import { useIsMultisig } from 'shared/hooks/useIsMultisig'; import { TxStageModal, TX_OPERATION, TX_STAGE } from 'shared/components'; import { L2Banner } from 'shared/l2-banner'; import { MATOMO_CLICK_EVENTS } from 'config'; -import { useTxCostInUsd, useStethByWsteth } from 'shared/hooks'; +import { useStethByWsteth } from 'shared/hooks'; import { useCurrencyInput } from 'shared/forms/hooks/useCurrencyInput'; import { formatBalance } from 'utils'; import { Connect } from 'shared/wallet'; import { InputDecoratorMaxButton } from 'shared/forms/components/input-decorator-max-button'; import { FormStyled, InputStyled } from 'features/wrap/styles'; -import { DataTableRowStethByWsteth } from 'shared/components/data-table-row-steth-by-wsteth'; -import { unwrapProcessing } from 'features/wrap/utils'; -import { useUnwrapGasLimit } from './hooks'; +import { unwrapProcessing } from './utils/unwrap-processing'; import { getTokenDisplayName } from 'utils/getTokenDisplayName'; -import { FormatToken } from 'shared/formatters/format-token'; -import { useIsMultisig } from 'shared/hooks/useIsMultisig'; +import { UnwrapStats } from './unwrap-stats'; export const UnwrapForm: FC = memo(() => { const { active, chainId } = useWeb3(); @@ -57,10 +49,6 @@ export const UnwrapForm: FC = memo(() => { const [txModalFailedText, setTxModalFailedText] = useState(''); const [inputValue, setInputValue] = useState(''); - const unwrapGasLimit = useUnwrapGasLimit(); - - const unwrapTxCostInUsd = useTxCostInUsd(unwrapGasLimit); - const openTxModal = useCallback(() => { setTxModalOpen(true); }, []); @@ -181,23 +169,7 @@ export const UnwrapForm: FC = memo(() => { - - - ${unwrapTxCostInUsd?.toFixed(2)} - - - - - - + { + const unwrapGasLimit = useUnwrapGasLimit(); + const unwrapTxCostInUsd = useTxCostInUsd(unwrapGasLimit); + + return ( + + + ${unwrapTxCostInUsd?.toFixed(2)} + + + + + + + ); +}; diff --git a/features/wrap/features/unwrap-form/utils/unwrap-processing.ts b/features/wrap/features/unwrap-form/utils/unwrap-processing.ts new file mode 100644 index 000000000..822af72b9 --- /dev/null +++ b/features/wrap/features/unwrap-form/utils/unwrap-processing.ts @@ -0,0 +1,102 @@ +import { parseEther } from '@ethersproject/units'; +import { BigNumber } from 'ethers'; +import { WstethAbi } from '@lido-sdk/contracts'; +import { CHAINS } from '@lido-sdk/constants'; +import { TX_STAGE } from 'shared/components'; +import { getErrorMessage, runWithTransactionLogger } from 'utils'; +import { getFeeData } from 'utils/getFeeData'; +import invariant from 'tiny-invariant'; +import type { Web3Provider } from '@ethersproject/providers'; + +type UnwrapProcessingProps = ( + providerWeb3: Web3Provider | undefined, + stethContractWeb3: WstethAbi | null, + openTxModal: () => void, + setTxStage: (value: TX_STAGE) => void, + setTxHash: (value: string | undefined) => void, + setTxModalFailedText: (value: string) => void, + wstethBalanceUpdate: () => Promise, + stethBalanceUpdate: () => Promise, + chainId: string | number | undefined, + inputValue: string, + resetForm: () => void, + isMultisig: boolean, +) => Promise; + +export const unwrapProcessing: UnwrapProcessingProps = async ( + providerWeb3, + wstethContractWeb3, + openTxModal, + setTxStage, + setTxHash, + setTxModalFailedText, + wstethBalanceUpdate, + stethBalanceUpdate, + chainId, + inputValue, + resetForm, + isMultisig, +) => { + invariant(wstethContractWeb3, 'must have wstethContractWeb3'); + invariant(chainId, 'must have chain id'); + invariant(providerWeb3, 'must have providerWeb3'); + + try { + const callback = async () => { + if (isMultisig) { + const tx = await wstethContractWeb3.populateTransaction.unwrap( + parseEther(inputValue), + ); + return providerWeb3.getSigner().sendUncheckedTransaction(tx); + } else { + const { maxFeePerGas, maxPriorityFeePerGas } = await getFeeData( + chainId as CHAINS, + ); + return wstethContractWeb3.unwrap(parseEther(inputValue), { + maxPriorityFeePerGas: maxPriorityFeePerGas ?? undefined, + maxFeePerGas: maxFeePerGas ?? undefined, + }); + } + }; + + setTxStage(TX_STAGE.SIGN); + openTxModal(); + + const transaction = await runWithTransactionLogger( + 'Unwrap signing', + callback, + ); + + const handleEnding = () => { + openTxModal(); + resetForm(); + void stethBalanceUpdate(); + void wstethBalanceUpdate(); + }; + + if (isMultisig) { + setTxStage(TX_STAGE.SUCCESS_MULTISIG); + handleEnding(); + return; + } + + if (typeof transaction === 'object') { + setTxHash(transaction.hash); + setTxStage(TX_STAGE.BLOCK); + openTxModal(); + await runWithTransactionLogger('Unwrap block confirmation', async () => + transaction.wait(), + ); + } + + setTxStage(TX_STAGE.SUCCESS); + handleEnding(); + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + } catch (error: any) { + // errors are sometimes nested :( + setTxModalFailedText(getErrorMessage(error)); + setTxStage(TX_STAGE.FAIL); + setTxHash(undefined); + openTxModal(); + } +}; diff --git a/features/wrap/features/wrap-form-context/compute-wrap-form-context-values.ts b/features/wrap/features/wrap-form-context/compute-wrap-form-context-values.ts new file mode 100644 index 000000000..6286c09a4 --- /dev/null +++ b/features/wrap/features/wrap-form-context/compute-wrap-form-context-values.ts @@ -0,0 +1,21 @@ +import { TOKENS_TO_WRAP, TokensWrappable } from 'features/wrap/types'; +import { WrapFormNetworkData } from './types'; + +type WrapFormComputeValuesArgs = { + token: TokensWrappable; + networkData: WrapFormNetworkData; +}; + +export const computeWrapFormContextValues = ({ + token, + networkData, +}: WrapFormComputeValuesArgs) => { + const { maxAmountStETH, maxAmountETH, gasLimitStETH, gasLimitETH } = + networkData; + const isSteth = token === TOKENS_TO_WRAP.STETH; + return { + isSteth, + maxAmount: isSteth ? maxAmountStETH : maxAmountETH, + wrapGasLimit: isSteth ? gasLimitStETH : gasLimitETH, + }; +}; diff --git a/features/wrap/features/wrap-form-context/index.ts b/features/wrap/features/wrap-form-context/index.ts new file mode 100644 index 000000000..0c9fccdda --- /dev/null +++ b/features/wrap/features/wrap-form-context/index.ts @@ -0,0 +1,2 @@ +export * from './wrap-form-context'; +export * from './types'; diff --git a/features/wrap/features/wrap-form-context/types.ts b/features/wrap/features/wrap-form-context/types.ts new file mode 100644 index 000000000..a116337ef --- /dev/null +++ b/features/wrap/features/wrap-form-context/types.ts @@ -0,0 +1,27 @@ +import type { BigNumber } from 'ethers'; +import type { TokensWrappable } from 'features/wrap/types'; +import type { useWrapFormNetworkData } from './use-wrap-form-network-data'; +import type { computeWrapFormContextValues } from './compute-wrap-form-context-values'; +import type { useWrapTxApprove } from '../wrap-form/hooks/use-wrap-tx-approve'; + +export type WrapFormInputType = { + amount: null | BigNumber; + token: TokensWrappable; +}; + +export type WrapFormNetworkData = ReturnType< + typeof useWrapFormNetworkData +>['networkData']; + +export type WrapFormApprovalData = ReturnType; + +export type WrapFormComputedContextValues = ReturnType< + typeof computeWrapFormContextValues +>; + +export type WrapFormDataContextValueType = WrapFormNetworkData & + WrapFormApprovalData & + WrapFormComputedContextValues & { + willReceiveWsteth?: BigNumber; + onSubmit: NonNullable['onSubmit']>; + }; diff --git a/features/wrap/features/wrap-form-context/use-wrap-form-network-data.tsx b/features/wrap/features/wrap-form-context/use-wrap-form-network-data.tsx new file mode 100644 index 000000000..c3816057f --- /dev/null +++ b/features/wrap/features/wrap-form-context/use-wrap-form-network-data.tsx @@ -0,0 +1,88 @@ +import { useCallback, useMemo } from 'react'; +import { + useWSTETHBalance, + useSTETHBalance, + useEthereumBalance, +} from '@lido-sdk/react'; + +import { useWrapGasLimit } from '../wrap-form/hooks/use-wrap-gas-limit'; +import { useIsMultisig } from 'shared/hooks/useIsMultisig'; +import { useCurrencyMaxAmount } from 'shared/forms/hooks/useCurrencyMaxAmount'; + +import { STRATEGY_LAZY } from 'utils/swrStrategies'; +import { TOKENS_TO_WRAP } from 'features/wrap/types'; +import { parseEther } from '@ethersproject/units'; +import { useAwaiter } from 'shared/hooks/use-awaiter'; + +// Provides all data fetching for form to function +export const useWrapFormNetworkData = () => { + const [isMultisig, isLoadingMultisig] = useIsMultisig(); + const { data: ethBalance, update: ethBalanceUpdate } = useEthereumBalance( + undefined, + STRATEGY_LAZY, + ); + const { data: stethBalance, update: stethBalanceUpdate } = useSTETHBalance(); + const { data: wstethBalance, update: wstethBalanceUpdate } = + useWSTETHBalance(); + + const { gasLimitETH, gasLimitStETH } = useWrapGasLimit(); + + const maxAmountETH = useCurrencyMaxAmount({ + limit: ethBalance, + token: TOKENS_TO_WRAP.ETH, + padded: isMultisig, + gasLimit: gasLimitETH, + }); + + const revalidateWrapFormData = useCallback(async () => { + void ethBalanceUpdate(); + void stethBalanceUpdate(); + void wstethBalanceUpdate(); + }, [ethBalanceUpdate, stethBalanceUpdate, wstethBalanceUpdate]); + + const networkData = useMemo( + () => ({ + isMultisig, + ethBalance, + stethBalance, + wstethBalance, + revalidateWrapFormData, + gasLimitETH, + gasLimitStETH, + maxAmountETH: parseEther(maxAmountETH), + maxAmountStETH: stethBalance, + }), + [ + isMultisig, + ethBalance, + stethBalance, + wstethBalance, + revalidateWrapFormData, + gasLimitETH, + gasLimitStETH, + maxAmountETH, + ], + ); + + const networkDataAwaited = useMemo(() => { + if ( + isLoadingMultisig || + !networkData.stethBalance || + !networkData.wstethBalance || + !networkData.gasLimitETH || + !networkData.gasLimitStETH || + !networkData.maxAmountETH || + !networkData.maxAmountStETH + ) { + return undefined; + } + return networkData; + }, [isLoadingMultisig, networkData]); + + const networkDataAwaiter = useAwaiter(networkDataAwaited); + + return { + networkData, + networkDataPromise: networkDataAwaiter.awaiter, + }; +}; diff --git a/features/wrap/features/wrap-form-context/wrap-form-context.tsx b/features/wrap/features/wrap-form-context/wrap-form-context.tsx new file mode 100644 index 000000000..d2164c9c4 --- /dev/null +++ b/features/wrap/features/wrap-form-context/wrap-form-context.tsx @@ -0,0 +1,110 @@ +import invariant from 'tiny-invariant'; +import { useMemo, createContext, useContext, useEffect } from 'react'; +import { useForm, FormProvider } from 'react-hook-form'; +import { useWstethBySteth } from 'shared/hooks'; +import { useWeb3 } from 'reef-knot/web3-react'; +import { useWrapTxApprove } from '../wrap-form/hooks/use-wrap-tx-approve'; + +import { + WrapFormDataContextValueType, + WrapFormInputType, + WrapFormNetworkData, +} from './types'; +import { useWrapFormNetworkData } from './use-wrap-form-network-data'; +import { useTransactionModal } from 'features/withdrawals/contexts/transaction-modal-context'; +import { useWrapFormProcessor } from '../wrap-form/hooks/use-wrap-form-processing'; +import { WrapFormValidationResolver } from './wrap-form-validators'; +import { TOKENS_TO_WRAP } from 'features/wrap/types'; +import { Zero } from '@ethersproject/constants'; +import { computeWrapFormContextValues } from './compute-wrap-form-context-values'; + +// +// Data context +// +const WrapFormDataContext = createContext( + null, +); +WrapFormDataContext.displayName = 'WrapFormDataContext'; + +export const useWrapFormData = () => { + const value = useContext(WrapFormDataContext); + invariant(value, 'useWrapFormData was used outside the provider'); + return value; +}; + +// +// Data provider +// +export const WrapFormProvider: React.FC = ({ children }) => { + const { active } = useWeb3(); + const { networkData, networkDataPromise } = useWrapFormNetworkData(); + + const formObject = useForm>({ + defaultValues: { + amount: null, + token: TOKENS_TO_WRAP.STETH, + }, + context: networkDataPromise, + criteriaMode: 'firstError', + mode: 'onChange', + resolver: WrapFormValidationResolver, + }); + + const { handleSubmit, reset, watch } = formObject; + const [token, amount] = watch(['token', 'amount']); + + const approvalData = useWrapTxApprove({ amount: amount ?? Zero, token }); + const processWrapFormFlow = useWrapFormProcessor({ approvalData }); + + const { revalidateWrapFormData } = networkData; + const onSubmit = useMemo( + () => + handleSubmit(async ({ token, amount }) => { + const success = await processWrapFormFlow({ token, amount }); + if (success) { + void revalidateWrapFormData(); + reset(); + } + }), + [handleSubmit, revalidateWrapFormData, processWrapFormFlow, reset], + ); + + const { dispatchModalState } = useTransactionModal(); + + useEffect(() => { + dispatchModalState({ type: 'set_on_retry', callback: onSubmit }); + }, [dispatchModalState, onSubmit]); + + // Reset form amount after disconnect wallet + useEffect(() => { + if (!active) reset(); + }, [active, reset]); + + const willReceiveWsteth = useWstethBySteth( + token === TOKENS_TO_WRAP.STETH && approvalData.isApprovalNeededBeforeWrap + ? Zero + : amount ?? Zero, + ); + + const value = useMemo( + (): WrapFormDataContextValueType => ({ + ...networkData, + ...approvalData, + ...computeWrapFormContextValues({ + networkData, + token, + }), + willReceiveWsteth, + onSubmit, + }), + [networkData, approvalData, token, willReceiveWsteth, onSubmit], + ); + + return ( + + + {children} + + + ); +}; diff --git a/features/wrap/features/wrap-form-context/wrap-form-validators.ts b/features/wrap/features/wrap-form-context/wrap-form-validators.ts new file mode 100644 index 000000000..190b8a048 --- /dev/null +++ b/features/wrap/features/wrap-form-context/wrap-form-validators.ts @@ -0,0 +1,52 @@ +import invariant from 'tiny-invariant'; +import { Resolver } from 'react-hook-form'; +import { validateEtherAmount } from 'shared/hook-form/validate-ether-amount'; +import { handleResolverValidationError } from 'shared/hook-form/validation-error'; +import { computeWrapFormContextValues } from './compute-wrap-form-context-values'; +import { WrapFormInputType, WrapFormNetworkData } from './types'; +import { awaitWithTimeout } from 'utils/await-with-timeout'; +import { VALIDATION_CONTEXT_TIMEOUT } from 'features/withdrawals/withdrawals-constants'; +import { validateBignumberMax } from 'shared/hook-form/validate-bignumber-max'; +import { getTokenDisplayName } from 'utils/getTokenDisplayName'; +import { BigNumber } from 'ethers'; +import { TokensWrappable } from 'features/wrap/types'; +import { formatEther } from '@ethersproject/units'; + +const messageMaxAmount = (max: BigNumber, token: TokensWrappable) => + `${getTokenDisplayName(token)} amount must not be greater than ${formatEther( + max, + )}`; + +export const WrapFormValidationResolver: Resolver< + WrapFormInputType, + Promise +> = async (values, networkDataPromise) => { + const { amount, token } = values; + try { + invariant(networkDataPromise, 'must have context promise'); + + validateEtherAmount('amount', amount, token); + + const networkData = await awaitWithTimeout( + networkDataPromise, + VALIDATION_CONTEXT_TIMEOUT, + ); + const { maxAmount } = computeWrapFormContextValues({ token, networkData }); + + invariant(maxAmount, 'maxAmount must be computed'); + + validateBignumberMax( + 'amount', + amount, + maxAmount, + messageMaxAmount(maxAmount, token), + ); + + return { + values, + errors: {}, + }; + } catch (error) { + return handleResolverValidationError(error, 'WrapForm', 'amount'); + } +}; diff --git a/features/wrap/features/wrap-form/controls/amount-input.tsx b/features/wrap/features/wrap-form/controls/amount-input.tsx new file mode 100644 index 000000000..8add38052 --- /dev/null +++ b/features/wrap/features/wrap-form/controls/amount-input.tsx @@ -0,0 +1,30 @@ +import { useController, useWatch } from 'react-hook-form'; +import { useWrapFormData, WrapFormInputType } from '../../wrap-form-context'; + +import { InputAmount } from 'shared/forms/components/input-amount'; +// import { InputDecoratorLocked } from 'shared/forms/components/input-decorator-locked'; +// import { InputDecoratorMaxButton } from 'shared/forms/components/input-decorator-max-button'; +// import { InputWrapper } from 'features/wrap/styles'; + +import { getTokenDisplayName } from 'utils/getTokenDisplayName'; + +export const AmountInput = () => { + const { maxAmount } = useWrapFormData(); + const token = useWatch({ name: 'token' }); + const { + field, + fieldState: { error }, + } = useController({ name: 'amount' }); + + return ( + + ); +}; diff --git a/features/wrap/features/wrap-form/controls/error-message-input-group.tsx b/features/wrap/features/wrap-form/controls/error-message-input-group.tsx new file mode 100644 index 000000000..cafc16acf --- /dev/null +++ b/features/wrap/features/wrap-form/controls/error-message-input-group.tsx @@ -0,0 +1,15 @@ +import { useFormState } from 'react-hook-form'; +import { WrapFormInputType } from '../../wrap-form-context'; +import { InputGroupStyled } from 'features/wrap/styles'; + +export const ErrorMessageInputGroup: React.FC = ({ children }) => { + const { + errors: { amount: amountError }, + } = useFormState({ name: 'amount' }); + const errorMessage = amountError?.type === 'validate' && amountError.message; + return ( + + {children} + + ); +}; diff --git a/features/wrap/features/wrap-form/controls/submit-button.tsx b/features/wrap/features/wrap-form/controls/submit-button.tsx new file mode 100644 index 000000000..a08878381 --- /dev/null +++ b/features/wrap/features/wrap-form/controls/submit-button.tsx @@ -0,0 +1,30 @@ +import { useWeb3 } from 'reef-knot/web3-react'; +import { useFormState } from 'react-hook-form'; +import { useWrapFormData } from '../../wrap-form-context'; + +import { ButtonIcon, Lock } from '@lidofinance/lido-ui'; +import { Connect } from 'shared/wallet'; + +import { RequestFormInputType } from 'features/withdrawals/request/request-form-context'; + +export const SubmitButton = () => { + const { active } = useWeb3(); + const { isApprovalNeededBeforeWrap } = useWrapFormData(); + const { isValidating, isSubmitting, errors } = + useFormState(); + + if (!active) return ; + + return ( + : <>} + data-testid="wrapBtn" + > + {isApprovalNeededBeforeWrap ? 'Unlock token to wrap' : 'Wrap'} + + ); +}; diff --git a/features/wrap/features/wrap-form/controls/token-input.tsx b/features/wrap/features/wrap-form/controls/token-input.tsx new file mode 100644 index 000000000..adcc155e9 --- /dev/null +++ b/features/wrap/features/wrap-form/controls/token-input.tsx @@ -0,0 +1,61 @@ +import { useController, useFormState, useFormContext } from 'react-hook-form'; + +import { SelectIcon, Option, Eth, Steth } from '@lidofinance/lido-ui'; + +import { trackEvent } from '@lidofinance/analytics-matomo'; +import { getTokenDisplayName } from 'utils/getTokenDisplayName'; +import { WrapFormInputType } from '../../wrap-form-context'; +import { TokensWrappable, TOKENS_TO_WRAP } from 'features/wrap/types'; +import { MATOMO_CLICK_EVENTS } from 'config'; + +const iconsMap = { + [TOKENS_TO_WRAP.ETH]: , + [TOKENS_TO_WRAP.STETH]: , +}; + +export const TokenInput = () => { + const { setValue } = useFormContext(); + const { field } = useController({ + name: 'token', + }); + + const { errors } = useFormState({ name: 'amount' }); + + return ( + { + setValue('token', value, { + shouldDirty: false, + shouldTouch: false, + shouldValidate: false, + }); + setValue('amount', null, { + shouldDirty: false, + shouldTouch: false, + shouldValidate: false, + }); + trackEvent( + ...(value === TOKENS_TO_WRAP.ETH + ? MATOMO_CLICK_EVENTS.wrapTokenSelectEth + : MATOMO_CLICK_EVENTS.wrapTokenSelectSteth), + ); + }} + > + + + + ); +}; diff --git a/features/wrap/features/wrap-form/form.tsx b/features/wrap/features/wrap-form/form.tsx deleted file mode 100644 index a3a0f6e7d..000000000 --- a/features/wrap/features/wrap-form/form.tsx +++ /dev/null @@ -1,265 +0,0 @@ -import { FC, useCallback, useEffect, useMemo } from 'react'; -import { - Button, - ButtonIcon, - Eth, - Lock, - Option, - Steth, -} from '@lidofinance/lido-ui'; -import { TOKENS } from '@lido-sdk/constants'; -import { - useEthereumBalance, - useSTETHBalance, - useWSTETHContractWeb3, - useSDK, -} from '@lido-sdk/react'; -import { useWeb3 } from 'reef-knot/web3-react'; -import { useCurrencyInput } from 'shared/forms/hooks/useCurrencyInput'; -import { useIsMultisig } from 'shared/hooks/useIsMultisig'; -import { wrapProcessingWithApprove } from 'features/wrap/utils'; -import { TX_OPERATION, TX_STAGE } from 'shared/components'; -import { L2Banner } from 'shared/l2-banner'; -import { MATOMO_CLICK_EVENTS } from 'config'; -import { Connect } from 'shared/wallet'; -import { InputDecoratorLocked } from 'shared/forms/components/input-decorator-locked'; -import { InputDecoratorMaxButton } from 'shared/forms/components/input-decorator-max-button'; -import { - FormStyled, - InputGroupStyled, - SelectIconWrapper, - InputWrapper, -} from 'features/wrap/styles'; -import { trackEvent } from '@lidofinance/analytics-matomo'; -import { getTokenDisplayName } from 'utils/getTokenDisplayName'; -import { STRATEGY_LAZY } from 'utils/swrStrategies'; - -const ETH = 'ETH'; - -const iconsMap = { - [ETH]: , - [TOKENS.STETH]: , -}; - -type FromProps = { - formRef: React.RefObject; - selectedToken: keyof typeof iconsMap; - setSelectedToken: (token: keyof typeof iconsMap) => void; - setWrappingAmountValue: (value: string) => void; - setTxOperation: (value: TX_OPERATION) => void; - setInputValue: (value: string) => void; - openTxModal: () => void; - setTxStage: (value: TX_STAGE) => void; - setTxHash: (value?: string) => void; - setTxModalFailedText: (value: string) => void; - needsApprove: boolean; - approve: () => Promise; - inputValue: string; - wrapGasLimit?: number; -}; - -export const Form: FC = (props) => { - const { - formRef, - selectedToken, - setSelectedToken, - setWrappingAmountValue, - setTxOperation, - openTxModal, - setTxStage, - setTxHash, - setTxModalFailedText, - needsApprove, - approve, - setInputValue, - inputValue, - wrapGasLimit, - } = props; - - const { active, account } = useWeb3(); - const { chainId, providerWeb3 } = useSDK(); - - const ethBalance = useEthereumBalance(undefined, STRATEGY_LAZY); - const stethBalance = useSTETHBalance(); - const wstethContractWeb3 = useWSTETHContractWeb3(); - const [isMultisig] = useIsMultisig(); - - const balanceBySelectedToken = useMemo(() => { - return selectedToken === ETH ? ethBalance.data : stethBalance.data; - }, [selectedToken, ethBalance.data, stethBalance.data]); - - const wrapProcessing = useCallback( - async (inputValue, resetForm) => { - // Needs for fix flashing balance in tx success modal - setWrappingAmountValue(inputValue); - - // Set operation type of transaction - setTxOperation( - needsApprove && selectedToken === TOKENS.STETH - ? TX_OPERATION.APPROVING - : TX_OPERATION.WRAPPING, - ); - - // Run approving or wrapping - await wrapProcessingWithApprove( - chainId, - providerWeb3, - wstethContractWeb3, - openTxModal, - setTxStage, - setTxHash, - setTxModalFailedText, - ethBalance.update, - stethBalance.update, - inputValue, - selectedToken, - needsApprove, - isMultisig, - approve, - resetForm, - ); - - // Needs for fix flashing balance in tx success modal - setWrappingAmountValue(''); - }, - [ - providerWeb3, - setWrappingAmountValue, - setTxOperation, - needsApprove, - selectedToken, - chainId, - wstethContractWeb3, - openTxModal, - setTxStage, - setTxHash, - setTxModalFailedText, - ethBalance.update, - stethBalance.update, - approve, - isMultisig, - ], - ); - - const inputName = `${getTokenDisplayName(selectedToken)} amount`; - - const { - handleSubmit, - handleChange, - error, - isSubmitting, - setMaxInputValue, - isMaxDisabled, - reset, - } = useCurrencyInput({ - inputValue, - inputName, - setInputValue, - submit: wrapProcessing, - limit: balanceBySelectedToken, - token: selectedToken, - gasLimit: wrapGasLimit, - padMaxAmount: !isMultisig, - }); - - const onChangeSelectToken = useCallback( - async (value) => { - if (value === selectedToken) return; - setSelectedToken(value as keyof typeof iconsMap); - setInputValue(''); - reset(); - trackEvent( - ...(value === 'ETH' - ? MATOMO_CLICK_EVENTS.wrapTokenSelectEth - : MATOMO_CLICK_EVENTS.wrapTokenSelectSteth), - ); - }, - [setSelectedToken, setInputValue, reset, selectedToken], - ); - - // Reset form amount after disconnect wallet - useEffect(() => { - if (!active) { - setInputValue(''); - reset(); - } - }, [active, reset, setInputValue]); - - const buttonProps: React.ComponentProps = { - fullwidth: true, - type: 'submit', - disabled: !!error, - loading: isSubmitting, - }; - - return ( - - - - - - - - - {account && needsApprove && selectedToken === TOKENS.STETH ? ( - - ) : ( - '' - )} - - } - label={inputName} - value={inputValue} - onChange={handleChange} - error={!!error} - /> - - {active ? ( - needsApprove && selectedToken === TOKENS.STETH ? ( - }> - Unlock token to wrap - - ) : ( - - ) - ) : ( - - )} - - - ); -}; diff --git a/features/wrap/features/wrap-form/hooks.tsx b/features/wrap/features/wrap-form/hooks.tsx deleted file mode 100644 index 2a18b3cf0..000000000 --- a/features/wrap/features/wrap-form/hooks.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { parseEther } from '@ethersproject/units'; - -import { getStaticRpcBatchProvider } from '@lido-sdk/providers'; -import { - useLidoSWR, - useSTETHContractRPC, - useWSTETHContractRPC, -} from '@lido-sdk/react'; -import { useWeb3 } from 'reef-knot/web3-react'; -import { - ESTIMATE_ACCOUNT, - getBackendRPCPath, - WRAP_FROM_ETH_GAS_LIMIT, - WRAP_GAS_LIMIT, - WRAP_GAS_LIMIT_GOERLI, - WSTETH_APPROVE_GAS_LIMIT, -} from 'config'; -import { BigNumber } from 'ethers'; -import { STRATEGY_IMMUTABLE } from 'utils/swrStrategies'; -import { CHAINS } from '@lido-sdk/constants'; - -export const useApproveGasLimit = () => { - const steth = useSTETHContractRPC(); - const wsteth = useWSTETHContractRPC(); - const { chainId } = useWeb3(); - - const { data } = useLidoSWR( - ['swr:approve-wrap-gas-limit', chainId], - async (_key, chainId) => { - if (!chainId) { - return; - } - - const gasLimit = await steth.estimateGas - .approve(wsteth.address, parseEther('0.001'), { - from: ESTIMATE_ACCOUNT, - }) - .catch((error) => { - console.warn('[swr:approve-wrap-gas-limit]', error); - return BigNumber.from(WSTETH_APPROVE_GAS_LIMIT); - }); - - return +gasLimit; - }, - STRATEGY_IMMUTABLE, - ); - - return data ?? WSTETH_APPROVE_GAS_LIMIT; -}; - -export const useWrapGasLimit = (fromEther: boolean) => { - const wsteth = useWSTETHContractRPC(); - const { chainId } = useWeb3(); - - const { data } = useLidoSWR( - ['swr:wrap-gas-limit', chainId, fromEther], - async (_key, chainId, fromEther) => { - if (!chainId) { - return; - } - - const provider = getStaticRpcBatchProvider( - chainId as CHAINS, - getBackendRPCPath(chainId as CHAINS), - ); - - if (fromEther) { - const gasLimit = await provider - .estimateGas({ - from: ESTIMATE_ACCOUNT, - to: wsteth.address, - value: parseEther('0.001'), - }) - .catch((error) => { - console.warn(error); - return BigNumber.from(WRAP_FROM_ETH_GAS_LIMIT); - }); - - return +gasLimit; - } else { - const gasLimit = await wsteth.estimateGas - .wrap(parseEther('0.0001'), { - from: ESTIMATE_ACCOUNT, - }) - .catch((error) => { - console.warn(error); - return BigNumber.from( - chainId === CHAINS.Goerli - ? WRAP_GAS_LIMIT_GOERLI - : WRAP_GAS_LIMIT, - ); - }); - - return +gasLimit; - } - }, - ); - - if (!data) { - if (fromEther) { - return WRAP_FROM_ETH_GAS_LIMIT; - } else { - if (chainId === CHAINS.Goerli) { - return WRAP_GAS_LIMIT_GOERLI; - } else { - return WRAP_GAS_LIMIT; - } - } - } - - return data; -}; diff --git a/features/wrap/features/wrap-form/hooks/use-approve-gas-limit.tsx b/features/wrap/features/wrap-form/hooks/use-approve-gas-limit.tsx new file mode 100644 index 000000000..9cb3f1f47 --- /dev/null +++ b/features/wrap/features/wrap-form/hooks/use-approve-gas-limit.tsx @@ -0,0 +1,41 @@ +import { parseEther } from '@ethersproject/units'; + +import { + useLidoSWR, + useSTETHContractRPC, + useWSTETHContractRPC, +} from '@lido-sdk/react'; +import { useWeb3 } from 'reef-knot/web3-react'; +import { ESTIMATE_ACCOUNT, WSTETH_APPROVE_GAS_LIMIT } from 'config'; +import { BigNumber } from 'ethers'; +import { STRATEGY_IMMUTABLE } from 'utils/swrStrategies'; + +export const useApproveGasLimit = () => { + const steth = useSTETHContractRPC(); + const wsteth = useWSTETHContractRPC(); + const { chainId } = useWeb3(); + + const { data } = useLidoSWR( + ['swr:approve-wrap-gas-limit', chainId], + async (_key, chainId) => { + if (!chainId) return; + + try { + const gasLimit = await steth.estimateGas.approve( + wsteth.address, + parseEther('0.001'), + { + from: ESTIMATE_ACCOUNT, + }, + ); + return +gasLimit; + } catch (error) { + console.warn(_key, error); + return BigNumber.from(WSTETH_APPROVE_GAS_LIMIT); + } + }, + STRATEGY_IMMUTABLE, + ); + + return data ?? WSTETH_APPROVE_GAS_LIMIT; +}; diff --git a/features/wrap/features/wrap-form/hooks/use-wrap-form-processing.ts b/features/wrap/features/wrap-form/hooks/use-wrap-form-processing.ts new file mode 100644 index 000000000..f74f39781 --- /dev/null +++ b/features/wrap/features/wrap-form/hooks/use-wrap-form-processing.ts @@ -0,0 +1,94 @@ +import invariant from 'tiny-invariant'; + +import { useCallback } from 'react'; +import { useSDK } from '@lido-sdk/react'; +import { useWeb3 } from 'reef-knot/web3-react'; +import { useWrapTxProcessing } from './use-wrap-tx-processing'; +import { useTransactionModal } from 'features/withdrawals/contexts/transaction-modal-context'; + +import { BigNumber } from 'ethers'; +import { getErrorMessage, runWithTransactionLogger } from 'utils'; +import { isContract } from 'utils/isContract'; +import { TX_STAGE } from 'features/withdrawals/shared/tx-stage-modal'; +import { TokensWrappable } from 'features/wrap/types'; +import { WrapFormApprovalData } from '../../wrap-form-context'; + +type UseWrapFormProcessorArgs = { + approvalData: WrapFormApprovalData; +}; + +type WrapFormProcessorArgs = { + amount: BigNumber | null; + token: TokensWrappable; +}; + +export const useWrapFormProcessor = ({ + approvalData, +}: UseWrapFormProcessorArgs) => { + const { account } = useWeb3(); + const { providerWeb3 } = useSDK(); + const { dispatchModalState } = useTransactionModal(); + const processWrapTx = useWrapTxProcessing(); + const { isApprovalNeededBeforeWrap, processApproveTx } = approvalData; + + return useCallback( + async ({ amount, token }: WrapFormProcessorArgs) => { + invariant(amount, 'amount should be presented'); + invariant(account, 'address should be presented'); + invariant(providerWeb3, 'provider should be presented'); + const isMultisig = await isContract(account, providerWeb3); + + try { + dispatchModalState({ + type: 'start', + flow: isApprovalNeededBeforeWrap ? TX_STAGE.APPROVE : TX_STAGE.SIGN, + token: token as any, // TODO: refactor modal state to be reusable to remove any + requestAmount: amount, + }); + + if (isApprovalNeededBeforeWrap) { + await processApproveTx(); + if (isMultisig) { + dispatchModalState({ type: 'success_multisig' }); + return true; + } + dispatchModalState({ type: 'signing' }); + } + + const transaction = await runWithTransactionLogger('Wrap signing', () => + processWrapTx({ amount, token, isMultisig }), + ); + + if (isMultisig) { + dispatchModalState({ type: 'success_multisig' }); + return true; + } + + if (typeof transaction === 'object') { + dispatchModalState({ type: 'block', txHash: transaction.hash }); + await runWithTransactionLogger('Wrap block confirmation', () => + transaction.wait(), + ); + } + + dispatchModalState({ type: 'success' }); + return true; + } catch (error) { + console.warn(error); + dispatchModalState({ + type: 'error', + errorText: getErrorMessage(error), + }); + return false; + } + }, + [ + account, + providerWeb3, + dispatchModalState, + isApprovalNeededBeforeWrap, + processApproveTx, + processWrapTx, + ], + ); +}; diff --git a/features/wrap/features/wrap-form/hooks/use-wrap-gas-limit.ts b/features/wrap/features/wrap-form/hooks/use-wrap-gas-limit.ts new file mode 100644 index 000000000..ef3983864 --- /dev/null +++ b/features/wrap/features/wrap-form/hooks/use-wrap-gas-limit.ts @@ -0,0 +1,75 @@ +import { parseEther } from '@ethersproject/units'; + +import { getStaticRpcBatchProvider } from '@lido-sdk/providers'; +import { useLidoSWR, useWSTETHContractRPC } from '@lido-sdk/react'; +import { useWeb3 } from 'reef-knot/web3-react'; +import { + ESTIMATE_ACCOUNT, + getBackendRPCPath, + WRAP_FROM_ETH_GAS_LIMIT, + WRAP_GAS_LIMIT, + WRAP_GAS_LIMIT_GOERLI, +} from 'config'; +import { BigNumber } from 'ethers'; +import { CHAINS } from '@lido-sdk/constants'; + +export const useWrapGasLimit = () => { + const wsteth = useWSTETHContractRPC(); + const { chainId } = useWeb3(); + + const { data } = useLidoSWR( + ['[swr:wrap-gas-limit]', chainId], + async (_key, chainId) => { + if (!chainId) return; + + const provider = getStaticRpcBatchProvider( + chainId as CHAINS, + getBackendRPCPath(chainId as CHAINS), + ); + + const fetchGasLimitETH = async () => { + try { + return await provider.estimateGas({ + from: ESTIMATE_ACCOUNT, + to: wsteth.address, + value: parseEther('0.001'), + }); + } catch (error) { + console.warn(`${_key}::[eth]`, error); + return BigNumber.from(WRAP_FROM_ETH_GAS_LIMIT); + } + }; + + const fetchGasLimitStETH = async () => { + try { + return await wsteth.estimateGas.wrap(parseEther('0.0001'), { + from: ESTIMATE_ACCOUNT, + }); + } catch (error) { + console.warn(`${_key}::[steth]`, error); + return BigNumber.from( + chainId === CHAINS.Goerli ? WRAP_GAS_LIMIT_GOERLI : WRAP_GAS_LIMIT, + ); + } + }; + + const [gasLimitETH, gasLimitStETH] = await Promise.all([ + fetchGasLimitETH(), + fetchGasLimitStETH(), + ]); + + return { + gasLimitETH, + gasLimitStETH, + }; + }, + ); + + return { + gasLimitETH: data?.gasLimitETH || BigNumber.from(WRAP_FROM_ETH_GAS_LIMIT), + gasLimitStETH: + data?.gasLimitStETH || chainId === CHAINS.Goerli + ? BigNumber.from(WRAP_GAS_LIMIT_GOERLI) + : BigNumber.from(WRAP_GAS_LIMIT), + }; +}; diff --git a/features/wrap/features/wrap-form/hooks/use-wrap-tx-approve.ts b/features/wrap/features/wrap-form/hooks/use-wrap-tx-approve.ts new file mode 100644 index 000000000..cc33dbfea --- /dev/null +++ b/features/wrap/features/wrap-form/hooks/use-wrap-tx-approve.ts @@ -0,0 +1,58 @@ +import { useMemo } from 'react'; +import { useWeb3 } from 'reef-knot/web3-react'; +import { useSDK } from '@lido-sdk/react'; +import { useApprove } from 'shared/hooks/useApprove'; + +import type { BigNumber } from 'ethers'; +import { getTokenAddress, TOKENS } from '@lido-sdk/constants'; +import { TokensWrappable, TOKENS_TO_WRAP } from 'features/wrap/types'; + +type UseWrapTxApproveArgs = { + amount: BigNumber; + token: TokensWrappable; +}; + +export const useWrapTxApprove = ({ amount, token }: UseWrapTxApproveArgs) => { + const { account } = useWeb3(); + const { chainId } = useSDK(); + + const [stethTokenAddress, wstethTokenAddress] = useMemo( + () => [ + getTokenAddress(chainId, TOKENS.STETH), + getTokenAddress(chainId, TOKENS.WSTETH), + ], + [chainId], + ); + + const { + approve: processApproveTx, + needsApprove, + allowance, + loading: isApprovalLoading, + } = useApprove( + amount, + stethTokenAddress, + wstethTokenAddress, + account ? account : undefined, + ); + + const isApprovalNeededBeforeWrap = + needsApprove && token === TOKENS_TO_WRAP.STETH; + + return useMemo( + () => ({ + processApproveTx, + needsApprove, + allowance, + isApprovalLoading, + isApprovalNeededBeforeWrap, + }), + [ + allowance, + isApprovalNeededBeforeWrap, + needsApprove, + isApprovalLoading, + processApproveTx, + ], + ); +}; diff --git a/features/wrap/features/wrap-form/hooks/use-wrap-tx-processing.ts b/features/wrap/features/wrap-form/hooks/use-wrap-tx-processing.ts new file mode 100644 index 000000000..e44075be4 --- /dev/null +++ b/features/wrap/features/wrap-form/hooks/use-wrap-tx-processing.ts @@ -0,0 +1,63 @@ +import invariant from 'tiny-invariant'; + +import { useCallback } from 'react'; +import { useSDK, useWSTETHContractWeb3 } from '@lido-sdk/react'; + +import { CHAINS } from '@lido-sdk/constants'; +import { getFeeData } from 'utils/getFeeData'; +import { getTokenAddress, TOKENS } from '@lido-sdk/constants'; +import { BigNumber } from 'ethers'; + +export const getGasParameters = async (chainId: CHAINS) => { + const feeData = await getFeeData(chainId); + return { + maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ?? undefined, + maxFeePerGas: feeData.maxFeePerGas ?? undefined, + }; +}; + +type WrapTxProcessorArgs = { + isMultisig: boolean; + amount: BigNumber; + token: string; +}; + +export const useWrapTxProcessing = () => { + const { chainId, providerWeb3 } = useSDK(); + const wstethContractWeb3 = useWSTETHContractWeb3(); + + return useCallback( + async ({ isMultisig, amount, token }: WrapTxProcessorArgs) => { + invariant(chainId, 'must have chain id'); + invariant(providerWeb3, 'must have providerWeb3'); + invariant(wstethContractWeb3, 'must have wstethContractWeb3'); + + if (token === TOKENS.STETH) { + if (isMultisig) { + const tx = await wstethContractWeb3.populateTransaction.wrap(amount); + return providerWeb3.getSigner().sendUncheckedTransaction(tx); + } else { + return wstethContractWeb3.wrap( + amount, + await getGasParameters(chainId), + ); + } + } else { + const wstethTokenAddress = getTokenAddress(chainId, TOKENS.WSTETH); + if (isMultisig) { + return providerWeb3.getSigner().sendUncheckedTransaction({ + to: wstethTokenAddress, + value: amount, + }); + } else { + return wstethContractWeb3.signer.sendTransaction({ + to: wstethTokenAddress, + value: amount, + ...(await getGasParameters(chainId)), + }); + } + } + }, + [chainId, providerWeb3, wstethContractWeb3], + ); +}; diff --git a/features/wrap/features/wrap-form/wrap-form-tx-modal.tsx b/features/wrap/features/wrap-form/wrap-form-tx-modal.tsx new file mode 100644 index 000000000..dab8ed815 --- /dev/null +++ b/features/wrap/features/wrap-form/wrap-form-tx-modal.tsx @@ -0,0 +1,66 @@ +import { useTransactionModal } from 'features/withdrawals/contexts/transaction-modal-context'; +import { useFormContext } from 'react-hook-form'; +import { useWrapFormData, WrapFormInputType } from '../wrap-form-context'; + +import { formatBalance } from 'utils'; +import { TxStageModal } from 'shared/components'; + +import { TX_STAGE } from 'features/withdrawals/shared/tx-stage-modal'; +import { + TX_STAGE as TX_STAGE_LEGACY, + TX_OPERATION as TX_OPERATION_LEGACY, +} from 'shared/components/tx-stage-modal'; +import { getTokenDisplayName } from 'utils/getTokenDisplayName'; + +const convertTxStageToLegacy = (txStage: TX_STAGE) => { + switch (txStage) { + case TX_STAGE.SIGN: + case TX_STAGE.APPROVE: + case TX_STAGE.PERMIT: + return TX_STAGE_LEGACY.SIGN; + case TX_STAGE.BLOCK: + return TX_STAGE_LEGACY.BLOCK; + case TX_STAGE.FAIL: + return TX_STAGE_LEGACY.FAIL; + case TX_STAGE.SUCCESS: + return TX_STAGE_LEGACY.SUCCESS; + case TX_STAGE.SUCCESS_MULTISIG: + return TX_STAGE_LEGACY.SUCCESS_MULTISIG; + case TX_STAGE.NONE: + case TX_STAGE.BUNKER: + return TX_STAGE_LEGACY.IDLE; + } +}; + +const convertTxStageToLegacyTxOperation = (txStage: TX_STAGE) => { + if (txStage === TX_STAGE.APPROVE) return TX_OPERATION_LEGACY.APPROVING; + return TX_OPERATION_LEGACY.WRAPPING; +}; + +export const WrapFormTxModal = () => { + const { watch } = useFormContext(); + const { allowance, wstethBalance, willReceiveWsteth } = useWrapFormData(); + const { dispatchModalState, onRetry, ...modalState } = useTransactionModal(); + const [token] = watch(['token']); + + return ( + dispatchModalState({ type: 'close_modal' })} + txStage={convertTxStageToLegacy(modalState.txStage)} + txOperation={convertTxStageToLegacyTxOperation(modalState.txStage)} + txHash={modalState.txHash} + amount={ + modalState.requestAmount ? formatBalance(modalState.requestAmount) : '' + } + amountToken={getTokenDisplayName(token)} + willReceiveAmount={formatBalance(willReceiveWsteth)} + willReceiveAmountToken="wstETH" + balance={wstethBalance} + balanceToken={'wstETH'} + allowanceAmount={allowance} + failedText={modalState.errorText} + onRetry={() => onRetry?.()} + /> + ); +}; diff --git a/features/wrap/features/wrap-form/wrap-form-with-providers.tsx b/features/wrap/features/wrap-form/wrap-form-with-providers.tsx new file mode 100644 index 000000000..95a16a1f7 --- /dev/null +++ b/features/wrap/features/wrap-form/wrap-form-with-providers.tsx @@ -0,0 +1,13 @@ +import { WrapForm } from './wrap-form'; +import { WrapFormProvider } from '../wrap-form-context/wrap-form-context'; +import { TransactionModalProvider } from 'features/withdrawals/contexts/transaction-modal-context'; + +export const WrapFormWithProviders = () => { + return ( + + + + + + ); +}; diff --git a/features/wrap/features/wrap-form/wrap-form.tsx b/features/wrap/features/wrap-form/wrap-form.tsx index 9181d3b63..ac0022f05 100644 --- a/features/wrap/features/wrap-form/wrap-form.tsx +++ b/features/wrap/features/wrap-form/wrap-form.tsx @@ -1,236 +1,33 @@ -import React, { FC, memo, useCallback, useMemo, useState, useRef } from 'react'; -import { - Block, - DataTable, - DataTableRow, - Eth, - Steth, -} from '@lidofinance/lido-ui'; -import { getTokenAddress, TOKENS } from '@lido-sdk/constants'; -import { useWeb3 } from 'reef-knot/web3-react'; -import { useSDK, useWSTETHBalance } from '@lido-sdk/react'; -import { parseEther } from '@ethersproject/units'; -import { TxStageModal, TX_OPERATION, TX_STAGE } from 'shared/components'; -import { useTxCostInUsd, useWstethBySteth } from 'shared/hooks'; -import { useIsMultisig } from 'shared/hooks/useIsMultisig'; -import { - formatBalance, - getErrorMessage, - runWithTransactionLogger, -} from 'utils'; +import { memo } from 'react'; -import { FormatToken } from 'shared/formatters'; -import { useApproveGasLimit, useWrapGasLimit } from './hooks'; -import { useApprove } from 'shared/hooks/useApprove'; -import { Form } from './form'; +import { WrapFormStats } from './wrap-stats'; +import { WrapFormTxModal } from './wrap-form-tx-modal'; -const ETH = 'ETH'; +import { useWrapFormData } from '../wrap-form-context'; +import { MATOMO_CLICK_EVENTS } from 'config'; +import { Block } from '@lidofinance/lido-ui'; +import { L2Banner } from 'shared/l2-banner'; +import { FormStyled } from 'features/wrap/styles'; -const iconsMap = { - [ETH]: , - [TOKENS.STETH]: , -}; - -export const WrapForm: FC = memo(() => { - const { account } = useWeb3(); - const { chainId } = useSDK(); - - const wstethBalance = useWSTETHBalance(); - - const formRef = useRef(null); - - const [selectedToken, setSelectedToken] = useState( - TOKENS.STETH, - ); - - const [inputValue, setInputValue] = useState(''); - // Needs for fix flashing balance in tx success modal - const [wrappingAmountValue, setWrappingAmountValue] = useState(''); - const [txModalOpen, setTxModalOpen] = useState(false); - const [txStage, setTxStage] = useState(TX_STAGE.SUCCESS); - const [txOperation, setTxOperation] = useState(TX_OPERATION.STAKING); - const [txHash, setTxHash] = useState(); - const [txModalFailedText, setTxModalFailedText] = useState(''); - - const inputValueAsBigNumber = useMemo(() => { - try { - return parseEther(inputValue ? inputValue : '0'); - } catch { - return parseEther('0'); - } - }, [inputValue]); - - const stethTokenAddress = useMemo( - () => getTokenAddress(chainId, TOKENS.STETH), - [chainId], - ); - - const wstethTokenAddress = useMemo( - () => getTokenAddress(chainId, TOKENS.WSTETH), - [chainId], - ); - - const oneSteth = useMemo(() => parseEther('1'), []); - - const approveGasLimit = useApproveGasLimit(); - const approveTxCostInUsd = useTxCostInUsd(approveGasLimit); - - const wrapGasLimit = useWrapGasLimit(selectedToken === ETH); - const wrapTxCostInUsd = useTxCostInUsd(wrapGasLimit); - - const oneWstethConverted = useWstethBySteth(oneSteth); - - const openTxModal = useCallback(() => { - setTxModalOpen(true); - }, []); - - const closeTxModal = useCallback(() => { - setTxModalOpen(false); - }, []); - - const [isMultisig] = useIsMultisig(); - - const approveWrapper = useCallback< - NonNullable[4]> - >( - async (callback) => { - try { - setTxStage(TX_STAGE.SIGN); - openTxModal(); - - const transaction = await runWithTransactionLogger( - 'Approve signing', - callback, - ); - - if (isMultisig) { - setTxStage(TX_STAGE.SUCCESS_MULTISIG); - openTxModal(); - return; - } - - if (typeof transaction !== 'string') { - setTxHash(transaction.hash); - setTxStage(TX_STAGE.BLOCK); - openTxModal(); - - await runWithTransactionLogger( - 'Approve block confirmation', - async () => transaction.wait(), - ); - } - - setTxStage(TX_STAGE.SUCCESS); - openTxModal(); - - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - } catch (error: any) { - setTxModalFailedText(getErrorMessage(error)); - setTxStage(TX_STAGE.FAIL); - openTxModal(); - } - }, - [openTxModal, isMultisig], - ); - - const { - approve, - needsApprove, - allowance, - loading: loadingUseApprove, - } = useApprove( - inputValueAsBigNumber, - stethTokenAddress, - wstethTokenAddress, - account ? account : undefined, - approveWrapper, - ); - - const willWrapSteth = useMemo(() => { - if (selectedToken === TOKENS.STETH && needsApprove) { - return parseEther('0'); - } - - return inputValueAsBigNumber; - }, [needsApprove, selectedToken, inputValueAsBigNumber]); - const willReceiveWsteth = useWstethBySteth(willWrapSteth); - - const isSteth = selectedToken === TOKENS.STETH; +import { TokenInput } from './controls/token-input'; +import { AmountInput } from './controls/amount-input'; +import { SubmitButton } from './controls/submit-button'; +import { ErrorMessageInputGroup } from './controls/error-message-input-group'; +export const WrapForm: React.FC = memo(() => { + const { onSubmit } = useWrapFormData(); return ( -
- - - - ${approveTxCostInUsd?.toFixed(2)} - - - ${wrapTxCostInUsd?.toFixed(2)} - - - 1 {isSteth ? 'stETH' : 'ETH'} ={' '} - - - - {isSteth ? : <>-} - - - - - - - formRef.current?.requestSubmit()} - /> + + + + + + + + + + ); }); diff --git a/features/wrap/features/wrap-form/wrap-stats.tsx b/features/wrap/features/wrap-form/wrap-stats.tsx new file mode 100644 index 000000000..42d330608 --- /dev/null +++ b/features/wrap/features/wrap-form/wrap-stats.tsx @@ -0,0 +1,69 @@ +import { useMemo } from 'react'; +import { parseEther } from '@ethersproject/units'; + +import { useFormContext } from 'react-hook-form'; +import { useTxCostInUsd, useWstethBySteth } from 'shared/hooks'; +import { useApproveGasLimit } from './hooks/use-approve-gas-limit'; +import { useWrapFormData, WrapFormInputType } from '../wrap-form-context'; + +import { DataTable, DataTableRow } from '@lidofinance/lido-ui'; +import { FormatToken } from 'shared/formatters'; +import { TOKENS_TO_WRAP } from 'features/wrap/types'; + +export const WrapFormStats = () => { + const { allowance, wrapGasLimit, willReceiveWsteth, isApprovalLoading } = + useWrapFormData(); + + const { watch } = useFormContext(); + const [token] = watch(['token']); + const isSteth = token === TOKENS_TO_WRAP.STETH; + + const oneSteth = useMemo(() => parseEther('1'), []); + const oneWstethConverted = useWstethBySteth(oneSteth); + + const approveGasLimit = useApproveGasLimit(); + const approveTxCostInUsd = useTxCostInUsd(Number(approveGasLimit)); + + const wrapTxCostInUsd = useTxCostInUsd(wrapGasLimit && Number(wrapGasLimit)); + + return ( + + + ${approveTxCostInUsd?.toFixed(2)} + + + ${wrapTxCostInUsd?.toFixed(2)} + + + 1 {isSteth ? 'stETH' : 'ETH'} ={' '} + + + + {isSteth ? : <>-} + + + + + + ); +}; diff --git a/features/wrap/index.ts b/features/wrap/index.ts index b7111fdbf..755a4671f 100644 --- a/features/wrap/index.ts +++ b/features/wrap/index.ts @@ -1,4 +1,3 @@ -export { WrapForm } from './features/wrap-form/wrap-form'; export { UnwrapForm } from './features/unwrap-form/unwrap-form'; export { Wallet } from './features/wallet/wallet'; export { WrapFaq } from './features/wrap-faq/wrap-faq'; diff --git a/features/wrap/styles.tsx b/features/wrap/styles.tsx index d27657494..6a78bd624 100644 --- a/features/wrap/styles.tsx +++ b/features/wrap/styles.tsx @@ -1,15 +1,7 @@ -import styled, { css } from 'styled-components'; -import { InputGroup, SelectIcon } from '@lidofinance/lido-ui'; +import styled from 'styled-components'; +import { InputGroup } from '@lidofinance/lido-ui'; import { InputNumber } from 'shared/forms/components/input-number'; -const errorCSS = css` - &, - &:hover, - &:focus-within { - border-color: var(--lido-color-error); - } -`; - export const FormStyled = styled.form` margin-bottom: 24px; `; @@ -23,13 +15,3 @@ export const InputGroupStyled = styled(InputGroup)` margin-bottom: ${({ theme }) => theme.spaceMap.md}px; z-index: 2; `; - -export const SelectIconWrapper = styled(SelectIcon)` - position: static; -`; - -export const InputWrapper = styled(InputNumber)<{ - error: boolean; -}>` - ${({ error }) => (error ? errorCSS : '')} -`; diff --git a/features/wrap/types.ts b/features/wrap/types.ts new file mode 100644 index 000000000..926c8884a --- /dev/null +++ b/features/wrap/types.ts @@ -0,0 +1,9 @@ +import { TOKENS } from '@lido-sdk/constants'; + +export const ETH = 'ETH'; +export const TOKENS_TO_WRAP = { + ETH, + [TOKENS.STETH]: TOKENS.STETH, +} as const; + +export type TokensWrappable = keyof typeof TOKENS_TO_WRAP; diff --git a/features/wrap/utils.ts b/features/wrap/utils.ts deleted file mode 100644 index 422dd9a3c..000000000 --- a/features/wrap/utils.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { parseEther } from '@ethersproject/units'; -import { BigNumber } from 'ethers'; -import { WstethAbi } from '@lido-sdk/contracts'; -import { CHAINS, getTokenAddress, TOKENS } from '@lido-sdk/constants'; -import { TX_STAGE } from 'shared/components'; -import { getErrorMessage, runWithTransactionLogger } from 'utils'; -import { getFeeData } from 'utils/getFeeData'; -import invariant from 'tiny-invariant'; -import type { Web3Provider } from '@ethersproject/providers'; - -const ETH = 'ETH'; - -type UnwrapProcessingProps = ( - providerWeb3: Web3Provider | undefined, - stethContractWeb3: WstethAbi | null, - openTxModal: () => void, - setTxStage: (value: TX_STAGE) => void, - setTxHash: (value: string | undefined) => void, - setTxModalFailedText: (value: string) => void, - wstethBalanceUpdate: () => Promise, - stethBalanceUpdate: () => Promise, - chainId: string | number | undefined, - inputValue: string, - resetForm: () => void, - isMultisig: boolean, -) => Promise; - -export const unwrapProcessing: UnwrapProcessingProps = async ( - providerWeb3, - wstethContractWeb3, - openTxModal, - setTxStage, - setTxHash, - setTxModalFailedText, - wstethBalanceUpdate, - stethBalanceUpdate, - chainId, - inputValue, - resetForm, - isMultisig, -) => { - invariant(wstethContractWeb3, 'must have wstethContractWeb3'); - invariant(chainId, 'must have chain id'); - invariant(providerWeb3, 'must have providerWeb3'); - - try { - const callback = async () => { - if (isMultisig) { - const tx = await wstethContractWeb3.populateTransaction.unwrap( - parseEther(inputValue), - ); - return providerWeb3.getSigner().sendUncheckedTransaction(tx); - } else { - const { maxFeePerGas, maxPriorityFeePerGas } = await getFeeData( - chainId as CHAINS, - ); - return wstethContractWeb3.unwrap(parseEther(inputValue), { - maxPriorityFeePerGas: maxPriorityFeePerGas ?? undefined, - maxFeePerGas: maxFeePerGas ?? undefined, - }); - } - }; - - setTxStage(TX_STAGE.SIGN); - openTxModal(); - - const transaction = await runWithTransactionLogger( - 'Unwrap signing', - callback, - ); - - const handleEnding = () => { - openTxModal(); - resetForm(); - void stethBalanceUpdate(); - void wstethBalanceUpdate(); - }; - - if (isMultisig) { - setTxStage(TX_STAGE.SUCCESS_MULTISIG); - handleEnding(); - return; - } - - if (typeof transaction === 'object') { - setTxHash(transaction.hash); - setTxStage(TX_STAGE.BLOCK); - openTxModal(); - await runWithTransactionLogger('Unwrap block confirmation', async () => - transaction.wait(), - ); - } - - setTxStage(TX_STAGE.SUCCESS); - handleEnding(); - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - } catch (error: any) { - // errors are sometimes nested :( - setTxModalFailedText(getErrorMessage(error)); - setTxStage(TX_STAGE.FAIL); - setTxHash(undefined); - openTxModal(); - } -}; - -type WrapProcessingWithApproveProps = ( - chainId: number | undefined, - providerWeb3: Web3Provider | undefined, - stethContractWeb3: WstethAbi | null, - openTxModal: () => void, - setTxStage: (value: TX_STAGE) => void, - setTxHash: (value: string | undefined) => void, - setTxModalFailedText: (value: string) => void, - ethBalanceUpdate: () => Promise, - stethBalanceUpdate: () => Promise, - inputValue: string, - selectedToken: string, - needsApprove: boolean, - isMultisig: boolean, - approve: () => Promise, - resetForm: () => void, -) => Promise; - -export const wrapProcessingWithApprove: WrapProcessingWithApproveProps = async ( - chainId, - providerWeb3, - wstethContractWeb3, - openTxModal, - setTxStage, - setTxHash, - setTxModalFailedText, - ethBalanceUpdate, - stethBalanceUpdate, - inputValue, - selectedToken, - needsApprove, - isMultisig, - approve, - resetForm, -) => { - const handleEnding = () => { - openTxModal(); - resetForm(); - void ethBalanceUpdate(); - void stethBalanceUpdate(); - }; - - const getGasParameters = async () => { - const feeData = await getFeeData(chainId as CHAINS); - return { - maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ?? undefined, - maxFeePerGas: feeData.maxFeePerGas ?? undefined, - }; - }; - - try { - invariant(providerWeb3, 'must have providerWeb3'); - invariant(wstethContractWeb3, 'must have wstethContractWeb3'); - invariant(chainId, 'must have chain id'); - - const wstethTokenAddress = getTokenAddress(chainId, TOKENS.WSTETH); - - if (selectedToken === ETH) { - const callback = async () => { - if (isMultisig) { - return providerWeb3.getSigner().sendUncheckedTransaction({ - to: wstethTokenAddress, - value: parseEther(inputValue), - }); - } else { - return wstethContractWeb3.signer.sendTransaction({ - to: wstethTokenAddress, - value: parseEther(inputValue), - ...(await getGasParameters()), - }); - } - }; - - setTxStage(TX_STAGE.SIGN); - openTxModal(); - - const transaction = await runWithTransactionLogger( - 'Wrap signing', - callback, - ); - - if (isMultisig) { - setTxStage(TX_STAGE.SUCCESS_MULTISIG); - handleEnding(); - return; - } - - if (typeof transaction === 'object') { - setTxHash(transaction.hash); - setTxStage(TX_STAGE.BLOCK); - openTxModal(); - await runWithTransactionLogger('Wrap block confirmation', async () => - transaction.wait(), - ); - } - - setTxStage(TX_STAGE.SUCCESS); - handleEnding(); - } else if (selectedToken === TOKENS.STETH) { - if (needsApprove) { - return approve(); - } else { - const callback = async () => { - if (isMultisig) { - const tx = await wstethContractWeb3.populateTransaction.wrap( - parseEther(inputValue), - ); - return providerWeb3.getSigner().sendUncheckedTransaction(tx); - } else { - return wstethContractWeb3.wrap( - parseEther(inputValue), - await getGasParameters(), - ); - } - }; - - setTxStage(TX_STAGE.SIGN); - openTxModal(); - - const transaction = await runWithTransactionLogger( - 'Wrap signing', - callback, - ); - - if (isMultisig) { - setTxStage(TX_STAGE.SUCCESS_MULTISIG); - handleEnding(); - return; - } - - if (typeof transaction === 'object') { - setTxHash(transaction.hash); - setTxStage(TX_STAGE.BLOCK); - openTxModal(); - await runWithTransactionLogger('Wrap block confirmation', async () => - transaction.wait(), - ); - } - - setTxStage(TX_STAGE.SUCCESS); - handleEnding(); - } - } - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - } catch (error: any) { - // errors are sometimes nested :( - setTxModalFailedText(getErrorMessage(error)); - setTxStage(TX_STAGE.FAIL); - setTxHash(undefined); - openTxModal(); - } -}; diff --git a/pages/wrap/[[...mode]].tsx b/pages/wrap/[[...mode]].tsx index e84244837..a5c45c020 100644 --- a/pages/wrap/[[...mode]].tsx +++ b/pages/wrap/[[...mode]].tsx @@ -3,7 +3,8 @@ import { GetServerSideProps } from 'next'; import Head from 'next/head'; import { useRouter } from 'next/router'; import { Layout } from 'shared/components'; -import { WrapForm, UnwrapForm, Wallet, WrapFaq } from 'features/wrap'; +import { UnwrapForm, Wallet, WrapFaq } from 'features/wrap'; +import { WrapFormWithProviders } from 'features/wrap/features/wrap-form/wrap-form-with-providers'; import { Switch } from 'shared/components/switch'; import { useSafeQueryString } from 'shared/hooks/useSafeQueryString'; import NoSsrWrapper from 'shared/components/no-ssr-wrapper'; @@ -38,7 +39,7 @@ const WrapPage: FC = ({ mode }) => { - {isUnwrapMode ? : } + {isUnwrapMode ? : } diff --git a/shared/components/tx-link-etherscan/tx-link-etherscan.tsx b/shared/components/tx-link-etherscan/tx-link-etherscan.tsx index b821f1f13..a09cdccec 100644 --- a/shared/components/tx-link-etherscan/tx-link-etherscan.tsx +++ b/shared/components/tx-link-etherscan/tx-link-etherscan.tsx @@ -4,7 +4,7 @@ import { getEtherscanTxLink } from '@lido-sdk/helpers'; type TxLinkEtherscanProps = { text?: string; - txHash?: string; + txHash?: string | null; onClick?: React.MouseEventHandler; }; diff --git a/shared/components/tx-stage-modal/tx-stage-modal.tsx b/shared/components/tx-stage-modal/tx-stage-modal.tsx index 7d6e9a4bb..7c1c12f7f 100644 --- a/shared/components/tx-stage-modal/tx-stage-modal.tsx +++ b/shared/components/tx-stage-modal/tx-stage-modal.tsx @@ -32,11 +32,11 @@ interface TxStageModalProps extends ModalProps { amountToken: string; willReceiveAmount?: string; willReceiveAmountToken?: string; - txHash?: string; + txHash?: string | null; balance?: BigNumber; balanceToken?: string; allowanceAmount?: BigNumber; - failedText?: string; + failedText?: string | null; onRetry: React.MouseEventHandler; } diff --git a/shared/forms/components/input-amount/input-amount.tsx b/shared/forms/components/input-amount/input-amount.tsx index 5ea146a56..882672209 100644 --- a/shared/forms/components/input-amount/input-amount.tsx +++ b/shared/forms/components/input-amount/input-amount.tsx @@ -23,7 +23,18 @@ const parseEtherSafe = (value: string) => { }; export const InputAmount = forwardRef( - ({ onChange, value, rightDecorator, isLocked, maxValue, ...props }, ref) => { + ( + { + onChange, + value, + rightDecorator, + isLocked, + maxValue, + placeholder = '0', + ...props + }, + ref, + ) => { const [stringValue, setStringValue] = useState(() => value ? formatEther(value) : '', ); @@ -75,6 +86,7 @@ export const InputAmount = forwardRef( return ( diff --git a/shared/forms/hooks/useCurrencyAmountValidator.ts b/shared/forms/hooks/useCurrencyAmountValidator.ts index 38ca94813..fb3b90f94 100644 --- a/shared/forms/hooks/useCurrencyAmountValidator.ts +++ b/shared/forms/hooks/useCurrencyAmountValidator.ts @@ -10,13 +10,11 @@ import type { ValidationFn } from 'shared/forms/types/validation-fn'; export type UseCurrencyAmountValidatorArgs = { inputName: string; limit?: BigNumber; - extraValidationFn?: ValidationFn; }; export const useCurrencyAmountValidator = ({ inputName, limit, - extraValidationFn, }: UseCurrencyAmountValidatorArgs) => { return useCallback( (value) => { @@ -44,10 +42,8 @@ export const useCurrencyAmountValidator = ({ if (limit && amountBigNumber.gt(limit)) return `${inputName} must not be greater than ${formatEther(limit)}`; - if (extraValidationFn) return extraValidationFn?.(value); - return ''; }, - [inputName, limit, extraValidationFn], + [inputName, limit], ); }; diff --git a/shared/forms/hooks/useCurrencyInput.ts b/shared/forms/hooks/useCurrencyInput.ts index f81e6067d..e15cddf40 100644 --- a/shared/forms/hooks/useCurrencyInput.ts +++ b/shared/forms/hooks/useCurrencyInput.ts @@ -5,7 +5,6 @@ import { useCurrencyAmountValidator } from 'shared/forms/hooks/useCurrencyAmount import { BigNumber } from 'ethers'; import { maxNumberValidation } from 'utils/maxNumberValidation'; -import type { ValidationFn } from 'shared/forms/types/validation-fn'; type UseCurrencyInputArgs = { inputValue: string; @@ -17,7 +16,6 @@ type UseCurrencyInputArgs = { token?: string; padMaxAmount?: boolean | ((padAmount: BigNumber) => boolean); gasLimit?: number; - extraValidationFn?: ValidationFn; shouldValidate?: boolean; }; @@ -31,16 +29,11 @@ export const useCurrencyInput = ({ token = 'ETH', padMaxAmount, gasLimit, - extraValidationFn, shouldValidate = true, }: UseCurrencyInputArgs) => { const [isSubmitting, setIsSubmitting] = useState(false); - const validationFn = useCurrencyAmountValidator({ - inputName, - limit, - extraValidationFn, - }); + const validationFn = useCurrencyAmountValidator({ inputName, limit }); const { doValidate, error, inputTouched, setInputTouched } = useInputValidate( { @@ -81,7 +74,8 @@ export const useCurrencyInput = ({ limit: limit ? limit : BigNumber.from(0), token, padded: padMaxAmount, - gasLimit, + gasLimit: + typeof gasLimit === 'number' ? BigNumber.from(gasLimit) : undefined, }); const isMaxDisabled = maxAmount === '0.0'; diff --git a/shared/forms/hooks/useCurrencyMaxAmount.ts b/shared/forms/hooks/useCurrencyMaxAmount.ts index 1d52afe34..fc563394b 100644 --- a/shared/forms/hooks/useCurrencyMaxAmount.ts +++ b/shared/forms/hooks/useCurrencyMaxAmount.ts @@ -5,14 +5,14 @@ import { useMemo } from 'react'; type UseMaxAmountArgs = { limit?: BigNumber; - gasLimit?: number; + gasLimit?: BigNumber; token?: string; padded?: boolean | ((padAmount: BigNumber) => boolean); }; export const useCurrencyMaxAmount = ({ limit, - gasLimit = 21000, + gasLimit = BigNumber.from(21000), token = 'ETH', padded = true, }: UseMaxAmountArgs): string => { @@ -27,7 +27,7 @@ export const useCurrencyMaxAmount = ({ if (!maxGasPrice) return '0.0'; const padAmount = maxGasPrice - .mul(BigNumber.from(gasLimit)) + .mul(gasLimit) .add(BigNumber.from(parseEther('0.01'))); if (typeof padded === 'function' ? padded(padAmount) : padded) { diff --git a/shared/hook-form/validate-bignumber-max.ts b/shared/hook-form/validate-bignumber-max.ts new file mode 100644 index 000000000..ff035b6d5 --- /dev/null +++ b/shared/hook-form/validate-bignumber-max.ts @@ -0,0 +1,11 @@ +import { ValidationError } from './validation-error'; +import type { BigNumber } from 'ethers'; + +export const validateBignumberMax = ( + field: string, + value: BigNumber, + max: BigNumber, + message: string, +) => { + if (value.gt(max)) throw new ValidationError(field, message); +}; diff --git a/shared/hook-form/validate-bignumber-min.ts b/shared/hook-form/validate-bignumber-min.ts new file mode 100644 index 000000000..4f4964f98 --- /dev/null +++ b/shared/hook-form/validate-bignumber-min.ts @@ -0,0 +1,11 @@ +import { ValidationError } from './validation-error'; +import type { BigNumber } from 'ethers'; + +export const validateBignumberMin = ( + field: string, + value: BigNumber, + min: BigNumber, + message: string, +) => { + if (value.lt(min)) throw new ValidationError(field, message); +}; diff --git a/shared/hook-form/validate-ether-amount.ts b/shared/hook-form/validate-ether-amount.ts new file mode 100644 index 000000000..656581d8b --- /dev/null +++ b/shared/hook-form/validate-ether-amount.ts @@ -0,0 +1,33 @@ +import type { BigNumber } from 'ethers'; +import { MaxUint256, Zero } from '@ethersproject/constants'; +import { + getTokenDisplayName, + TOKEN_DISPLAY_NAMES, +} from 'utils/getTokenDisplayName'; +import { ValidationError } from './validation-error'; + +// asserts only work with function declaration +// eslint-disable-next-line func-style +export function validateEtherAmount( + field: string, + amount: BigNumber | null, + token: keyof typeof TOKEN_DISPLAY_NAMES, +): asserts amount is BigNumber { + if (!amount) + throw new ValidationError( + field, + `${getTokenDisplayName(token)} ${field} is required`, + ); + + if (amount.lte(Zero)) + throw new ValidationError( + field, + `${getTokenDisplayName(token)} ${field} must be greater than 0`, + ); + + if (amount.gt(MaxUint256)) + throw new ValidationError( + field, + `${getTokenDisplayName(token)} ${field} is not valid`, + ); +} diff --git a/shared/hooks/useApprove.ts b/shared/hooks/useApprove.ts index 3405881e6..3a68f0629 100644 --- a/shared/hooks/useApprove.ts +++ b/shared/hooks/useApprove.ts @@ -7,12 +7,9 @@ import { Zero } from '@ethersproject/constants'; import { useAllowance, useMountedState, useSDK } from '@lido-sdk/react'; import { isContract } from 'utils/isContract'; import { getFeeData } from 'utils/getFeeData'; +import { runWithTransactionLogger } from 'utils'; -type TransactionCallback = () => Promise; - -export type UseApproveWrapper = ( - callback: TransactionCallback, -) => Promise | void; +export type TransactionCallback = () => Promise; export type UseApproveResponse = { approve: () => Promise; @@ -24,21 +21,11 @@ export type UseApproveResponse = { error: unknown; }; -const defaultWrapper: UseApproveWrapper = async ( - callback: TransactionCallback, -) => { - const result = await callback(); - if (typeof result === 'object') { - await result.wait(); - } -}; - export const useApprove = ( amount: BigNumber, token: string, spender: string, owner?: string, - wrapper: UseApproveWrapper = defaultWrapper, ): UseApproveResponse => { const { providerWeb3, account, chainId } = useSDK(); const mergedOwner = owner ?? account; @@ -65,7 +52,8 @@ export const useApprove = ( invariant(account, 'account is required'); const contractWeb3 = getERC20Contract(token, providerWeb3.getSigner()); const isMultisig = await isContract(account, providerWeb3); - await wrapper(async () => { + + const processApproveTx = async () => { if (isMultisig) { const tx = await contractWeb3.populateTransaction.approve( spender, @@ -88,10 +76,21 @@ export const useApprove = ( }); return tx; } - }); - await updateAllowance(); + }; + + const approveTx = await runWithTransactionLogger( + 'Approve signing', + processApproveTx, + ); + + if (typeof approveTx === 'object') { + await runWithTransactionLogger('Approve block confirmation', () => + approveTx.wait(), + ); + } } finally { setApproving(false); + await updateAllowance(); } }, [ setApproving, @@ -99,7 +98,6 @@ export const useApprove = ( chainId, account, token, - wrapper, updateAllowance, spender, amount, diff --git a/shared/hooks/useWstethBySteth.ts b/shared/hooks/useWstethBySteth.ts index a53b57fb3..295fd92bf 100644 --- a/shared/hooks/useWstethBySteth.ts +++ b/shared/hooks/useWstethBySteth.ts @@ -1,31 +1,15 @@ -import { useEffect, useMemo, useState } from 'react'; -import { useWSTETHContractRPC } from '@lido-sdk/react'; +import { useContractSWR, useWSTETHContractRPC } from '@lido-sdk/react'; import { BigNumber } from 'ethers'; -import debounce from 'lodash/debounce'; +import { STRATEGY_LAZY } from 'utils/swrStrategies'; export const useWstethBySteth = ( steth: BigNumber | undefined, ): BigNumber | undefined => { - const [wstethBalance, setWstethBalance] = useState(); - - const wstethContractRPC = useWSTETHContractRPC(); - - const getWstethBalance = useMemo( - () => - debounce(async (steth: BigNumber | undefined) => { - if (!steth) { - return; - } - - const wsteth = await wstethContractRPC.getWstETHByStETH(steth); - setWstethBalance(wsteth); - }, 500), - [wstethContractRPC], - ); - - useEffect(() => { - void getWstethBalance(steth); - }, [getWstethBalance, steth]); - - return wstethBalance; + return useContractSWR({ + contract: useWSTETHContractRPC(), + method: 'getWstETHByStETH', + params: [steth], + shouldFetch: !!steth, + config: STRATEGY_LAZY, + }).data; }; diff --git a/utils/await-with-timeout.ts b/utils/await-with-timeout.ts new file mode 100644 index 000000000..11969108d --- /dev/null +++ b/utils/await-with-timeout.ts @@ -0,0 +1,7 @@ +export const awaitWithTimeout = (toWait: Promise, timeout: number) => + Promise.race([ + toWait, + new Promise((_, reject) => + setTimeout(() => reject(new Error('promise timeout')), timeout), + ), + ]); From bb1be279286309a2a76a7fd064a0e08af5439764 Mon Sep 17 00:00:00 2001 From: Dmitrii Podlesnyi Date: Tue, 22 Aug 2023 16:25:53 +0400 Subject: [PATCH 02/22] chore: wrap tx modal updating balances before success stage --- features/withdrawals/hooks/contract/useRequest.ts | 4 ++++ .../request-form-context/request-form-context.tsx | 11 +++++------ .../use-request-form-data-context-value.ts | 8 ++++---- .../use-wrap-form-network-data.tsx | 8 +++++--- .../wrap-form-context/wrap-form-context.tsx | 14 +++++++------- .../wrap-form/hooks/use-wrap-form-processing.ts | 4 ++++ 6 files changed, 29 insertions(+), 20 deletions(-) diff --git a/features/withdrawals/hooks/contract/useRequest.ts b/features/withdrawals/hooks/contract/useRequest.ts index a52673ff4..3ba84fe2e 100644 --- a/features/withdrawals/hooks/contract/useRequest.ts +++ b/features/withdrawals/hooks/contract/useRequest.ts @@ -279,11 +279,13 @@ const useWithdrawalRequestMethods = () => { type useWithdrawalRequestParams = { amount: BigNumber | null; token: TOKENS.STETH | TOKENS.WSTETH; + onBeforeSuccess?: () => Promise; }; export const useWithdrawalRequest = ({ amount, token, + onBeforeSuccess, }: useWithdrawalRequestParams) => { const { chainId } = useSDK(); const withdrawalQueueAddress = getWithdrawalQueueAddress(chainId); @@ -380,6 +382,7 @@ export const useWithdrawalRequest = ({ await method({ signature, requests }); } // end flow + if (!isMultisig) await onBeforeSuccess?.(); dispatchModalState({ type: isMultisig ? 'success_multisig' : 'success', }); @@ -399,6 +402,7 @@ export const useWithdrawalRequest = ({ isBunker, isMultisig, needsApprove, + onBeforeSuccess, ], ); diff --git a/features/withdrawals/request/request-form-context/request-form-context.tsx b/features/withdrawals/request/request-form-context/request-form-context.tsx index 3c596c8ce..eb0ef93b6 100644 --- a/features/withdrawals/request/request-form-context/request-form-context.tsx +++ b/features/withdrawals/request/request-form-context/request-form-context.tsx @@ -52,7 +52,8 @@ export const RequestFormProvider: React.FC = ({ children }) => { useState({ requests: null }); const requestFormData = useRequestFormDataContextValue(); - const { balanceSteth, balanceWSteth, onSuccessRequest } = requestFormData; + const { balanceSteth, balanceWSteth, revalidateRequestFormData } = + requestFormData; const validationContext = useValidationContext( requestFormData, setIntermediateValidationResults, @@ -85,18 +86,16 @@ export const RequestFormProvider: React.FC = ({ children }) => { } = useWithdrawalRequest({ token, amount, + onBeforeSuccess: revalidateRequestFormData, }); const onSubmit = useMemo( () => handleSubmit(async ({ requests, amount, token }) => { const { success } = await request(requests, amount, token); - if (success) { - await onSuccessRequest(); - reset(); - } + if (success) reset(); }), - [reset, handleSubmit, request, onSuccessRequest], + [reset, handleSubmit, request], ); useEffect(() => { diff --git a/features/withdrawals/request/request-form-context/use-request-form-data-context-value.ts b/features/withdrawals/request/request-form-context/use-request-form-data-context-value.ts index b0c320173..5b03ecaef 100644 --- a/features/withdrawals/request/request-form-context/use-request-form-data-context-value.ts +++ b/features/withdrawals/request/request-form-context/use-request-form-data-context-value.ts @@ -43,8 +43,8 @@ export const useRequestFormDataContextValue = () => { config: STRATEGY_LAZY, }).data; - const onSuccessRequest = useCallback(() => { - return Promise.all([ + const revalidateRequestFormData = useCallback(() => { + return Promise.allSettled([ stethUpdate(), wstethUpdate(), revalidateClaimData(), @@ -62,7 +62,7 @@ export const useRequestFormDataContextValue = () => { minUnstakeWSteth, stethTotalSupply, unfinalizedStETH, - onSuccessRequest, + revalidateRequestFormData, }), [ balanceSteth, @@ -73,7 +73,7 @@ export const useRequestFormDataContextValue = () => { minUnstakeWSteth, stethTotalSupply, unfinalizedStETH, - onSuccessRequest, + revalidateRequestFormData, ], ); }; diff --git a/features/wrap/features/wrap-form-context/use-wrap-form-network-data.tsx b/features/wrap/features/wrap-form-context/use-wrap-form-network-data.tsx index c3816057f..1a457825d 100644 --- a/features/wrap/features/wrap-form-context/use-wrap-form-network-data.tsx +++ b/features/wrap/features/wrap-form-context/use-wrap-form-network-data.tsx @@ -35,9 +35,11 @@ export const useWrapFormNetworkData = () => { }); const revalidateWrapFormData = useCallback(async () => { - void ethBalanceUpdate(); - void stethBalanceUpdate(); - void wstethBalanceUpdate(); + await Promise.allSettled([ + ethBalanceUpdate(), + stethBalanceUpdate(), + wstethBalanceUpdate(), + ]); }, [ethBalanceUpdate, stethBalanceUpdate, wstethBalanceUpdate]); const networkData = useMemo( diff --git a/features/wrap/features/wrap-form-context/wrap-form-context.tsx b/features/wrap/features/wrap-form-context/wrap-form-context.tsx index d2164c9c4..0d4bf39d7 100644 --- a/features/wrap/features/wrap-form-context/wrap-form-context.tsx +++ b/features/wrap/features/wrap-form-context/wrap-form-context.tsx @@ -52,21 +52,21 @@ export const WrapFormProvider: React.FC = ({ children }) => { const { handleSubmit, reset, watch } = formObject; const [token, amount] = watch(['token', 'amount']); + const { revalidateWrapFormData } = networkData; const approvalData = useWrapTxApprove({ amount: amount ?? Zero, token }); - const processWrapFormFlow = useWrapFormProcessor({ approvalData }); + const processWrapFormFlow = useWrapFormProcessor({ + approvalData, + onBeforeSuccess: revalidateWrapFormData, + }); - const { revalidateWrapFormData } = networkData; const onSubmit = useMemo( () => handleSubmit(async ({ token, amount }) => { const success = await processWrapFormFlow({ token, amount }); - if (success) { - void revalidateWrapFormData(); - reset(); - } + if (success) reset(); }), - [handleSubmit, revalidateWrapFormData, processWrapFormFlow, reset], + [handleSubmit, processWrapFormFlow, reset], ); const { dispatchModalState } = useTransactionModal(); diff --git a/features/wrap/features/wrap-form/hooks/use-wrap-form-processing.ts b/features/wrap/features/wrap-form/hooks/use-wrap-form-processing.ts index f74f39781..15543b6be 100644 --- a/features/wrap/features/wrap-form/hooks/use-wrap-form-processing.ts +++ b/features/wrap/features/wrap-form/hooks/use-wrap-form-processing.ts @@ -15,6 +15,7 @@ import { WrapFormApprovalData } from '../../wrap-form-context'; type UseWrapFormProcessorArgs = { approvalData: WrapFormApprovalData; + onBeforeSuccess?: () => Promise; }; type WrapFormProcessorArgs = { @@ -24,6 +25,7 @@ type WrapFormProcessorArgs = { export const useWrapFormProcessor = ({ approvalData, + onBeforeSuccess, }: UseWrapFormProcessorArgs) => { const { account } = useWeb3(); const { providerWeb3 } = useSDK(); @@ -71,6 +73,7 @@ export const useWrapFormProcessor = ({ ); } + await onBeforeSuccess?.(); dispatchModalState({ type: 'success' }); return true; } catch (error) { @@ -89,6 +92,7 @@ export const useWrapFormProcessor = ({ isApprovalNeededBeforeWrap, processApproveTx, processWrapTx, + onBeforeSuccess, ], ); }; From 08e5ac0ee6f0319ea6fad46926ce49e02a4129df Mon Sep 17 00:00:00 2001 From: Dmitrii Podlesnyi Date: Tue, 22 Aug 2023 16:26:35 +0400 Subject: [PATCH 03/22] chore: wrap form amount input finishing --- features/wrap/features/wrap-form/controls/amount-input.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/features/wrap/features/wrap-form/controls/amount-input.tsx b/features/wrap/features/wrap-form/controls/amount-input.tsx index 8add38052..ca54344e3 100644 --- a/features/wrap/features/wrap-form/controls/amount-input.tsx +++ b/features/wrap/features/wrap-form/controls/amount-input.tsx @@ -2,14 +2,11 @@ import { useController, useWatch } from 'react-hook-form'; import { useWrapFormData, WrapFormInputType } from '../../wrap-form-context'; import { InputAmount } from 'shared/forms/components/input-amount'; -// import { InputDecoratorLocked } from 'shared/forms/components/input-decorator-locked'; -// import { InputDecoratorMaxButton } from 'shared/forms/components/input-decorator-max-button'; -// import { InputWrapper } from 'features/wrap/styles'; import { getTokenDisplayName } from 'utils/getTokenDisplayName'; export const AmountInput = () => { - const { maxAmount } = useWrapFormData(); + const { maxAmount, isApprovalNeededBeforeWrap } = useWrapFormData(); const token = useWatch({ name: 'token' }); const { field, @@ -21,7 +18,7 @@ export const AmountInput = () => { fullwidth data-testid="wrapInput" error={error?.type === 'validate'} - // isLocked={isTokenLocked} + isLocked={isApprovalNeededBeforeWrap} maxValue={maxAmount} label={`${getTokenDisplayName(token)} amount`} {...field} From 57b21eb6d9379282365c8fb16e36655a8e6e2d96 Mon Sep 17 00:00:00 2001 From: Dmitrii Podlesnyi Date: Tue, 22 Aug 2023 16:27:00 +0400 Subject: [PATCH 04/22] chore: wrap form components structure improved --- .../wrap-form/controls/form-controlled.tsx | 12 ++++++ .../wrap-form/wrap-form-with-providers.tsx | 13 ------ .../wrap/features/wrap-form/wrap-form.tsx | 43 ++++++++++--------- pages/wrap/[[...mode]].tsx | 4 +- 4 files changed, 37 insertions(+), 35 deletions(-) create mode 100644 features/wrap/features/wrap-form/controls/form-controlled.tsx delete mode 100644 features/wrap/features/wrap-form/wrap-form-with-providers.tsx diff --git a/features/wrap/features/wrap-form/controls/form-controlled.tsx b/features/wrap/features/wrap-form/controls/form-controlled.tsx new file mode 100644 index 000000000..239da9972 --- /dev/null +++ b/features/wrap/features/wrap-form/controls/form-controlled.tsx @@ -0,0 +1,12 @@ +import { FormStyled } from 'features/wrap/styles'; +import { useWrapFormData } from '../../wrap-form-context'; + +export const FormControlled: React.FC = ({ children }) => { + const { onSubmit } = useWrapFormData(); + + return ( + + {children} + + ); +}; diff --git a/features/wrap/features/wrap-form/wrap-form-with-providers.tsx b/features/wrap/features/wrap-form/wrap-form-with-providers.tsx deleted file mode 100644 index 95a16a1f7..000000000 --- a/features/wrap/features/wrap-form/wrap-form-with-providers.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { WrapForm } from './wrap-form'; -import { WrapFormProvider } from '../wrap-form-context/wrap-form-context'; -import { TransactionModalProvider } from 'features/withdrawals/contexts/transaction-modal-context'; - -export const WrapFormWithProviders = () => { - return ( - - - - - - ); -}; diff --git a/features/wrap/features/wrap-form/wrap-form.tsx b/features/wrap/features/wrap-form/wrap-form.tsx index ac0022f05..2faf37edd 100644 --- a/features/wrap/features/wrap-form/wrap-form.tsx +++ b/features/wrap/features/wrap-form/wrap-form.tsx @@ -1,33 +1,36 @@ import { memo } from 'react'; -import { WrapFormStats } from './wrap-stats'; -import { WrapFormTxModal } from './wrap-form-tx-modal'; - -import { useWrapFormData } from '../wrap-form-context'; -import { MATOMO_CLICK_EVENTS } from 'config'; import { Block } from '@lidofinance/lido-ui'; import { L2Banner } from 'shared/l2-banner'; -import { FormStyled } from 'features/wrap/styles'; - +import { WrapFormStats } from './wrap-stats'; +import { WrapFormTxModal } from './wrap-form-tx-modal'; +import { WrapFormProvider } from '../wrap-form-context/wrap-form-context'; +import { TransactionModalProvider } from 'features/withdrawals/contexts/transaction-modal-context'; +import { FormControlled } from './controls/form-controlled'; import { TokenInput } from './controls/token-input'; import { AmountInput } from './controls/amount-input'; import { SubmitButton } from './controls/submit-button'; import { ErrorMessageInputGroup } from './controls/error-message-input-group'; +import { MATOMO_CLICK_EVENTS } from 'config'; + export const WrapForm: React.FC = memo(() => { - const { onSubmit } = useWrapFormData(); return ( - - - - - - - - - - - - + + + + + + + + + + + + + + + + ); }); diff --git a/pages/wrap/[[...mode]].tsx b/pages/wrap/[[...mode]].tsx index a5c45c020..e01a84815 100644 --- a/pages/wrap/[[...mode]].tsx +++ b/pages/wrap/[[...mode]].tsx @@ -4,7 +4,7 @@ import Head from 'next/head'; import { useRouter } from 'next/router'; import { Layout } from 'shared/components'; import { UnwrapForm, Wallet, WrapFaq } from 'features/wrap'; -import { WrapFormWithProviders } from 'features/wrap/features/wrap-form/wrap-form-with-providers'; +import { WrapForm } from 'features/wrap/features/wrap-form/wrap-form'; import { Switch } from 'shared/components/switch'; import { useSafeQueryString } from 'shared/hooks/useSafeQueryString'; import NoSsrWrapper from 'shared/components/no-ssr-wrapper'; @@ -39,7 +39,7 @@ const WrapPage: FC = ({ mode }) => { - {isUnwrapMode ? : } + {isUnwrapMode ? : } From 99cedadc4826c1a2f6b9f8dc7fc476a54b96cf08 Mon Sep 17 00:00:00 2001 From: Dmitrii Podlesnyi Date: Tue, 22 Aug 2023 16:40:13 +0400 Subject: [PATCH 05/22] fix: request form on before success types --- .../use-request-form-data-context-value.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/withdrawals/request/request-form-context/use-request-form-data-context-value.ts b/features/withdrawals/request/request-form-context/use-request-form-data-context-value.ts index 5b03ecaef..0b45fb3d5 100644 --- a/features/withdrawals/request/request-form-context/use-request-form-data-context-value.ts +++ b/features/withdrawals/request/request-form-context/use-request-form-data-context-value.ts @@ -43,8 +43,8 @@ export const useRequestFormDataContextValue = () => { config: STRATEGY_LAZY, }).data; - const revalidateRequestFormData = useCallback(() => { - return Promise.allSettled([ + const revalidateRequestFormData = useCallback(async () => { + await Promise.allSettled([ stethUpdate(), wstethUpdate(), revalidateClaimData(), From 90d4c7f379bbdf279d5b5cb4b3ae6c8611fc5385 Mon Sep 17 00:00:00 2001 From: Dmitrii Podlesnyi Date: Tue, 22 Aug 2023 16:54:15 +0400 Subject: [PATCH 06/22] chore: validation error type refactored --- .../withdrawals/claim/form/submit-button.tsx | 6 +++++- .../request/form/inputs/amount-input.tsx | 3 ++- .../request/form/inputs/input-group.tsx | 4 +++- .../request/form/inputs/token-input.tsx | 3 ++- .../withdrawals/request/form/request-form.tsx | 6 +++++- .../wrap-form/controls/amount-input.tsx | 3 ++- .../controls/error-message-input-group.tsx | 4 +++- .../wrap-form/controls/submit-button.tsx | 7 +++++-- .../wrap-form/controls/token-input.tsx | 3 ++- shared/hook-form/validation-error.ts | 20 +++++++++++++++++-- 10 files changed, 47 insertions(+), 12 deletions(-) diff --git a/features/withdrawals/claim/form/submit-button.tsx b/features/withdrawals/claim/form/submit-button.tsx index 23f1952ed..3ad10651e 100644 --- a/features/withdrawals/claim/form/submit-button.tsx +++ b/features/withdrawals/claim/form/submit-button.tsx @@ -5,6 +5,7 @@ import { FormatToken } from 'shared/formatters/format-token'; import { ClaimFormInputType, useClaimFormData } from '../claim-form-context'; import { Zero } from '@ethersproject/constants'; import { useFormState } from 'react-hook-form'; +import { isValidationErrorTypeUnhandled } from 'shared/hook-form/validation-error'; export const SubmitButton = () => { const { active } = useWeb3(); @@ -19,7 +20,10 @@ export const SubmitButton = () => { ); - const disabled = Boolean(errors.requests) || selectedRequests.length === 0; + const disabled = + (!!errors.requests && + !isValidationErrorTypeUnhandled(errors.requests.type)) || + selectedRequests.length === 0; return ( - ) : ( - - )} - - - - - - formRef.current?.requestSubmit()} - /> -
+ + + + + + + + + + + + + + + ); }); diff --git a/features/wsteth/unwrap/unwrap-form/unwrap-stats.tsx b/features/wsteth/unwrap/unwrap-form/unwrap-stats.tsx index 4a8dd220b..edccc0fc5 100644 --- a/features/wsteth/unwrap/unwrap-form/unwrap-stats.tsx +++ b/features/wsteth/unwrap/unwrap-form/unwrap-stats.tsx @@ -1,24 +1,19 @@ -import { BigNumber } from 'ethers'; - import { useTxCostInUsd } from 'shared/hooks'; -import { useUnwrapGasLimit } from './hooks'; +import { useUnwrapGasLimit } from '../hooks/use-unwrap-gas-limit'; +import { useUnwrapFormData } from '../unwrap-form-context'; -import { DataTable, DataTableRow } from '@lidofinance/lido-ui'; +import { DataTableRow } from '@lidofinance/lido-ui'; +import { StatsDataTable } from 'features/wsteth/shared/styles'; import { FormatToken } from 'shared/formatters/format-token'; import { DataTableRowStethByWsteth } from 'shared/components/data-table-row-steth-by-wsteth'; -type UnwrapStatsProps = { - willReceiveStethAsBigNumber?: BigNumber; -}; - -export const UnwrapStats = ({ - willReceiveStethAsBigNumber, -}: UnwrapStatsProps) => { +export const UnwrapStats = () => { const unwrapGasLimit = useUnwrapGasLimit(); - const unwrapTxCostInUsd = useTxCostInUsd(unwrapGasLimit); + const unwrapTxCostInUsd = useTxCostInUsd(Number(unwrapGasLimit)); + const { willReceiveStETH } = useUnwrapFormData(); return ( - + - + ); }; diff --git a/features/wsteth/unwrap/utils/unwrap-processing.ts b/features/wsteth/unwrap/utils/unwrap-processing.ts deleted file mode 100644 index 822af72b9..000000000 --- a/features/wsteth/unwrap/utils/unwrap-processing.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { parseEther } from '@ethersproject/units'; -import { BigNumber } from 'ethers'; -import { WstethAbi } from '@lido-sdk/contracts'; -import { CHAINS } from '@lido-sdk/constants'; -import { TX_STAGE } from 'shared/components'; -import { getErrorMessage, runWithTransactionLogger } from 'utils'; -import { getFeeData } from 'utils/getFeeData'; -import invariant from 'tiny-invariant'; -import type { Web3Provider } from '@ethersproject/providers'; - -type UnwrapProcessingProps = ( - providerWeb3: Web3Provider | undefined, - stethContractWeb3: WstethAbi | null, - openTxModal: () => void, - setTxStage: (value: TX_STAGE) => void, - setTxHash: (value: string | undefined) => void, - setTxModalFailedText: (value: string) => void, - wstethBalanceUpdate: () => Promise, - stethBalanceUpdate: () => Promise, - chainId: string | number | undefined, - inputValue: string, - resetForm: () => void, - isMultisig: boolean, -) => Promise; - -export const unwrapProcessing: UnwrapProcessingProps = async ( - providerWeb3, - wstethContractWeb3, - openTxModal, - setTxStage, - setTxHash, - setTxModalFailedText, - wstethBalanceUpdate, - stethBalanceUpdate, - chainId, - inputValue, - resetForm, - isMultisig, -) => { - invariant(wstethContractWeb3, 'must have wstethContractWeb3'); - invariant(chainId, 'must have chain id'); - invariant(providerWeb3, 'must have providerWeb3'); - - try { - const callback = async () => { - if (isMultisig) { - const tx = await wstethContractWeb3.populateTransaction.unwrap( - parseEther(inputValue), - ); - return providerWeb3.getSigner().sendUncheckedTransaction(tx); - } else { - const { maxFeePerGas, maxPriorityFeePerGas } = await getFeeData( - chainId as CHAINS, - ); - return wstethContractWeb3.unwrap(parseEther(inputValue), { - maxPriorityFeePerGas: maxPriorityFeePerGas ?? undefined, - maxFeePerGas: maxFeePerGas ?? undefined, - }); - } - }; - - setTxStage(TX_STAGE.SIGN); - openTxModal(); - - const transaction = await runWithTransactionLogger( - 'Unwrap signing', - callback, - ); - - const handleEnding = () => { - openTxModal(); - resetForm(); - void stethBalanceUpdate(); - void wstethBalanceUpdate(); - }; - - if (isMultisig) { - setTxStage(TX_STAGE.SUCCESS_MULTISIG); - handleEnding(); - return; - } - - if (typeof transaction === 'object') { - setTxHash(transaction.hash); - setTxStage(TX_STAGE.BLOCK); - openTxModal(); - await runWithTransactionLogger('Unwrap block confirmation', async () => - transaction.wait(), - ); - } - - setTxStage(TX_STAGE.SUCCESS); - handleEnding(); - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - } catch (error: any) { - // errors are sometimes nested :( - setTxModalFailedText(getErrorMessage(error)); - setTxStage(TX_STAGE.FAIL); - setTxHash(undefined); - openTxModal(); - } -}; diff --git a/features/wsteth/wrap/hooks/use-approve-gas-limit.tsx b/features/wsteth/wrap/hooks/use-approve-gas-limit.tsx index 9cb3f1f47..19936ad0e 100644 --- a/features/wsteth/wrap/hooks/use-approve-gas-limit.tsx +++ b/features/wsteth/wrap/hooks/use-approve-gas-limit.tsx @@ -24,11 +24,9 @@ export const useApproveGasLimit = () => { const gasLimit = await steth.estimateGas.approve( wsteth.address, parseEther('0.001'), - { - from: ESTIMATE_ACCOUNT, - }, + { from: ESTIMATE_ACCOUNT }, ); - return +gasLimit; + return gasLimit; } catch (error) { console.warn(_key, error); return BigNumber.from(WSTETH_APPROVE_GAS_LIMIT); diff --git a/features/wsteth/wrap/hooks/use-wrap-form-network-data.tsx b/features/wsteth/wrap/hooks/use-wrap-form-network-data.tsx index dd4aebec6..acbdefbfb 100644 --- a/features/wsteth/wrap/hooks/use-wrap-form-network-data.tsx +++ b/features/wsteth/wrap/hooks/use-wrap-form-network-data.tsx @@ -8,11 +8,11 @@ import { import { useWrapGasLimit } from './use-wrap-gas-limit'; import { useIsMultisig } from 'shared/hooks/useIsMultisig'; import { useCurrencyMaxAmount } from 'shared/forms/hooks/useCurrencyMaxAmount'; +import { useAwaiter } from 'shared/hooks/use-awaiter'; import { STRATEGY_LAZY } from 'utils/swrStrategies'; import { TOKENS_TO_WRAP } from 'features/wsteth/shared/types'; import { parseEther } from '@ethersproject/units'; -import { useAwaiter } from 'shared/hooks/use-awaiter'; // Provides all data fetching for form to function export const useWrapFormNetworkData = () => { diff --git a/features/wsteth/wrap/hooks/use-wrap-form-processing.ts b/features/wsteth/wrap/hooks/use-wrap-form-processing.ts index 4cd167006..21b7f9aa0 100644 --- a/features/wsteth/wrap/hooks/use-wrap-form-processing.ts +++ b/features/wsteth/wrap/hooks/use-wrap-form-processing.ts @@ -6,23 +6,19 @@ import { useWeb3 } from 'reef-knot/web3-react'; import { useWrapTxProcessing } from './use-wrap-tx-processing'; import { useTransactionModal } from 'features/withdrawals/contexts/transaction-modal-context'; -import { BigNumber } from 'ethers'; import { getErrorMessage, runWithTransactionLogger } from 'utils'; import { isContract } from 'utils/isContract'; import { TX_STAGE } from 'features/withdrawals/shared/tx-stage-modal'; -import { TokensWrappable } from 'features/wsteth/shared/types'; -import { WrapFormApprovalData } from '../wrap-form-context'; +import type { + WrapFormApprovalData, + WrapFormInputType, +} from '../wrap-form-context'; type UseWrapFormProcessorArgs = { approvalData: WrapFormApprovalData; onConfirm?: () => Promise; }; -type WrapFormProcessorArgs = { - amount: BigNumber | null; - token: TokensWrappable; -}; - export const useWrapFormProcessor = ({ approvalData, onConfirm, @@ -34,7 +30,7 @@ export const useWrapFormProcessor = ({ const { isApprovalNeededBeforeWrap, processApproveTx } = approvalData; return useCallback( - async ({ amount, token }: WrapFormProcessorArgs) => { + async ({ amount, token }: WrapFormInputType) => { invariant(amount, 'amount should be presented'); invariant(account, 'address should be presented'); invariant(providerWeb3, 'provider should be presented'); diff --git a/features/wsteth/wrap/hooks/use-wrap-tx-processing.ts b/features/wsteth/wrap/hooks/use-wrap-tx-processing.ts index e44075be4..6d4720639 100644 --- a/features/wsteth/wrap/hooks/use-wrap-tx-processing.ts +++ b/features/wsteth/wrap/hooks/use-wrap-tx-processing.ts @@ -3,10 +3,10 @@ import invariant from 'tiny-invariant'; import { useCallback } from 'react'; import { useSDK, useWSTETHContractWeb3 } from '@lido-sdk/react'; -import { CHAINS } from '@lido-sdk/constants'; import { getFeeData } from 'utils/getFeeData'; import { getTokenAddress, TOKENS } from '@lido-sdk/constants'; -import { BigNumber } from 'ethers'; +import type { CHAINS } from '@lido-sdk/constants'; +import type { WrapFormInputType } from '../wrap-form-context'; export const getGasParameters = async (chainId: CHAINS) => { const feeData = await getFeeData(chainId); @@ -16,10 +16,8 @@ export const getGasParameters = async (chainId: CHAINS) => { }; }; -type WrapTxProcessorArgs = { +type WrapTxProcessorArgs = WrapFormInputType & { isMultisig: boolean; - amount: BigNumber; - token: string; }; export const useWrapTxProcessing = () => { @@ -28,9 +26,10 @@ export const useWrapTxProcessing = () => { return useCallback( async ({ isMultisig, amount, token }: WrapTxProcessorArgs) => { - invariant(chainId, 'must have chain id'); - invariant(providerWeb3, 'must have providerWeb3'); - invariant(wstethContractWeb3, 'must have wstethContractWeb3'); + invariant(amount, 'amount id must be presented'); + invariant(chainId, 'chain id must be presented'); + invariant(providerWeb3, 'providerWeb3 must be presented'); + invariant(wstethContractWeb3, 'wstethContractWeb3 must be presented'); if (token === TOKENS.STETH) { if (isMultisig) { diff --git a/features/wsteth/wrap/wrap-form-context/types.ts b/features/wsteth/wrap/wrap-form-context/types.ts index 215a0782b..25f869bb0 100644 --- a/features/wsteth/wrap/wrap-form-context/types.ts +++ b/features/wsteth/wrap/wrap-form-context/types.ts @@ -1,8 +1,9 @@ +import type { useWrapFormNetworkData } from '../hooks/use-wrap-form-network-data'; +import type { useWrapTxApprove } from '../hooks/use-wrap-tx-approve'; + import type { BigNumber } from 'ethers'; import type { TokensWrappable } from 'features/wsteth/shared/types'; -import type { useWrapFormNetworkData } from '../hooks/use-wrap-form-network-data'; import type { computeWrapFormContextValues } from './compute-wrap-form-context-values'; -import type { useWrapTxApprove } from '../hooks/use-wrap-tx-approve'; export type WrapFormInputType = { amount: null | BigNumber; @@ -23,5 +24,5 @@ export type WrapFormDataContextValueType = WrapFormNetworkData & WrapFormApprovalData & WrapFormComputedContextValues & { willReceiveWsteth?: BigNumber; - onSubmit: NonNullable['onSubmit']>; + onSubmit: (args: WrapFormInputType) => Promise; }; diff --git a/features/wsteth/wrap/wrap-form-context/wrap-form-context.tsx b/features/wsteth/wrap/wrap-form-context/wrap-form-context.tsx index b4eeb150d..bf7da0751 100644 --- a/features/wsteth/wrap/wrap-form-context/wrap-form-context.tsx +++ b/features/wsteth/wrap/wrap-form-context/wrap-form-context.tsx @@ -1,8 +1,7 @@ import invariant from 'tiny-invariant'; -import { useMemo, createContext, useContext, useEffect } from 'react'; +import { useMemo, createContext, useContext } from 'react'; import { useForm, FormProvider } from 'react-hook-form'; import { useWstethBySteth } from 'shared/hooks'; -import { useWeb3 } from 'reef-knot/web3-react'; import { useWrapTxApprove } from '../hooks/use-wrap-tx-approve'; import { @@ -11,7 +10,6 @@ import { WrapFormNetworkData, } from './types'; import { useWrapFormNetworkData } from '../hooks/use-wrap-form-network-data'; -import { useTransactionModal } from 'features/withdrawals/contexts/transaction-modal-context'; import { useWrapFormProcessor } from '../hooks/use-wrap-form-processing'; import { WrapFormValidationResolver } from './wrap-form-validators'; import { TOKENS_TO_WRAP } from 'features/wsteth/shared/types'; @@ -36,7 +34,6 @@ export const useWrapFormData = () => { // Data provider // export const WrapFormProvider: React.FC = ({ children }) => { - const { active } = useWeb3(); const { networkData, networkDataPromise } = useWrapFormNetworkData(); const formObject = useForm>({ @@ -50,36 +47,15 @@ export const WrapFormProvider: React.FC = ({ children }) => { resolver: WrapFormValidationResolver, }); - const { handleSubmit, reset, watch } = formObject; + const { watch } = formObject; const [token, amount] = watch(['token', 'amount']); - const { revalidateWrapFormData } = networkData; const approvalData = useWrapTxApprove({ amount: amount ?? Zero, token }); const processWrapFormFlow = useWrapFormProcessor({ approvalData, - onConfirm: revalidateWrapFormData, + onConfirm: networkData.revalidateWrapFormData, }); - const onSubmit = useMemo( - () => - handleSubmit(async ({ token, amount }) => { - const success = await processWrapFormFlow({ token, amount }); - if (success) reset(); - }), - [handleSubmit, processWrapFormFlow, reset], - ); - - const { dispatchModalState } = useTransactionModal(); - - useEffect(() => { - dispatchModalState({ type: 'set_on_retry', callback: onSubmit }); - }, [dispatchModalState, onSubmit]); - - // Reset form amount after disconnect wallet - useEffect(() => { - if (!active) reset(); - }, [active, reset]); - const willReceiveWsteth = useWstethBySteth( token === TOKENS_TO_WRAP.STETH && approvalData.isApprovalNeededBeforeWrap ? Zero @@ -95,9 +71,9 @@ export const WrapFormProvider: React.FC = ({ children }) => { token, }), willReceiveWsteth, - onSubmit, + onSubmit: processWrapFormFlow, }), - [networkData, approvalData, token, willReceiveWsteth, onSubmit], + [networkData, approvalData, token, willReceiveWsteth, processWrapFormFlow], ); return ( diff --git a/features/wsteth/wrap/wrap-form-context/wrap-form-validators.ts b/features/wsteth/wrap/wrap-form-context/wrap-form-validators.ts index 404f545f8..5dd8fa7e1 100644 --- a/features/wsteth/wrap/wrap-form-context/wrap-form-validators.ts +++ b/features/wsteth/wrap/wrap-form-context/wrap-form-validators.ts @@ -25,7 +25,10 @@ export const WrapFormValidationResolver: Resolver< > = async (values, networkDataPromise) => { const { amount, token } = values; try { - invariant(networkDataPromise, 'must have context promise'); + invariant( + networkDataPromise, + 'network data must be presented as context promise', + ); validateEtherAmount('amount', amount, token); diff --git a/features/wsteth/wrap/wrap-form-controls/submit-button-wrap.tsx b/features/wsteth/wrap/wrap-form-controls/submit-button-wrap.tsx index 0b13e569f..ce0c5273b 100644 --- a/features/wsteth/wrap/wrap-form-controls/submit-button-wrap.tsx +++ b/features/wsteth/wrap/wrap-form-controls/submit-button-wrap.tsx @@ -1,20 +1,14 @@ -import { useFormState } from 'react-hook-form'; import { useWrapFormData } from '../wrap-form-context'; -import type { RequestFormInputType } from 'features/withdrawals/request/request-form-context'; -import { isValidationErrorTypeUnhandled } from 'shared/hook-form/validation/validation-error'; import { SubmitButtonHookForm } from 'shared/hook-form/controls/submit-button-hook-form'; export const SubmitButtonWrap = () => { const { isApprovalNeededBeforeWrap: isLocked } = useWrapFormData(); - const { errors } = useFormState(); - const disabled = - !!errors.amount && !isValidationErrorTypeUnhandled(errors.amount.type); return ( {isLocked ? 'Unlock token to wrap' : 'Wrap'} diff --git a/features/wsteth/wrap/wrap-form-controls/token-amount-input-wrap.tsx b/features/wsteth/wrap/wrap-form-controls/token-amount-input-wrap.tsx index 1bc3cd96a..c35079d45 100644 --- a/features/wsteth/wrap/wrap-form-controls/token-amount-input-wrap.tsx +++ b/features/wsteth/wrap/wrap-form-controls/token-amount-input-wrap.tsx @@ -1,15 +1,20 @@ -import { useWrapFormData } from '../wrap-form-context'; +import { useWatch } from 'react-hook-form'; +import { useWrapFormData, WrapFormInputType } from '../wrap-form-context'; import { TokenAmountInputHookForm } from 'shared/hook-form/controls/token-amount-input-hook-form'; export const TokenAmountInputWrap = () => { + const token = useWatch({ name: 'token' }); const { maxAmount, isApprovalNeededBeforeWrap } = useWrapFormData(); return ( ); }; diff --git a/features/wsteth/wrap/wrap-form-controls/wrap-form-controller.tsx b/features/wsteth/wrap/wrap-form-controls/wrap-form-controller.tsx index 2ddccab39..8fb1e8931 100644 --- a/features/wsteth/wrap/wrap-form-controls/wrap-form-controller.tsx +++ b/features/wsteth/wrap/wrap-form-controls/wrap-form-controller.tsx @@ -1,12 +1,7 @@ -import { FormStyled } from 'features/wsteth/shared/styles'; +import { FormController } from 'features/wsteth/shared/components/form-controller'; import { useWrapFormData } from '../wrap-form-context'; export const WrapFormController: React.FC = ({ children }) => { const { onSubmit } = useWrapFormData(); - - return ( - - {children} - - ); + return {children}; }; diff --git a/features/wsteth/wrap/wrap-form/wrap-form-tx-modal.tsx b/features/wsteth/wrap/wrap-form/wrap-form-tx-modal.tsx index dab8ed815..7b8a73876 100644 --- a/features/wsteth/wrap/wrap-form/wrap-form-tx-modal.tsx +++ b/features/wsteth/wrap/wrap-form/wrap-form-tx-modal.tsx @@ -5,37 +5,11 @@ import { useWrapFormData, WrapFormInputType } from '../wrap-form-context'; import { formatBalance } from 'utils'; import { TxStageModal } from 'shared/components'; -import { TX_STAGE } from 'features/withdrawals/shared/tx-stage-modal'; -import { - TX_STAGE as TX_STAGE_LEGACY, - TX_OPERATION as TX_OPERATION_LEGACY, -} from 'shared/components/tx-stage-modal'; import { getTokenDisplayName } from 'utils/getTokenDisplayName'; - -const convertTxStageToLegacy = (txStage: TX_STAGE) => { - switch (txStage) { - case TX_STAGE.SIGN: - case TX_STAGE.APPROVE: - case TX_STAGE.PERMIT: - return TX_STAGE_LEGACY.SIGN; - case TX_STAGE.BLOCK: - return TX_STAGE_LEGACY.BLOCK; - case TX_STAGE.FAIL: - return TX_STAGE_LEGACY.FAIL; - case TX_STAGE.SUCCESS: - return TX_STAGE_LEGACY.SUCCESS; - case TX_STAGE.SUCCESS_MULTISIG: - return TX_STAGE_LEGACY.SUCCESS_MULTISIG; - case TX_STAGE.NONE: - case TX_STAGE.BUNKER: - return TX_STAGE_LEGACY.IDLE; - } -}; - -const convertTxStageToLegacyTxOperation = (txStage: TX_STAGE) => { - if (txStage === TX_STAGE.APPROVE) return TX_OPERATION_LEGACY.APPROVING; - return TX_OPERATION_LEGACY.WRAPPING; -}; +import { + convertTxStageToLegacy, + convertTxStageToLegacyTxOperation, +} from 'features/wsteth/shared/utils/convertTxModalStageToLegacy'; export const WrapFormTxModal = () => { const { watch } = useFormContext(); diff --git a/features/wsteth/wrap/wrap-form/wrap-stats.tsx b/features/wsteth/wrap/wrap-form/wrap-stats.tsx index 760b3a1e7..bdf33882a 100644 --- a/features/wsteth/wrap/wrap-form/wrap-stats.tsx +++ b/features/wsteth/wrap/wrap-form/wrap-stats.tsx @@ -6,7 +6,8 @@ import { useTxCostInUsd, useWstethBySteth } from 'shared/hooks'; import { useApproveGasLimit } from '../hooks/use-approve-gas-limit'; import { useWrapFormData, WrapFormInputType } from '../wrap-form-context'; -import { DataTable, DataTableRow } from '@lidofinance/lido-ui'; +import { DataTableRow } from '@lidofinance/lido-ui'; +import { StatsDataTable } from 'features/wsteth/shared/styles'; import { FormatToken } from 'shared/formatters'; import { TOKENS_TO_WRAP } from 'features/wsteth/shared/types'; @@ -27,7 +28,7 @@ export const WrapFormStats = () => { const wrapTxCostInUsd = useTxCostInUsd(wrapGasLimit && Number(wrapGasLimit)); return ( - + { symbol="wstETH" /> - + ); }; diff --git a/shared/hook-form/controls/submit-button-hook-form.tsx b/shared/hook-form/controls/submit-button-hook-form.tsx index 00796fbb0..fbc962120 100644 --- a/shared/hook-form/controls/submit-button-hook-form.tsx +++ b/shared/hook-form/controls/submit-button-hook-form.tsx @@ -3,20 +3,27 @@ import { useFormState } from 'react-hook-form'; import { ButtonIcon, Lock } from '@lidofinance/lido-ui'; import { Connect } from 'shared/wallet'; +import { isValidationErrorTypeUnhandled } from '../validation/validation-error'; type SubmitButtonHookFormProps = Partial< React.ComponentProps > & { + errorField: string; isLocked?: boolean; }; export const SubmitButtonHookForm: React.FC = ({ isLocked, + errorField, icon, ...props }) => { const { active } = useWeb3(); const { isValidating, isSubmitting } = useFormState(); + const { errors } = useFormState>(); + const disabled = + !!errors.amount && + !isValidationErrorTypeUnhandled(errors?.[errorField]?.type); if (!active) return ; @@ -25,6 +32,7 @@ export const SubmitButtonHookForm: React.FC = ({ fullwidth type="submit" loading={isValidating || isSubmitting} + disabled={disabled} icon={icon || isLocked ? : <>} {...props} /> diff --git a/shared/hook-form/controls/token-amount-input-hook-form.tsx b/shared/hook-form/controls/token-amount-input-hook-form.tsx index fec4c484d..0d08cb58d 100644 --- a/shared/hook-form/controls/token-amount-input-hook-form.tsx +++ b/shared/hook-form/controls/token-amount-input-hook-form.tsx @@ -1,4 +1,4 @@ -import { useController, useWatch } from 'react-hook-form'; +import { useController } from 'react-hook-form'; import { InputAmount } from 'shared/forms/components/input-amount'; @@ -11,28 +11,31 @@ type TokenAmountInputHookFormProps = Partial< > & { isLocked?: boolean; maxValue?: BigNumber; - tokenFieldName?: string; - valueFieldName?: string; + token: Parameters[0]; + fieldName: string; + showErrorMessage?: boolean; }; export const TokenAmountInputHookForm = ({ isLocked, maxValue, - tokenFieldName = 'token', - valueFieldName = 'amount', + token, + fieldName, + showErrorMessage = true, ...props }: TokenAmountInputHookFormProps) => { - const token = useWatch({ name: tokenFieldName }); const { field, fieldState: { error }, - } = useController({ name: valueFieldName }); + } = useController({ name: fieldName }); + const hasErrorHighlight = isValidationErrorTypeDefault(error?.type); + const errorMessage = hasErrorHighlight && error?.message; return ( Date: Thu, 24 Aug 2023 19:45:52 +0400 Subject: [PATCH 12/22] refactor: wrap/unwrap form controller --- .../form-controller-context.tsx | 18 ++++++++++++++++++ .../form-controller.tsx | 16 +++++----------- .../wsteth/unwrap/unwrap-form-context/types.ts | 12 +++++++----- .../unwrap-form-context.tsx | 10 +++++++--- .../unwrap-form-controller.tsx | 7 ------- .../wsteth/unwrap/unwrap-form/unwrap-form.tsx | 6 +++--- .../wsteth/wrap/wrap-form-context/types.ts | 5 +++-- .../wrap-form-context/wrap-form-context.tsx | 10 +++++++--- .../wrap-form-controller.tsx | 7 ------- features/wsteth/wrap/wrap-form/wrap-form.tsx | 6 +++--- 10 files changed, 53 insertions(+), 44 deletions(-) create mode 100644 features/wsteth/shared/form-controller/form-controller-context.tsx rename features/wsteth/shared/{components => form-controller}/form-controller.tsx (69%) delete mode 100644 features/wsteth/unwrap/unwrap-form-controls/unwrap-form-controller.tsx delete mode 100644 features/wsteth/wrap/wrap-form-controls/wrap-form-controller.tsx diff --git a/features/wsteth/shared/form-controller/form-controller-context.tsx b/features/wsteth/shared/form-controller/form-controller-context.tsx new file mode 100644 index 000000000..802c0711c --- /dev/null +++ b/features/wsteth/shared/form-controller/form-controller-context.tsx @@ -0,0 +1,18 @@ +import invariant from 'tiny-invariant'; +import { createContext, useContext } from 'react'; +import type { FieldValues } from 'react-hook-form'; + +export type FormControllerContextValueType = { + isLocked?: boolean; + onSubmit: (args: F) => Promise; +}; + +export const FormControllerContext = + createContext(null); +FormControllerContext.displayName = 'FormControllerContext'; + +export const useFormControllerContext = () => { + const value = useContext(FormControllerContext); + invariant(value, 'useFormControllerContext was used outside the provider'); + return value; +}; diff --git a/features/wsteth/shared/components/form-controller.tsx b/features/wsteth/shared/form-controller/form-controller.tsx similarity index 69% rename from features/wsteth/shared/components/form-controller.tsx rename to features/wsteth/shared/form-controller/form-controller.tsx index 72672b2de..6e98fe760 100644 --- a/features/wsteth/shared/components/form-controller.tsx +++ b/features/wsteth/shared/form-controller/form-controller.tsx @@ -2,24 +2,18 @@ import { useEffect, useMemo } from 'react'; import { useWeb3 } from 'reef-knot/web3-react'; import { useFormContext } from 'react-hook-form'; import { useTransactionModal } from 'features/withdrawals/contexts/transaction-modal-context'; +import { useFormControllerContext } from './form-controller-context'; -export type FormController = { - onSubmit: (args: FieldValues) => Promise; - children: React.ReactNode; -}; - -export const FormController = ({ - onSubmit, - children, -}: FormController) => { +export const FormController: React.FC = ({ children }) => { const { active } = useWeb3(); - const { reset, handleSubmit } = useFormContext(); + const { reset, handleSubmit } = useFormContext(); + const { onSubmit } = useFormControllerContext(); const { dispatchModalState } = useTransactionModal(); // Bind submit action const doSubmit = useMemo( () => - handleSubmit(async (args: FieldValues) => { + handleSubmit(async (args) => { const success = await onSubmit(args); if (success) reset(); }), diff --git a/features/wsteth/unwrap/unwrap-form-context/types.ts b/features/wsteth/unwrap/unwrap-form-context/types.ts index 614e6fbbc..472adabfa 100644 --- a/features/wsteth/unwrap/unwrap-form-context/types.ts +++ b/features/wsteth/unwrap/unwrap-form-context/types.ts @@ -1,6 +1,8 @@ -import type { BigNumber } from 'ethers'; import type { useUnwrapFormNetworkData } from '../hooks/use-unwrap-form-network-data'; +import type { BigNumber } from 'ethers'; +import type { FormControllerContextValueType } from 'features/wsteth/shared/form-controller/form-controller-context'; + export type UnwrapFormInputType = { amount: null | BigNumber; }; @@ -9,7 +11,7 @@ export type UnwrapFormNetworkData = ReturnType< typeof useUnwrapFormNetworkData >['networkData']; -export type UnwrapFormDataContextValueType = UnwrapFormNetworkData & { - willReceiveStETH?: BigNumber; - onSubmit: (args: UnwrapFormInputType) => Promise; -}; +export type UnwrapFormDataContextValueType = UnwrapFormNetworkData & + FormControllerContextValueType & { + willReceiveStETH?: BigNumber; + }; diff --git a/features/wsteth/unwrap/unwrap-form-context/unwrap-form-context.tsx b/features/wsteth/unwrap/unwrap-form-context/unwrap-form-context.tsx index 51e79f0fe..af0425ffe 100644 --- a/features/wsteth/unwrap/unwrap-form-context/unwrap-form-context.tsx +++ b/features/wsteth/unwrap/unwrap-form-context/unwrap-form-context.tsx @@ -2,14 +2,16 @@ import invariant from 'tiny-invariant'; import { useMemo, createContext, useContext } from 'react'; import { useForm, FormProvider } from 'react-hook-form'; import { useStethByWsteth } from 'shared/hooks'; +import { useUnwrapFormNetworkData } from '../hooks/use-unwrap-form-network-data'; +import { useUnwrapFormProcessor } from '../hooks/use-unwrap-form-processing'; + +import { FormControllerContext } from 'features/wsteth/shared/form-controller/form-controller-context'; import { UnwrapFormDataContextValueType, UnwrapFormInputType, UnwrapFormNetworkData, } from './types'; -import { useUnwrapFormNetworkData } from '../hooks/use-unwrap-form-network-data'; -import { useUnwrapFormProcessor } from '../hooks/use-unwrap-form-processing'; import { UnwrapFormValidationResolver } from './unwrap-form-validators'; import { Zero } from '@ethersproject/constants'; @@ -66,7 +68,9 @@ export const UnwrapFormProvider: React.FC = ({ children }) => { return ( - {children} + + {children} + ); diff --git a/features/wsteth/unwrap/unwrap-form-controls/unwrap-form-controller.tsx b/features/wsteth/unwrap/unwrap-form-controls/unwrap-form-controller.tsx deleted file mode 100644 index 4247acf62..000000000 --- a/features/wsteth/unwrap/unwrap-form-controls/unwrap-form-controller.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { FormController } from 'features/wsteth/shared/components/form-controller'; -import { useUnwrapFormData } from '../unwrap-form-context'; - -export const UnwrapFormController: React.FC = ({ children }) => { - const { onSubmit } = useUnwrapFormData(); - return {children}; -}; diff --git a/features/wsteth/unwrap/unwrap-form/unwrap-form.tsx b/features/wsteth/unwrap/unwrap-form/unwrap-form.tsx index cd1a2e27d..0b6d875cc 100644 --- a/features/wsteth/unwrap/unwrap-form/unwrap-form.tsx +++ b/features/wsteth/unwrap/unwrap-form/unwrap-form.tsx @@ -6,7 +6,7 @@ import { UnwrapStats } from './unwrap-stats'; import { UnwrapFormTxModal } from './unwrap-form-tx-modal'; import { TransactionModalProvider } from 'features/withdrawals/contexts/transaction-modal-context'; import { UnwrapFormProvider } from '../unwrap-form-context'; -import { UnwrapFormController } from '../unwrap-form-controls/unwrap-form-controller'; +import { FormController } from 'features/wsteth/shared/form-controller/form-controller'; import { TokenAmountInputUnwrap } from '../unwrap-form-controls/amount-input-unwrap'; import { SubmitButtonUnwrap } from '../unwrap-form-controls/submit-button-unwrap'; import { InputWrap } from 'features/wsteth/shared/styles'; @@ -16,13 +16,13 @@ export const UnwrapForm: React.FC = memo(() => { - + - + diff --git a/features/wsteth/wrap/wrap-form-context/types.ts b/features/wsteth/wrap/wrap-form-context/types.ts index 25f869bb0..7ef934292 100644 --- a/features/wsteth/wrap/wrap-form-context/types.ts +++ b/features/wsteth/wrap/wrap-form-context/types.ts @@ -4,6 +4,7 @@ import type { useWrapTxApprove } from '../hooks/use-wrap-tx-approve'; import type { BigNumber } from 'ethers'; import type { TokensWrappable } from 'features/wsteth/shared/types'; import type { computeWrapFormContextValues } from './compute-wrap-form-context-values'; +import type { FormControllerContextValueType } from 'features/wsteth/shared/form-controller/form-controller-context'; export type WrapFormInputType = { amount: null | BigNumber; @@ -22,7 +23,7 @@ export type WrapFormComputedContextValues = ReturnType< export type WrapFormDataContextValueType = WrapFormNetworkData & WrapFormApprovalData & - WrapFormComputedContextValues & { + WrapFormComputedContextValues & + FormControllerContextValueType & { willReceiveWsteth?: BigNumber; - onSubmit: (args: WrapFormInputType) => Promise; }; diff --git a/features/wsteth/wrap/wrap-form-context/wrap-form-context.tsx b/features/wsteth/wrap/wrap-form-context/wrap-form-context.tsx index bf7da0751..5dbdc7a4c 100644 --- a/features/wsteth/wrap/wrap-form-context/wrap-form-context.tsx +++ b/features/wsteth/wrap/wrap-form-context/wrap-form-context.tsx @@ -3,14 +3,16 @@ import { useMemo, createContext, useContext } from 'react'; import { useForm, FormProvider } from 'react-hook-form'; import { useWstethBySteth } from 'shared/hooks'; import { useWrapTxApprove } from '../hooks/use-wrap-tx-approve'; +import { useWrapFormNetworkData } from '../hooks/use-wrap-form-network-data'; +import { useWrapFormProcessor } from '../hooks/use-wrap-form-processing'; + +import { FormControllerContext } from 'features/wsteth/shared/form-controller/form-controller-context'; import { WrapFormDataContextValueType, WrapFormInputType, WrapFormNetworkData, } from './types'; -import { useWrapFormNetworkData } from '../hooks/use-wrap-form-network-data'; -import { useWrapFormProcessor } from '../hooks/use-wrap-form-processing'; import { WrapFormValidationResolver } from './wrap-form-validators'; import { TOKENS_TO_WRAP } from 'features/wsteth/shared/types'; import { Zero } from '@ethersproject/constants'; @@ -79,7 +81,9 @@ export const WrapFormProvider: React.FC = ({ children }) => { return ( - {children} + + {children} + ); diff --git a/features/wsteth/wrap/wrap-form-controls/wrap-form-controller.tsx b/features/wsteth/wrap/wrap-form-controls/wrap-form-controller.tsx deleted file mode 100644 index 8fb1e8931..000000000 --- a/features/wsteth/wrap/wrap-form-controls/wrap-form-controller.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { FormController } from 'features/wsteth/shared/components/form-controller'; -import { useWrapFormData } from '../wrap-form-context'; - -export const WrapFormController: React.FC = ({ children }) => { - const { onSubmit } = useWrapFormData(); - return {children}; -}; diff --git a/features/wsteth/wrap/wrap-form/wrap-form.tsx b/features/wsteth/wrap/wrap-form/wrap-form.tsx index d12eb9949..732bd9991 100644 --- a/features/wsteth/wrap/wrap-form/wrap-form.tsx +++ b/features/wsteth/wrap/wrap-form/wrap-form.tsx @@ -6,7 +6,7 @@ import { WrapFormStats } from './wrap-stats'; import { WrapFormTxModal } from './wrap-form-tx-modal'; import { WrapFormProvider } from '../wrap-form-context/wrap-form-context'; import { TransactionModalProvider } from 'features/withdrawals/contexts/transaction-modal-context'; -import { WrapFormController } from '../wrap-form-controls/wrap-form-controller'; +import { FormController } from 'features/wsteth/shared/form-controller/form-controller'; import { TokenSelectWrap } from '../wrap-form-controls/token-select-wrap'; import { TokenAmountInputWrap } from '../wrap-form-controls/token-amount-input-wrap'; import { SubmitButtonWrap } from '../wrap-form-controls/submit-button-wrap'; @@ -19,14 +19,14 @@ export const WrapForm: React.FC = memo(() => { - + - + From a368b4bf7bae5f9efb2b7f0e7640f2eb9099b8f3 Mon Sep 17 00:00:00 2001 From: Dmitrii Podlesnyi Date: Thu, 24 Aug 2023 20:20:00 +0400 Subject: [PATCH 13/22] fix: unwrap tx modal stage title --- .../wsteth/shared/utils/convertTxModalStageToLegacy.ts | 2 +- .../wsteth/unwrap/unwrap-form/unwrap-form-tx-modal.tsx | 8 +++----- features/wsteth/wrap/wrap-form/wrap-form-tx-modal.tsx | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/features/wsteth/shared/utils/convertTxModalStageToLegacy.ts b/features/wsteth/shared/utils/convertTxModalStageToLegacy.ts index ef8c79dce..c7f5a3970 100644 --- a/features/wsteth/shared/utils/convertTxModalStageToLegacy.ts +++ b/features/wsteth/shared/utils/convertTxModalStageToLegacy.ts @@ -24,7 +24,7 @@ export const convertTxStageToLegacy = (txStage: TX_STAGE) => { } }; -export const convertTxStageToLegacyTxOperation = (txStage: TX_STAGE) => { +export const convertTxStageToLegacyTxOperationWrap = (txStage: TX_STAGE) => { if (txStage === TX_STAGE.APPROVE) return TX_OPERATION_LEGACY.APPROVING; return TX_OPERATION_LEGACY.WRAPPING; }; diff --git a/features/wsteth/unwrap/unwrap-form/unwrap-form-tx-modal.tsx b/features/wsteth/unwrap/unwrap-form/unwrap-form-tx-modal.tsx index 972b10ebc..213dd251e 100644 --- a/features/wsteth/unwrap/unwrap-form/unwrap-form-tx-modal.tsx +++ b/features/wsteth/unwrap/unwrap-form/unwrap-form-tx-modal.tsx @@ -1,11 +1,9 @@ import { useTransactionModal } from 'features/withdrawals/contexts/transaction-modal-context'; -import { - convertTxStageToLegacy, - convertTxStageToLegacyTxOperation, -} from 'features/wsteth/shared/utils/convertTxModalStageToLegacy'; +import { convertTxStageToLegacy } from 'features/wsteth/shared/utils/convertTxModalStageToLegacy'; import { TxStageModal } from 'shared/components'; import { formatBalance } from 'utils'; import { useUnwrapFormData } from '../unwrap-form-context'; +import { TX_OPERATION as TX_OPERATION_LEGACY } from 'shared/components/tx-stage-modal'; export const UnwrapFormTxModal = () => { const { stethBalance, willReceiveStETH } = useUnwrapFormData(); @@ -16,7 +14,7 @@ export const UnwrapFormTxModal = () => { open={modalState.isModalOpen} onClose={() => dispatchModalState({ type: 'close_modal' })} txStage={convertTxStageToLegacy(modalState.txStage)} - txOperation={convertTxStageToLegacyTxOperation(modalState.txStage)} + txOperation={TX_OPERATION_LEGACY.UNWRAPPING} txHash={modalState.txHash} amount={ modalState.requestAmount ? formatBalance(modalState.requestAmount) : '' diff --git a/features/wsteth/wrap/wrap-form/wrap-form-tx-modal.tsx b/features/wsteth/wrap/wrap-form/wrap-form-tx-modal.tsx index 7b8a73876..b5116cd84 100644 --- a/features/wsteth/wrap/wrap-form/wrap-form-tx-modal.tsx +++ b/features/wsteth/wrap/wrap-form/wrap-form-tx-modal.tsx @@ -8,7 +8,7 @@ import { TxStageModal } from 'shared/components'; import { getTokenDisplayName } from 'utils/getTokenDisplayName'; import { convertTxStageToLegacy, - convertTxStageToLegacyTxOperation, + convertTxStageToLegacyTxOperationWrap, } from 'features/wsteth/shared/utils/convertTxModalStageToLegacy'; export const WrapFormTxModal = () => { @@ -22,7 +22,7 @@ export const WrapFormTxModal = () => { open={modalState.isModalOpen} onClose={() => dispatchModalState({ type: 'close_modal' })} txStage={convertTxStageToLegacy(modalState.txStage)} - txOperation={convertTxStageToLegacyTxOperation(modalState.txStage)} + txOperation={convertTxStageToLegacyTxOperationWrap(modalState.txStage)} txHash={modalState.txHash} amount={ modalState.requestAmount ? formatBalance(modalState.requestAmount) : '' From bfa2459e1efaad0afe66659c65194717f5cc8445 Mon Sep 17 00:00:00 2001 From: Dmitrii Podlesnyi Date: Mon, 28 Aug 2023 15:53:18 +0400 Subject: [PATCH 14/22] fix: do not show unhandled validation errors --- .../request/form/controls/token-select-request.tsx | 4 ++-- shared/hook-form/controls/input-group-hook-form.tsx | 4 ++-- shared/hook-form/controls/submit-button-hook-form.tsx | 4 ++-- shared/hook-form/controls/token-amount-input-hook-form.tsx | 4 ++-- shared/hook-form/controls/token-select-hook-form.tsx | 4 ++-- shared/hook-form/validation/validation-error.ts | 3 +++ 6 files changed, 13 insertions(+), 10 deletions(-) diff --git a/features/withdrawals/request/form/controls/token-select-request.tsx b/features/withdrawals/request/form/controls/token-select-request.tsx index 13cb49220..8b4414310 100644 --- a/features/withdrawals/request/form/controls/token-select-request.tsx +++ b/features/withdrawals/request/form/controls/token-select-request.tsx @@ -6,7 +6,7 @@ import { RequestFormInputType } from 'features/withdrawals/request/request-form- import { getTokenDisplayName } from 'utils/getTokenDisplayName'; import { TokensWithdrawable } from 'features/withdrawals/types/tokens-withdrawable'; -import { isValidationErrorTypeDefault } from 'shared/hook-form/validation/validation-error'; +import { isValidationErrorTypeValidate } from 'shared/hook-form/validation/validation-error'; const iconsMap = { [TOKENS.WSTETH]: , @@ -25,7 +25,7 @@ export const TokenSelectRequest = () => { { setValue('token', value, { shouldDirty: false, diff --git a/shared/hook-form/controls/input-group-hook-form.tsx b/shared/hook-form/controls/input-group-hook-form.tsx index 93783ea60..a3ede5934 100644 --- a/shared/hook-form/controls/input-group-hook-form.tsx +++ b/shared/hook-form/controls/input-group-hook-form.tsx @@ -1,7 +1,7 @@ import styled from 'styled-components'; import { useFormState } from 'react-hook-form'; import { InputGroup } from '@lidofinance/lido-ui'; -import { isValidationErrorTypeDefault } from 'shared/hook-form/validation/validation-error'; +import { isValidationErrorTypeValidate } from 'shared/hook-form/validation/validation-error'; type InputGroupProps = React.ComponentProps; @@ -27,7 +27,7 @@ export const InputGroupHookForm: React.FC = ({ name: errorField, }); const errorMessage = - isValidationErrorTypeDefault(errors[errorField]?.type) && + isValidationErrorTypeValidate(errors[errorField]?.type) && errors[errorField]?.message; return ; }; diff --git a/shared/hook-form/controls/submit-button-hook-form.tsx b/shared/hook-form/controls/submit-button-hook-form.tsx index fbc962120..5c9d4db81 100644 --- a/shared/hook-form/controls/submit-button-hook-form.tsx +++ b/shared/hook-form/controls/submit-button-hook-form.tsx @@ -3,7 +3,7 @@ import { useFormState } from 'react-hook-form'; import { ButtonIcon, Lock } from '@lidofinance/lido-ui'; import { Connect } from 'shared/wallet'; -import { isValidationErrorTypeUnhandled } from '../validation/validation-error'; +import { isValidationErrorTypeValidate } from '../validation/validation-error'; type SubmitButtonHookFormProps = Partial< React.ComponentProps @@ -23,7 +23,7 @@ export const SubmitButtonHookForm: React.FC = ({ const { errors } = useFormState>(); const disabled = !!errors.amount && - !isValidationErrorTypeUnhandled(errors?.[errorField]?.type); + !isValidationErrorTypeValidate(errors?.[errorField]?.type); if (!active) return ; diff --git a/shared/hook-form/controls/token-amount-input-hook-form.tsx b/shared/hook-form/controls/token-amount-input-hook-form.tsx index 0d08cb58d..20d071562 100644 --- a/shared/hook-form/controls/token-amount-input-hook-form.tsx +++ b/shared/hook-form/controls/token-amount-input-hook-form.tsx @@ -3,7 +3,7 @@ import { useController } from 'react-hook-form'; import { InputAmount } from 'shared/forms/components/input-amount'; import { getTokenDisplayName } from 'utils/getTokenDisplayName'; -import { isValidationErrorTypeDefault } from 'shared/hook-form/validation/validation-error'; +import { isValidationErrorTypeValidate } from 'shared/hook-form/validation/validation-error'; import type { BigNumber } from 'ethers'; type TokenAmountInputHookFormProps = Partial< @@ -28,7 +28,7 @@ export const TokenAmountInputHookForm = ({ field, fieldState: { error }, } = useController({ name: fieldName }); - const hasErrorHighlight = isValidationErrorTypeDefault(error?.type); + const hasErrorHighlight = isValidationErrorTypeValidate(error?.type); const errorMessage = hasErrorHighlight && error?.message; return ( diff --git a/shared/hook-form/controls/token-select-hook-form.tsx b/shared/hook-form/controls/token-select-hook-form.tsx index 89c7b2357..be39470bd 100644 --- a/shared/hook-form/controls/token-select-hook-form.tsx +++ b/shared/hook-form/controls/token-select-hook-form.tsx @@ -3,7 +3,7 @@ import { useController, useFormState, useFormContext } from 'react-hook-form'; import { SelectIcon, Option, Eth, Steth, Wsteth } from '@lidofinance/lido-ui'; import { getTokenDisplayName } from 'utils/getTokenDisplayName'; -import { isValidationErrorTypeDefault } from 'shared/hook-form/validation/validation-error'; +import { isValidationErrorTypeValidate } from 'shared/hook-form/validation/validation-error'; import { TOKENS as TOKENS_SDK } from '@lido-sdk/constants'; export const TOKENS = { @@ -45,7 +45,7 @@ export const TokenSelectHookForm = ({ { setValue(fieldName, value, { shouldDirty: false, diff --git a/shared/hook-form/validation/validation-error.ts b/shared/hook-form/validation/validation-error.ts index 3134ec5a5..34fffabbe 100644 --- a/shared/hook-form/validation/validation-error.ts +++ b/shared/hook-form/validation/validation-error.ts @@ -11,6 +11,9 @@ export const isValidationErrorTypeDefault = (type?: string) => ] as (string | undefined)[] ).includes(type); +export const isValidationErrorTypeValidate = (type?: string) => + type === DefaultValidationErrorTypes.VALIDATE; + export const isValidationErrorTypeUnhandled = (type?: string) => type === DefaultValidationErrorTypes.UNHANDLED; From ad6ec887343189665311ee399dc18d8b20ba3646 Mon Sep 17 00:00:00 2001 From: Dmitrii Podlesnyi Date: Mon, 28 Aug 2023 15:54:56 +0400 Subject: [PATCH 15/22] refactor: wrap/unwrap validation contexts --- .../use-unwra-form-validation-context.ts | 28 +++++++++++++ .../hooks/use-unwrap-form-network-data.ts | 21 +--------- .../unwrap/unwrap-form-context/types.ts | 9 ++-- .../unwrap-form-context.tsx | 12 ++++-- .../unwrap-form-validators.tsx | 30 +++++++------- ...data.tsx => use-wrap-form-network-data.ts} | 28 +------------ .../hooks/use-wrap-form-validation-context.ts | 29 +++++++++++++ .../compute-wrap-form-context-values.ts | 21 ---------- .../wsteth/wrap/wrap-form-context/types.ts | 17 ++++---- .../wrap-form-context/wrap-form-context.tsx | 41 +++++++++++++------ .../wrap-form-context/wrap-form-validators.ts | 37 +++++++++-------- 11 files changed, 148 insertions(+), 125 deletions(-) create mode 100644 features/wsteth/unwrap/hooks/use-unwra-form-validation-context.ts rename features/wsteth/wrap/hooks/{use-wrap-form-network-data.tsx => use-wrap-form-network-data.ts} (72%) create mode 100644 features/wsteth/wrap/hooks/use-wrap-form-validation-context.ts delete mode 100644 features/wsteth/wrap/wrap-form-context/compute-wrap-form-context-values.ts diff --git a/features/wsteth/unwrap/hooks/use-unwra-form-validation-context.ts b/features/wsteth/unwrap/hooks/use-unwra-form-validation-context.ts new file mode 100644 index 000000000..dd03036dc --- /dev/null +++ b/features/wsteth/unwrap/hooks/use-unwra-form-validation-context.ts @@ -0,0 +1,28 @@ +import { useMemo } from 'react'; +import { useWeb3 } from 'reef-knot/web3-react'; +import { useAwaiter } from 'shared/hooks/use-awaiter'; + +import type { UnwrapFormNetworkData } from '../unwrap-form-context'; + +type UseUnwrapFormValidationContextArgs = { + networkData: UnwrapFormNetworkData; +}; + +export const useUnwrapFormValidationContext = ({ + networkData, +}: UseUnwrapFormValidationContextArgs) => { + const { active } = useWeb3(); + const { maxAmount } = networkData; + + const validationContextAwaited = useMemo(() => { + if (active && !maxAmount) { + return undefined; + } + return { + active, + maxAmount, + }; + }, [active, maxAmount]); + + return useAwaiter(validationContextAwaited).awaiter; +}; diff --git a/features/wsteth/unwrap/hooks/use-unwrap-form-network-data.ts b/features/wsteth/unwrap/hooks/use-unwrap-form-network-data.ts index 434a58a8b..58e09e63c 100644 --- a/features/wsteth/unwrap/hooks/use-unwrap-form-network-data.ts +++ b/features/wsteth/unwrap/hooks/use-unwrap-form-network-data.ts @@ -1,10 +1,9 @@ import { useCallback, useMemo } from 'react'; -import { useAwaiter } from 'shared/hooks/use-awaiter'; import { useIsMultisig } from 'shared/hooks/useIsMultisig'; import { useSTETHBalance, useWSTETHBalance } from '@lido-sdk/react'; export const useUnwrapFormNetworkData = () => { - const [isMultisig, isLoadingMultisig] = useIsMultisig(); + const [isMultisig] = useIsMultisig(); const { data: stethBalance, update: stethBalanceUpdate } = useSTETHBalance(); const { data: wstethBalance, update: wstethBalanceUpdate } = useWSTETHBalance(); @@ -24,21 +23,5 @@ export const useUnwrapFormNetworkData = () => { [isMultisig, stethBalance, wstethBalance, revalidateUnwrapFormData], ); - const networkDataAwaited = useMemo(() => { - if ( - isLoadingMultisig || - !networkData.stethBalance || - !networkData.wstethBalance - ) { - return undefined; - } - return networkData; - }, [isLoadingMultisig, networkData]); - - const networkDataAwaiter = useAwaiter(networkDataAwaited); - - return { - networkData, - networkDataPromise: networkDataAwaiter.awaiter, - }; + return networkData; }; diff --git a/features/wsteth/unwrap/unwrap-form-context/types.ts b/features/wsteth/unwrap/unwrap-form-context/types.ts index 472adabfa..1940bfae0 100644 --- a/features/wsteth/unwrap/unwrap-form-context/types.ts +++ b/features/wsteth/unwrap/unwrap-form-context/types.ts @@ -7,9 +7,12 @@ export type UnwrapFormInputType = { amount: null | BigNumber; }; -export type UnwrapFormNetworkData = ReturnType< - typeof useUnwrapFormNetworkData ->['networkData']; +export type UnwrapFormNetworkData = ReturnType; + +export type UnwrapFormValidationContext = { + active: boolean; + maxAmount?: BigNumber; +}; export type UnwrapFormDataContextValueType = UnwrapFormNetworkData & FormControllerContextValueType & { diff --git a/features/wsteth/unwrap/unwrap-form-context/unwrap-form-context.tsx b/features/wsteth/unwrap/unwrap-form-context/unwrap-form-context.tsx index af0425ffe..c2a423123 100644 --- a/features/wsteth/unwrap/unwrap-form-context/unwrap-form-context.tsx +++ b/features/wsteth/unwrap/unwrap-form-context/unwrap-form-context.tsx @@ -4,13 +4,14 @@ import { useForm, FormProvider } from 'react-hook-form'; import { useStethByWsteth } from 'shared/hooks'; import { useUnwrapFormNetworkData } from '../hooks/use-unwrap-form-network-data'; import { useUnwrapFormProcessor } from '../hooks/use-unwrap-form-processing'; +import { useUnwrapFormValidationContext } from '../hooks/use-unwra-form-validation-context'; import { FormControllerContext } from 'features/wsteth/shared/form-controller/form-controller-context'; import { UnwrapFormDataContextValueType, UnwrapFormInputType, - UnwrapFormNetworkData, + UnwrapFormValidationContext, } from './types'; import { UnwrapFormValidationResolver } from './unwrap-form-validators'; import { Zero } from '@ethersproject/constants'; @@ -32,16 +33,19 @@ export const useUnwrapFormData = () => { // Data provider // export const UnwrapFormProvider: React.FC = ({ children }) => { - const { networkData, networkDataPromise } = useUnwrapFormNetworkData(); + const networkData = useUnwrapFormNetworkData(); + const validationContextPromise = useUnwrapFormValidationContext({ + networkData, + }); const formObject = useForm< UnwrapFormInputType, - Promise + Promise >({ defaultValues: { amount: null, }, - context: networkDataPromise, + context: validationContextPromise, criteriaMode: 'firstError', mode: 'onChange', resolver: UnwrapFormValidationResolver, diff --git a/features/wsteth/unwrap/unwrap-form-context/unwrap-form-validators.tsx b/features/wsteth/unwrap/unwrap-form-context/unwrap-form-validators.tsx index 491680c1a..47ac5e370 100644 --- a/features/wsteth/unwrap/unwrap-form-context/unwrap-form-validators.tsx +++ b/features/wsteth/unwrap/unwrap-form-context/unwrap-form-validators.tsx @@ -11,7 +11,7 @@ import { handleResolverValidationError } from 'shared/hook-form/validation/valid import { awaitWithTimeout } from 'utils/await-with-timeout'; import { TOKENS } from '@lido-sdk/constants'; import { VALIDATION_CONTEXT_TIMEOUT } from 'features/withdrawals/withdrawals-constants'; -import type { UnwrapFormInputType, UnwrapFormNetworkData } from './types'; +import type { UnwrapFormInputType, UnwrapFormValidationContext } from './types'; const messageMaxAmount = (max: BigNumber) => `${getTokenDisplayName( @@ -20,30 +20,32 @@ const messageMaxAmount = (max: BigNumber) => export const UnwrapFormValidationResolver: Resolver< UnwrapFormInputType, - Promise -> = async (values, networkDataPromise) => { + Promise +> = async (values, validationContextPromise) => { const { amount } = values; try { invariant( - networkDataPromise, - 'network data must be presented as context promise', + validationContextPromise, + 'validation context must be presented as context promise', ); validateEtherAmount('amount', amount, TOKENS.WSTETH); - const { maxAmount } = await awaitWithTimeout( - networkDataPromise, + const { active, maxAmount } = await awaitWithTimeout( + validationContextPromise, VALIDATION_CONTEXT_TIMEOUT, ); - invariant(maxAmount, 'maxAmount must be presented'); + if (active) { + invariant(maxAmount, 'maxAmount must be presented'); - validateBignumberMax( - 'amount', - amount, - maxAmount, - messageMaxAmount(maxAmount), - ); + validateBignumberMax( + 'amount', + amount, + maxAmount, + messageMaxAmount(maxAmount), + ); + } return { values, diff --git a/features/wsteth/wrap/hooks/use-wrap-form-network-data.tsx b/features/wsteth/wrap/hooks/use-wrap-form-network-data.ts similarity index 72% rename from features/wsteth/wrap/hooks/use-wrap-form-network-data.tsx rename to features/wsteth/wrap/hooks/use-wrap-form-network-data.ts index acbdefbfb..d26b55fa3 100644 --- a/features/wsteth/wrap/hooks/use-wrap-form-network-data.tsx +++ b/features/wsteth/wrap/hooks/use-wrap-form-network-data.ts @@ -4,11 +4,9 @@ import { useSTETHBalance, useEthereumBalance, } from '@lido-sdk/react'; - import { useWrapGasLimit } from './use-wrap-gas-limit'; import { useIsMultisig } from 'shared/hooks/useIsMultisig'; import { useCurrencyMaxAmount } from 'shared/forms/hooks/useCurrencyMaxAmount'; -import { useAwaiter } from 'shared/hooks/use-awaiter'; import { STRATEGY_LAZY } from 'utils/swrStrategies'; import { TOKENS_TO_WRAP } from 'features/wsteth/shared/types'; @@ -16,7 +14,7 @@ import { parseEther } from '@ethersproject/units'; // Provides all data fetching for form to function export const useWrapFormNetworkData = () => { - const [isMultisig, isLoadingMultisig] = useIsMultisig(); + const [isMultisig] = useIsMultisig(); const { data: ethBalance, update: ethBalanceUpdate } = useEthereumBalance( undefined, STRATEGY_LAZY, @@ -42,7 +40,7 @@ export const useWrapFormNetworkData = () => { ]); }, [ethBalanceUpdate, stethBalanceUpdate, wstethBalanceUpdate]); - const networkData = useMemo( + return useMemo( () => ({ isMultisig, ethBalance, @@ -65,26 +63,4 @@ export const useWrapFormNetworkData = () => { maxAmountETH, ], ); - - const networkDataAwaited = useMemo(() => { - if ( - isLoadingMultisig || - !networkData.stethBalance || - !networkData.wstethBalance || - !networkData.gasLimitETH || - !networkData.gasLimitStETH || - !networkData.maxAmountETH || - !networkData.maxAmountStETH - ) { - return undefined; - } - return networkData; - }, [isLoadingMultisig, networkData]); - - const networkDataAwaiter = useAwaiter(networkDataAwaited); - - return { - networkData, - networkDataPromise: networkDataAwaiter.awaiter, - }; }; diff --git a/features/wsteth/wrap/hooks/use-wrap-form-validation-context.ts b/features/wsteth/wrap/hooks/use-wrap-form-validation-context.ts new file mode 100644 index 000000000..32cf26d7a --- /dev/null +++ b/features/wsteth/wrap/hooks/use-wrap-form-validation-context.ts @@ -0,0 +1,29 @@ +import { useMemo } from 'react'; +import { useWeb3 } from 'reef-knot/web3-react'; +import { useAwaiter } from 'shared/hooks/use-awaiter'; + +import type { WrapFormNetworkData } from '../wrap-form-context'; + +type UseWrapFormValidationContextArgs = { + networkData: WrapFormNetworkData; +}; + +export const useWrapFormValidationContext = ({ + networkData, +}: UseWrapFormValidationContextArgs) => { + const { active } = useWeb3(); + const { maxAmountETH, maxAmountStETH } = networkData; + + const validationContextAwaited = useMemo(() => { + if (active && (!maxAmountETH || !maxAmountStETH)) { + return undefined; + } + return { + active, + maxAmountETH, + maxAmountStETH, + }; + }, [active, maxAmountETH, maxAmountStETH]); + + return useAwaiter(validationContextAwaited).awaiter; +}; diff --git a/features/wsteth/wrap/wrap-form-context/compute-wrap-form-context-values.ts b/features/wsteth/wrap/wrap-form-context/compute-wrap-form-context-values.ts deleted file mode 100644 index 89fa66e6c..000000000 --- a/features/wsteth/wrap/wrap-form-context/compute-wrap-form-context-values.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { TOKENS_TO_WRAP, TokensWrappable } from 'features/wsteth/shared/types'; -import { WrapFormNetworkData } from './types'; - -type WrapFormComputeValuesArgs = { - token: TokensWrappable; - networkData: WrapFormNetworkData; -}; - -export const computeWrapFormContextValues = ({ - token, - networkData, -}: WrapFormComputeValuesArgs) => { - const { maxAmountStETH, maxAmountETH, gasLimitStETH, gasLimitETH } = - networkData; - const isSteth = token === TOKENS_TO_WRAP.STETH; - return { - isSteth, - maxAmount: isSteth ? maxAmountStETH : maxAmountETH, - wrapGasLimit: isSteth ? gasLimitStETH : gasLimitETH, - }; -}; diff --git a/features/wsteth/wrap/wrap-form-context/types.ts b/features/wsteth/wrap/wrap-form-context/types.ts index 7ef934292..7b10a3744 100644 --- a/features/wsteth/wrap/wrap-form-context/types.ts +++ b/features/wsteth/wrap/wrap-form-context/types.ts @@ -3,7 +3,6 @@ import type { useWrapTxApprove } from '../hooks/use-wrap-tx-approve'; import type { BigNumber } from 'ethers'; import type { TokensWrappable } from 'features/wsteth/shared/types'; -import type { computeWrapFormContextValues } from './compute-wrap-form-context-values'; import type { FormControllerContextValueType } from 'features/wsteth/shared/form-controller/form-controller-context'; export type WrapFormInputType = { @@ -11,19 +10,21 @@ export type WrapFormInputType = { token: TokensWrappable; }; -export type WrapFormNetworkData = ReturnType< - typeof useWrapFormNetworkData ->['networkData']; +export type WrapFormNetworkData = ReturnType; export type WrapFormApprovalData = ReturnType; -export type WrapFormComputedContextValues = ReturnType< - typeof computeWrapFormContextValues ->; +export type WrapFormValidationContext = { + active: boolean; + maxAmountETH?: BigNumber; + maxAmountStETH?: BigNumber; +}; export type WrapFormDataContextValueType = WrapFormNetworkData & WrapFormApprovalData & - WrapFormComputedContextValues & FormControllerContextValueType & { + isSteth: boolean; + maxAmount?: BigNumber; + wrapGasLimit?: BigNumber; willReceiveWsteth?: BigNumber; }; diff --git a/features/wsteth/wrap/wrap-form-context/wrap-form-context.tsx b/features/wsteth/wrap/wrap-form-context/wrap-form-context.tsx index 5dbdc7a4c..fbe650047 100644 --- a/features/wsteth/wrap/wrap-form-context/wrap-form-context.tsx +++ b/features/wsteth/wrap/wrap-form-context/wrap-form-context.tsx @@ -5,18 +5,18 @@ import { useWstethBySteth } from 'shared/hooks'; import { useWrapTxApprove } from '../hooks/use-wrap-tx-approve'; import { useWrapFormNetworkData } from '../hooks/use-wrap-form-network-data'; import { useWrapFormProcessor } from '../hooks/use-wrap-form-processing'; +import { useWrapFormValidationContext } from '../hooks/use-wrap-form-validation-context'; import { FormControllerContext } from 'features/wsteth/shared/form-controller/form-controller-context'; import { WrapFormDataContextValueType, WrapFormInputType, - WrapFormNetworkData, + WrapFormValidationContext, } from './types'; import { WrapFormValidationResolver } from './wrap-form-validators'; import { TOKENS_TO_WRAP } from 'features/wsteth/shared/types'; import { Zero } from '@ethersproject/constants'; -import { computeWrapFormContextValues } from './compute-wrap-form-context-values'; // // Data context @@ -36,14 +36,20 @@ export const useWrapFormData = () => { // Data provider // export const WrapFormProvider: React.FC = ({ children }) => { - const { networkData, networkDataPromise } = useWrapFormNetworkData(); + const networkData = useWrapFormNetworkData(); + const validationContextPromise = useWrapFormValidationContext({ + networkData, + }); - const formObject = useForm>({ + const formObject = useForm< + WrapFormInputType, + Promise + >({ defaultValues: { amount: null, token: TOKENS_TO_WRAP.STETH, }, - context: networkDataPromise, + context: validationContextPromise, criteriaMode: 'firstError', mode: 'onChange', resolver: WrapFormValidationResolver, @@ -58,24 +64,33 @@ export const WrapFormProvider: React.FC = ({ children }) => { onConfirm: networkData.revalidateWrapFormData, }); + const isSteth = token === TOKENS_TO_WRAP.STETH; + const willReceiveWsteth = useWstethBySteth( - token === TOKENS_TO_WRAP.STETH && approvalData.isApprovalNeededBeforeWrap - ? Zero - : amount ?? Zero, + isSteth && approvalData.isApprovalNeededBeforeWrap ? Zero : amount ?? Zero, ); const value = useMemo( (): WrapFormDataContextValueType => ({ ...networkData, ...approvalData, - ...computeWrapFormContextValues({ - networkData, - token, - }), + isSteth, + maxAmount: isSteth + ? networkData.maxAmountStETH + : networkData.maxAmountETH, + wrapGasLimit: isSteth + ? networkData.gasLimitStETH + : networkData.gasLimitETH, willReceiveWsteth, onSubmit: processWrapFormFlow, }), - [networkData, approvalData, token, willReceiveWsteth, processWrapFormFlow], + [ + networkData, + approvalData, + isSteth, + willReceiveWsteth, + processWrapFormFlow, + ], ); return ( diff --git a/features/wsteth/wrap/wrap-form-context/wrap-form-validators.ts b/features/wsteth/wrap/wrap-form-context/wrap-form-validators.ts index 5dd8fa7e1..4e5b852a0 100644 --- a/features/wsteth/wrap/wrap-form-context/wrap-form-validators.ts +++ b/features/wsteth/wrap/wrap-form-context/wrap-form-validators.ts @@ -6,13 +6,12 @@ import type { Resolver } from 'react-hook-form'; import { validateEtherAmount } from 'shared/hook-form/validation/validate-ether-amount'; import { validateBignumberMax } from 'shared/hook-form/validation/validate-bignumber-max'; import { getTokenDisplayName } from 'utils/getTokenDisplayName'; -import { computeWrapFormContextValues } from './compute-wrap-form-context-values'; import { handleResolverValidationError } from 'shared/hook-form/validation/validation-error'; import { awaitWithTimeout } from 'utils/await-with-timeout'; import { VALIDATION_CONTEXT_TIMEOUT } from 'features/withdrawals/withdrawals-constants'; -import type { WrapFormInputType, WrapFormNetworkData } from './types'; -import type { TokensWrappable } from 'features/wsteth/shared/types'; +import type { WrapFormInputType, WrapFormValidationContext } from './types'; +import { TokensWrappable, TOKENS_TO_WRAP } from 'features/wsteth/shared/types'; const messageMaxAmount = (max: BigNumber, token: TokensWrappable) => `${getTokenDisplayName(token)} amount must not be greater than ${formatEther( @@ -21,31 +20,35 @@ const messageMaxAmount = (max: BigNumber, token: TokensWrappable) => export const WrapFormValidationResolver: Resolver< WrapFormInputType, - Promise -> = async (values, networkDataPromise) => { + Promise +> = async (values, validationContextPromise) => { const { amount, token } = values; try { invariant( - networkDataPromise, - 'network data must be presented as context promise', + validationContextPromise, + 'validation context must be presented as context promise', ); validateEtherAmount('amount', amount, token); - const networkData = await awaitWithTimeout( - networkDataPromise, + const { active, maxAmountETH, maxAmountStETH } = await awaitWithTimeout( + validationContextPromise, VALIDATION_CONTEXT_TIMEOUT, ); - const { maxAmount } = computeWrapFormContextValues({ token, networkData }); - invariant(maxAmount, 'maxAmount must be computed'); + if (active) { + const isSteth = token === TOKENS_TO_WRAP.STETH; + const maxAmount = isSteth ? maxAmountStETH : maxAmountETH; - validateBignumberMax( - 'amount', - amount, - maxAmount, - messageMaxAmount(maxAmount, token), - ); + invariant(maxAmount, 'maxAmount must be presented'); + + validateBignumberMax( + 'amount', + amount, + maxAmount, + messageMaxAmount(maxAmount, token), + ); + } return { values, From 459f739ffb920e68a8a0fab1607e98427ff7ed7b Mon Sep 17 00:00:00 2001 From: Dmitrii Podlesnyi Date: Mon, 28 Aug 2023 16:01:04 +0400 Subject: [PATCH 16/22] fix: do not show lock icon on wrap form if wallet not connected --- features/wsteth/wrap/hooks/use-wrap-tx-approve.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/wsteth/wrap/hooks/use-wrap-tx-approve.ts b/features/wsteth/wrap/hooks/use-wrap-tx-approve.ts index d693dd7b2..ada13cc5e 100644 --- a/features/wsteth/wrap/hooks/use-wrap-tx-approve.ts +++ b/features/wsteth/wrap/hooks/use-wrap-tx-approve.ts @@ -13,7 +13,7 @@ type UseWrapTxApproveArgs = { }; export const useWrapTxApprove = ({ amount, token }: UseWrapTxApproveArgs) => { - const { account } = useWeb3(); + const { active, account } = useWeb3(); const { chainId } = useSDK(); const [stethTokenAddress, wstethTokenAddress] = useMemo( @@ -37,7 +37,7 @@ export const useWrapTxApprove = ({ amount, token }: UseWrapTxApproveArgs) => { ); const isApprovalNeededBeforeWrap = - needsApprove && token === TOKENS_TO_WRAP.STETH; + active && needsApprove && token === TOKENS_TO_WRAP.STETH; return useMemo( () => ({ From bc31542898bbf9c32a7ba95b089427aec3348ff3 Mon Sep 17 00:00:00 2001 From: Dmitrii Podlesnyi Date: Mon, 28 Aug 2023 16:19:53 +0400 Subject: [PATCH 17/22] refactor: token select component options label support --- .../form/controls/token-select-request.tsx | 56 ++++--------------- .../wrap-form-controls/token-select-wrap.tsx | 11 +++- .../controls/token-select-hook-form.tsx | 25 +++++---- 3 files changed, 35 insertions(+), 57 deletions(-) diff --git a/features/withdrawals/request/form/controls/token-select-request.tsx b/features/withdrawals/request/form/controls/token-select-request.tsx index 8b4414310..aee282420 100644 --- a/features/withdrawals/request/form/controls/token-select-request.tsx +++ b/features/withdrawals/request/form/controls/token-select-request.tsx @@ -1,50 +1,14 @@ -import { TOKENS } from '@lido-sdk/constants'; -import { SelectIcon, Steth, Wsteth, Option } from '@lidofinance/lido-ui'; +import { + TOKENS, + TokenOption, + TokenSelectHookForm, +} from 'shared/hook-form/controls/token-select-hook-form'; -import { useController, useFormContext, useFormState } from 'react-hook-form'; -import { RequestFormInputType } from 'features/withdrawals/request/request-form-context'; - -import { getTokenDisplayName } from 'utils/getTokenDisplayName'; -import { TokensWithdrawable } from 'features/withdrawals/types/tokens-withdrawable'; -import { isValidationErrorTypeValidate } from 'shared/hook-form/validation/validation-error'; - -const iconsMap = { - [TOKENS.WSTETH]: , - [TOKENS.STETH]: , -} as const; +const OPTIONS: TokenOption[] = [ + { token: TOKENS.STETH }, + { token: TOKENS.WSTETH }, +]; export const TokenSelectRequest = () => { - const { setValue } = useFormContext(); - const { field } = useController({ - name: 'token', - }); - - const { errors } = useFormState({ name: 'amount' }); - - return ( - { - setValue('token', value, { - shouldDirty: false, - shouldTouch: false, - shouldValidate: false, - }); - setValue('amount', null, { - shouldDirty: false, - shouldTouch: false, - shouldValidate: false, - }); - }} - > - - - - ); + return ; }; diff --git a/features/wsteth/wrap/wrap-form-controls/token-select-wrap.tsx b/features/wsteth/wrap/wrap-form-controls/token-select-wrap.tsx index 8f86f9b3e..0e5fb68ee 100644 --- a/features/wsteth/wrap/wrap-form-controls/token-select-wrap.tsx +++ b/features/wsteth/wrap/wrap-form-controls/token-select-wrap.tsx @@ -3,7 +3,16 @@ import { TOKENS_TO_WRAP } from 'features/wsteth/shared/types'; import { MATOMO_CLICK_EVENTS } from 'config'; import { TokenSelectHookForm } from 'shared/hook-form/controls/token-select-hook-form'; -const OPTIONS = [TOKENS_TO_WRAP.ETH, TOKENS_TO_WRAP.STETH]; +const OPTIONS = [ + { + label: 'Lido (stETH)', + token: TOKENS_TO_WRAP.STETH, + }, + { + label: 'Ethereum (ETH)', + token: TOKENS_TO_WRAP.ETH, + }, +]; export const TokenSelectWrap = () => { return ( diff --git a/shared/hook-form/controls/token-select-hook-form.tsx b/shared/hook-form/controls/token-select-hook-form.tsx index be39470bd..299573410 100644 --- a/shared/hook-form/controls/token-select-hook-form.tsx +++ b/shared/hook-form/controls/token-select-hook-form.tsx @@ -10,31 +10,36 @@ export const TOKENS = { ETH: 'ETH', [TOKENS_SDK.STETH]: TOKENS_SDK.STETH, [TOKENS_SDK.WSTETH]: TOKENS_SDK.WSTETH, -}; +} as const; export type TOKENS = keyof typeof TOKENS; +export type TokenOption = { + label?: string; + token: TOKENS; +}; + const iconsMap = { [TOKENS.ETH]: , [TOKENS.STETH]: , [TOKENS.WSTETH]: , } as const; -type TokenSelectHookFormProps = { - options: T[]; +type TokenSelectHookFormProps = { + options: TokenOption[]; fieldName?: string; resetField?: string; errorField?: string; - onChange?: (value: T) => void; + onChange?: (value: TOKENS) => void; }; -export const TokenSelectHookForm = ({ +export const TokenSelectHookForm = ({ options, fieldName = 'token', resetField = 'amount', errorField = 'amount', onChange, -}: TokenSelectHookFormProps) => { - const { field } = useController({ name: fieldName }); +}: TokenSelectHookFormProps) => { + const { field } = useController>({ name: fieldName }); const { setValue, clearErrors } = useFormContext(); const { errors, defaultValues } = useFormState>({ @@ -46,7 +51,7 @@ export const TokenSelectHookForm = ({ {...field} icon={iconsMap[field.value]} error={isValidationErrorTypeValidate(errors[errorField]?.type)} - onChange={(value: T) => { + onChange={(value: TOKENS) => { setValue(fieldName, value, { shouldDirty: false, shouldTouch: false, @@ -61,9 +66,9 @@ export const TokenSelectHookForm = ({ onChange?.(value); }} > - {options.map((token) => ( + {options.map(({ label, token }) => ( ))} From 16560164d4b811852274460a2d5548665737865c Mon Sep 17 00:00:00 2001 From: Dmitrii Podlesnyi Date: Mon, 28 Aug 2023 16:20:28 +0400 Subject: [PATCH 18/22] fix: show will receive wsteth --- features/wsteth/wrap/wrap-form-context/wrap-form-context.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/features/wsteth/wrap/wrap-form-context/wrap-form-context.tsx b/features/wsteth/wrap/wrap-form-context/wrap-form-context.tsx index fbe650047..8489cb9e7 100644 --- a/features/wsteth/wrap/wrap-form-context/wrap-form-context.tsx +++ b/features/wsteth/wrap/wrap-form-context/wrap-form-context.tsx @@ -66,9 +66,7 @@ export const WrapFormProvider: React.FC = ({ children }) => { const isSteth = token === TOKENS_TO_WRAP.STETH; - const willReceiveWsteth = useWstethBySteth( - isSteth && approvalData.isApprovalNeededBeforeWrap ? Zero : amount ?? Zero, - ); + const willReceiveWsteth = useWstethBySteth(amount ?? Zero); const value = useMemo( (): WrapFormDataContextValueType => ({ From 32939c9a18431146957730ab5bf71efd7a2dbb20 Mon Sep 17 00:00:00 2001 From: Dmitrii Podlesnyi Date: Mon, 28 Aug 2023 16:22:15 +0400 Subject: [PATCH 19/22] fix: wrap max amount padded --- features/wsteth/wrap/hooks/use-wrap-form-network-data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/wsteth/wrap/hooks/use-wrap-form-network-data.ts b/features/wsteth/wrap/hooks/use-wrap-form-network-data.ts index d26b55fa3..45974adbd 100644 --- a/features/wsteth/wrap/hooks/use-wrap-form-network-data.ts +++ b/features/wsteth/wrap/hooks/use-wrap-form-network-data.ts @@ -28,7 +28,7 @@ export const useWrapFormNetworkData = () => { const maxAmountETH = useCurrencyMaxAmount({ limit: ethBalance, token: TOKENS_TO_WRAP.ETH, - padded: isMultisig, + padded: !isMultisig, gasLimit: gasLimitETH, }); From 5e3c2ef995db12ce5500f00636262967215eb75b Mon Sep 17 00:00:00 2001 From: Dmitrii Podlesnyi Date: Mon, 28 Aug 2023 17:59:27 +0400 Subject: [PATCH 20/22] fix: wrap form reset only amount field --- .../form-controller/form-controller.tsx | 21 ++++++++++++++++--- .../form-controller-wrap.tsx | 14 +++++++++++++ features/wsteth/wrap/wrap-form/wrap-form.tsx | 6 +++--- 3 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 features/wsteth/wrap/wrap-form-controls/form-controller-wrap.tsx diff --git a/features/wsteth/shared/form-controller/form-controller.tsx b/features/wsteth/shared/form-controller/form-controller.tsx index 6e98fe760..d65438dfd 100644 --- a/features/wsteth/shared/form-controller/form-controller.tsx +++ b/features/wsteth/shared/form-controller/form-controller.tsx @@ -1,15 +1,30 @@ -import { useEffect, useMemo } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { useWeb3 } from 'reef-knot/web3-react'; import { useFormContext } from 'react-hook-form'; import { useTransactionModal } from 'features/withdrawals/contexts/transaction-modal-context'; import { useFormControllerContext } from './form-controller-context'; -export const FormController: React.FC = ({ children }) => { +type FormControllerProps = { + reset?: () => void; +}; + +export const FormController: React.FC = ({ + reset: resetProp, + children, +}) => { const { active } = useWeb3(); - const { reset, handleSubmit } = useFormContext(); + const { handleSubmit, reset: resetDefault } = useFormContext(); const { onSubmit } = useFormControllerContext(); const { dispatchModalState } = useTransactionModal(); + const reset = useCallback(() => { + if (resetProp) { + resetProp(); + } else { + resetDefault(); + } + }, [resetDefault, resetProp]); + // Bind submit action const doSubmit = useMemo( () => diff --git a/features/wsteth/wrap/wrap-form-controls/form-controller-wrap.tsx b/features/wsteth/wrap/wrap-form-controls/form-controller-wrap.tsx new file mode 100644 index 000000000..bf3689c7d --- /dev/null +++ b/features/wsteth/wrap/wrap-form-controls/form-controller-wrap.tsx @@ -0,0 +1,14 @@ +import { useCallback } from 'react'; +import { FormController } from 'features/wsteth/shared/form-controller/form-controller'; +import { useFormContext } from 'react-hook-form'; + +export const FormControllerWrap: React.FC = ({ children }) => { + const { setValue, clearErrors } = useFormContext(); + + const handleReset = useCallback(() => { + setValue('amount', undefined); + clearErrors('amount'); + }, [clearErrors, setValue]); + + return {children}; +}; diff --git a/features/wsteth/wrap/wrap-form/wrap-form.tsx b/features/wsteth/wrap/wrap-form/wrap-form.tsx index 732bd9991..4c4c29bca 100644 --- a/features/wsteth/wrap/wrap-form/wrap-form.tsx +++ b/features/wsteth/wrap/wrap-form/wrap-form.tsx @@ -6,7 +6,7 @@ import { WrapFormStats } from './wrap-stats'; import { WrapFormTxModal } from './wrap-form-tx-modal'; import { WrapFormProvider } from '../wrap-form-context/wrap-form-context'; import { TransactionModalProvider } from 'features/withdrawals/contexts/transaction-modal-context'; -import { FormController } from 'features/wsteth/shared/form-controller/form-controller'; +import { FormControllerWrap } from '../wrap-form-controls/form-controller-wrap'; import { TokenSelectWrap } from '../wrap-form-controls/token-select-wrap'; import { TokenAmountInputWrap } from '../wrap-form-controls/token-amount-input-wrap'; import { SubmitButtonWrap } from '../wrap-form-controls/submit-button-wrap'; @@ -19,14 +19,14 @@ export const WrapForm: React.FC = memo(() => { - + - + From 10a5aeed920e56ba4802cef7e2711cc4f1d9f9f3 Mon Sep 17 00:00:00 2001 From: Dmitrii Podlesnyi Date: Tue, 29 Aug 2023 16:28:54 +0400 Subject: [PATCH 21/22] fix: unlock submit buttons text --- .../request/form/controls/submit-button-request.tsx | 4 +++- .../wsteth/wrap/wrap-form-controls/submit-button-wrap.tsx | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/features/withdrawals/request/form/controls/submit-button-request.tsx b/features/withdrawals/request/form/controls/submit-button-request.tsx index de140a2bd..50efc735a 100644 --- a/features/withdrawals/request/form/controls/submit-button-request.tsx +++ b/features/withdrawals/request/form/controls/submit-button-request.tsx @@ -7,6 +7,7 @@ import { import { SubmitButtonHookForm } from 'shared/hook-form/controls/submit-button-hook-form'; import { useFormState } from 'react-hook-form'; import { isValidationErrorTypeUnhandled } from 'shared/hook-form/validation/validation-error'; +import { useIsMultisig } from 'shared/hooks/useIsMultisig'; // conditional render breaks useFormState, so it can't be inside SubmitButton export const useRequestSubmitButtonProps = (): SubmitButtonRequestProps => { @@ -31,9 +32,10 @@ export const SubmitButtonRequest = ({ loading, disabled, }: SubmitButtonRequestProps) => { + const [isMultisig] = useIsMultisig(); const { isTokenLocked } = useRequestFormData(); const buttonTitle = isTokenLocked - ? 'Unlock tokens for withdrawal' + ? `Unlock tokens ${isMultisig ? 'for' : 'and'} withdrawal` : 'Request withdrawal'; return ( diff --git a/features/wsteth/wrap/wrap-form-controls/submit-button-wrap.tsx b/features/wsteth/wrap/wrap-form-controls/submit-button-wrap.tsx index ce0c5273b..55169b36f 100644 --- a/features/wsteth/wrap/wrap-form-controls/submit-button-wrap.tsx +++ b/features/wsteth/wrap/wrap-form-controls/submit-button-wrap.tsx @@ -3,7 +3,8 @@ import { useWrapFormData } from '../wrap-form-context'; import { SubmitButtonHookForm } from 'shared/hook-form/controls/submit-button-hook-form'; export const SubmitButtonWrap = () => { - const { isApprovalNeededBeforeWrap: isLocked } = useWrapFormData(); + const { isMultisig, isApprovalNeededBeforeWrap: isLocked } = + useWrapFormData(); return ( { errorField="amount" data-testid="wrapBtn" > - {isLocked ? 'Unlock token to wrap' : 'Wrap'} + {isLocked ? `Unlock tokens ${isMultisig ? 'to' : 'and'} wrap` : 'Wrap'} ); }; From cc9a855057a5c5eaa2fe825451790816220a28f6 Mon Sep 17 00:00:00 2001 From: Dmitrii Podlesnyi Date: Tue, 29 Aug 2023 16:30:08 +0400 Subject: [PATCH 22/22] fix: debounce amount calculation on wrap form --- .../wsteth/wrap/wrap-form-context/wrap-form-context.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/features/wsteth/wrap/wrap-form-context/wrap-form-context.tsx b/features/wsteth/wrap/wrap-form-context/wrap-form-context.tsx index 8489cb9e7..22af29230 100644 --- a/features/wsteth/wrap/wrap-form-context/wrap-form-context.tsx +++ b/features/wsteth/wrap/wrap-form-context/wrap-form-context.tsx @@ -1,7 +1,7 @@ import invariant from 'tiny-invariant'; import { useMemo, createContext, useContext } from 'react'; import { useForm, FormProvider } from 'react-hook-form'; -import { useWstethBySteth } from 'shared/hooks'; +import { useDebouncedValue, useWstethBySteth } from 'shared/hooks'; import { useWrapTxApprove } from '../hooks/use-wrap-tx-approve'; import { useWrapFormNetworkData } from '../hooks/use-wrap-form-network-data'; import { useWrapFormProcessor } from '../hooks/use-wrap-form-processing'; @@ -66,7 +66,9 @@ export const WrapFormProvider: React.FC = ({ children }) => { const isSteth = token === TOKENS_TO_WRAP.STETH; - const willReceiveWsteth = useWstethBySteth(amount ?? Zero); + const amountDebounced = useDebouncedValue(amount, 500); + + const willReceiveWsteth = useWstethBySteth(amountDebounced ?? Zero); const value = useMemo( (): WrapFormDataContextValueType => ({