diff --git a/src/components/new-safe/create/steps/ReviewStep/index.tsx b/src/components/new-safe/create/steps/ReviewStep/index.tsx index 74e8033c2f..585939ed23 100644 --- a/src/components/new-safe/create/steps/ReviewStep/index.tsx +++ b/src/components/new-safe/create/steps/ReviewStep/index.tsx @@ -1,4 +1,5 @@ import { getAvailableSaltNonce } from '@/components/new-safe/create/logic/utils' +import type { NamedAddress } from '@/components/new-safe/create/types' import ErrorMessage from '@/components/tx/ErrorMessage' import { createCounterfactualSafe } from '@/features/counterfactual/utils' import useWalletCanPay from '@/hooks/useWalletCanPay' @@ -11,9 +12,8 @@ import lightPalette from '@/components/theme/lightPalette' import ChainIndicator from '@/components/common/ChainIndicator' import EthHashInfo from '@/components/common/EthHashInfo' import { useCurrentChain, useHasFeature } from '@/hooks/useChains' -import useGasPrice, { getTotalFee } from '@/hooks/useGasPrice' +import useGasPrice, { getTotalFeeFormatted } from '@/hooks/useGasPrice' import { useEstimateSafeCreationGas } from '@/components/new-safe/create/useEstimateSafeCreationGas' -import { formatVisualAmount } from '@/utils/formatters' import type { StepRenderProps } from '@/components/new-safe/CardStepper/useCardStepper' import type { NewSafeFormData } from '@/components/new-safe/create' import css from '@/components/new-safe/create/steps/ReviewStep/styles.module.css' @@ -100,6 +100,52 @@ export const NetworkFee = ({ ) } +export const SafeSetupOverview = ({ + name, + owners, + threshold, +}: { + name?: string + owners: NamedAddress[] + threshold: number +}) => { + const chain = useCurrentChain() + + return ( + + } /> + {name && {name}} />} + + {owners.map((owner, index) => ( + + ))} + + } + /> + + {threshold} out of {owners.length} owner(s) + + } + /> + + ) +} + const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps) => { const isWrongChain = useIsWrongChain() useSyncSafeCreationStep(setStep) @@ -136,10 +182,7 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps 0.001' + const totalFee = getTotalFeeFormatted(maxFeePerGas, maxPriorityFeePerGas, gasLimit, chain) // Only 1 out of 1 safe setups are supported for now const isCounterfactual = data.threshold === 1 && data.owners.length === 1 && isCounterfactualEnabled @@ -193,37 +236,7 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps - - } /> - {data.name && {data.name}} />} - - {data.owners.map((owner, index) => ( - - ))} - - } - /> - - {data.threshold} out of {data.owners.length} owner(s) - - } - /> - + {!isCounterfactual && ( diff --git a/src/features/counterfactual/ActivateAccount.tsx b/src/features/counterfactual/ActivateAccount.tsx new file mode 100644 index 0000000000..860316236f --- /dev/null +++ b/src/features/counterfactual/ActivateAccount.tsx @@ -0,0 +1,226 @@ +import { createNewSafe, relaySafeCreation } from '@/components/new-safe/create/logic' +import NetworkWarning from '@/components/new-safe/create/NetworkWarning' +import { NetworkFee, SafeSetupOverview } from '@/components/new-safe/create/steps/ReviewStep' +import { SafeCreationStatus } from '@/components/new-safe/create/steps/StatusStep/useSafeCreation' +import ReviewRow from '@/components/new-safe/ReviewRow' +import { TxModalContext } from '@/components/tx-flow' +import TxCard from '@/components/tx-flow/common/TxCard' + +import TxLayout from '@/components/tx-flow/common/TxLayout' +import ErrorMessage from '@/components/tx/ErrorMessage' +import { ExecutionMethod, ExecutionMethodSelector } from '@/components/tx/ExecutionMethodSelector' +import useDeployGasLimit from '@/features/counterfactual/hooks/useDeployGasLimit' +import { removeUndeployedSafe, selectUndeployedSafe } from '@/features/counterfactual/store/undeployedSafesSlice' +import useChainId from '@/hooks/useChainId' +import { useCurrentChain } from '@/hooks/useChains' +import useGasPrice, { getTotalFeeFormatted } from '@/hooks/useGasPrice' +import useIsWrongChain from '@/hooks/useIsWrongChain' +import { useLeastRemainingRelays } from '@/hooks/useRemainingRelays' +import useSafeInfo from '@/hooks/useSafeInfo' +import useWalletCanPay from '@/hooks/useWalletCanPay' +import useWallet from '@/hooks/wallets/useWallet' +import { useWeb3 } from '@/hooks/wallets/web3' +import { asError } from '@/services/exceptions/utils' +import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' +import { waitForCreateSafeTx } from '@/services/tx/txMonitor' +import { useAppDispatch, useAppSelector } from '@/store' +import { hasFeature } from '@/utils/chains' +import { hasRemainingRelays } from '@/utils/relaying' +import { Alert, Box, Button, CircularProgress, Divider, Grid, Typography } from '@mui/material' +import type { DeploySafeProps } from '@safe-global/protocol-kit' +import { FEATURES } from '@safe-global/safe-gateway-typescript-sdk' +import React, { useContext, useState } from 'react' + +const useActivateAccount = () => { + const chain = useCurrentChain() + const [gasPrice] = useGasPrice() + const { gasLimit } = useDeployGasLimit() + + const isEIP1559 = chain && hasFeature(chain, FEATURES.EIP1559) + const maxFeePerGas = gasPrice?.maxFeePerGas + const maxPriorityFeePerGas = gasPrice?.maxPriorityFeePerGas + + const options: DeploySafeProps['options'] = isEIP1559 + ? { + maxFeePerGas: maxFeePerGas?.toString(), + maxPriorityFeePerGas: maxPriorityFeePerGas?.toString(), + gasLimit: gasLimit?.toString(), + } + : { gasPrice: maxFeePerGas?.toString(), gasLimit: gasLimit?.toString() } + + const totalFee = getTotalFeeFormatted(maxFeePerGas, maxPriorityFeePerGas, gasLimit, chain) + const walletCanPay = useWalletCanPay({ gasLimit, maxFeePerGas, maxPriorityFeePerGas }) + + return { options, totalFee, walletCanPay } +} + +const ActivateAccountFlow = () => { + const [isSubmittable, setIsSubmittable] = useState(true) + const [submitError, setSubmitError] = useState() + const [executionMethod, setExecutionMethod] = useState(ExecutionMethod.RELAY) + + const isWrongChain = useIsWrongChain() + const chain = useCurrentChain() + const chainId = useChainId() + const { safeAddress } = useSafeInfo() + const provider = useWeb3() + const undeployedSafe = useAppSelector((state) => selectUndeployedSafe(state, chainId, safeAddress)) + const { setTxFlow } = useContext(TxModalContext) + const dispatch = useAppDispatch() + const wallet = useWallet() + const { options, totalFee, walletCanPay } = useActivateAccount() + + const ownerAddresses = undeployedSafe?.safeAccountConfig.owners || [] + const [minRelays] = useLeastRemainingRelays(ownerAddresses) + + // Every owner has remaining relays and relay method is selected + const canRelay = hasRemainingRelays(minRelays) + const willRelay = canRelay && executionMethod === ExecutionMethod.RELAY + + if (!undeployedSafe) return null + + const onSuccess = () => { + dispatch(removeUndeployedSafe({ chainId, address: safeAddress })) + } + + const createSafe = async () => { + if (!provider || !chain) return + + setIsSubmittable(false) + setSubmitError(undefined) + + try { + if (willRelay) { + const taskId = await relaySafeCreation( + chain, + undeployedSafe.safeAccountConfig.owners, + undeployedSafe.safeAccountConfig.threshold, + Number(undeployedSafe.safeDeploymentConfig?.saltNonce!), + ) + + waitForCreateSafeTx(taskId, (status) => { + if (status === SafeCreationStatus.SUCCESS) { + onSuccess() + } + }) + } else { + await createNewSafe(provider, { + safeAccountConfig: undeployedSafe.safeAccountConfig, + saltNonce: undeployedSafe.safeDeploymentConfig?.saltNonce, + options, + }) + onSuccess() + } + } catch (_err) { + const err = asError(_err) + setIsSubmittable(true) + setSubmitError(err) + return + } + + setTxFlow(undefined) + } + + const submitDisabled = !isSubmittable + const isSocialLogin = isSocialLoginWallet(wallet?.label) + + return ( + + + + You're about to deploy this Safe Account and will have to confirm the transaction with your connected + wallet. + + + + + ({ name: '', address: owner }))} + threshold={undeployedSafe.safeAccountConfig.threshold} + /> + + + + {canRelay && !isSocialLogin && ( + + + } + /> + + )} + + + + + + {!willRelay && !isSocialLogin && ( + + You will have to confirm a transaction with your connected wallet. + + )} + + } + /> + + + {submitError && ( + + Error submitting the transaction. Please try again. + + )} + + {isWrongChain && } + + {!walletCanPay && !willRelay && ( + + Your connected wallet doesn't have enough funds to execute this transaction + + )} + + + + + + + + + + ) +} + +const ActivateAccount = () => { + const { safe } = useSafeInfo() + const { setTxFlow } = useContext(TxModalContext) + + if (safe.deployed) return null + + const activateAccount = () => { + setTxFlow() + } + + return ( + + Activate your account? + + Activate your account now by deploying it and paying a network fee. + + + + ) +} + +export default ActivateAccount diff --git a/src/features/counterfactual/hooks/useDeployGasLimit.ts b/src/features/counterfactual/hooks/useDeployGasLimit.ts index a2a7a35e7b..2fae468ca3 100644 --- a/src/features/counterfactual/hooks/useDeployGasLimit.ts +++ b/src/features/counterfactual/hooks/useDeployGasLimit.ts @@ -10,13 +10,13 @@ const useDeployGasLimit = (safeTx?: SafeTransaction) => { const chainId = useChainId() const [gasLimit, gasLimitError, gasLimitLoading] = useAsync(async () => { - if (!safeTx || !onboard) return + if (!onboard) return const sdk = await getSafeSDKWithSigner(onboard, chainId) const [gas, safeTxGas, safeDeploymentGas] = await Promise.all([ - estimateTxBaseGas(sdk, safeTx), - estimateSafeTxGas(sdk, safeTx), + safeTx ? estimateTxBaseGas(sdk, safeTx) : '0', + safeTx ? estimateSafeTxGas(sdk, safeTx) : '0', estimateSafeDeploymentGas(sdk), ]) diff --git a/src/features/counterfactual/utils.ts b/src/features/counterfactual/utils.ts index 671b2e4689..0439528c75 100644 --- a/src/features/counterfactual/utils.ts +++ b/src/features/counterfactual/utils.ts @@ -1,10 +1,10 @@ -import type { NewSafeFormData } from '@/components/new-safe/create' import { LATEST_SAFE_VERSION } from '@/config/constants' -import { AppRoutes } from '@/config/routes' -import { addUndeployedSafe } from '@/features/counterfactual/store/undeployedSafesSlice' +import type { NewSafeFormData } from '@/components/new-safe/create' import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' import { asError } from '@/services/exceptions/utils' import { assertWalletChain, getUncheckedSafeSDK } from '@/services/tx/tx-sender/sdk' +import { AppRoutes } from '@/config/routes' +import { addUndeployedSafe } from '@/features/counterfactual/store/undeployedSafesSlice' import { txDispatch, TxEvent } from '@/services/tx/txEvents' import type { AppDispatch } from '@/store' import { addOrUpdateSafe } from '@/store/addedSafesSlice' diff --git a/src/hooks/useGasPrice.ts b/src/hooks/useGasPrice.ts index 6074cd65f2..a5c6ba9ef5 100644 --- a/src/hooks/useGasPrice.ts +++ b/src/hooks/useGasPrice.ts @@ -1,5 +1,7 @@ +import { formatVisualAmount } from '@/utils/formatters' import type { FeeData } from 'ethers' import type { + ChainInfo, GasPrice, GasPriceFixed, GasPriceFixedEIP1559, @@ -133,6 +135,17 @@ export const getTotalFee = ( return (maxFeePerGas + (maxPriorityFeePerGas || 0n)) * gasLimit } +export const getTotalFeeFormatted = ( + maxFeePerGas: bigint | null | undefined, + maxPriorityFeePerGas: bigint | null | undefined, + gasLimit: bigint | undefined, + chain: ChainInfo | undefined, +) => { + return gasLimit && maxFeePerGas + ? formatVisualAmount(getTotalFee(maxFeePerGas, maxPriorityFeePerGas, gasLimit), chain?.nativeCurrency.decimals) + : '> 0.001' +} + const useGasPrice = (): AsyncResult => { const chain = useCurrentChain() const gasPriceConfigs = chain?.gasPrice diff --git a/src/hooks/useWalletCanPay.ts b/src/hooks/useWalletCanPay.ts index f3990f023b..339c5a19bd 100644 --- a/src/hooks/useWalletCanPay.ts +++ b/src/hooks/useWalletCanPay.ts @@ -1,3 +1,4 @@ +import { getTotalFee } from '@/hooks/useGasPrice' import useWalletBalance from '@/hooks/wallets/useWalletBalance' const useWalletCanPay = ({ @@ -15,7 +16,7 @@ const useWalletCanPay = ({ // if gasLimit, maxFeePerGas or their walletBalance are missing if (!gasLimit || !maxFeePerGas || walletBalance === undefined) return true - const totalFee = (maxFeePerGas + BigInt(maxPriorityFeePerGas || 0)) * gasLimit + const totalFee = getTotalFee(maxFeePerGas, maxPriorityFeePerGas, gasLimit) return walletBalance >= totalFee } diff --git a/src/pages/settings/setup.tsx b/src/pages/settings/setup.tsx index 3229762803..414c1f0da7 100644 --- a/src/pages/settings/setup.tsx +++ b/src/pages/settings/setup.tsx @@ -1,3 +1,4 @@ +import ActivateAccount from '@/features/counterfactual/ActivateAccount' import type { NextPage } from 'next' import Head from 'next/head' import { Grid, Paper, Skeleton, SvgIcon, Tooltip, Typography } from '@mui/material' @@ -24,6 +25,8 @@ const Setup: NextPage = () => {
+ +