Merge branch 'development' of github.com:gnosis/safe-react into development

This commit is contained in:
Mati Dastugue 2021-03-29 09:53:30 -03:00
commit f38fc95258
46 changed files with 895 additions and 485 deletions

View File

@ -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",

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -38,7 +38,7 @@ const styles = () => ({
zIndex: 1301,
},
logo: {
flexBasis: '114px',
flexBasis: '140px',
flexShrink: '0',
flexGrow: '0',
maxWidth: '55px',

View File

@ -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 (
<BasicTxInfoWrapper>
{/* TO */}
<>
<Text size="lg" strong>
{`Send ${txValue} ETH to:`}
</Text>
<EthHashInfo
hash={txRecipient}
showIdenticon
textSize="lg"
showCopyBtn
explorerUrl={getExplorerInfo(txRecipient)}
/>
</>
<>
{/* Data */}
<Text size="lg" strong>
Data (hex encoded):
</Text>
<FlexWrapper margin={5}>
<Text size="lg">{web3.utils.hexToBytes(txData).length} bytes</Text>
<CopyToClipboardBtn textToCopy={txData} />
</FlexWrapper>
</>
</BasicTxInfoWrapper>
)
}
export const getParameterElement = (parameter: DecodedDataBasicParameter, index: number): ReactElement => {
let valueElement
if (parameter.type === 'address') {
valueElement = (
<EthHashInfo
hash={parameter.value}
showIdenticon
textSize="lg"
showCopyBtn
explorerUrl={getExplorerInfo(parameter.value)}
/>
)
}
if (parameter.type.startsWith('bytes')) {
valueElement = (
<FlexWrapper margin={5}>
<Text size="lg">{web3.utils.hexToBytes(parameter.value).length} bytes</Text>
<CopyToClipboardBtn textToCopy={parameter.value} />
</FlexWrapper>
)
}
if (!valueElement) {
let value = parameter.value
if (parameter.type.endsWith('[]')) {
try {
value = JSON.stringify(parameter.value)
} catch (e) {}
}
valueElement = <Text size="lg">{value}</Text>
}
return (
<ElementWrapper key={index}>
<Text size="lg" strong>
{parameter.name} ({parameter.type})
</Text>
{valueElement}
</ElementWrapper>
)
}
const SingleTx = ({
decodedData,
onTxItemClick,
}: {
decodedData: DecodedData | null
onTxItemClick: (decodedTxDetails: DecodedData) => void
}): ReactElement | null => {
if (!decodedData) {
return null
}
return (
<TxList>
<TxListItem onClick={() => onTxItemClick(decodedData)}>
<IconText iconSize="sm" iconType="code" text="Contract interaction" textSize="xl" />
<FlexWrapper margin={20}>
<Text size="xl">{decodedData.method}</Text>
<FixedIcon type="chevronRight" />
</FlexWrapper>
</TxListItem>
</TxList>
)
}
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 (
<TxList>
{txs.map((tx, index) => (
<TxListItem key={index} onClick={() => onTxItemClick(tx)}>
<IconText iconSize="sm" iconType="code" text="Contract interaction" textSize="xl" />
<FlexWrapper margin={20}>
{tx.dataDecoded && <Text size="xl">{tx.dataDecoded.method}</Text>}
<FixedIcon type="chevronRight" />
</FlexWrapper>
</TxListItem>
))}
</TxList>
)
}
type Props = {
txs: Transaction[]
decodedData: DecodedData | null
onTxItemClick: (decodedTxDetails: DecodedTxDetail) => void
}
export const DecodeTxs = ({ txs, decodedData, onTxItemClick }: Props): ReactElement => {
return txs.length > 1 ? (
<MultiSendTx decodedData={decodedData} onTxItemClick={onTxItemClick} />
) : (
<SingleTx decodedData={decodedData} onTxItemClick={onTxItemClick} />
)
}

View File

@ -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 (
<StyledRow align="center" grow>
<TitleWrapper>
{goBack && (
<GoBackWrapper>
<IconButton onClick={goBack}>
<Icon type="arrowLeft" size="md" />
</IconButton>
</GoBackWrapper>
)}
{iconUrl && <IconImg alt={title} src={iconUrl} />}
<StyledParagraph noMargin weight="bolder">
{title}

View File

@ -80,9 +80,7 @@ export const mustBeEthereumContractAddress = memoize(
async (address: string): Promise<ValidatorReturnType> => {
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
},

View File

@ -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,
})
}
}

