diff --git a/package.json b/package.json index 825b8173..bddbfe6c 100644 --- a/package.json +++ b/package.json @@ -161,7 +161,7 @@ "@gnosis.pm/safe-apps-sdk": "1.0.3", "@gnosis.pm/safe-apps-sdk-v1": "npm:@gnosis.pm/safe-apps-sdk@0.4.2", "@gnosis.pm/safe-contracts": "1.1.1-dev.2", - "@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#f610327", + "@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#80f5db6", "@gnosis.pm/util-contracts": "2.0.6", "@ledgerhq/hw-transport-node-hid-singleton": "5.45.0", "@material-ui/core": "^4.11.0", diff --git a/src/components/AppLayout/Header/assets/gnosis-safe-multisig-logo.svg b/src/components/AppLayout/Header/assets/gnosis-safe-multisig-logo.svg index 26f71351..62ed0fd9 100644 --- a/src/components/AppLayout/Header/assets/gnosis-safe-multisig-logo.svg +++ b/src/components/AppLayout/Header/assets/gnosis-safe-multisig-logo.svg @@ -1,6 +1,5 @@ - - - - - + + horizontal_left_small_black + + diff --git a/src/components/AppLayout/Header/components/Layout.tsx b/src/components/AppLayout/Header/components/Layout.tsx index 5845013f..85051334 100644 --- a/src/components/AppLayout/Header/components/Layout.tsx +++ b/src/components/AppLayout/Header/components/Layout.tsx @@ -38,7 +38,7 @@ const styles = () => ({ zIndex: 1301, }, logo: { - flexBasis: '114px', + flexBasis: '140px', flexShrink: '0', flexGrow: '0', maxWidth: '55px', diff --git a/src/components/DecodeTxs/index.tsx b/src/components/DecodeTxs/index.tsx new file mode 100644 index 00000000..2b49ed61 --- /dev/null +++ b/src/components/DecodeTxs/index.tsx @@ -0,0 +1,200 @@ +import React, { ReactElement } from 'react' +import styled from 'styled-components' +import { Transaction } from '@gnosis.pm/safe-apps-sdk-v1' +import { Text, EthHashInfo, CopyToClipboardBtn, IconText, FixedIcon } from '@gnosis.pm/safe-react-components' +import get from 'lodash.get' + +import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3' +import { getExplorerInfo } from 'src/config' +import { DecodedData, DecodedDataBasicParameter, DecodedDataParameterValue } from 'src/types/transactions/decode.d' +import { DecodedTxDetail } from 'src/routes/safe/components/Apps/components/ConfirmTxModal' + +const FlexWrapper = styled.div<{ margin: number }>` + display: flex; + align-items: center; + + > :nth-child(2) { + margin-left: ${({ margin }) => margin}px; + } +` + +const BasicTxInfoWrapper = styled.div` + margin-bottom: 15px; + + > :nth-child(2) { + margin-bottom: 15px; + } +` + +const TxList = styled.div` + width: 100%; + max-height: 260px; + overflow-y: auto; + border-top: 2px solid ${({ theme }) => theme.colors.separator}; +` + +const TxListItem = styled.div` + display: flex; + justify-content: space-between; + + padding: 0 24px; + height: 50px; + border-bottom: 2px solid ${({ theme }) => theme.colors.separator}; + + :hover { + cursor: pointer; + } +` +const ElementWrapper = styled.div` + margin-bottom: 15px; +` + +export const BasicTxInfo = ({ + txRecipient, + txData, + txValue, +}: { + txRecipient: string + txData: string + txValue: string +}): ReactElement => { + return ( + + {/* TO */} + <> + + {`Send ${txValue} ETH to:`} + + + + <> + {/* Data */} + + Data (hex encoded): + + + {web3.utils.hexToBytes(txData).length} bytes + + + + + ) +} + +export const getParameterElement = (parameter: DecodedDataBasicParameter, index: number): ReactElement => { + let valueElement + + if (parameter.type === 'address') { + valueElement = ( + + ) + } + + if (parameter.type.startsWith('bytes')) { + valueElement = ( + + {web3.utils.hexToBytes(parameter.value).length} bytes + + + ) + } + + if (!valueElement) { + let value = parameter.value + if (parameter.type.endsWith('[]')) { + try { + value = JSON.stringify(parameter.value) + } catch (e) {} + } + valueElement = {value} + } + + return ( + + + {parameter.name} ({parameter.type}) + + {valueElement} + + ) +} + +const SingleTx = ({ + decodedData, + onTxItemClick, +}: { + decodedData: DecodedData | null + onTxItemClick: (decodedTxDetails: DecodedData) => void +}): ReactElement | null => { + if (!decodedData) { + return null + } + + return ( + + onTxItemClick(decodedData)}> + + + + {decodedData.method} + + + + + ) +} + +const MultiSendTx = ({ + decodedData, + onTxItemClick, +}: { + decodedData: DecodedData | null + onTxItemClick: (decodedTxDetails: DecodedDataParameterValue) => void +}): ReactElement | null => { + const txs: DecodedDataParameterValue[] | undefined = get(decodedData, 'parameters[0].valueDecoded') + + if (!txs) { + return null + } + + return ( + + {txs.map((tx, index) => ( + onTxItemClick(tx)}> + + + + {tx.dataDecoded && {tx.dataDecoded.method}} + + + + ))} + + ) +} + +type Props = { + txs: Transaction[] + decodedData: DecodedData | null + onTxItemClick: (decodedTxDetails: DecodedTxDetail) => void +} + +export const DecodeTxs = ({ txs, decodedData, onTxItemClick }: Props): ReactElement => { + return txs.length > 1 ? ( + + ) : ( + + ) +} diff --git a/src/components/ModalTitle/index.tsx b/src/components/ModalTitle/index.tsx index f2af9051..4b2a7f64 100644 --- a/src/components/ModalTitle/index.tsx +++ b/src/components/ModalTitle/index.tsx @@ -2,6 +2,7 @@ import React from 'react' import styled from 'styled-components' import IconButton from '@material-ui/core/IconButton' import Close from '@material-ui/icons/Close' +import { Icon } from '@gnosis.pm/safe-react-components' import Paragraph from 'src/components/layout/Paragraph' import { md, lg } from 'src/theme/variables' @@ -33,18 +34,28 @@ const StyledClose = styled(Close)` width: 35px; ` -const ModalTitle = ({ - iconUrl, - title, - onClose, -}: { +const GoBackWrapper = styled.div` + margin-right: 15px; +` + +type Props = { title: string - iconUrl: string + goBack?: () => void + iconUrl?: string onClose?: () => void -}): React.ReactElement => { +} + +const ModalTitle = ({ goBack, iconUrl, title, onClose }: Props): React.ReactElement => { return ( + {goBack && ( + + + + + + )} {iconUrl && } {title} diff --git a/src/components/forms/validator.ts b/src/components/forms/validator.ts index 271c27ba..09496afa 100644 --- a/src/components/forms/validator.ts +++ b/src/components/forms/validator.ts @@ -80,9 +80,7 @@ export const mustBeEthereumContractAddress = memoize( async (address: string): Promise => { const contractCode = await getWeb3().eth.getCode(address) - const errorMessage = `Input must be a valid Ethereum contract address${ - isFeatureEnabled(FEATURES.DOMAIN_LOOKUP) ? ', ENS or Unstoppable domain' : '' - }` + const errorMessage = `Must resolve to a valid smart contract address.` return !contractCode || contractCode.replace('0x', '').replace(/0/g, '') === '' ? errorMessage : undefined }, diff --git a/src/logic/hooks/useEstimateTransactionGas.tsx b/src/logic/hooks/useEstimateTransactionGas.tsx index 8cd5e268..567d7659 100644 --- a/src/logic/hooks/useEstimateTransactionGas.tsx +++ b/src/logic/hooks/useEstimateTransactionGas.tsx @@ -218,10 +218,9 @@ export const useEstimateTransactionGas = ({ ) const fixedGasCosts = getFixedGasCosts(Number(threshold)) + const isOffChainSignature = checkIfOffChainSignatureIsPossible(isExecution, smartContractWallet, safeVersion) try { - const isOffChainSignature = checkIfOffChainSignatureIsPossible(isExecution, smartContractWallet, safeVersion) - const gasEstimation = await estimateTransactionGas({ safeAddress, txRecipient, @@ -279,7 +278,7 @@ export const useEstimateTransactionGas = ({ gasLimit: '0', isExecution, isCreation, - isOffChainSignature: false, + isOffChainSignature, }) } } diff --git a/src/routes/safe/components/AddressBook/style.ts b/src/routes/safe/components/AddressBook/style.ts index f0e6c980..a8314a31 100644 --- a/src/routes/safe/components/AddressBook/style.ts +++ b/src/routes/safe/components/AddressBook/style.ts @@ -14,7 +14,7 @@ export const styles = createStyles({ }, hide: { '&:hover': { - backgroundColor: '#fff3e2', + backgroundColor: '#f7f5f5', }, '&:hover $actions': { visibility: 'initial', diff --git a/src/routes/safe/components/Apps/communicator.ts b/src/routes/safe/components/Apps/communicator.ts index 8422d3d7..00155bec 100644 --- a/src/routes/safe/components/Apps/communicator.ts +++ b/src/routes/safe/components/Apps/communicator.ts @@ -15,12 +15,12 @@ type MessageHandler = ( ) => void | MethodToResponse[Methods] | ErrorResponse | Promise class AppCommunicator { - private iframe: HTMLIFrameElement + private iframeRef: MutableRefObject private handlers = new Map() private app: SafeApp - constructor(iframeRef: MutableRefObject, app: SafeApp) { - this.iframe = iframeRef.current + constructor(iframeRef: MutableRefObject, app: SafeApp) { + this.iframeRef = iframeRef this.app = app window.addEventListener('message', this.handleIncomingMessage) @@ -49,7 +49,7 @@ class AppCommunicator { ? MessageFormatter.makeErrorResponse(requestId, data, sdkVersion) : MessageFormatter.makeResponse(requestId, data, sdkVersion) - this.iframe.contentWindow?.postMessage(msg, this.app.url) + this.iframeRef.current?.contentWindow?.postMessage(msg, this.app.url) } handleIncomingMessage = async (msg: SDKMessageEvent): Promise => { @@ -83,7 +83,6 @@ const useAppCommunicator = ( app?: SafeApp, ): AppCommunicator | undefined => { const [communicator, setCommunicator] = useState(undefined) - useEffect(() => { let communicatorInstance const initCommunicator = (iframeRef: MutableRefObject, app: SafeApp) => { @@ -91,7 +90,7 @@ const useAppCommunicator = ( setCommunicator(communicatorInstance) } - if (app && iframeRef.current !== null) { + if (app) { initCommunicator(iframeRef as MutableRefObject, app) } diff --git a/src/routes/safe/components/Apps/components/AppFrame.tsx b/src/routes/safe/components/Apps/components/AppFrame.tsx index aebff2ab..02d41f29 100644 --- a/src/routes/safe/components/Apps/components/AppFrame.tsx +++ b/src/routes/safe/components/Apps/components/AppFrame.tsx @@ -32,7 +32,7 @@ import { LoadingContainer } from 'src/components/LoaderContainer/index' import { TIMEOUT } from 'src/utils/constants' import { web3ReadOnly } from 'src/logic/wallets/getWeb3' -import { ConfirmTransactionModal } from '../components/ConfirmTransactionModal' +import { ConfirmTxModal } from '../components/ConfirmTxModal' import { useIframeMessageHandler } from '../hooks/useIframeMessageHandler' import { useLegalConsent } from '../hooks/useLegalConsent' import LegalDisclaimer from './LegalDisclaimer' @@ -56,6 +56,7 @@ const AppWrapper = styled.div` const StyledCard = styled(Card)` flex-grow: 1; + padding: 0; ` const StyledIframe = styled.iframe` @@ -354,7 +355,7 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => { /> )} - { - if (!['string', 'number'].includes(typeof t.value)) { - return false - } - - if (typeof t.value === 'string' && !/^(0x)?[0-9a-f]+$/i.test(t.value)) { - return false - } - - const isAddressValid = mustBeEthereumAddress(t.to) === undefined - return isAddressValid && !!t.data && typeof t.data === 'string' -} - -const Wrapper = styled.div` - margin-bottom: 15px; -` -const CollapseContent = styled.div` - padding: 15px 0; - - .section { - margin-bottom: 15px; - } - - .value-section { - display: flex; - align-items: center; - } -` - -const IconText = styled.div` - display: flex; - align-items: center; - - span { - margin-right: 4px; - } -` -const StyledTextBox = styled(TextBox)` - max-width: 444px; -` - -const Container = styled.div` - max-width: 480px; - padding: ${md} ${lg}; -` - -const ModalFooter = styled(Row)` - padding: ${md} ${lg}; - justify-content: center; -` -const TransactionFeesWrapper = styled.div` - background-color: ${({ theme }) => theme.colors.background}; - padding: ${sm} ${lg}; -` - -type OwnProps = { - isOpen: boolean - app: SafeApp - txs: Transaction[] - params?: TransactionParams - safeAddress: string - safeName: string - ethBalance: string - onUserConfirm: (safeTxHash: string) => void - onTxReject: () => void - onClose: () => void -} - -const { nativeCoin } = getNetworkInfo() - -const parseTxValue = (value: string | number): string => { - return web3ReadOnly.utils.toBN(value).toString() -} - -export const ConfirmTransactionModal = ({ - isOpen, - app, - txs, - safeAddress, - ethBalance, - safeName, - params, - onUserConfirm, - onClose, - onTxReject, -}: OwnProps): React.ReactElement | null => { - const [estimatedSafeTxGas, setEstimatedSafeTxGas] = useState(0) - const threshold = useSelector(safeThresholdSelector) || 1 - - const txRecipient: string | undefined = useMemo(() => (txs.length > 1 ? MULTI_SEND_ADDRESS : txs[0]?.to), [txs]) - const txData: string | undefined = useMemo(() => (txs.length > 1 ? encodeMultiSendCall(txs) : txs[0]?.data), [txs]) - const txValue: string | undefined = useMemo( - () => (txs.length > 1 ? '0' : txs[0]?.value && parseTxValue(txs[0]?.value)), - [txs], - ) - const operation = useMemo(() => (txs.length > 1 ? DELEGATE_CALL : CALL), [txs]) - const [manualSafeTxGas, setManualSafeTxGas] = useState(0) - const [manualGasPrice, setManualGasPrice] = useState() - - const { - gasLimit, - gasPriceFormatted, - gasEstimation, - isOffChainSignature, - isCreation, - isExecution, - gasCostFormatted, - txEstimationExecutionStatus, - } = useEstimateTransactionGas({ - txData: txData || '', - txRecipient, - operation, - txAmount: txValue, - safeTxGas: manualSafeTxGas, - manualGasPrice, - }) - - useEffect(() => { - if (params?.safeTxGas) { - setEstimatedSafeTxGas(gasEstimation) - } - }, [params, gasEstimation]) - - const dispatch = useDispatch() - if (!isOpen) { - return null - } - - const handleTxRejection = () => { - onTxReject() - onClose() - } - - const handleUserConfirmation = (safeTxHash: string): void => { - onUserConfirm(safeTxHash) - onClose() - } - - const getParametersStatus = () => (threshold > 1 ? 'ETH_DISABLED' : 'ENABLED') - - const confirmTransactions = async (txParameters: TxParameters) => { - await dispatch( - createTransaction( - { - safeAddress, - to: txRecipient, - valueInWei: txValue, - txData, - operation, - origin: app.id, - navigateToTransactionsTab: false, - txNonce: txParameters.safeNonce, - safeTxGas: txParameters.safeTxGas - ? Number(txParameters.safeTxGas) - : Math.max(params?.safeTxGas || 0, estimatedSafeTxGas), - ethParameters: txParameters, - notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, - }, - handleUserConfirmation, - handleTxRejection, - ), - ) - } - - 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 (newSafeTxGas && oldSafeTxGas !== newSafeTxGas) { - setManualSafeTxGas(newSafeTxGas) - } - } - - const areTxsMalformed = txs.some((t) => !isTxValid(t)) - - const body = areTxsMalformed - ? () => ( - <> - - - Transaction error - - - This Safe App initiated a transaction which cannot be processed. Please get in touch with the developer of - this Safe App for more information. - - - ) - : (txParameters, toggleEditMode) => { - return ( - <> - - - - {txs.map((tx, index) => ( - - } title={`Transaction ${index + 1}`}> - -
- Value -
- Ether - - {fromTokenUnit(tx.value, nativeCoin.decimals)} {nativeCoin.name} - -
-
-
- Data (hex encoded)* - {tx.data} -
- - - - ))} - - {params?.safeTxGas && ( -
- SafeTxGas - {params?.safeTxGas} - -
- )} - - {/* Tx Parameters */} - - - {txEstimationExecutionStatus === EstimationStatus.LOADING ? null : ( - - - - )} - - ) - } - - return ( - - - {(txParameters, toggleEditMode) => ( - <> - - - - - {body(txParameters, toggleEditMode)} - - - confirmTransactions(txParameters)} - okDisabled={areTxsMalformed} - okText="Submit" - /> - - - )} - - - ) -} diff --git a/src/routes/safe/components/Apps/components/ConfirmTxModal/DecodedTxDetail.tsx b/src/routes/safe/components/Apps/components/ConfirmTxModal/DecodedTxDetail.tsx new file mode 100644 index 00000000..8bf9bb6a --- /dev/null +++ b/src/routes/safe/components/Apps/components/ConfirmTxModal/DecodedTxDetail.tsx @@ -0,0 +1,62 @@ +import React, { ReactElement } from 'react' +import styled from 'styled-components' + +import { getNetworkInfo } from 'src/config' +import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' +import { md, lg } from 'src/theme/variables' +import ModalTitle from 'src/components/ModalTitle' +import Hairline from 'src/components/layout/Hairline' +import { DecodedDataParameterValue, DecodedData } from 'src/types/transactions/decode.d' +import { BasicTxInfo, getParameterElement } from 'src/components/DecodeTxs' + +const { nativeCoin } = getNetworkInfo() + +const Container = styled.div` + max-width: 480px; + padding: ${md} ${lg}; + word-break: break-word; +` + +function isDataDecodedParameterValue(arg: any): arg is DecodedDataParameterValue { + return arg.operation !== undefined +} + +type Props = { + hideDecodedTxData: () => void + onClose: () => void + decodedTxData: DecodedDataParameterValue | DecodedData +} + +export const DecodedTxDetail = ({ hideDecodedTxData, onClose, decodedTxData: tx }: Props): ReactElement => { + let body + // If we are dealing with a multiSend + // decodedTxData is of type DataDecodedParameter + if (isDataDecodedParameterValue(tx)) { + const txValue = fromTokenUnit(tx.value, nativeCoin.decimals) + + body = ( + <> + + {tx.dataDecoded?.parameters.map((p, index) => getParameterElement(p, index))} + + ) + } else { + // If we are dealing with a single tx + // decodedTxData is of type DecodedData + body = <>{tx.parameters.map((p, index) => getParameterElement(p, index))} + } + + return ( + <> + + + + + {body} + + ) +} diff --git a/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx b/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx new file mode 100644 index 00000000..0a775471 --- /dev/null +++ b/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx @@ -0,0 +1,260 @@ +import React, { useEffect, useMemo, useState } from 'react' +import { ModalFooterConfirmation } from '@gnosis.pm/safe-react-components' +import styled from 'styled-components' +import { useDispatch } from 'react-redux' + +import DividerLine from 'src/components/DividerLine' +import TextBox from 'src/components/TextBox' +import ModalTitle from 'src/components/ModalTitle' +import Hairline from 'src/components/layout/Hairline' +import Heading from 'src/components/layout/Heading' +import { createTransaction } from 'src/logic/safe/store/actions/createTransaction' +import { MULTI_SEND_ADDRESS } from 'src/logic/contracts/safeContracts' +import { DELEGATE_CALL, TX_NOTIFICATION_TYPES, CALL } from 'src/logic/safe/transactions' +import { encodeMultiSendCall } from 'src/logic/safe/transactions/multisend' +import { getNetworkInfo } from 'src/config' +import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' +import { TransactionFees } from 'src/components/TransactionsFees' +import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters' +import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail' +import { md, lg, sm } from 'src/theme/variables' +import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' +import AddressInfo from 'src/components/AddressInfo' +import { DecodeTxs, BasicTxInfo } from 'src/components/DecodeTxs' +import { fetchTxDecoder } from 'src/utils/decodeTx' +import { DecodedData } from 'src/types/transactions/decode.d' +import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' + +import GasEstimationInfo from '../GasEstimationInfo' +import { ConfirmTxModalProps, DecodedTxDetail } from '.' + +const { nativeCoin } = getNetworkInfo() + +const StyledTextBox = styled(TextBox)` + max-width: 444px; +` + +const Container = styled.div` + max-width: 480px; + padding: ${md} ${lg} 0; +` +const TransactionFeesWrapper = styled.div` + background-color: ${({ theme }) => theme.colors.background}; + padding: ${sm} ${lg}; + margin-bottom: 15px; +` + +const FooterWrapper = styled.div` + margin-bottom: 15px; +` + +const DecodeTxsWrapper = styled.div` + margin: 24px -24px; +` + +type Props = ConfirmTxModalProps & { + areTxsMalformed: boolean + showDecodedTxData: (decodedTxDetails: DecodedTxDetail) => void + hidden: boolean // used to prevent re-rendering the modal each time a tx is inspected +} + +export const ReviewConfirm = ({ + app, + txs, + safeAddress, + ethBalance, + safeName, + params, + hidden, + onUserConfirm, + onClose, + onTxReject, + areTxsMalformed, + showDecodedTxData, +}: Props): React.ReactElement => { + const [estimatedSafeTxGas, setEstimatedSafeTxGas] = useState(0) + const isMultiSend = txs.length > 1 + const [decodedData, setDecodedData] = useState(null) + const dispatch = useDispatch() + + const txRecipient: string | undefined = useMemo(() => (isMultiSend ? MULTI_SEND_ADDRESS : txs[0]?.to), [ + txs, + isMultiSend, + ]) + const txData: string | undefined = useMemo(() => (isMultiSend ? encodeMultiSendCall(txs) : txs[0]?.data), [ + txs, + isMultiSend, + ]) + const txValue: string | undefined = useMemo( + () => (isMultiSend ? '0' : txs[0]?.value && fromTokenUnit(txs[0]?.value, nativeCoin.decimals)), + [txs, isMultiSend], + ) + + const operation = useMemo(() => (isMultiSend ? DELEGATE_CALL : CALL), [isMultiSend]) + const [manualSafeTxGas, setManualSafeTxGas] = useState(0) + const [manualGasPrice, setManualGasPrice] = useState() + + const { + gasLimit, + gasPriceFormatted, + gasEstimation, + isOffChainSignature, + isCreation, + isExecution, + gasCostFormatted, + txEstimationExecutionStatus, + } = useEstimateTransactionGas({ + txData: txData || '', + txRecipient, + operation, + txAmount: txValue, + safeTxGas: manualSafeTxGas, + manualGasPrice, + }) + + useEffect(() => { + if (params?.safeTxGas) { + setEstimatedSafeTxGas(gasEstimation) + } + }, [params, gasEstimation]) + + // Decode tx data. + useEffect(() => { + const decodeTxData = async () => { + const res = await fetchTxDecoder(txData) + setDecodedData(res) + } + + decodeTxData() + }, [txData]) + + const handleTxRejection = () => { + onTxReject() + onClose() + } + + const handleUserConfirmation = (safeTxHash: string): void => { + onUserConfirm(safeTxHash) + onClose() + } + + const confirmTransactions = async (txParameters: TxParameters) => { + await dispatch( + createTransaction( + { + safeAddress, + to: txRecipient, + valueInWei: txValue, + txData, + operation, + origin: app.id, + navigateToTransactionsTab: false, + txNonce: txParameters.safeNonce, + safeTxGas: txParameters.safeTxGas + ? Number(txParameters.safeTxGas) + : Math.max(params?.safeTxGas || 0, estimatedSafeTxGas), + ethParameters: txParameters, + notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, + }, + handleUserConfirmation, + handleTxRejection, + ), + ) + } + + 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 (newSafeTxGas && oldSafeTxGas !== newSafeTxGas) { + setManualSafeTxGas(newSafeTxGas) + } + } + + return ( + + {(txParameters, toggleEditMode) => ( + + )} + + ) +} diff --git a/src/routes/safe/components/Apps/components/ConfirmTxModal/SafeAppLoadError.tsx b/src/routes/safe/components/Apps/components/ConfirmTxModal/SafeAppLoadError.tsx new file mode 100644 index 00000000..feddb412 --- /dev/null +++ b/src/routes/safe/components/Apps/components/ConfirmTxModal/SafeAppLoadError.tsx @@ -0,0 +1,47 @@ +import React, { ReactElement } from 'react' +import { Icon, Text, Title, ModalFooterConfirmation } from '@gnosis.pm/safe-react-components' +import styled from 'styled-components' +import { ConfirmTxModalProps } from '.' + +const IconText = styled.div` + display: flex; + align-items: center; + + span { + margin-right: 4px; + } +` + +const FooterWrapper = styled.div` + margin-top: 15px; +` + +export const SafeAppLoadError = ({ onTxReject, onClose }: ConfirmTxModalProps): ReactElement => { + const handleTxRejection = () => { + onTxReject() + onClose() + } + + return ( + <> + + + Transaction error + + + This Safe App initiated a transaction which cannot be processed. Please get in touch with the developer of this + Safe App for more information. + + + + handleTxRejection()} + handleOk={() => {}} + okDisabled={true} + okText="Submit" + /> + + + ) +} diff --git a/src/routes/safe/components/Apps/components/ConfirmTxModal/index.tsx b/src/routes/safe/components/Apps/components/ConfirmTxModal/index.tsx new file mode 100644 index 00000000..ebadeded --- /dev/null +++ b/src/routes/safe/components/Apps/components/ConfirmTxModal/index.tsx @@ -0,0 +1,72 @@ +import React, { ReactElement, useState } from 'react' +import { Transaction } from '@gnosis.pm/safe-apps-sdk-v1' + +import Modal from 'src/components/Modal' +import { SafeApp } from 'src/routes/safe/components/Apps/types.d' +import { TransactionParams } from 'src/routes/safe/components/Apps/components/AppFrame' +import { mustBeEthereumAddress } from 'src/components/forms/validator' +import { SafeAppLoadError } from './SafeAppLoadError' +import { ReviewConfirm } from './ReviewConfirm' +import { DecodedDataParameterValue, DecodedData } from 'src/types/transactions/decode' +import { DecodedTxDetail } from './DecodedTxDetail' + +export type ConfirmTxModalProps = { + isOpen: boolean + app: SafeApp + txs: Transaction[] + params?: TransactionParams + safeAddress: string + safeName: string + ethBalance: string + onUserConfirm: (safeTxHash: string) => void + onTxReject: () => void + onClose: () => void +} + +const isTxValid = (t: Transaction): boolean => { + if (!['string', 'number'].includes(typeof t.value)) { + return false + } + + if (typeof t.value === 'string' && !/^(0x)?[0-9a-f]+$/i.test(t.value)) { + return false + } + + const isAddressValid = mustBeEthereumAddress(t.to) === undefined + return isAddressValid && !!t.data && typeof t.data === 'string' +} + +export type DecodedTxDetail = DecodedDataParameterValue | DecodedData | undefined + +export const ConfirmTxModal = (props: ConfirmTxModalProps): ReactElement | null => { + const [decodedTxDetails, setDecodedTxDetails] = useState() + const areTxsMalformed = props.txs.some((t) => !isTxValid(t)) + + const showDecodedTxData = setDecodedTxDetails + const hideDecodedTxData = () => setDecodedTxDetails(undefined) + + const closeDecodedTxDetail = () => { + hideDecodedTxData() + props.onClose() + } + + return ( + + {areTxsMalformed && } + {decodedTxDetails && ( + + )} + + + ) +} diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts index 6d8a8c7d..52ca818f 100644 --- a/src/routes/safe/components/Apps/utils.ts +++ b/src/routes/safe/components/Apps/utils.ts @@ -3,7 +3,6 @@ import memoize from 'lodash.memoize' import { SafeApp, SAFE_APP_FETCH_STATUS } from './types.d' -import { getGnosisSafeAppsUrl } from 'src/config' import { getContentFromENS } from 'src/logic/wallets/getWeb3' import appsIconSvg from 'src/assets/icons/apps.svg' import { ETHEREUM_NETWORK } from 'src/config/networks/network.d' @@ -17,7 +16,6 @@ const removeLastTrailingSlash = (url) => { return url } -const gnosisAppsUrl = removeLastTrailingSlash(getGnosisSafeAppsUrl()) export type StaticAppInfo = { url: string disabled: boolean @@ -26,7 +24,7 @@ export type StaticAppInfo = { export const staticAppsList: Array = [ // 1inch { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmRWtuktjfU6WMAEJFgzBC4cUfqp3FF5uN9QoWb55SdGG5`, + url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmUXF1yVGdqUfMbhNyfM3jpP6Bw66cYnKPoWq6iHkhd3Aw`, disabled: false, networks: [ETHEREUM_NETWORK.MAINNET], }, @@ -56,7 +54,11 @@ export const staticAppsList: Array = [ networks: [ETHEREUM_NETWORK.RINKEBY, ETHEREUM_NETWORK.XDAI], }, // Compound - { url: `${gnosisAppsUrl}/compound`, disabled: false, networks: [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY] }, + { + url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmX31xCdhFDmJzoVG33Y6kJtJ5Ujw8r5EJJBrsp8Fbjm7k`, + disabled: false, + networks: [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY], + }, // dHedge { url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmaiemnumMaaK9wE1pbMfm3YSBUpcFNgDh3Bf6VZCZq57Q`, @@ -65,7 +67,7 @@ export const staticAppsList: Array = [ }, // Idle { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmVkGHm6gfQumJhnRfFCh7m2oSYwLXb51EKHzChpcV9J3N`, + url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmTvrLwJtyjG8QFHgvqdPhcV5DBMQ7oZceSU4uvPw9vQaj`, disabled: false, networks: [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY], }, @@ -131,7 +133,7 @@ export const staticAppsList: Array = [ }, // Wallet-Connect { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmRMGTA5ARMwfhYbdmK83zzMd13NnEUKFJSZEgEjKa8YQm`, + url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmU1pT35yPXxpnABcH3pZ1MxFeyYVtftT5RKhWopQmZHQV`, disabled: false, networks: [ ETHEREUM_NETWORK.MAINNET, diff --git a/src/routes/safe/components/Balances/Coins/styles.ts b/src/routes/safe/components/Balances/Coins/styles.ts index 3c10059d..ae4ae6ac 100644 --- a/src/routes/safe/components/Balances/Coins/styles.ts +++ b/src/routes/safe/components/Balances/Coins/styles.ts @@ -12,7 +12,7 @@ export const styles = createStyles({ }, hide: { '&:hover': { - backgroundColor: '#fff3e2', + backgroundColor: '#f7f5f5', }, '&:hover $actions': { visibility: 'initial', diff --git a/src/routes/safe/components/Balances/SendModal/index.tsx b/src/routes/safe/components/Balances/SendModal/index.tsx index 68a4e5ea..7623d9fa 100644 --- a/src/routes/safe/components/Balances/SendModal/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/index.tsx @@ -133,6 +133,7 @@ const SendModal = ({ {activeScreen === 'sendFunds' && (
diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ReviewCustomTx/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ReviewCustomTx/index.tsx index d7e4e736..0a39b8dc 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ReviewCustomTx/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ReviewCustomTx/index.tsx @@ -94,7 +94,13 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => { } return ( - + {(txParameters, toggleEditMode) => ( <> @@ -168,6 +174,7 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => { onEdit={toggleEditMode} isTransactionCreation={isCreation} isTransactionExecution={isExecution} + isOffChainSignature={isOffChainSignature} /> {txEstimationExecutionStatus === EstimationStatus.LOADING ? null : ( diff --git a/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx index f46d897c..ea68ea2a 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx @@ -140,6 +140,8 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement = return (
diff --git a/src/routes/safe/components/Balances/SendModal/screens/ReviewSendFundsTx/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ReviewSendFundsTx/index.tsx index 21e0075c..1e2ab83c 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ReviewSendFundsTx/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ReviewSendFundsTx/index.tsx @@ -178,6 +178,8 @@ const ReviewSendFundsTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactE return ( - - {/* Disclaimer */} + + {/* Disclaimer */} {txEstimationExecutionStatus !== EstimationStatus.LOADING && (
void onReview: (txInfo: unknown) => void recipientAddress?: string @@ -80,6 +81,7 @@ const InputAdornmentChildSymbol = ({ symbol }: { symbol?: string }): ReactElemen } const SendFunds = ({ + initialValues, onClose, onReview, recipientAddress, @@ -93,12 +95,14 @@ const SendFunds = ({ const defaultEntry = { address: recipientAddress || '', name: '' } // if there's nothing to lookup for, we return the default entry - if (!recipientAddress) { + if (!initialValues?.recipientAddress && !recipientAddress) { return defaultEntry } + // if there's something to lookup for, `initialValues` has precedence over `recipientAddress` + const predefinedAddress = initialValues?.recipientAddress ?? recipientAddress const addressBookEntry = addressBook.find(({ address }) => { - return sameAddress(recipientAddress, address) + return sameAddress(predefinedAddress, address) }) // if found in the Address Book, then we return the entry @@ -170,7 +174,11 @@ const SendFunds = ({ diff --git a/src/routes/safe/components/Settings/Advanced/RemoveModuleModal.tsx b/src/routes/safe/components/Settings/Advanced/RemoveModuleModal.tsx index ad66f1b1..042f47d3 100644 --- a/src/routes/safe/components/Settings/Advanced/RemoveModuleModal.tsx +++ b/src/routes/safe/components/Settings/Advanced/RemoveModuleModal.tsx @@ -127,6 +127,8 @@ export const RemoveModuleModal = ({ onClose, selectedModulePair }: RemoveModuleM open > diff --git a/src/routes/safe/components/Settings/Advanced/style.ts b/src/routes/safe/components/Settings/Advanced/style.ts index 272453bb..69925f4f 100644 --- a/src/routes/safe/components/Settings/Advanced/style.ts +++ b/src/routes/safe/components/Settings/Advanced/style.ts @@ -8,7 +8,7 @@ export const styles = createStyles({ }, hide: { '&:hover': { - backgroundColor: '#fff3e2', + backgroundColor: '#f7f5f5', }, '&:hover $actions': { visibility: 'initial', diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx index dd53427a..1acbd6ef 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx @@ -101,6 +101,8 @@ export const ReviewAddOwner = ({ onClickBack, onClose, onSubmit, values }: Revie return ( diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/style.ts b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/style.ts index 1a94ae9b..78644036 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/style.ts +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/style.ts @@ -64,7 +64,7 @@ export const styles = createStyles({ selectedOwner: { padding: sm, alignItems: 'center', - backgroundColor: '#fff3e2', + backgroundColor: '#f7f5f5', }, user: { justifyContent: 'left', diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx index c69aba52..7d77084c 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx @@ -123,6 +123,8 @@ export const ReviewRemoveOwnerModal = ({ return ( {txEstimationExecutionStatus === EstimationStatus.LOADING ? null : ( diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.tsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.tsx index fa47290b..52327599 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.tsx @@ -120,6 +120,8 @@ export const ReviewReplaceOwnerModal = ({ return ( diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/style.ts b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/style.ts index 0cf8e781..ffa58763 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/style.ts +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/style.ts @@ -69,7 +69,7 @@ export const styles = createStyles({ selectedOwnerAdded: { padding: sm, alignItems: 'center', - backgroundColor: '#fff3e2', + backgroundColor: '#f7f5f5', }, user: { justifyContent: 'left', diff --git a/src/routes/safe/components/Settings/ManageOwners/style.ts b/src/routes/safe/components/Settings/ManageOwners/style.ts index 957a20d0..0e9c4109 100644 --- a/src/routes/safe/components/Settings/ManageOwners/style.ts +++ b/src/routes/safe/components/Settings/ManageOwners/style.ts @@ -14,7 +14,7 @@ export const styles = createStyles({ }, hide: { '&:hover': { - backgroundColor: '#fff3e2', + backgroundColor: '#f7f5f5', }, '&:hover $actions': { visibility: 'initial', diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx index 9ef63bd4..9d803ded 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx @@ -233,6 +233,8 @@ export const ReviewSpendingLimits = ({ onBack, onClose, txToken, values }: Revie return (
diff --git a/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx b/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx index 0c5a5090..6ad0ec6e 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx @@ -116,6 +116,8 @@ export const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendin description="Remove the selected Spending Limit" > diff --git a/src/routes/safe/components/Settings/SpendingLimit/style.ts b/src/routes/safe/components/Settings/SpendingLimit/style.ts index 41a54264..0b8a9568 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/style.ts +++ b/src/routes/safe/components/Settings/SpendingLimit/style.ts @@ -21,7 +21,7 @@ export const useStyles = makeStyles( }, hide: { '&:hover': { - backgroundColor: '#fff3e2', + backgroundColor: '#f7f5f5', }, '&:hover $actions': { visibility: 'initial', diff --git a/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx b/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx index 8f6a7455..0560b57a 100644 --- a/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx +++ b/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx @@ -84,8 +84,6 @@ export const ChangeThresholdModal = ({ } }, [safeAddress, editedThreshold]) - const getParametersStatus = () => (threshold > 1 ? 'ETH_DISABLED' : 'ENABLED') - const handleSubmit = async ({ txParameters }) => { await dispatch( createTransaction({ @@ -120,6 +118,8 @@ export const ChangeThresholdModal = ({ return ( {txEstimationExecutionStatus !== EstimationStatus.LOADING && ( diff --git a/src/routes/safe/components/Settings/UpdateSafeModal/index.tsx b/src/routes/safe/components/Settings/UpdateSafeModal/index.tsx index aea5f0ce..b56ae3d0 100644 --- a/src/routes/safe/components/Settings/UpdateSafeModal/index.tsx +++ b/src/routes/safe/components/Settings/UpdateSafeModal/index.tsx @@ -76,7 +76,13 @@ export const UpdateSafeModal = ({ onClose, safeAddress }: Props): React.ReactEle }) return ( - + {(txParameters, toggleEditMode) => ( <> @@ -116,6 +122,7 @@ export const UpdateSafeModal = ({ onClose, safeAddress }: Props): React.ReactEle compact={false} isTransactionCreation={isCreation} isTransactionExecution={isExecution} + isOffChainSignature={isOffChainSignature} /> {txEstimationExecutionStatus === EstimationStatus.LOADING ? null : ( diff --git a/src/routes/safe/components/Transactions/TxList/modals/ApproveTxModal.tsx b/src/routes/safe/components/Transactions/TxList/modals/ApproveTxModal.tsx index 687dea96..c8b24649 100644 --- a/src/routes/safe/components/Transactions/TxList/modals/ApproveTxModal.tsx +++ b/src/routes/safe/components/Transactions/TxList/modals/ApproveTxModal.tsx @@ -317,6 +317,8 @@ export const ApproveTxModal = ({ return ( )} diff --git a/src/routes/safe/components/Transactions/TxList/modals/RejectTxModal.tsx b/src/routes/safe/components/Transactions/TxList/modals/RejectTxModal.tsx index 48280b8e..8ebf6186 100644 --- a/src/routes/safe/components/Transactions/TxList/modals/RejectTxModal.tsx +++ b/src/routes/safe/components/Transactions/TxList/modals/RejectTxModal.tsx @@ -82,6 +82,8 @@ export const RejectTxModal = ({ isOpen, onClose, gwTransaction }: Props): React. return ( diff --git a/src/routes/safe/components/Transactions/helpers/EditTxParametersForm/index.tsx b/src/routes/safe/components/Transactions/helpers/EditTxParametersForm/index.tsx index 436df795..a497eb2b 100644 --- a/src/routes/safe/components/Transactions/helpers/EditTxParametersForm/index.tsx +++ b/src/routes/safe/components/Transactions/helpers/EditTxParametersForm/index.tsx @@ -15,11 +15,11 @@ import GnoForm from 'src/components/forms/GnoForm' import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' import { composeValidators, minValue } from 'src/components/forms/validator' -import { ParametersStatus, areSafeParamsEnabled, areEthereumParamsEnabled } from '../utils' +import { ParametersStatus, areSafeParamsEnabled, areEthereumParamsVisible, ethereumTxParametersTitle } from '../utils' import { getNetworkInfo } from 'src/config' const StyledDivider = styled(Divider)` - margin: 0px; + margin: 16px 0; ` const SafeOptions = styled.div` @@ -39,7 +39,7 @@ const EthereumOptions = styled.div` } ` const StyledLink = styled(Link)` - margin: 16px 0; + margin: 16px 0 0 0; display: inline-flex; align-items: center; @@ -65,6 +65,7 @@ interface Props { txParameters: TxParameters onClose: (txParameters?: TxParameters) => void parametersStatus: ParametersStatus + isExecution: boolean } const formValidation = (values) => { @@ -101,6 +102,7 @@ export const EditTxParametersForm = ({ onClose, txParameters, parametersStatus = 'ENABLED', + isExecution, }: Props): React.ReactElement => { const classes = useStyles() const { safeNonce, safeTxGas, ethNonce, ethGasLimit, ethGasPrice } = txParameters @@ -142,7 +144,7 @@ export const EditTxParametersForm = ({ {() => ( <> - Safe transactions parameters + Safe transaction @@ -168,49 +170,53 @@ export const EditTxParametersForm = ({ /> - - Ethereum transactions parameters - + {areEthereumParamsVisible(parametersStatus) && ( + <> + + {ethereumTxParametersTitle(isExecution)} + - - - - - + + + + + - - - How can I configure the gas price manually? - - - + + + How can I configure these parameters manually? + + + + + )} diff --git a/src/routes/safe/components/Transactions/helpers/EditableTxParameters.tsx b/src/routes/safe/components/Transactions/helpers/EditableTxParameters.tsx index a4a29ada..fb9eac57 100644 --- a/src/routes/safe/components/Transactions/helpers/EditableTxParameters.tsx +++ b/src/routes/safe/components/Transactions/helpers/EditableTxParameters.tsx @@ -7,6 +7,8 @@ import { safeThresholdSelector } from 'src/logic/safe/store/selectors' type Props = { children: (txParameters: TxParameters, toggleStatus: (txParameters?: TxParameters) => void) => any + isOffChainSignature: boolean + isExecution: boolean parametersStatus?: ParametersStatus ethGasLimit?: TxParameters['ethGasLimit'] ethGasPrice?: TxParameters['ethGasPrice'] @@ -17,6 +19,8 @@ type Props = { export const EditableTxParameters = ({ children, + isOffChainSignature, + isExecution, parametersStatus, ethGasLimit, ethGasPrice, @@ -27,7 +31,7 @@ export const EditableTxParameters = ({ const [isEditMode, toggleEditMode] = useState(false) const [useManualValues, setUseManualValues] = useState(false) const threshold = useSelector(safeThresholdSelector) || 1 - const defaultParameterStatus = threshold > 1 ? 'ETH_DISABLED' : 'ENABLED' + const defaultParameterStatus = isOffChainSignature && threshold > 1 ? 'ETH_HIDDEN' : 'ENABLED' const txParameters = useTransactionParameters({ parameterStatus: parametersStatus || defaultParameterStatus, initialEthGasLimit: ethGasLimit, @@ -65,6 +69,7 @@ export const EditableTxParameters = ({ return isEditMode ? ( void compact?: boolean parametersStatus?: ParametersStatus - isTransactionExecution: boolean isTransactionCreation: boolean + isTransactionExecution: boolean + isOffChainSignature: boolean } export const TxParametersDetail = ({ @@ -46,11 +47,12 @@ export const TxParametersDetail = ({ parametersStatus, isTransactionCreation, isTransactionExecution, + isOffChainSignature, }: Props): ReactElement | null => { const threshold = useSelector(safeThresholdSelector) || 1 - const defaultParameterStatus = threshold > 1 ? 'ETH_DISABLED' : 'ENABLED' + const defaultParameterStatus = isOffChainSignature && threshold > 1 ? 'ETH_HIDDEN' : 'ENABLED' - if (!isTransactionExecution && !isTransactionCreation) { + if (!isTransactionExecution && !isTransactionCreation && isOffChainSignature) { return null } @@ -62,7 +64,7 @@ export const TxParametersDetail = ({ - Safe transactions parameters + Safe transaction @@ -95,57 +97,30 @@ export const TxParametersDetail = ({ - - - Ethereum transaction parameters - - + {areEthereumParamsVisible(parametersStatus || defaultParameterStatus) && ( + <> + + + {ethereumTxParametersTitle(isTransactionExecution)} + + - - - Ethereum nonce - - - {txParameters.ethNonce} - - + + Nonce + {txParameters.ethNonce} + - - - Ethereum gas limit - - - {txParameters.ethGasLimit} - - - - - - Ethereum gas price - - - {txParameters.ethGasPrice} - - + + Gas limit + {txParameters.ethGasLimit} + + + Gas price + {txParameters.ethGasPrice} + + + )} Edit diff --git a/src/routes/safe/components/Transactions/helpers/utils.ts b/src/routes/safe/components/Transactions/helpers/utils.ts index ce1564e6..ccb3742f 100644 --- a/src/routes/safe/components/Transactions/helpers/utils.ts +++ b/src/routes/safe/components/Transactions/helpers/utils.ts @@ -1,8 +1,8 @@ -export type ParametersStatus = 'ENABLED' | 'DISABLED' | 'SAFE_DISABLED' | 'ETH_DISABLED' | 'CANCEL_TRANSACTION' +export type ParametersStatus = 'ENABLED' | 'DISABLED' | 'SAFE_DISABLED' | 'ETH_HIDDEN' | 'CANCEL_TRANSACTION' -export const areEthereumParamsEnabled = (parametersStatus: ParametersStatus): boolean => { +export const areEthereumParamsVisible = (parametersStatus: ParametersStatus): boolean => { return ( - parametersStatus === 'ENABLED' || (parametersStatus !== 'ETH_DISABLED' && parametersStatus !== 'CANCEL_TRANSACTION') + parametersStatus === 'ENABLED' || (parametersStatus !== 'ETH_HIDDEN' && parametersStatus !== 'CANCEL_TRANSACTION') ) } @@ -12,3 +12,7 @@ export const areSafeParamsEnabled = (parametersStatus: ParametersStatus): boolea (parametersStatus !== 'SAFE_DISABLED' && parametersStatus !== 'CANCEL_TRANSACTION') ) } + +export const ethereumTxParametersTitle = (isExecution: boolean): string => { + return `Owner transaction ${isExecution ? '(Execution)' : '(On-chain approval)'}` +} diff --git a/src/theme/mui.ts b/src/theme/mui.ts index 54562d69..4d793d54 100644 --- a/src/theme/mui.ts +++ b/src/theme/mui.ts @@ -455,7 +455,7 @@ export const DropdownListTheme = { }, button: { '&:hover': { - backgroundColor: '#fff3e2', + backgroundColor: '#f7f5f5', }, }, }, diff --git a/src/types/transactions/decode.d.ts b/src/types/transactions/decode.d.ts new file mode 100644 index 00000000..8beb92e7 --- /dev/null +++ b/src/types/transactions/decode.d.ts @@ -0,0 +1,24 @@ +export type DecodedDataBasicParameter = { + name: string + type: string + value: string +} +export type DecodedDataParameterValue = { + operation: 0 | 1 + to: string + value: string + data: string + dataDecoded: { + method: string + parameters: DecodedDataBasicParameter[] + } | null +} + +export type DecodedDataParameter = { + valueDecoded?: DecodedDataParameterValue[] +} & DecodedDataBasicParameter + +export type DecodedData = { + method: string + parameters: DecodedDataParameter[] +} diff --git a/src/utils/decodeTx.ts b/src/utils/decodeTx.ts new file mode 100644 index 00000000..1ab6f3c0 --- /dev/null +++ b/src/utils/decodeTx.ts @@ -0,0 +1,18 @@ +import axios from 'axios' + +import { getTxServiceUrl } from 'src/config' +import { DecodedData } from 'src/types/transactions/decode.d' + +export const fetchTxDecoder = async (txData: string): Promise => { + if (!txData?.length || txData === '0x') { + return null + } + + const url = `${getTxServiceUrl()}/data-decoder/` + try { + const res = await axios.post(url, { data: txData }) + return res.data + } catch (error) { + return null + } +} diff --git a/yarn.lock b/yarn.lock index 71abef68..00a9ec29 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1596,9 +1596,9 @@ solc "0.5.14" truffle "^5.1.21" -"@gnosis.pm/safe-react-components@https://github.com/gnosis/safe-react-components.git#f610327": +"@gnosis.pm/safe-react-components@https://github.com/gnosis/safe-react-components.git#80f5db6": version "0.5.0" - resolved "https://github.com/gnosis/safe-react-components.git#f610327c109810547513079196514b05cda63844" + resolved "https://github.com/gnosis/safe-react-components.git#80f5db672d417ea410d58c8d713e46e16e3c7e7f" dependencies: classnames "^2.2.6" react-media "^1.10.0"