diff --git a/cypress/e2e/pages/safeapps.pages.js b/cypress/e2e/pages/safeapps.pages.js
index ec67d8cc5f..19f9c525b4 100644
--- a/cypress/e2e/pages/safeapps.pages.js
+++ b/cypress/e2e/pages/safeapps.pages.js
@@ -64,7 +64,7 @@ export const testBooleanValue3 = '3 testBooleanValue'
export const transfer2AssetsStr = 'Transfer 2 assets'
export const testTransfer1 = '1 transfer'
-export const testTransfer2 = '2 transfer'
+export const testTransfer2 = '2 MetaMultiSigWallet: transfer'
export const nativeTransfer2 = '2 native transfer'
export const nativeTransfer1 = '1 native transfer'
diff --git a/src/components/common/ErrorBoundary/index.tsx b/src/components/common/ErrorBoundary/index.tsx
index f25c1bbcfa..bb410b7c98 100644
--- a/src/components/common/ErrorBoundary/index.tsx
+++ b/src/components/common/ErrorBoundary/index.tsx
@@ -1,5 +1,4 @@
import { Typography, Link } from '@mui/material'
-import type { FallbackRender } from '@sentry/react'
import { HELP_CENTER_URL, IS_PRODUCTION } from '@/config/constants'
import { AppRoutes } from '@/config/routes'
@@ -8,8 +7,12 @@ import WarningIcon from '@/public/images/notifications/warning.svg'
import css from '@/components/common/ErrorBoundary/styles.module.css'
import CircularIcon from '../icons/CircularIcon'
import ExternalLink from '../ExternalLink'
+interface ErrorBoundaryProps {
+ error: Error
+ componentStack: string
+}
-const ErrorBoundary: FallbackRender = ({ error, componentStack }) => {
+const ErrorBoundary = ({ error, componentStack }: ErrorBoundaryProps) => {
return (
diff --git a/src/components/transactions/TxDetails/Summary/SafeTxHashDataRow/index.tsx b/src/components/transactions/TxDetails/Summary/SafeTxHashDataRow/index.tsx
index c6b1d2b26a..20a26981c8 100644
--- a/src/components/transactions/TxDetails/Summary/SafeTxHashDataRow/index.tsx
+++ b/src/components/transactions/TxDetails/Summary/SafeTxHashDataRow/index.tsx
@@ -16,6 +16,7 @@ export const SafeTxHashDataRow = ({
}) => {
const chainId = useChainId()
const safeAddress = useSafeAddress()
+
const domainHash = TypedDataEncoder.hashDomain({
chainId,
verifyingContract: safeAddress,
diff --git a/src/components/transactions/TxDetails/TxData/DecodedData/Multisend/index.tsx b/src/components/transactions/TxDetails/TxData/DecodedData/Multisend/index.tsx
index 0672be131c..5110562ff6 100644
--- a/src/components/transactions/TxDetails/TxData/DecodedData/Multisend/index.tsx
+++ b/src/components/transactions/TxDetails/TxData/DecodedData/Multisend/index.tsx
@@ -71,7 +71,6 @@ export const Multisend = ({ txData, compact = false }: MultisendProps): ReactEle
if (!multiSendTransactions) {
return null
}
-
return (
<>
diff --git a/src/components/tx-flow/SafeTxProvider.tsx b/src/components/tx-flow/SafeTxProvider.tsx
index b964f7546c..f83c86663a 100644
--- a/src/components/tx-flow/SafeTxProvider.tsx
+++ b/src/components/tx-flow/SafeTxProvider.tsx
@@ -9,7 +9,7 @@ import useSafeInfo from '@/hooks/useSafeInfo'
import { useCurrentChain } from '@/hooks/useChains'
import { prependSafeToL2Migration } from '@/utils/transactions'
-export const SafeTxContext = createContext<{
+export type SafeTxContextParams = {
safeTx?: SafeTransaction
setSafeTx: Dispatch
>
@@ -28,7 +28,9 @@ export const SafeTxContext = createContext<{
setSafeTxGas: Dispatch>
recommendedNonce?: number
-}>({
+}
+
+export const SafeTxContext = createContext({
setSafeTx: () => {},
setSafeMessage: () => {},
setSafeTxError: () => {},
@@ -81,7 +83,10 @@ const SafeTxProvider = ({ children }: { children: ReactNode }): ReactElement =>
if (safeTx.data.nonce === finalNonce && safeTx.data.safeTxGas === finalSafeTxGas) return
createTx({ ...safeTx.data, safeTxGas: String(finalSafeTxGas) }, finalNonce)
- .then(setSafeTx)
+ .then((tx) => {
+ console.log('SafeTxProvider: Updated tx with nonce and safeTxGas', tx)
+ setSafeTx(tx)
+ })
.catch(setSafeTxError)
}, [isSigned, finalNonce, finalSafeTxGas, safeTx?.data])
diff --git a/src/components/tx-flow/flows/AddOwner/ReviewOwner.tsx b/src/components/tx-flow/flows/AddOwner/ReviewOwner.tsx
index 7806b28b97..7c148cc2d2 100644
--- a/src/components/tx-flow/flows/AddOwner/ReviewOwner.tsx
+++ b/src/components/tx-flow/flows/AddOwner/ReviewOwner.tsx
@@ -1,6 +1,5 @@
import { useCurrentChain } from '@/hooks/useChains'
import { useContext, useEffect } from 'react'
-import { Typography, Divider, Box, SvgIcon, Paper } from '@mui/material'
import SignOrExecuteForm from '@/components/tx/SignOrExecuteForm'
import useSafeInfo from '@/hooks/useSafeInfo'
@@ -11,11 +10,7 @@ import { upsertAddressBookEntries } from '@/store/addressBookSlice'
import { SafeTxContext } from '../../SafeTxProvider'
import type { AddOwnerFlowProps } from '.'
import type { ReplaceOwnerFlowProps } from '../ReplaceOwner'
-import { OwnerList } from '../../common/OwnerList'
-import MinusIcon from '@/public/images/common/minus.svg'
-import EthHashInfo from '@/components/common/EthHashInfo'
-import commonCss from '@/components/tx-flow/common/styles.module.css'
-import { ChangeSignerSetupWarning } from '@/features/multichain/components/SignerSetupWarning/ChangeSignerSetupWarning'
+import { SettingsChangeContext } from './context'
export const ReviewOwner = ({ params }: { params: AddOwnerFlowProps | ReplaceOwnerFlowProps }) => {
const dispatch = useAppDispatch()
@@ -57,33 +52,8 @@ export const ReviewOwner = ({ params }: { params: AddOwnerFlowProps | ReplaceOwn
}
return (
-
- {params.removedOwner && (
- palette.warning.background, p: 2 }}>
-
-
- Previous signer
-
-
-
- )}
-
-
-
-
-
- Any transaction requires the confirmation of:
-
- {threshold} out of {safe.owners.length + (removedOwner ? 0 : 1)} signers
-
-
-
-
+
+
+
)
}
diff --git a/src/components/tx-flow/flows/AddOwner/context.ts b/src/components/tx-flow/flows/AddOwner/context.ts
new file mode 100644
index 0000000000..ce829bcb8b
--- /dev/null
+++ b/src/components/tx-flow/flows/AddOwner/context.ts
@@ -0,0 +1,7 @@
+import { type Context, createContext } from 'react'
+import { type AddOwnerFlowProps } from '.'
+import { type ReplaceOwnerFlowProps } from '../ReplaceOwner'
+
+type SettingsChange = Context
+
+export const SettingsChangeContext: SettingsChange = createContext({} as AddOwnerFlowProps | ReplaceOwnerFlowProps)
diff --git a/src/components/tx-flow/flows/ChangeThreshold/ReviewChangeThreshold.tsx b/src/components/tx-flow/flows/ChangeThreshold/ReviewChangeThreshold.tsx
index 8fe4425152..48e7a2a238 100644
--- a/src/components/tx-flow/flows/ChangeThreshold/ReviewChangeThreshold.tsx
+++ b/src/components/tx-flow/flows/ChangeThreshold/ReviewChangeThreshold.tsx
@@ -1,6 +1,5 @@
import useSafeInfo from '@/hooks/useSafeInfo'
import { useContext, useEffect } from 'react'
-import { Box, Divider, Typography } from '@mui/material'
import { createUpdateThresholdTx } from '@/services/tx/tx-sender'
import { SETTINGS_EVENTS, trackEvent } from '@/services/analytics'
@@ -9,8 +8,7 @@ import { SafeTxContext } from '@/components/tx-flow/SafeTxProvider'
import { ChangeThresholdFlowFieldNames } from '@/components/tx-flow/flows/ChangeThreshold'
import type { ChangeThresholdFlowProps } from '@/components/tx-flow/flows/ChangeThreshold'
-import commonCss from '@/components/tx-flow/common/styles.module.css'
-import { ChangeSignerSetupWarning } from '@/features/multichain/components/SignerSetupWarning/ChangeSignerSetupWarning'
+import { ChangeThresholdReviewContext } from './context'
const ReviewChangeThreshold = ({ params }: { params: ChangeThresholdFlowProps }) => {
const { safe } = useSafeInfo()
@@ -28,22 +26,9 @@ const ReviewChangeThreshold = ({ params }: { params: ChangeThresholdFlowProps })
}
return (
-
-
-
-
-
- Any transaction will require the confirmation of:
-
-
-
- {newThreshold} out of {safe.owners.length} signer(s)
-
-
-
-
-
-
+
+
+
)
}
diff --git a/src/components/tx-flow/flows/ChangeThreshold/context.tsx b/src/components/tx-flow/flows/ChangeThreshold/context.tsx
new file mode 100644
index 0000000000..037a1fe6b7
--- /dev/null
+++ b/src/components/tx-flow/flows/ChangeThreshold/context.tsx
@@ -0,0 +1,5 @@
+import { createContext } from 'react'
+
+export const ChangeThresholdReviewContext = createContext({
+ newThreshold: 0,
+})
diff --git a/src/components/tx-flow/flows/ConfirmBatch/index.tsx b/src/components/tx-flow/flows/ConfirmBatch/index.tsx
index 27e53d211f..a0159b21e8 100644
--- a/src/components/tx-flow/flows/ConfirmBatch/index.tsx
+++ b/src/components/tx-flow/flows/ConfirmBatch/index.tsx
@@ -8,7 +8,6 @@ import { OperationType } from '@safe-global/safe-core-sdk-types'
import TxLayout from '../../common/TxLayout'
import BatchIcon from '@/public/images/common/batch.svg'
import { useDraftBatch } from '@/hooks/useDraftBatch'
-import BatchTxList from '@/components/batch/BatchSidebar/BatchTxList'
type ConfirmBatchProps = {
onSubmit: () => void
@@ -32,11 +31,7 @@ const ConfirmBatch = ({ onSubmit }: ConfirmBatchProps): ReactElement => {
createMultiSendCallOnlyTx(calls).then(setSafeTx).catch(setSafeTxError)
}, [batchTxs, setSafeTx, setSafeTxError])
- return (
-
-
-
- )
+ return
}
const ConfirmBatchFlow = (props: ConfirmBatchProps) => {
diff --git a/src/components/tx-flow/flows/ConfirmTx/ConfirmProposedTx.tsx b/src/components/tx-flow/flows/ConfirmTx/ConfirmProposedTx.tsx
index c9e4587cb9..c62c776a3b 100644
--- a/src/components/tx-flow/flows/ConfirmTx/ConfirmProposedTx.tsx
+++ b/src/components/tx-flow/flows/ConfirmTx/ConfirmProposedTx.tsx
@@ -4,10 +4,10 @@ import type { TransactionSummary } from '@safe-global/safe-gateway-typescript-sd
import useSafeInfo from '@/hooks/useSafeInfo'
import { useChainId } from '@/hooks/useChainId'
import useWallet from '@/hooks/wallets/useWallet'
-import SignOrExecuteForm from '@/components/tx/SignOrExecuteForm'
import { isExecutable, isMultisigExecutionInfo, isSignableBy } from '@/utils/transaction-guards'
import { createExistingTx } from '@/services/tx/tx-sender'
import { SafeTxContext } from '../../SafeTxProvider'
+import SignOrExecuteForm from '@/components/tx/SignOrExecuteForm'
type ConfirmProposedTxProps = {
txSummary: TransactionSummary
diff --git a/src/components/tx-flow/flows/RecoverAccount/RecoverAccountFlowReview.tsx b/src/components/tx-flow/flows/RecoverAccount/RecoverAccountFlowReview.tsx
index e057ea107f..71db742fce 100644
--- a/src/components/tx-flow/flows/RecoverAccount/RecoverAccountFlowReview.tsx
+++ b/src/components/tx-flow/flows/RecoverAccount/RecoverAccountFlowReview.tsx
@@ -34,6 +34,8 @@ import WalletRejectionError from '@/components/tx/SignOrExecuteForm/WalletReject
import commonCss from '@/components/tx-flow/common/styles.module.css'
import { BlockaidBalanceChanges } from '@/components/tx/security/blockaid/BlockaidBalanceChange'
import NetworkWarning from '@/components/new-safe/create/NetworkWarning'
+import { useGetTransactionDetailsQuery } from '@/store/api/gateway'
+import { skipToken } from '@reduxjs/toolkit/query'
export function RecoverAccountFlowReview({ params }: { params: RecoverAccountFlowProps }): ReactElement | null {
// Form state
@@ -52,6 +54,8 @@ export function RecoverAccountFlowReview({ params }: { params: RecoverAccountFlo
const recovery = data && selectDelayModifierByRecoverer(data, wallet?.address ?? '')
const [, executionValidationError] = useIsValidRecoveryExecTransactionFromModule(recovery?.address, safeTx)
+ const { data: txDetails } = useGetTransactionDetailsQuery(skipToken)
+
// Proposal
const newThreshold = Number(params[RecoverAccountFlowFields.threshold])
const newOwners = params[RecoverAccountFlowFields.owners]
@@ -127,7 +131,7 @@ export function RecoverAccountFlowReview({ params }: { params: RecoverAccountFlo
-
+
diff --git a/src/components/tx-flow/flows/SignMessageOnChain/ReviewSignMessageOnChain.test.tsx b/src/components/tx-flow/flows/SignMessageOnChain/ReviewSignMessageOnChain.test.tsx
index 4acae50f16..a874915640 100644
--- a/src/components/tx-flow/flows/SignMessageOnChain/ReviewSignMessageOnChain.test.tsx
+++ b/src/components/tx-flow/flows/SignMessageOnChain/ReviewSignMessageOnChain.test.tsx
@@ -3,15 +3,26 @@ import * as web3 from '@/hooks/wallets/web3'
import * as useSafeInfo from '@/hooks/useSafeInfo'
import { render, screen } from '@/tests/test-utils'
import * as execThroughRoleHooks from '@/components/tx/SignOrExecuteForm/ExecuteThroughRoleForm/hooks'
+import type { TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk'
import { SafeAppAccessPolicyTypes } from '@safe-global/safe-gateway-typescript-sdk'
import ReviewSignMessageOnChain from '@/components/tx-flow/flows/SignMessageOnChain/ReviewSignMessageOnChain'
import { JsonRpcProvider, zeroPadValue } from 'ethers'
import { act } from '@testing-library/react'
+import type { SafeTxContextParams } from '../../SafeTxProvider'
+import { SafeTxContext } from '../../SafeTxProvider'
+import { createSafeTx } from '@/tests/builders/safeTx'
+import * as hooks from '@/components/tx/SignOrExecuteForm/hooks'
jest.spyOn(execThroughRoleHooks, 'useRoles').mockReturnValue([])
-
describe('ReviewSignMessageOnChain', () => {
test('can handle messages with EIP712Domain type in the JSON-RPC payload', async () => {
+ jest.spyOn(hooks, 'useProposeTx').mockReturnValue([
+ {
+ txInfo: {},
+ } as TransactionDetails,
+ undefined,
+ false,
+ ])
jest.spyOn(web3, 'getWeb3ReadOnly').mockImplementation(() => new JsonRpcProvider())
jest.spyOn(useSafeInfo, 'default').mockImplementation(
() =>
@@ -27,66 +38,74 @@ describe('ReviewSignMessageOnChain', () => {
await act(async () => {
render(
- ,
+
+
+ ,
)
})
diff --git a/src/components/tx-flow/flows/TokenTransfer/ReviewSpendingLimitTx.tsx b/src/components/tx-flow/flows/TokenTransfer/ReviewSpendingLimitTx.tsx
index 8c28146da8..80f51861f4 100644
--- a/src/components/tx-flow/flows/TokenTransfer/ReviewSpendingLimitTx.tsx
+++ b/src/components/tx-flow/flows/TokenTransfer/ReviewSpendingLimitTx.tsx
@@ -23,7 +23,7 @@ import useOnboard from '@/hooks/wallets/useOnboard'
import { asError } from '@/services/exceptions/utils'
import TxCard from '@/components/tx-flow/common/TxCard'
import { TxModalContext } from '@/components/tx-flow'
-import { type SubmitCallback } from '@/components/tx/SignOrExecuteForm'
+import { type SubmitCallback } from '@/components/tx/SignOrExecuteForm/SignOrExecuteForm'
import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions'
import { isWalletRejection } from '@/utils/wallets'
import { safeParseUnits } from '@/utils/formatters'
diff --git a/src/components/tx-flow/flows/TokenTransfer/ReviewTokenTransfer.tsx b/src/components/tx-flow/flows/TokenTransfer/ReviewTokenTransfer.tsx
index d7ec1e3ea1..0c161bff10 100644
--- a/src/components/tx-flow/flows/TokenTransfer/ReviewTokenTransfer.tsx
+++ b/src/components/tx-flow/flows/TokenTransfer/ReviewTokenTransfer.tsx
@@ -1,6 +1,6 @@
import { useContext, useEffect, useMemo } from 'react'
import useBalances from '@/hooks/useBalances'
-import SignOrExecuteForm, { type SubmitCallback } from '@/components/tx/SignOrExecuteForm'
+import SignOrExecuteForm from '@/components/tx/SignOrExecuteForm'
import SendAmountBlock from '@/components/tx-flow/flows/TokenTransfer/SendAmountBlock'
import SendToBlock from '@/components/tx/SendToBlock'
import { createTokenTransferParams } from '@/services/tx/tokenTransferParams'
@@ -8,6 +8,7 @@ import { createTx } from '@/services/tx/tx-sender'
import type { TokenTransferParams } from '.'
import { SafeTxContext } from '../../SafeTxProvider'
import { safeParseUnits } from '@/utils/formatters'
+import type { SubmitCallback } from '@/components/tx/SignOrExecuteForm/SignOrExecuteForm'
const ReviewTokenTransfer = ({
params,
diff --git a/src/components/tx/DecodedTx/index.test.tsx b/src/components/tx/DecodedTx/index.test.tsx
index 078c5c8895..0cf81f15f1 100644
--- a/src/components/tx/DecodedTx/index.test.tsx
+++ b/src/components/tx/DecodedTx/index.test.tsx
@@ -2,12 +2,110 @@ import { fireEvent, render } from '@/tests/test-utils'
import { type SafeTransaction } from '@safe-global/safe-core-sdk-types'
import DecodedTx from '.'
import { waitFor } from '@testing-library/react'
+import { createMockTransactionDetails } from '@/tests/transactions'
+import {
+ DetailedExecutionInfoType,
+ SettingsInfoType,
+ TransactionInfoType,
+} from '@safe-global/safe-gateway-typescript-sdk'
import type { DecodedDataResponse } from '@safe-global/safe-gateway-typescript-sdk'
+const txDetails = createMockTransactionDetails({
+ txInfo: {
+ type: TransactionInfoType.SETTINGS_CHANGE,
+ humanDescription: 'Add new owner 0xd8dA...6045 with threshold 1',
+ dataDecoded: {
+ method: 'addOwnerWithThreshold',
+ parameters: [
+ {
+ name: 'owner',
+ type: 'address',
+ value: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
+ },
+ {
+ name: '_threshold',
+ type: 'uint256',
+ value: '1',
+ },
+ ],
+ },
+ settingsInfo: {
+ type: SettingsInfoType.ADD_OWNER,
+ owner: {
+ value: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
+ name: 'Nevinha',
+ logoUri: 'http://something.com',
+ },
+ threshold: 1,
+ },
+ },
+ txData: {
+ hexData:
+ '0x0d582f13000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000000000000000001',
+ dataDecoded: {
+ method: 'addOwnerWithThreshold',
+ parameters: [
+ {
+ name: 'owner',
+ type: 'address',
+ value: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
+ },
+ {
+ name: '_threshold',
+ type: 'uint256',
+ value: '1',
+ },
+ ],
+ },
+ to: {
+ value: '0xE20CcFf2c38Ef3b64109361D7b7691ff2c7D5f67',
+ name: '',
+ },
+ value: '0',
+ operation: 0,
+ trustedDelegateCallTarget: false,
+ addressInfoIndex: {
+ '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045': {
+ value: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
+ name: 'MetaMultiSigWallet',
+ },
+ },
+ },
+ detailedExecutionInfo: {
+ type: DetailedExecutionInfoType.MULTISIG,
+ submittedAt: 1726064794013,
+ nonce: 4,
+ safeTxGas: '0',
+ baseGas: '0',
+ gasPrice: '0',
+ gasToken: '0x0000000000000000000000000000000000000000',
+ refundReceiver: {
+ value: '0x0000000000000000000000000000000000000000',
+ name: 'MetaMultiSigWallet',
+ },
+ safeTxHash: '0x96a96c11b8d013ff5d7a6ce960b22e961046cfa42eff422ac71c1daf6adef2e0',
+ signers: [
+ {
+ value: '0xDa5e9FA404881Ff36DDa97b41Da402dF6430EE6b',
+ name: '',
+ },
+ ],
+ confirmationsRequired: 1,
+ confirmations: [],
+ rejectors: [],
+ trusted: false,
+ proposer: {
+ value: '0xDa5e9FA404881Ff36DDa97b41Da402dF6430EE6b',
+ name: '',
+ },
+ },
+})
describe('DecodedTx', () => {
it('should render a native transfer', async () => {
const result = render(
{
fireEvent.click(result.getByText('Advanced details'))
await waitFor(() => {
- expect(result.queryByText('safeTxGas:')).toBeInTheDocument()
- expect(result.queryByText('Raw data:')).toBeInTheDocument()
+ expect(result.queryAllByText('safeTxGas:').length).toBeGreaterThan(0)
+ expect(result.queryAllByText('Raw data:').length).toBeGreaterThan(0)
})
})
@@ -95,6 +193,9 @@ describe('DecodedTx', () => {
it('should render an ERC20 transfer', async () => {
const result = render(
{
},
],
}}
- showMethodCall
/>,
)
@@ -134,12 +234,12 @@ describe('DecodedTx', () => {
await waitFor(() => {
expect(result.queryByText('transfer')).toBeInTheDocument()
- expect(result.queryByText('Parameters')).toBeInTheDocument()
+ expect(result.queryAllByText('Parameters').length).toBeGreaterThan(0)
expect(result.queryByText('to')).toBeInTheDocument()
- expect(result.queryByText('address')).toBeInTheDocument()
+ expect(result.queryAllByText('address').length).toBeGreaterThan(0)
expect(result.queryByText('0x474e...78C8')).toBeInTheDocument()
expect(result.queryByText('value')).toBeInTheDocument()
- expect(result.queryByText('uint256')).toBeInTheDocument()
+ expect(result.queryAllByText('uint256').length).toBeGreaterThan(0)
expect(result.queryByText('16745726664999765048')).toBeInTheDocument()
})
})
@@ -228,6 +328,7 @@ describe('DecodedTx', () => {
],
}}
showMethodCall
+ showMultisend
/>,
)
@@ -237,6 +338,7 @@ describe('DecodedTx', () => {
it('should render a function call without parameters', async () => {
const result = render(
{
},
} as SafeTransaction
}
+ showMultisend={false}
decodedData={{
method: 'deposit',
parameters: [],
diff --git a/src/components/tx/DecodedTx/index.tsx b/src/components/tx/DecodedTx/index.tsx
index 09912275da..e213290c02 100644
--- a/src/components/tx/DecodedTx/index.tsx
+++ b/src/components/tx/DecodedTx/index.tsx
@@ -1,11 +1,14 @@
import { type SyntheticEvent, type ReactElement, memo } from 'react'
-import { isCustomTxInfo } from '@/utils/transaction-guards'
-import { Accordion, AccordionDetails, AccordionSummary, Box, Skeleton, Stack } from '@mui/material'
+import {
+ isCustomTxInfo,
+ isMultisigDetailedExecutionInfo,
+ isNativeTokenTransfer,
+ isTransferTxInfo,
+} from '@/utils/transaction-guards'
+import { Accordion, AccordionDetails, AccordionSummary, Box, Stack } from '@mui/material'
import { OperationType, type SafeTransaction } from '@safe-global/safe-core-sdk-types'
-import type { DecodedDataResponse } from '@safe-global/safe-gateway-typescript-sdk'
+import type { DecodedDataResponse, TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk'
import { Operation } from '@safe-global/safe-gateway-typescript-sdk'
-import useChainId from '@/hooks/useChainId'
-import ErrorMessage from '../ErrorMessage'
import Summary, { PartialSummary } from '@/components/transactions/TxDetails/Summary'
import { trackEvent, MODALS_EVENTS } from '@/services/analytics'
import Multisend from '@/components/transactions/TxDetails/TxData/DecodedData/Multisend'
@@ -13,13 +16,11 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import DecodedData from '@/components/transactions/TxDetails/TxData/DecodedData'
import accordionCss from '@/styles/accordion.module.css'
import HelpToolTip from './HelpTooltip'
-import { useGetTransactionDetailsQuery } from '@/store/api/gateway'
-import { skipToken } from '@reduxjs/toolkit/query/react'
-import { asError } from '@/services/exceptions/utils'
type DecodedTxProps = {
tx?: SafeTransaction
txId?: string
+ txDetails?: TransactionDetails
showMultisend?: boolean
decodedData?: DecodedDataResponse
showMethodCall?: boolean
@@ -36,33 +37,24 @@ export const Divider = () => (
const DecodedTx = ({
tx,
- txId,
+ txDetails,
decodedData,
showMultisend = true,
showMethodCall = false,
}: DecodedTxProps): ReactElement => {
- const chainId = useChainId()
- const isMultisend = !!decodedData?.parameters?.[0]?.valueDecoded
+ const isMultisend = decodedData?.parameters && !!decodedData?.parameters[0]?.valueDecoded
const isMethodCallInAdvanced = !showMethodCall || (isMultisend && showMultisend)
- const {
- data: txDetails,
- error: txDetailsError,
- isLoading: txDetailsLoading,
- } = useGetTransactionDetailsQuery(
- chainId && txId
- ? {
- chainId,
- txId,
- }
- : skipToken,
- )
-
const onChangeExpand = (_: SyntheticEvent, expanded: boolean) => {
trackEvent({ ...MODALS_EVENTS.TX_DETAILS, label: expanded ? 'Open' : 'Close' })
}
const addressInfoIndex = txDetails?.txData?.addressInfoIndex
+ const isCreation =
+ txDetails &&
+ isMultisigDetailedExecutionInfo(txDetails.detailedExecutionInfo) &&
+ txDetails.detailedExecutionInfo.confirmations.length === 0
+
const txData = {
dataDecoded: decodedData,
to: { value: tx?.data.to || '' },
@@ -76,11 +68,12 @@ const DecodedTx = ({
let toInfo = tx && {
value: tx.data.to,
}
- if (txDetails && isCustomTxInfo(txDetails.txInfo)) {
- toInfo = txDetails.txInfo.to
+ if (txDetails && isCustomTxInfo(txDetails?.txInfo)) {
+ toInfo = txDetails?.txInfo.to
}
const decodedDataBlock =
+ const showDecodedData = isMethodCallInAdvanced && decodedData?.method
return (
@@ -103,18 +96,20 @@ const DecodedTx = ({
{isMethodCallInAdvanced && decodedData?.method}
- {!showMethodCall && !decodedData?.method && Number(tx?.data.value) > 0 && 'native transfer'}
+ {txDetails &&
+ isTransferTxInfo(txDetails.txInfo) &&
+ isNativeTokenTransfer(txDetails.txInfo.transferInfo) &&
+ 'native transfer'}
-
- {isMethodCallInAdvanced && decodedData?.method && (
+ {showDecodedData && (
<>
{decodedDataBlock}
>
)}
- {txDetails ? (
+ {txDetails && !showDecodedData && !isCreation ? (
)}
-
- {txDetailsLoading && }
-
- {txDetailsError && (
- Failed loading all transaction details
- )}
diff --git a/src/components/tx/SignOrExecuteForm/DelegateForm.tsx b/src/components/tx/SignOrExecuteForm/DelegateForm.tsx
index 37b222ef98..eb02a623bf 100644
--- a/src/components/tx/SignOrExecuteForm/DelegateForm.tsx
+++ b/src/components/tx/SignOrExecuteForm/DelegateForm.tsx
@@ -9,7 +9,7 @@ import commonCss from '@/components/tx-flow/common/styles.module.css'
import ErrorMessage from '@/components/tx/ErrorMessage'
import { TxSecurityContext } from '@/components/tx/security/shared/TxSecurityContext'
import { useTxActions } from '@/components/tx/SignOrExecuteForm/hooks'
-import type { SignOrExecuteProps } from '@/components/tx/SignOrExecuteForm/index'
+import type { SignOrExecuteProps } from '@/components/tx/SignOrExecuteForm/SignOrExecuteForm'
import useWallet from '@/hooks/wallets/useWallet'
import { Errors, trackError } from '@/services/exceptions'
import { asError } from '@/services/exceptions/utils'
diff --git a/src/components/tx/SignOrExecuteForm/ExecuteForm.tsx b/src/components/tx/SignOrExecuteForm/ExecuteForm.tsx
index ea19347871..398614fdf9 100644
--- a/src/components/tx/SignOrExecuteForm/ExecuteForm.tsx
+++ b/src/components/tx/SignOrExecuteForm/ExecuteForm.tsx
@@ -15,7 +15,7 @@ import { useRelaysBySafe } from '@/hooks/useRemainingRelays'
import useWalletCanRelay from '@/hooks/useWalletCanRelay'
import { ExecutionMethod, ExecutionMethodSelector } from '../ExecutionMethodSelector'
import { hasRemainingRelays } from '@/utils/relaying'
-import type { SignOrExecuteProps } from '.'
+import type { SignOrExecuteProps } from './SignOrExecuteForm'
import type { SafeTransaction } from '@safe-global/safe-core-sdk-types'
import { TxModalContext } from '@/components/tx-flow'
import { SuccessScreenFlow } from '@/components/tx-flow/flows'
@@ -48,6 +48,7 @@ export const ExecuteForm = ({
isExecutionLoop: ReturnType
txActions: ReturnType
txSecurity: ReturnType
+ isCreation?: boolean
safeTx?: SafeTransaction
}): ReactElement => {
// Form state
diff --git a/src/components/tx/SignOrExecuteForm/ExecuteThroughRoleForm/__test__/ExecuteThroughRoleForm.test.tsx b/src/components/tx/SignOrExecuteForm/ExecuteThroughRoleForm/__test__/ExecuteThroughRoleForm.test.tsx
index ee0382dcd5..3563336048 100644
--- a/src/components/tx/SignOrExecuteForm/ExecuteThroughRoleForm/__test__/ExecuteThroughRoleForm.test.tsx
+++ b/src/components/tx/SignOrExecuteForm/ExecuteThroughRoleForm/__test__/ExecuteThroughRoleForm.test.tsx
@@ -143,6 +143,7 @@ describe('ExecuteThroughRoleForm', () => {
const { findByText, getByText } = render(
,
@@ -170,7 +171,9 @@ describe('ExecuteThroughRoleForm', () => {
const onSubmit = jest.fn()
- const { findByText } = render( )
+ const { findByText } = render(
+ ,
+ )
fireEvent.click(await findByText('Execute'))
diff --git a/src/components/tx/SignOrExecuteForm/ExecuteThroughRoleForm/index.tsx b/src/components/tx/SignOrExecuteForm/ExecuteThroughRoleForm/index.tsx
index 39acb5a1d1..b8fcc7a600 100644
--- a/src/components/tx/SignOrExecuteForm/ExecuteThroughRoleForm/index.tsx
+++ b/src/components/tx/SignOrExecuteForm/ExecuteThroughRoleForm/index.tsx
@@ -9,7 +9,7 @@ import { useCurrentChain } from '@/hooks/useChains'
import { getTxOptions } from '@/utils/transactions'
import CheckWallet from '@/components/common/CheckWallet'
-import type { SignOrExecuteProps } from '..'
+import type { SignOrExecuteProps } from '../SignOrExecuteForm'
import type { SafeTransaction } from '@safe-global/safe-core-sdk-types'
import { TxModalContext } from '@/components/tx-flow'
import { SuccessScreenFlow } from '@/components/tx-flow/flows'
diff --git a/src/components/tx/SignOrExecuteForm/SignForm.tsx b/src/components/tx/SignOrExecuteForm/SignForm.tsx
index 850432f10a..3e16bc37fe 100644
--- a/src/components/tx/SignOrExecuteForm/SignForm.tsx
+++ b/src/components/tx/SignOrExecuteForm/SignForm.tsx
@@ -7,7 +7,7 @@ import { trackError, Errors } from '@/services/exceptions'
import useIsSafeOwner from '@/hooks/useIsSafeOwner'
import CheckWallet from '@/components/common/CheckWallet'
import { useAlreadySigned, useTxActions } from './hooks'
-import type { SignOrExecuteProps } from '.'
+import type { SignOrExecuteProps } from './SignOrExecuteForm'
import type { SafeTransaction } from '@safe-global/safe-core-sdk-types'
import { TxModalContext } from '@/components/tx-flow'
import commonCss from '@/components/tx-flow/common/styles.module.css'
@@ -34,6 +34,7 @@ export const SignForm = ({
isOwner: ReturnType
txActions: ReturnType
txSecurity: ReturnType
+ isCreation?: boolean
safeTx?: SafeTransaction
}): ReactElement => {
// Form state
diff --git a/src/components/tx/SignOrExecuteForm/SignOrExecuteForm.tsx b/src/components/tx/SignOrExecuteForm/SignOrExecuteForm.tsx
new file mode 100644
index 0000000000..98bf41fada
--- /dev/null
+++ b/src/components/tx/SignOrExecuteForm/SignOrExecuteForm.tsx
@@ -0,0 +1,242 @@
+import DelegateForm from '@/components/tx/SignOrExecuteForm/DelegateForm'
+import CounterfactualForm from '@/features/counterfactual/CounterfactualForm'
+import { useIsWalletDelegate } from '@/hooks/useDelegates'
+import useSafeInfo from '@/hooks/useSafeInfo'
+import { type ReactElement, type ReactNode, useState, useContext, useCallback } from 'react'
+import madProps from '@/utils/mad-props'
+import ExecuteCheckbox from '../ExecuteCheckbox'
+import { useImmediatelyExecutable, useValidateNonce } from './hooks'
+import ExecuteForm from './ExecuteForm'
+import SignForm from './SignForm'
+import { SafeTxContext } from '@/components/tx-flow/SafeTxProvider'
+import ErrorMessage from '../ErrorMessage'
+import TxChecks from './TxChecks'
+import TxCard from '@/components/tx-flow/common/TxCard'
+import ConfirmationTitle, { ConfirmationTitleTypes } from '@/components/tx/SignOrExecuteForm/ConfirmationTitle'
+import { useAppSelector } from '@/store'
+import { selectSettings } from '@/store/settingsSlice'
+import UnknownContractError from './UnknownContractError'
+import { ErrorBoundary } from '@sentry/react'
+import ApprovalEditor from '../ApprovalEditor'
+import { isDelegateCall } from '@/services/tx/tx-sender/sdk'
+import { getTransactionTrackingType } from '@/services/analytics/tx-tracking'
+import { TX_EVENTS } from '@/services/analytics/events/transactions'
+import { trackEvent } from '@/services/analytics'
+import useChainId from '@/hooks/useChainId'
+import ExecuteThroughRoleForm from './ExecuteThroughRoleForm'
+import { findAllowingRole, findMostLikelyRole, useRoles } from './ExecuteThroughRoleForm/hooks'
+import useIsSafeOwner from '@/hooks/useIsSafeOwner'
+import { BlockaidBalanceChanges } from '../security/blockaid/BlockaidBalanceChange'
+import { Blockaid } from '../security/blockaid'
+
+import { MigrateToL2Information } from './MigrateToL2Information'
+import { extractMigrationL2MasterCopyAddress } from '@/utils/transactions'
+
+import { useLazyGetTransactionDetailsQuery } from '@/store/api/gateway'
+import { useApprovalInfos } from '../ApprovalEditor/hooks/useApprovalInfos'
+
+import type { TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk'
+import NetworkWarning from '@/components/new-safe/create/NetworkWarning'
+import ConfirmationView from '../confirmation-views'
+
+export type SubmitCallback = (txId: string, isExecuted?: boolean) => void
+
+export type SignOrExecuteProps = {
+ txId?: string
+ onSubmit?: SubmitCallback
+ children?: ReactNode
+ isExecutable?: boolean
+ isRejection?: boolean
+ isBatch?: boolean
+ isBatchable?: boolean
+ onlyExecute?: boolean
+ disableSubmit?: boolean
+ origin?: string
+ showMethodCall?: boolean
+}
+
+const trackTxEvents = (
+ details: TransactionDetails | undefined,
+ isCreation: boolean,
+ isExecuted: boolean,
+ isRoleExecution: boolean,
+ isDelegateCreation: boolean,
+) => {
+ const creationEvent = isRoleExecution
+ ? TX_EVENTS.CREATE_VIA_ROLE
+ : isDelegateCreation
+ ? TX_EVENTS.CREATE_VIA_DELEGATE
+ : TX_EVENTS.CREATE
+ const executionEvent = isRoleExecution ? TX_EVENTS.EXECUTE_VIA_ROLE : TX_EVENTS.EXECUTE
+ const event = isCreation ? creationEvent : isExecuted ? executionEvent : TX_EVENTS.CONFIRM
+ const txType = getTransactionTrackingType(details)
+ trackEvent({ ...event, label: txType })
+
+ // Immediate execution on creation
+ if (isCreation && isExecuted) {
+ trackEvent({ ...executionEvent, label: txType })
+ }
+}
+
+export const SignOrExecuteForm = ({
+ chainId,
+ safeTx,
+ safeTxError,
+ onSubmit,
+ isCreation,
+ ...props
+}: SignOrExecuteProps & {
+ chainId: ReturnType
+ safeTx: ReturnType
+ safeTxError: ReturnType
+ isCreation?: boolean
+ txDetails?: TransactionDetails
+}): ReactElement => {
+ const { transactionExecution } = useAppSelector(selectSettings)
+ const [shouldExecute, setShouldExecute] = useState(transactionExecution)
+ const isNewExecutableTx = useImmediatelyExecutable() && isCreation
+ const isCorrectNonce = useValidateNonce(safeTx)
+
+ console.log(props.txDetails)
+
+ // TODO: move it to the confirmation view
+ // const showTxDetails =
+ // !isAnyStakingTxInfo(txDetails.txInfo) &&
+ // !isOrderTxInfo(txDetails.txInfo)
+
+ const isBatchable = props.isBatchable !== false && safeTx && !isDelegateCall(safeTx)
+
+ const isDelegate = useIsWalletDelegate()
+
+ const [trigger] = useLazyGetTransactionDetailsQuery()
+ const [readableApprovals] = useApprovalInfos({ safeTransaction: safeTx })
+ const isApproval = readableApprovals && readableApprovals.length > 0
+ const { safe } = useSafeInfo()
+ const isSafeOwner = useIsSafeOwner()
+ const isCounterfactualSafe = !safe.deployed
+ const multiChainMigrationTarget = extractMigrationL2MasterCopyAddress(safeTx)
+ const isMultiChainMigration = !!multiChainMigrationTarget
+
+ // Check if a Zodiac Roles mod is enabled and if the user is a member of any role that allows the transaction
+ const roles = useRoles(
+ !isCounterfactualSafe && isCreation && !(isNewExecutableTx && isSafeOwner) ? safeTx : undefined,
+ )
+ const allowingRole = findAllowingRole(roles)
+ const mostLikelyRole = findMostLikelyRole(roles)
+ const canExecuteThroughRole = !!allowingRole || (!!mostLikelyRole && !isSafeOwner)
+ const preferThroughRole = canExecuteThroughRole && !isSafeOwner // execute through role if a non-owner role member wallet is connected
+
+ // 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 && !preferThroughRole
+ const willExecuteThroughRole =
+ (props.onlyExecute || shouldExecute) && canExecuteThroughRole && (!canExecute || preferThroughRole)
+
+ const onFormSubmit = useCallback(
+ async (txId: string, isExecuted = false, isRoleExecution = false, isDelegateCreation = false) => {
+ onSubmit?.(txId, isExecuted)
+
+ const { data: details } = await trigger({ chainId, txId })
+ // Track tx event
+ trackTxEvents(details, !!isCreation, isExecuted, isRoleExecution, isDelegateCreation)
+ },
+ [chainId, isCreation, onSubmit, trigger],
+ )
+
+ const onRoleExecutionSubmit = useCallback(
+ (txId, isExecuted) => onFormSubmit(txId, isExecuted, true),
+ [onFormSubmit],
+ )
+
+ const onDelegateFormSubmit = useCallback(
+ (txId, isExecuted) => onFormSubmit(txId, isExecuted, false, true),
+ [onFormSubmit],
+ )
+
+ return (
+ <>
+
+ {props.children}
+ {isMultiChainMigration && }
+
+
+ {!props.isRejection && (
+ Error parsing data }>
+ {isApproval &&
}
+
+ )}
+
+
+ {!isCounterfactualSafe && !props.isRejection &&
}
+
+
+ {!isCounterfactualSafe && !props.isRejection &&
}
+
+
+
+
+ {safeTxError && (
+
+ This transaction will most likely fail. To save gas costs, avoid confirming the transaction.
+
+ )}
+
+ {(canExecute || canExecuteThroughRole) && !props.onlyExecute && !isCounterfactualSafe && !isDelegate && (
+
+ )}
+
+
+
+ {!isMultiChainMigration && }
+
+
+
+ {isCounterfactualSafe && !isDelegate && (
+
+ )}
+ {!isCounterfactualSafe && willExecute && !isDelegate && (
+
+ )}
+ {!isCounterfactualSafe && willExecuteThroughRole && (
+
+ )}
+ {!isCounterfactualSafe && !willExecute && !willExecuteThroughRole && !isDelegate && (
+
+ )}
+
+ {isDelegate && }
+
+ >
+ )
+}
+
+const useSafeTx = () => useContext(SafeTxContext).safeTx
+const useSafeTxError = () => useContext(SafeTxContext).safeTxError
+
+export default madProps(SignOrExecuteForm, {
+ chainId: useChainId,
+ safeTx: useSafeTx,
+ safeTxError: useSafeTxError,
+})
diff --git a/src/components/tx/SignOrExecuteForm/SignOrExecuteSkeleton.tsx b/src/components/tx/SignOrExecuteForm/SignOrExecuteSkeleton.tsx
new file mode 100644
index 0000000000..d6c6c20f71
--- /dev/null
+++ b/src/components/tx/SignOrExecuteForm/SignOrExecuteSkeleton.tsx
@@ -0,0 +1,13 @@
+import LoadingSpinner, { SpinnerStatus } from '@/components/new-safe/create/steps/StatusStep/LoadingSpinner'
+import TxCard from '@/components/tx-flow/common/TxCard'
+import { Box } from '@mui/material'
+
+const SignOrExecuteSkeleton = () => (
+
+
+
+
+
+)
+
+export default SignOrExecuteSkeleton
diff --git a/src/components/tx/SignOrExecuteForm/__tests__/ExecuteForm.test.tsx b/src/components/tx/SignOrExecuteForm/__tests__/ExecuteForm.test.tsx
index 60141794fd..3b85d1baee 100644
--- a/src/components/tx/SignOrExecuteForm/__tests__/ExecuteForm.test.tsx
+++ b/src/components/tx/SignOrExecuteForm/__tests__/ExecuteForm.test.tsx
@@ -31,9 +31,16 @@ describe('ExecuteForm', () => {
const defaultProps = {
onSubmit: jest.fn(),
isOwner: true,
+ txId: '0x123123',
isExecutionLoop: false,
relays: [undefined, undefined, false] as AsyncResult
,
- txActions: { signTx: jest.fn(), addToBatch: jest.fn(), executeTx: jest.fn(), signDelegateTx: jest.fn() },
+ txActions: {
+ proposeTx: jest.fn(),
+ signTx: jest.fn(),
+ addToBatch: jest.fn(),
+ executeTx: jest.fn(),
+ signDelegateTx: jest.fn(),
+ },
txSecurity: defaultSecurityContextValues,
}
@@ -107,7 +114,13 @@ describe('ExecuteForm', () => {
const { getByText } = render(
,
)
@@ -138,7 +151,13 @@ describe('ExecuteForm', () => {
{...defaultProps}
safeTx={safeTransaction}
onSubmit={jest.fn()}
- txActions={{ signTx: jest.fn(), addToBatch: jest.fn(), executeTx: mockExecuteTx, signDelegateTx: jest.fn() }}
+ txActions={{
+ proposeTx: jest.fn(),
+ signTx: jest.fn(),
+ addToBatch: jest.fn(),
+ executeTx: mockExecuteTx,
+ signDelegateTx: jest.fn(),
+ }}
/>,
)
@@ -158,7 +177,13 @@ describe('ExecuteForm', () => {
,
)
diff --git a/src/components/tx/SignOrExecuteForm/__tests__/SignForm.test.tsx b/src/components/tx/SignOrExecuteForm/__tests__/SignForm.test.tsx
index ea18ea7c9e..0b4f58851a 100644
--- a/src/components/tx/SignOrExecuteForm/__tests__/SignForm.test.tsx
+++ b/src/components/tx/SignOrExecuteForm/__tests__/SignForm.test.tsx
@@ -24,8 +24,15 @@ describe('SignForm', () => {
const defaultProps = {
onSubmit: jest.fn(),
+ txId: '0x01231',
isOwner: true,
- txActions: { signTx: jest.fn(), addToBatch: jest.fn(), executeTx: jest.fn(), signDelegateTx: jest.fn() },
+ txActions: {
+ proposeTx: jest.fn(),
+ signTx: jest.fn(),
+ addToBatch: jest.fn(),
+ executeTx: jest.fn(),
+ signDelegateTx: jest.fn(),
+ },
txSecurity: defaultSecurityContextValues,
}
@@ -70,7 +77,13 @@ describe('SignForm', () => {
,
)
@@ -90,7 +103,13 @@ describe('SignForm', () => {
,
)
@@ -133,7 +152,13 @@ describe('SignForm', () => {
safeTx={safeTransaction}
isBatchable
isCreation
- txActions={{ signTx: jest.fn(), addToBatch: mockAddToBatch, executeTx: jest.fn(), signDelegateTx: jest.fn() }}
+ txActions={{
+ proposeTx: jest.fn(),
+ signTx: jest.fn(),
+ addToBatch: mockAddToBatch,
+ executeTx: jest.fn(),
+ signDelegateTx: jest.fn(),
+ }}
/>,
)
diff --git a/src/components/tx/SignOrExecuteForm/__tests__/SignOrExecute.test.tsx b/src/components/tx/SignOrExecuteForm/__tests__/SignOrExecute.test.tsx
index 7008fa0c19..44d7769f9d 100644
--- a/src/components/tx/SignOrExecuteForm/__tests__/SignOrExecute.test.tsx
+++ b/src/components/tx/SignOrExecuteForm/__tests__/SignOrExecute.test.tsx
@@ -1,11 +1,10 @@
-import { SignOrExecuteForm } from '@/components/tx/SignOrExecuteForm'
-import * as hooks from '@/components/tx/SignOrExecuteForm/hooks'
-import * as execThroughRoleHooks from '@/components/tx/SignOrExecuteForm/ExecuteThroughRoleForm/hooks'
-import { safeTxBuilder } from '@/tests/builders/safeTx'
+import SignOrExecute from '../index'
import { render } from '@/tests/test-utils'
-import { fireEvent } from '@testing-library/react'
-import { encodeBytes32String } from 'ethers'
-import { Status } from 'zodiac-roles-deployments'
+import * as hooks from '../hooks'
+import type { TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk'
+import type { SafeTxContextParams } from '@/components/tx-flow/SafeTxProvider'
+import { SafeTxContext } from '@/components/tx-flow/SafeTxProvider'
+import { createSafeTx } from '@/tests/builders/safeTx'
let isSafeOwner = true
// mock useIsSafeOwner
@@ -14,215 +13,65 @@ jest.mock('@/hooks/useIsSafeOwner', () => ({
default: jest.fn(() => isSafeOwner),
}))
+// Mock proposeTx
+jest.mock('@/services/tx/proposeTransaction', () => ({
+ __esModule: true,
+ default: jest.fn(() => Promise.resolve({ txId: '123' })),
+}))
+
describe('SignOrExecute', () => {
beforeEach(() => {
isSafeOwner = true
+ jest.clearAllMocks()
})
- it('should display a safeTxError', () => {
- const { getByText } = render(
- ,
- )
-
- expect(
- getByText('This transaction will most likely fail. To save gas costs, avoid confirming the transaction.'),
- ).toBeInTheDocument()
- })
-
- describe('Existing transaction', () => {
- it('should display radio options to sign or execute if both are possible', () => {
- jest.spyOn(hooks, 'useValidateNonce').mockReturnValue(true)
-
- const { getByText } = render(
- ,
- )
-
- expect(getByText('Would you like to execute the transaction immediately?')).toBeInTheDocument()
- })
- })
-
- describe('New transaction', () => {
- it('should display radio options to sign or execute if both are possible', () => {
- jest.spyOn(hooks, 'useValidateNonce').mockReturnValue(true)
- jest.spyOn(hooks, 'useImmediatelyExecutable').mockReturnValue(true)
-
- const { getByText } = render(
- ,
- )
-
- expect(getByText('Would you like to execute the transaction immediately?')).toBeInTheDocument()
- })
-
- it('should offer to execute through a role if the user is a role member and the transaction is executable through the role', () => {
- jest.spyOn(execThroughRoleHooks, 'useRoles').mockReturnValue([TEST_ROLE_OK])
- jest.spyOn(hooks, 'useValidateNonce').mockReturnValue(true)
- jest.spyOn(hooks, 'useImmediatelyExecutable').mockReturnValue(false)
-
- const { queryByTestId } = render(
- ,
- )
-
- expect(queryByTestId('execute-through-role-form-btn')).toBeInTheDocument()
- })
-
- it('should not offer to execute through a role if the user is a safe owner and role member but the role lacks permissions', () => {
- jest.spyOn(execThroughRoleHooks, 'useRoles').mockReturnValue([TEST_ROLE_TARGET_NOT_ALLOWED])
- jest.spyOn(hooks, 'useValidateNonce').mockReturnValue(true)
- jest.spyOn(hooks, 'useImmediatelyExecutable').mockReturnValue(false)
- isSafeOwner = true
-
- const { queryByTestId } = render(
- ,
- )
-
- expect(queryByTestId('execute-through-role-form-btn')).not.toBeInTheDocument()
- })
-
- it('should offer to execute through a role if the user is a role member but not a safe owner, even if the role lacks permissions', () => {
- jest.spyOn(execThroughRoleHooks, 'useRoles').mockReturnValue([TEST_ROLE_TARGET_NOT_ALLOWED])
- jest.spyOn(hooks, 'useValidateNonce').mockReturnValue(true)
- jest.spyOn(hooks, 'useImmediatelyExecutable').mockReturnValue(false)
- isSafeOwner = false
-
- const { queryByTestId } = render(
- ,
- )
-
- expect(queryByTestId('execute-through-role-form-btn')).toBeInTheDocument()
- })
-
- it('should not offer to execute through a role if the transaction can also be directly executed without going through the role', () => {
- jest.spyOn(execThroughRoleHooks, 'useRoles').mockReturnValue([TEST_ROLE_OK])
- jest.spyOn(hooks, 'useValidateNonce').mockReturnValue(true)
-
- const { queryByTestId } = render(
- ,
- )
-
- expect(queryByTestId('execute-through-role-form-btn')).not.toBeInTheDocument()
- })
- })
-
- it('should not display radio options if execution is the only option', () => {
- jest.spyOn(execThroughRoleHooks, 'useRoles').mockReturnValue([])
+ it('should display a loading component', () => {
+ const { container } = render( )
- const { queryByText } = render(
- ,
- )
- expect(queryByText('Would you like to execute the transaction immediately?')).not.toBeInTheDocument()
+ expect(container).toMatchSnapshot()
})
- it('should display a sign/execute title if that option is selected', () => {
- jest.spyOn(hooks, 'useValidateNonce').mockReturnValue(true)
-
- const { getByTestId, getByText } = render(
- ,
+ it('should display a confirmation screen', async () => {
+ jest.spyOn(hooks, 'useProposeTx').mockReturnValue([
+ {
+ txInfo: {},
+ } as TransactionDetails,
+ undefined,
+ false,
+ ])
+
+ const { container, getByTestId } = render(
+
+
+ ,
)
- expect(getByText('Would you like to execute the transaction immediately?')).toBeInTheDocument()
-
- const executeCheckbox = getByTestId('execute-checkbox')
- const signCheckbox = getByTestId('sign-checkbox')
-
- expect(getByText("You're about to execute this transaction.")).toBeInTheDocument()
-
- fireEvent.click(signCheckbox)
-
- expect(getByText("You're about to confirm this transaction.")).toBeInTheDocument()
-
- fireEvent.click(executeCheckbox)
-
- expect(getByText("You're about to execute this transaction.")).toBeInTheDocument()
+ expect(getByTestId('sign-btn')).toBeInTheDocument()
+ expect(container).toMatchSnapshot()
})
- it('should not display safeTxError message for valid transactions', () => {
- const { queryByText } = render(
- ,
+ it('should display an error screen', async () => {
+ jest.spyOn(hooks, 'useProposeTx').mockReturnValue([undefined, new Error('This is a mock error message'), false])
+
+ const { container } = render(
+
+
+ ,
)
- expect(
- queryByText('This transaction will most likely fail. To save gas costs, avoid confirming the transaction.'),
- ).not.toBeInTheDocument()
+ expect(container.querySelector('sign-btn')).not.toBeInTheDocument()
+ expect(container).toMatchSnapshot()
})
})
-
-const ROLES_MOD_ADDRESS = '0x1234567890000000000000000000000000000000'
-const ROLE_KEY = encodeBytes32String('eth_wrapping')
-
-const TEST_ROLE_OK: execThroughRoleHooks.Role = {
- modAddress: ROLES_MOD_ADDRESS,
- roleKey: ROLE_KEY as `0x${string}`,
- multiSend: '0x9641d764fc13c8b624c04430c7356c1c7c8102e2',
- status: Status.Ok,
-}
-
-const TEST_ROLE_TARGET_NOT_ALLOWED: execThroughRoleHooks.Role = {
- modAddress: ROLES_MOD_ADDRESS,
- roleKey: ROLE_KEY as `0x${string}`,
- multiSend: '0x9641d764fc13c8b624c04430c7356c1c7c8102e2',
- status: Status.TargetAddressNotAllowed,
-}
diff --git a/src/components/tx/SignOrExecuteForm/__tests__/SignOrExecuteForm.test.tsx b/src/components/tx/SignOrExecuteForm/__tests__/SignOrExecuteForm.test.tsx
new file mode 100644
index 0000000000..2abab2eef7
--- /dev/null
+++ b/src/components/tx/SignOrExecuteForm/__tests__/SignOrExecuteForm.test.tsx
@@ -0,0 +1,365 @@
+import { SignOrExecuteForm } from '@/components/tx/SignOrExecuteForm/SignOrExecuteForm'
+import * as hooks from '@/components/tx/SignOrExecuteForm/hooks'
+import * as execThroughRoleHooks from '@/components/tx/SignOrExecuteForm/ExecuteThroughRoleForm/hooks'
+import { safeTxBuilder } from '@/tests/builders/safeTx'
+import { render } from '@/tests/test-utils'
+import { fireEvent } from '@testing-library/react'
+import { encodeBytes32String } from 'ethers'
+import { Status } from 'zodiac-roles-deployments'
+import type { TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk'
+
+let isSafeOwner = true
+// mock useIsSafeOwner
+jest.mock('@/hooks/useIsSafeOwner', () => ({
+ __esModule: true,
+ default: jest.fn(() => isSafeOwner),
+}))
+
+jest.mock('@/hooks/useSafeAddress', () => ({
+ __esModule: true,
+ default: jest.fn(() => '0xE20CcFf2c38Ef3b64109361D7b7691ff2c7D5f67'),
+}))
+
+const txDetails = {
+ safeAddress: '0xE20CcFf2c38Ef3b64109361D7b7691ff2c7D5f67',
+ txId: 'multisig_0xE20CcFf2c38Ef3b64109361D7b7691ff2c7D5f67_0x938635afdeab5ab17b377896f10dbe161fcc44d488296bc0000b733623d57c80',
+ executedAt: null,
+ txStatus: 'AWAITING_EXECUTION',
+ txInfo: {
+ type: 'SettingsChange',
+ humanDescription: 'Add new owner 0xd8dA...6045 with threshold 1',
+ dataDecoded: {
+ method: 'addOwnerWithThreshold',
+ parameters: [
+ {
+ name: 'owner',
+ type: 'address',
+ value: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
+ valueDecoded: null,
+ },
+ {
+ name: '_threshold',
+ type: 'uint256',
+ value: '1',
+ valueDecoded: null,
+ },
+ ],
+ },
+ settingsInfo: {
+ type: 'ADD_OWNER',
+ owner: {
+ value: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
+ name: null,
+ logoUri: null,
+ },
+ threshold: 1,
+ },
+ },
+ txData: {
+ hexData:
+ '0x0d582f13000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000000000000000001',
+ dataDecoded: {
+ method: 'addOwnerWithThreshold',
+ parameters: [
+ {
+ name: 'owner',
+ type: 'address',
+ value: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
+ valueDecoded: null,
+ },
+ {
+ name: '_threshold',
+ type: 'uint256',
+ value: '1',
+ valueDecoded: null,
+ },
+ ],
+ },
+ to: {
+ value: '0xE20CcFf2c38Ef3b64109361D7b7691ff2c7D5f67',
+ name: 'SafeProxy',
+ logoUri: null,
+ },
+ value: '0',
+ operation: 0,
+ trustedDelegateCallTarget: null,
+ addressInfoIndex: null,
+ },
+ txHash: null,
+ detailedExecutionInfo: {
+ type: 'MULTISIG',
+ submittedAt: 1726497729356,
+ nonce: 8,
+ safeTxGas: '0',
+ baseGas: '0',
+ gasPrice: '0',
+ gasToken: '0x0000000000000000000000000000000000000000',
+ refundReceiver: {
+ value: '0x0000000000000000000000000000000000000000',
+ name: 'MetaMultiSigWallet',
+ logoUri: null,
+ },
+ safeTxHash: '0x938635afdeab5ab17b377896f10dbe161fcc44d488296bc0000b733623d57c80',
+ executor: null,
+ signers: [
+ {
+ value: '0xDa5e9FA404881Ff36DDa97b41Da402dF6430EE6b',
+ name: null,
+ logoUri: null,
+ },
+ ],
+ confirmationsRequired: 1,
+ confirmations: [
+ {
+ signer: {
+ value: '0xDa5e9FA404881Ff36DDa97b41Da402dF6430EE6b',
+ name: null,
+ logoUri: null,
+ },
+ signature:
+ '0xd91721922d38384a4d40b20d923c49cefb56f60bfe0b357de11a4a044483d670075842d7bba26cf4aa84788ab0bd85137ad09c7f9cd84154db00d456b15e42dc1b',
+ submittedAt: 1726497740521,
+ },
+ ],
+ rejectors: [],
+ gasTokenInfo: null,
+ trusted: true,
+ proposer: {
+ value: '0xDa5e9FA404881Ff36DDa97b41Da402dF6430EE6b',
+ name: null,
+ logoUri: null,
+ },
+ },
+ safeAppInfo: null,
+} as unknown as TransactionDetails
+
+describe('SignOrExecute', () => {
+ beforeEach(() => {
+ isSafeOwner = true
+ })
+
+ it('should display a safeTxError', () => {
+ const { getByText } = render(
+ ,
+ )
+
+ expect(
+ getByText('This transaction will most likely fail. To save gas costs, avoid confirming the transaction.'),
+ ).toBeInTheDocument()
+ })
+
+ describe('Existing transaction', () => {
+ it('should display radio options to sign or execute if both are possible', () => {
+ jest.spyOn(hooks, 'useValidateNonce').mockReturnValue(true)
+
+ const { getByText } = render(
+ ,
+ )
+
+ expect(getByText('Would you like to execute the transaction immediately?')).toBeInTheDocument()
+ })
+ })
+
+ describe('New transaction', () => {
+ it('should display radio options to sign or execute if both are possible', () => {
+ jest.spyOn(hooks, 'useValidateNonce').mockReturnValue(true)
+ jest.spyOn(hooks, 'useImmediatelyExecutable').mockReturnValue(true)
+
+ const { getByText } = render(
+ ,
+ )
+
+ expect(getByText('Would you like to execute the transaction immediately?')).toBeInTheDocument()
+ })
+
+ it('should offer to execute through a role if the user is a role member and the transaction is executable through the role', () => {
+ jest.spyOn(execThroughRoleHooks, 'useRoles').mockReturnValue([TEST_ROLE_OK])
+ jest.spyOn(hooks, 'useValidateNonce').mockReturnValue(true)
+ jest.spyOn(hooks, 'useImmediatelyExecutable').mockReturnValue(false)
+
+ const { queryByTestId } = render(
+ ,
+ )
+
+ expect(queryByTestId('execute-through-role-form-btn')).toBeInTheDocument()
+ })
+
+ it('should not offer to execute through a role if the user is a safe owner and role member but the role lacks permissions', () => {
+ jest.spyOn(execThroughRoleHooks, 'useRoles').mockReturnValue([TEST_ROLE_TARGET_NOT_ALLOWED])
+ jest.spyOn(hooks, 'useValidateNonce').mockReturnValue(true)
+ jest.spyOn(hooks, 'useImmediatelyExecutable').mockReturnValue(false)
+ isSafeOwner = true
+
+ const { queryByTestId } = render(
+ ,
+ )
+
+ expect(queryByTestId('execute-through-role-form-btn')).not.toBeInTheDocument()
+ })
+
+ it('should offer to execute through a role if the user is a role member but not a safe owner, even if the role lacks permissions', () => {
+ jest.spyOn(execThroughRoleHooks, 'useRoles').mockReturnValue([TEST_ROLE_TARGET_NOT_ALLOWED])
+ jest.spyOn(hooks, 'useValidateNonce').mockReturnValue(true)
+ jest.spyOn(hooks, 'useImmediatelyExecutable').mockReturnValue(false)
+ isSafeOwner = false
+
+ const { queryByTestId } = render(
+ ,
+ )
+
+ expect(queryByTestId('execute-through-role-form-btn')).toBeInTheDocument()
+ })
+
+ it('should not offer to execute through a role if the transaction can also be directly executed without going through the role', () => {
+ jest.spyOn(execThroughRoleHooks, 'useRoles').mockReturnValue([TEST_ROLE_OK])
+ jest.spyOn(hooks, 'useValidateNonce').mockReturnValue(true)
+
+ const { queryByTestId } = render(
+ ,
+ )
+
+ expect(queryByTestId('execute-through-role-form-btn')).not.toBeInTheDocument()
+ })
+ })
+
+ it('should not display radio options if execution is the only option', () => {
+ jest.spyOn(execThroughRoleHooks, 'useRoles').mockReturnValue([])
+
+ const { queryByText } = render(
+ ,
+ )
+ expect(queryByText('Would you like to execute the transaction immediately?')).not.toBeInTheDocument()
+ })
+
+ it('should display a sign/execute title if that option is selected', () => {
+ jest.spyOn(hooks, 'useValidateNonce').mockReturnValue(true)
+
+ const { getByTestId, getByText } = render(
+ ,
+ )
+
+ expect(getByText('Would you like to execute the transaction immediately?')).toBeInTheDocument()
+
+ const executeCheckbox = getByTestId('execute-checkbox')
+ const signCheckbox = getByTestId('sign-checkbox')
+
+ expect(getByText("You're about to execute this transaction.")).toBeInTheDocument()
+
+ fireEvent.click(signCheckbox)
+
+ expect(getByText("You're about to confirm this transaction.")).toBeInTheDocument()
+
+ fireEvent.click(executeCheckbox)
+
+ expect(getByText("You're about to execute this transaction.")).toBeInTheDocument()
+ })
+
+ it('should not display safeTxError message for valid transactions', () => {
+ const { queryByText } = render(
+ ,
+ )
+
+ expect(
+ queryByText('This transaction will most likely fail. To save gas costs, avoid confirming the transaction.'),
+ ).not.toBeInTheDocument()
+ })
+})
+
+const ROLES_MOD_ADDRESS = '0x1234567890000000000000000000000000000000'
+const ROLE_KEY = encodeBytes32String('eth_wrapping')
+
+const TEST_ROLE_OK: execThroughRoleHooks.Role = {
+ modAddress: ROLES_MOD_ADDRESS,
+ roleKey: ROLE_KEY as `0x${string}`,
+ multiSend: '0x9641d764fc13c8b624c04430c7356c1c7c8102e2',
+ status: Status.Ok,
+}
+
+const TEST_ROLE_TARGET_NOT_ALLOWED: execThroughRoleHooks.Role = {
+ modAddress: ROLES_MOD_ADDRESS,
+ roleKey: ROLE_KEY as `0x${string}`,
+ multiSend: '0x9641d764fc13c8b624c04430c7356c1c7c8102e2',
+ status: Status.TargetAddressNotAllowed,
+}
diff --git a/src/components/tx/SignOrExecuteForm/__tests__/__snapshots__/SignOrExecute.test.tsx.snap b/src/components/tx/SignOrExecuteForm/__tests__/__snapshots__/SignOrExecute.test.tsx.snap
new file mode 100644
index 0000000000..87e0722b7b
--- /dev/null
+++ b/src/components/tx/SignOrExecuteForm/__tests__/__snapshots__/SignOrExecute.test.tsx.snap
@@ -0,0 +1,524 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SignOrExecute should display a confirmation screen 1`] = `
+
+
+
+
+
+
+
+
+
+
+ confirm
+
+
+ You're about to
+ create and
+ confirm
+ this transaction.
+
+
+
+
+
+
+
+`;
+
+exports[`SignOrExecute should display a loading component 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`SignOrExecute should display an error screen 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+ bytes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ confirm
+
+
+ You're about to
+ create and
+ confirm
+ this transaction.
+
+
+
+
+
+
+
+`;
diff --git a/src/components/tx/SignOrExecuteForm/hooks.ts b/src/components/tx/SignOrExecuteForm/hooks.ts
index 528b64c22c..0230cf14c2 100644
--- a/src/components/tx/SignOrExecuteForm/hooks.ts
+++ b/src/components/tx/SignOrExecuteForm/hooks.ts
@@ -16,10 +16,13 @@ import {
} from '@/services/tx/tx-sender'
import { useHasPendingTxs } from '@/hooks/usePendingTxs'
import { getSafeTxGas, getNonces } from '@/services/tx/tx-sender/recommendedNonce'
+import type { AsyncResult } from '@/hooks/useAsync'
import useAsync from '@/hooks/useAsync'
import { useUpdateBatch } from '@/hooks/useDraftBatch'
-import { type TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk'
+import { getTransactionDetails, type TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk'
import { useCurrentChain } from '@/hooks/useChains'
+import directProposeTx from '@/services/tx/proposeTransaction'
+import { getAndValidateSafeSDK } from '@/services/tx/tx-sender/sdk'
type TxActions = {
addToBatch: (safeTx?: SafeTransaction, origin?: string) => Promise
@@ -32,6 +35,27 @@ type TxActions = {
isRelayed?: boolean,
) => Promise
signDelegateTx: (safeTx?: SafeTransaction) => Promise
+ proposeTx: (safeTx: SafeTransaction, txId?: string, origin?: string) => Promise
+}
+
+type txDetails = AsyncResult
+
+export const useProposeTx = (safeTx?: SafeTransaction, txId?: string, origin?: string): txDetails => {
+ const { safe } = useSafeInfo()
+ const wallet = useWallet()
+ const sender = wallet?.address || safe.owners?.[0]?.value
+
+ return useAsync(
+ async () => {
+ if (txId) return getTransactionDetails(safe.chainId, txId)
+ if (!safeTx || !sender) return
+ const safeSDK = getAndValidateSafeSDK()
+ const safeTxHash = await safeSDK.getTransactionHash(safeTx)
+ return directProposeTx(safe.chainId, safe.address.value, sender, safeTx, safeTxHash, origin)
+ },
+ [safeTx, txId, origin, safe.chainId, safe.address.value, sender],
+ false,
+ )
}
export const useTxActions = (): TxActions => {
@@ -45,7 +69,7 @@ export const useTxActions = (): TxActions => {
const safeAddress = safe.address.value
const { chainId, version } = safe
- const proposeTx = async (sender: string, safeTx: SafeTransaction, txId?: string, origin?: string) => {
+ const _propose = async (sender: string, safeTx: SafeTransaction, txId?: string, origin?: string) => {
return dispatchTxProposal({
chainId,
safeAddress,
@@ -56,11 +80,16 @@ export const useTxActions = (): TxActions => {
})
}
+ const proposeTx: TxActions['proposeTx'] = async (safeTx, txId, origin) => {
+ assertTx(safeTx)
+ return _propose(wallet?.address || safe.owners[0].value, safeTx, txId, origin)
+ }
+
const addToBatch: TxActions['addToBatch'] = async (safeTx, origin) => {
assertTx(safeTx)
assertWallet(wallet)
- const tx = await proposeTx(wallet.address, safeTx, undefined, origin)
+ const tx = await _propose(wallet.address, safeTx, undefined, origin)
await addTxToBatch(tx)
return tx.txId
}
@@ -86,14 +115,14 @@ export const useTxActions = (): TxActions => {
// If the first signature is a smart contract wallet, we have to propose w/o signatures
// Otherwise the backend won't pick up the tx
// The signature will be added once the on-chain signature is indexed
- const id = txId || (await proposeTx(wallet.address, safeTx, txId, origin)).txId
+ const id = txId || (await _propose(wallet.address, safeTx, txId, origin)).txId
await dispatchOnChainSigning(safeTx, id, wallet.provider, chainId, wallet.address, safeAddress)
return id
}
// Otherwise, sign off-chain
const signedTx = await dispatchTxSigning(safeTx, version, wallet.provider, txId)
- const tx = await proposeTx(wallet.address, signedTx, txId, origin)
+ const tx = await _propose(wallet.address, signedTx, txId, origin)
return tx.txId
}
@@ -104,7 +133,7 @@ export const useTxActions = (): TxActions => {
const signedTx = await dispatchDelegateTxSigning(safeTx, wallet)
- const tx = await proposeTx(wallet.address, signedTx)
+ const tx = await _propose(wallet.address, signedTx)
return tx.txId
}
@@ -119,9 +148,9 @@ export const useTxActions = (): TxActions => {
if (isRelayed && safeTx.signatures.size < safe.threshold) {
if (txId) {
safeTx = await signRelayedTx(safeTx)
- tx = await proposeTx(wallet.address, safeTx, txId, origin)
+ tx = await _propose(wallet.address, safeTx, txId, origin)
} else {
- tx = await proposeTx(wallet.address, safeTx, txId, origin)
+ tx = await _propose(wallet.address, safeTx, txId, origin)
safeTx = await signRelayedTx(safeTx)
}
txId = tx.txId
@@ -129,7 +158,7 @@ export const useTxActions = (): TxActions => {
// Propose the tx if there's no id yet ("immediate execution")
if (!txId) {
- tx = await proposeTx(wallet.address, safeTx, txId, origin)
+ tx = await _propose(wallet.address, safeTx, txId, origin)
txId = tx.txId
}
@@ -145,7 +174,7 @@ export const useTxActions = (): TxActions => {
return txId
}
- return { addToBatch, signTx, executeTx, signDelegateTx }
+ return { addToBatch, signTx, executeTx, signDelegateTx, proposeTx }
}, [safe, wallet, addTxToBatch, onboard, chain])
}
diff --git a/src/components/tx/SignOrExecuteForm/index.tsx b/src/components/tx/SignOrExecuteForm/index.tsx
index 2fcbde8c56..d7696aeb0b 100644
--- a/src/components/tx/SignOrExecuteForm/index.tsx
+++ b/src/components/tx/SignOrExecuteForm/index.tsx
@@ -1,54 +1,15 @@
-import DelegateForm from '@/components/tx/SignOrExecuteForm/DelegateForm'
-import CounterfactualForm from '@/features/counterfactual/CounterfactualForm'
-import { useIsWalletDelegate } from '@/hooks/useDelegates'
-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'
-import ExecuteCheckbox from '../ExecuteCheckbox'
-import { useImmediatelyExecutable, useValidateNonce } from './hooks'
-import ExecuteForm from './ExecuteForm'
-import SignForm from './SignForm'
import { SafeTxContext } from '@/components/tx-flow/SafeTxProvider'
-import ErrorMessage from '../ErrorMessage'
-import TxChecks from './TxChecks'
-import TxCard from '@/components/tx-flow/common/TxCard'
-import ConfirmationTitle, { ConfirmationTitleTypes } from '@/components/tx/SignOrExecuteForm/ConfirmationTitle'
-import { useAppSelector } from '@/store'
-import { selectSettings } from '@/store/settingsSlice'
-import UnknownContractError from './UnknownContractError'
-import useDecodeTx from '@/hooks/useDecodeTx'
-import { ErrorBoundary } from '@sentry/react'
-import ApprovalEditor from '../ApprovalEditor'
-import { isDelegateCall } from '@/services/tx/tx-sender/sdk'
-import { getTransactionTrackingType } from '@/services/analytics/tx-tracking'
-import { TX_EVENTS } from '@/services/analytics/events/transactions'
-import { trackEvent } from '@/services/analytics'
-import useChainId from '@/hooks/useChainId'
-import ExecuteThroughRoleForm from './ExecuteThroughRoleForm'
-import { findAllowingRole, findMostLikelyRole, useRoles } from './ExecuteThroughRoleForm/hooks'
-import { isAnyStakingTxInfo, isCustomTxInfo, isGenericConfirmation, isOrderTxInfo } from '@/utils/transaction-guards'
-import useIsSafeOwner from '@/hooks/useIsSafeOwner'
-import { BlockaidBalanceChanges } from '../security/blockaid/BlockaidBalanceChange'
-import { Blockaid } from '../security/blockaid'
-
-import TxData from '@/components/transactions/TxDetails/TxData'
-import ConfirmationOrder from '@/components/tx/ConfirmationOrder'
-import { useApprovalInfos } from '../ApprovalEditor/hooks/useApprovalInfos'
-import { MigrateToL2Information } from './MigrateToL2Information'
-import { extractMigrationL2MasterCopyAddress } from '@/utils/transactions'
-
-import type { TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk'
-import { useGetTransactionDetailsQuery, useLazyGetTransactionDetailsQuery } from '@/store/api/gateway'
-import { skipToken } from '@reduxjs/toolkit/query/react'
-import NetworkWarning from '@/components/new-safe/create/NetworkWarning'
-
-export type SubmitCallback = (txId: string, isExecuted?: boolean) => void
+import SignOrExecuteForm from './SignOrExecuteForm'
+import type { SignOrExecuteProps, SubmitCallback } from './SignOrExecuteForm'
+import SignOrExecuteSkeleton from './SignOrExecuteSkeleton'
+import { useProposeTx } from './hooks'
+import { useContext } from 'react'
+import useSafeInfo from '@/hooks/useSafeInfo'
-export type SignOrExecuteProps = {
- txId?: string
+type SignOrExecuteExtendedProps = Omit & {
onSubmit?: SubmitCallback
- children?: ReactNode
+ txId?: string
+ children?: React.ReactNode
isExecutable?: boolean
isRejection?: boolean
isBatch?: boolean
@@ -60,203 +21,21 @@ export type SignOrExecuteProps = {
showMethodCall?: boolean
}
-const trackTxEvents = (
- details: TransactionDetails | undefined,
- isCreation: boolean,
- isExecuted: boolean,
- isRoleExecution: boolean,
- isDelegateCreation: boolean,
-) => {
- const creationEvent = isRoleExecution
- ? TX_EVENTS.CREATE_VIA_ROLE
- : isDelegateCreation
- ? TX_EVENTS.CREATE_VIA_DELEGATE
- : TX_EVENTS.CREATE
- const executionEvent = isRoleExecution ? TX_EVENTS.EXECUTE_VIA_ROLE : TX_EVENTS.EXECUTE
- const event = isCreation ? creationEvent : isExecuted ? executionEvent : TX_EVENTS.CONFIRM
- const txType = getTransactionTrackingType(details)
- trackEvent({ ...event, label: txType })
-
- // Immediate execution on creation
- if (isCreation && isExecuted) {
- trackEvent({ ...executionEvent, label: txType })
- }
-}
-
-export const SignOrExecuteForm = ({
- chainId,
- safeTx,
- safeTxError,
- onSubmit,
- ...props
-}: SignOrExecuteProps & {
- chainId: ReturnType
- safeTx: ReturnType
- safeTxError: ReturnType
-}): ReactElement => {
- const { transactionExecution } = useAppSelector(selectSettings)
- const [shouldExecute, setShouldExecute] = useState(transactionExecution)
- const isCreation = !props.txId
- const isNewExecutableTx = useImmediatelyExecutable() && isCreation
- const isCorrectNonce = useValidateNonce(safeTx)
- const [decodedData] = useDecodeTx(safeTx)
-
- const isBatchable = props.isBatchable !== false && safeTx && !isDelegateCall(safeTx)
-
- const { data: txDetails } = useGetTransactionDetailsQuery(
- chainId && props.txId
- ? {
- chainId,
- txId: props.txId,
- }
- : skipToken,
- )
- const showTxDetails =
- props.txId &&
- txDetails &&
- !isCustomTxInfo(txDetails.txInfo) &&
- !isAnyStakingTxInfo(txDetails.txInfo) &&
- !isOrderTxInfo(txDetails.txInfo)
- const isDelegate = useIsWalletDelegate()
- const [trigger] = useLazyGetTransactionDetailsQuery()
- const [readableApprovals] = useApprovalInfos({ safeTransaction: safeTx })
- const isApproval = readableApprovals && readableApprovals.length > 0
-
+const SignOrExecute = (props: SignOrExecuteExtendedProps) => {
+ const { safeTx } = useContext(SafeTxContext)
const { safe } = useSafeInfo()
- const isSafeOwner = useIsSafeOwner()
- const isCounterfactualSafe = !safe.deployed
- const multiChainMigrationTarget = extractMigrationL2MasterCopyAddress(safeTx)
- const isMultiChainMigration = !!multiChainMigrationTarget
-
- // Check if a Zodiac Roles mod is enabled and if the user is a member of any role that allows the transaction
- const roles = useRoles(
- !isCounterfactualSafe && isCreation && !(isNewExecutableTx && isSafeOwner) ? safeTx : undefined,
- )
- const allowingRole = findAllowingRole(roles)
- const mostLikelyRole = findMostLikelyRole(roles)
- const canExecuteThroughRole = !!allowingRole || (!!mostLikelyRole && !isSafeOwner)
- const preferThroughRole = canExecuteThroughRole && !isSafeOwner // execute through role if a non-owner role member wallet is connected
-
- // 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 && !preferThroughRole
- const willExecuteThroughRole =
- (props.onlyExecute || shouldExecute) && canExecuteThroughRole && (!canExecute || preferThroughRole)
-
- const onFormSubmit = useCallback(
- async (txId: string, isExecuted = false, isRoleExecution = false, isDelegateCreation = false) => {
- onSubmit?.(txId, isExecuted)
-
- const { data: details } = await trigger({ chainId, txId })
- // Track tx event
- trackTxEvents(details, isCreation, isExecuted, isRoleExecution, isDelegateCreation)
- },
- [chainId, isCreation, onSubmit, trigger],
- )
-
- const onRoleExecutionSubmit = useCallback(
- (txId, isExecuted) => onFormSubmit(txId, isExecuted, true),
- [onFormSubmit],
- )
+ const [txDetails, error] = useProposeTx(safe.deployed ? safeTx : undefined, props.txId, props.origin)
- const onDelegateFormSubmit = useCallback(
- (txId, isExecuted) => onFormSubmit(txId, isExecuted, false, true),
- [onFormSubmit],
- )
+ // Show the loader only the first time the tx is being loaded
+ if ((!txDetails && !error && safe.deployed) || !safeTx) {
+ return
+ }
return (
- <>
-
- {props.children}
-
- {isMultiChainMigration && }
-
- {decodedData && (
- >}>
-
-
- )}
-
- {!props.isRejection && decodedData && (
- Error parsing data }>
- {isApproval &&