From 0ff4793549304f48b058202342f4b227eaf57042 Mon Sep 17 00:00:00 2001
From: Usame Algan <5880855+usame-algan@users.noreply.github.com>
Date: Thu, 8 Feb 2024 12:49:33 +0100
Subject: [PATCH] [Counterfactual] Add deploy safe flow (#3199)
* feat: Create counterfactual 1/1 safes
* fix: Add feature flag
* fix: Lint issues
* fix: Use incremental saltNonce for all safe creations
* fix: Replace useCounterfactualBalance hook with get function and write tests
* refactor: Move creation logic out of Review component
* fix: useLoadBalance check for undefined value
* fix: Extract saltNonce, safeAddress calculation into a hook
* refactor: Rename redux slice
* fix: Show error message in case saltNonce can't be retrieved
* fix: Disable create button if deploy props are loading
* fix: Revert hook change and update comment
* feat: Deploy counterfactual safe with first transaction
* fix: Get gas limit estimations in parallel
* fix: Add getBalances fallback for safe app calls
* fix: Hide nonce in tx flow for counterfactual safes
* fix: Show txEvents for first tx and close the flow when user submits tx
* feat: Add deploy safe flow
* refactor: Extract getTotalFeeFormatted and move logic into a local hook
* fix: Allow relaying when deploying a safe
* refactor: Extract safe setup overview component
* fix: Wait for relayed deployment before clearing counterfactual state
---
.../create/steps/ReviewStep/index.tsx | 87 ++++---
.../counterfactual/ActivateAccount.tsx | 226 ++++++++++++++++++
.../counterfactual/hooks/useDeployGasLimit.ts | 6 +-
src/features/counterfactual/utils.ts | 6 +-
src/hooks/useGasPrice.ts | 13 +
src/hooks/useWalletCanPay.ts | 3 +-
src/pages/settings/setup.tsx | 3 +
7 files changed, 300 insertions(+), 44 deletions(-)
create mode 100644 src/features/counterfactual/ActivateAccount.tsx
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 = () => {
+
+