diff --git a/features/adjustment/mint/form/address-field/address-field.tsx b/features/adjustment/mint/form/address-field/address-field.tsx
index 3cca8abc..8986f2fc 100644
--- a/features/adjustment/mint/form/address-field/address-field.tsx
+++ b/features/adjustment/mint/form/address-field/address-field.tsx
@@ -14,7 +14,6 @@ export const AddressField = () => {
});
};
- // TODO: add error
return (
{
{isLoading && }
{/*TODO: replace static by real data*/}
- {data && {'$99.99'}}
- {isError && Is not available}
- {!isLoading && !data && !isError && {'$0'}}
+ {data?.result && !isLoading && {data?.result}}
+ {isError && !isLoading && Is not available}
+ {!isLoading && !data?.result && !isError && -}
);
diff --git a/features/adjustment/mint/hooks/use-mint.ts b/features/adjustment/mint/hooks/use-mint.ts
index 3f09d201..25a7cea0 100644
--- a/features/adjustment/mint/hooks/use-mint.ts
+++ b/features/adjustment/mint/hooks/use-mint.ts
@@ -2,8 +2,8 @@ import { useCallback } from 'react';
import {
useConfig,
useSimulateContract,
- useWaitForTransactionReceipt,
useWriteContract,
+ usePublicClient,
UseSimulateContractParameters,
} from 'wagmi';
import { Address } from 'viem';
@@ -11,12 +11,17 @@ import { Address } from 'viem';
import { dashboardAbi } from 'abi/dashboard-abi';
import { useDappStatus } from 'modules/web3/hooks/use-dapp-status';
import { useVaultInfo } from 'features/overview/contexts';
+import {
+ SubmitStep,
+ SubmitStepEnum,
+} from 'shared/components/submit-modal/types';
+import invariant from 'tiny-invariant';
export const useMint = (onMutate = () => {}) => {
const { chainId } = useDappStatus();
const wagmiConfig = useConfig();
const { activeVault } = useVaultInfo();
-
+ const publicClient = usePublicClient();
const { data: mintTx, writeContractAsync } = useWriteContract({
config: wagmiConfig,
mutation: {
@@ -24,27 +29,37 @@ export const useMint = (onMutate = () => {}) => {
},
});
- const { data: mintReceipt } = useWaitForTransactionReceipt({
- hash: mintTx,
- });
-
const callMint = useCallback(
- async (recipient: Address, amount: bigint, token: string) => {
- return await writeContractAsync({
+ async (
+ recipient: Address,
+ amount: bigint,
+ token: string,
+ setModalState: (submitStep: { step: SubmitStep; tx?: Address }) => void,
+ ) => {
+ invariant(publicClient, '[useMintDashboard] publicClient is undefined');
+
+ setModalState({ step: SubmitStepEnum.confirming });
+ const tx = await writeContractAsync({
abi: dashboardAbi,
address: activeVault?.owner as Address,
functionName: token === 'stETH' ? 'mintStETH' : 'mintWstETH',
args: [recipient, amount],
chainId,
});
+
+ setModalState({ step: SubmitStepEnum.submitting, tx });
+ await publicClient.waitForTransactionReceipt({
+ hash: tx,
+ });
+
+ return tx;
},
- [chainId, writeContractAsync, activeVault?.owner],
+ [chainId, writeContractAsync, activeVault?.owner, publicClient],
);
return {
callMint,
mintTx,
- mintReceipt,
};
};
diff --git a/features/adjustment/mint/mint-form-context/mint-form-provider.tsx b/features/adjustment/mint/mint-form-context/mint-form-provider.tsx
index 11110f0f..80c1c87a 100644
--- a/features/adjustment/mint/mint-form-context/mint-form-provider.tsx
+++ b/features/adjustment/mint/mint-form-context/mint-form-provider.tsx
@@ -5,6 +5,7 @@ import {
useCallback,
createContext,
useContext,
+ useState,
} from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import invariant from 'tiny-invariant';
@@ -19,6 +20,12 @@ import {
FormControllerContextValueType,
} from 'shared/hook-form/form-controller';
import { MintFormSchema } from 'features/adjustment/mint/types';
+import { SubmitModal } from 'shared/components';
+import {
+ SubmitStep,
+ SubmitStepEnum,
+} from 'shared/components/submit-modal/types';
+import { Address } from 'viem';
type MintDataContextValue = {
mintableStETH: bigint;
@@ -39,11 +46,15 @@ export const useMintFormData = () => {
};
export const MintFormProvider: FC<{ children: ReactNode }> = ({ children }) => {
+ const [submitStep, setSubmitStep] = useState<{
+ step: SubmitStep;
+ tx?: Address;
+ }>(() => ({ step: SubmitStepEnum.edit }));
const formObject = useForm({
defaultValues: {
amount: undefined,
token: 'stETH',
- recipient: undefined,
+ recipient: '' as Address,
},
mode: 'all',
reValidateMode: 'onChange',
@@ -60,16 +71,36 @@ export const MintFormProvider: FC<{ children: ReactNode }> = ({ children }) => {
}, [activeVault]);
const { retryEvent, retryFire } = useFormControllerRetry();
+ const setModalState = useCallback(
+ (submitStep: { step: SubmitStep; tx?: Address }) => {
+ setSubmitStep(submitStep);
+ },
+ [],
+ );
const onSubmit = useCallback(
async ({ recipient, amount, token }: MintFormSchema) => {
if (amount && recipient) {
- await callMint(recipient, amount, token);
- return true;
+ try {
+ setModalState({ step: SubmitStepEnum.initiate });
+ const tx = await callMint(recipient, amount, token, setModalState);
+ setModalState({ step: SubmitStepEnum.success, tx });
+ return true;
+ } catch (err) {
+ if (
+ err instanceof Error &&
+ err.message.includes('User rejected the request')
+ ) {
+ setModalState({ step: SubmitStepEnum.reject });
+ } else {
+ setModalState({ step: SubmitStepEnum.error });
+ }
+ }
}
return false;
},
+ // eslint-disable-next-line react-hooks/exhaustive-deps
[callMint],
);
@@ -89,6 +120,7 @@ export const MintFormProvider: FC<{ children: ReactNode }> = ({ children }) => {
{children}
+
diff --git a/features/adjustment/mint/types.ts b/features/adjustment/mint/types.ts
index ff93bc83..a2051261 100644
--- a/features/adjustment/mint/types.ts
+++ b/features/adjustment/mint/types.ts
@@ -3,5 +3,5 @@ import { Address } from 'viem';
export type MintFormSchema = {
amount: bigint | undefined;
token: string;
- recipient: Address | undefined;
+ recipient: Address;
};
diff --git a/features/adjustment/repay/form/balance/balance.tsx b/features/adjustment/repay/form/balance/balance.tsx
index b76e0c47..11aac132 100644
--- a/features/adjustment/repay/form/balance/balance.tsx
+++ b/features/adjustment/repay/form/balance/balance.tsx
@@ -29,7 +29,9 @@ export const Balance = () => {
{formatBalance(balance).trimmed} {token}
)}
- {isError && stEth amount is not available}
+ {isError && !isLoading && (
+ stEth amount is not available
+ )}
);
diff --git a/features/adjustment/repay/form/feature-tx-info/feature-tx-info.tsx b/features/adjustment/repay/form/feature-tx-info/feature-tx-info.tsx
index 41739ce4..dfb574a8 100644
--- a/features/adjustment/repay/form/feature-tx-info/feature-tx-info.tsx
+++ b/features/adjustment/repay/form/feature-tx-info/feature-tx-info.tsx
@@ -30,10 +30,9 @@ export const FeatureTxInfo = () => {
Transaction cost
{isLoading && }
- {/*TODO: replace static by real data*/}
- {data && {'$99.99'}}
- {isError && Is not available}
- {!isLoading && !data && !isError && {'$0'}}
+ {!!data?.result && {data?.result}}
+ {isError && !isLoading && Is not available}
+ {!isLoading && !data?.result && !isError && -}
);
diff --git a/features/adjustment/repay/hooks/use-burn.ts b/features/adjustment/repay/hooks/use-burn.ts
index 044f22ec..84d136f5 100644
--- a/features/adjustment/repay/hooks/use-burn.ts
+++ b/features/adjustment/repay/hooks/use-burn.ts
@@ -3,17 +3,24 @@ import {
useConfig,
useWaitForTransactionReceipt,
useWriteContract,
+ usePublicClient,
} from 'wagmi';
import { Address } from 'viem';
import { dashboardAbi } from 'abi/dashboard-abi';
import { useDappStatus } from 'modules/web3/hooks/use-dapp-status';
import { useVaultInfo } from 'features/overview/contexts';
+import {
+ SubmitStep,
+ SubmitStepEnum,
+} from 'shared/components/submit-modal/types';
+import invariant from 'tiny-invariant';
export const useBurn = (onMutate = () => {}) => {
const { chainId } = useDappStatus();
const wagmiConfig = useConfig();
const { activeVault } = useVaultInfo();
+ const publicClient = usePublicClient();
const { data: burnTx, writeContractAsync } = useWriteContract({
config: wagmiConfig,
@@ -27,16 +34,34 @@ export const useBurn = (onMutate = () => {}) => {
});
const callBurn = useCallback(
- async ({ token, amount }: { token: string; amount: bigint }) => {
- return await writeContractAsync({
+ async ({
+ token,
+ amount,
+ setModalState,
+ }: {
+ token: string;
+ amount: bigint;
+ setModalState: (submitStep: { step: SubmitStep; tx?: Address }) => void;
+ }) => {
+ invariant(publicClient, '[useBurn] publicClient is undefined');
+
+ setModalState({ step: SubmitStepEnum.confirming });
+ const tx = await writeContractAsync({
abi: dashboardAbi,
address: activeVault?.owner as Address,
functionName: token === 'stETH' ? 'burnStETH' : 'burnWstETH',
args: [amount],
chainId,
});
+
+ setModalState({ step: SubmitStepEnum.submitting, tx });
+ await publicClient.waitForTransactionReceipt({
+ hash: tx,
+ });
+
+ return tx;
},
- [chainId, activeVault?.owner, writeContractAsync],
+ [chainId, activeVault?.owner, writeContractAsync, publicClient],
);
return {
diff --git a/features/adjustment/repay/repay-form-context/repay-form-provider.tsx b/features/adjustment/repay/repay-form-context/repay-form-provider.tsx
index d33e5e37..ed8888c1 100644
--- a/features/adjustment/repay/repay-form-context/repay-form-provider.tsx
+++ b/features/adjustment/repay/repay-form-context/repay-form-provider.tsx
@@ -4,6 +4,7 @@ import {
useCallback,
createContext,
useContext,
+ useState,
} from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import invariant from 'tiny-invariant';
@@ -19,6 +20,9 @@ import {
} from 'shared/hook-form/form-controller';
import { RepayFormSchema } from 'features/adjustment/repay/types';
+import { SubmitModal } from 'shared/components';
+import { SubmitStep, SubmitStepEnum } from 'shared/components/submit-modal';
+import { Address } from 'viem';
type RepayDataContextValue = {
stEthBalance: bigint | undefined;
@@ -45,6 +49,10 @@ export const useRepayFormData = () => {
};
export const RepayFormProvider = ({ children }: { children: ReactNode }) => {
+ const [submitStep, setSubmitStep] = useState<{
+ step: SubmitStep;
+ tx?: Address;
+ }>(() => ({ step: SubmitStepEnum.edit }));
const formObject = useForm({
defaultValues: {
amount: undefined,
@@ -92,16 +100,38 @@ export const RepayFormProvider = ({ children }: { children: ReactNode }) => {
);
const { retryEvent, retryFire } = useFormControllerRetry();
+ const setModalState = useCallback(
+ (submitStep: { step: SubmitStep; tx?: Address }) => {
+ setSubmitStep(submitStep);
+ },
+ [],
+ );
const onSubmit = useCallback(
async ({ amount, token }: RepayFormSchema) => {
- if (amount) {
- await callBurn({ amount, token });
- return true;
+ try {
+ if (amount) {
+ setModalState({ step: SubmitStepEnum.initiate });
+ const tx = await callBurn({ amount, token, setModalState });
+ setModalState({ step: SubmitStepEnum.success, tx });
+ return true;
+ }
+
+ return false;
+ } catch (err) {
+ if (
+ err instanceof Error &&
+ err.message.includes('User rejected the request')
+ ) {
+ setModalState({ step: SubmitStepEnum.reject });
+ } else {
+ setModalState({ step: SubmitStepEnum.error });
+ }
}
return false;
},
+ // eslint-disable-next-line react-hooks/exhaustive-deps
[callBurn],
);
@@ -121,6 +151,7 @@ export const RepayFormProvider = ({ children }: { children: ReactNode }) => {
{children}
+
diff --git a/features/claim/claim-form/claim-form-context/claim-form-provider.tsx b/features/claim/claim-form/claim-form-context/claim-form-provider.tsx
index 21b63e1d..19b36b60 100644
--- a/features/claim/claim-form/claim-form-context/claim-form-provider.tsx
+++ b/features/claim/claim-form/claim-form-context/claim-form-provider.tsx
@@ -5,8 +5,9 @@ import {
useCallback,
createContext,
useContext,
+ useState,
} from 'react';
-import { Address, isAddress } from 'viem';
+import { Address, isAddress, ReadContractErrorType } from 'viem';
import { useReadContract } from 'wagmi';
import { FormProvider, useForm } from 'react-hook-form';
@@ -23,11 +24,17 @@ import {
import { dashboardAbi } from 'abi/dashboard-abi';
import { ClaimFormSchema } from 'features/claim/claim-form/types';
import invariant from 'tiny-invariant';
+import {
+ SubmitStep,
+ SubmitStepEnum,
+} from 'shared/components/submit-modal/types';
+import { SubmitModal } from '../submit-modal';
type ClaimDataContextValue = {
availableToClaim: bigint | undefined;
isLoadingClaimInfo: boolean;
isErrorClaimInfo: boolean;
+ errorClaimInfo: ReadContractErrorType | null;
};
const ClaimDataContext = createContext(null);
@@ -46,12 +53,17 @@ export const useClaimFormData = () => {
export const ClaimFormProvider: FC<{ children: ReactNode }> = ({
children,
}) => {
+ const [submitStep, setSubmitStep] = useState<{
+ step: SubmitStep;
+ tx?: Address;
+ }>(() => ({ step: SubmitStepEnum.edit }));
const { activeVault } = useVaultInfo();
const {
data: availableToClaim,
isFetching: isLoadingClaimInfo,
isError: isErrorClaimInfo,
+ error: errorClaimInfo,
} = useReadContract({
abi: dashboardAbi,
address: activeVault?.owner,
@@ -66,8 +78,9 @@ export const ClaimFormProvider: FC<{ children: ReactNode }> = ({
availableToClaim,
isLoadingClaimInfo,
isErrorClaimInfo,
+ errorClaimInfo,
}),
- [availableToClaim, isLoadingClaimInfo, isErrorClaimInfo],
+ [availableToClaim, isLoadingClaimInfo, isErrorClaimInfo, errorClaimInfo],
);
const formObject = useForm({
@@ -80,17 +93,36 @@ export const ClaimFormProvider: FC<{ children: ReactNode }> = ({
const { callClaim } = useClaim();
const { retryEvent, retryFire } = useFormControllerRetry();
+ const setModalState = useCallback(
+ (submitStep: { step: SubmitStep; tx?: Address }) => {
+ setSubmitStep(submitStep);
+ },
+ [],
+ );
const onSubmit = useCallback(
async ({ recipient }: ClaimFormSchema) => {
- if (isAddress(recipient as string)) {
- // TODO: resolve recipient if ens domain
- await callClaim(recipient as Address);
- return true;
+ if (recipient && isAddress(recipient)) {
+ try {
+ setModalState({ step: SubmitStepEnum.initiate });
+ const tx = await callClaim(recipient, setModalState);
+ setModalState({ step: SubmitStepEnum.success, tx });
+ return true;
+ } catch (err) {
+ if (
+ err instanceof Error &&
+ err.message.includes('User rejected the request')
+ ) {
+ setModalState({ step: SubmitStepEnum.reject });
+ } else {
+ setModalState({ step: SubmitStepEnum.error });
+ }
+ }
}
return false;
},
+ // eslint-disable-next-line react-hooks/exhaustive-deps
[callClaim],
);
@@ -110,6 +142,7 @@ export const ClaimFormProvider: FC<{ children: ReactNode }> = ({
{children}
+
diff --git a/features/claim/claim-form/form/balance/balance.tsx b/features/claim/claim-form/form/balance/balance.tsx
index 3517b466..9c6c5960 100644
--- a/features/claim/claim-form/form/balance/balance.tsx
+++ b/features/claim/claim-form/form/balance/balance.tsx
@@ -8,8 +8,9 @@ export const Balance = () => {
const { isLoadingClaimInfo, availableToClaim, isErrorClaimInfo } =
useClaimFormData();
+ const isAvailableToClaim = typeof availableToClaim === 'bigint';
const isLoading =
- (!isErrorClaimInfo && !availableToClaim) || isLoadingClaimInfo;
+ (!isErrorClaimInfo && !isAvailableToClaim) || isLoadingClaimInfo;
return (
@@ -18,7 +19,7 @@ export const Balance = () => {
Available to claim
{isLoading && }
- {!!availableToClaim && (
+ {isAvailableToClaim && (
{formatBalance(availableToClaim).trimmed} ETH
)}
diff --git a/features/claim/claim-form/form/feature-tx-info/feature-tx-info.tsx b/features/claim/claim-form/form/feature-tx-info/feature-tx-info.tsx
index 9d23480d..6faf8914 100644
--- a/features/claim/claim-form/form/feature-tx-info/feature-tx-info.tsx
+++ b/features/claim/claim-form/form/feature-tx-info/feature-tx-info.tsx
@@ -3,10 +3,11 @@ import { useFormContext } from 'react-hook-form';
import { Loader, Text } from '@lidofinance/lido-ui';
import { AmountInfo, InfoRow, Wrapper } from './styles';
import { useSimulationClaim } from 'features/claim/claim-form/hooks';
+import { Address } from 'viem';
export const FeatureTxInfo = () => {
- const { getValues } = useFormContext();
- const { recipient } = getValues();
+ const { watch } = useFormContext();
+ const recipient: Address = watch('recipient');
const { data, isLoading, isError } = useSimulationClaim(recipient);
return (
@@ -16,9 +17,13 @@ export const FeatureTxInfo = () => {
Transaction cost
{isLoading && }
- {data && {'$99.99'}}
- {isError && Is not available}
- {!isLoading && !data && !isError && {'$0'}}
+ {/*TODO: get simulated data*/}
+ {/*TODO: show error message*/}
+ {!!data?.result && !isLoading && (
+ {data?.result}
+ )}
+ {isError && !isLoading && -}
+ {!isLoading && !data && !isError && -}
);
diff --git a/features/claim/claim-form/hooks/use-claim.ts b/features/claim/claim-form/hooks/use-claim.ts
index 6846bc8c..c1108458 100644
--- a/features/claim/claim-form/hooks/use-claim.ts
+++ b/features/claim/claim-form/hooks/use-claim.ts
@@ -2,7 +2,7 @@ import { useCallback } from 'react';
import {
useConfig,
useSimulateContract,
- useWaitForTransactionReceipt,
+ usePublicClient,
useWriteContract,
} from 'wagmi';
import { Address } from 'viem';
@@ -10,11 +10,17 @@ import { Address } from 'viem';
import { dashboardAbi } from 'abi/dashboard-abi';
import { useDappStatus } from 'modules/web3/hooks/use-dapp-status';
import { useVaultInfo } from 'features/overview/contexts';
+import invariant from 'tiny-invariant';
+import {
+ SubmitStep,
+ SubmitStepEnum,
+} from 'shared/components/submit-modal/types';
export const useClaim = (onMutate = () => {}) => {
const { chainId } = useDappStatus();
const wagmiConfig = useConfig();
const { activeVault } = useVaultInfo();
+ const publicClient = usePublicClient();
const owner = activeVault?.owner;
const { data: claimTx, writeContractAsync } = useWriteContract({
@@ -24,27 +30,39 @@ export const useClaim = (onMutate = () => {}) => {
},
});
- const { data: claimReceipt } = useWaitForTransactionReceipt({
- hash: claimTx,
- });
-
const callClaim = useCallback(
- async (recipient: Address) => {
- return await writeContractAsync({
+ async (
+ recipient: Address,
+ setModalState: (submitStep: { step: SubmitStep; tx?: Address }) => void,
+ ) => {
+ invariant(owner, '[useClaimDashboard] owner is not available');
+ invariant(
+ publicClient,
+ '[useClaimDashboard] publicClient is not available',
+ );
+
+ setModalState({ step: SubmitStepEnum.confirming });
+ const tx = await writeContractAsync({
abi: dashboardAbi,
- address: owner as Address,
+ address: owner,
functionName: 'claimNodeOperatorFee',
args: [recipient],
chainId,
});
+
+ setModalState({ step: SubmitStepEnum.submitting, tx });
+ await publicClient.waitForTransactionReceipt({
+ hash: tx,
+ });
+
+ return tx;
},
- [chainId, writeContractAsync, owner],
+ [chainId, writeContractAsync, owner, publicClient],
);
return {
callClaim,
claimTx,
- claimReceipt,
};
};
diff --git a/features/claim/claim-form/submit-modal/index.ts b/features/claim/claim-form/submit-modal/index.ts
new file mode 100644
index 00000000..1c0e3509
--- /dev/null
+++ b/features/claim/claim-form/submit-modal/index.ts
@@ -0,0 +1 @@
+export { SubmitModal } from './submit-modal';
diff --git a/features/claim/claim-form/submit-modal/styles.ts b/features/claim/claim-form/submit-modal/styles.ts
new file mode 100644
index 00000000..dfca0ac4
--- /dev/null
+++ b/features/claim/claim-form/submit-modal/styles.ts
@@ -0,0 +1,8 @@
+import styled from 'styled-components';
+
+export const Content = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${({ theme }) => theme.spaceMap.xl}px;
+ margin-top: ${({ theme }) => theme.spaceMap.sm}px;
+`;
diff --git a/features/claim/claim-form/submit-modal/submit-modal.tsx b/features/claim/claim-form/submit-modal/submit-modal.tsx
new file mode 100644
index 00000000..def6ae51
--- /dev/null
+++ b/features/claim/claim-form/submit-modal/submit-modal.tsx
@@ -0,0 +1,140 @@
+import { FC, useMemo } from 'react';
+import { useRouter } from 'next/router';
+import { useFormContext } from 'react-hook-form';
+
+import {
+ Loader,
+ Modal,
+ Text,
+ Success,
+ Error,
+ type ModalProps,
+ Button,
+} from '@lidofinance/lido-ui';
+
+import { useFormControllerContext } from 'shared/hook-form/form-controller';
+import { ButtonLink, TxLinkEtherscan } from 'shared/components';
+import { Content } from './styles';
+
+import {
+ SubmitStepEnum,
+ SubmitStep,
+} from 'shared/components/submit-modal/types';
+import { AppPaths } from 'consts/urls';
+import { useVaultInfo } from 'features/overview/contexts';
+import { Address } from 'viem';
+
+const getIconComponent = (step: SubmitStep) => {
+ switch (step) {
+ case SubmitStepEnum.success:
+ return ;
+ case SubmitStepEnum.reject:
+ return ;
+ case SubmitStepEnum.error:
+ return ;
+ default:
+ return ;
+ }
+};
+
+const getModalTitle = (step: SubmitStep) => {
+ switch (step) {
+ case SubmitStepEnum.success:
+ return 'Fees was getting successfully';
+ case SubmitStepEnum.reject:
+ return 'Wallet tx signature';
+ case SubmitStepEnum.error:
+ return 'Transaction error';
+ default:
+ return 'You are requesting fees';
+ }
+};
+
+const getModalSubTitle = (step: SubmitStep) => {
+ switch (step) {
+ case SubmitStepEnum.submitting:
+ return 'Awaiting wallet signature';
+ case SubmitStepEnum.reject:
+ return 'User denied transaction signature';
+ case SubmitStepEnum.error:
+ return 'Got error when called contract simulation or transaction';
+ default:
+ return '';
+ }
+};
+
+interface SubmitModalProps extends ModalProps {
+ submitStep: {
+ step: SubmitStep;
+ tx?: Address;
+ };
+ setModalState: ({ step }: { step: SubmitStep }) => void;
+}
+
+export const SubmitModal: FC = ({
+ submitStep,
+ setModalState,
+}) => {
+ const router = useRouter();
+ const {
+ formState: { isSubmitting, isSubmitted },
+ } = useFormContext();
+ const { retryFire } = useFormControllerContext();
+ const { step, tx } = submitStep ?? {};
+ const { activeVault } = useVaultInfo();
+
+ const iconComponent = useMemo(() => getIconComponent(step), [step]);
+ const title = getModalTitle(step);
+ const subtitle = getModalSubTitle(step);
+
+ const handleNavigateToVault = () => {
+ void router.push(`/${activeVault?.address}/${AppPaths.overview}`);
+ };
+
+ const handleCloseModal = () => {
+ setModalState({ step: SubmitStepEnum.edit });
+ };
+
+ return (
+
+
+ {step === SubmitStepEnum.initiate && (
+
+ Wait for wallet confirmation window
+
+ )}
+
+ {step === SubmitStepEnum.confirming && (
+
+ Confirm this transaction in your wallet
+
+ )}
+
+ {step === SubmitStepEnum.success && (
+
+ )}
+
+ {(SubmitStepEnum.success === step ||
+ SubmitStepEnum.submitting === step) &&
+ tx && }
+
+ {step === SubmitStepEnum.reject && (
+ retry
+ )}
+
+ {step === SubmitStepEnum.error && (
+ close
+ )}
+
+
+ );
+};
diff --git a/features/claim/claim-form/types.ts b/features/claim/claim-form/types.ts
index df91e904..590d92e1 100644
--- a/features/claim/claim-form/types.ts
+++ b/features/claim/claim-form/types.ts
@@ -1,3 +1,3 @@
export type ClaimFormSchema = {
- recipient: string | null;
+ recipient: string;
};
diff --git a/features/create-vault/create-vault-form/submit-modal/submit-modal.tsx b/features/create-vault/create-vault-form/submit-modal/submit-modal.tsx
index 2bcec850..98bf204f 100644
--- a/features/create-vault/create-vault-form/submit-modal/submit-modal.tsx
+++ b/features/create-vault/create-vault-form/submit-modal/submit-modal.tsx
@@ -21,27 +21,42 @@ import { SubmitStepEnum, SubmitStep } from 'features/create-vault/types';
import { AppPaths } from '../../../../consts/urls';
const getIconComponent = (step: SubmitStep) => {
- if (step === SubmitStepEnum.success)
- return ;
- if (step === SubmitStepEnum.reject) return ;
- if (step === SubmitStepEnum.error) return ;
- return ;
+ switch (step) {
+ case SubmitStepEnum.success:
+ return ;
+ case SubmitStepEnum.reject:
+ return ;
+ case SubmitStepEnum.error:
+ return ;
+ default:
+ return ;
+ }
};
const getModalTitle = (step: SubmitStep) => {
- if (step === SubmitStepEnum.success) return 'New vault has been created';
- if (step === SubmitStepEnum.reject) return 'Wallet tx signature';
- if (step === SubmitStepEnum.error) return 'Simulation error';
- return 'You are creating a new vault';
+ switch (step) {
+ case SubmitStepEnum.success:
+ return 'New vault has been created';
+ case SubmitStepEnum.reject:
+ return 'Wallet tx signature';
+ case SubmitStepEnum.error:
+ return 'Simulation error';
+ default:
+ return 'You are creating a new vault';
+ }
};
const getModalSubTitle = (step: SubmitStep) => {
- if (step === SubmitStepEnum.submitting) return 'Awaiting wallet signature';
- if (step === SubmitStepEnum.reject)
- return 'User denied transaction signature';
- if (step === SubmitStepEnum.error)
- return 'Got error when called contract simulation';
- return '';
+ switch (step) {
+ case SubmitStepEnum.submitting:
+ return 'Awaiting wallet signature';
+ case SubmitStepEnum.reject:
+ return 'User denied transaction signature';
+ case SubmitStepEnum.error:
+ return 'Got error when called contract simulation';
+ default:
+ return '';
+ }
};
export const SubmitModal: FC = () => {
diff --git a/features/home/all-vaults/all-vaults.tsx b/features/home/all-vaults/all-vaults.tsx
index 321715e4..625e7b9a 100644
--- a/features/home/all-vaults/all-vaults.tsx
+++ b/features/home/all-vaults/all-vaults.tsx
@@ -1,17 +1,19 @@
import { Loader, Pagination } from '@lidofinance/lido-ui';
import { VaultTable } from 'features/home/components/vault-table';
import { AllVaultsWrapper } from './styles';
-import { useVaultsDataAll } from 'modules/vaults';
+import { useVaultsDataAll, VAULTS_PER_PAGE } from 'modules/vaults';
+import { useConnectedVaultsNumber } from 'features/home/hooks';
export const AllVaults = () => {
const {
vaults,
isLoading: isLoadingAllVaults,
- pagesCount,
handlePagination,
page,
} = useVaultsDataAll();
- const showPagination = !!pagesCount;
+ const { data } = useConnectedVaultsNumber();
+ const pagesCount = Math.ceil(Number(data ?? 0) / VAULTS_PER_PAGE);
+ const showPagination = !!vaults && vaults?.length > 0 && pagesCount > 1;
return (
diff --git a/features/home/hooks/index.ts b/features/home/hooks/index.ts
new file mode 100644
index 00000000..ff8b55a6
--- /dev/null
+++ b/features/home/hooks/index.ts
@@ -0,0 +1 @@
+export { useConnectedVaultsNumber } from './use-connected-vaults-number';
diff --git a/features/home/hooks/use-connected-vaults-number.ts b/features/home/hooks/use-connected-vaults-number.ts
new file mode 100644
index 00000000..8a250cc6
--- /dev/null
+++ b/features/home/hooks/use-connected-vaults-number.ts
@@ -0,0 +1,19 @@
+import { usePublicClient, useReadContract } from 'wagmi';
+import { getContractAddress } from 'config';
+import { VaultHubAbi } from 'abi/vault-hub';
+
+export const useConnectedVaultsNumber = () => {
+ const publicClient = usePublicClient();
+ const address = publicClient?.chain.id
+ ? getContractAddress(publicClient.chain.id, 'vaultHub')
+ : undefined;
+
+ return useReadContract({
+ address,
+ abi: VaultHubAbi,
+ functionName: 'vaultsCount',
+ query: {
+ enabled: !!address,
+ },
+ });
+};
diff --git a/features/settings/permissions/consts.ts b/features/settings/permissions/consts.ts
index 600107dc..cfe3c26f 100644
--- a/features/settings/permissions/consts.ts
+++ b/features/settings/permissions/consts.ts
@@ -82,12 +82,12 @@ export const editPermissionsSchema = z.object({
export const adminPermissionsList: PermissionsRoles[] = [
{
role: 'FUND_ROLE',
- title: 'Mint ETH',
- tooltip: 'Allows Funding ETH',
+ title: 'Supply ETH',
+ tooltip: 'Allows Supplying ETH',
},
{
role: 'WITHDRAW_ROLE',
- title: 'Repay ETH',
+ title: 'Withdraw ETH',
tooltip: 'Allows Withdrawing unlocked ETH from stVault',
},
{
@@ -126,8 +126,8 @@ export const adminPermissionsList: PermissionsRoles[] = [
},
{
role: 'BURN_ROLE',
- title: 'Burn stETH',
- tooltip: 'Allows Burning stETH',
+ title: 'Repay stETH',
+ tooltip: 'Allows Repaying stETH',
},
{
role: 'VOLUNTARY_DISCONNECT_ROLE',
diff --git a/features/supply/fund/form/balance/balance.tsx b/features/supply/fund/form/balance/balance.tsx
index db4cd94c..defd0d9e 100644
--- a/features/supply/fund/form/balance/balance.tsx
+++ b/features/supply/fund/form/balance/balance.tsx
@@ -8,7 +8,9 @@ import { formatBalance } from 'utils';
export const Balance = () => {
const { address } = useDappStatus();
- const { data, isLoading, isSuccess, isError } = useBalance({ address });
+ const { data, isLoading, isSuccess, isError } = useBalance({
+ address,
+ });
return (
@@ -20,7 +22,7 @@ export const Balance = () => {
{isSuccess && !isLoading && (
{formatBalance(data.value).trimmed} ETH
)}
- {isError && Balance is not available}
+ {isError && !isLoading && -}
);
diff --git a/features/supply/fund/form/feature-tx-info/feature-tx-info.tsx b/features/supply/fund/form/feature-tx-info/feature-tx-info.tsx
index 46647392..c51b6cc9 100644
--- a/features/supply/fund/form/feature-tx-info/feature-tx-info.tsx
+++ b/features/supply/fund/form/feature-tx-info/feature-tx-info.tsx
@@ -1,32 +1,21 @@
import { Text, Loader } from '@lidofinance/lido-ui';
import { useFormContext } from 'react-hook-form';
import { useSimulationFund } from 'features/supply/fund/hooks';
+import { useVaultInfo } from 'features/overview/contexts';
import { AmountInfo, InfoRow, Wrapper } from './styles';
-import { useVaultInfo } from 'features/overview/contexts';
export const FeatureTxInfo = () => {
- // TODO: simulate tx, add tx price, convert ETH to stEth
- // TODO: add question info
- const { getValues } = useFormContext();
- const { amount } = getValues();
+ const { watch } = useFormContext();
+ const amount: bigint | undefined = watch('amount');
const { activeVault } = useVaultInfo();
const { data, isLoading, isError } = useSimulationFund({
- address: activeVault?.address,
- amount: amount ?? 0,
+ address: activeVault?.owner,
+ amount: amount ?? 0n,
});
return (
- {/*
-
- You will receive
-
-
- {'50 stETH'}
-
-
- */}
Transaction cost
@@ -37,8 +26,10 @@ export const FeatureTxInfo = () => {
Can't load gas simulation info
)}
- {data && {'$99.99'}}
- {!data && !isError && !isLoading && -}
+ {!!data?.result && !isLoading && (
+ {data?.result}
+ )}
+ {!data?.result && !isError && !isLoading && -}
);
diff --git a/features/supply/fund/form/submit-button/submit-button.tsx b/features/supply/fund/form/submit-button/submit-button.tsx
index 91ffa090..ba1f82ab 100644
--- a/features/supply/fund/form/submit-button/submit-button.tsx
+++ b/features/supply/fund/form/submit-button/submit-button.tsx
@@ -5,6 +5,7 @@ export const SubmitButton = () => {
const {
formState: { isSubmitting, isValid, isDirty },
} = useFormContext();
+
return (
= ({ children }) => {
+ const [submitStep, setSubmitStep] = useState<{
+ step: SubmitStep;
+ tx?: Address;
+ }>(() => ({ step: SubmitStepEnum.edit }));
const formObject = useForm({
defaultValues: {
amount: undefined,
@@ -20,16 +30,38 @@ export const FundFormProvider: FC<{ children: ReactNode }> = ({ children }) => {
const { callVaultFund } = useFund();
const { retryEvent, retryFire } = useFormControllerRetry();
+ const setModalState = useCallback(
+ (submitStep: { step: SubmitStep; tx?: Address }) => {
+ setSubmitStep(submitStep);
+ },
+ [],
+ );
const onSubmit = useCallback(
async ({ amount }: FundFormSchema) => {
- if (amount) {
- await callVaultFund(amount);
- return true;
+ try {
+ if (amount) {
+ setModalState({ step: SubmitStepEnum.initiate });
+ const tx = await callVaultFund(amount, setModalState);
+ setModalState({ step: SubmitStepEnum.success, tx });
+ return true;
+ }
+ } catch (err) {
+ if (
+ err instanceof Error &&
+ err.message.includes('User rejected the request')
+ ) {
+ setModalState({ step: SubmitStepEnum.reject });
+ } else {
+ setModalState({ step: SubmitStepEnum.error });
+ }
+
+ return false;
}
return false;
},
+ // eslint-disable-next-line react-hooks/exhaustive-deps
[callVaultFund],
);
@@ -48,6 +80,7 @@ export const FundFormProvider: FC<{ children: ReactNode }> = ({ children }) => {
{children}
+
);
diff --git a/features/supply/fund/hooks/use-fund.ts b/features/supply/fund/hooks/use-fund.ts
index 390df902..cb6bbad5 100644
--- a/features/supply/fund/hooks/use-fund.ts
+++ b/features/supply/fund/hooks/use-fund.ts
@@ -1,9 +1,8 @@
import { useCallback } from 'react';
import {
+ usePublicClient,
useSimulateContract,
- useWaitForTransactionReceipt,
useWriteContract,
- UseSimulateContractParameters,
useAccount,
} from 'wagmi';
import { Address } from 'viem';
@@ -12,9 +11,14 @@ import { dashboardAbi } from 'abi/dashboard-abi';
import { useVaultInfo } from 'features/overview/contexts';
import invariant from 'tiny-invariant';
import { useVaultPermissions } from 'modules/vaults/hooks/use-vault-permissions';
+import {
+ SubmitStep,
+ SubmitStepEnum,
+} from 'shared/components/submit-modal/types';
export const useFund = (onMutate = () => {}) => {
const { activeVault } = useVaultInfo();
+ const publicClient = usePublicClient();
const { data: fundTx, writeContractAsync } = useWriteContract({
mutation: {
@@ -22,27 +26,41 @@ export const useFund = (onMutate = () => {}) => {
},
});
- const { data: fundReceipt } = useWaitForTransactionReceipt({
- hash: fundTx,
- });
-
const callVaultFund = useCallback(
- async (amount: bigint) => {
- invariant(activeVault?.owner, 'activeVault?.owner is undefined');
- return writeContractAsync({
+ async (
+ amount: bigint,
+ setModalState: (submitStep: { step: SubmitStep; tx?: Address }) => void,
+ ) => {
+ invariant(
+ activeVault?.owner,
+ '[useFundWithDashboard] owner is undefined',
+ );
+ invariant(
+ publicClient,
+ '[useFundWithDashboard] publicClient is undefined',
+ );
+
+ setModalState({ step: SubmitStepEnum.confirming });
+ const tx = await writeContractAsync({
abi: dashboardAbi,
address: activeVault.owner,
functionName: 'fund',
value: amount,
});
+
+ setModalState({ step: SubmitStepEnum.submitting, tx });
+ await publicClient.waitForTransactionReceipt({
+ hash: tx,
+ });
+
+ return tx;
},
- [writeContractAsync, activeVault?.owner],
+ [writeContractAsync, activeVault?.owner, publicClient],
);
return {
callVaultFund,
fundTx,
- fundReceipt,
};
};
@@ -54,16 +72,15 @@ export type SimulationFundProps = {
export const useSimulationFund = ({ address, amount }: SimulationFundProps) => {
const { address: accountAddress } = useAccount();
const { hasPermission } = useVaultPermissions('funder');
- const simulationContractPayload: UseSimulateContractParameters = {
+
+ return useSimulateContract({
abi: dashboardAbi,
address,
account: accountAddress,
functionName: 'fund',
value: amount,
query: {
- enabled: !!address && hasPermission,
+ enabled: !!address && hasPermission && !!amount,
},
- };
-
- return useSimulateContract(simulationContractPayload);
+ });
};
diff --git a/features/supply/withdraw/form/balance/balance.tsx b/features/supply/withdraw/form/balance/balance.tsx
index 14fba951..9632dbff 100644
--- a/features/supply/withdraw/form/balance/balance.tsx
+++ b/features/supply/withdraw/form/balance/balance.tsx
@@ -24,7 +24,7 @@ export const Balance = () => {
{formatBalance(withdrawableAmount).trimmed} ETH
)}
- {isWithdrawableError && (
+ {isWithdrawableError && !isWithdrawableLoading && (
Balance is not available
)}
diff --git a/features/supply/withdraw/form/feature-tx-info/feature-tx-info.tsx b/features/supply/withdraw/form/feature-tx-info/feature-tx-info.tsx
index 043ac549..a5c7250d 100644
--- a/features/supply/withdraw/form/feature-tx-info/feature-tx-info.tsx
+++ b/features/supply/withdraw/form/feature-tx-info/feature-tx-info.tsx
@@ -3,6 +3,7 @@ import { useSimulateWithdrawDashboard } from 'features/supply/withdraw/hooks';
import { Loader, Text } from '@lidofinance/lido-ui';
import { AmountInfo, InfoRow, Wrapper } from './styles';
+import { formatBalance } from 'utils';
export const FeatureTxInfo = () => {
const { watch } = useFormContext();
@@ -15,10 +16,10 @@ export const FeatureTxInfo = () => {
return (
- {/*
+
You will receive
- {'50 ETH'} */}
+ {formatBalance(amount ?? 0n).trimmed} ETH
@@ -30,9 +31,10 @@ export const FeatureTxInfo = () => {
Can't load gas simulation info
)}
- {/*TODO: replace by real data*/}
- {data && {'$99.99'}}
- {!data && !isError && !isLoading && -}
+ {!!data?.result && !isLoading && (
+ {data?.result}
+ )}
+ {!data?.result && !isError && !isLoading && -}
);
diff --git a/features/supply/withdraw/hooks/use-withdraw-with-dashboard.ts b/features/supply/withdraw/hooks/use-withdraw-with-dashboard.ts
index 663d8240..51a2f8bd 100644
--- a/features/supply/withdraw/hooks/use-withdraw-with-dashboard.ts
+++ b/features/supply/withdraw/hooks/use-withdraw-with-dashboard.ts
@@ -1,9 +1,5 @@
import { useCallback } from 'react';
-import {
- useSimulateContract,
- useWaitForTransactionReceipt,
- useWriteContract,
-} from 'wagmi';
+import { usePublicClient, useSimulateContract, useWriteContract } from 'wagmi';
import { Address } from 'viem';
import { dashboardAbi } from 'abi/dashboard-abi';
@@ -11,14 +7,20 @@ import { useDappStatus } from 'modules/web3/hooks/use-dapp-status';
import { useVaultInfo } from 'features/overview/contexts';
import invariant from 'tiny-invariant';
import { useVaultPermissions } from 'modules/vaults/hooks/use-vault-permissions';
+import {
+ SubmitStep,
+ SubmitStepEnum,
+} from 'shared/components/submit-modal/types';
type WithdrawWithDashboardArgs = {
recipient: Address;
amount: bigint;
+ setModalState: (submitStep: { step: SubmitStep; tx?: Address }) => void;
};
export const useWithdrawWithDashboard = (onMutate = () => {}) => {
const { activeVault } = useVaultInfo();
+ const publicClient = usePublicClient();
const { data: withdrawTx, writeContractAsync } = useWriteContract({
mutation: {
@@ -26,27 +28,38 @@ export const useWithdrawWithDashboard = (onMutate = () => {}) => {
},
});
- const { data: withdrawReceipt } = useWaitForTransactionReceipt({
- hash: withdrawTx,
- });
-
const callWithdraw = useCallback(
- async ({ amount, recipient }: WithdrawWithDashboardArgs) => {
- invariant(activeVault, 'activeVault is undefined');
- return await writeContractAsync({
+ async ({ amount, recipient, setModalState }: WithdrawWithDashboardArgs) => {
+ invariant(
+ activeVault,
+ '[useWithdrawWithDashboard] activeVault is undefined',
+ );
+ invariant(
+ publicClient,
+ '[useFundWithDashboard] publicClient is undefined',
+ );
+
+ setModalState({ step: SubmitStepEnum.confirming });
+ const tx = await writeContractAsync({
abi: dashboardAbi,
address: activeVault.owner,
functionName: 'withdraw',
args: [recipient, amount],
});
+
+ setModalState({ step: SubmitStepEnum.submitting, tx });
+ await publicClient.waitForTransactionReceipt({
+ hash: tx,
+ });
+
+ return tx;
},
- [activeVault, writeContractAsync],
+ [activeVault, writeContractAsync, publicClient],
);
return {
callWithdraw,
withdrawTx,
- withdrawReceipt,
};
};
diff --git a/features/supply/withdraw/withdraw-form-context/withdraw-form-provider.tsx b/features/supply/withdraw/withdraw-form-context/withdraw-form-provider.tsx
index 6122b75a..b9001a95 100644
--- a/features/supply/withdraw/withdraw-form-context/withdraw-form-provider.tsx
+++ b/features/supply/withdraw/withdraw-form-context/withdraw-form-provider.tsx
@@ -5,24 +5,30 @@ import {
useCallback,
createContext,
useContext,
+ useState,
} from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { Address, isAddress } from 'viem';
import invariant from 'tiny-invariant';
import { useFormControllerRetry } from 'shared/hook-form/form-controller/use-form-controller-retry-delegate';
+import {
+ useWithdrawable,
+ useWithdrawWithDashboard,
+} from 'features/supply/withdraw/hooks';
import {
FormController,
FormControllerContext,
FormControllerContextValueType,
} from 'shared/hook-form/form-controller';
+import { SubmitModal } from 'shared/components';
import { WithdrawFormSchema } from 'features/supply/withdraw/types';
import {
- useWithdrawable,
- useWithdrawWithDashboard,
-} from 'features/supply/withdraw/hooks';
+ SubmitStep,
+ SubmitStepEnum,
+} from 'shared/components/submit-modal/types';
type WithdrawDataContextValue = {
withdrawableAmount: bigint | undefined;
@@ -49,6 +55,10 @@ export const useWithdrawFormData = () => {
export const WithdrawFormProvider: FC<{ children: ReactNode }> = ({
children,
}) => {
+ const [submitStep, setSubmitStep] = useState<{
+ step: SubmitStep;
+ tx?: Address;
+ }>(() => ({ step: SubmitStepEnum.edit }));
const formObject = useForm({
defaultValues: {
amount: undefined,
@@ -59,6 +69,12 @@ export const WithdrawFormProvider: FC<{ children: ReactNode }> = ({
});
const { callWithdraw } = useWithdrawWithDashboard();
const { retryEvent, retryFire } = useFormControllerRetry();
+ const setModalState = useCallback(
+ (submitStep: { step: SubmitStep; tx?: Address }) => {
+ setSubmitStep(submitStep);
+ },
+ [],
+ );
const {
data: withdrawableAmount,
@@ -86,15 +102,27 @@ export const WithdrawFormProvider: FC<{ children: ReactNode }> = ({
async ({ amount, recipient }: WithdrawFormSchema) => {
try {
if (amount && recipient && isAddress(recipient)) {
- await callWithdraw({ amount, recipient });
+ setModalState({ step: SubmitStepEnum.initiate });
+ const tx = await callWithdraw({ amount, recipient, setModalState });
+ setModalState({ step: SubmitStepEnum.success, tx });
return true;
}
- } catch (e) {
+ } catch (err) {
+ if (
+ err instanceof Error &&
+ err.message.includes('User rejected the request')
+ ) {
+ setModalState({ step: SubmitStepEnum.reject });
+ } else {
+ setModalState({ step: SubmitStepEnum.error });
+ }
+
return false;
}
return false;
},
+ // eslint-disable-next-line react-hooks/exhaustive-deps
[callWithdraw],
);
@@ -114,6 +142,7 @@ export const WithdrawFormProvider: FC<{ children: ReactNode }> = ({
{children}
+
diff --git a/modules/vaults/hooks/use-vaults-data-all.ts b/modules/vaults/hooks/use-vaults-data-all.ts
index 3d18b028..2b47e750 100644
--- a/modules/vaults/hooks/use-vaults-data-all.ts
+++ b/modules/vaults/hooks/use-vaults-data-all.ts
@@ -3,7 +3,7 @@ import { useState, useCallback, useMemo } from 'react';
import { useVaultData } from 'modules/vaults/hooks/use-vault-data';
import { useVaultsConnectedBound } from 'modules/vaults/hooks/use-vaults-connected-bound';
-import { VAULTS_PER_PAGE } from '../consts';
+import { VAULTS_PER_PAGE } from 'modules/vaults/consts';
import type { Address } from 'viem';
diff --git a/shared/components/index.ts b/shared/components/index.ts
index 0cb66663..80b70f0f 100644
--- a/shared/components/index.ts
+++ b/shared/components/index.ts
@@ -16,3 +16,4 @@ export { VaultImpactDashboard } from './vault-impact-dashboard';
export { VaultImpactValuation } from './vault-impact-valuation';
export { InputAmount } from './input-amount';
export { Chip } from './chip';
+export { SubmitModal } from './submit-modal';
diff --git a/shared/components/submit-modal/index.ts b/shared/components/submit-modal/index.ts
new file mode 100644
index 00000000..09ec63ec
--- /dev/null
+++ b/shared/components/submit-modal/index.ts
@@ -0,0 +1,2 @@
+export { SubmitModal } from './submit-modal';
+export * from './types';
diff --git a/shared/components/submit-modal/styles.ts b/shared/components/submit-modal/styles.ts
new file mode 100644
index 00000000..dfca0ac4
--- /dev/null
+++ b/shared/components/submit-modal/styles.ts
@@ -0,0 +1,8 @@
+import styled from 'styled-components';
+
+export const Content = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${({ theme }) => theme.spaceMap.xl}px;
+ margin-top: ${({ theme }) => theme.spaceMap.sm}px;
+`;
diff --git a/shared/components/submit-modal/submit-modal.tsx b/shared/components/submit-modal/submit-modal.tsx
new file mode 100644
index 00000000..3c201f74
--- /dev/null
+++ b/shared/components/submit-modal/submit-modal.tsx
@@ -0,0 +1,142 @@
+import { FC, useMemo } from 'react';
+import { useRouter } from 'next/router';
+import { useFormContext } from 'react-hook-form';
+
+import {
+ Loader,
+ Modal,
+ Text,
+ Success,
+ Error,
+ type ModalProps,
+ Button,
+} from '@lidofinance/lido-ui';
+
+import { useFormControllerContext } from 'shared/hook-form/form-controller';
+import { ButtonLink, TxLinkEtherscan } from 'shared/components';
+import { Content } from './styles';
+
+import {
+ SubmitStepEnum,
+ SubmitStep,
+} from 'shared/components/submit-modal/types';
+import { AppPaths } from 'consts/urls';
+import { useVaultInfo } from 'features/overview/contexts';
+import { Address } from 'viem';
+
+const getIconComponent = (step: SubmitStep) => {
+ switch (step) {
+ case SubmitStepEnum.success:
+ return ;
+ case SubmitStepEnum.reject:
+ return ;
+ case SubmitStepEnum.error:
+ return ;
+ default:
+ return ;
+ }
+};
+
+const getModalTitle = (step: SubmitStep) => {
+ switch (step) {
+ case SubmitStepEnum.success:
+ return 'Transaction was finished successfully';
+ case SubmitStepEnum.reject:
+ return 'Wallet tx signature';
+ case SubmitStepEnum.error:
+ return 'Transaction error';
+ default:
+ return 'Transaction';
+ }
+};
+
+const getModalSubTitle = (step: SubmitStep) => {
+ switch (step) {
+ case SubmitStepEnum.confirming:
+ return 'Awaiting wallet signature';
+ case SubmitStepEnum.submitting:
+ return 'Awaiting block confirmation';
+ case SubmitStepEnum.reject:
+ return 'User denied transaction signature';
+ case SubmitStepEnum.error:
+ return 'Got error when called contract simulation or transaction';
+ default:
+ return '';
+ }
+};
+
+interface SubmitModalProps extends ModalProps {
+ submitStep: {
+ step: SubmitStep;
+ tx?: Address;
+ };
+ setModalState: ({ step }: { step: SubmitStep }) => void;
+}
+
+export const SubmitModal: FC = ({
+ submitStep,
+ setModalState,
+}) => {
+ const router = useRouter();
+ const {
+ formState: { isSubmitting, isSubmitted },
+ } = useFormContext();
+ const { retryFire } = useFormControllerContext();
+ const { step, tx } = submitStep ?? {};
+ const { activeVault } = useVaultInfo();
+
+ const iconComponent = useMemo(() => getIconComponent(step), [step]);
+ const title = getModalTitle(step);
+ const subtitle = getModalSubTitle(step);
+
+ const handleNavigateToVault = () => {
+ void router.push(`/${activeVault?.address}/${AppPaths.overview}`);
+ };
+
+ const handleCloseModal = () => {
+ setModalState({ step: SubmitStepEnum.edit });
+ };
+
+ return (
+
+
+ {step === SubmitStepEnum.initiate && (
+
+ Wait for wallet confirmation window
+
+ )}
+
+ {step === SubmitStepEnum.confirming && (
+
+ Confirm this transaction in your wallet
+
+ )}
+
+ {step === SubmitStepEnum.success && (
+
+ )}
+
+ {(SubmitStepEnum.success === step ||
+ SubmitStepEnum.submitting === step) &&
+ tx && }
+
+ {step === SubmitStepEnum.reject && (
+ retry
+ )}
+
+ {step === SubmitStepEnum.error && (
+ close
+ )}
+
+
+ );
+};
diff --git a/shared/components/submit-modal/types.ts b/shared/components/submit-modal/types.ts
new file mode 100644
index 00000000..3fe8c3b3
--- /dev/null
+++ b/shared/components/submit-modal/types.ts
@@ -0,0 +1,11 @@
+export enum SubmitStepEnum {
+ edit = 'edit',
+ initiate = 'initiate',
+ confirming = 'confirming',
+ reject = 'reject',
+ error = 'error',
+ submitting = 'submitting',
+ success = 'success',
+}
+
+export type SubmitStep = keyof typeof SubmitStepEnum;