Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Human readable descriptions #2371

Merged
merged 24 commits into from
Aug 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b1472a9
Display human readable description if it exists
usame-algan Aug 8, 2023
15db3c5
Add some spacing
usame-algan Aug 10, 2023
9f0bcb6
Add readable description to nonce replace form, remove it in every ot…
usame-algan Aug 11, 2023
f231cd7
Show txDescription in notifications and notification center
usame-algan Aug 11, 2023
ec3cdcf
Merge remote-tracking branch 'origin/main' into human-readable-txs
usame-algan Aug 14, 2023
c7e2653
Update gateway-sdk package, rename to humanDescription
usame-algan Aug 14, 2023
e854d06
Merge remote-tracking branch 'origin/dev' into human-readable-txs
usame-algan Aug 16, 2023
89bdefc
Display human description in txSummary if possible, update nonce repl…
usame-algan Aug 17, 2023
1cea04a
fix: Adjust history column layout, display transfer descriptions in h…
usame-algan Aug 18, 2023
82b12bb
Revert env change, hide human description in queue
usame-algan Aug 25, 2023
9ececeb
Merge remote-tracking branch 'origin/dev' into human-readable-txs
usame-algan Aug 28, 2023
0154d0b
fix: Human-readable notifications
usame-algan Aug 28, 2023
31fc3fe
fix: Failing tests
usame-algan Aug 28, 2023
e761fd2
revert: Remove human descriptions from tx history
usame-algan Aug 28, 2023
12fa7ae
fix: Add human description fallbacks for notifications
usame-algan Aug 28, 2023
4accc6b
fix: Failing tests
usame-algan Aug 28, 2023
88c6a13
fix: fallback label when replacing nonce, move humanDescriptions outs…
usame-algan Aug 29, 2023
7749282
fix: Revert constants, add fallback for pending tx notifications
usame-algan Aug 29, 2023
286b398
fix: Add generic humanDescription fallback to notifications
usame-algan Aug 29, 2023
2c33674
fix: Remove custom fallbacks and use one fallback inside Notifications
usame-algan Aug 29, 2023
c748e45
revert: Changes to the tx history
usame-algan Aug 29, 2023
a7d513f
fix: Notification title fallback if human description is undefined
usame-algan Aug 30, 2023
3220be0
Merge remote-tracking branch 'origin/dev' into human-readable-txs
usame-algan Aug 30, 2023
83799b6
fix: Add bigger timeout to batch tx e2e test
usame-algan Aug 30, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cypress/e2e/smoke/batch_tx.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe('Create batch transaction', () => {
cy.visit(`/home?safe=${SAFE}`)
cy.contains('Accept selection').click()

cy.contains(/E2E Wallet @ G(ö|oe)rli/)
cy.contains(/E2E Wallet @ G(ö|oe)rli/, { timeout: 10000 })
})

