From 32aa75664283cc36dafb7bed73a572a6d71ccd30 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Wed, 6 Sep 2023 16:13:46 +0200 Subject: [PATCH] fix: Add new hook useTransactionDescription to combine txType and txInfo --- .../transactions/HumanDescription/index.tsx | 74 +++++++ .../HumanDescription/styles.module.css | 34 +++ .../transactions/TxSummary/index.tsx | 32 ++- .../transactions/TxSummary/styles.module.css | 10 + src/hooks/useTransactionDescription.tsx | 202 ++++++++++++++++++ 5 files changed, 345 insertions(+), 7 deletions(-) create mode 100644 src/components/transactions/HumanDescription/index.tsx create mode 100644 src/components/transactions/HumanDescription/styles.module.css create mode 100644 src/hooks/useTransactionDescription.tsx diff --git a/src/components/transactions/HumanDescription/index.tsx b/src/components/transactions/HumanDescription/index.tsx new file mode 100644 index 0000000000..5a20d59a9d --- /dev/null +++ b/src/components/transactions/HumanDescription/index.tsx @@ -0,0 +1,74 @@ +import { Typography } from '@mui/material' +import EthHashInfo from '@/components/common/EthHashInfo' +import TokenIcon from '@/components/common/TokenIcon' + +import css from './styles.module.css' +import useAddressBook from '@/hooks/useAddressBook' + +// TODO: Export these to the gateway-sdk +export enum ValueType { + Text = 'text', + TokenValue = 'tokenValue', + Address = 'address', +} + +export interface RichTokenValueFragment { + type: ValueType.TokenValue + value: string + symbol: string | null + logoUri: string | null +} + +export interface RichTextFragment { + type: ValueType.Text + value: string +} + +export interface RichAddressFragment { + type: ValueType.Address + value: `0x${string}` +} + +export type HumanDescriptionFragment = RichTextFragment | RichTokenValueFragment | RichAddressFragment + +const AddressFragment = ({ fragment }: { fragment: RichAddressFragment }) => { + const addressBook = useAddressBook() + + return ( +
+ +
+ ) +} + +const TokenValueFragment = ({ fragment }: { fragment: RichTokenValueFragment }) => { + const address = ( + <> + + {fragment.symbol} + + ) + + return ( + + {fragment.value} {address} + + ) +} + +export const HumanDescription = ({ fragments }: { fragments: HumanDescriptionFragment[] }) => { + return ( +
+ {fragments.map((fragment) => { + switch (fragment.type) { + case ValueType.Text: + return {fragment.value} + case ValueType.Address: + return + case ValueType.TokenValue: + return + } + })} +
+ ) +} diff --git a/src/components/transactions/HumanDescription/styles.module.css b/src/components/transactions/HumanDescription/styles.module.css new file mode 100644 index 0000000000..d7baae86d0 --- /dev/null +++ b/src/components/transactions/HumanDescription/styles.module.css @@ -0,0 +1,34 @@ +.summary { + display: flex; + gap: 8px; + align-items: center; + width: 100%; +} + +.address { +} + +.address > div { + gap: 4px; + font-family: monospace; +} + +/* TODO: This is a workaround to hide address in case there is a title */ +.address div[title] + div { + display: none; +} + +.value { + display: flex; + align-items: center; + background: #efefef; + padding: 0 8px; + border-radius: 5px; + font-size: 14px; + font-weight: bold; +} + +.wrapper { + display: flex; + gap: 8px; +} diff --git a/src/components/transactions/TxSummary/index.tsx b/src/components/transactions/TxSummary/index.tsx index d8b2b3dbe6..fb3e014fe6 100644 --- a/src/components/transactions/TxSummary/index.tsx +++ b/src/components/transactions/TxSummary/index.tsx @@ -1,10 +1,9 @@ import type { Palette } from '@mui/material' import { Box, CircularProgress, Typography } from '@mui/material' import type { ReactElement } from 'react' -import { type Transaction, TransactionStatus } from '@safe-global/safe-gateway-typescript-sdk' +import { type Transaction, TransactionStatus, type TransactionSummary } from '@safe-global/safe-gateway-typescript-sdk' import DateTime from '@/components/common/DateTime' -import TxInfo from '@/components/transactions/TxInfo' import SignTxButton from '@/components/transactions/SignTxButton' import ExecuteTxButton from '@/components/transactions/ExecuteTxButton' import css from './styles.module.css' @@ -12,9 +11,11 @@ import useWallet from '@/hooks/wallets/useWallet' import { isAwaitingExecution, isMultisigExecutionInfo, isTxQueued } from '@/utils/transaction-guards' import RejectTxButton from '@/components/transactions/RejectTxButton' import useTransactionStatus from '@/hooks/useTransactionStatus' -import TxType from '@/components/transactions/TxType' import TxConfirmations from '../TxConfirmations' import useIsPending from '@/hooks/useIsPending' +import { HumanDescription, type HumanDescriptionFragment } from '@/components/transactions/HumanDescription' +import { useTransactionDescription } from '@/hooks/useTransactionDescription' +import SafeAppIconCard from '@/components/safe-apps/SafeAppIconCard' const getStatusColor = (value: TransactionStatus, palette: Palette) => { switch (value) { @@ -36,6 +37,26 @@ type TxSummaryProps = { item: Transaction } +const TxDescription = ({ tx }: { tx: TransactionSummary }) => { + const { text, icon } = useTransactionDescription(tx) + + // @ts-ignore + const humanDescription = tx.txInfo.richDecodedInfo?.fragments as HumanDescriptionFragment[] + + return ( + + + {humanDescription ? : text} + + ) +} + const TxSummary = ({ item, isGrouped }: TxSummaryProps): ReactElement => { const tx = item.transaction const wallet = useWallet() @@ -67,10 +88,7 @@ const TxSummary = ({ item, isGrouped }: TxSummaryProps): ReactElement => { {nonce && !isGrouped && {nonce}} - - - - + diff --git a/src/components/transactions/TxSummary/styles.module.css b/src/components/transactions/TxSummary/styles.module.css index d2370c27c2..e33c874911 100644 --- a/src/components/transactions/TxSummary/styles.module.css +++ b/src/components/transactions/TxSummary/styles.module.css @@ -34,6 +34,16 @@ border-radius: 4px; font-size: 14px; font-weight: bold; + display: flex; + align-items: center; + gap: 0.5em; +} + +.description { + display: flex; + align-items: center; + gap: var(--space-1); + color: var(--color-text-primary); } @media (max-width: 599.95px) { diff --git a/src/hooks/useTransactionDescription.tsx b/src/hooks/useTransactionDescription.tsx new file mode 100644 index 0000000000..d6b7f26b01 --- /dev/null +++ b/src/hooks/useTransactionDescription.tsx @@ -0,0 +1,202 @@ +import { type ReactNode, useMemo } from 'react' +import { + SettingsInfoType, + TransactionInfoType, + TransferDirection, + type AddressEx, + type TransactionSummary, + type Transfer, + type SettingsChange, + type Custom, + type MultiSend, + type SafeAppInfo, +} from '@safe-global/safe-gateway-typescript-sdk' + +import { isCancellationTxInfo, isModuleExecutionInfo, isMultiSendTxInfo } from '@/utils/transaction-guards' +import useAddressBook from './useAddressBook' +import type { AddressBook } from '@/store/addressBookSlice' +import css from '@/components/transactions/TxSummary/styles.module.css' +import { shortenAddress } from '@/utils/formatters' +import EthHashInfo from '@/components/common/EthHashInfo' +import { TransferTx } from '@/components/transactions/TxInfo' +import SafeAppIconCard from '@/components/safe-apps/SafeAppIconCard' + +const getTxTo = ({ txInfo }: Pick): AddressEx | undefined => { + switch (txInfo.type) { + case TransactionInfoType.CREATION: { + return txInfo.factory + } + case TransactionInfoType.TRANSFER: { + return txInfo.recipient + } + case TransactionInfoType.SETTINGS_CHANGE: { + return undefined + } + case TransactionInfoType.CUSTOM: { + return txInfo.to + } + } +} + +type TxType = { + icon: string + text: ReactNode +} + +const TransferDescription = ({ txInfo, isSendTx }: { txInfo: Transfer; isSendTx: boolean }) => { + return ( + <> + {isSendTx ? 'Send' : 'Receive'} + + <> + {isSendTx ? 'to' : 'from'} + + + + ) +} + +const SettingsChangeDescription = ({ info }: { info: SettingsChange }) => { + const isDeleteGuard = info.settingsInfo?.type === SettingsInfoType.DELETE_GUARD + + if (isDeleteGuard) return <>deleteGuard + + if ( + info.settingsInfo?.type === SettingsInfoType.ENABLE_MODULE || + info.settingsInfo?.type === SettingsInfoType.DISABLE_MODULE + ) { + return <>{info.settingsInfo.module.name} + } + + return <>{info.dataDecoded.method} +} + +const SafeAppTxDescription = ({ + info, + safeAppInfo, + addressName, +}: { + info: Custom | MultiSend + safeAppInfo: SafeAppInfo + addressName?: string +}) => { + const origin = addressName ? ( + <> + on {addressName} + + ) : undefined + + const name = safeAppInfo.name ? ( + <> + via + + + {safeAppInfo.name} + + + ) : undefined + + const method = info.methodName ? ( + <> + Called + {info.methodName} + + ) : undefined + + return ( + <> + {method} + {origin || name} + {isMultiSendTxInfo(info) && ` with ${info.actionCount} action${info.actionCount > 1 ? 's' : ''}`} + + ) +} + +const CustomTxDescription = ({ info, addressName }: { info: Custom | MultiSend; addressName?: string }) => { + return ( + <> + {addressName || 'Contract interaction'} + {info.methodName && ': '} + {info.methodName ? {info.methodName} : ''} + {isMultiSendTxInfo(info) && `with ${info.actionCount} action${info.actionCount > 1 ? 's' : ''}`} + + ) +} + +export const getTransactionDescription = (tx: TransactionSummary, addressBook: AddressBook): TxType => { + const toAddress = getTxTo(tx) + const addressName = addressBook[toAddress?.value || ''] || toAddress?.name + + switch (tx.txInfo.type) { + case TransactionInfoType.CREATION: { + return { + icon: toAddress?.logoUri || '/images/transactions/settings.svg', + text: `Safe Account created by ${shortenAddress(tx.txInfo.creator.value)}`, + } + } + + case TransactionInfoType.TRANSFER: { + const isSendTx = tx.txInfo.direction === TransferDirection.OUTGOING + + return { + icon: isSendTx ? '/images/transactions/outgoing.svg' : '/images/transactions/incoming.svg', + text: , + } + } + + case TransactionInfoType.SETTINGS_CHANGE: { + return { + icon: '/images/transactions/settings.svg', + text: , + } + } + case TransactionInfoType.CUSTOM: { + if (isModuleExecutionInfo(tx.executionInfo)) { + return { + icon: toAddress?.logoUri || '/images/transactions/custom.svg', + text: toAddress?.name || 'Contract interaction', + } + } + + if (isCancellationTxInfo(tx.txInfo)) { + return { + icon: '/images/transactions/circle-cross-red.svg', + text: 'On-chain rejection', + } + } + + if (tx.safeAppInfo) { + return { + icon: '/images/transactions/custom.svg', + text: , + } + } + + return { + icon: toAddress?.logoUri || '/images/transactions/custom.svg', + text: , + } + } + + default: { + return { + icon: '/images/transactions/custom.svg', + text: addressName || 'Contract interaction', + } + } + } +} + +export const useTransactionDescription = (tx: TransactionSummary): TxType => { + const addressBook = useAddressBook() + + return useMemo(() => { + return getTransactionDescription(tx, addressBook) + }, [tx, addressBook]) +}