diff --git a/src/components/safe-apps/AppFrame/index.tsx b/src/components/safe-apps/AppFrame/index.tsx
index 8b59b5b613..3b07088f25 100644
--- a/src/components/safe-apps/AppFrame/index.tsx
+++ b/src/components/safe-apps/AppFrame/index.tsx
@@ -1,3 +1,4 @@
+import useBalances from '@/hooks/useBalances'
import { useContext, useState } from 'react'
import type { ReactElement } from 'react'
import { useMemo } from 'react'
@@ -72,6 +73,7 @@ const AppFrame = ({ appUrl, allowedFeaturesList, safeAppFromManifest }: AppFrame
const addressBook = useAddressBook()
const chain = useCurrentChain()
+ const { balances } = useBalances()
const router = useRouter()
const {
expanded: queueBarExpanded,
@@ -164,10 +166,13 @@ const AppFrame = ({ appUrl, allowedFeaturesList, safeAppFromManifest }: AppFrame
onGetSafeInfo: useGetSafeInfo(),
onGetSafeBalances: (currency) => {
const isDefaultTokenlistSupported = chain && hasFeature(chain, FEATURES.DEFAULT_TOKENLIST)
- return getBalances(chainId, safeAddress, currency, {
- exclude_spam: true,
- trusted: isDefaultTokenlistSupported && TOKEN_LISTS.TRUSTED === tokenlist,
- })
+
+ return safe.deployed
+ ? getBalances(chainId, safeAddress, currency, {
+ exclude_spam: true,
+ trusted: isDefaultTokenlistSupported && TOKEN_LISTS.TRUSTED === tokenlist,
+ })
+ : Promise.resolve(balances)
},
onGetChainInfo: () => {
if (!chain) return
diff --git a/src/components/tx-flow/common/TxLayout/index.tsx b/src/components/tx-flow/common/TxLayout/index.tsx
index eda536aa11..f51a98fdd1 100644
--- a/src/components/tx-flow/common/TxLayout/index.tsx
+++ b/src/components/tx-flow/common/TxLayout/index.tsx
@@ -1,3 +1,4 @@
+import useSafeInfo from '@/hooks/useSafeInfo'
import { type ComponentType, type ReactElement, type ReactNode, useContext, useEffect, useState } from 'react'
import { Box, Container, Grid, Typography, Button, Paper, SvgIcon, IconButton, useMediaQuery } from '@mui/material'
import { useTheme } from '@mui/material/styles'
@@ -23,6 +24,7 @@ const TxLayoutHeader = ({
icon: TxLayoutProps['icon']
subtitle: TxLayoutProps['subtitle']
}) => {
+ const { safe } = useSafeInfo()
const { nonceNeeded } = useContext(SafeTxContext)
if (hideNonce && !icon && !subtitle) return null
@@ -41,7 +43,7 @@ const TxLayoutHeader = ({
- {!hideNonce && nonceNeeded && }
+ {!hideNonce && safe.deployed && nonceNeeded && }
)
}
diff --git a/src/components/tx/GasParams/GasParams.test.tsx b/src/components/tx/GasParams/GasParams.test.tsx
index 9e4ca9b461..3d6178ef0d 100644
--- a/src/components/tx/GasParams/GasParams.test.tsx
+++ b/src/components/tx/GasParams/GasParams.test.tsx
@@ -65,7 +65,7 @@ describe('GasParams', () => {
)
expect(getByText('Estimated fee')).toBeInTheDocument()
- expect(getByText('0.21 SepoliaETH')).toBeInTheDocument()
+ expect(getByText('0.42 SepoliaETH')).toBeInTheDocument()
})
it("Doesn't show an estimated fee if there is no gasLimit", () => {
diff --git a/src/components/tx/GasParams/index.tsx b/src/components/tx/GasParams/index.tsx
index d223e30376..ce527316ea 100644
--- a/src/components/tx/GasParams/index.tsx
+++ b/src/components/tx/GasParams/index.tsx
@@ -1,3 +1,4 @@
+import { getTotalFee } from '@/hooks/useGasPrice'
import type { ReactElement, SyntheticEvent } from 'react'
import { Accordion, AccordionDetails, AccordionSummary, Skeleton, Typography, Link, Grid } from '@mui/material'
import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk'
@@ -51,8 +52,9 @@ export const _GasParams = ({
const isError = gasLimitError && !gasLimit
// Total gas cost
- // TODO: Check how to use getTotalFee here
- const totalFee = !isLoading ? formatVisualAmount(maxFeePerGas * gasLimit, chain?.nativeCurrency.decimals) : '> 0.001'
+ const totalFee = !isLoading
+ ? formatVisualAmount(getTotalFee(maxFeePerGas, maxPriorityFeePerGas, gasLimit), chain?.nativeCurrency.decimals)
+ : '> 0.001'
// Individual gas params
const gasLimitString = gasLimit?.toString() || ''
diff --git a/src/components/tx/SignOrExecuteForm/hooks.ts b/src/components/tx/SignOrExecuteForm/hooks.ts
index 8628c33745..27b2279d1b 100644
--- a/src/components/tx/SignOrExecuteForm/hooks.ts
+++ b/src/components/tx/SignOrExecuteForm/hooks.ts
@@ -1,3 +1,4 @@
+import { assertTx, assertWallet, assertOnboard } from '@/utils/helpers'
import { useMemo } from 'react'
import { type TransactionOptions, type SafeTransaction } from '@safe-global/safe-core-sdk-types'
import { sameString } from '@safe-global/protocol-kit/dist/src/utils'
@@ -13,8 +14,6 @@ import {
dispatchTxSigning,
} from '@/services/tx/tx-sender'
import { useHasPendingTxs } from '@/hooks/usePendingTxs'
-import type { ConnectedWallet } from '@/hooks/wallets/useOnboard'
-import type { OnboardAPI } from '@web3-onboard/core'
import { getSafeTxGas, getNonces } from '@/services/tx/tx-sender/recommendedNonce'
import useAsync from '@/hooks/useAsync'
import { useUpdateBatch } from '@/hooks/useDraftBatch'
@@ -32,16 +31,6 @@ type TxActions = {
) => Promise
}
-function assertTx(safeTx: SafeTransaction | undefined): asserts safeTx {
- if (!safeTx) throw new Error('Transaction not provided')
-}
-function assertWallet(wallet: ConnectedWallet | null): asserts wallet {
- if (!wallet) throw new Error('Wallet not connected')
-}
-function assertOnboard(onboard: OnboardAPI | undefined): asserts onboard {
- if (!onboard) throw new Error('Onboard not connected')
-}
-
export const useTxActions = (): TxActions => {
const { safe } = useSafeInfo()
const onboard = useOnboard()
@@ -135,7 +124,7 @@ export const useTxActions = (): TxActions => {
}
return { addToBatch, signTx, executeTx }
- }, [safe, onboard, wallet, addTxToBatch])
+ }, [safe, wallet, addTxToBatch, onboard])
}
export const useValidateNonce = (safeTx: SafeTransaction | undefined): boolean => {
@@ -162,6 +151,7 @@ export const useRecommendedNonce = (): number | undefined => {
const [recommendedNonce] = useAsync(
async () => {
if (!safe.chainId || !safeAddress) return
+ if (!safe.deployed) return 0
const nonces = await getNonces(safe.chainId, safeAddress)
diff --git a/src/components/tx/SignOrExecuteForm/index.tsx b/src/components/tx/SignOrExecuteForm/index.tsx
index 87ee435a0e..eb5b0712aa 100644
--- a/src/components/tx/SignOrExecuteForm/index.tsx
+++ b/src/components/tx/SignOrExecuteForm/index.tsx
@@ -1,3 +1,5 @@
+import CounterfactualForm from '@/features/counterfactual/CounterfactualForm'
+import useSafeInfo from '@/hooks/useSafeInfo'
import { type ReactElement, type ReactNode, useState, useContext, useCallback } from 'react'
import madProps from '@/utils/mad-props'
import DecodedTx from '../DecodedTx'
@@ -71,6 +73,9 @@ export const SignOrExecuteForm = ({
const [decodedData, decodedDataError, decodedDataLoading] = useDecodeTx(safeTx)
const isBatchable = props.isBatchable !== false && safeTx && !isDelegateCall(safeTx)
+ const { safe } = useSafeInfo()
+ const isCounterfactualSafe = !safe.deployed
+
// If checkbox is checked and the transaction is executable, execute it, otherwise sign it
const canExecute = isCorrectNonce && (props.isExecutable || isNewExecutableTx)
const willExecute = (props.onlyExecute || shouldExecute) && canExecute
@@ -122,7 +127,7 @@ export const SignOrExecuteForm = ({
)}
- {canExecute && !props.onlyExecute && }
+ {canExecute && !props.onlyExecute && !isCounterfactualSafe && }
@@ -130,7 +135,9 @@ export const SignOrExecuteForm = ({
- {willExecute ? (
+ {isCounterfactualSafe ? (
+
+ ) : willExecute ? (
) : (
+ isExecutionLoop: ReturnType
+ txSecurity: ReturnType
+ safeTx?: SafeTransaction
+}): ReactElement => {
+ const wallet = useWallet()
+ const onboard = useOnboard()
+ const chainId = useChainId()
+ const dispatch = useAppDispatch()
+ const { safeAddress } = useSafeInfo()
+
+ // Form state
+ const [isSubmittable, setIsSubmittable] = useState(true)
+ const [submitError, setSubmitError] = useState()
+
+ // Hooks
+ const currentChain = useCurrentChain()
+ const { needsRiskConfirmation, isRiskConfirmed, setIsRiskIgnored } = txSecurity
+ const { setTxFlow } = useContext(TxModalContext)
+
+ // Estimate gas limit
+ const { gasLimit, gasLimitError } = useDeployGasLimit(safeTx)
+ const [advancedParams, setAdvancedParams] = useAdvancedParams(gasLimit)
+
+ // On modal submit
+ const handleSubmit = async (e: SyntheticEvent) => {
+ e.preventDefault()
+
+ if (needsRiskConfirmation && !isRiskConfirmed) {
+ setIsRiskIgnored(true)
+ return
+ }
+
+ setIsSubmittable(false)
+ setSubmitError(undefined)
+
+ const txOptions = getTxOptions(advancedParams, currentChain)
+
+ const onSuccess = () => {
+ dispatch(removeUndeployedSafe({ chainId, address: safeAddress }))
+ }
+
+ try {
+ await deploySafeAndExecuteTx(txOptions, chainId, wallet, safeTx, onboard, onSuccess)
+ } catch (_err) {
+ const err = asError(_err)
+ trackError(Errors._804, err)
+ setIsSubmittable(true)
+ setSubmitError(err)
+ return
+ }
+
+ // TODO: Show a success or status screen
+ setTxFlow(undefined)
+ }
+
+ const walletCanPay = useWalletCanPay({
+ gasLimit,
+ maxFeePerGas: advancedParams.maxFeePerGas,
+ maxPriorityFeePerGas: advancedParams.maxPriorityFeePerGas,
+ })
+
+ const cannotPropose = !isOwner && !onlyExecute
+ const submitDisabled =
+ !safeTx ||
+ !isSubmittable ||
+ disableSubmit ||
+ isExecutionLoop ||
+ cannotPropose ||
+ (needsRiskConfirmation && !isRiskConfirmed)
+
+ return (
+ <>
+
+ >
+ )
+}
+
+const useTxSecurityContext = () => useContext(TxSecurityContext)
+
+export default madProps(CounterfactualForm, {
+ isOwner: useIsSafeOwner,
+ isExecutionLoop: useIsExecutionLoop,
+ txSecurity: useTxSecurityContext,
+})
diff --git a/src/features/counterfactual/hooks/useDeployGasLimit.ts b/src/features/counterfactual/hooks/useDeployGasLimit.ts
new file mode 100644
index 0000000000..a2a7a35e7b
--- /dev/null
+++ b/src/features/counterfactual/hooks/useDeployGasLimit.ts
@@ -0,0 +1,29 @@
+import useAsync from '@/hooks/useAsync'
+import useChainId from '@/hooks/useChainId'
+import useOnboard from '@/hooks/wallets/useOnboard'
+import { getSafeSDKWithSigner } from '@/services/tx/tx-sender/sdk'
+import { estimateSafeDeploymentGas, estimateSafeTxGas, estimateTxBaseGas } from '@safe-global/protocol-kit'
+import type { SafeTransaction } from '@safe-global/safe-core-sdk-types'
+
+const useDeployGasLimit = (safeTx?: SafeTransaction) => {
+ const onboard = useOnboard()
+ const chainId = useChainId()
+
+ const [gasLimit, gasLimitError, gasLimitLoading] = useAsync(async () => {
+ if (!safeTx || !onboard) return
+
+ const sdk = await getSafeSDKWithSigner(onboard, chainId)
+
+ const [gas, safeTxGas, safeDeploymentGas] = await Promise.all([
+ estimateTxBaseGas(sdk, safeTx),
+ estimateSafeTxGas(sdk, safeTx),
+ estimateSafeDeploymentGas(sdk),
+ ])
+
+ return BigInt(gas) + BigInt(safeTxGas) + BigInt(safeDeploymentGas)
+ }, [onboard, chainId, safeTx])
+
+ return { gasLimit, gasLimitError, gasLimitLoading }
+}
+
+export default useDeployGasLimit
diff --git a/src/features/counterfactual/utils.ts b/src/features/counterfactual/utils.ts
index b58954338e..671b2e4689 100644
--- a/src/features/counterfactual/utils.ts
+++ b/src/features/counterfactual/utils.ts
@@ -2,11 +2,20 @@ 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 ConnectedWallet } from '@/hooks/wallets/useOnboard'
+import { asError } from '@/services/exceptions/utils'
+import { assertWalletChain, getUncheckedSafeSDK } from '@/services/tx/tx-sender/sdk'
+import { txDispatch, TxEvent } from '@/services/tx/txEvents'
import type { AppDispatch } from '@/store'
import { addOrUpdateSafe } from '@/store/addedSafesSlice'
import { upsertAddressBookEntry } from '@/store/addressBookSlice'
import { defaultSafeInfo } from '@/store/safeInfoSlice'
+import { didReprice, didRevert, type EthersError } from '@/utils/ethers-utils'
+import { assertOnboard, assertTx, assertWallet } from '@/utils/helpers'
import type { DeploySafeProps, PredictedSafeProps } from '@safe-global/protocol-kit'
+import type { SafeTransaction, TransactionOptions } from '@safe-global/safe-core-sdk-types'
+import type { OnboardAPI } from '@web3-onboard/core'
+import type { ContractTransactionResponse } from 'ethers'
import { ZERO_ADDRESS } from '@safe-global/protocol-kit/dist/src/utils/constants'
import type { SafeVersion } from '@safe-global/safe-core-sdk-types'
import {
@@ -14,8 +23,10 @@ import {
ImplementationVersionState,
type SafeBalanceResponse,
TokenType,
+ type SafeInfo,
} from '@safe-global/safe-gateway-typescript-sdk'
import type { BrowserProvider } from 'ethers'
+import { createWeb3 } from '@/hooks/wallets/web3'
import type { NextRouter } from 'next/router'
export const getUndeployedSafeInfo = (undeployedSafe: PredictedSafeProps, address: string, chainId: string) => {
@@ -33,6 +44,79 @@ export const getUndeployedSafeInfo = (undeployedSafe: PredictedSafeProps, addres
})
}
+export const dispatchTxExecutionAndDeploySafe = async (
+ safeTx: SafeTransaction,
+ txOptions: TransactionOptions,
+ onboard: OnboardAPI,
+ chainId: SafeInfo['chainId'],
+ onSuccess?: () => void,
+) => {
+ const sdkUnchecked = await getUncheckedSafeSDK(onboard, chainId)
+ const eventParams = { groupKey: 'cf-tx' }
+ const safeAddress = await sdkUnchecked.getAddress()
+
+ let result: ContractTransactionResponse | undefined
+ try {
+ const signedTx = await sdkUnchecked.signTransaction(safeTx)
+
+ const wallet = await assertWalletChain(onboard, chainId)
+ const provider = createWeb3(wallet.provider)
+ const signer = await provider.getSigner()
+
+ const deploymentTx = await sdkUnchecked.wrapSafeTransactionIntoDeploymentBatch(signedTx, txOptions)
+
+ // @ts-ignore TODO: Check why TransactionResponse type doesn't work
+ result = await signer.sendTransaction(deploymentTx)
+ txDispatch(TxEvent.EXECUTING, eventParams)
+ } catch (error) {
+ txDispatch(TxEvent.FAILED, { ...eventParams, error: asError(error) })
+ throw error
+ }
+
+ txDispatch(TxEvent.PROCESSING, { ...eventParams, txHash: result!.hash })
+
+ result
+ ?.wait()
+ .then((receipt) => {
+ if (receipt === null) {
+ txDispatch(TxEvent.FAILED, { ...eventParams, error: new Error('No transaction receipt found') })
+ } else if (didRevert(receipt)) {
+ txDispatch(TxEvent.REVERTED, { ...eventParams, error: new Error('Transaction reverted by EVM') })
+ } else {
+ txDispatch(TxEvent.PROCESSED, { ...eventParams, safeAddress })
+ }
+ })
+ .catch((err) => {
+ const error = err as EthersError
+
+ if (didReprice(error)) {
+ txDispatch(TxEvent.PROCESSED, { ...eventParams, safeAddress })
+ } else {
+ txDispatch(TxEvent.FAILED, { ...eventParams, error: asError(error) })
+ }
+ })
+ .finally(() => {
+ onSuccess?.()
+ })
+
+ return result!.hash
+}
+
+export const deploySafeAndExecuteTx = async (
+ txOptions: TransactionOptions,
+ chainId: string,
+ wallet: ConnectedWallet | null,
+ safeTx?: SafeTransaction,
+ onboard?: OnboardAPI,
+ onSuccess?: () => void,
+) => {
+ assertTx(safeTx)
+ assertWallet(wallet)
+ assertOnboard(onboard)
+
+ return dispatchTxExecutionAndDeploySafe(safeTx, txOptions, onboard, chainId, onSuccess)
+}
+
export const getCounterfactualBalance = async (safeAddress: string, provider?: BrowserProvider, chain?: ChainInfo) => {
const balance = await provider?.getBalance(safeAddress)
diff --git a/src/features/walletconnect/services/__tests__/WalletConnectWallet.test.ts b/src/features/walletconnect/services/__tests__/WalletConnectWallet.test.ts
index c963bfbbcf..ce86f0dbbb 100644
--- a/src/features/walletconnect/services/__tests__/WalletConnectWallet.test.ts
+++ b/src/features/walletconnect/services/__tests__/WalletConnectWallet.test.ts
@@ -274,6 +274,7 @@ describe('WalletConnectWallet', () => {
},
requiredNamespaces: {} as ProposalTypes.RequiredNamespaces,
optionalNamespaces: {} as ProposalTypes.OptionalNamespaces,
+ expiryTimestamp: 2,
},
verifyContext: {} as Verify.Context,
},
@@ -311,6 +312,7 @@ describe('WalletConnectWallet', () => {
},
requiredNamespaces: {} as ProposalTypes.RequiredNamespaces,
optionalNamespaces: {} as ProposalTypes.OptionalNamespaces,
+ expiryTimestamp: 2,
},
verifyContext: {} as Verify.Context,
},
diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts
index 6592417b31..0c90822e51 100644
--- a/src/utils/helpers.ts
+++ b/src/utils/helpers.ts
@@ -1,4 +1,8 @@
// `assert` does not work with arrow functions
+import type { ConnectedWallet } from '@/hooks/wallets/useOnboard'
+import type { SafeTransaction } from '@safe-global/safe-core-sdk-types'
+import type { OnboardAPI } from '@web3-onboard/core'
+
export function invariant(condition: T, error: string): asserts condition {
if (condition) {
return
@@ -6,3 +10,15 @@ export function invariant(condition: T, error: string): asser
throw new Error(error)
}
+
+export function assertTx(safeTx: SafeTransaction | undefined): asserts safeTx {
+ return invariant(safeTx, 'Transaction not provided')
+}
+
+export function assertWallet(wallet: ConnectedWallet | null): asserts wallet {
+ return invariant(wallet, 'Wallet not connected')
+}
+
+export function assertOnboard(onboard: OnboardAPI | undefined): asserts onboard {
+ return invariant(onboard, 'Onboard not connected')
+}