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: public transaction notes #4693

Merged
merged 20 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
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
26 changes: 26 additions & 0 deletions apps/web/src/components/transactions/TxDetails/TxNote.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Tooltip, Typography } from '@mui/material'
import type { TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk'
import InfoIcon from '@/public/images/notifications/info.svg'

const TxNote = ({ txDetails }: { txDetails: TransactionDetails | undefined }) => {
const note = (txDetails as TransactionDetails & { note: string | null })?.note

return note ? (
<div>
<Typography variant="h5" display="flex" alignItems="center" justifyItems="center">
Note
<Tooltip title="This note is left by the transaction creator." arrow>
<Typography color="text.secondary" component="span" height="1em">
<InfoIcon height="1em" />
katspaugh marked this conversation as resolved.
Show resolved Hide resolved
</Typography>
</Tooltip>
</Typography>

<Typography p={2} mt={1} borderRadius={1} bgcolor="background.main">
{note}
</Typography>
</div>
) : null
}
katspaugh marked this conversation as resolved.
Show resolved Hide resolved

export default TxNote
5 changes: 5 additions & 0 deletions apps/web/src/components/transactions/TxDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { FEATURES } from '@/utils/chains'
import { useGetTransactionDetailsQuery } from '@/store/api/gateway'
import { asError } from '@/services/exceptions/utils'
import { POLLING_INTERVAL } from '@/config/constants'
import TxNote from './TxNote'

export const NOT_AVAILABLE = 'n/a'

Expand Down Expand Up @@ -82,6 +83,10 @@ const TxDetailsBlock = ({ txSummary, txDetails }: TxDetailsProps): ReactElement
<>
{/* /Details */}
<div className={`${css.details} ${isUnsigned ? css.noSigners : ''}`}>
<div className={css.txNote}>
<TxNote txDetails={txDetails} />
</div>

<div className={css.shareLink}>
<TxShareLink id={txSummary.id} />
</div>
Expand Down
17 changes: 14 additions & 3 deletions apps/web/src/components/transactions/TxDetails/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,20 @@
}

.shareLink {
position: absolute;
right: 16px;
top: 16px;
display: flex;
justify-content: flex-end;
margin: var(--space-1);
margin-bottom: -40px;
}

.txNote {
margin: var(--space-1) 0;
padding: 0 var(--space-2) var(--space-2);
border-bottom: 1px solid var(--color-border-light);
}

.txNote:empty {
display: none;
}

.loading,
Expand Down
80 changes: 52 additions & 28 deletions apps/web/src/components/tx/SignOrExecuteForm/SignOrExecuteForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ import ConfirmationView from '../confirmation-views'
import { SignerForm } from './SignerForm'
import { useSigner } from '@/hooks/wallets/useWallet'
import { trackTxEvents } from './tracking'
import TxNoteForm from './TxNoteForm'
import TxNote from '@/components/transactions/TxDetails/TxNote'
import { encodeTxNote } from '@/utils/transactions'

export type SubmitCallback = (txId: string, isExecuted?: boolean) => void

Expand Down Expand Up @@ -64,6 +67,7 @@ export const SignOrExecuteForm = ({
isCreation?: boolean
txDetails?: TransactionDetails
}): ReactElement => {
const [customOrigin, setCustomOrigin] = useState<string | undefined>(props.origin)
const { transactionExecution } = useAppSelector(selectSettings)
const [shouldExecute, setShouldExecute] = useState<boolean>(transactionExecution)
const isNewExecutableTx = useImmediatelyExecutable() && isCreation
Expand Down Expand Up @@ -108,10 +112,10 @@ export const SignOrExecuteForm = ({
isRoleExecution,
isProposerCreation,
!!signer?.isSafe,
props.origin,
customOrigin,
)
},
[chainId, isCreation, onSubmit, trigger, signer?.isSafe, props.origin],
[chainId, isCreation, onSubmit, trigger, signer?.isSafe, customOrigin],
)