View File

@ -14,7 +14,7 @@ export const styles = createStyles({
},
hide: {
'&:hover': {
backgroundColor: '#fff3e2',
backgroundColor: '#f7f5f5',
},
'&:hover $actions': {
visibility: 'initial',

View File

@ -15,12 +15,12 @@ type MessageHandler = (
) => void | MethodToResponse[Methods] | ErrorResponse | Promise<MethodToResponse[Methods] | ErrorResponse | void>
class AppCommunicator {
private iframe: HTMLIFrameElement
private iframeRef: MutableRefObject<HTMLIFrameElement | null>
private handlers = new Map<Methods, MessageHandler>()
private app: SafeApp
constructor(iframeRef: MutableRefObject<HTMLIFrameElement>, app: SafeApp) {
this.iframe = iframeRef.current
constructor(iframeRef: MutableRefObject<HTMLIFrameElement | null>, 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<void> => {
@ -83,7 +83,6 @@ const useAppCommunicator = (
app?: SafeApp,
): AppCommunicator | undefined => {
const [communicator, setCommunicator] = useState<AppCommunicator | undefined>(undefined)
useEffect(() => {
let communicatorInstance
const initCommunicator = (iframeRef: MutableRefObject<HTMLIFrameElement>, app: SafeApp) => {
@ -91,7 +90,7 @@ const useAppCommunicator = (
setCommunicator(communicatorInstance)
}
if (app && iframeRef.current !== null) {
if (app) {
initCommunicator(iframeRef as MutableRefObject<HTMLIFrameElement>, app)
}

View File

@ -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 => {
/>
)}
<ConfirmTransactionModal
<ConfirmTxModal
isOpen={confirmTransactionModal.isOpen}
app={safeApp as SafeApp}
safeAddress={safeAddress}

View File

@ -1,328 +0,0 @@
import React, { useEffect, useMemo, useState } from 'react'
import { Icon, ModalFooterConfirmation, Text, Title } from '@gnosis.pm/safe-react-components'
import { Transaction } from '@gnosis.pm/safe-apps-sdk-v1'
import styled from 'styled-components'
import { useDispatch, useSelector } from 'react-redux'
import AddressInfo from 'src/components/AddressInfo'
import DividerLine from 'src/components/DividerLine'
import Collapse from 'src/components/Collapse'
import TextBox from 'src/components/TextBox'
import ModalTitle from 'src/components/ModalTitle'
import { mustBeEthereumAddress } from 'src/components/forms/validator'
import Bold from 'src/components/layout/Bold'
import Heading from 'src/components/layout/Heading'
import Img from 'src/components/layout/Img'
import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers'
import { SafeApp } from 'src/routes/safe/components/Apps/types.d'
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
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 { web3ReadOnly } from 'src/logic/wallets/getWeb3'
import GasEstimationInfo from './GasEstimationInfo'
import { getNetworkInfo } from 'src/config'
import { TransactionParams } from './AppFrame'
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
import { safeThresholdSelector } from 'src/logic/safe/store/selectors'
import Modal from 'src/components/Modal'
import Row from 'src/components/layout/Row'
import Hairline from 'src/components/layout/Hairline'
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'
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'
}
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<string | undefined>()
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
? () => (
<>
<IconText>
<Icon color="error" size="md" type="info" />
<Title size="xs">Transaction error</Title>
</IconText>
<Text size="lg">
This Safe App initiated a transaction which cannot be processed. Please get in touch with the developer of
this Safe App for more information.
</Text>
</>
)
: (txParameters, toggleEditMode) => {
return (
<>
<Container>
<AddressInfo ethBalance={ethBalance} safeAddress={safeAddress} safeName={safeName} />
<DividerLine withArrow />
{txs.map((tx, index) => (
<Wrapper key={index}>
<Collapse description={<AddressInfo safeAddress={tx.to} />} title={`Transaction ${index + 1}`}>
<CollapseContent>
<div className="section">
<Heading tag="h3">Value</Heading>
<div className="value-section">
<Img alt="Ether" height={40} src={getEthAsToken('0').logoUri} />
<Bold>
{fromTokenUnit(tx.value, nativeCoin.decimals)} {nativeCoin.name}
</Bold>
</div>
</div>
<div className="section">
<Heading tag="h3">Data (hex encoded)*</Heading>
<StyledTextBox>{tx.data}</StyledTextBox>
</div>
</CollapseContent>
</Collapse>
</Wrapper>
))}
<DividerLine withArrow={false} />
{params?.safeTxGas && (
<div className="section">
<Heading tag="h3">SafeTxGas</Heading>
<StyledTextBox>{params?.safeTxGas}</StyledTextBox>
<GasEstimationInfo
appEstimation={params.safeTxGas}
internalEstimation={estimatedSafeTxGas}
loading={txEstimationExecutionStatus === EstimationStatus.LOADING}
/>
</div>
)}
{/* Tx Parameters */}
<TxParametersDetail
txParameters={txParameters}
onEdit={toggleEditMode}
parametersStatus={getParametersStatus()}
isTransactionCreation={isCreation}
isTransactionExecution={isExecution}
/>
</Container>
{txEstimationExecutionStatus === EstimationStatus.LOADING ? null : (
<TransactionFeesWrapper>
<TransactionFees
gasCostFormatted={gasCostFormatted}
isExecution={isExecution}
isCreation={isCreation}
isOffChainSignature={isOffChainSignature}
txEstimationExecutionStatus={txEstimationExecutionStatus}
/>
</TransactionFeesWrapper>
)}
</>
)
}
return (
<Modal description="Safe App transaction" title="Safe App transaction" open>
<EditableTxParameters
ethGasLimit={gasLimit}
ethGasPrice={gasPriceFormatted}
safeTxGas={gasEstimation.toString()}
parametersStatus={getParametersStatus()}
closeEditModalCallback={closeEditModalCallback}
>
{(txParameters, toggleEditMode) => (
<>
<ModalTitle title={app.name} iconUrl={app.iconUrl} onClose={handleTxRejection} />
<Hairline />
{body(txParameters, toggleEditMode)}
<ModalFooter align="center" grow>
<ModalFooterConfirmation
cancelText="Cancel"
handleCancel={handleTxRejection}
handleOk={() => confirmTransactions(txParameters)}
okDisabled={areTxsMalformed}
okText="Submit"
/>
</ModalFooter>
</>
)}
</EditableTxParameters>
</Modal>
)
}

View File

@ -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 = (
<>
<BasicTxInfo txRecipient={tx.to} txData={tx.data} txValue={txValue} />
{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 (
<>
<ModalTitle
title={(tx as DecodedDataParameterValue).dataDecoded?.method || (tx as DecodedData).method}
onClose={onClose}
goBack={hideDecodedTxData}
/>
<Hairline />
<Container>{body}</Container>
</>
)
}

View File

@ -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<DecodedData | null>(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<string | undefined>()
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 (
<EditableTxParameters
ethGasLimit={gasLimit}
ethGasPrice={gasPriceFormatted}
safeTxGas={gasEstimation.toString()}
closeEditModalCallback={closeEditModalCallback}
isOffChainSignature={isOffChainSignature}
isExecution={isExecution}
>
{(txParameters, toggleEditMode) => (
<div hidden={hidden}>
<ModalTitle title={app.name} iconUrl={app.iconUrl} onClose={handleTxRejection} />
<Hairline />
<Container>
{/* Safe */}
<AddressInfo ethBalance={ethBalance} safeAddress={safeAddress} safeName={safeName} />
<DividerLine withArrow />
{/* Txs decoded */}
<BasicTxInfo txRecipient={txRecipient} txData={txData} txValue={txValue} />
<DecodeTxsWrapper>
<DecodeTxs txs={txs} decodedData={decodedData} onTxItemClick={showDecodedTxData} />
</DecodeTxsWrapper>
{!isMultiSend && <DividerLine withArrow={false} />}
{/* Warning gas estimation */}
{params?.safeTxGas && (
<div className="section">
<Heading tag="h3">SafeTxGas</Heading>
<StyledTextBox>{params?.safeTxGas}</StyledTextBox>
<GasEstimationInfo
appEstimation={params.safeTxGas}
internalEstimation={estimatedSafeTxGas}
loading={txEstimationExecutionStatus === EstimationStatus.LOADING}
/>
</div>
)}
{/* Tx Parameters */}
<TxParametersDetail
txParameters={txParameters}
onEdit={toggleEditMode}
isTransactionCreation={isCreation}
isTransactionExecution={isExecution}
isOffChainSignature={isOffChainSignature}
/>
</Container>
{/* Gas info */}
{txEstimationExecutionStatus === EstimationStatus.LOADING ? null : (
<TransactionFeesWrapper>
<TransactionFees
gasCostFormatted={gasCostFormatted}
isExecution={isExecution}
isCreation={isCreation}
isOffChainSignature={isOffChainSignature}
txEstimationExecutionStatus={txEstimationExecutionStatus}
/>
</TransactionFeesWrapper>
)}
{/* Buttons */}
<FooterWrapper>
<ModalFooterConfirmation
cancelText="Cancel"
handleCancel={handleTxRejection}
handleOk={() => confirmTransactions(txParameters)}
okDisabled={areTxsMalformed}
okText="Submit"
/>
</FooterWrapper>
</div>
)}
</EditableTxParameters>
)
}

View File

@ -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 (
<>
<IconText>
<Icon color="error" size="md" type="info" />
<Title size="xs">Transaction error</Title>
</IconText>
<Text size="lg">
This Safe App initiated a transaction which cannot be processed. Please get in touch with the developer of this
Safe App for more information.
</Text>
<FooterWrapper>
<ModalFooterConfirmation
cancelText="Cancel"
handleCancel={() => handleTxRejection()}
handleOk={() => {}}
okDisabled={true}
okText="Submit"
/>
</FooterWrapper>
</>
)
}

View File

@ -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<DecodedTxDetail>()
const areTxsMalformed = props.txs.some((t) => !isTxValid(t))
const showDecodedTxData = setDecodedTxDetails
const hideDecodedTxData = () => setDecodedTxDetails(undefined)
const closeDecodedTxDetail = () => {
hideDecodedTxData()
props.onClose()
}
return (
<Modal description="Safe App transaction" title="Safe App transaction" open={props.isOpen}>
{areTxsMalformed && <SafeAppLoadError {...props} />}
{decodedTxDetails && (
<DecodedTxDetail
onClose={closeDecodedTxDetail}
hideDecodedTxData={hideDecodedTxData}
decodedTxData={decodedTxDetails}
/>
)}
<ReviewConfirm
{...props}
areTxsMalformed={areTxsMalformed}
showDecodedTxData={showDecodedTxData}
hidden={areTxsMalformed || !!decodedTxDetails}
/>
</Modal>
)
}

View File

@ -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<StaticAppInfo> = [
// 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<StaticAppInfo> = [
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<StaticAppInfo> = [
},
// 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<StaticAppInfo> = [
},
// Wallet-Connect
{
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmRMGTA5ARMwfhYbdmK83zzMd13NnEUKFJSZEgEjKa8YQm`,
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmU1pT35yPXxpnABcH3pZ1MxFeyYVtftT5RKhWopQmZHQV`,
disabled: false,
networks: [
ETHEREUM_NETWORK.MAINNET,

View File

@ -12,7 +12,7 @@ export const styles = createStyles({
},
hide: {
'&:hover': {
backgroundColor: '#fff3e2',
backgroundColor: '#f7f5f5',
},
'&:hover $actions': {
visibility: 'initial',

View File

@ -133,6 +133,7 @@ const SendModal = ({
{activeScreen === 'sendFunds' && (
<SendFunds
initialValues={tx as ReviewTxProp}
onClose={onClose}
onReview={handleTxCreation}
recipientAddress={recipientAddress}

View File

@ -126,6 +126,8 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE
return (
<EditableTxParameters
isOffChainSignature={isOffChainSignature}
isExecution={isExecution}
ethGasLimit={gasLimit}
ethGasPrice={gasPriceFormatted}
safeTxGas={gasEstimation.toString()}
@ -210,6 +212,7 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE
onEdit={toggleEditMode}
isTransactionCreation={isCreation}
isTransactionExecution={isExecution}
isOffChainSignature={isOffChainSignature}
/>
</Block>
<div className={classes.gasCostsContainer}>

View File

@ -94,7 +94,13 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => {
}
return (
<EditableTxParameters ethGasLimit={gasLimit} ethGasPrice={gasPriceFormatted} safeTxGas={gasEstimation.toString()}>
<EditableTxParameters
isOffChainSignature={isOffChainSignature}
isExecution={isExecution}
ethGasLimit={gasLimit}
ethGasPrice={gasPriceFormatted}
safeTxGas={gasEstimation.toString()}
>
{(txParameters, toggleEditMode) => (
<>
<Row align="center" className={classes.heading} grow>
@ -168,6 +174,7 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => {
onEdit={toggleEditMode}
isTransactionCreation={isCreation}
isTransactionExecution={isExecution}
isOffChainSignature={isOffChainSignature}
/>
</Block>
{txEstimationExecutionStatus === EstimationStatus.LOADING ? null : (

View File

@ -140,6 +140,8 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement =
return (
<EditableTxParameters
isOffChainSignature={isOffChainSignature}
isExecution={isExecution}
ethGasLimit={gasLimit}
ethGasPrice={gasPriceFormatted}
safeTxGas={gasEstimation.toString()}
@ -206,6 +208,7 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement =
onEdit={toggleEditMode}
isTransactionCreation={isCreation}
isTransactionExecution={isExecution}
isOffChainSignature={isOffChainSignature}
/>
</Block>
<div className={classes.gasCostsContainer}>

View File

@ -178,6 +178,8 @@ const ReviewSendFundsTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactE
return (
<EditableTxParameters
isOffChainSignature={isOffChainSignature}
isExecution={isExecution}
ethGasLimit={gasLimit}
ethGasPrice={gasPriceFormatted}
safeTxGas={gasEstimation.toString()}
@ -260,10 +262,11 @@ const ReviewSendFundsTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactE
onEdit={toggleEditMode}
isTransactionCreation={isCreation}
isTransactionExecution={isExecution}
isOffChainSignature={isOffChainSignature}
/>
{/* Disclaimer */}
</Block>
{/* Disclaimer */}
{txEstimationExecutionStatus !== EstimationStatus.LOADING && (
<div className={classes.gasCostsContainer}>
<TransactionFees

View File

@ -68,6 +68,7 @@ export type SendFundsTx = {
}
type SendFundsProps = {
initialValues: SendFundsTx
onClose: () => 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 = ({
<Hairline />
<GnoForm
formMutators={formMutators}
initialValues={{ amount, recipientAddress, token: selectedToken }}
initialValues={{
amount: initialValues?.amount || amount,
recipientAddress: initialValues.recipientAddress || recipientAddress,
token: initialValues?.token || selectedToken,
}}
onSubmit={handleSubmit}
validation={sendFundsValidation}
>

View File

@ -127,6 +127,8 @@ export const RemoveModuleModal = ({ onClose, selectedModulePair }: RemoveModuleM
open
>
<EditableTxParameters
isOffChainSignature={isOffChainSignature}
isExecution={isExecution}
ethGasLimit={gasLimit}
ethGasPrice={gasPriceFormatted}
safeTxGas={gasEstimation.toString()}
@ -181,6 +183,7 @@ export const RemoveModuleModal = ({ onClose, selectedModulePair }: RemoveModuleM
onEdit={toggleEditMode}
isTransactionCreation={isCreation}
isTransactionExecution={isExecution}
isOffChainSignature={isOffChainSignature}
/>
</Block>
<Row className={cn(classes.modalDescription, classes.gasCostsContainer)}>

View File

@ -8,7 +8,7 @@ export const styles = createStyles({
},
hide: {
'&:hover': {
backgroundColor: '#fff3e2',
backgroundColor: '#f7f5f5',
},
'&:hover $actions': {
visibility: 'initial',

View File

@ -101,6 +101,8 @@ export const ReviewAddOwner = ({ onClickBack, onClose, onSubmit, values }: Revie
return (
<EditableTxParameters
isOffChainSignature={isOffChainSignature}
isExecution={isExecution}
ethGasLimit={gasLimit}
ethGasPrice={gasPriceFormatted}
safeTxGas={gasEstimation.toString()}
@ -214,6 +216,7 @@ export const ReviewAddOwner = ({ onClickBack, onClose, onSubmit, values }: Revie
compact={false}
isTransactionCreation={isCreation}
isTransactionExecution={isExecution}
isOffChainSignature={isOffChainSignature}
/>
<Block className={classes.gasCostsContainer}>

View File

@ -64,7 +64,7 @@ export const styles = createStyles({
selectedOwner: {
padding: sm,
alignItems: 'center',
backgroundColor: '#fff3e2',
backgroundColor: '#f7f5f5',
},
user: {
justifyContent: 'left',

View File

@ -123,6 +123,8 @@ export const ReviewRemoveOwnerModal = ({
return (
<EditableTxParameters
isOffChainSignature={isOffChainSignature}
isExecution={isExecution}
ethGasLimit={gasLimit}
ethGasPrice={gasPriceFormatted}
safeTxGas={gasEstimation.toString()}
@ -241,6 +243,7 @@ export const ReviewRemoveOwnerModal = ({
compact={false}
isTransactionCreation={isCreation}
isTransactionExecution={isExecution}
isOffChainSignature={isOffChainSignature}
/>
{txEstimationExecutionStatus === EstimationStatus.LOADING ? null : (

View File

@ -120,6 +120,8 @@ export const ReviewReplaceOwnerModal = ({
return (
<EditableTxParameters
isOffChainSignature={isOffChainSignature}
isExecution={isExecution}
ethGasLimit={gasLimit}
ethGasPrice={gasPriceFormatted}
safeTxGas={gasEstimation.toString()}
@ -261,6 +263,7 @@ export const ReviewReplaceOwnerModal = ({
compact={false}
isTransactionCreation={isCreation}
isTransactionExecution={isExecution}
isOffChainSignature={isOffChainSignature}
/>
<Block className={classes.gasCostsContainer}>

View File

@ -69,7 +69,7 @@ export const styles = createStyles({
selectedOwnerAdded: {
padding: sm,
alignItems: 'center',
backgroundColor: '#fff3e2',
backgroundColor: '#f7f5f5',
},
user: {
justifyContent: 'left',

View File

@ -14,7 +14,7 @@ export const styles = createStyles({
},
hide: {
'&:hover': {
backgroundColor: '#fff3e2',
backgroundColor: '#f7f5f5',
},
'&:hover $actions': {
visibility: 'initial',

View File

@ -233,6 +233,8 @@ export const ReviewSpendingLimits = ({ onBack, onClose, txToken, values }: Revie
return (
<EditableTxParameters
isOffChainSignature={isOffChainSignature}
isExecution={isExecution}
ethGasLimit={gasLimit}
ethGasPrice={gasPriceFormatted}
safeTxGas={gasEstimation.toString()}
@ -282,6 +284,7 @@ export const ReviewSpendingLimits = ({ onBack, onClose, txToken, values }: Revie
onEdit={toggleEditMode}
isTransactionCreation={isCreation}
isTransactionExecution={isExecution}
isOffChainSignature={isOffChainSignature}
/>
</Block>
<div className={classes.gasCostsContainer}>

View File

@ -116,6 +116,8 @@ export const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendin
description="Remove the selected Spending Limit"
>
<EditableTxParameters
isOffChainSignature={isOffChainSignature}
isExecution={isExecution}
ethGasLimit={gasLimit}
ethGasPrice={gasPriceFormatted}
safeTxGas={gasEstimation.toString()}
@ -148,6 +150,7 @@ export const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendin
onEdit={toggleEditMode}
isTransactionCreation={isCreation}
isTransactionExecution={isExecution}
isOffChainSignature={isOffChainSignature}
/>
</Block>

View File

@ -21,7 +21,7 @@ export const useStyles = makeStyles(
},
hide: {
'&:hover': {
backgroundColor: '#fff3e2',
backgroundColor: '#f7f5f5',
},
'&:hover $actions': {
visibility: 'initial',

View File

@ -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 (
<EditableTxParameters
isOffChainSignature={isOffChainSignature}
isExecution={isExecution}
ethGasLimit={gasLimit}
ethGasPrice={gasPriceFormatted}
safeTxGas={gasEstimation.toString()}
@ -181,9 +181,9 @@ export const ChangeThresholdModal = ({
<TxParametersDetail
txParameters={txParameters}
onEdit={toggleEditMode}
parametersStatus={getParametersStatus()}
isTransactionCreation={isCreation}
isTransactionExecution={isExecution}
isOffChainSignature={isOffChainSignature}
/>
</Block>
{txEstimationExecutionStatus !== EstimationStatus.LOADING && (

View File

@ -76,7 +76,13 @@ export const UpdateSafeModal = ({ onClose, safeAddress }: Props): React.ReactEle
})
return (
<EditableTxParameters ethGasLimit={gasLimit} ethGasPrice={gasPriceFormatted} safeTxGas={gasEstimation.toString()}>
<EditableTxParameters
isOffChainSignature={isOffChainSignature}
isExecution={isExecution}
ethGasLimit={gasLimit}
ethGasPrice={gasPriceFormatted}
safeTxGas={gasEstimation.toString()}
>
{(txParameters, toggleEditMode) => (
<>
<Row align="center" className={classes.heading} grow>
@ -116,6 +122,7 @@ export const UpdateSafeModal = ({ onClose, safeAddress }: Props): React.ReactEle
compact={false}
isTransactionCreation={isCreation}
isTransactionExecution={isExecution}
isOffChainSignature={isOffChainSignature}
/>
</Block>
{txEstimationExecutionStatus === EstimationStatus.LOADING ? null : (

View File

@ -317,6 +317,8 @@ export const ApproveTxModal = ({
return (
<Modal description={description} handleClose={onClose} open={isOpen} title={title}>
<EditableTxParameters
isOffChainSignature={isOffChainSignature}
isExecution={isExecution}
parametersStatus={getParametersStatus()}
ethGasLimit={gasLimit}
ethGasPrice={gasPriceFormatted}
@ -370,13 +372,14 @@ export const ApproveTxModal = ({
)}
{/* Tx Parameters */}
{approveAndExecute && (
{(approveAndExecute || !isOffChainSignature) && (
<TxParametersDetail
txParameters={txParameters}
onEdit={toggleEditMode}
parametersStatus={getParametersStatus()}
isTransactionCreation={isCreation}
isTransactionExecution={isExecution}
isOffChainSignature={isOffChainSignature}
/>
)}
</Row>

View File

@ -82,6 +82,8 @@ export const RejectTxModal = ({ isOpen, onClose, gwTransaction }: Props): React.
return (
<Modal description="Reject Transaction" handleClose={onClose} open={isOpen} title="Reject Transaction">
<EditableTxParameters
isOffChainSignature={isOffChainSignature}
isExecution={isExecution}
ethGasLimit={gasLimit}
ethGasPrice={gasPriceFormatted}
safeTxGas={'0'}
@ -119,6 +121,7 @@ export const RejectTxModal = ({ isOpen, onClose, gwTransaction }: Props): React.
parametersStatus={getParametersStatus()}
isTransactionCreation={isCreation}
isTransactionExecution={isExecution}
isOffChainSignature={isOffChainSignature}
/>
</Block>

View File

@ -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 = ({
{() => (
<>
<StyledText size="xl" strong>
Safe transactions parameters
Safe transaction
</StyledText>
<SafeOptions>
@ -168,49 +170,53 @@ export const EditTxParametersForm = ({
/>
</SafeOptions>
<StyledTextMt size="xl" strong>
Ethereum transactions parameters
</StyledTextMt>
{areEthereumParamsVisible(parametersStatus) && (
<>
<StyledTextMt size="xl" strong>
{ethereumTxParametersTitle(isExecution)}
</StyledTextMt>
<EthereumOptions>
<Field
name="ethNonce"
defaultValue={ethNonce}
placeholder="Ethereum nonce"
text="Ethereum nonce"
type="number"
component={TextField}
disabled={!areEthereumParamsEnabled(parametersStatus)}
/>
<Field
name="ethGasLimit"
defaultValue={ethGasLimit}
placeholder="Ethereum gas limit"
text="Ethereum gas limit"
type="number"
component={TextField}
disabled={parametersStatus === 'CANCEL_TRANSACTION'}
/>
<Field
name="ethGasPrice"
defaultValue={ethGasPrice}
type="number"
placeholder="Ethereum gas price (GWEI)"
text="Ethereum gas price (GWEI)"
component={TextField}
disabled={!areEthereumParamsEnabled(parametersStatus)}
/>
</EthereumOptions>
<EthereumOptions>
<Field
name="ethNonce"
defaultValue={ethNonce}
placeholder="Nonce"
text="Nonce"
type="number"
component={TextField}
disabled={!areEthereumParamsVisible(parametersStatus)}
/>
<Field
name="ethGasLimit"
defaultValue={ethGasLimit}
placeholder="Gas limit"
text="Gas limit"
type="number"
component={TextField}
disabled={parametersStatus === 'CANCEL_TRANSACTION'}
/>
<Field
name="ethGasPrice"
defaultValue={ethGasPrice}
type="number"
placeholder="Gas price (GWEI)"
text="Gas price (GWEI)"
component={TextField}
disabled={!areEthereumParamsVisible(parametersStatus)}
/>
</EthereumOptions>
<StyledLink
href="https://help.gnosis-safe.io/en/articles/4738445-configure-advanced-transaction-parameters-manually"
target="_blank"
>
<Text size="xl" color="primary">
How can I configure the gas price manually?
</Text>
<Icon size="sm" type="externalLink" color="primary" />
</StyledLink>
<StyledLink
href="https://help.gnosis-safe.io/en/articles/4738445-configure-advanced-transaction-parameters-manually"
target="_blank"
>
<Text size="xl" color="primary">
How can I configure these parameters manually?
</Text>
<Icon size="sm" type="externalLink" color="primary" />
</StyledLink>
</>
)}
<StyledDivider />

View File

@ -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 ? (
<EditTxParametersForm
isExecution={isExecution}
txParameters={txParameters}
onClose={closeEditFormHandler}
parametersStatus={parametersStatus ? parametersStatus : defaultParameterStatus}

View File

@ -3,7 +3,7 @@ import styled from 'styled-components'
import { Text, ButtonLink, Accordion, AccordionSummary, AccordionDetails } from '@gnosis.pm/safe-react-components'
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
import { ParametersStatus, areEthereumParamsEnabled, areSafeParamsEnabled } from '../utils'
import { ParametersStatus, areEthereumParamsVisible, areSafeParamsEnabled, ethereumTxParametersTitle } from '../utils'
import { useSelector } from 'react-redux'
import { safeThresholdSelector } from 'src/logic/safe/store/selectors'
@ -35,8 +35,9 @@ type Props = {
onEdit: () => 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 = ({
<AccordionDetails>
<AccordionDetailsWrapper>
<StyledText size="md" color="placeHolder">
Safe transactions parameters
Safe transaction
</StyledText>
<TxParameterWrapper>
@ -95,57 +97,30 @@ export const TxParametersDetail = ({
</Text>
</TxParameterWrapper>
<TxParameterWrapper>
<StyledText size="md" color="placeHolder">
Ethereum transaction parameters
</StyledText>
</TxParameterWrapper>
{areEthereumParamsVisible(parametersStatus || defaultParameterStatus) && (
<>
<TxParameterWrapper>
<StyledText size="md" color="placeHolder">
{ethereumTxParametersTitle(isTransactionExecution)}
</StyledText>
</TxParameterWrapper>
<TxParameterWrapper>
<Text
size="lg"
color={areEthereumParamsEnabled(parametersStatus || defaultParameterStatus) ? 'text' : 'secondaryLight'}
>
Ethereum nonce
</Text>
<Text
size="lg"
color={areEthereumParamsEnabled(parametersStatus || defaultParameterStatus) ? 'text' : 'secondaryLight'}
>
{txParameters.ethNonce}
</Text>
</TxParameterWrapper>
<TxParameterWrapper>
<Text size="lg">Nonce</Text>
<Text size="lg">{txParameters.ethNonce}</Text>
</TxParameterWrapper>
<TxParameterWrapper>
<Text
size="lg"
color={areEthereumParamsEnabled(parametersStatus || defaultParameterStatus) ? 'text' : 'secondaryLight'}
>
Ethereum gas limit
</Text>
<Text
size="lg"
color={areEthereumParamsEnabled(parametersStatus || defaultParameterStatus) ? 'text' : 'secondaryLight'}
>
{txParameters.ethGasLimit}
</Text>
</TxParameterWrapper>
<TxParameterWrapper>
<Text
size="lg"
color={areEthereumParamsEnabled(parametersStatus || defaultParameterStatus) ? 'text' : 'secondaryLight'}
>
Ethereum gas price
</Text>
<Text
size="lg"
color={areEthereumParamsEnabled(parametersStatus || defaultParameterStatus) ? 'text' : 'secondaryLight'}
>
{txParameters.ethGasPrice}
</Text>
</TxParameterWrapper>
<TxParameterWrapper>
<Text size="lg">Gas limit</Text>
<Text size="lg">{txParameters.ethGasLimit}</Text>
</TxParameterWrapper>
<TxParameterWrapper>
<Text size="lg">Gas price</Text>
<Text size="lg">{txParameters.ethGasPrice}</Text>
</TxParameterWrapper>
</>
)}
<StyledButtonLink color="primary" textSize="xl" onClick={onEdit}>
Edit
</StyledButtonLink>

View File

@ -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)'}`
}

View File

@ -455,7 +455,7 @@ export const DropdownListTheme = {
},
button: {
'&:hover': {
backgroundColor: '#fff3e2',
backgroundColor: '#f7f5f5',
},
},
},

24
src/types/transactions/decode.d.ts vendored Normal file
View File

@ -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[]
}

18
src/utils/decodeTx.ts Normal file
View File

@ -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<DecodedData | null> => {
if (!txData?.length || txData === '0x') {
return null
}
const url = `${getTxServiceUrl()}/data-decoder/`
try {
const res = await axios.post<DecodedData>(url, { data: txData })
return res.data
} catch (error) {
return null
}
}

View File

@ -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"