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 @@ + fill="currentColor" /> \ 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{' '} + Tenderly + +
+ +
+ {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