const onRoleExecutionSubmit = useCallback<typeof onFormSubmit>(
Expand All @@ -124,6 +128,49 @@ export const SignOrExecuteForm = ({
[onFormSubmit],
)

const onNoteSubmit = useCallback(
(note: string) => {
setCustomOrigin(encodeTxNote(note, props.origin))
},
[setCustomOrigin, props.origin],
)

const getForm = () => {
const commonProps = {
...props,
safeTx,
isCreation,
origin: customOrigin,
onSubmit: onFormSubmit,
}
if (isCounterfactualSafe && !isProposing) {
return <CounterfactualForm {...commonProps} onlyExecute />
}

if (!isCounterfactualSafe && willExecute && !isProposing) {
return <ExecuteForm {...commonProps} />
}

if (!isCounterfactualSafe && willExecuteThroughRole) {
return (
<ExecuteThroughRoleForm
{...commonProps}
role={(allowingRole || mostLikelyRole)!}
safeTxError={safeTxError}
onSubmit={onRoleExecutionSubmit}
/>
)
}

if (!isCounterfactualSafe && !willExecute && !willExecuteThroughRole && !isProposing) {
return <SignForm {...commonProps} isBatchable={isBatchable} />
}

clovisdasilvaneto marked this conversation as resolved.
Show resolved Hide resolved
if (isProposing) {
return <ProposerForm {...commonProps} onSubmit={onProposerFormSubmit} />
}
}

return (
<>
<TxCard>
Expand All @@ -149,6 +196,8 @@ export const SignOrExecuteForm = ({

{!isCounterfactualSafe && !props.isRejection && <TxChecks />}

<TxCard>{isCreation ? <TxNoteForm onSubmit={onNoteSubmit} /> : <TxNote txDetails={props.txDetails} />}</TxCard>

<SignerForm willExecute={willExecute} />

<TxCard>
Expand Down Expand Up @@ -179,32 +228,7 @@ export const SignOrExecuteForm = ({

<Blockaid />

{isCounterfactualSafe && !isProposing && (
<CounterfactualForm {...props} safeTx={safeTx} isCreation={isCreation} onSubmit={onFormSubmit} onlyExecute />
)}
{!isCounterfactualSafe && willExecute && !isProposing && (
<ExecuteForm {...props} safeTx={safeTx} isCreation={isCreation} onSubmit={onFormSubmit} />
)}
{!isCounterfactualSafe && willExecuteThroughRole && (
<ExecuteThroughRoleForm
{...props}
safeTx={safeTx}
safeTxError={safeTxError}
onSubmit={onRoleExecutionSubmit}
role={(allowingRole || mostLikelyRole)!}
/>
)}
{!isCounterfactualSafe && !willExecute && !willExecuteThroughRole && !isProposing && (
<SignForm
{...props}
safeTx={safeTx}
isBatchable={isBatchable}
isCreation={isCreation}
onSubmit={onFormSubmit}
/>
)}

{isProposing && <ProposerForm {...props} safeTx={safeTx} onSubmit={onProposerFormSubmit} />}
{getForm()}
</TxCard>
</>
)
Expand Down
58 changes: 58 additions & 0 deletions apps/web/src/components/tx/SignOrExecuteForm/TxNoteForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useCallback, useState } from 'react'
import { InputAdornment, Stack, TextField, Typography } from '@mui/material'
import InfoIcon from '@/public/images/notifications/info.svg'

const MAX_NOTE_LENGTH = 100

const TxNoteForm = ({ onSubmit }: { onSubmit: (note: string) => void }) => {
const [note, setNote] = useState('')

const onInput = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setNote(e.target.value)
}, [])

const onChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
onSubmit(e.target.value.slice(0, MAX_NOTE_LENGTH))
},
[onSubmit],
)

return (
<>
<Stack direction="row" alignItems="center" gap={1}>
<Typography variant="h5">What does this transaction do?</Typography>
<Typography variant="body2" color="text.secondary">
Optional
</Typography>
</Stack>

<TextField
name="note"
label="Add note"
fullWidth
slotProps={{
htmlInput: { maxLength: MAX_NOTE_LENGTH },
input: {
endAdornment: (
<InputAdornment position="end">
<Typography variant="caption" mt={3}>
{note.length}/{MAX_NOTE_LENGTH}
</Typography>
</InputAdornment>
),
},
}}
onInput={onInput}
onChange={onChange}
/>

<Typography variant="caption" color="text.secondary" display="flex" alignItems="center">
<InfoIcon height="1.2em" />
This note will be publicly visible and accessible to anyone.
</Typography>
</>
)
}

export default TxNoteForm
Loading
Loading