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/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/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/components/AppFrame.tsx b/src/routes/safe/components/Apps/components/AppFrame.tsx index aebff2ab..62097c63 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' @@ -354,7 +354,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 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 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 31ae589f..a2e25269 100644 --- a/src/routes/safe/components/Apps/utils.ts +++ b/src/routes/safe/components/Apps/utils.ts @@ -26,7 +26,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], }, 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' && ( 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/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/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/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/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/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"