it('Should open an empty batch list', () => {
Expand Down
9 changes: 8 additions & 1 deletion src/components/common/Notifications/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useAppDispatch, useAppSelector } from '@/store'
import type { Notification } from '@/store/notificationsSlice'
import { closeNotification, readNotification, selectNotifications } from '@/store/notificationsSlice'
import type { AlertColor, SnackbarCloseReason } from '@mui/material'
import { Alert, Link, Snackbar } from '@mui/material'
import { Alert, Link, Snackbar, Typography } from '@mui/material'
import css from './styles.module.css'
import NextLink from 'next/link'
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
Expand Down Expand Up @@ -45,6 +45,7 @@ export const NotificationLink = ({
}

const Toast = ({
title,
message,
detailedMessage,
variant,
Expand Down Expand Up @@ -73,6 +74,12 @@ const Toast = ({
return (
<Snackbar open onClose={handleClose} sx={toastStyle} autoHideDuration={autoHideDuration}>
<Alert severity={variant} onClose={handleClose} elevation={3} sx={{ width: '340px' }}>
{title && (
<Typography variant="body2" fontWeight="700">
{title}
</Typography>
)}

{message}

{detailedMessage && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { formatTimeInWords } from '@/utils/date'
import css from './styles.module.css'
import classnames from 'classnames'
import SvgIcon from '@mui/material/SvgIcon'
import { Typography } from '@mui/material'

const VARIANT_ICONS = {
error: ErrorIcon,
Expand All @@ -35,6 +36,7 @@ const NotificationCenterItem = ({
timestamp,
link,
handleClose,
title,
}: Notification & { handleClose: () => void }): ReactElement => {
const requiresAction = !isRead && !!link

Expand All @@ -45,6 +47,13 @@ const NotificationCenterItem = ({
</span>
)

const primaryText = (
<>
{title && <Typography fontWeight="700">{title}</Typography>}
<Typography>{message}</Typography>
</>
)

return (
<ListItem className={classnames(css.item, { [css.requiresAction]: requiresAction })}>
<ListItemAvatar className={css.avatar}>
Expand All @@ -58,7 +67,7 @@ const NotificationCenterItem = ({
{getNotificationIcon(variant)}
</UnreadBadge>
</ListItemAvatar>
<ListItemText primary={message} secondary={secondaryText} />
<ListItemText primary={primaryText} secondary={secondaryText} />
</ListItem>
)
}
Expand Down
10 changes: 6 additions & 4 deletions src/components/tx-flow/common/TxNonce/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,21 +59,23 @@ const NonceFormOption = memo(function NonceFormOption({
const addressBook = useAddressBook()
const transactions = useQueuedTxByNonce(Number(nonce))

const label = useMemo(() => {
const txLabel = useMemo(() => {
const latestTransactions = getLatestTransactions(transactions)

if (latestTransactions.length === 0) {
return
}

const [{ transaction }] = latestTransactions
return getTransactionType(transaction, addressBook).text
return transaction.txInfo.humanDescription || `${getTransactionType(transaction, addressBook).text} transaction`
}, [addressBook, transactions])

const label = txLabel || 'New transaction'
katspaugh marked this conversation as resolved.
Show resolved Hide resolved

return (
<MenuItem {...menuItemProps}>
<Typography variant="body2">
<b>{nonce}</b>&nbsp;- {`${label || 'New'} transaction`}
<b>{nonce}</b>&nbsp;- {label}
</Typography>
</MenuItem>
)
Expand Down Expand Up @@ -168,7 +170,7 @@ const TxNonceForm = ({ nonce, recommendedNonce }: { nonce: string; recommendedNo
return (
<>
{isRecommendedNonce && <NonceFormHeader>Recommended nonce</NonceFormHeader>}
{isInitialPreviousNonce && <NonceFormHeader sx={{ pt: 3 }}>Already in queue</NonceFormHeader>}
{isInitialPreviousNonce && <NonceFormHeader sx={{ pt: 3 }}>Replace existing</NonceFormHeader>}
<NonceFormOption key={option} menuItemProps={props} nonce={option} />
</>
)
Expand Down
6 changes: 5 additions & 1 deletion src/components/tx/SignOrExecuteForm/ExecuteForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import commonCss from '@/components/tx-flow/common/styles.module.css'
import { TxSecurityContext } from '../security/shared/TxSecurityContext'
import useIsSafeOwner from '@/hooks/useIsSafeOwner'
import NonOwnerError from '@/components/tx/SignOrExecuteForm/NonOwnerError'
import { useAppSelector } from '@/store'
import { selectQueuedTransactionById } from '@/store/txQueueSlice'

const ExecuteForm = ({
safeTx,
Expand All @@ -50,6 +52,8 @@ const ExecuteForm = ({
const { setTxFlow } = useContext(TxModalContext)
const { needsRiskConfirmation, isRiskConfirmed, setIsRiskIgnored } = useContext(TxSecurityContext)

const tx = useAppSelector((state) => selectQueuedTransactionById(state, txId))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the transaction dispatchers (e.g. executeTx) were thunks, you could select this inside it as you already have the txId. It's an opinionated change though so I won't request it be changed.


// Check that the transaction is executable
const isExecutionLoop = useIsExecutionLoop()

Expand Down Expand Up @@ -85,7 +89,7 @@ const ExecuteForm = ({
const txOptions = getTxOptions(advancedParams, currentChain)

try {
const executedTxId = await executeTx(txOptions, safeTx, txId, origin, willRelay)
const executedTxId = await executeTx(txOptions, safeTx, txId, origin, willRelay, tx)
setTxFlow(<SuccessScreen txId={executedTxId} />, undefined, false)
} catch (_err) {
const err = asError(_err)
Expand Down
6 changes: 5 additions & 1 deletion src/components/tx/SignOrExecuteForm/SignForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import commonCss from '@/components/tx-flow/common/styles.module.css'
import { TxSecurityContext } from '../security/shared/TxSecurityContext'
import NonOwnerError from '@/components/tx/SignOrExecuteForm/NonOwnerError'
import BatchButton from './BatchButton'
import { useAppSelector } from '@/store'
import { selectQueuedTransactionById } from '@/store/txQueueSlice'

const SignForm = ({
safeTx,
Expand All @@ -38,6 +40,8 @@ const SignForm = ({
const { needsRiskConfirmation, isRiskConfirmed, setIsRiskIgnored } = useContext(TxSecurityContext)
const hasSigned = useAlreadySigned(safeTx)

const tx = useAppSelector((state) => selectQueuedTransactionById(state, txId))

// On modal submit
const handleSubmit = async (e: SyntheticEvent, isAddingToBatch = false) => {
e.preventDefault()
Expand All @@ -53,7 +57,7 @@ const SignForm = ({
setSubmitError(undefined)

try {
await (isAddingToBatch ? addToBatch(safeTx, origin) : signTx(safeTx, txId, origin))
await (isAddingToBatch ? addToBatch(safeTx, origin) : signTx(safeTx, txId, origin, tx))
} catch (_err) {
const err = asError(_err)
logError(Errors._804, err)
Expand Down
25 changes: 16 additions & 9 deletions src/components/tx/SignOrExecuteForm/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,18 @@ import type { OnboardAPI } from '@web3-onboard/core'
import { getSafeTxGas, getRecommendedNonce } from '@/services/tx/tx-sender/recommendedNonce'
import useAsync from '@/hooks/useAsync'
import { useUpdateBatch } from '@/hooks/useDraftBatch'
import { type Transaction, type TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk'

type TxActions = {
addToBatch: (safeTx?: SafeTransaction, origin?: string) => Promise<string>
signTx: (safeTx?: SafeTransaction, txId?: string, origin?: string) => Promise<string>
signTx: (safeTx?: SafeTransaction, txId?: string, origin?: string, transaction?: Transaction) => Promise<string>
executeTx: (
txOptions: TransactionOptions,
safeTx?: SafeTransaction,
txId?: string,
origin?: string,
isRelayed?: boolean,
transaction?: Transaction,
) => Promise<string>
}

Expand Down Expand Up @@ -83,50 +85,55 @@ export const useTxActions = (): TxActions => {
return await dispatchTxSigning(safeTx, version, onboard, chainId, txId)
}

const signTx: TxActions['signTx'] = async (safeTx, txId, origin) => {
const signTx: TxActions['signTx'] = async (safeTx, txId, origin, transaction) => {
assertTx(safeTx)
assertWallet(wallet)
assertOnboard(onboard)

const humanDescription = transaction?.transaction?.txInfo?.humanDescription

// Smart contract wallets must sign via an on-chain tx
if (await isSmartContractWallet(wallet)) {
// 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
await dispatchOnChainSigning(safeTx, id, onboard, chainId)
await dispatchOnChainSigning(safeTx, id, onboard, chainId, humanDescription)
return id
}

// Otherwise, sign off-chain
const signedTx = await dispatchTxSigning(safeTx, version, onboard, chainId, txId)
const signedTx = await dispatchTxSigning(safeTx, version, onboard, chainId, txId, humanDescription)
iamacook marked this conversation as resolved.
Show resolved Hide resolved
const tx = await proposeTx(wallet.address, signedTx, txId, origin)
return tx.txId
}

const executeTx: TxActions['executeTx'] = async (txOptions, safeTx, txId, origin, isRelayed) => {
const executeTx: TxActions['executeTx'] = async (txOptions, safeTx, txId, origin, isRelayed, transaction) => {
assertTx(safeTx)
assertWallet(wallet)
assertOnboard(onboard)

let tx: TransactionDetails | undefined
// Relayed transactions must be fully signed, so request a final signature if needed
if (isRelayed && safeTx.signatures.size < safe.threshold) {
safeTx = await signRelayedTx(safeTx)
const tx = await proposeTx(wallet.address, safeTx, txId, origin)
tx = await proposeTx(wallet.address, safeTx, txId, origin)
txId = tx.txId
}

// Propose the tx if there's no id yet ("immediate execution")
if (!txId) {
const tx = await proposeTx(wallet.address, safeTx, txId, origin)
tx = await proposeTx(wallet.address, safeTx, txId, origin)
txId = tx.txId
}

const humanDescription = tx?.txInfo?.humanDescription || transaction?.transaction?.txInfo?.humanDescription

// Relay or execute the tx via connected wallet
if (isRelayed) {
await dispatchTxRelay(safeTx, safe, txId, txOptions.gasLimit)
await dispatchTxRelay(safeTx, safe, txId, txOptions.gasLimit, humanDescription)
} else {
await dispatchTxExecution(safeTx, txOptions, txId, onboard, chainId, safeAddress)
await dispatchTxExecution(safeTx, txOptions, txId, onboard, chainId, safeAddress, humanDescription)
}

return txId
Expand Down
34 changes: 17 additions & 17 deletions src/hooks/useTxNotifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,20 @@ import useSafeAddress from './useSafeAddress'
import { getExplorerLink } from '@/utils/gateway'

const TxNotifications = {
[TxEvent.SIGN_FAILED]: 'Signature failed. Please try again.',
[TxEvent.PROPOSED]: 'Your transaction was successfully proposed.',
[TxEvent.PROPOSE_FAILED]: 'Failed proposing the transaction. Please try again.',
[TxEvent.SIGNATURE_PROPOSED]: 'You successfully signed the transaction.',
[TxEvent.SIGNATURE_PROPOSE_FAILED]: 'Failed to send the signature. Please try again.',
[TxEvent.EXECUTING]: 'Please confirm the execution in your wallet.',
[TxEvent.PROCESSING]: 'Your transaction is being processed.',
[TxEvent.PROCESSING_MODULE]:
'Your transaction has been submitted and will appear in the interface only after it has been successfully processed and indexed.',
[TxEvent.ONCHAIN_SIGNATURE_REQUESTED]:
'An on-chain signature is required. Please confirm the transaction in your wallet.',
[TxEvent.ONCHAIN_SIGNATURE_SUCCESS]:
"The on-chain signature request was confirmed. Once it's on chain, the transaction will be signed.",
[TxEvent.PROCESSED]: 'Your transaction was successfully processed and is now being indexed.',
[TxEvent.REVERTED]: 'Transaction reverted. Please check your gas settings.',
[TxEvent.SUCCESS]: 'Your transaction was successfully executed.',
[TxEvent.FAILED]: 'Your transaction was unsuccessful.',
[TxEvent.SIGN_FAILED]: 'Failed to sign. Please try again.',
[TxEvent.PROPOSED]: 'Successfully added to queue.',
[TxEvent.PROPOSE_FAILED]: 'Failed to add to queue. Please try again.',
[TxEvent.SIGNATURE_PROPOSED]: 'Successfully signed.',
[TxEvent.SIGNATURE_PROPOSE_FAILED]: 'Failed to send signature. Please try again.',
[TxEvent.EXECUTING]: 'Confirm the execution in your wallet.',
[TxEvent.PROCESSING]: 'Validating...',
[TxEvent.PROCESSING_MODULE]: 'Validating module interaction...',
[TxEvent.ONCHAIN_SIGNATURE_REQUESTED]: 'Confirm on-chain signature in your wallet.',
[TxEvent.ONCHAIN_SIGNATURE_SUCCESS]: 'On-chain signature request confirmed.',
[TxEvent.PROCESSED]: 'Successfully validated. Indexing...',
[TxEvent.REVERTED]: 'Reverted. Please check your gas settings.',
[TxEvent.SUCCESS]: 'Successfully executed.',
[TxEvent.FAILED]: 'Failed.',
Comment on lines +19 to +32
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these changes necessary? If there's no humanDescription, we now lose some context.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

humanDescription will be always there unless the feature is disabled for technical reasons.

Copy link
Member

@iamacook iamacook Aug 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the signature hash does not match on the gateway the humanDescription will be null. You can test this with spending limits atm:

image
image

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I think we should add a fallback for the title. Maybe we can include the nonce, something like Transaction #113

}

enum Variant {
Expand Down Expand Up @@ -79,9 +76,12 @@ const useTxNotifications = (): void => {
const txId = 'txId' in detail ? detail.txId : undefined
const txHash = 'txHash' in detail ? detail.txHash : undefined
const groupKey = 'groupKey' in detail && detail.groupKey ? detail.groupKey : txId || ''
const humanDescription =
'humanDescription' in detail && detail.humanDescription ? detail.humanDescription : 'Transaction'

dispatch(
showNotification({
title: humanDescription,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What will it look like w/o a title?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went through each tx event that we show a notification for and made sure to add a humanDescription there, either from the CGW response or an appropriate fallback i.e. Transaction #<nonce> or Module transaction

message,
detailedMessage: isError ? detail.error.message : undefined,
groupKey,
Expand Down
13 changes: 11 additions & 2 deletions src/services/tx/tx-sender/__tests__/ts-sender.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ const mockSafeSDK = {
createTransaction: jest.fn(() => ({
signatures: new Map(),
addSignature: jest.fn(),
data: {
nonce: '1',
},
})),
createRejectionTransaction: jest.fn(() => ({
addSignature: jest.fn(),
Expand Down Expand Up @@ -399,7 +402,10 @@ describe('txSender', () => {

expect((error as Error).message).toBe('rejected')

expect(txEvents.txDispatch).toHaveBeenCalledWith('SIGN_FAILED', { txId: '0x345', error })
expect(txEvents.txDispatch).toHaveBeenCalledWith('SIGN_FAILED', {
txId: '0x345',
error,
})
expect(txEvents.txDispatch).not.toHaveBeenCalledWith('SIGNED', { txId: '0x345' })
}
})
Expand Down Expand Up @@ -430,7 +436,10 @@ describe('txSender', () => {

expect((error as Error).message).toBe('failure-specific error')

expect(txEvents.txDispatch).toHaveBeenCalledWith('SIGN_FAILED', { txId: '0x345', error })
expect(txEvents.txDispatch).toHaveBeenCalledWith('SIGN_FAILED', {
txId: '0x345',
error,
})
expect(txEvents.txDispatch).not.toHaveBeenCalledWith('SIGNED', { txId: '0x345' })
}
})
Expand Down
Loading
Loading