Skip to content

Commit

Permalink
fix: Add new hook useTransactionDescription to combine txType and txInfo
Browse files Browse the repository at this point in the history
  • Loading branch information
usame-algan committed Sep 6, 2023
1 parent 8a6393d commit 32aa756
Show file tree
Hide file tree
Showing 5 changed files with 345 additions and 7 deletions.
74 changes: 74 additions & 0 deletions src/components/transactions/HumanDescription/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={css.address}>
<EthHashInfo address={fragment.value} name={addressBook[fragment.value]} avatarSize={20} />
</div>
)
}

const TokenValueFragment = ({ fragment }: { fragment: RichTokenValueFragment }) => {
const address = (
<>
<TokenIcon logoUri={fragment.logoUri || undefined} tokenSymbol={fragment.symbol || undefined} size={20} />
{fragment.symbol}
</>
)

return (
<Typography className={css.value}>
{fragment.value} {address}
</Typography>
)
}

export const HumanDescription = ({ fragments }: { fragments: HumanDescriptionFragment[] }) => {
return (
<div className={css.wrapper}>
{fragments.map((fragment) => {
switch (fragment.type) {
case ValueType.Text:
return <span>{fragment.value}</span>
case ValueType.Address:
return <AddressFragment fragment={fragment} />
case ValueType.TokenValue:
return <TokenValueFragment fragment={fragment} />
}
})}
</div>
)
}
34 changes: 34 additions & 0 deletions src/components/transactions/HumanDescription/styles.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
32 changes: 25 additions & 7 deletions src/components/transactions/TxSummary/index.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
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'
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) {
Expand All @@ -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 (
<Box className={css.description}>
<SafeAppIconCard
src={icon}
alt="Transaction icon"
width={16}
height={16}
fallback="/images/transactions/custom.svg"
/>
{humanDescription ? <HumanDescription fragments={humanDescription} /> : text}
</Box>
)
}

const TxSummary = ({ item, isGrouped }: TxSummaryProps): ReactElement => {
const tx = item.transaction
const wallet = useWallet()
Expand Down Expand Up @@ -67,10 +88,7 @@ const TxSummary = ({ item, isGrouped }: TxSummaryProps): ReactElement => {
{nonce && !isGrouped && <Box gridArea="nonce">{nonce}</Box>}

<Box gridArea="type" className={css.columnWrap}>
<Box display="flex" alignItems="center" gap={1}>
<TxType tx={tx} />
<TxInfo info={tx.txInfo} />
</Box>
<TxDescription tx={tx} />
</Box>

<Box gridArea="date">
Expand Down
10 changes: 10 additions & 0 deletions src/components/transactions/TxSummary/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
202 changes: 202 additions & 0 deletions src/hooks/useTransactionDescription.tsx
Original file line number Diff line number Diff line change
@@ -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<TransactionSummary, 'txInfo'>): 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'}
<TransferTx info={txInfo} />
<>
{isSendTx ? 'to' : 'from'}
<EthHashInfo address={txInfo.recipient.value} avatarSize={16} showName={false} showPrefix={false} />
</>
</>
)
}

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 <b className={css.method}>{addressName}</b>
</>
) : undefined

const name = safeAppInfo.name ? (
<>
via
<b className={css.method}>
<SafeAppIconCard
src={safeAppInfo.logoUri}
alt="Transaction icon"
width={16}
height={16}
fallback="/images/transactions/custom.svg"
/>
{safeAppInfo.name}
</b>
</>
) : undefined

const method = info.methodName ? (
<>
<span>Called </span>
<b className={css.method}>{info.methodName}</b>
</>
) : 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 ? <b className={css.method}>{info.methodName}</b> : ''}
{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: <TransferDescription txInfo={tx.txInfo} isSendTx={isSendTx} />,
}
}

case TransactionInfoType.SETTINGS_CHANGE: {
return {
icon: '/images/transactions/settings.svg',
text: <SettingsChangeDescription info={tx.txInfo} />,
}
}
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: <SafeAppTxDescription info={tx.txInfo} safeAppInfo={tx.safeAppInfo} addressName={addressName} />,
}
}

return {
icon: toAddress?.logoUri || '/images/transactions/custom.svg',
text: <CustomTxDescription info={tx.txInfo} addressName={addressName} />,
}
}

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])
}

0 comments on commit 32aa756

Please sign in to comment.