Skip to content

Commit

Permalink
feat: Add human readable transfers to history and queue (#2518)
Browse files Browse the repository at this point in the history
* Add human readable transfers to history and queue

* Format token value to adhere to the style guide

* Fix failing tests

* Fix mobile view
  • Loading branch information
usame-algan authored Sep 20, 2023
1 parent 2e062eb commit b9a5842
Show file tree
Hide file tree
Showing 13 changed files with 178 additions and 50 deletions.
2 changes: 1 addition & 1 deletion cypress/e2e/pages/create_tx.pages.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const rotateLeftIcon = '[data-testid="RotateLeftIcon"]'
const viewTransactionBtn = 'View transaction'
const transactionDetailsTitle = 'Transaction details'
const QueueLabel = 'needs to be executed first'
const TransactionSummary = 'Send-'
const TransactionSummary = 'Send'

const maxAmountBtnStr = 'Max'
const nextBtnStr = 'Next'
Expand Down
6 changes: 4 additions & 2 deletions cypress/e2e/pages/dashboard.pages.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ export function verifyTxQueueWidget() {
cy.contains(noTransactionStr).should('not.exist')

// Queued txns
cy.contains(`a[href^="/transactions/tx?id=multisig_0x"]`, '13' + 'Send' + '-0.00002 GOR' + '1/1').should('exist')

cy.contains(
`a[href^="/transactions/tx?id=multisig_0x"]`,
'13' + 'Send' + '0.00002 GOR' + 'to' + 'gor:0xE297...9665' + '1/1',
).should('exist')
cy.contains(`a[href="${constants.transactionQueueUrl}${encodeURIComponent(constants.TEST_SAFE)}"]`, viewAllStr)
})
}
Expand Down
22 changes: 12 additions & 10 deletions cypress/e2e/smoke/tx_history.cy.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as constants from '../../support/constants'

const INCOMING = 'Received'
const OUTGOING = 'Sent'
const INCOMING = 'Receive'
const OUTGOING = 'Send'
const CONTRACT_INTERACTION = 'Contract interaction'

