diff --git a/public/images/common/close.svg b/public/images/common/close.svg
index 8bff2f1262..0c4cb574b1 100644
--- a/public/images/common/close.svg
+++ b/public/images/common/close.svg
@@ -1,5 +1,5 @@
\ No newline at end of file
diff --git a/public/images/transactions/tenderly-dark.svg b/public/images/transactions/tenderly-dark.svg
new file mode 100644
index 0000000000..b03291881f
--- /dev/null
+++ b/public/images/transactions/tenderly-dark.svg
@@ -0,0 +1,13 @@
+
diff --git a/public/images/transactions/tenderly-light.svg b/public/images/transactions/tenderly-light.svg
new file mode 100644
index 0000000000..59e62bf609
--- /dev/null
+++ b/public/images/transactions/tenderly-light.svg
@@ -0,0 +1,13 @@
+
diff --git a/src/components/tx-flow/TxInfoProvider.tsx b/src/components/tx-flow/TxInfoProvider.tsx
new file mode 100644
index 0000000000..03c19bb780
--- /dev/null
+++ b/src/components/tx-flow/TxInfoProvider.tsx
@@ -0,0 +1,23 @@
+import { createContext } from 'react'
+
+import { useSimulation, type UseSimulationReturn } from '@/components/tx/TxSimulation/useSimulation'
+import { FETCH_STATUS } from '@/components/tx/TxSimulation/types'
+
+export const TxInfoContext = createContext<{
+ simulation: UseSimulationReturn
+}>({
+ simulation: {
+ simulateTransaction: () => {},
+ simulation: undefined,
+ simulationRequestStatus: FETCH_STATUS.NOT_ASKED,
+ simulationLink: '',
+ requestError: undefined,
+ resetSimulation: () => {},
+ },
+})
+
+export const TxInfoProvider = ({ children }: { children: JSX.Element }) => {
+ const simulation = useSimulation()
+
+ return {children}
+}
diff --git a/src/components/tx-flow/common/TxLayout/index.tsx b/src/components/tx-flow/common/TxLayout/index.tsx
index fcbc234add..9b451ac301 100644
--- a/src/components/tx-flow/common/TxLayout/index.tsx
+++ b/src/components/tx-flow/common/TxLayout/index.tsx
@@ -4,9 +4,11 @@ import { useTheme } from '@mui/material/styles'
import type { TransactionSummary } from '@safe-global/safe-gateway-typescript-sdk'
import { ProgressBar } from '@/components/common/ProgressBar'
import SafeTxProvider from '../../SafeTxProvider'
+import { TxInfoProvider } from '@/components/tx-flow/TxInfoProvider'
import TxNonce from '../TxNonce'
import TxStatusWidget from '../TxStatusWidget'
import css from './styles.module.css'
+import { TxSimulationMessage } from '@/components/tx/NewTxSimulation'
import SafeLogo from '@/public/images/logo-no-text.svg'
type TxLayoutProps = {
@@ -48,65 +50,70 @@ const TxLayout = ({
return (
-
-
-
-
- {title}
-
-
-
-
-
+
+
+
+
+
+ {title}
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
- {icon && (
-
-
-
- )}
+
+
+ {icon && (
+
+
+
+ )}
-
- {subtitle}
-
-
+
+ {subtitle}
+
+
- {!hideNonce && }
-
-
+ {!hideNonce && }
+
+
-
- {steps[step]}
+
+ {steps[step]}
- {onBack && step > 0 && (
-
- )}
-
-
+ {onBack && step > 0 && (
+
+ )}
+
+
- {statusVisible && (
- setStatusVisible(false)} />
+ {statusVisible && (
+ setStatusVisible(false)} />
+ )}
+
+
+
- )}
+
-
-
+
+
)
}
diff --git a/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx b/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx
index bd5b83ae8e..e9d6359e42 100644
--- a/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx
+++ b/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx
@@ -9,7 +9,7 @@ import ErrorMessage from '@/components/tx/ErrorMessage'
import { ExecutionMethod, ExecutionMethodSelector } from '@/components/tx/ExecutionMethodSelector'
import DecodedTxs from '@/components/tx-flow/flows/ExecuteBatch/DecodedTxs'
import SendToBlock from '@/components/tx/SendToBlock'
-import { TxSimulation } from '@/components/tx/TxSimulation'
+import { TxSimulation } from '@/components/tx/NewTxSimulation'
import { WrongChainWarning } from '@/components/tx/WrongChainWarning'
import useAsync from '@/hooks/useAsync'
import { useCurrentChain } from '@/hooks/useChains'
@@ -150,7 +150,7 @@ executions from the same Safe Account."
Transaction checks
-
+
)}
diff --git a/src/components/tx/NewTxSimulation/index.tsx b/src/components/tx/NewTxSimulation/index.tsx
new file mode 100644
index 0000000000..260b174015
--- /dev/null
+++ b/src/components/tx/NewTxSimulation/index.tsx
@@ -0,0 +1,168 @@
+import { Alert, AlertTitle, Button, Paper, SvgIcon, Typography } from '@mui/material'
+import { useContext, useEffect } from 'react'
+import type { ReactElement } from 'react'
+
+import useSafeInfo from '@/hooks/useSafeInfo'
+import useWallet from '@/hooks/wallets/useWallet'
+import CheckIcon from '@/public/images/common/check.svg'
+import CloseIcon from '@/public/images/common/close.svg'
+import { useDarkMode } from '@/hooks/useDarkMode'
+import CircularProgress from '@mui/material/CircularProgress'
+import ExternalLink from '@/components/common/ExternalLink'
+import { useCurrentChain } from '@/hooks/useChains'
+import { FETCH_STATUS } from '../TxSimulation/types'
+import { isTxSimulationEnabled } from '../TxSimulation/utils'
+import type { SimulationTxParams } from '../TxSimulation/utils'
+import type { TenderlySimulation } from '../TxSimulation/types'
+
+import css from './styles.module.css'
+import { TxInfoContext } from '@/components/tx-flow/TxInfoProvider'
+import { SafeTxContext } from '@/components/tx-flow/SafeTxProvider'
+
+export type TxSimulationProps = {
+ transactions?: SimulationTxParams['transactions']
+ gasLimit?: number
+ disabled: boolean
+}
+
+const getCallTraceErrors = (simulation?: TenderlySimulation) => {
+ if (!simulation || !simulation.simulation.status) {
+ return []
+ }
+
+ return simulation.transaction.call_trace.filter((call) => call.error)
+}
+
+// TODO: Investigate resetting on gasLimit change as we are not simulating with the gasLimit of the tx
+// otherwise remove all usage of gasLimit in simulation. Note: this was previously being done.
+const TxSimulationBlock = ({ transactions, disabled, gasLimit }: TxSimulationProps): ReactElement => {
+ const { safe } = useSafeInfo()
+ const wallet = useWallet()
+ const isDarkMode = useDarkMode()
+ const { safeTx } = useContext(SafeTxContext)
+ const {
+ simulation: { simulateTransaction, simulationRequestStatus, resetSimulation },
+ } = useContext(TxInfoContext)
+
+ const isLoading = simulationRequestStatus === FETCH_STATUS.LOADING
+ const isSuccess = simulationRequestStatus === FETCH_STATUS.SUCCESS
+ const isError = simulationRequestStatus === FETCH_STATUS.ERROR
+
+ const handleSimulation = async () => {
+ if (!wallet) {
+ return
+ }
+
+ simulateTransaction({
+ safe,
+ executionOwner: wallet.address,
+ transactions,
+ gasLimit,
+ } as SimulationTxParams)
+ }
+
+ // Reset simulation if safeTx changes
+ useEffect(() => {
+ resetSimulation()
+ }, [safeTx, resetSimulation])
+
+ return (
+
+
+
+ Simulate transaction
+
+
+ Powered by{' '}
+
+
+
+
+
+ {isSuccess ? (
+
+
+ Success
+
+ ) : isError ? (
+
+
+ Error
+
+ ) : isLoading ? (
+
+ ) : (
+
+ )}
+
+
+ )
+}
+
+export const TxSimulation = (props: TxSimulationProps): ReactElement | null => {
+ const chain = useCurrentChain()
+
+ if (!chain || !isTxSimulationEnabled(chain)) {
+ return null
+ }
+
+ return
+}
+
+export const TxSimulationMessage = () => {
+ const {
+ simulation: { simulationRequestStatus, simulationLink, simulation, requestError },
+ } = useContext(TxInfoContext)
+
+ const isSuccess = simulationRequestStatus === FETCH_STATUS.SUCCESS
+ const isError = simulationRequestStatus === FETCH_STATUS.ERROR
+ const isFinished = isSuccess || isError
+
+ // Safe can emit failure event even though Tenderly simulation succeeds
+ const isCallTraceError = getCallTraceErrors(simulation).length > 0
+
+ if (!isFinished) {
+ return null
+ }
+
+ return (
+
+ {isSuccess ? (
+
+ Simulation successful
+ Full simulation report is available on Tenderly.
+
+ ) : isError ? (
+
+ Simulation failed
+ {requestError ? (
+ <>
+ An unexpected error occurred during simulation: {requestError}.
+ >
+ ) : isCallTraceError ? (
+ 'The transaction failed during the simulation.'
+ ) : (
+ <>
+ The transaction failed during the simulation throwing error {simulation?.transaction.error_message}{' '}
+ in the contract at {simulation?.transaction.error_info?.address}.
+ >
+ )}{' '}
+ Full simulation report is available on Tenderly.
+
+ ) : null}
+
+ )
+}
diff --git a/src/components/tx/NewTxSimulation/styles.module.css b/src/components/tx/NewTxSimulation/styles.module.css
new file mode 100644
index 0000000000..3c6c48c30d
--- /dev/null
+++ b/src/components/tx/NewTxSimulation/styles.module.css
@@ -0,0 +1,23 @@
+.wrapper {
+ display: flex;
+ justify-content: space-between;
+ padding: var(--space-1) var(--space-2);
+ border-width: 1px;
+}
+
+.poweredBy {
+ color: var(--color-text-secondary);
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-1);
+}
+
+.result {
+ display: inline-flex;
+ align-items: center;
+}
+
+.simulate {
+ margin: 0;
+ padding: calc(var(--space-1) / 2) var(--space-2);
+}
diff --git a/src/components/tx/SignOrExecuteForm/TxChecks.tsx b/src/components/tx/SignOrExecuteForm/TxChecks.tsx
index 81e75a7383..9bc1f10b1b 100644
--- a/src/components/tx/SignOrExecuteForm/TxChecks.tsx
+++ b/src/components/tx/SignOrExecuteForm/TxChecks.tsx
@@ -1,5 +1,5 @@
import { type ReactElement, useContext } from 'react'
-import { TxSimulation } from '../TxSimulation'
+import { TxSimulation } from '../NewTxSimulation'
import { SafeTxContext } from '@/components/tx-flow/SafeTxProvider'
import { Typography } from '@mui/material'
import { RedefineScanResult } from '@/components/tx/security/redefine/RedefineScanResult/RedefineScanResult'
@@ -11,7 +11,7 @@ const TxChecks = (): ReactElement => {
<>
Transaction checks
-
+
>
diff --git a/src/components/tx/TxSimulation/__tests__/useSimulation.test.ts b/src/components/tx/TxSimulation/__tests__/useSimulation.test.ts
index 8e2f4fad12..bd3f89e922 100644
--- a/src/components/tx/TxSimulation/__tests__/useSimulation.test.ts
+++ b/src/components/tx/TxSimulation/__tests__/useSimulation.test.ts
@@ -80,7 +80,6 @@ describe('useSimulation()', () => {
chainId,
} as SafeInfo,
executionOwner: safeAddress,
- canExecute: true,
}),
)
@@ -150,7 +149,6 @@ describe('useSimulation()', () => {
chainId,
} as SafeInfo,
executionOwner: safeAddress,
- canExecute: true,
}),
)
@@ -221,7 +219,6 @@ describe('useSimulation()', () => {
chainId,
} as SafeInfo,
executionOwner: safeAddress,
- canExecute: false,
}),
)
diff --git a/src/components/tx/TxSimulation/__tests__/utils.test.ts b/src/components/tx/TxSimulation/__tests__/utils.test.ts
index 3f5db7d551..9156a8fd17 100644
--- a/src/components/tx/TxSimulation/__tests__/utils.test.ts
+++ b/src/components/tx/TxSimulation/__tests__/utils.test.ts
@@ -71,7 +71,6 @@ describe('simulation utils', () => {
})
const tenderlyPayload = await getSimulationPayload({
- canExecute: true,
executionOwner: ownerAddress,
gasLimit: 50_000,
safe: mockSafeInfo as SafeInfo,
@@ -140,7 +139,6 @@ describe('simulation utils', () => {
mockTx.addSignature(generatePreValidatedSignature(otherOwnerAddress2))
const tenderlyPayload = await getSimulationPayload({
- canExecute: true,
executionOwner: ownerAddress,
gasLimit: 50_000,
safe: mockSafeInfo as SafeInfo,
@@ -181,7 +179,6 @@ describe('simulation utils', () => {
mockTx.addSignature(generatePreValidatedSignature(otherOwnerAddress1))
const tenderlyPayload = await getSimulationPayload({
- canExecute: false,
executionOwner: ownerAddress,
safe: mockSafeInfo as SafeInfo,
transactions: mockTx,
@@ -226,7 +223,6 @@ describe('simulation utils', () => {
mockTx.addSignature(generatePreValidatedSignature(otherOwnerAddress1))
const tenderlyPayload = await getSimulationPayload({
- canExecute: true,
executionOwner: ownerAddress,
gasLimit: 50_000,
safe: mockSafeInfo as SafeInfo,
@@ -265,7 +261,6 @@ describe('simulation utils', () => {
})
const tenderlyPayload = await getSimulationPayload({
- canExecute: false,
executionOwner: ownerAddress,
gasLimit: 50_000,
safe: mockSafeInfo as SafeInfo,
@@ -315,7 +310,6 @@ describe('simulation utils', () => {
]
const tenderlyPayload = await getSimulationPayload({
- canExecute: true,
executionOwner: ownerAddress,
safe: mockSafeInfo as SafeInfo,
transactions: mockTxs,
diff --git a/src/components/tx/TxSimulation/useSimulation.ts b/src/components/tx/TxSimulation/useSimulation.ts
index bec0e03050..f1ba377efa 100644
--- a/src/components/tx/TxSimulation/useSimulation.ts
+++ b/src/components/tx/TxSimulation/useSimulation.ts
@@ -7,7 +7,7 @@ import { useAppSelector } from '@/store'
import { selectTenderly } from '@/store/settingsSlice'
import { asError } from '@/services/exceptions/utils'
-type UseSimulationReturn =
+export type UseSimulationReturn =
| {
simulationRequestStatus: FETCH_STATUS.NOT_ASKED | FETCH_STATUS.ERROR | FETCH_STATUS.LOADING
simulation: undefined
diff --git a/src/components/tx/TxSimulation/utils.ts b/src/components/tx/TxSimulation/utils.ts
index c133dc576d..acbeede35b 100644
--- a/src/components/tx/TxSimulation/utils.ts
+++ b/src/components/tx/TxSimulation/utils.ts
@@ -67,7 +67,6 @@ type SingleTransactionSimulationParams = {
executionOwner: string
transactions: SafeTransaction
gasLimit?: number
- canExecute: boolean
}
type MultiSendTransactionSimulationParams = {
@@ -75,7 +74,6 @@ type MultiSendTransactionSimulationParams = {
executionOwner: string
transactions: MetaTransactionData[]
gasLimit?: number
- canExecute: boolean
}
export type SimulationTxParams = SingleTransactionSimulationParams | MultiSendTransactionSimulationParams