Merge branch 'development' into update-wallet-connect
This commit is contained in:
commit
b1be77c996
|
@ -161,7 +161,7 @@
|
||||||
"@gnosis.pm/safe-apps-sdk": "1.0.3",
|
"@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-apps-sdk-v1": "npm:@gnosis.pm/safe-apps-sdk@0.4.2",
|
||||||
"@gnosis.pm/safe-contracts": "1.1.1-dev.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",
|
"@gnosis.pm/util-contracts": "2.0.6",
|
||||||
"@ledgerhq/hw-transport-node-hid-singleton": "5.45.0",
|
"@ledgerhq/hw-transport-node-hid-singleton": "5.45.0",
|
||||||
"@material-ui/core": "^4.11.0",
|
"@material-ui/core": "^4.11.0",
|
||||||
|
|
|
@ -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} />
|
||||||
|
)
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import React from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import IconButton from '@material-ui/core/IconButton'
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
import Close from '@material-ui/icons/Close'
|
import Close from '@material-ui/icons/Close'
|
||||||
|
import { Icon } from '@gnosis.pm/safe-react-components'
|
||||||
|
|
||||||
import Paragraph from 'src/components/layout/Paragraph'
|
import Paragraph from 'src/components/layout/Paragraph'
|
||||||
import { md, lg } from 'src/theme/variables'
|
import { md, lg } from 'src/theme/variables'
|
||||||
|
@ -33,18 +34,28 @@ const StyledClose = styled(Close)`
|
||||||
width: 35px;
|
width: 35px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const ModalTitle = ({
|
const GoBackWrapper = styled.div`
|
||||||
iconUrl,
|
margin-right: 15px;
|
||||||
title,
|
`
|
||||||
onClose,
|
|
||||||
}: {
|
type Props = {
|
||||||
title: string
|
title: string
|
||||||
iconUrl: string
|
goBack?: () => void
|
||||||
|
iconUrl?: string
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
}): React.ReactElement => {
|
}
|
||||||
|
|
||||||
|
const ModalTitle = ({ goBack, iconUrl, title, onClose }: Props): React.ReactElement => {
|
||||||
return (
|
return (
|
||||||
<StyledRow align="center" grow>
|
<StyledRow align="center" grow>
|
||||||
<TitleWrapper>
|
<TitleWrapper>
|
||||||
|
{goBack && (
|
||||||
|
<GoBackWrapper>
|
||||||
|
<IconButton onClick={goBack}>
|
||||||
|
<Icon type="arrowLeft" size="md" />
|
||||||
|
</IconButton>
|
||||||
|
</GoBackWrapper>
|
||||||
|
)}
|
||||||
{iconUrl && <IconImg alt={title} src={iconUrl} />}
|
{iconUrl && <IconImg alt={title} src={iconUrl} />}
|
||||||
<StyledParagraph noMargin weight="bolder">
|
<StyledParagraph noMargin weight="bolder">
|
||||||
{title}
|
{title}
|
||||||
|
|
|
@ -14,7 +14,7 @@ export const styles = createStyles({
|
||||||
},
|
},
|
||||||
hide: {
|
hide: {
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: '#fff3e2',
|
backgroundColor: '#f7f5f5',
|
||||||
},
|
},
|
||||||
'&:hover $actions': {
|
'&:hover $actions': {
|
||||||
visibility: 'initial',
|
visibility: 'initial',
|
||||||
|
|
|
@ -32,7 +32,7 @@ import { LoadingContainer } from 'src/components/LoaderContainer/index'
|
||||||
import { TIMEOUT } from 'src/utils/constants'
|
import { TIMEOUT } from 'src/utils/constants'
|
||||||
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
|
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
|
||||||
|
|
||||||
import { ConfirmTransactionModal } from '../components/ConfirmTransactionModal'
|
import { ConfirmTxModal } from '../components/ConfirmTxModal'
|
||||||
import { useIframeMessageHandler } from '../hooks/useIframeMessageHandler'
|
import { useIframeMessageHandler } from '../hooks/useIframeMessageHandler'
|
||||||
import { useLegalConsent } from '../hooks/useLegalConsent'
|
import { useLegalConsent } from '../hooks/useLegalConsent'
|
||||||
import LegalDisclaimer from './LegalDisclaimer'
|
import LegalDisclaimer from './LegalDisclaimer'
|
||||||
|
@ -354,7 +354,7 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ConfirmTransactionModal
|
<ConfirmTxModal
|
||||||
isOpen={confirmTransactionModal.isOpen}
|
isOpen={confirmTransactionModal.isOpen}
|
||||||
app={safeApp as SafeApp}
|
app={safeApp as SafeApp}
|
||||||
safeAddress={safeAddress}
|
safeAddress={safeAddress}
|
||||||
|
|
|
@ -1,324 +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 } 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 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 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 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}
|
|
||||||
isTransactionCreation={isCreation}
|
|
||||||
isTransactionExecution={isExecution}
|
|
||||||
isOffChainSignature={isOffChainSignature}
|
|
||||||
/>
|
|
||||||
</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
|
|
||||||
isOffChainSignature={isOffChainSignature}
|
|
||||||
isExecution={isExecution}
|
|
||||||
ethGasLimit={gasLimit}
|
|
||||||
ethGasPrice={gasPriceFormatted}
|
|
||||||
safeTxGas={gasEstimation.toString()}
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -26,7 +26,7 @@ export type StaticAppInfo = {
|
||||||
export const staticAppsList: Array<StaticAppInfo> = [
|
export const staticAppsList: Array<StaticAppInfo> = [
|
||||||
// 1inch
|
// 1inch
|
||||||
{
|
{
|
||||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmRWtuktjfU6WMAEJFgzBC4cUfqp3FF5uN9QoWb55SdGG5`,
|
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmUXF1yVGdqUfMbhNyfM3jpP6Bw66cYnKPoWq6iHkhd3Aw`,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
networks: [ETHEREUM_NETWORK.MAINNET],
|
networks: [ETHEREUM_NETWORK.MAINNET],
|
||||||
},
|
},
|
||||||
|
|
|
@ -12,7 +12,7 @@ export const styles = createStyles({
|
||||||
},
|
},
|
||||||
hide: {
|
hide: {
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: '#fff3e2',
|
backgroundColor: '#f7f5f5',
|
||||||
},
|
},
|
||||||
'&:hover $actions': {
|
'&:hover $actions': {
|
||||||
visibility: 'initial',
|
visibility: 'initial',
|
||||||
|
|
|
@ -133,6 +133,7 @@ const SendModal = ({
|
||||||
|
|
||||||
{activeScreen === 'sendFunds' && (
|
{activeScreen === 'sendFunds' && (
|
||||||
<SendFunds
|
<SendFunds
|
||||||
|
initialValues={tx as ReviewTxProp}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onReview={handleTxCreation}
|
onReview={handleTxCreation}
|
||||||
recipientAddress={recipientAddress}
|
recipientAddress={recipientAddress}
|
||||||
|
|
|
@ -68,6 +68,7 @@ export type SendFundsTx = {
|
||||||
}
|
}
|
||||||
|
|
||||||
type SendFundsProps = {
|
type SendFundsProps = {
|
||||||
|
initialValues: SendFundsTx
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onReview: (txInfo: unknown) => void
|
onReview: (txInfo: unknown) => void
|
||||||
recipientAddress?: string
|
recipientAddress?: string
|
||||||
|
@ -80,6 +81,7 @@ const InputAdornmentChildSymbol = ({ symbol }: { symbol?: string }): ReactElemen
|
||||||
}
|
}
|
||||||
|
|
||||||
const SendFunds = ({
|
const SendFunds = ({
|
||||||
|
initialValues,
|
||||||
onClose,
|
onClose,
|
||||||
onReview,
|
onReview,
|
||||||
recipientAddress,
|
recipientAddress,
|
||||||
|
@ -93,12 +95,14 @@ const SendFunds = ({
|
||||||
const defaultEntry = { address: recipientAddress || '', name: '' }
|
const defaultEntry = { address: recipientAddress || '', name: '' }
|
||||||
|
|
||||||
// if there's nothing to lookup for, we return the default entry
|
// if there's nothing to lookup for, we return the default entry
|
||||||
if (!recipientAddress) {
|
if (!initialValues?.recipientAddress && !recipientAddress) {
|
||||||
return defaultEntry
|
return defaultEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if there's something to lookup for, `initialValues` has precedence over `recipientAddress`
|
||||||
|
const predefinedAddress = initialValues?.recipientAddress ?? recipientAddress
|
||||||
const addressBookEntry = addressBook.find(({ address }) => {
|
const addressBookEntry = addressBook.find(({ address }) => {
|
||||||
return sameAddress(recipientAddress, address)
|
return sameAddress(predefinedAddress, address)
|
||||||
})
|
})
|
||||||
|
|
||||||
// if found in the Address Book, then we return the entry
|
// if found in the Address Book, then we return the entry
|
||||||
|
@ -170,7 +174,11 @@ const SendFunds = ({
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<GnoForm
|
<GnoForm
|
||||||
formMutators={formMutators}
|
formMutators={formMutators}
|
||||||
initialValues={{ amount, recipientAddress, token: selectedToken }}
|
initialValues={{
|
||||||
|
amount: initialValues?.amount || amount,
|
||||||
|
recipientAddress: initialValues.recipientAddress || recipientAddress,
|
||||||
|
token: initialValues?.token || selectedToken,
|
||||||
|
}}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
validation={sendFundsValidation}
|
validation={sendFundsValidation}
|
||||||
>
|
>
|
||||||
|
|
|
@ -8,7 +8,7 @@ export const styles = createStyles({
|
||||||
},
|
},
|
||||||
hide: {
|
hide: {
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: '#fff3e2',
|
backgroundColor: '#f7f5f5',
|
||||||
},
|
},
|
||||||
'&:hover $actions': {
|
'&:hover $actions': {
|
||||||
visibility: 'initial',
|
visibility: 'initial',
|
||||||
|
|
|
@ -64,7 +64,7 @@ export const styles = createStyles({
|
||||||
selectedOwner: {
|
selectedOwner: {
|
||||||
padding: sm,
|
padding: sm,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: '#fff3e2',
|
backgroundColor: '#f7f5f5',
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
justifyContent: 'left',
|
justifyContent: 'left',
|
||||||
|
|
|
@ -69,7 +69,7 @@ export const styles = createStyles({
|
||||||
selectedOwnerAdded: {
|
selectedOwnerAdded: {
|
||||||
padding: sm,
|
padding: sm,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: '#fff3e2',
|
backgroundColor: '#f7f5f5',
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
justifyContent: 'left',
|
justifyContent: 'left',
|
||||||
|
|
|
@ -14,7 +14,7 @@ export const styles = createStyles({
|
||||||
},
|
},
|
||||||
hide: {
|
hide: {
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: '#fff3e2',
|
backgroundColor: '#f7f5f5',
|
||||||
},
|
},
|
||||||
'&:hover $actions': {
|
'&:hover $actions': {
|
||||||
visibility: 'initial',
|
visibility: 'initial',
|
||||||
|
|
|
@ -21,7 +21,7 @@ export const useStyles = makeStyles(
|
||||||
},
|
},
|
||||||
hide: {
|
hide: {
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: '#fff3e2',
|
backgroundColor: '#f7f5f5',
|
||||||
},
|
},
|
||||||
'&:hover $actions': {
|
'&:hover $actions': {
|
||||||
visibility: 'initial',
|
visibility: 'initial',
|
||||||
|
|
|
@ -455,7 +455,7 @@ export const DropdownListTheme = {
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: '#fff3e2',
|
backgroundColor: '#f7f5f5',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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[]
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -1596,9 +1596,9 @@
|
||||||
solc "0.5.14"
|
solc "0.5.14"
|
||||||
truffle "^5.1.21"
|
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"
|
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:
|
dependencies:
|
||||||
classnames "^2.2.6"
|
classnames "^2.2.6"
|
||||||
react-media "^1.10.0"
|
react-media "^1.10.0"
|
||||||
|
|
Loading…
Reference in New Issue