Skip to content
This repository was archived by the owner on Nov 10, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions src/logic/exceptions/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ enum ErrorCodes {
_803 = '803: Error creating a transaction',
_804 = '804: Error processing a transaction',
_805 = '805: TX monitor error',
_806 = '806: Failed to remove module',
_807 = '806: Failed to remove guard',
_900 = '900: Error loading Safe App',
_901 = '901: Error processing Safe Apps SDK request',
_902 = '902: Error loading Safe Apps list',
Expand Down
2 changes: 2 additions & 0 deletions src/logic/safe/store/actions/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ describe('extractRemoteSafeInfo', () => {
threshold: 2,
currentVersion: '1.3.0',
needsUpdate: false,
guard: undefined,
featuresEnabled: [FEATURES.ERC721, FEATURES.ERC1155, FEATURES.SAFE_APPS, FEATURES.CONTRACT_INTERACTION],
}

Expand All @@ -205,6 +206,7 @@ describe('extractRemoteSafeInfo', () => {
threshold: 2,
currentVersion: '1.3.0',
needsUpdate: false,
guard: '0x4f8a82d73729A33E0165aDeF3450A7F85f007528',
featuresEnabled: [FEATURES.ERC721, FEATURES.ERC1155, FEATURES.SAFE_APPS, FEATURES.CONTRACT_INTERACTION],
}

Expand Down
3 changes: 3 additions & 0 deletions src/logic/safe/store/actions/mocks/safeInformation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export const remoteSafeInfoWithModules = {
logoUrl:
'https://safe-transaction-assets.staging.gnosisdev.com/contracts/logos/0x3E5c63644E683549055b9Be8653de26E0B4CD36E.png',
},
guard: {
value: '0x4f8a82d73729A33E0165aDeF3450A7F85f007528',
},
modules: [
{
value: '0xCFbFaC74C26F8647cBDb8c5caf80BB5b32E43134',
Expand Down
1 change: 1 addition & 0 deletions src/logic/safe/store/actions/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export const extractRemoteSafeInfo = async (remoteSafeInfo: SafeInfo): Promise<P
safeInfo.currentVersion = remoteSafeInfo.version
safeInfo.needsUpdate = safeNeedsUpdate(safeInfo.currentVersion, LATEST_SAFE_VERSION)
safeInfo.featuresEnabled = enabledFeatures(safeInfo.currentVersion)
safeInfo.guard = remoteSafeInfo.guard ? remoteSafeInfo.guard.value : undefined

return safeInfo
}
Expand Down
2 changes: 2 additions & 0 deletions src/logic/safe/store/models/safe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export type SafeRecordProps = {
needsUpdate: boolean
featuresEnabled: Array<FEATURES>
loadedViaUrl: boolean
guard: string
}

const makeSafe = Record<SafeRecordProps>({
Expand All @@ -54,6 +55,7 @@ const makeSafe = Record<SafeRecordProps>({
needsUpdate: false,
featuresEnabled: [],
loadedViaUrl: true,
guard: '',
})

export type SafeRecord = RecordOf<SafeRecordProps>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ const getMockedOldSafe = ({
nonce,
modules,
spendingLimits,
guard,
}: Partial<SafeRecordProps>): SafeRecordProps => {
const owner1 = '0x3bE3c2dE077FBC409ae50AFFA66a94a9aE669A8d'
const owner2 = '0xA2366b0c2607de70777d87aCdD1D22F0708fA6a3'
const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1'
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
const mockedGuardAddress = '0x4f8a82d73729A33E0165aDeF3450A7F85f007528'

return {
address: address || '0xAE173F30ec9A293d37c44BA68d3fCD35F989Ce9F',
Expand All @@ -38,6 +40,7 @@ const getMockedOldSafe = ({
featuresEnabled: featuresEnabled || [],
totalFiatBalance: '110',
loadedViaUrl: false,
guard: guard || mockedGuardAddress,
}
}

Expand Down
12 changes: 12 additions & 0 deletions src/logic/safe/utils/guardManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'

export const getSetGuardTxData = (guardAddress: string, safeAddress: string, safeVersion: string): string => {
const safeInstance = getGnosisSafeInstanceAt(safeAddress, safeVersion)

return safeInstance.methods.setGuard(guardAddress).encodeABI()
}

export const getRemoveGuardTxData = (safeAddress: string, safeVersion: string): string => {
return getSetGuardTxData(ZERO_ADDRESS, safeAddress, safeVersion)
}
191 changes: 191 additions & 0 deletions src/routes/safe/components/Settings/Advanced/RemoveGuardModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
import IconButton from '@material-ui/core/IconButton'
import Close from '@material-ui/icons/Close'
import cn from 'classnames'
import React, { ReactElement, useMemo, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'

import Block from 'src/components/layout/Block'
import Col from 'src/components/layout/Col'
import Hairline from 'src/components/layout/Hairline'
import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import Modal, { ButtonStatus, Modal as GenericModal } from 'src/components/Modal'
import { getExplorerInfo } from 'src/config'
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'

import { currentSafe } from 'src/logic/safe/store/selectors'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'

import { useStyles } from './style'
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
import { useEstimationStatus } from 'src/logic/hooks/useEstimationStatus'
import { TransactionFees } from 'src/components/TransactionsFees'
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
import { getRemoveGuardTxData } from 'src/logic/safe/utils/guardManager'
import { Errors, logError } from 'src/logic/exceptions/CodedException'

interface RemoveGuardModalProps {
onClose: () => void
guardAddress: string
}

export const RemoveGuardModal = ({ onClose, guardAddress }: RemoveGuardModalProps): ReactElement => {
const classes = useStyles()

const { address: safeAddress, currentVersion: safeVersion } = useSelector(currentSafe)
const dispatch = useDispatch()
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
const [manualGasLimit, setManualGasLimit] = useState<string | undefined>()

const txData = useMemo(() => getRemoveGuardTxData(safeAddress, safeVersion), [safeAddress, safeVersion])

const {
gasCostFormatted,
txEstimationExecutionStatus,
isExecution,
isOffChainSignature,
isCreation,
gasLimit,
gasEstimation,
gasPriceFormatted,
} = useEstimateTransactionGas({
txData,
txRecipient: safeAddress,
txAmount: '0',
safeTxGas: manualSafeTxGas,
manualGasPrice,
manualGasLimit,
})

const [buttonStatus] = useEstimationStatus(txEstimationExecutionStatus)

const removeTransactionGuard = async (txParameters: TxParameters): Promise<void> => {
try {
dispatch(
createTransaction({
safeAddress,
to: safeAddress,
valueInWei: '0',
txData,
txNonce: txParameters.safeNonce,
safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined,
ethParameters: txParameters,
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
}),
)
} catch (e) {
logError(Errors._807, `${guardAddress} – ${e.message}`)
}
}

const closeEditModalCallback = (txParameters: TxParameters) => {
const oldGasPrice = Number(gasPriceFormatted)
const newGasPrice = Number(txParameters.ethGasPrice)
const oldSafeTxGas = Number(gasEstimation)
const newSafeTxGas = Number(txParameters.safeTxGas)

if (newGasPrice && oldGasPrice !== newGasPrice) {
setManualGasPrice(txParameters.ethGasPrice)
}

if (txParameters.ethGasLimit && gasLimit !== txParameters.ethGasLimit) {
setManualGasLimit(txParameters.ethGasLimit)
}

if (newSafeTxGas && oldSafeTxGas !== newSafeTxGas) {
setManualSafeTxGas(newSafeTxGas)
}
}

let confirmButtonText = 'Remove'
if (ButtonStatus.LOADING === buttonStatus) {
confirmButtonText = txEstimationExecutionStatus === EstimationStatus.LOADING ? 'Estimating' : 'Removing'
}

return (
<Modal
description="Remove the selected Transaction Guard"
handleClose={onClose}
paperClassName="modal"
title="Remove Transaction Guard"
open
>
<EditableTxParameters
isOffChainSignature={isOffChainSignature}
isExecution={isExecution}
ethGasLimit={gasLimit}
ethGasPrice={gasPriceFormatted}
safeTxGas={gasEstimation.toString()}
closeEditModalCallback={closeEditModalCallback}
>
{(txParameters, toggleEditMode) => {
return (
<>
<Row align="center" className={classes.modalHeading} grow>
<Paragraph className={classes.modalManage} noMargin weight="bolder">
Remove Transaction Guard
</Paragraph>
<IconButton disableRipple onClick={onClose}>
<Close className={classes.modalClose} />
</IconButton>
</Row>
<Hairline />
<Block>
<Row className={classes.modalOwner}>
<Col align="center" xs={1}>
<EthHashInfo
hash={guardAddress}
showCopyBtn
showAvatar
explorerUrl={getExplorerInfo(guardAddress)}
/>
</Col>
</Row>
<Row className={classes.modalDescription}>
<Paragraph noMargin size="lg">
Once the transaction guard has been removed, checks by the transaction guard will not be conducted
before or after any subsequent transactions.
</Paragraph>
</Row>
</Block>
<Block className={classes.accordionContainer}>
{/* Tx Parameters */}
<TxParametersDetail
txParameters={txParameters}
onEdit={toggleEditMode}
isTransactionCreation={isCreation}
isTransactionExecution={isExecution}
isOffChainSignature={isOffChainSignature}
/>
</Block>
<Row className={cn(classes.modalDescription, classes.gasCostsContainer)}>
<TransactionFees
gasCostFormatted={gasCostFormatted}
isExecution={isExecution}
isCreation={isCreation}
isOffChainSignature={isOffChainSignature}
txEstimationExecutionStatus={txEstimationExecutionStatus}
/>
</Row>
<GenericModal.Footer withoutBorder={buttonStatus !== ButtonStatus.LOADING}>
<GenericModal.Footer.Buttons
cancelButtonProps={{ onClick: onClose }}
confirmButtonProps={{
color: 'error',
onClick: () => removeTransactionGuard(txParameters),
status: buttonStatus,
text: confirmButtonText,
}}
/>
</GenericModal.Footer>
</>
)
}}
</EditableTxParameters>
</Modal>
)
Copy link
Member

Choose a reason for hiding this comment

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

That's a lot of code duplication from the Review modal. For this PR it's fine but we should definitely think of a way to reuse the same code for tx popups.

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { currentSafe } from 'src/logic/safe/store/selectors'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'

import { useStyles } from './style'
import { Errors, logError } from 'src/logic/exceptions/CodedException'
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
import { useEstimationStatus } from 'src/logic/hooks/useEstimationStatus'
import { TransactionFees } from 'src/components/TransactionsFees'
Expand All @@ -35,7 +36,7 @@ interface RemoveModuleModalProps {
export const RemoveModuleModal = ({ onClose, selectedModulePair }: RemoveModuleModalProps): ReactElement => {
const classes = useStyles()

const { address: safeAddress = '', currentVersion: safeVersion = '' } = useSelector(currentSafe) ?? {}
const { address: safeAddress, currentVersion: safeVersion } = useSelector(currentSafe)
const [txData, setTxData] = useState('')
const dispatch = useDispatch()
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
Expand Down Expand Up @@ -84,7 +85,7 @@ export const RemoveModuleModal = ({ onClose, selectedModulePair }: RemoveModuleM
}),
)
} catch (e) {
console.error(`failed to remove the module ${selectedModulePair}`, e.message)
logError(Errors._806, `${selectedModulePair} – ${e.message}`)
}
}

Expand Down
82 changes: 82 additions & 0 deletions src/routes/safe/components/Settings/Advanced/TransactionGuard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Icon, EthHashInfo } from '@gnosis.pm/safe-react-components'
import TableContainer from '@material-ui/core/TableContainer'
import cn from 'classnames'
import React from 'react'
import { useSelector } from 'react-redux'

import { generateColumns } from './dataFetcher'
import { RemoveGuardModal } from './RemoveGuardModal'
import { useStyles } from './style'

import ButtonHelper from 'src/components/ButtonHelper'
import { grantedSelector } from 'src/routes/safe/container/selector'
import Table from 'src/components/Table'
import { TableCell, TableRow } from 'src/components/layout/Table'
import Block from 'src/components/layout/Block'
import Row from 'src/components/layout/Row'
import { getExplorerInfo } from 'src/config'

const REMOVE_GUARD_BTN_TEST_ID = 'remove-guard-btn'
const GUARDS_ROW_TEST_ID = 'guards-row'

interface TransactionGuardProps {
address: string
}

export const TransactionGuard = ({ address }: TransactionGuardProps): React.ReactElement => {
const classes = useStyles()

const columns = generateColumns()
const autoColumns = columns.filter(({ custom }) => !custom)

const granted = useSelector(grantedSelector)

const [viewRemoveGuardModal, setViewRemoveGuardModal] = React.useState(false)
const hideRemoveGuardModal = () => setViewRemoveGuardModal(false)

const triggerRemoveSelectedGuard = (): void => {
setViewRemoveGuardModal(true)
}

return (
<>
<TableContainer>
<Table columns={columns} data={[address]} defaultFixed disablePagination label="Modules" noBorder>
Copy link
Contributor

Choose a reason for hiding this comment

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

I know it was written on the ticket, but we don't really need a table there. It makes things complicated. There's only one transaction guard per safe, so it will always use 1 row.

This is not required, but you can think about how we can remove it if you want.

{(sortedData) =>
sortedData.map((row, index) => (
<TableRow
className={cn(classes.hide, index >= 3 && index === sortedData.size - 1 && classes.noBorderBottom)}
data-testid={GUARDS_ROW_TEST_ID}
key={index}
tabIndex={-1}
>
{autoColumns.map((column, index) => {
const columnId = column.id
return (
<React.Fragment key={`${columnId}-${index}`}>
<TableCell align={column.align} component="td" key={columnId}>
<Block justify="left">
<EthHashInfo hash={row} showCopyBtn showAvatar explorerUrl={getExplorerInfo(row)} />
</Block>
</TableCell>
<TableCell component="td">
<Row align="end" className={classes.actions}>
{granted && (
<ButtonHelper onClick={triggerRemoveSelectedGuard} data-testid={REMOVE_GUARD_BTN_TEST_ID}>
<Icon size="sm" type="delete" color="error" tooltip="Remove module" />
</ButtonHelper>
)}
</Row>
</TableCell>
</React.Fragment>
)
})}
</TableRow>
))
}
</Table>
</TableContainer>
{viewRemoveGuardModal && address && <RemoveGuardModal onClose={hideRemoveGuardModal} guardAddress={address} />}
</>
)
}
Loading