describe('Transaction history', () => {
Expand Down Expand Up @@ -31,8 +31,8 @@ describe('Transaction history', () => {
.last()
.within(() => {
// Type
cy.get('img').should('have.attr', 'alt', INCOMING)
cy.contains('div', 'Received').should('exist')
cy.get('img').should('have.attr', 'alt', 'Received')
cy.contains('div', INCOMING).should('exist')

// Info
cy.get('img[alt="GOR"]').should('be.visible')
Expand Down Expand Up @@ -73,10 +73,12 @@ describe('Transaction history', () => {
// Type
// TODO: update next line after fixing the logo
// cy.find('img').should('have.attr', 'src').should('include', WRAPPED_ETH)
cy.contains('div', 'Wrapped Ether').should('exist')
cy.contains('div', 'WETH').should('exist')

cy.contains('div', 'unlimited').should('exist')

// Info
cy.contains('div', 'approve').should('exist')
cy.contains('div', 'Approve').should('exist')

// Time
cy.contains('span', '5:00 PM').should('exist')
Expand All @@ -103,11 +105,11 @@ describe('Transaction history', () => {
.prev()
.within(() => {
// Type
cy.get('img').should('have.attr', 'alt', OUTGOING)
cy.contains('div', 'Sent').should('exist')
cy.get('img').should('have.attr', 'alt', 'Sent')
cy.contains('div', 'Send').should('exist')

// Info
cy.contains('span', '-0.11 WETH').should('exist')
cy.contains('span', '0.11 WETH').should('exist')

// Time
cy.contains('span', '5:01 PM').should('exist')
Expand All @@ -119,7 +121,7 @@ describe('Transaction history', () => {
.prev()
.within(() => {
// Type
cy.contains('div', 'Received').should('exist')
cy.contains('div', INCOMING).should('exist')

// Info
cy.contains('span', '120,497.61 DAI').should('exist')
Expand Down
3 changes: 3 additions & 0 deletions jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ const customJestConfig = {
},
testEnvironment: 'jest-environment-jsdom',
testEnvironmentOptions: { url: 'http://localhost/balances?safe=rin:0xb3b83bf204C458B461de9B0CD2739DB152b4fa5A' },
globals: {
fetch: global.fetch
}
}

// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"@safe-global/safe-core-sdk-utils": "^1.7.4",
"@safe-global/safe-deployments": "1.25.0",
"@safe-global/safe-ethers-lib": "^1.9.4",
"@safe-global/safe-gateway-typescript-sdk": "^3.9.0",
"@safe-global/safe-gateway-typescript-sdk": "^3.12.0",
"@safe-global/safe-modules-deployments": "^1.0.0",
"@safe-global/safe-react-components": "^2.0.6",
"@sentry/react": "^7.28.1",
Expand Down
11 changes: 8 additions & 3 deletions src/components/dashboard/PendingTxs/PendingTxListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import TxType from '@/components/transactions/TxType'
import css from './styles.module.css'
import OwnersIcon from '@/public/images/common/owners.svg'
import { AppRoutes } from '@/config/routes'
import { TransactionInfoType } from '@safe-global/safe-gateway-typescript-sdk'

type PendingTxType = {
transaction: TransactionSummary
Expand All @@ -31,6 +32,8 @@ const PendingTx = ({ transaction }: PendingTxType): ReactElement => {
[router, id],
)

const displayInfo = !transaction.txInfo.richDecodedInfo && transaction.txInfo.type !== TransactionInfoType.TRANSFER

return (
<NextLink href={url} passHref>
<Box className={css.container}>
Expand All @@ -40,9 +43,11 @@ const PendingTx = ({ transaction }: PendingTxType): ReactElement => {
<TxType tx={transaction} />
</Box>

<Box flex={1} className={css.txInfo}>
<TxInfo info={transaction.txInfo} />
</Box>
{displayInfo && (
<Box flex={1} className={css.txInfo}>
<TxInfo info={transaction.txInfo} />
</Box>
)}

{isMultisigExecutionInfo(transaction.executionInfo) ? (
<Box className={css.confirmationsCount}>
Expand Down
73 changes: 73 additions & 0 deletions src/components/transactions/HumanDescription/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {
type RichAddressFragment,
type RichDecodedInfo,
type RichTokenValueFragment,
RichFragmentType,
} from '@safe-global/safe-gateway-typescript-sdk/dist/types/human-description'
import EthHashInfo from '@/components/common/EthHashInfo'
import css from './styles.module.css'
import useAddressBook from '@/hooks/useAddressBook'
import TokenAmount from '@/components/common/TokenAmount'
import React from 'react'
import { type Transfer } from '@safe-global/safe-gateway-typescript-sdk'
import { TransferTx } from '@/components/transactions/TxInfo'
import { formatAmount } from '@/utils/formatNumber'

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 isUnlimitedApproval = fragment.value === 'unlimited'

return (
<TokenAmount
// formatAmount should ideally be done in the CGW or fragment should contain the raw value as well
value={isUnlimitedApproval ? fragment.value : formatAmount(fragment.value)}
direction={undefined}
logoUri={fragment.logoUri || undefined}
tokenSymbol={fragment.symbol || undefined}
/>
)
}

export const TransferDescription = ({ txInfo, isSendTx }: { txInfo: Transfer; isSendTx: boolean }) => {
const action = isSendTx ? 'Send' : 'Receive'
const direction = isSendTx ? 'to' : 'from'
const address = isSendTx ? txInfo.recipient.value : txInfo.sender.value
const name = isSendTx ? txInfo.recipient.name : txInfo.sender.name

return (
<>
{action}
<TransferTx info={txInfo} omitSign={true} />
{direction}
<div className={css.address}>
<EthHashInfo address={address} name={name} avatarSize={20} />
</div>
</>
)
}

export const HumanDescription = ({ fragments }: RichDecodedInfo) => {
return (
<>
{fragments.map((fragment) => {
switch (fragment.type) {
case RichFragmentType.Text:
return <span>{fragment.value}</span>
case RichFragmentType.Address:
return <AddressFragment fragment={fragment} />
case RichFragmentType.TokenValue:
return <TokenValueFragment fragment={fragment} />
}
})}
</>
)
}
30 changes: 30 additions & 0 deletions src/components/transactions/HumanDescription/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
.summary {
display: flex;
gap: 8px;
align-items: center;
width: 100%;
}

/* 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;
font-weight: bold;
gap: 4px;
}

.method {
display: inline-flex;
align-items: center;
gap: 0.5em;
}

.wrapper {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
19 changes: 12 additions & 7 deletions src/components/transactions/TxSummary/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
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, TransactionInfoType, TransactionStatus } from '@safe-global/safe-gateway-typescript-sdk'

import DateTime from '@/components/common/DateTime'
import TxInfo from '@/components/transactions/TxInfo'
Expand Down Expand Up @@ -52,15 +52,18 @@ const TxSummary = ({ item, isGrouped }: TxSummaryProps): ReactElement => {
: undefined

const displayConfirmations = isQueue && !!submittedConfirmations && !!requiredConfirmations
const displayInfo = !tx.txInfo.richDecodedInfo && tx.txInfo.type !== TransactionInfoType.TRANSFER

return (
<Box
className={`${css.gridContainer} ${
isQueue
? nonce && !isGrouped
? displayInfo
? css.columnTemplate
: css.columnTemplateWithoutNonce
: css.columnTemplateTxHistory
: css.columnTemplateShort
: displayInfo
? css.columnTemplateTxHistory
: css.columnTemplateTxHistoryShort
}`}
id={tx.id}
>
Expand All @@ -70,9 +73,11 @@ const TxSummary = ({ item, isGrouped }: TxSummaryProps): ReactElement => {
<TxType tx={tx} />
</Box>

<Box gridArea="info" className={css.columnWrap}>
<TxInfo info={tx.txInfo} />
</Box>
{displayInfo && (
<Box gridArea="info" className={css.columnWrap}>
<TxInfo info={tx.txInfo} />
</Box>
)}

<Box gridArea="date">
<DateTime value={tx.timestamp} />
Expand Down
40 changes: 16 additions & 24 deletions src/components/transactions/TxSummary/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,30 @@
}

.columnTemplate {
grid-template-columns: minmax(50px, 0.25fr) minmax(150px, 2fr) minmax(150px, 2fr) minmax(200px, 2fr) 1fr 1fr minmax(
170px,
1fr
);
grid-template-columns:
minmax(50px, 0.25fr) minmax(150px, 2fr) minmax(150px, 2fr) minmax(200px, 2fr) 1fr minmax(60px, 0.5fr)
minmax(170px, 1fr);
grid-template-areas: 'nonce type info date confirmations actions status';
}

.columnTemplateWithoutNonce {
grid-template-columns: minmax(50px, 0.25fr) minmax(150px, 2fr) minmax(150px, 2fr) minmax(200px, 2fr) 1fr 1fr minmax(
.columnTemplateShort {
grid-template-columns: minmax(50px, 0.25fr) minmax(150px, 4fr) minmax(200px, 2fr) 1fr minmax(60px, 0.5fr) minmax(
170px,
1fr
);
grid-template-areas: 'nonce type info date confirmations actions status';
grid-template-areas: 'nonce type date confirmations actions status';
}

.columnTemplateTxHistory {
grid-template-columns: minmax(50px, 0.25fr) minmax(150px, 2fr) minmax(150px, 2fr) minmax(100px, 1fr) minmax(
170px,
1fr
);
grid-template-columns: minmax(50px, 0.25fr) minmax(150px, 3fr) minmax(150px, 3fr) 0.75fr 0.5fr;
grid-template-areas: 'nonce type info date status';
}

.columnTemplateTxHistoryShort {
grid-template-columns: minmax(50px, 0.25fr) 6fr 0.75fr 0.5fr;
grid-template-areas: 'nonce type date status';
}

.columnWrap {
white-space: normal;
}
Expand All @@ -43,7 +44,8 @@
gap: var(--space-1);
}

.columnTemplate {
.columnTemplate,
.columnTemplateShort {
grid-template-columns: repeat(12, auto);
grid-template-areas:
'nonce type type type type type type type type type type type'
Expand All @@ -54,18 +56,8 @@
'empty actions actions actions actions actions actions actions actions actions actions actions';
}

.columnTemplateWithoutNonce {
grid-template-columns: repeat(12, 1fr);
grid-template-areas:
'nonce type type type type type type type type type type type'
'empty info info info info info info info info info info info'
'empty date date date date date date date date date date date'
'empty confirmations confirmations confirmations confirmations confirmations confirmations confirmations confirmations confirmations confirmations confirmations'
'empty status status status status status status status status status status status'
'empty actions actions actions actions actions actions actions actions actions actions actions';
}

.columnTemplateTxHistory {
.columnTemplateTxHistory,
.columnTemplateTxHistoryShort {
grid-template-columns: repeat(12, 1fr);
grid-template-areas:
'nonce type type type type type type type type type type type'
Expand Down
12 changes: 11 additions & 1 deletion src/components/transactions/TxType/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useTransactionType } from '@/hooks/useTransactionType'
import type { TransactionSummary } from '@safe-global/safe-gateway-typescript-sdk'
import { TransactionInfoType, TransferDirection } from '@safe-global/safe-gateway-typescript-sdk'
import { Box } from '@mui/material'
import css from './styles.module.css'
import SafeAppIconCard from '@/components/safe-apps/SafeAppIconCard'
import { HumanDescription, TransferDescription } from '@/components/transactions/HumanDescription'

type TxTypeProps = {
tx: TransactionSummary
Expand All @@ -11,6 +13,8 @@ type TxTypeProps = {
const TxType = ({ tx }: TxTypeProps) => {
const type = useTransactionType(tx)

const humanDescription = tx.txInfo.richDecodedInfo?.fragments

return (
<Box className={css.txType}>
<SafeAppIconCard
Expand All @@ -20,7 +24,13 @@ const TxType = ({ tx }: TxTypeProps) => {
height={16}
fallback="/images/transactions/custom.svg"
/>
{type.text}
{humanDescription ? (
<HumanDescription fragments={humanDescription} />
) : tx.txInfo.type === TransactionInfoType.TRANSFER ? (
<TransferDescription isSendTx={tx.txInfo.direction === TransferDirection.OUTGOING} txInfo={tx.txInfo} />
) : (
type.text
)}
</Box>
)
}
Expand Down
1 change: 1 addition & 0 deletions src/components/transactions/TxType/styles.module.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.txType {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-1);
color: var(--color-text-primary);
}
Loading

0 comments on commit b9a5842

Please sign in to comment.