Repository: safe-global/safe-wallet-monorepo
Pull request #5438: feat(Signing UX): always show hashes on receipt
Author: login: iamacook
Created: 2025-03-20T13:04:59Z | Merged: 2025-03-21T14:38:09Z

## What it solves

Resolves #5417

## How this PR fixes it

There is no common display of hashes (domain, message, Safe) in wallets and our receipt screen currently shows them depending on whether a hardware wallet is connect. We should instead always show them.

This adjusts the receipt screen to include a collapsable section, displaying the hashes when opened. It is opened by default if a hardware wallet is connected, however.

## How to test it

With the receipt screen open, observe the new section that can be opened/closed on click. If a hardware wallet is connected, it should be open by default.

## Screenshots

![image](https://github.com/user-attachments/assets/cd8ea540-47ee-44b7-80e0-354d06695415)

![image](https://github.com/user-attachments/assets/ce6ffb16-9df8-483e-8801-7230d355a2aa)

## Checklist

- [ ] I've tested the branch on mobile 📱
- [ ] I've documented how it affects the analytics (if at all) 📊
- [ ] I've written a unit/e2e test for it (if applicable) 🧑‍💻


Linked issue #5417: [Signing UX v2] Show tx hashes for all wallets
For hardware wallets, this block should be expanded by default.

![Image](https://github.com/user-attachments/assets/94a2e628-7e72-4c63-99f7-3d6af3857756)

![Image](https://github.com/user-attachments/assets/11b0873c-b7cf-4d5f-976f-5c12e7bef949)
Linked issue comments: user: login: iamacook
created_at: 2025-03-20T10:19:06Z
body: For reference, Ledger Live (connected via WalletConnect) shows the message hash:

![Image](https://github.com/user-attachments/assets/dc8b7607-160e-4768-85a5-03e563d7643e)

Edit: via MetaMask, only hashes are shown on the Ledger device.
author_association: CONTRIBUTOR

Diff:
diff --git a/apps/web/src/components/transactions/HexEncodedData/styles.module.css b/apps/web/src/components/transactions/HexEncodedData/styles.module.css
index 3f7fe4ef7d1..fe1e820d84d 100644
--- a/apps/web/src/components/transactions/HexEncodedData/styles.module.css
+++ b/apps/web/src/components/transactions/HexEncodedData/styles.module.css
@@ -7,4 +7,5 @@
 .encodedData button {
   padding-top: 0;
   padding-bottom: 0;
+  padding-left: 0;
 }
diff --git a/apps/web/src/components/transactions/TxDetails/Summary/SafeTxHashDataRow/index.tsx b/apps/web/src/components/transactions/TxDetails/Summary/SafeTxHashDataRow/index.tsx
index 2929b8147ba..baf1c7141da 100644
--- a/apps/web/src/components/transactions/TxDetails/Summary/SafeTxHashDataRow/index.tsx
+++ b/apps/web/src/components/transactions/TxDetails/Summary/SafeTxHashDataRow/index.tsx
@@ -4,7 +4,6 @@ import { TxDataRow, generateDataRowValue } from '../TxDataRow'
 import { type SafeTransactionData, type SafeVersion } from '@safe-global/safe-core-sdk-types'
 import { calculateSafeTransactionHash } from '@safe-global/protocol-kit/dist/src/utils'
 import useSafeInfo from '@/hooks/useSafeInfo'
-import useChainId from '@/hooks/useChainId'
 import { getDomainHash, getSafeTxMessageHash } from '@/utils/safe-hashes'
 
 export const SafeTxHashDataRow = ({
@@ -14,40 +13,14 @@ export const SafeTxHashDataRow = ({
   safeTxData: SafeTransactionData
   safeTxHash?: string
 }) => {
-  const { safe, safeAddress } = useSafeInfo()
-  const chainId = useChainId()
-  const safeVersion = safe.version as SafeVersion
-
-  const computedSafeTxHash = useMemo(() => {
-    if (safeTxHash) return safeTxHash
-    if (!safe.version) return
-    try {
-      return calculateSafeTransactionHash(safeAddress, safeTxData, safe.version, BigInt(safe.chainId))
-    } catch {
-      return
-    }
-  }, [safe.chainId, safe.version, safeAddress, safeTxData, safeTxHash])
-
-  const domainHash = useMemo(() => {
-    try {
-      return getDomainHash({ chainId, safeAddress, safeVersion })
-    } catch {
-      return ''
-    }
-  }, [chainId, safeAddress, safeVersion])
-
-  const messageHash = useMemo(() => {
-    try {
-      return getSafeTxMessageHash({ safeVersion, safeTxData })
-    } catch {
-      return ''
-    }
-  }, [safeTxData, safeVersion])
+  const domainHash = useDomainHash()
+  const messageHash = useMessageHash({ safeTxData })
+  const computedSafeTxHash = useSafeTxHash({ safeTxData, safeTxHash })
 
   return (
     <Stack gap={1}>
       <TxDataRow datatestid="tx-domain-hash" title="Domain hash:">
-        {generateDataRowValue(domainHash, 'rawData')}
+        {generateDataRowValue(domainHash ?? '', 'rawData')}
       </TxDataRow>
       {messageHash && (
         <TxDataRow datatestid="tx-message-hash" title="Message hash:">
@@ -55,8 +28,62 @@ export const SafeTxHashDataRow = ({
         </TxDataRow>
       )}
       <TxDataRow datatestid="tx-safe-hash" title="safeTxHash:">
-        {generateDataRowValue(computedSafeTxHash, 'rawData')}
+        {generateDataRowValue(computedSafeTxHash ?? '', 'rawData')}
       </TxDataRow>
     </Stack>
   )
 }
+
+export function useDomainHash(): string | null {
+  const { safe, safeAddress } = useSafeInfo()
+
+  return useMemo(() => {
+    if (!safe.version) {
+      return null
+    }
+    try {
+      return getDomainHash({ chainId: safe.chainId, safeAddress, safeVersion: safe.version as SafeVersion })
+    } catch {
+      return null
+    }
+  }, [safe.chainId, safe.version, safeAddress])
+}
+
+export function useMessageHash({ safeTxData }: { safeTxData: SafeTransactionData }): string | null {
+  const { safe } = useSafeInfo()
+
+  return useMemo(() => {
+    if (!safe.version) {
+      return null
+    }
+    try {
+      return getSafeTxMessageHash({ safeVersion: safe.version as SafeVersion, safeTxData })
+    } catch {
+      return null
+    }
+  }, [safe.version, safeTxData])
+}
+
+export function useSafeTxHash({
+  safeTxData,
+  safeTxHash,
+}: {
+  safeTxData: SafeTransactionData
+  safeTxHash?: string
+}): string | null {
+  const { safe, safeAddress } = useSafeInfo()
+
+  return useMemo(() => {
+    if (safeTxHash) {
+      return safeTxHash
+    }
+    if (!safe.version) {
+      return null
+    }
+    try {
+      return calculateSafeTransactionHash(safeAddress, safeTxData, safe.version, BigInt(safe.chainId))
+    } catch {
+      return null
+    }
+  }, [safeTxData, safe.chainId, safe.version, safeAddress, safeTxHash])
+}
diff --git a/apps/web/src/components/tx-flow/common/TxLayout/styles.module.css b/apps/web/src/components/tx-flow/common/TxLayout/styles.module.css
index 5344a505cbf..2a9f267ffcc 100644
--- a/apps/web/src/components/tx-flow/common/TxLayout/styles.module.css
+++ b/apps/web/src/components/tx-flow/common/TxLayout/styles.module.css
@@ -11,8 +11,7 @@
   display: flex;
   justify-content: space-between;
   align-items: center;
-  /* Remove height of progress bar */
-  padding: calc(var(--space-3) - 6px) var(--space-3) var(--space-3);
+  padding: var(--space-3);
   border-bottom: 1px solid var(--color-border-light);
 }
 
@@ -108,15 +107,18 @@
   /* Height of transaction type title */
   margin-top: 46px;
 }
+
 @media (max-width: 1199px) {
   .backButton {
     left: 50%;
     transform: translateX(-50%);
   }
+
   .step :global(.MuiCardActions-root) {
     margin-bottom: var(--space-8);
   }
 }
+
 @media (max-width: 899.95px) {
   .widget {
     position: absolute;
diff --git a/apps/web/src/components/tx/ConfirmTxDetails/TxDetails.tsx b/apps/web/src/components/tx/ConfirmTxDetails/TxDetails.tsx
index 4b3af3df03a..d9a0ec5018e 100644
--- a/apps/web/src/components/tx/ConfirmTxDetails/TxDetails.tsx
+++ b/apps/web/src/components/tx/ConfirmTxDetails/TxDetails.tsx
@@ -1,15 +1,20 @@
 import type { StackProps } from '@mui/material'
-import { Box, Chip, Divider, Stack, Typography } from '@mui/material'
+import { Box, Button, Chip, Divider, Stack, Typography } from '@mui/material'
+import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
 import type { SafeTransaction } from '@safe-global/safe-core-sdk-types'
 import { PaperViewToggle } from '../../common/PaperViewToggle'
 import TableRowsRoundedIcon from '@mui/icons-material/TableRowsRounded'
 import DataObjectIcon from '@mui/icons-material/DataObject'
 import EthHashInfo from '@/components/common/EthHashInfo'
-import type { ReactElement, ReactNode } from 'react'
+import { useState, type ReactElement, type ReactNode } from 'react'
 import { isNumber, isString } from 'lodash'
 import { Operation, type TransactionData } from '@safe-global/safe-gateway-typescript-sdk/dist/types/transactions'
 import { HexEncodedData } from '@/components/transactions/HexEncodedData'
-import { SafeTxHashDataRow } from '@/components/transactions/TxDetails/Summary/SafeTxHashDataRow'
+import {
+  useDomainHash,
+  useMessageHash,
+  useSafeTxHash,
+} from '@/components/transactions/TxDetails/Summary/SafeTxHashDataRow'
 
 type TxDetailsProps = {
   safeTx: SafeTransaction
@@ -40,15 +45,20 @@ const TxDetailsRow = ({
   </Stack>
 )
 
+const ContentWrapper = ({ children }: { children: ReactElement | ReactElement[] }) => (
+  <Box sx={{ maxHeight: '550px', overflowY: 'auto', px: 2 }}>{children}</Box>
+)
+
 export const TxDetails = ({ safeTx, txData, showHashes }: TxDetailsProps) => {
+  const [expandHashes, setExpandHashes] = useState(showHashes)
+  const safeTxHash = useSafeTxHash({ safeTxData: safeTx.data })
+  const domainHash = useDomainHash()
+  const messageHash = useMessageHash({ safeTxData: safeTx.data })
+
   const toInfo = txData?.addressInfoIndex?.[safeTx.data.to] || txData?.to
   const toName = toInfo?.name || (toInfo && 'displayName' in toInfo ? String(toInfo.displayName || '') : undefined)
   const toLogo = toInfo?.logoUri
 
-  const ContentWrapper = ({ children }: { children: ReactElement | ReactElement[] }) => (
-    <Box sx={{ maxHeight: '550px', overflowY: 'auto', px: 2 }}>{children}</Box>
-  )
-
   return (
     <PaperViewToggle>
       {[
@@ -64,18 +74,6 @@ export const TxDetails = ({ safeTx, txData, showHashes }: TxDetailsProps) => {
               <Divider sx={{ mb: 1 }} />
 
               <Stack spacing={1} divider={<Divider />}>
-                {showHashes && (
-                  <Typography
-                    variant="body2"
-                    sx={{
-                      '& p': { color: 'text.secondary', fontSize: '14px' },
-                      '& div': { flexWrap: 'wrap', width: '100%' },
-                    }}
-                  >
-                    <SafeTxHashDataRow safeTxData={safeTx.data} />
-                  </Typography>
-                )}
-
                 <TxDetailsRow label="To">
                   {toName || toLogo ? (
                     <Chip
@@ -157,6 +155,44 @@ export const TxDetails = ({ safeTx, txData, showHashes }: TxDetailsProps) => {
                 </TxDetailsRow>
 
                 <TxDetailsRow label="Nonce">{safeTx.data.nonce}</TxDetailsRow>
+
+                <Button onClick={() => setExpandHashes(!expandHashes)} sx={{ all: 'unset' }}>
+                  <Typography
+                    variant="body2"
+                    fontWeight={700}
+                    display="inline-flex"
+                    alignItems="center"
+                    color="primary.light"
+                    sx={{ cursor: 'pointer' }}
+                  >
+                    Transaction hashes{' '}
+                    <ExpandMoreIcon sx={expandHashes ? { transform: 'rotate(180deg)' } : undefined} />
+                  </Typography>
+                </Button>
+
+                {expandHashes && domainHash && (
+                  <TxDetailsRow label="Domain hash">
+                    <Typography variant="body2" width="100%" sx={{ wordWrap: 'break-word' }}>
+                      <HexEncodedData hexData={domainHash} limit={66} highlightFirstBytes={false} />
+                    </Typography>
+                  </TxDetailsRow>
+                )}
+
+                {expandHashes && messageHash && (
+                  <TxDetailsRow label="Message hash">
+                    <Typography variant="body2" width="100%" sx={{ wordWrap: 'break-word' }}>
+                      <HexEncodedData hexData={messageHash} limit={66} highlightFirstBytes={false} />
+                    </Typography>
+                  </TxDetailsRow>
+                )}
+
+                {expandHashes && safeTxHash && (
+                  <TxDetailsRow label="safeTxHash">
+                    <Typography variant="body2" width="100%" sx={{ wordWrap: 'break-word' }}>
+                      <HexEncodedData hexData={safeTxHash} limit={66} highlightFirstBytes={false} />
+                    </Typography>
+                  </TxDetailsRow>
+                )}
               </Stack>
             </ContentWrapper>
           ),
diff --git a/apps/web/src/components/tx/ConfirmTxDetails/index.tsx b/apps/web/src/components/tx/ConfirmTxDetails/index.tsx
index e244e36d986..2eb30398214 100644
--- a/apps/web/src/components/tx/ConfirmTxDetails/index.tsx
+++ b/apps/web/src/components/tx/ConfirmTxDetails/index.tsx
@@ -106,9 +106,9 @@ export const ConfirmTxDetails = (props: SignOrExecuteProps) => {
       <Divider className={commonCss.nestedDivider} sx={{ pt: 3 }} />
 
       <FormControlLabel
-        sx={{ mt: 2 }}
+        sx={{ mt: 2, mb: -1.3 }}
         control={<Checkbox checked={checked} onChange={handleCheckboxChange} />}
-        label="I understand what I'm signing and acknowledge that this is an irreversible action."
+        label="I understand what I'm signing and that this is an irreversible action."
       />
 
       <SignOrExecuteFormV2
diff --git a/apps/web/src/components/tx/ReviewTransaction/__tests__/__snapshots__/index.test.tsx.snap b/apps/web/src/components/tx/ReviewTransaction/__tests__/__snapshots__/index.test.tsx.snap
index 4001f1117cf..55ba404efb5 100644
--- a/apps/web/src/components/tx/ReviewTransaction/__tests__/__snapshots__/index.test.tsx.snap
+++ b/apps/web/src/components/tx/ReviewTransaction/__tests__/__snapshots__/index.test.tsx.snap
@@ -337,6 +337,52 @@ exports[`ReviewTransaction should display a confirmation screen 1`] = `
                               </div>
                             </div>
                           </div>
+                          <div
+                            class="MuiGrid-root MuiGrid-container css-1m1clne-MuiGrid-root"
+                          >
+                            <div
+                              class="MuiGrid-root MuiGrid-item css-p1l0hx-MuiGrid-root"
+                              data-testid="tx-row-title"
+                              style="word-break: break-word;"
+                            >
+                              <p
+                                class="MuiTypography-root MuiTypography-body1 css-shf88x-MuiTypography-root"
+                              >
+                                safeTxHash:
+                              </p>
+                            </div>
+                            <div
+                              class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-true css-1vd824g-MuiGrid-root"
+                              data-testid="tx-data-row"
+                            >
+                              <div
+                                class="encodedData MuiBox-root css-0"
+                                data-testid="tx-hexData"
+                              >
+                                <span
+                                  aria-label="Copy to clipboard"
+                                  class=""
+                                  data-mui-internal-clone-element="true"
+                                  style="cursor: pointer;"
+                                >
+                                  <button
+                                    aria-label="Copy to clipboard"
+                                    class="MuiButtonBase-root MuiIconButton-root MuiIconButton-sizeSmall css-xyrzjn-MuiButtonBase-root-MuiIconButton-root"
+                                    tabindex="0"
+                                    type="button"
+                                  >
+                                    <mock-icon
+                                      aria-hidden=""
+                                      class="MuiSvgIcon-root MuiSvgIcon-colorBorder MuiSvgIcon-fontSizeSmall css-gvpe62-MuiSvgIcon-root"
+                                      data-testid="copy-btn-icon"
+                                      focusable="false"
+                                    />
+                                  </button>
+                                </span>
+                                 
+                              </div>
+                            </div>
+                          </div>
                         </div>
                       </div>
                     </div>
diff --git a/apps/web/src/components/tx/SignOrExecuteForm/__tests__/__snapshots__/SignOrExecute.test.tsx.snap b/apps/web/src/components/tx/SignOrExecuteForm/__tests__/__snapshots__/SignOrExecute.test.tsx.snap
index 9e027e01c50..9c2d9ec64e9 100644
--- a/apps/web/src/components/tx/SignOrExecuteForm/__tests__/__snapshots__/SignOrExecute.test.tsx.snap
+++ b/apps/web/src/components/tx/SignOrExecuteForm/__tests__/__snapshots__/SignOrExecute.test.tsx.snap
@@ -337,6 +337,52 @@ exports[`SignOrExecute should display a confirmation screen 1`] = `
                               </div>
                             </div>
                           </div>
+                          <div
+                            class="MuiGrid-root MuiGrid-container css-1m1clne-MuiGrid-root"
+                          >
+                            <div
+                              class="MuiGrid-root MuiGrid-item css-p1l0hx-MuiGrid-root"
+                              data-testid="tx-row-title"
+                              style="word-break: break-word;"
+                            >
+                              <p
+                                class="MuiTypography-root MuiTypography-body1 css-shf88x-MuiTypography-root"
+                              >
+                                safeTxHash:
+                              </p>
+                            </div>
+                            <div
+                              class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-true css-1vd824g-MuiGrid-root"
+                              data-testid="tx-data-row"
+                            >
+                              <div
+                                class="encodedData MuiBox-root css-0"
+                                data-testid="tx-hexData"
+                              >
+                                <span
+                                  aria-label="Copy to clipboard"
+                                  class=""
+                                  data-mui-internal-clone-element="true"
+                                  style="cursor: pointer;"
+                                >
+                                  <button
+                                    aria-label="Copy to clipboard"
+                                    class="MuiButtonBase-root MuiIconButton-root MuiIconButton-sizeSmall css-xyrzjn-MuiButtonBase-root-MuiIconButton-root"
+                                    tabindex="0"
+                                    type="button"
+                                  >
+                                    <mock-icon
+                                      aria-hidden=""
+                                      class="MuiSvgIcon-root MuiSvgIcon-colorBorder MuiSvgIcon-fontSizeSmall css-gvpe62-MuiSvgIcon-root"
+                                      data-testid="copy-btn-icon"
+                                      focusable="false"
+                                    />
+                                  </button>
+                                </span>
+                                 
+                              </div>
+                            </div>
+                          </div>
                         </div>
                       </div>
                     </div>


Reviews:
user: login: katspaugh
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T13:20:23Z
user: login: katspaugh
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T13:22:47Z
user: login: katspaugh
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T13:23:21Z
user: login: katspaugh
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T13:23:53Z
user: login: katspaugh
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T13:30:13Z
user: login: iamacook
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T13:55:57Z
user: login: iamacook
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T13:56:23Z
user: login: iamacook
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T13:56:28Z
user: login: iamacook
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T13:56:39Z
user: login: iamacook
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T13:56:45Z
user: login: katspaugh
body: As per the ticket, we should show expanded hashes on top of the block for hardware wallets. For all other wallets, at the bottom.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T14:09:16Z
user: login: katspaugh
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T14:10:22Z
user: login: iamacook
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T14:19:49Z
user: login: iamacook
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T14:19:51Z
user: login: iamacook
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T14:19:53Z
user: login: katspaugh
body: Thank you!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-21T06:04:37Z
user: login: katspaugh
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T06:05:17Z

Review comments:
diff_hunk: @@ -185,3 +195,27 @@ export const TxDetails = ({ safeTx, txData, showHashes }: TxDetailsProps) => {
     </PaperViewToggle>
   )
 }
+
+function TxDetailsHashes({ safeTx }: Pick<TxDetailsProps, 'safeTx'>) {
+  const safeTxHash = useSafeTxHash({ safeTxData: safeTx.data })
+  const domainHash = useDomainHash()
+  const messageHash = useMessageHash({ safeTxData: safeTx.data })
+
+  return (
+    <>
+      {[
+        ['Domain hash', domainHash] as const,
+        ['Message hash', messageHash] as const,
+        ['safeTxHash', safeTxHash] as const,
+      ].map(([label, hash]) => {
+        return (
+          <TxDetailsRow label={label} key={hash}>
+            <Typography variant="body2" width="100%" sx={{ wordWrap: 'break-word' }}>
+              {hash}
+            </Typography>
path: apps/web/src/components/tx/ConfirmTxDetails/TxDetails.tsx
user: login: katspaugh
body: I would keep the HexEncodedData component here.
created_at: 2025-03-20T13:20:23Z
author_association: MEMBER
diff_hunk: @@ -40,15 +45,17 @@ const TxDetailsRow = ({
   </Stack>
 )
 
-export const TxDetails = ({ safeTx, txData, showHashes }: TxDetailsProps) => {
+const ContentWrapper = ({ children }: { children: ReactElement | ReactElement[] }) => (
+  <Box sx={{ maxHeight: '750px', overflowY: 'auto', px: 2 }}>{children}</Box>
path: apps/web/src/components/tx/ConfirmTxDetails/TxDetails.tsx
user: login: katspaugh
body: It's way too tall now. I suggest we keep the hashes collapsed by default except for hardware wallets, and restoring 550px.
created_at: 2025-03-20T13:22:46Z
author_association: MEMBER
diff_hunk: @@ -157,6 +152,21 @@ export const TxDetails = ({ safeTx, txData, showHashes }: TxDetailsProps) => {
                 </TxDetailsRow>
 
                 <TxDetailsRow label="Nonce">{safeTx.data.nonce}</TxDetailsRow>
+
+                <Button onClick={() => setShowHashes(!showHashes)} sx={{ all: 'unset' }}>
+                  <Typography
+                    variant="body2"
+                    fontWeight={700}
+                    display="inline-flex"
+                    alignItems="center"
+                    color="primary.light"
path: apps/web/src/components/tx/ConfirmTxDetails/TxDetails.tsx
user: login: katspaugh
body: cursor=pointer pls
created_at: 2025-03-20T13:23:21Z
author_association: MEMBER
diff_hunk: @@ -185,3 +195,27 @@ export const TxDetails = ({ safeTx, txData, showHashes }: TxDetailsProps) => {
     </PaperViewToggle>
   )
 }
+
+function TxDetailsHashes({ safeTx }: Pick<TxDetailsProps, 'safeTx'>) {
+  const safeTxHash = useSafeTxHash({ safeTxData: safeTx.data })
+  const domainHash = useDomainHash()
+  const messageHash = useMessageHash({ safeTxData: safeTx.data })
+
+  return (
+    <>
+      {[
+        ['Domain hash', domainHash] as const,
+        ['Message hash', messageHash] as const,
+        ['safeTxHash', safeTxHash] as const,
+      ].map(([label, hash]) => {
+        return (
+          <TxDetailsRow label={label} key={hash}>
+            <Typography variant="body2" width="100%" sx={{ wordWrap: 'break-word' }}>
path: apps/web/src/components/tx/ConfirmTxDetails/TxDetails.tsx
user: login: katspaugh
body: `mb={-0.5}`
created_at: 2025-03-20T13:23:53Z
author_association: MEMBER
diff_hunk: @@ -157,6 +152,21 @@ export const TxDetails = ({ safeTx, txData, showHashes }: TxDetailsProps) => {
                 </TxDetailsRow>
 
                 <TxDetailsRow label="Nonce">{safeTx.data.nonce}</TxDetailsRow>
+
+                <Button onClick={() => setShowHashes(!showHashes)} sx={{ all: 'unset' }}>
+                  <Typography
+                    variant="body2"
+                    fontWeight={700}
+                    display="inline-flex"
+                    alignItems="center"
+                    color="primary.light"
+                  >
+                    Show transaction hashes{' '}
path: apps/web/src/components/tx/ConfirmTxDetails/TxDetails.tsx
user: login: katspaugh
body: ```suggestion
                    Transaction hashes{' '}
```

Makes more sense in the noun form.
created_at: 2025-03-20T13:30:13Z
author_association: MEMBER
diff_hunk: @@ -185,3 +195,27 @@ export const TxDetails = ({ safeTx, txData, showHashes }: TxDetailsProps) => {
     </PaperViewToggle>
   )
 }
+
+function TxDetailsHashes({ safeTx }: Pick<TxDetailsProps, 'safeTx'>) {
+  const safeTxHash = useSafeTxHash({ safeTxData: safeTx.data })
+  const domainHash = useDomainHash()
+  const messageHash = useMessageHash({ safeTxData: safeTx.data })
+
+  return (
+    <>
+      {[
+        ['Domain hash', domainHash] as const,
+        ['Message hash', messageHash] as const,
+        ['safeTxHash', safeTxHash] as const,
+      ].map(([label, hash]) => {
+        return (
+          <TxDetailsRow label={label} key={hash}>
+            <Typography variant="body2" width="100%" sx={{ wordWrap: 'break-word' }}>
+              {hash}
+            </Typography>
path: apps/web/src/components/tx/ConfirmTxDetails/TxDetails.tsx
user: login: iamacook
body: Added in 8088f81565c50bbda19222d8b920bd696b365acd.
created_at: 2025-03-20T13:55:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,15 +45,17 @@ const TxDetailsRow = ({
   </Stack>
 )
 
-export const TxDetails = ({ safeTx, txData, showHashes }: TxDetailsProps) => {
+const ContentWrapper = ({ children }: { children: ReactElement | ReactElement[] }) => (
+  <Box sx={{ maxHeight: '750px', overflowY: 'auto', px: 2 }}>{children}</Box>
path: apps/web/src/components/tx/ConfirmTxDetails/TxDetails.tsx
user: login: iamacook
body: I see your point, but I think showing all the data with the hashes provides a clearer view for verification (see below). Nonetheless, I changed it in 8088f81565c50bbda19222d8b920bd696b365acd.

![image](https://github.com/user-attachments/assets/3693f67e-5ca1-41d1-844c-8d3e54020a9f)
created_at: 2025-03-20T13:56:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -157,6 +152,21 @@ export const TxDetails = ({ safeTx, txData, showHashes }: TxDetailsProps) => {
                 </TxDetailsRow>
 
                 <TxDetailsRow label="Nonce">{safeTx.data.nonce}</TxDetailsRow>
+
+                <Button onClick={() => setShowHashes(!showHashes)} sx={{ all: 'unset' }}>
+                  <Typography
+                    variant="body2"
+                    fontWeight={700}
+                    display="inline-flex"
+                    alignItems="center"
+                    color="primary.light"
path: apps/web/src/components/tx/ConfirmTxDetails/TxDetails.tsx
user: login: iamacook
body: Added in 8088f81565c50bbda19222d8b920bd696b365acd.
created_at: 2025-03-20T13:56:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -185,3 +195,27 @@ export const TxDetails = ({ safeTx, txData, showHashes }: TxDetailsProps) => {
     </PaperViewToggle>
   )
 }
+
+function TxDetailsHashes({ safeTx }: Pick<TxDetailsProps, 'safeTx'>) {
+  const safeTxHash = useSafeTxHash({ safeTxData: safeTx.data })
+  const domainHash = useDomainHash()
+  const messageHash = useMessageHash({ safeTxData: safeTx.data })
+
+  return (
+    <>
+      {[
+        ['Domain hash', domainHash] as const,
+        ['Message hash', messageHash] as const,
+        ['safeTxHash', safeTxHash] as const,
+      ].map(([label, hash]) => {
+        return (
+          <TxDetailsRow label={label} key={hash}>
+            <Typography variant="body2" width="100%" sx={{ wordWrap: 'break-word' }}>
path: apps/web/src/components/tx/ConfirmTxDetails/TxDetails.tsx
user: login: iamacook
body: Looking back at the design, my initial commit was incorrect. I adjusted the implementation slightly to include `Divider`s between each hash in 8088f81565c50bbda19222d8b920bd696b365acd. What do you think of the following (without a margin adjustment)?

![image](https://github.com/user-attachments/assets/9a8dcd94-23f2-4f86-93c0-61dfd6117072)
created_at: 2025-03-20T13:56:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -157,6 +152,21 @@ export const TxDetails = ({ safeTx, txData, showHashes }: TxDetailsProps) => {
                 </TxDetailsRow>
 
                 <TxDetailsRow label="Nonce">{safeTx.data.nonce}</TxDetailsRow>
+
+                <Button onClick={() => setShowHashes(!showHashes)} sx={{ all: 'unset' }}>
+                  <Typography
+                    variant="body2"
+                    fontWeight={700}
+                    display="inline-flex"
+                    alignItems="center"
+                    color="primary.light"
+                  >
+                    Show transaction hashes{' '}
path: apps/web/src/components/tx/ConfirmTxDetails/TxDetails.tsx
user: login: iamacook
body: Changed in 8088f81565c50bbda19222d8b920bd696b365acd.
created_at: 2025-03-20T13:56:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,15 +45,20 @@ const TxDetailsRow = ({
   </Stack>
 )
 
-export const TxDetails = ({ safeTx, txData, showHashes }: TxDetailsProps) => {
+const ContentWrapper = ({ children }: { children: ReactElement | ReactElement[] }) => (
+  <Box sx={{ maxHeight: '550px', overflowY: 'auto', px: 2 }}>{children}</Box>
+)
+
+export const TxDetails = ({ safeTx, txData, showHashes: _showHashes }: TxDetailsProps) => {
+  const [showHashes, setShowHashes] = useState(_showHashes)
path: apps/web/src/components/tx/ConfirmTxDetails/TxDetails.tsx
user: login: katspaugh
body: ```suggestion
  const [expandHashes, setExpandHashes] = useState(showHashes)
```

Clearer + you don't need to rename the prop then.
created_at: 2025-03-20T13:58:54Z
author_association: MEMBER
diff_hunk: @@ -157,6 +155,43 @@ export const TxDetails = ({ safeTx, txData, showHashes }: TxDetailsProps) => {
                 </TxDetailsRow>
 
                 <TxDetailsRow label="Nonce">{safeTx.data.nonce}</TxDetailsRow>
+
+                <Button onClick={() => setShowHashes(!showHashes)} sx={{ all: 'unset' }}>
+                  <Typography
+                    variant="body2"
+                    fontWeight={700}
+                    display="inline-flex"
+                    alignItems="center"
+                    color="primary.light"
+                    sx={{ cursor: 'pointer' }}
+                  >
+                    Transaction hashes <ExpandMoreIcon sx={{ transform: !showHashes ? 'rotate(180deg)' : undefined }} />
+                  </Typography>
+                </Button>
+
+                {showHashes && domainHash && (
+                  <TxDetailsRow label="Domain hash">
+                    <Typography variant="body2" width="100%" sx={{ wordWrap: 'break-word' }}>
+                      <HexEncodedData hexData={domainHash} limit={66} highlightFirstBytes={false} />
+                    </Typography>
+                  </TxDetailsRow>
+                )}
+
+                {showHashes && messageHash && (
+                  <TxDetailsRow label="Message hash">
+                    <Typography variant="body2" width="100%" sx={{ wordWrap: 'break-word' }}>
+                      <HexEncodedData hexData={messageHash} limit={66} highlightFirstBytes={false} />
+                    </Typography>
+                  </TxDetailsRow>
+                )}
+
+                {showHashes && safeTxHash && (
+                  <TxDetailsRow label="safeTxHash">
+                    <Typography variant="body2" width="100%" sx={{ wordWrap: 'break-word' }}>
+                      <HexEncodedData hexData={safeTxHash} limit={66} highlightFirstBytes={false} />
path: apps/web/src/components/tx/ConfirmTxDetails/TxDetails.tsx
user: login: katspaugh
body: highlightFirstBytes is false by default.
created_at: 2025-03-20T14:01:12Z
author_association: MEMBER
diff_hunk: @@ -157,6 +155,43 @@ export const TxDetails = ({ safeTx, txData, showHashes }: TxDetailsProps) => {
                 </TxDetailsRow>
 
                 <TxDetailsRow label="Nonce">{safeTx.data.nonce}</TxDetailsRow>
+
+                <Button onClick={() => setShowHashes(!showHashes)} sx={{ all: 'unset' }}>
+                  <Typography
+                    variant="body2"
+                    fontWeight={700}
+                    display="inline-flex"
+                    alignItems="center"
+                    color="primary.light"
+                    sx={{ cursor: 'pointer' }}
+                  >
+                    Transaction hashes <ExpandMoreIcon sx={{ transform: !showHashes ? 'rotate(180deg)' : undefined }} />
path: apps/web/src/components/tx/ConfirmTxDetails/TxDetails.tsx
user: login: katspaugh
body: ```suggestion
                    Transaction hashes <ExpandMoreIcon sx={showHashes ? { transform: 'rotate(180deg)' } : undefined }} />
```

In the collapsed state, the arrow should face down.
created_at: 2025-03-20T14:06:13Z
author_association: MEMBER
diff_hunk: @@ -185,3 +195,27 @@ export const TxDetails = ({ safeTx, txData, showHashes }: TxDetailsProps) => {
     </PaperViewToggle>
   )
 }
+
+function TxDetailsHashes({ safeTx }: Pick<TxDetailsProps, 'safeTx'>) {
+  const safeTxHash = useSafeTxHash({ safeTxData: safeTx.data })
+  const domainHash = useDomainHash()
+  const messageHash = useMessageHash({ safeTxData: safeTx.data })
+
+  return (
+    <>
+      {[
+        ['Domain hash', domainHash] as const,
+        ['Message hash', messageHash] as const,
+        ['safeTxHash', safeTxHash] as const,
+      ].map(([label, hash]) => {
+        return (
+          <TxDetailsRow label={label} key={hash}>
+            <Typography variant="body2" width="100%" sx={{ wordWrap: 'break-word' }}>
path: apps/web/src/components/tx/ConfirmTxDetails/TxDetails.tsx
user: login: katspaugh
body: Looks good! Could you also add `padding-left: 0` to the copy button in HexEncodedData? (There's already CSS for it).
created_at: 2025-03-20T14:10:22Z
author_association: MEMBER
diff_hunk: @@ -40,15 +45,20 @@ const TxDetailsRow = ({
   </Stack>
 )
 
-export const TxDetails = ({ safeTx, txData, showHashes }: TxDetailsProps) => {
+const ContentWrapper = ({ children }: { children: ReactElement | ReactElement[] }) => (
+  <Box sx={{ maxHeight: '550px', overflowY: 'auto', px: 2 }}>{children}</Box>
+)
+
+export const TxDetails = ({ safeTx, txData, showHashes: _showHashes }: TxDetailsProps) => {
+  const [showHashes, setShowHashes] = useState(_showHashes)
path: apps/web/src/components/tx/ConfirmTxDetails/TxDetails.tsx
user: login: iamacook
body: Changed in 8369eb9d3ff46dae34824fe69e7dd15b4e828fb2.
created_at: 2025-03-20T14:19:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -157,6 +155,43 @@ export const TxDetails = ({ safeTx, txData, showHashes }: TxDetailsProps) => {
                 </TxDetailsRow>
 
                 <TxDetailsRow label="Nonce">{safeTx.data.nonce}</TxDetailsRow>
+
+                <Button onClick={() => setShowHashes(!showHashes)} sx={{ all: 'unset' }}>
+                  <Typography
+                    variant="body2"
+                    fontWeight={700}
+                    display="inline-flex"
+                    alignItems="center"
+                    color="primary.light"
+                    sx={{ cursor: 'pointer' }}
+                  >
+                    Transaction hashes <ExpandMoreIcon sx={{ transform: !showHashes ? 'rotate(180deg)' : undefined }} />
+                  </Typography>
+                </Button>
+
+                {showHashes && domainHash && (
+                  <TxDetailsRow label="Domain hash">
+                    <Typography variant="body2" width="100%" sx={{ wordWrap: 'break-word' }}>
+                      <HexEncodedData hexData={domainHash} limit={66} highlightFirstBytes={false} />
+                    </Typography>
+                  </TxDetailsRow>
+                )}
+
+                {showHashes && messageHash && (
+                  <TxDetailsRow label="Message hash">
+                    <Typography variant="body2" width="100%" sx={{ wordWrap: 'break-word' }}>
+                      <HexEncodedData hexData={messageHash} limit={66} highlightFirstBytes={false} />
+                    </Typography>
+                  </TxDetailsRow>
+                )}
+
+                {showHashes && safeTxHash && (
+                  <TxDetailsRow label="safeTxHash">
+                    <Typography variant="body2" width="100%" sx={{ wordWrap: 'break-word' }}>
+                      <HexEncodedData hexData={safeTxHash} limit={66} highlightFirstBytes={false} />
path: apps/web/src/components/tx/ConfirmTxDetails/TxDetails.tsx
user: login: iamacook
body: It [defaults to `true`](https://github.com/safe-global/safe-wallet-monorepo/blob/dev/apps/web/src/components/transactions/HexEncodedData/index.tsx#L18).
created_at: 2025-03-20T14:19:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -157,6 +155,43 @@ export const TxDetails = ({ safeTx, txData, showHashes }: TxDetailsProps) => {
                 </TxDetailsRow>
 
                 <TxDetailsRow label="Nonce">{safeTx.data.nonce}</TxDetailsRow>
+
+                <Button onClick={() => setShowHashes(!showHashes)} sx={{ all: 'unset' }}>
+                  <Typography
+                    variant="body2"
+                    fontWeight={700}
+                    display="inline-flex"
+                    alignItems="center"
+                    color="primary.light"
+                    sx={{ cursor: 'pointer' }}
+                  >
+                    Transaction hashes <ExpandMoreIcon sx={{ transform: !showHashes ? 'rotate(180deg)' : undefined }} />
path: apps/web/src/components/tx/ConfirmTxDetails/TxDetails.tsx
user: login: iamacook
body: Fixed in 8369eb9d3ff46dae34824fe69e7dd15b4e828fb2.
created_at: 2025-03-20T14:19:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -185,3 +195,27 @@ export const TxDetails = ({ safeTx, txData, showHashes }: TxDetailsProps) => {
     </PaperViewToggle>
   )
 }
+
+function TxDetailsHashes({ safeTx }: Pick<TxDetailsProps, 'safeTx'>) {
+  const safeTxHash = useSafeTxHash({ safeTxData: safeTx.data })
+  const domainHash = useDomainHash()
+  const messageHash = useMessageHash({ safeTxData: safeTx.data })
+
+  return (
+    <>
+      {[
+        ['Domain hash', domainHash] as const,
+        ['Message hash', messageHash] as const,
+        ['safeTxHash', safeTxHash] as const,
+      ].map(([label, hash]) => {
+        return (
+          <TxDetailsRow label={label} key={hash}>
+            <Typography variant="body2" width="100%" sx={{ wordWrap: 'break-word' }}>
path: apps/web/src/components/tx/ConfirmTxDetails/TxDetails.tsx
user: login: katspaugh
body: Nvm, I've removed the padding here: https://github.com/safe-global/safe-wallet-monorepo/pull/5451
created_at: 2025-03-21T06:05:16Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-20T13:05:12Z
body: <!-- add-pr-comment:praul -->

## Branch preview
✅  Deploy successful!

**Website:**
https://feat_receipt_hashes--walletweb.review.5afe.dev/home?safe=eth:0xA77DE01e157f9f57C7c4A326eeE9C4874D0598b6

**Storybook:**
https://feat_receipt_hashes--walletweb.review.5afe.dev/storybook/
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-20T13:12:01Z
body: <!-- jest coverage report action for options with hash 538b3aeca5435921509d9329a38101b7 -->

## Coverage report for `apps/web`


| <div title="Status of coverage:&#10; 🟢 - ok&#10; 🟡 - slightly more than threshold&#10; 🔴 - under the threshold">St.<sup>:grey_question:</sup></div> | Category | Percentage | Covered / Total |
| :-: | :- | :- | :-: |
| 🟡 | Statements | <div title="Base coverage is: 77.34%">77.63% (+0.29% 🔼)</div> | 14527/18713 |
| 🔴 | Branches | <div title="Base coverage is: 56.4%">56.68% (+0.28% 🔼)</div> | 3777/6664 |
| 🟡 | Functions | <div title="Base coverage is: 62.04%">62.68% (+0.64% 🔼)</div> | 2167/3457 |
| 🟡 | Lines | <div title="Base coverage is: 78.75%">79.07% (+0.32% 🔼)</div> | 13124/16598 |


<details><summary>Show files with reduced coverage 🔻</summary>
<br/>

| <div title="Status of coverage:&#10; 🟢 - ok&#10; 🟡 - slightly more than threshold&#10; 🔴 - under the threshold">St.<sup>:grey_question:</sup></div> | File | Statements | Branches | Functions | Lines |
| :-: | :- | :- | :- | :- | :- |
| 🟢 | <div title="src/store/txHistorySlice.ts">`...` / txHistorySlice.ts</div> | <div title="Base coverage is: 93.18%">90.91% (-2.27% 🔻)</div> | <div title="Base coverage is: 72.22%">72.73% (+0.51% 🔼)</div> | 60% | <div title="Base coverage is: 94.87%">92.86% (-2.01% 🔻)</div> |
| 🟡 | <div title="src/store/api/gateway/index.ts">`...` / index.ts</div> | <div title="Base coverage is: 62.22%">64.1% (+1.88% 🔼)</div> | 100% | <div title="Base coverage is: 42.11%">40% (-2.11% 🔻)</div> | <div title="Base coverage is: 65.79%">66.67% (+0.88% 🔼)</div> |
| 🟢 | <div title="src/features/multichain/utils/utils.ts">`...` / utils.ts</div> | <div title="Base coverage is: 90.24%">91.25% (+1.01% 🔼)</div> | 80.65% | <div title="Base coverage is: 90.91%">90.48% (-0.43% 🔻)</div> | <div title="Base coverage is: 95.38%">95.31% (-0.07% 🔻)</div> |
| 🟢 | <div title="src/components/transactions/TxDetails/Summary/SafeTxHashDataRow/index.tsx">`...` / index.tsx</div> | <div title="Base coverage is: 96.43%">86.49% (-9.94% 🔻)</div> | <div title="Base coverage is: 66.67%">88.89% (+22.22% 🔼)</div> | 100% | <div title="Base coverage is: 100%">86.11% (-13.89% 🔻)</div> |
| 🟢 | <div title="src/components/common/ModalDialog/index.tsx">`...` / index.tsx</div> | 86.67% | <div title="Base coverage is: 81.82%">80% (-1.82% 🔻)</div> | 75% | 92.31% |
| 🟢 | <div title="src/hooks/useMnemonicName/index.ts">`...` / index.ts</div> | <div title="Base coverage is: 90%">94.44% (+4.44% 🔼)</div> | <div title="Base coverage is: 83.33%">75% (-8.33% 🔻)</div> | 100% | <div title="Base coverage is: 93.33%">100% (+6.67% 🔼)</div> |
| 🟢 | <div title="src/components/tx/confirmation-views/index.tsx">`...` / index.tsx</div> | <div title="Base coverage is: 94.12%">93.55% (-0.57% 🔻)</div> | <div title="Base coverage is: 60%">65.38% (+5.38% 🔼)</div> | 100% | 100% |
| 🔴 | <div title="src/components/sidebar/SafeListContextMenu/index.tsx">`...` / index.tsx</div> | <div title="Base coverage is: 55.56%">54.76% (-0.79% 🔻)</div> | <div title="Base coverage is: 8.33%">16.67% (+8.33% 🔼)</div> | <div title="Base coverage is: 10%">11.11% (+1.11% 🔼)</div> | <div title="Base coverage is: 58%">56.41% (-1.59% 🔻)</div> |
| 🔴 | <div title="src/components/tx/ConfirmTxDetails/TxDetails.tsx">`...` / TxDetails.tsx</div> | <div title="Base coverage is: 69.23%">55% (-14.23% 🔻)</div> | 0% | 0% | <div title="Base coverage is: 75%">57.89% (-17.11% 🔻)</div> |

</details>

## Test suite run success
1880 tests passing in 256 suites.

<p align="right">Report generated by <a href="https://github.com/ArtiomTr/jest-coverage-report-action">🧪jest coverage report action</a> from a74ee34fe98db918f2b23052ea1214950d5c6b2f</p>

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-21T09:28:48Z
body: ## 📦 Next.js Bundle Analysis for @safe-global/web

This analysis was generated by the [Next.js Bundle Analysis action](https://github.com/hashicorp/nextjs-bundle-analysis). 🤖

This PR introduced no changes to the JavaScript bundle! 🙌<!-- __NEXTJS_BUNDLE_@safe-global/web -->
<!-- Sticky Pull Request Commentnext-bundle-analysis -->
author_association: CONTRIBUTOR
user: login: francovenica
created_at: 2025-03-21T14:31:34Z
body: Looks good. The hashes show fine

Note: It is expanded by default when you use a ledger that is connected through LedgerLive, but if the hardware wallet was imported in something like the MM extension, then is not expanded by default. I assume there is no way to know if the address that metamask has is a hardware wallet or not. 
For ledger is not an issue since you have Ledger live, but Trezor currently can only be used by being imported into a wallet like MM
author_association: CONTRIBUTOR