diff --git a/cypress/e2e/smoke/create_tx.cy.js b/cypress/e2e/smoke/create_tx.cy.js index 49d2f9a66d..6845f84a60 100644 --- a/cypress/e2e/smoke/create_tx.cy.js +++ b/cypress/e2e/smoke/create_tx.cy.js @@ -69,7 +69,7 @@ describe('Queue a transaction on 1/N', () => { cy.contains('Estimated fee').should('exist') // Asserting the sponsored info is present - cy.contains('Execute').should('be.visible') + cy.contains('Execute').scrollIntoView().should('be.visible') cy.get('span').contains('Estimated fee').next().should('have.css', 'text-decoration-line', 'line-through') cy.contains('Transactions per hour') @@ -103,7 +103,7 @@ describe('Queue a transaction on 1/N', () => { .type(currentNonce + 10, { force: true }) .type('{enter}', { force: true }) - cy.contains('Submit').click() + cy.contains('Sign').click() }) it('should click the notification and see the transaction queued', () => { diff --git a/cypress/e2e/smoke/nfts.cy.js b/cypress/e2e/smoke/nfts.cy.js index 75686930a7..3a5d8e9d46 100644 --- a/cypress/e2e/smoke/nfts.cy.js +++ b/cypress/e2e/smoke/nfts.cy.js @@ -84,7 +84,7 @@ describe('Assets > NFTs', () => { cy.contains('1') cy.contains('2') cy.get('b:contains("safeTransferFrom")').should('have.length', 2) - cy.contains('button:not([disabled])', 'Submit') + cy.contains('button:not([disabled])', 'Execute') }) }) }) diff --git a/src/components/tx-flow/common/TxLayout/index.tsx b/src/components/tx-flow/common/TxLayout/index.tsx index 733ad21b77..4ef5154aa5 100644 --- a/src/components/tx-flow/common/TxLayout/index.tsx +++ b/src/components/tx-flow/common/TxLayout/index.tsx @@ -9,11 +9,10 @@ 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/security/tenderly' import SafeLogo from '@/public/images/logo-no-text.svg' -import { RedefineMessage } from '@/components/tx/security/redefine' import { TxSecurityProvider } from '@/components/tx/security/shared/TxSecurityContext' import ChainIndicator from '@/components/common/ChainIndicator' +import SecurityWarnings from '@/components/tx/security/SecurityWarnings' const TxLayoutHeader = ({ hideNonce, @@ -147,9 +146,7 @@ const TxLayout = ({ )} - - - + diff --git a/src/components/tx-flow/common/TxLayout/styles.module.css b/src/components/tx-flow/common/TxLayout/styles.module.css index 5ff2454e6a..ef349f1bc1 100644 --- a/src/components/tx-flow/common/TxLayout/styles.module.css +++ b/src/components/tx-flow/common/TxLayout/styles.module.css @@ -1,5 +1,5 @@ .container { - margin-top: 42px; + margin-top: 10px; } .header { diff --git a/src/components/tx-flow/flows/NewTx/styles.module.css b/src/components/tx-flow/flows/NewTx/styles.module.css index b6fb3f8456..a8e29e5b8c 100644 --- a/src/components/tx-flow/flows/NewTx/styles.module.css +++ b/src/components/tx-flow/flows/NewTx/styles.module.css @@ -1,7 +1,3 @@ -.container { - margin-top: 50px; -} - .chain { align-self: flex-end; margin-bottom: var(--space-2); diff --git a/src/components/tx-flow/flows/SuccessScreen/index.tsx b/src/components/tx-flow/flows/SuccessScreen/index.tsx index 641a0b51d1..6aa0226c5e 100644 --- a/src/components/tx-flow/flows/SuccessScreen/index.tsx +++ b/src/components/tx-flow/flows/SuccessScreen/index.tsx @@ -1,26 +1,27 @@ -import { useRouter } from 'next/router' import StatusMessage from './StatusMessage' import StatusStepper from './StatusStepper' -import { AppRoutes } from '@/config/routes' import { Button, Container, Divider, Paper } from '@mui/material' import classnames from 'classnames' import Link from 'next/link' -import { type UrlObject } from 'url' import css from './styles.module.css' import { useAppSelector } from '@/store' import { selectPendingTxById } from '@/store/pendingTxsSlice' -import { useEffect, useState } from 'react' -import { getBlockExplorerLink } from '@/utils/chains' +import { useEffect, useState, useCallback, useContext } from 'react' +import { getTxLink } from '@/hooks/useTxNotifications' import { useCurrentChain } from '@/hooks/useChains' import { TxEvent, txSubscribe } from '@/services/tx/txEvents' +import useSafeInfo from '@/hooks/useSafeInfo' +import { TxModalContext } from '../..' export const SuccessScreen = ({ txId }: { txId: string }) => { const [localTxHash, setLocalTxHash] = useState() const [error, setError] = useState() - const router = useRouter() + const { setTxFlow } = useContext(TxModalContext) const chain = useCurrentChain() const pendingTx = useAppSelector((state) => selectPendingTxById(state, txId)) + const { safeAddress } = useSafeInfo() const { txHash = '', status } = pendingTx || {} + const txLink = chain && getTxLink(txId, chain, safeAddress) useEffect(() => { if (!txHash) return @@ -38,12 +39,9 @@ export const SuccessScreen = ({ txId }: { txId: string }) => { return () => unsubFns.forEach((unsubscribe) => unsubscribe()) }, [txId]) - const homeLink: UrlObject = { - pathname: AppRoutes.home, - query: { safe: router.query.safe }, - } - - const txLink = chain && localTxHash ? getBlockExplorerLink(chain, localTxHash) : undefined + const onFinishClick = useCallback(() => { + setTxFlow(undefined) + }, [setTxFlow]) return ( { )} +
- - - {txLink && ( - + + + )} + +
) diff --git a/src/components/tx/SignOrExecuteForm/ExecuteForm.tsx b/src/components/tx/SignOrExecuteForm/ExecuteForm.tsx index 51b48fa60d..e5acd6fab4 100644 --- a/src/components/tx/SignOrExecuteForm/ExecuteForm.tsx +++ b/src/components/tx/SignOrExecuteForm/ExecuteForm.tsx @@ -154,7 +154,7 @@ const ExecuteForm = ({ {(isOk) => ( )} diff --git a/src/components/tx/SignOrExecuteForm/SignForm.tsx b/src/components/tx/SignOrExecuteForm/SignForm.tsx index c3d39beb04..2f9c64eb7c 100644 --- a/src/components/tx/SignOrExecuteForm/SignForm.tsx +++ b/src/components/tx/SignOrExecuteForm/SignForm.tsx @@ -5,7 +5,7 @@ import ErrorMessage from '@/components/tx/ErrorMessage' import { logError, Errors } from '@/services/exceptions' import useIsSafeOwner from '@/hooks/useIsSafeOwner' import CheckWallet from '@/components/common/CheckWallet' -import { useTxActions } from './hooks' +import { useAlreadySigned, useTxActions } from './hooks' import type { SignOrExecuteProps } from '.' import type { SafeTransaction } from '@safe-global/safe-core-sdk-types' import { TxModalContext } from '@/components/tx-flow' @@ -32,6 +32,7 @@ const SignForm = ({ const { signTx } = useTxActions() const { setTxFlow } = useContext(TxModalContext) const { needsRiskConfirmation, isRiskConfirmed, setIsRiskIgnored } = useContext(TxSecurityContext) + const hasSigned = useAlreadySigned(safeTx) // On modal submit const handleSubmit = async (e: SyntheticEvent) => { @@ -64,6 +65,8 @@ const SignForm = ({ return (
+ {hasSigned && You have already signed this transaction.} + {cannotPropose ? ( ) : ( @@ -79,7 +82,7 @@ const SignForm = ({ {(isOk) => ( )} diff --git a/src/components/tx/SignOrExecuteForm/hooks.test.ts b/src/components/tx/SignOrExecuteForm/hooks.test.ts index d40e5c1861..3cb72d20e8 100644 --- a/src/components/tx/SignOrExecuteForm/hooks.test.ts +++ b/src/components/tx/SignOrExecuteForm/hooks.test.ts @@ -10,7 +10,7 @@ import * as pending from '@/hooks/usePendingTxs' import * as txSender from '@/services/tx/tx-sender/dispatch' import * as onboardHooks from '@/hooks/wallets/useOnboard' import { type OnboardAPI } from '@web3-onboard/core' -import { useImmediatelyExecutable, useIsExecutionLoop, useTxActions, useValidateNonce } from './hooks' +import { useAlreadySigned, useImmediatelyExecutable, useIsExecutionLoop, useTxActions, useValidateNonce } from './hooks' const createSafeTx = (data = '0x'): SafeTransaction => { return { @@ -542,4 +542,43 @@ describe('SignOrExecute hooks', () => { expect(relaySpy).not.toHaveBeenCalled() }) }) + + describe('useAlreadySigned', () => { + it('should return true if wallet already signed a tx', () => { + // Wallet + jest.spyOn(wallet, 'default').mockReturnValue({ + chainId: '1', + label: 'MetaMask', + address: '0x1234567890000000000000000000000000000000', + } as unknown as ConnectedWallet) + + const tx = createSafeTx() + tx.addSignature({ + signer: '0x1234567890000000000000000000000000000000', + data: '0x0001', + staticPart: () => '', + dynamicPart: () => '', + }) + const { result } = renderHook(() => useAlreadySigned(tx)) + expect(result.current).toEqual(true) + }) + }) + it('should return false if wallet has not signed a tx yet', () => { + // Wallet + jest.spyOn(wallet, 'default').mockReturnValue({ + chainId: '1', + label: 'MetaMask', + address: '0x1234567890000000000000000000000000000000', + } as unknown as ConnectedWallet) + + const tx = createSafeTx() + tx.addSignature({ + signer: '0x00000000000000000000000000000000000000000', + data: '0x0001', + staticPart: () => '', + dynamicPart: () => '', + }) + const { result } = renderHook(() => useAlreadySigned(tx)) + expect(result.current).toEqual(false) + }) }) diff --git a/src/components/tx/SignOrExecuteForm/hooks.ts b/src/components/tx/SignOrExecuteForm/hooks.ts index 09a1d5bfc2..17492530a3 100644 --- a/src/components/tx/SignOrExecuteForm/hooks.ts +++ b/src/components/tx/SignOrExecuteForm/hooks.ts @@ -179,3 +179,10 @@ export const useSafeTxGas = (safeTx: SafeTransaction | undefined): number | unde return safeTxGas } + +export const useAlreadySigned = (safeTx: SafeTransaction | undefined): boolean => { + const wallet = useWallet() + const hasSigned = + safeTx && wallet && (safeTx.signatures.has(wallet.address.toLowerCase()) || safeTx.signatures.has(wallet.address)) + return Boolean(hasSigned) +} diff --git a/src/components/tx/security/SecurityWarnings.tsx b/src/components/tx/security/SecurityWarnings.tsx new file mode 100644 index 0000000000..f78c9fde22 --- /dev/null +++ b/src/components/tx/security/SecurityWarnings.tsx @@ -0,0 +1,11 @@ +import { RedefineMessage } from './redefine' +import { TxSimulationMessage } from './tenderly' + +const SecurityWarnings = () => ( + <> + + + +) + +export default SecurityWarnings diff --git a/src/hooks/useTxNotifications.ts b/src/hooks/useTxNotifications.ts index 20411402ed..cbe777b785 100644 --- a/src/hooks/useTxNotifications.ts +++ b/src/hooks/useTxNotifications.ts @@ -43,7 +43,11 @@ enum Variant { const successEvents = [TxEvent.PROPOSED, TxEvent.SIGNATURE_PROPOSED, TxEvent.ONCHAIN_SIGNATURE_SUCCESS, TxEvent.SUCCESS] -const getTxLink = (txId: string, chain: ChainInfo, safeAddress: string): { href: LinkProps['href']; title: string } => { +export const getTxLink = ( + txId: string, + chain: ChainInfo, + safeAddress: string, +): { href: LinkProps['href']; title: string } => { return { href: { pathname: AppRoutes.transactions.tx, @@ -53,14 +57,6 @@ const getTxLink = (txId: string, chain: ChainInfo, safeAddress: string): { href: } } -const getTxExplorerLink = (txHash: string, chain: ChainInfo): { href: LinkProps['href']; title: string } => { - const { href } = getExplorerLink(txHash, chain.blockExplorerUriTemplate) - return { - href, - title: 'View on explorer', - } -} - const useTxNotifications = (): void => { const dispatch = useAppDispatch() const chain = useCurrentChain() @@ -90,7 +86,11 @@ const useTxNotifications = (): void => { detailedMessage: isError ? detail.error.message : undefined, groupKey, variant: isError ? Variant.ERROR : isSuccess ? Variant.SUCCESS : Variant.INFO, - link: txId ? getTxLink(txId, chain, safeAddress) : txHash ? getTxExplorerLink(txHash, chain) : undefined, + link: txId + ? getTxLink(txId, chain, safeAddress) + : txHash + ? getExplorerLink(txHash, chain.blockExplorerUriTemplate) + : undefined, }), ) }),