mirror of
https://github.com/status-im/safe-react.git
synced 2026-08-31 13:41:06 +00:00
@@ -5,7 +5,6 @@ REACT_APP_NETWORK=
|
||||
REACT_APP_GOOGLE_ANALYTICS=
|
||||
REACT_APP_INFURA_TOKEN=
|
||||
REACT_APP_IPFS_GATEWAY=https://ipfs.io/ipfs
|
||||
PUBLIC_URL=/app/
|
||||
REACT_APP_SENTRY_DSN=
|
||||
|
||||
# For production environments
|
||||
|
||||
+1
-1
@@ -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#80f5db6",
|
||||
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#2e427ee",
|
||||
"@gnosis.pm/util-contracts": "2.0.6",
|
||||
"@ledgerhq/hw-transport-node-hid-singleton": "5.45.0",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
|
||||
@@ -4,16 +4,14 @@ The most trusted platform to store digital assets on Ethereum
|
||||
|
||||
## Getting Started
|
||||
|
||||
These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See deployment for notes on how to deploy the project on a live system.
|
||||
These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See [Deployment](#deployment) for notes on how to deploy the project on a live system.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
What things you need to install the software and how to install them
|
||||
What things you need to install globally:
|
||||
|
||||
```
|
||||
yarn add truffle // recommended usage of -g flag
|
||||
yarn add ganache-cli // recommended usage of -g flag
|
||||
yarn add flow-type // recommended usage of -g flag
|
||||
yarn global add truffle ganache-cli
|
||||
```
|
||||
|
||||
We use [yarn](https://yarnpkg.com) in our infrastacture, so we decided to go with yarn in the README
|
||||
@@ -37,6 +35,18 @@ If you prefer using Mainnet ones:
|
||||
yarn start-mainnet
|
||||
```
|
||||
|
||||
### Environment variables
|
||||
The app grabs environment variables from the `.env` file. Copy our template to your own local file:
|
||||
```
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
To execute transactions, you'll need to create an [Infura](https://infura.io) project and set the project ID in the `.env` you've just created:
|
||||
```
|
||||
REACT_APP_INFURA_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
```
|
||||
Once done, you'll need to restart the app.
|
||||
|
||||
### Building
|
||||
For Rinkeby:
|
||||
```
|
||||
@@ -120,7 +130,7 @@ Please read [CONTRIBUTING.md](https://gist.github.com/PurpleBooth/b24679402957c6
|
||||
|
||||
## Versioning
|
||||
|
||||
We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/gnosis/gnosis-team-safe/tags).
|
||||
We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/gnosis/gnosis-team-safe/tags).
|
||||
|
||||
## Authors
|
||||
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import React from 'react'
|
||||
import { getExplorerInfo, getNetworkInfo } from 'src/config'
|
||||
import CopyBtn from 'src/components/CopyBtn'
|
||||
import Identicon from 'src/components/Identicon'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Bold from 'src/components/layout/Bold'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import { border, xs } from 'src/theme/variables'
|
||||
import styled from 'styled-components'
|
||||
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.icon-section {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.data-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.address {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
`
|
||||
const StyledBlock = styled(Block)`
|
||||
font-size: 12px;
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.5;
|
||||
background-color: ${border};
|
||||
width: fit-content;
|
||||
padding: 5px 10px;
|
||||
margin-top: ${xs};
|
||||
border-radius: 3px;
|
||||
`
|
||||
interface Props {
|
||||
safeName?: string
|
||||
safeAddress: string
|
||||
ethBalance?: string
|
||||
}
|
||||
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
|
||||
const AddressInfo = ({ ethBalance, safeAddress, safeName }: Props): React.ReactElement => {
|
||||
return (
|
||||
<Wrapper>
|
||||
<div className="icon-section">
|
||||
<Identicon address={safeAddress} diameter={32} />
|
||||
</div>
|
||||
<div className="data-section">
|
||||
{safeName && (
|
||||
<Paragraph noMargin weight="bolder">
|
||||
{safeName}
|
||||
</Paragraph>
|
||||
)}
|
||||
<div className="address">
|
||||
<Paragraph noMargin weight="bolder">
|
||||
{safeAddress}
|
||||
</Paragraph>
|
||||
<CopyBtn content={safeAddress} />
|
||||
<ExplorerButton explorerUrl={getExplorerInfo(safeAddress)} />
|
||||
</div>
|
||||
{ethBalance && (
|
||||
<StyledBlock>
|
||||
<Paragraph noMargin>
|
||||
Balance: <Bold data-testid="current-eth-balance">{`${ethBalance} ${nativeCoin.symbol}`}</Bold>
|
||||
</Paragraph>
|
||||
</StyledBlock>
|
||||
)}
|
||||
</div>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddressInfo
|
||||
@@ -50,12 +50,6 @@ const notificationStyles = {
|
||||
info: {
|
||||
background: '#fff',
|
||||
},
|
||||
receiveModal: {
|
||||
height: 'auto',
|
||||
maxWidth: 'calc(100% - 30px)',
|
||||
minHeight: '544px',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
}
|
||||
|
||||
const Frame = styled.div`
|
||||
@@ -149,7 +143,7 @@ const App: React.FC = ({ children }) => {
|
||||
description="Receive Tokens Form"
|
||||
handleClose={onReceiveHide}
|
||||
open={safeActionsState.showReceive}
|
||||
paperClassName={classes.receiveModal}
|
||||
paperClassName="receive-modal"
|
||||
title="Receive Tokens"
|
||||
>
|
||||
<ReceiveModal onClose={onReceiveHide} safeAddress={safeAddress} safeName={safeName} />
|
||||
|
||||
@@ -12,7 +12,7 @@ import Spacer from 'src/components/Spacer'
|
||||
import Col from 'src/components/layout/Col'
|
||||
import Img from 'src/components/layout/Img'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { border, headerHeight, md, screenSm, sm } from 'src/theme/variables'
|
||||
import { headerHeight, md, screenSm, sm } from 'src/theme/variables'
|
||||
import { useStateHandler } from 'src/logic/hooks/useStateHandler'
|
||||
|
||||
import SafeLogo from '../assets/gnosis-safe-multisig-logo.svg'
|
||||
@@ -29,8 +29,6 @@ const styles = () => ({
|
||||
summary: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'white',
|
||||
borderBottom: `solid 2px ${border}`,
|
||||
boxShadow: '0 2px 4px 0 rgba(212, 212, 211, 0.59)',
|
||||
flexWrap: 'nowrap',
|
||||
height: headerHeight,
|
||||
position: 'fixed',
|
||||
|
||||
@@ -6,13 +6,11 @@ import ConnectButton from 'src/components/ConnectButton'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { lg, md } from 'src/theme/variables'
|
||||
import { KeyRing } from 'src/components/AppLayout/Header/components/KeyRing'
|
||||
import { Card } from '@gnosis.pm/safe-react-components'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const styles = () => ({
|
||||
container: {
|
||||
padding: `${md} 12px`,
|
||||
},
|
||||
logo: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
@@ -22,8 +20,8 @@ const styles = () => ({
|
||||
textAlign: 'center',
|
||||
},
|
||||
connect: {
|
||||
padding: `${md} ${lg}`,
|
||||
textAlign: 'center',
|
||||
marginTop: '60px',
|
||||
},
|
||||
connectText: {
|
||||
letterSpacing: '1px',
|
||||
@@ -33,22 +31,24 @@ const styles = () => ({
|
||||
},
|
||||
})
|
||||
|
||||
const StyledCard = styled(Card)`
|
||||
padding: 20px;
|
||||
`
|
||||
const ConnectDetails = ({ classes }) => (
|
||||
<>
|
||||
<div className={classes.container}>
|
||||
<Row align="center" margin="lg">
|
||||
<Paragraph className={classes.text} noMargin size="lg" weight="bolder">
|
||||
Connect a Wallet
|
||||
</Paragraph>
|
||||
</Row>
|
||||
</div>
|
||||
<Row className={classes.logo} margin="lg">
|
||||
<KeyRing center circleSize={75} dotRight={25} dotSize={25} dotTop={50} keySize={32} mode="error" />
|
||||
<StyledCard>
|
||||
<Row align="center" margin="lg">
|
||||
<Paragraph className={classes.text} noMargin size="xl" weight="bolder">
|
||||
Connect a Wallet
|
||||
</Paragraph>
|
||||
</Row>
|
||||
|
||||
<Row className={classes.logo}>
|
||||
<KeyRing center circleSize={60} dotRight={20} dotSize={20} dotTop={50} keySize={28} mode="error" />
|
||||
</Row>
|
||||
<Block className={classes.connect}>
|
||||
<ConnectButton data-testid="heading-connect-btn" />
|
||||
</Block>
|
||||
</>
|
||||
</StyledCard>
|
||||
)
|
||||
|
||||
export default withStyles(styles as any)(ConnectDetails)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { makeStyles } from '@material-ui/core/styles'
|
||||
import Dot from '@material-ui/icons/FiberManualRecord'
|
||||
import classNames from 'classnames'
|
||||
import * as React from 'react'
|
||||
import { EthHashInfo, Identicon } from '@gnosis.pm/safe-react-components'
|
||||
import { EthHashInfo, Identicon, Card } from '@gnosis.pm/safe-react-components'
|
||||
|
||||
import Spacer from 'src/components/Spacer'
|
||||
import Block from 'src/components/layout/Block'
|
||||
@@ -18,6 +18,7 @@ import { getExplorerInfo } from 'src/config'
|
||||
import { KeyRing } from 'src/components/AppLayout/Header/components/KeyRing'
|
||||
import { CircleDot } from '../CircleDot'
|
||||
import { createStyles } from '@material-ui/core'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import WalletIcon from '../../assets/wallet.svg'
|
||||
|
||||
@@ -91,6 +92,9 @@ const styles = createStyles({
|
||||
},
|
||||
})
|
||||
|
||||
const StyledCard = styled(Card)`
|
||||
padding: 0px;
|
||||
`
|
||||
type Props = {
|
||||
connected: boolean
|
||||
network: ETHEREUM_NETWORK
|
||||
@@ -116,11 +120,11 @@ export const UserDetails = ({
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledCard>
|
||||
<Block className={classes.container}>
|
||||
<Row align="center" className={classes.identicon} margin="md">
|
||||
{connected ? (
|
||||
<Identicon address={userAddress || 'random'} size="lg" />
|
||||
<Identicon address={userAddress || 'random'} size="xxl" />
|
||||
) : (
|
||||
<KeyRing circleSize={75} dotRight={25} dotSize={25} dotTop={50} hideDot keySize={30} mode="warning" />
|
||||
)}
|
||||
@@ -185,11 +189,11 @@ export const UserDetails = ({
|
||||
variant="contained"
|
||||
data-testid="disconnect-btn"
|
||||
>
|
||||
<Paragraph className={classes.disconnectText} color="white" noMargin size="md">
|
||||
<Paragraph className={classes.disconnectText} color="white" noMargin size="lg">
|
||||
Disconnect
|
||||
</Paragraph>
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
</StyledCard>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -89,8 +89,8 @@ const ProviderInfo = ({ connected, provider, userAddress }: ProviderInfoProps):
|
||||
<EthHashInfo
|
||||
hash={userAddress}
|
||||
shortenHash={4}
|
||||
showIdenticon
|
||||
identiconSize="xs"
|
||||
showAvatar
|
||||
avatarSize="xs"
|
||||
textColor={addressColor}
|
||||
textSize="sm"
|
||||
/>
|
||||
|
||||
@@ -45,7 +45,7 @@ const IconContainer = styled.div`
|
||||
`
|
||||
const StyledButton = styled(Button)`
|
||||
&&.MuiButton-root {
|
||||
padding: 0 16px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
*:first-child {
|
||||
margin: 0 4px 0 0;
|
||||
@@ -170,7 +170,7 @@ const SafeHeader = ({
|
||||
<StyledText size="xl">{balance}</StyledText>
|
||||
<StyledButton size="md" disabled={!granted} color="primary" variant="contained" onClick={onNewTransactionClick}>
|
||||
<FixedIcon type="arrowSentWhite" />
|
||||
<Text size="lg" color="white">
|
||||
<Text size="xl" color="white">
|
||||
New transaction
|
||||
</Text>
|
||||
</StyledButton>
|
||||
|
||||
@@ -17,16 +17,16 @@ const Container = styled.div`
|
||||
`
|
||||
|
||||
const HeaderWrapper = styled.nav`
|
||||
height: 54px;
|
||||
height: 52px;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
|
||||
background-color: white;
|
||||
box-shadow: 0 0 4px 0 rgba(212, 212, 211, 0.59);
|
||||
box-shadow: 0 2px 4px 0 rgba(40, 54, 61, 0.18);
|
||||
`
|
||||
|
||||
const BodyWrapper = styled.div`
|
||||
height: calc(100% - 54px);
|
||||
height: calc(100% - 52px);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -41,7 +41,7 @@ const SidebarWrapper = styled.aside`
|
||||
|
||||
padding: 8px 8px 0 8px;
|
||||
background-color: ${({ theme }) => theme.colors.white};
|
||||
border-right: 2px solid ${({ theme }) => theme.colors.separator};
|
||||
box-shadow: 0 2px 4px 0 rgba(40, 54, 61, 0.18);
|
||||
`
|
||||
|
||||
const ContentWrapper = styled.section`
|
||||
|
||||
@@ -73,7 +73,7 @@ export const onConnectButtonClick = async () => {
|
||||
}
|
||||
|
||||
const ConnectButton = (props): React.ReactElement => (
|
||||
<Button color="primary" minWidth={140} onClick={onConnectButtonClick} variant="contained" {...props}>
|
||||
<Button color="primary" minWidth={240} onClick={onConnectButtonClick} variant="contained" {...props}>
|
||||
Connect
|
||||
</Button>
|
||||
)
|
||||
|
||||
@@ -21,7 +21,7 @@ const useStyles = makeStyles({
|
||||
container: {
|
||||
backgroundColor: '#fff',
|
||||
bottom: '0',
|
||||
boxShadow: '0 2px 4px 0 rgba(212, 212, 211, 0.59)',
|
||||
boxShadow: '1px 2px 10px 0 rgba(40, 54, 61, 0.18)',
|
||||
boxSizing: 'border-box',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Text } from '@gnosis.pm/safe-react-components'
|
||||
import React from 'react'
|
||||
import React, { ReactElement } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
@@ -12,11 +12,11 @@ const Icon = styled.img`
|
||||
margin-right: 9px;
|
||||
`
|
||||
|
||||
const CustomIconText = ({ iconUrl, text }: { iconUrl: string; text?: string }) => (
|
||||
type Props = { iconUrl: string | null | undefined; text?: string }
|
||||
|
||||
export const CustomIconText = ({ iconUrl, text }: Props): ReactElement => (
|
||||
<Wrapper>
|
||||
<Icon alt={text} src={iconUrl} />
|
||||
{iconUrl && <Icon alt={text} src={iconUrl} />}
|
||||
{text && <Text size="xl">{text}</Text>}
|
||||
</Wrapper>
|
||||
)
|
||||
|
||||
export default CustomIconText
|
||||
|
||||
@@ -69,7 +69,7 @@ export const BasicTxInfo = ({
|
||||
</Text>
|
||||
<EthHashInfo
|
||||
hash={txRecipient}
|
||||
showIdenticon
|
||||
showAvatar
|
||||
textSize="lg"
|
||||
showCopyBtn
|
||||
explorerUrl={getExplorerInfo(txRecipient)}
|
||||
@@ -96,7 +96,7 @@ export const getParameterElement = (parameter: DecodedDataBasicParameter, index:
|
||||
valueElement = (
|
||||
<EthHashInfo
|
||||
hash={parameter.value}
|
||||
showIdenticon
|
||||
showAvatar
|
||||
textSize="lg"
|
||||
showCopyBtn
|
||||
explorerUrl={getExplorerInfo(parameter.value)}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import { Text } from '@gnosis.pm/safe-react-components'
|
||||
import React, { ReactElement, useState } from 'react'
|
||||
|
||||
import TextField from 'src/components/forms/TextField'
|
||||
import GnoField from 'src/components/forms/Field'
|
||||
import GnoForm from 'src/components/forms/GnoForm'
|
||||
import { required } from 'src/components/forms/validator'
|
||||
|
||||
import { Modal } from '.'
|
||||
|
||||
export default {
|
||||
title: 'Modal',
|
||||
component: Modal,
|
||||
parameters: {
|
||||
children: 'The body of the modal or the whole modal being composed by `Modal.Header` and `Modal.Footer` components',
|
||||
title: 'The title, useful for screen readers',
|
||||
description: 'A description, useful for screen readers',
|
||||
handleClose:
|
||||
'A callback which will be called when an action to close the modal is triggered (Esc, clicking outside, etc)',
|
||||
open: 'If `true`, the modal will be displayed. Hidden otherwise.',
|
||||
},
|
||||
compositionElements: [
|
||||
{
|
||||
title: 'Modal.Header',
|
||||
component: <Modal.Header />,
|
||||
parameters: {
|
||||
title: 'The title that will be displayed in the modal',
|
||||
titleNote: 'An annotation for the title, like "1 of 2"',
|
||||
onClose: 'Callback to be called when attempt to close the modal',
|
||||
},
|
||||
compositionElements: [
|
||||
{
|
||||
title: 'Modal.Header.Title',
|
||||
component: <Modal.Header.Title size="xs">{}</Modal.Header.Title>,
|
||||
description: 'safe-react-component exposed with a few styles added to personalize the modal header',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Modal.Header',
|
||||
component: <Modal.Body>{}</Modal.Body>,
|
||||
parameters: {
|
||||
children: 'whatever is required to be rendered in the footer. Usually buttons.',
|
||||
noPadding: 'a flag that will set padding to 0 (zero) in case it is needed',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Modal.Footer',
|
||||
component: <Modal.Footer>{}</Modal.Footer>,
|
||||
parameters: {
|
||||
children: 'whatever is required to be rendered in the footer. Usually buttons.',
|
||||
},
|
||||
compositionElements: [
|
||||
{
|
||||
title: 'Modal.Footer.Buttons',
|
||||
component: <Modal.Footer.Buttons />,
|
||||
description: 'standard two buttons wrapped implementation. One "Cancel" and one "Submit" button.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const SimpleFormModal = ({ title, description, handleClose, handleSubmit, isOpen, children }) => (
|
||||
<Modal title={title} description={description} handleClose={handleClose} open={isOpen}>
|
||||
{/* header */}
|
||||
<Modal.Header onClose={handleClose}>
|
||||
<Modal.Header.Title>{title}</Modal.Header.Title>
|
||||
</Modal.Header>
|
||||
|
||||
<GnoForm onSubmit={handleSubmit}>
|
||||
{() => (
|
||||
<>
|
||||
{/* body */}
|
||||
<Modal.Body>{children}</Modal.Body>
|
||||
|
||||
{/* footer */}
|
||||
<Modal.Footer>
|
||||
<Modal.Footer.Buttons cancelButtonProps={{ text: 'Close', onClick: handleClose }} />
|
||||
</Modal.Footer>
|
||||
</>
|
||||
)}
|
||||
</GnoForm>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
const Username = () => (
|
||||
<label htmlFor="username">
|
||||
<Text size="lg" strong>
|
||||
Username
|
||||
</Text>
|
||||
<GnoField
|
||||
autoComplete="off"
|
||||
component={TextField}
|
||||
name="username"
|
||||
id="username"
|
||||
placeholder="your username"
|
||||
validate={required}
|
||||
/>
|
||||
</label>
|
||||
)
|
||||
|
||||
export const FormModal = (): ReactElement => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false)
|
||||
console.log('modal closed')
|
||||
}
|
||||
|
||||
const handleSubmit = (values) => {
|
||||
alert(JSON.stringify(values, null, 2))
|
||||
console.log('form submitted', values)
|
||||
handleClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => setIsOpen(true)}>Open Modal</button>
|
||||
{/* Modal with Form */}
|
||||
<SimpleFormModal
|
||||
title="My first modal"
|
||||
description="My first modal description"
|
||||
handleClose={handleClose}
|
||||
handleSubmit={handleSubmit}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
{/* Form Fields */}
|
||||
<Username />
|
||||
</SimpleFormModal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const RemoveSomething = (): ReactElement => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const title = 'Remove Something'
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false)
|
||||
console.log('modal closed')
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
alert('Something was removed')
|
||||
handleClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => setIsOpen(true)}>Open Modal</button>
|
||||
{/* Modal */}
|
||||
<Modal handleClose={handleClose} title={title} description={title} open={isOpen}>
|
||||
{/* Header */}
|
||||
<Modal.Header onClose={handleClose}>
|
||||
<Modal.Header.Title>{title}</Modal.Header.Title>
|
||||
</Modal.Header>
|
||||
|
||||
{/* Body */}
|
||||
<Modal.Body>
|
||||
<Text size="md">You are about to remove something</Text>
|
||||
</Modal.Body>
|
||||
|
||||
{/* Footer */}
|
||||
<Modal.Footer>
|
||||
<Modal.Footer.Buttons
|
||||
cancelButtonProps={{ onClick: handleClose }}
|
||||
confirmButtonProps={{ onClick: handleSubmit, color: 'error', text: 'Remove' }}
|
||||
/>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+244
-46
@@ -1,69 +1,267 @@
|
||||
import Modal from '@material-ui/core/Modal'
|
||||
import { makeStyles, createStyles } from '@material-ui/core/styles'
|
||||
import { Button, Icon, theme, Title as TitleSRC } from '@gnosis.pm/safe-react-components'
|
||||
import { ButtonProps as ButtonPropsMUI, Modal as ModalMUI } from '@material-ui/core'
|
||||
import cn from 'classnames'
|
||||
import React, { ReactElement, ReactNode } from 'react'
|
||||
import React, { ReactElement, ReactNode, ReactNodeArray } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { sm } from 'src/theme/variables'
|
||||
type Theme = typeof theme
|
||||
|
||||
const useStyles = makeStyles(
|
||||
createStyles({
|
||||
root: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
display: 'flex',
|
||||
overflowY: 'scroll',
|
||||
},
|
||||
paper: {
|
||||
position: 'relative',
|
||||
top: '68px',
|
||||
width: '500px',
|
||||
borderRadius: sm,
|
||||
backgroundColor: '#ffffff',
|
||||
boxShadow: '0 0 5px 0 rgba(74, 85, 121, 0.5)',
|
||||
'&:focus': {
|
||||
outline: 'none',
|
||||
},
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
}),
|
||||
)
|
||||
const ModalStyled = styled(ModalMUI)`
|
||||
& {
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
background-color: rgba(232, 231, 230, 0.75) !important;
|
||||
}
|
||||
|
||||
.paper {
|
||||
position: relative;
|
||||
top: 68px;
|
||||
width: 500px;
|
||||
border-radius: 8px;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 1px 2px 10px 0 rgba(40, 54, 61, 0.18);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
// TODO: replace class-based styles by params
|
||||
&.receive-modal {
|
||||
height: auto;
|
||||
max-width: calc(100% - 130px);
|
||||
min-height: 544px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&.bigger-modal-window {
|
||||
width: 775px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
&.smaller-modal-window {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
&.modal {
|
||||
height: auto;
|
||||
max-width: calc(100% - 130px);
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
interface GnoModalProps {
|
||||
children: ReactNode
|
||||
description: string
|
||||
// type copied from Material-UI Modal's `close` prop
|
||||
handleClose?: {
|
||||
bivarianceHack(event: Record<string, unknown>, reason: 'backdropClick' | 'escapeKeyDown'): void
|
||||
}['bivarianceHack']
|
||||
modalClassName?: string
|
||||
handleClose?: (event: Record<string, unknown>, reason: 'backdropClick' | 'escapeKeyDown') => void
|
||||
open: boolean
|
||||
paperClassName?: string
|
||||
title: string
|
||||
}
|
||||
|
||||
const GnoModal = ({
|
||||
children,
|
||||
description,
|
||||
handleClose,
|
||||
modalClassName,
|
||||
open,
|
||||
paperClassName,
|
||||
title,
|
||||
}: GnoModalProps): ReactElement => {
|
||||
const classes = useStyles()
|
||||
|
||||
const GnoModal = ({ children, description, handleClose, open, paperClassName, title }: GnoModalProps): ReactElement => {
|
||||
return (
|
||||
<Modal
|
||||
<ModalStyled
|
||||
BackdropProps={{ className: 'overlay' }}
|
||||
aria-describedby={description}
|
||||
aria-labelledby={title}
|
||||
className={cn(classes.root, modalClassName)}
|
||||
onClose={handleClose}
|
||||
open={open}
|
||||
>
|
||||
<div className={cn(classes.paper, paperClassName, 'classpep')}>{children}</div>
|
||||
</Modal>
|
||||
<div className={cn('paper', paperClassName)}>{children}</div>
|
||||
</ModalStyled>
|
||||
)
|
||||
}
|
||||
|
||||
export default GnoModal
|
||||
|
||||
/*****************/
|
||||
/* Generic Modal */
|
||||
/*****************/
|
||||
|
||||
/*** Header ***/
|
||||
const HeaderSection = styled.div`
|
||||
display: flex;
|
||||
padding: 24px 18px 24px 24px;
|
||||
border-bottom: 2px solid ${({ theme }) => theme.colors.separator};
|
||||
|
||||
h5 {
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
}
|
||||
|
||||
.close-button {
|
||||
align-self: flex-end;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 5px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
|
||||
span {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
:hover {
|
||||
background: ${({ theme }) => theme.colors.separator};
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const TitleStyled = styled(TitleSRC)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-basis: 100%;
|
||||
|
||||
.image,
|
||||
img {
|
||||
width: 20px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.note,
|
||||
span {
|
||||
margin-left: 12px;
|
||||
}
|
||||
`
|
||||
|
||||
interface TitleProps {
|
||||
children: string | ReactNode
|
||||
size?: keyof Theme['title']['size']
|
||||
withoutMargin?: boolean
|
||||
strong?: boolean
|
||||
}
|
||||
|
||||
const Title = ({ children, ...props }: TitleProps): ReactElement => (
|
||||
<TitleStyled size="xs" withoutMargin {...props}>
|
||||
{children}
|
||||
</TitleStyled>
|
||||
)
|
||||
|
||||
interface HeaderProps {
|
||||
children?: ReactNode
|
||||
onClose?: (event: any) => void
|
||||
}
|
||||
|
||||
const Header = ({ children, onClose }: HeaderProps): ReactElement => {
|
||||
return (
|
||||
<HeaderSection className="modal-header">
|
||||
{children}
|
||||
|
||||
{onClose && (
|
||||
<button className="close-button" onClick={onClose}>
|
||||
<Icon size="sm" type="cross" />
|
||||
</button>
|
||||
)}
|
||||
</HeaderSection>
|
||||
)
|
||||
}
|
||||
|
||||
Header.Title = Title
|
||||
|
||||
/*** Body ***/
|
||||
const BodySection = styled.div<{ withoutPadding: BodyProps['withoutPadding'] }>`
|
||||
padding: ${({ withoutPadding }) => (withoutPadding ? 0 : '24px')};
|
||||
`
|
||||
|
||||
interface BodyProps {
|
||||
children: ReactNode | ReactNodeArray
|
||||
withoutPadding?: boolean
|
||||
}
|
||||
|
||||
const Body = ({ children, withoutPadding = false }: BodyProps): ReactElement => (
|
||||
<BodySection className="modal-body" withoutPadding={withoutPadding}>
|
||||
{children}
|
||||
</BodySection>
|
||||
)
|
||||
|
||||
/*** Footer ***/
|
||||
const FooterSection = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-top: 2px solid ${({ theme }) => theme.colors.separator};
|
||||
padding: 24px;
|
||||
`
|
||||
|
||||
const ButtonStyled = styled(Button)`
|
||||
&.MuiButtonBase-root {
|
||||
margin: 0 10px;
|
||||
}
|
||||
`
|
||||
|
||||
type CustomButtonMUIProps = Omit<ButtonPropsMUI, 'size' | 'color' | 'variant'> & {
|
||||
to?: string
|
||||
component?: ReactNode
|
||||
}
|
||||
|
||||
interface ButtonProps extends CustomButtonMUIProps {
|
||||
text?: string
|
||||
size?: keyof Theme['buttons']['size']
|
||||
color?: 'primary' | 'secondary' | 'error'
|
||||
variant?: 'bordered' | 'contained' | 'outlined'
|
||||
}
|
||||
|
||||
interface ButtonsProps {
|
||||
cancelButtonProps?: ButtonProps
|
||||
confirmButtonProps?: ButtonProps
|
||||
}
|
||||
|
||||
const Buttons = ({ cancelButtonProps = {}, confirmButtonProps = {} }: ButtonsProps): ReactElement => {
|
||||
const { text: cancelText = 'Cancel' } = cancelButtonProps
|
||||
const { text: confirmText = 'Submit' } = confirmButtonProps
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonStyled
|
||||
size="md"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
type={cancelButtonProps?.onClick ? 'button' : 'submit'}
|
||||
{...cancelButtonProps}
|
||||
>
|
||||
{cancelText}
|
||||
</ButtonStyled>
|
||||
<ButtonStyled size="md" type={confirmButtonProps?.onClick ? 'button' : 'submit'} {...confirmButtonProps}>
|
||||
{confirmText}
|
||||
</ButtonStyled>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface FooterProps {
|
||||
children: ReactNode | ReactNodeArray
|
||||
}
|
||||
|
||||
const Footer = ({ children }: FooterProps): ReactElement => (
|
||||
<FooterSection className="modal-footer">{children}</FooterSection>
|
||||
)
|
||||
|
||||
Footer.Buttons = Buttons
|
||||
|
||||
interface ModalProps {
|
||||
children: ReactNode
|
||||
description: string
|
||||
handleClose: () => void
|
||||
open: boolean
|
||||
title: string
|
||||
}
|
||||
|
||||
export const Modal = ({ children, ...props }: ModalProps): ReactElement => {
|
||||
return (
|
||||
<GnoModal {...props} paperClassName="modal">
|
||||
{children}
|
||||
</GnoModal>
|
||||
)
|
||||
}
|
||||
|
||||
Modal.Header = Header
|
||||
Modal.Body = Body
|
||||
Modal.Footer = Footer
|
||||
|
||||
@@ -62,7 +62,7 @@ export const AddressWrapper = (props: Props): React.ReactElement => {
|
||||
|
||||
return (
|
||||
<div className={classes.wrapper}>
|
||||
<EthHashInfo hash={safe.address} name={safe.name} showIdenticon shortenHash={4} />
|
||||
<EthHashInfo hash={safe.address} name={safe.name} showAvatar shortenHash={4} />
|
||||
|
||||
<div className={classes.addressDetails}>
|
||||
<Text size="xl">{`${formatAmount(safe.ethBalance)} ${nativeCoin.name}`}</Text>
|
||||
|
||||
@@ -41,7 +41,7 @@ export const TransactionFailText = ({
|
||||
if (isExecution) {
|
||||
errorMessage =
|
||||
threshold && threshold > 1
|
||||
? `To save gas costs, cancel this transaction`
|
||||
? `To save gas costs, reject this transaction`
|
||||
: `To save gas costs, avoid executing the transaction.`
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig } from 'src/config
|
||||
|
||||
const baseConfig: EnvironmentSettings = {
|
||||
clientGatewayUrl: 'https://safe-client.rinkeby.staging.gnosisdev.com/v1',
|
||||
txServiceUrl: 'https://safe-transaction.staging.gnosisdev.com/api/v1',
|
||||
txServiceUrl: 'https://safe-transaction.rinkeby.staging.gnosisdev.com/api/v1',
|
||||
safeAppsUrl: 'https://safe-apps.dev.gnosisdev.com',
|
||||
gasPriceOracle: {
|
||||
url: 'https://ethgasstation.info/json/ethgasAPI.json',
|
||||
|
||||
@@ -36,8 +36,8 @@ const mainnet: NetworkConfig = {
|
||||
isTestNet: true,
|
||||
nativeCoin: {
|
||||
address: '0x0000000000000000000000000000000000000000',
|
||||
name: 'Energy web token',
|
||||
symbol: 'EWT',
|
||||
name: 'Volta Token',
|
||||
symbol: 'VT',
|
||||
decimals: 18,
|
||||
logoUri: EwcLogo,
|
||||
},
|
||||
|
||||
@@ -52,17 +52,6 @@ const getProxyFactoryContract = (web3: Web3, networkId: ETHEREUM_NETWORK): Gnosi
|
||||
return (new web3.eth.Contract(ProxyFactorySol.abi as AbiItem[], contractAddress) as unknown) as GnosisSafeProxyFactory
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Contract instance of the GnosisSafeProxyFactory contract
|
||||
*/
|
||||
export const getSpendingLimitContract = () => {
|
||||
const web3 = getWeb3()
|
||||
return (new web3.eth.Contract(
|
||||
SpendingLimitModule.abi as AbiItem[],
|
||||
SPENDING_LIMIT_MODULE_ADDRESS,
|
||||
) as unknown) as AllowanceModule
|
||||
}
|
||||
|
||||
export const getMasterCopyAddressFromProxyAddress = async (proxyAddress: string): Promise<string | undefined> => {
|
||||
const res = await getSafeInfo(proxyAddress)
|
||||
const masterCopyAddress = (res as SafeInfo)?.masterCopy
|
||||
@@ -115,7 +104,7 @@ export const estimateGasForDeployingSafe = async (
|
||||
userAccount: string,
|
||||
safeCreationSalt: number,
|
||||
) => {
|
||||
const gnosisSafeData = await safeMaster.methods
|
||||
const gnosisSafeData = safeMaster.methods
|
||||
.setup(
|
||||
safeAccounts,
|
||||
numConfirmations,
|
||||
@@ -134,10 +123,22 @@ export const estimateGasForDeployingSafe = async (
|
||||
data: proxyFactoryData,
|
||||
from: userAccount,
|
||||
to: proxyFactoryMaster.options.address,
|
||||
})
|
||||
}).then((value) => value * 2)
|
||||
}
|
||||
|
||||
export const getGnosisSafeInstanceAt = (safeAddress: string): GnosisSafe => {
|
||||
const web3 = getWeb3()
|
||||
return (new web3.eth.Contract(GnosisSafeSol.abi as AbiItem[], safeAddress) as unknown) as GnosisSafe
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Contract instance of the SpendingLimitModule contract
|
||||
*/
|
||||
export const getSpendingLimitContract = () => {
|
||||
const web3 = getWeb3()
|
||||
|
||||
return (new web3.eth.Contract(
|
||||
SpendingLimitModule.abi as AbiItem[],
|
||||
SPENDING_LIMIT_MODULE_ADDRESS,
|
||||
) as unknown) as AllowanceModule
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { List } from 'immutable'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import { getNetworkInfo } from 'src/config'
|
||||
import {
|
||||
estimateGasForTransactionApproval,
|
||||
estimateGasForTransactionCreation,
|
||||
estimateGasForTransactionExecution,
|
||||
getFixedGasCosts,
|
||||
SAFE_TX_GAS_DATA_COST,
|
||||
checkTransactionExecution,
|
||||
estimateSafeTxGas,
|
||||
estimateTransactionGasLimit,
|
||||
} from 'src/logic/safe/transactions/gas'
|
||||
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
|
||||
import { calculateGasPrice } from 'src/logic/wallets/ethTransactions'
|
||||
import { getNetworkInfo } from 'src/config'
|
||||
import { useSelector } from 'react-redux'
|
||||
import {
|
||||
safeCurrentVersionSelector,
|
||||
safeParamAddressFromStateSelector,
|
||||
@@ -21,7 +20,6 @@ import { CALL } from 'src/logic/safe/transactions'
|
||||
import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3'
|
||||
import { providerSelector } from 'src/logic/wallets/store/selectors'
|
||||
|
||||
import { List } from 'immutable'
|
||||
import { Confirmation } from 'src/logic/safe/store/models/types/confirmation'
|
||||
import { checkIfOffChainSignatureIsPossible } from 'src/logic/safe/safeTxSigner'
|
||||
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
||||
@@ -64,90 +62,16 @@ export const checkIfTxIsApproveAndExecution = (
|
||||
return txConfirmations + 1 === threshold || sameString(txType, 'spendingLimit')
|
||||
}
|
||||
|
||||
if (threshold === 1) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export const checkIfTxIsCreation = (txConfirmations: number, txType?: string): boolean =>
|
||||
txConfirmations === 0 && !sameString(txType, 'spendingLimit')
|
||||
|
||||
type TransactionEstimationProps = {
|
||||
txData: string
|
||||
safeAddress: string
|
||||
txRecipient: string
|
||||
txConfirmations?: List<Confirmation>
|
||||
txAmount?: string
|
||||
operation?: number
|
||||
gasPrice?: string
|
||||
gasToken?: string
|
||||
refundReceiver?: string // Address of receiver of gas payment (or 0 if tx.origin).
|
||||
safeTxGas?: number
|
||||
from?: string
|
||||
isExecution: boolean
|
||||
isCreation: boolean
|
||||
isOffChainSignature?: boolean
|
||||
approvalAndExecution?: boolean
|
||||
}
|
||||
|
||||
const estimateTransactionGas = async ({
|
||||
txData,
|
||||
safeAddress,
|
||||
txRecipient,
|
||||
txConfirmations,
|
||||
txAmount,
|
||||
operation,
|
||||
gasPrice,
|
||||
gasToken,
|
||||
refundReceiver,
|
||||
safeTxGas,
|
||||
from,
|
||||
isExecution,
|
||||
isCreation,
|
||||
isOffChainSignature = false,
|
||||
approvalAndExecution,
|
||||
}: TransactionEstimationProps): Promise<number> => {
|
||||
if (isCreation) {
|
||||
return estimateGasForTransactionCreation(
|
||||
safeAddress,
|
||||
txData,
|
||||
txRecipient,
|
||||
txAmount || '0',
|
||||
operation || CALL,
|
||||
safeTxGas,
|
||||
)
|
||||
}
|
||||
|
||||
if (!from) {
|
||||
throw new Error('No from provided for approving or execute transaction')
|
||||
}
|
||||
|
||||
if (isExecution) {
|
||||
return estimateGasForTransactionExecution({
|
||||
safeAddress,
|
||||
txRecipient,
|
||||
txConfirmations,
|
||||
txAmount: txAmount || '0',
|
||||
txData,
|
||||
operation: operation || CALL,
|
||||
from,
|
||||
gasPrice: gasPrice || '0',
|
||||
gasToken: gasToken || ZERO_ADDRESS,
|
||||
refundReceiver: refundReceiver || ZERO_ADDRESS,
|
||||
safeTxGas: safeTxGas || 0,
|
||||
approvalAndExecution,
|
||||
})
|
||||
}
|
||||
|
||||
return estimateGasForTransactionApproval({
|
||||
safeAddress,
|
||||
operation: operation || CALL,
|
||||
txData,
|
||||
txAmount: txAmount || '0',
|
||||
txRecipient,
|
||||
from,
|
||||
isOffChainSignature,
|
||||
})
|
||||
}
|
||||
|
||||
type UseEstimateTransactionGasProps = {
|
||||
txData: string
|
||||
txRecipient: string
|
||||
@@ -158,6 +82,7 @@ type UseEstimateTransactionGasProps = {
|
||||
safeTxGas?: number
|
||||
txType?: string
|
||||
manualGasPrice?: string
|
||||
manualGasLimit?: string
|
||||
}
|
||||
|
||||
export type TransactionGasEstimationResult = {
|
||||
@@ -183,6 +108,7 @@ export const useEstimateTransactionGas = ({
|
||||
safeTxGas,
|
||||
txType,
|
||||
manualGasPrice,
|
||||
manualGasLimit,
|
||||
}: UseEstimateTransactionGasProps): TransactionGasEstimationResult => {
|
||||
const [gasEstimation, setGasEstimation] = useState<TransactionGasEstimationResult>({
|
||||
txEstimationExecutionStatus: EstimationStatus.LOADING,
|
||||
@@ -208,51 +134,79 @@ export const useEstimateTransactionGas = ({
|
||||
return
|
||||
}
|
||||
|
||||
const isExecution = checkIfTxIsExecution(Number(threshold), preApprovingOwner, txConfirmations?.size, txType)
|
||||
const isCreation = checkIfTxIsCreation(txConfirmations?.size || 0, txType)
|
||||
const isExecution = checkIfTxIsExecution(Number(threshold), preApprovingOwner, txConfirmations?.size, txType)
|
||||
const approvalAndExecution = checkIfTxIsApproveAndExecution(
|
||||
Number(threshold),
|
||||
txConfirmations?.size || 0,
|
||||
txType,
|
||||
preApprovingOwner,
|
||||
)
|
||||
|
||||
const fixedGasCosts = getFixedGasCosts(Number(threshold))
|
||||
const isOffChainSignature = checkIfOffChainSignatureIsPossible(isExecution, smartContractWallet, safeVersion)
|
||||
|
||||
try {
|
||||
const gasEstimation = await estimateTransactionGas({
|
||||
safeAddress,
|
||||
txRecipient,
|
||||
txData,
|
||||
txAmount,
|
||||
txConfirmations,
|
||||
isExecution,
|
||||
isCreation,
|
||||
isOffChainSignature,
|
||||
operation,
|
||||
from,
|
||||
safeTxGas,
|
||||
approvalAndExecution,
|
||||
})
|
||||
let safeTxGasEstimation = safeTxGas || 0
|
||||
let ethGasLimitEstimation = 0
|
||||
let transactionCallSuccess = true
|
||||
let txEstimationExecutionStatus = EstimationStatus.LOADING
|
||||
|
||||
if (isCreation) {
|
||||
safeTxGasEstimation = await estimateSafeTxGas({
|
||||
safeAddress,
|
||||
txData,
|
||||
txRecipient,
|
||||
txAmount: txAmount || '0',
|
||||
operation: operation || CALL,
|
||||
safeTxGas,
|
||||
})
|
||||
}
|
||||
if (isExecution || approvalAndExecution) {
|
||||
ethGasLimitEstimation = await estimateTransactionGasLimit({
|
||||
safeAddress,
|
||||
txRecipient,
|
||||
txData,
|
||||
txAmount: txAmount || '0',
|
||||
txConfirmations,
|
||||
isExecution,
|
||||
isOffChainSignature,
|
||||
operation: operation || CALL,
|
||||
from,
|
||||
safeTxGas: safeTxGasEstimation,
|
||||
approvalAndExecution,
|
||||
})
|
||||
}
|
||||
|
||||
const totalGasEstimation = (gasEstimation + fixedGasCosts) * 2
|
||||
const gasPrice = manualGasPrice ? web3.utils.toWei(manualGasPrice, 'gwei') : await calculateGasPrice()
|
||||
const gasPriceFormatted = web3.utils.fromWei(gasPrice, 'gwei')
|
||||
const estimatedGasCosts = totalGasEstimation * parseInt(gasPrice, 10)
|
||||
const estimatedGasCosts = ethGasLimitEstimation * parseInt(gasPrice, 10)
|
||||
const gasCost = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals)
|
||||
const gasCostFormatted = formatAmount(gasCost)
|
||||
const gasLimit = totalGasEstimation.toString()
|
||||
const gasLimit = manualGasLimit || ethGasLimitEstimation.toString()
|
||||
|
||||
let txEstimationExecutionStatus = EstimationStatus.SUCCESS
|
||||
|
||||
if (gasEstimation <= 0) {
|
||||
txEstimationExecutionStatus = isOffChainSignature ? EstimationStatus.SUCCESS : EstimationStatus.FAILURE
|
||||
txEstimationExecutionStatus = EstimationStatus.SUCCESS
|
||||
if (isExecution) {
|
||||
transactionCallSuccess = await checkTransactionExecution({
|
||||
safeAddress,
|
||||
txRecipient,
|
||||
txData,
|
||||
txAmount: txAmount || '0',
|
||||
txConfirmations,
|
||||
operation: operation || CALL,
|
||||
from,
|
||||
gasPrice: '0',
|
||||
gasToken: ZERO_ADDRESS,
|
||||
gasLimit,
|
||||
refundReceiver: ZERO_ADDRESS,
|
||||
safeTxGas: safeTxGasEstimation,
|
||||
approvalAndExecution,
|
||||
})
|
||||
}
|
||||
|
||||
txEstimationExecutionStatus = transactionCallSuccess ? EstimationStatus.SUCCESS : EstimationStatus.FAILURE
|
||||
|
||||
setGasEstimation({
|
||||
txEstimationExecutionStatus,
|
||||
gasEstimation,
|
||||
gasEstimation: safeTxGasEstimation,
|
||||
gasCost,
|
||||
gasCostFormatted,
|
||||
gasPrice,
|
||||
@@ -264,15 +218,12 @@ export const useEstimateTransactionGas = ({
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(error.message)
|
||||
// We put a fixed the amount of gas to let the user try to execute the tx, but it's not accurate so it will probably fail
|
||||
const gasEstimation = fixedGasCosts + SAFE_TX_GAS_DATA_COST
|
||||
const gasCost = fromTokenUnit(gasEstimation, nativeCoin.decimals)
|
||||
const gasCostFormatted = formatAmount(gasCost)
|
||||
// If safeTxGas estimation fail we set this value to 0 (so up to all gasLimit can be used)
|
||||
setGasEstimation({
|
||||
txEstimationExecutionStatus: EstimationStatus.FAILURE,
|
||||
gasEstimation,
|
||||
gasCost,
|
||||
gasCostFormatted,
|
||||
gasEstimation: 0,
|
||||
gasCost: '0',
|
||||
gasCostFormatted: '< 0.001',
|
||||
gasPrice: '1',
|
||||
gasPriceFormatted: '1',
|
||||
gasLimit: '0',
|
||||
@@ -301,6 +252,7 @@ export const useEstimateTransactionGas = ({
|
||||
txType,
|
||||
providerName,
|
||||
manualGasPrice,
|
||||
manualGasLimit,
|
||||
])
|
||||
|
||||
return gasEstimation
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import axios from 'axios'
|
||||
|
||||
import { getSafeServiceBaseUrl } from 'src/config'
|
||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||
|
||||
export type GasEstimationResponse = {
|
||||
safeTxGas: string
|
||||
}
|
||||
|
||||
type FetchSafeTxGasEstimationProps = {
|
||||
safeAddress: string
|
||||
to: string
|
||||
value: string
|
||||
data?: string
|
||||
operation: number
|
||||
}
|
||||
|
||||
export const fetchSafeTxGasEstimation = async ({
|
||||
safeAddress,
|
||||
...body
|
||||
}: FetchSafeTxGasEstimationProps): Promise<string> => {
|
||||
const url = `${getSafeServiceBaseUrl(checksumAddress(safeAddress))}/multisig-transactions/estimations/`
|
||||
|
||||
return axios.post(url, body).then(({ data }) => data.safeTxGas)
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
import axios from 'axios'
|
||||
|
||||
import { getSafeClientGatewayBaseUrl, getNetworkInfo } from 'src/config'
|
||||
import { getSafeClientGatewayBaseUrl } from 'src/config'
|
||||
import { TokenProps } from 'src/logic/tokens/store/model/token'
|
||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||
|
||||
import { ZERO_ADDRESS, sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
|
||||
export type TokenBalance = {
|
||||
tokenInfo: TokenProps
|
||||
balance: string
|
||||
@@ -35,24 +33,5 @@ export const fetchTokenCurrenciesBalances = async ({
|
||||
checksumAddress(safeAddress),
|
||||
)}/balances/${selectedCurrency}/?trusted=${trustedTokens}&exclude_spam=${excludeSpamTokens}`
|
||||
|
||||
return axios.get(url).then(({ data }) => {
|
||||
// Currently the client-gateway is not returning the balance using network token symbol and name
|
||||
// FIXME remove this logic and return data directly once this is fixed
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
|
||||
if (data.items && data.items.length) {
|
||||
data.items = data.items.map((element) => {
|
||||
const { tokenInfo } = element
|
||||
if (sameAddress(ZERO_ADDRESS, tokenInfo.address)) {
|
||||
// If it's native coin we swap symbol and name
|
||||
tokenInfo.symbol = nativeCoin.symbol
|
||||
tokenInfo.name = nativeCoin.name
|
||||
}
|
||||
|
||||
return element
|
||||
})
|
||||
}
|
||||
|
||||
return data
|
||||
})
|
||||
return axios.get(url).then(({ data }) => data)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
saveTxToHistory,
|
||||
tryOffchainSigning,
|
||||
} from 'src/logic/safe/transactions'
|
||||
import { estimateGasForTransactionCreation } from 'src/logic/safe/transactions/gas'
|
||||
import { estimateSafeTxGas } from 'src/logic/safe/transactions/gas'
|
||||
import * as aboutToExecuteTx from 'src/logic/safe/utils/aboutToExecuteTx'
|
||||
import { getCurrentSafeVersion } from 'src/logic/safe/utils/safeVersion'
|
||||
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
||||
@@ -78,7 +78,7 @@ export const createTransaction = (
|
||||
if (!ready) return
|
||||
|
||||
const { account: from, hardwareWallet, smartContractWallet } = providerSelector(state)
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const safeInstance = getGnosisSafeInstanceAt(safeAddress)
|
||||
const lastTx = await getLastTx(safeAddress)
|
||||
const nextNonce = await getNewTxNonce(lastTx, safeInstance)
|
||||
const nonce = txNonce !== undefined ? txNonce.toString() : nextNonce
|
||||
@@ -88,7 +88,7 @@ export const createTransaction = (
|
||||
let safeTxGas = safeTxGasArg || 0
|
||||
try {
|
||||
if (safeTxGasArg === undefined) {
|
||||
safeTxGas = await estimateGasForTransactionCreation(safeAddress, txData, to, valueInWei, operation)
|
||||
safeTxGas = await estimateSafeTxGas({ safeAddress, txData, txRecipient: to, txAmount: valueInWei, operation })
|
||||
}
|
||||
} catch (error) {
|
||||
safeTxGas = safeTxGasArg || 0
|
||||
|
||||
@@ -73,7 +73,7 @@ export const processTransaction = ({
|
||||
const state = getState()
|
||||
|
||||
const { account: from, hardwareWallet, smartContractWallet } = providerSelector(state)
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const safeInstance = getGnosisSafeInstanceAt(safeAddress)
|
||||
|
||||
const lastTx = await getLastTx(safeAddress)
|
||||
const nonce = await getNewTxNonce(lastTx, safeInstance)
|
||||
|
||||
+2
-2
@@ -133,7 +133,7 @@ type BaseCustom = {
|
||||
dataSize: string
|
||||
value: string
|
||||
isCancellation: boolean
|
||||
toInfo: AddressInfo
|
||||
toInfo?: AddressInfo
|
||||
}
|
||||
|
||||
type Custom = BaseCustom & {
|
||||
@@ -236,7 +236,7 @@ type MultiSigExecutionDetails = {
|
||||
type DetailedExecutionInfo = ModuleExecutionDetails | MultiSigExecutionDetails
|
||||
|
||||
type ExpandedTxDetails = {
|
||||
executedAt: number
|
||||
executedAt: number | null
|
||||
txStatus: TransactionStatus
|
||||
txInfo: TransactionInfo
|
||||
txData: TransactionData | null
|
||||
|
||||
+142
-137
@@ -1,42 +1,18 @@
|
||||
import axios from 'axios'
|
||||
import { BigNumber } from 'bignumber.js'
|
||||
import { List } from 'immutable'
|
||||
|
||||
import { getRpcServiceUrl, usesInfuraRPC } from 'src/config'
|
||||
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||
import { calculateGasOf, EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||
import { getWeb3, web3ReadOnly } from 'src/logic/wallets/getWeb3'
|
||||
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
||||
import { generateSignaturesFromTxConfirmations } from 'src/logic/safe/safeTxSigner'
|
||||
import { List } from 'immutable'
|
||||
import { fetchSafeTxGasEstimation } from 'src/logic/safe/api/fetchSafeTxGasEstimation'
|
||||
import { Confirmation } from 'src/logic/safe/store/models/types/confirmation'
|
||||
import axios from 'axios'
|
||||
import { getRpcServiceUrl, usesInfuraRPC } from 'src/config'
|
||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||
import { sameString } from 'src/utils/strings'
|
||||
|
||||
// 21000 - additional gas costs (e.g. base tx costs, transfer costs)
|
||||
export const MINIMUM_TRANSACTION_GAS = 21000
|
||||
// Estimation of gas required for each signature (aproximately 7800, roundup to 8000)
|
||||
export const GAS_REQUIRED_PER_SIGNATURE = 8000
|
||||
// We require some gas to emit the events (at least 2500) after the execution and some to perform code until the execution (500)
|
||||
// We also add 3k pay when processing safeTxGas value. We don't know this value when creating the transaction
|
||||
// Hex values different than 0 has some gas cost
|
||||
export const SAFE_TX_GAS_DATA_COST = 6000
|
||||
|
||||
// Receives the response data of the safe method requiredTxGas() and parses it to get the gas amount
|
||||
const parseRequiredTxGasResponse = (data: string): number => {
|
||||
const reducer = (accumulator, currentValue) => {
|
||||
if (currentValue === EMPTY_DATA) {
|
||||
return accumulator + 0
|
||||
}
|
||||
|
||||
if (currentValue === '00') {
|
||||
return accumulator + 4
|
||||
}
|
||||
|
||||
return accumulator + 16
|
||||
}
|
||||
|
||||
return data.match(/.{2}/g)?.reduce(reducer, 0)
|
||||
}
|
||||
|
||||
interface ErrorDataJson extends JSON {
|
||||
originalError?: {
|
||||
data?: string
|
||||
@@ -178,94 +154,113 @@ export const getGasEstimationTxResponse = async (txConfig: {
|
||||
return estimateGasWithWeb3Provider(txConfig)
|
||||
}
|
||||
|
||||
const calculateMinimumGasForTransaction = async (
|
||||
additionalGasBatches: number[],
|
||||
safeAddress: string,
|
||||
estimateData: string,
|
||||
safeTxGasEstimation: number,
|
||||
fixedGasCosts: number,
|
||||
): Promise<number> => {
|
||||
for (const additionalGas of additionalGasBatches) {
|
||||
const batchedSafeTxGas = safeTxGasEstimation + additionalGas
|
||||
// To simulate if safeTxGas is enough we need to send an estimated gasLimit that will be the sum
|
||||
// of the safeTxGasEstimation and fixedGas costs for ethereum transaction
|
||||
const gasLimit = batchedSafeTxGas + fixedGasCosts
|
||||
console.info(`Estimating safeTxGas with gas amount: ${batchedSafeTxGas}`)
|
||||
try {
|
||||
const estimation = await getGasEstimationTxResponse({
|
||||
to: safeAddress,
|
||||
from: safeAddress,
|
||||
data: estimateData,
|
||||
gasPrice: 0,
|
||||
gas: gasLimit,
|
||||
})
|
||||
if (estimation > 0) {
|
||||
console.info(`Gas estimation successfully finished with gas amount: ${batchedSafeTxGas}`)
|
||||
return batchedSafeTxGas
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Error trying to estimate gas with amount: ${batchedSafeTxGas}`)
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
type SafeTxGasEstimationProps = {
|
||||
safeAddress: string
|
||||
txData: string
|
||||
txRecipient: string
|
||||
txAmount: string
|
||||
operation: number
|
||||
safeTxGas?: number
|
||||
}
|
||||
|
||||
export const getFixedGasCosts = (threshold: number): number => {
|
||||
// There are some minimum gas costs to execute an Ethereum transaction
|
||||
// We add this fixed network minimum gas, the gas required to check each signature
|
||||
return MINIMUM_TRANSACTION_GAS + (threshold || 1) * GAS_REQUIRED_PER_SIGNATURE
|
||||
}
|
||||
|
||||
export const estimateGasForTransactionCreation = async (
|
||||
safeAddress: string,
|
||||
data: string,
|
||||
to: string,
|
||||
valueInWei: string,
|
||||
operation: number,
|
||||
safeTxGas?: number,
|
||||
): Promise<number> => {
|
||||
export const estimateSafeTxGas = async ({
|
||||
safeAddress,
|
||||
txData,
|
||||
txRecipient,
|
||||
txAmount,
|
||||
operation,
|
||||
safeTxGas,
|
||||
}: SafeTxGasEstimationProps): Promise<number> => {
|
||||
try {
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
|
||||
const estimateData = safeInstance.methods.requiredTxGas(to, valueInWei, data, operation).encodeABI()
|
||||
const threshold = await safeInstance.methods.getThreshold().call()
|
||||
|
||||
const fixedGasCosts = getFixedGasCosts(Number(threshold))
|
||||
|
||||
const gasEstimationResponse = await getGasEstimationTxResponse({
|
||||
to: safeAddress,
|
||||
from: safeAddress,
|
||||
data: estimateData,
|
||||
gas: safeTxGas ? safeTxGas + fixedGasCosts : undefined,
|
||||
const safeTxGasEstimation = await fetchSafeTxGasEstimation({
|
||||
safeAddress,
|
||||
to: checksumAddress(txRecipient),
|
||||
value: txAmount,
|
||||
data: txData,
|
||||
operation,
|
||||
})
|
||||
|
||||
console.log('Backend gas estimation', safeTxGasEstimation)
|
||||
|
||||
if (safeTxGas) {
|
||||
// When we execute we get a more precise estimate value, we log for debug purposes
|
||||
console.info('This is the smart contract minimum expected safeTxGas', gasEstimationResponse)
|
||||
// If safeTxGas was already defined we leave it but log our estimation for debug purposes
|
||||
console.info('This is the smart contract minimum expected safeTxGas', safeTxGasEstimation)
|
||||
// We return set safeTxGas
|
||||
return safeTxGas
|
||||
}
|
||||
|
||||
const dataGasEstimation = parseRequiredTxGasResponse(estimateData)
|
||||
// Adding this values we should get the full safeTxGas value
|
||||
const safeTxGasEstimation = gasEstimationResponse + dataGasEstimation + SAFE_TX_GAS_DATA_COST
|
||||
// We will add gas batches in case is not enough
|
||||
const additionalGasBatches = [0, 10000, 20000, 40000, 80000, 160000, 320000, 640000, 1280000, 2560000, 5120000]
|
||||
|
||||
return await calculateMinimumGasForTransaction(
|
||||
additionalGasBatches,
|
||||
safeAddress,
|
||||
estimateData,
|
||||
safeTxGasEstimation,
|
||||
fixedGasCosts,
|
||||
)
|
||||
return parseInt(safeTxGasEstimation)
|
||||
} catch (error) {
|
||||
console.info('Error calculating tx gas estimation', error.message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
type TransactionEstimationProps = {
|
||||
txData: string
|
||||
safeAddress: string
|
||||
txRecipient: string
|
||||
txConfirmations?: List<Confirmation>
|
||||
txAmount: string
|
||||
operation: number
|
||||
gasPrice?: string
|
||||
gasToken?: string
|
||||
refundReceiver?: string // Address of receiver of gas payment (or 0 if tx.origin).
|
||||
safeTxGas?: number
|
||||
from?: string
|
||||
isExecution: boolean
|
||||
isOffChainSignature?: boolean
|
||||
approvalAndExecution?: boolean
|
||||
}
|
||||
|
||||
export const estimateTransactionGasLimit = async ({
|
||||
txData,
|
||||
safeAddress,
|
||||
txRecipient,
|
||||
txConfirmations,
|
||||
txAmount,
|
||||
operation,
|
||||
gasPrice,
|
||||
gasToken,
|
||||
refundReceiver,
|
||||
safeTxGas,
|
||||
from,
|
||||
isExecution,
|
||||
isOffChainSignature = false,
|
||||
approvalAndExecution,
|
||||
}: TransactionEstimationProps): Promise<number> => {
|
||||
if (!from) {
|
||||
throw new Error('No from provided for approving or execute transaction')
|
||||
}
|
||||
|
||||
if (isExecution) {
|
||||
return estimateGasForTransactionExecution({
|
||||
safeAddress,
|
||||
txRecipient,
|
||||
txConfirmations,
|
||||
txAmount,
|
||||
txData,
|
||||
operation,
|
||||
from,
|
||||
gasPrice: gasPrice || '0',
|
||||
gasToken: gasToken || ZERO_ADDRESS,
|
||||
refundReceiver: refundReceiver || ZERO_ADDRESS,
|
||||
safeTxGas: safeTxGas || 0,
|
||||
approvalAndExecution,
|
||||
})
|
||||
}
|
||||
|
||||
return estimateGasForTransactionApproval({
|
||||
safeAddress,
|
||||
operation,
|
||||
txData,
|
||||
txAmount,
|
||||
txRecipient,
|
||||
from,
|
||||
isOffChainSignature,
|
||||
})
|
||||
}
|
||||
|
||||
type TransactionExecutionEstimationProps = {
|
||||
txData: string
|
||||
safeAddress: string
|
||||
@@ -275,65 +270,75 @@ type TransactionExecutionEstimationProps = {
|
||||
operation: number
|
||||
gasPrice: string
|
||||
gasToken: string
|
||||
gasLimit?: string
|
||||
refundReceiver: string // Address of receiver of gas payment (or 0 if tx.origin).
|
||||
safeTxGas: number
|
||||
from: string
|
||||
approvalAndExecution?: boolean
|
||||
}
|
||||
|
||||
export const estimateGasForTransactionExecution = async ({
|
||||
const estimateGasForTransactionExecution = async ({
|
||||
safeAddress,
|
||||
txRecipient,
|
||||
txConfirmations,
|
||||
txAmount,
|
||||
txData,
|
||||
operation,
|
||||
from,
|
||||
gasPrice,
|
||||
gasToken,
|
||||
refundReceiver,
|
||||
safeTxGas,
|
||||
approvalAndExecution,
|
||||
}: TransactionExecutionEstimationProps): Promise<number> => {
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
try {
|
||||
let gasEstimation
|
||||
// If safeTxGas === 0 we still have to estimate the gas limit to execute the transaction so we need to get an estimation
|
||||
if (approvalAndExecution || safeTxGas === 0) {
|
||||
console.info(`Estimating transaction necessary gas...`)
|
||||
// @todo (agustin) once we solve the problem with the preApprovingOwner, we need to use the method bellow (execTransaction) with sigs = generateSignaturesFromTxConfirmations(txConfirmations,from)
|
||||
gasEstimation = await estimateGasForTransactionCreation(
|
||||
safeAddress,
|
||||
txData,
|
||||
txRecipient,
|
||||
txAmount,
|
||||
operation,
|
||||
safeTxGas,
|
||||
)
|
||||
const safeInstance = getGnosisSafeInstanceAt(safeAddress)
|
||||
// If it's approvalAndExecution we have to add a preapproved signature else we have all signatures
|
||||
const sigs = generateSignaturesFromTxConfirmations(txConfirmations, approvalAndExecution ? from : undefined)
|
||||
|
||||
if (approvalAndExecution) {
|
||||
// If it's approve and execute we don't have all the signatures to do a complete simulation, we return the gas estimation
|
||||
console.info(`Gas estimation successfully finished with gas amount: ${gasEstimation}`)
|
||||
return gasEstimation
|
||||
}
|
||||
}
|
||||
// If we have all signatures we can do a call to ensure the transaction will be successful or fail
|
||||
const sigs = generateSignaturesFromTxConfirmations(txConfirmations)
|
||||
console.info(`Check transaction success with gas amount: ${safeTxGas}...`)
|
||||
await safeInstance.methods
|
||||
.execTransaction(txRecipient, txAmount, txData, operation, safeTxGas, 0, gasPrice, gasToken, refundReceiver, sigs)
|
||||
.call()
|
||||
console.info(`Gas estimation successfully finished with gas amount: ${safeTxGas}`)
|
||||
return safeTxGas || gasEstimation
|
||||
} catch (error) {
|
||||
throw new Error(`Gas estimation failed with gas amount: ${safeTxGas}`)
|
||||
}
|
||||
const estimationData = safeInstance.methods
|
||||
.execTransaction(txRecipient, txAmount, txData, operation, safeTxGas, 0, gasPrice, gasToken, refundReceiver, sigs)
|
||||
.encodeABI()
|
||||
|
||||
return calculateGasOf({
|
||||
data: estimationData,
|
||||
from,
|
||||
to: safeAddress,
|
||||
})
|
||||
}
|
||||
|
||||
export const checkTransactionExecution = async ({
|
||||
safeAddress,
|
||||
txRecipient,
|
||||
txConfirmations,
|
||||
txAmount,
|
||||
txData,
|
||||
operation,
|
||||
from,
|
||||
gasPrice,
|
||||
gasToken,
|
||||
gasLimit,
|
||||
refundReceiver,
|
||||
safeTxGas,
|
||||
approvalAndExecution,
|
||||
}: TransactionExecutionEstimationProps): Promise<boolean> => {
|
||||
const safeInstance = getGnosisSafeInstanceAt(safeAddress)
|
||||
// If it's approvalAndExecution we have to add a preapproved signature else we have all signatures
|
||||
const sigs = generateSignaturesFromTxConfirmations(txConfirmations, approvalAndExecution ? from : undefined)
|
||||
|
||||
return safeInstance.methods
|
||||
.execTransaction(txRecipient, txAmount, txData, operation, safeTxGas, 0, gasPrice, gasToken, refundReceiver, sigs)
|
||||
.call({
|
||||
from,
|
||||
gas: gasLimit,
|
||||
})
|
||||
.catch(() => false)
|
||||
}
|
||||
|
||||
type TransactionApprovalEstimationProps = {
|
||||
txData: string
|
||||
safeAddress: string
|
||||
txRecipient: string
|
||||
txAmount: string
|
||||
txData: string
|
||||
operation: number
|
||||
from: string
|
||||
isOffChainSignature: boolean
|
||||
@@ -352,7 +357,7 @@ export const estimateGasForTransactionApproval = async ({
|
||||
return 0
|
||||
}
|
||||
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const safeInstance = getGnosisSafeInstanceAt(safeAddress)
|
||||
|
||||
const nonce = await safeInstance.methods.nonce().call()
|
||||
const txHash = await safeInstance.methods
|
||||
@@ -360,7 +365,7 @@ export const estimateGasForTransactionApproval = async ({
|
||||
.call({
|
||||
from,
|
||||
})
|
||||
const approveTransactionTxData = await safeInstance.methods.approveHash(txHash).encodeABI()
|
||||
const approveTransactionTxData = safeInstance.methods.approveHash(txHash).encodeABI()
|
||||
return calculateGasOf({
|
||||
data: approveTransactionTxData,
|
||||
from,
|
||||
|
||||
@@ -49,7 +49,7 @@ export const getEncodedMultiSendCallData = (txs: MultiSendTx[], web3: Web3): str
|
||||
}
|
||||
|
||||
export const getUpgradeSafeTransactionHash = async (safeAddress: string): Promise<string> => {
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const safeInstance = getGnosisSafeInstanceAt(safeAddress)
|
||||
const fallbackHandlerTxData = safeInstance.methods.setFallbackHandler(DEFAULT_FALLBACK_HANDLER_ADDRESS).encodeABI()
|
||||
const updateSafeTxData = safeInstance.methods.changeMasterCopy(SAFE_MASTER_COPY_ADDRESS).encodeABI()
|
||||
const txs = [
|
||||
|
||||
@@ -6,7 +6,7 @@ import { getGasPrice, getGasPriceOracle } from 'src/config'
|
||||
|
||||
export const EMPTY_DATA = '0x'
|
||||
|
||||
export const checkReceiptStatus = async (hash) => {
|
||||
export const checkReceiptStatus = async (hash: string): Promise<void> => {
|
||||
if (!hash) {
|
||||
return Promise.reject(new Error('No valid Tx hash to get receipt from'))
|
||||
}
|
||||
@@ -27,10 +27,6 @@ export const checkReceiptStatus = async (hash) => {
|
||||
}
|
||||
|
||||
export const calculateGasPrice = async (): Promise<string> => {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return '20000000000'
|
||||
}
|
||||
|
||||
const gasPrice = getGasPrice()
|
||||
const gasPriceOracle = getGasPriceOracle()
|
||||
|
||||
@@ -61,7 +57,7 @@ export const calculateGasOf = async (txConfig: {
|
||||
try {
|
||||
const gas = await web3.eth.estimateGas(txConfig)
|
||||
|
||||
return gas * 2
|
||||
return gas
|
||||
} catch (err) {
|
||||
return Promise.reject(err)
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ const OwnerListComponent = (props) => {
|
||||
|
||||
const fetchSafe = async () => {
|
||||
const safeAddress = values[FIELD_LOAD_ADDRESS]
|
||||
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const gnosisSafe = getGnosisSafeInstanceAt(safeAddress)
|
||||
const safeOwners = await gnosisSafe.methods.getOwners().call()
|
||||
const threshold = await gnosisSafe.methods.getThreshold().call()
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ export const CreateEditEntryModal = ({
|
||||
description={isNew ? 'Create new addressBook entry' : 'Edit addressBook entry'}
|
||||
handleClose={onClose}
|
||||
open={isOpen}
|
||||
paperClassName={classes.smallerModalWindow}
|
||||
paperClassName="smaller-modal-window"
|
||||
title={isNew ? 'Create new entry' : 'Edit entry'}
|
||||
>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
|
||||
@@ -23,8 +23,5 @@ export const useStyles = makeStyles(
|
||||
height: '84px',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
smallerModalWindow: {
|
||||
height: 'auto',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -25,7 +25,7 @@ const DeleteEntryModalComponent = ({ classes, deleteEntryModalHandler, entryToDe
|
||||
description="Delete entry"
|
||||
handleClose={onClose}
|
||||
open={isOpen}
|
||||
paperClassName={classes.smallerModalWindow}
|
||||
paperClassName="smaller-modal-window"
|
||||
title="Delete entry"
|
||||
>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
|
||||
@@ -27,7 +27,4 @@ export const styles = () => ({
|
||||
buttonCancel: {
|
||||
color: '#008c73',
|
||||
},
|
||||
smallerModalWindow: {
|
||||
height: 'auto',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -11,7 +11,6 @@ import { styles } from './style'
|
||||
import Table from 'src/components/Table'
|
||||
import { cellWidth } from 'src/components/Table/TableHead'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Button from 'src/components/layout/Button'
|
||||
import ButtonLink from 'src/components/layout/ButtonLink'
|
||||
import Col from 'src/components/layout/Col'
|
||||
import Img from 'src/components/layout/Img'
|
||||
@@ -40,7 +39,19 @@ import { addressBookQueryParamsSelector, safesListSelector } from 'src/logic/saf
|
||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||
import { grantedSelector } from 'src/routes/safe/container/selector'
|
||||
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
|
||||
import { FixedIcon, Text, Button } from '@gnosis.pm/safe-react-components'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
&&.MuiButton-root {
|
||||
margin: 4px 12px 4px 0px;
|
||||
padding: 0 12px;
|
||||
min-width: auto;
|
||||
}
|
||||
svg {
|
||||
margin: 0 6px 0 0;
|
||||
}
|
||||
`
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
interface AddressBookSelectedEntry extends AddressBookEntry {
|
||||
@@ -202,19 +213,21 @@ const AddressBookTable = (): ReactElement => {
|
||||
testId={REMOVE_ENTRY_BUTTON}
|
||||
/>
|
||||
{granted ? (
|
||||
<Button
|
||||
className={classes.send}
|
||||
<StyledButton
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setSelectedEntry({ entry: row })
|
||||
setSendFundsModalOpen(true)
|
||||
}}
|
||||
size="small"
|
||||
testId={SEND_ENTRY_BUTTON}
|
||||
size="md"
|
||||
variant="contained"
|
||||
data-testid={SEND_ENTRY_BUTTON}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
<FixedIcon type="arrowSentWhite" />
|
||||
<Text size="xl" color="white">
|
||||
Send
|
||||
</Text>
|
||||
</StyledButton>
|
||||
) : null}
|
||||
</Row>
|
||||
</TableCell>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { lg, marginButtonImg, md, sm } from 'src/theme/variables'
|
||||
import { lg, md, sm } from 'src/theme/variables'
|
||||
import { createStyles } from '@material-ui/core'
|
||||
|
||||
export const styles = createStyles({
|
||||
@@ -37,7 +37,7 @@ export const styles = createStyles({
|
||||
},
|
||||
editEntryButton: {
|
||||
cursor: 'pointer',
|
||||
marginBottom: marginButtonImg,
|
||||
marginBottom: '16px',
|
||||
},
|
||||
editEntryButtonNonOwner: {
|
||||
cursor: 'pointer',
|
||||
@@ -45,13 +45,13 @@ export const styles = createStyles({
|
||||
removeEntryButton: {
|
||||
marginLeft: lg,
|
||||
marginRight: lg,
|
||||
marginBottom: marginButtonImg,
|
||||
marginBottom: '16px',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
removeEntryButtonDisabled: {
|
||||
marginLeft: lg,
|
||||
marginRight: lg,
|
||||
marginBottom: marginButtonImg,
|
||||
marginBottom: '16px',
|
||||
cursor: 'default',
|
||||
},
|
||||
removeEntryButtonNonOwner: {
|
||||
@@ -65,14 +65,6 @@ export const styles = createStyles({
|
||||
boxSizing: 'border-box',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
send: {
|
||||
width: '75px',
|
||||
minWidth: '75px',
|
||||
borderRadius: '4px',
|
||||
'& > span': {
|
||||
fontSize: '14px',
|
||||
},
|
||||
},
|
||||
leftIcon: {
|
||||
marginRight: sm,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { ModalFooterConfirmation } from '@gnosis.pm/safe-react-components'
|
||||
import { Text, EthHashInfo, ModalFooterConfirmation } from '@gnosis.pm/safe-react-components'
|
||||
import styled from 'styled-components'
|
||||
import { useDispatch } from 'react-redux'
|
||||
|
||||
@@ -20,11 +20,12 @@ import { EditableTxParameters } from 'src/routes/safe/components/Transactions/he
|
||||
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 { getExplorerInfo } from 'src/config'
|
||||
import Block from 'src/components/layout/Block'
|
||||
|
||||
import GasEstimationInfo from '../GasEstimationInfo'
|
||||
import { ConfirmTxModalProps, DecodedTxDetail } from '.'
|
||||
@@ -53,6 +54,19 @@ const DecodeTxsWrapper = styled.div`
|
||||
margin: 24px -24px;
|
||||
`
|
||||
|
||||
const StyledBlock = styled(Block)`
|
||||
background-color: ${({ theme }) => theme.colors.separator};
|
||||
width: fit-content;
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px;
|
||||
margin: 4px 0 0 40px;
|
||||
|
||||
display: flex;
|
||||
> :nth-child(1) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
`
|
||||
|
||||
type Props = ConfirmTxModalProps & {
|
||||
areTxsMalformed: boolean
|
||||
showDecodedTxData: (decodedTxDetails: DecodedTxDetail) => void
|
||||
@@ -81,6 +95,7 @@ export const ReviewConfirm = ({
|
||||
const isMultiSend = txs.length > 1
|
||||
const [decodedData, setDecodedData] = useState<DecodedData | null>(null)
|
||||
const dispatch = useDispatch()
|
||||
const explorerUrl = getExplorerInfo(safeAddress)
|
||||
|
||||
const txRecipient: string | undefined = useMemo(() => (isMultiSend ? MULTI_SEND_ADDRESS : txs[0]?.to), [
|
||||
txs,
|
||||
@@ -97,6 +112,7 @@ export const ReviewConfirm = ({
|
||||
const operation = useMemo(() => (isMultiSend ? DELEGATE_CALL : CALL), [isMultiSend])
|
||||
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
|
||||
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
|
||||
const [manualGasLimit, setManualGasLimit] = useState<string | undefined>()
|
||||
|
||||
const {
|
||||
gasLimit,
|
||||
@@ -114,6 +130,7 @@ export const ReviewConfirm = ({
|
||||
txAmount: txValue,
|
||||
safeTxGas: manualSafeTxGas,
|
||||
manualGasPrice,
|
||||
manualGasLimit,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@@ -176,6 +193,10 @@ export const ReviewConfirm = ({
|
||||
setManualGasPrice(txParameters.ethGasPrice)
|
||||
}
|
||||
|
||||
if (txParameters.ethGasLimit && gasLimit !== txParameters.ethGasLimit) {
|
||||
setManualGasLimit(txParameters.ethGasLimit)
|
||||
}
|
||||
|
||||
if (newSafeTxGas && oldSafeTxGas !== newSafeTxGas) {
|
||||
setManualSafeTxGas(newSafeTxGas)
|
||||
}
|
||||
@@ -198,7 +219,11 @@ export const ReviewConfirm = ({
|
||||
|
||||
<Container>
|
||||
{/* Safe */}
|
||||
<AddressInfo ethBalance={ethBalance} safeAddress={safeAddress} safeName={safeName} />
|
||||
<EthHashInfo name={safeName} hash={safeAddress} showAvatar showCopyBtn explorerUrl={explorerUrl} />
|
||||
<StyledBlock>
|
||||
<Text size="md">Balance:</Text>
|
||||
<Text size="md" strong>{`${ethBalance} ${nativeCoin.symbol}`}</Text>
|
||||
</StyledBlock>
|
||||
|
||||
<DividerLine withArrow />
|
||||
|
||||
@@ -212,9 +237,7 @@ export const ReviewConfirm = ({
|
||||
<DecodeTxsWrapper>
|
||||
<DecodeTxs txs={txs} decodedData={decodedData} onTxItemClick={showDecodedTxData} />
|
||||
</DecodeTxsWrapper>
|
||||
|
||||
{!isMultiSend && <DividerLine withArrow={false} />}
|
||||
|
||||
{/* Warning gas estimation */}
|
||||
{params?.safeTxGas && (
|
||||
<div className="section">
|
||||
@@ -227,7 +250,6 @@ export const ReviewConfirm = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tx Parameters */}
|
||||
<TxParametersDetail
|
||||
txParameters={txParameters}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { List } from 'immutable'
|
||||
import TableCell from '@material-ui/core/TableCell'
|
||||
@@ -8,11 +9,11 @@ import TableRow from '@material-ui/core/TableRow'
|
||||
import { Skeleton } from '@material-ui/lab'
|
||||
|
||||
import InfoIcon from 'src/assets/icons/info_red.svg'
|
||||
import { FixedIcon, Text, Button } from '@gnosis.pm/safe-react-components'
|
||||
|
||||
import Img from 'src/components/layout/Img'
|
||||
import Table from 'src/components/Table'
|
||||
import { cellWidth } from 'src/components/Table/TableHead'
|
||||
import Button from 'src/components/layout/Button'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { BALANCE_ROW_TEST_ID } from 'src/routes/safe/components/Balances'
|
||||
import AssetTableCell from 'src/routes/safe/components/Balances/AssetTableCell'
|
||||
@@ -30,6 +31,17 @@ import { makeStyles } from '@material-ui/core/styles'
|
||||
import { styles } from './styles'
|
||||
import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors'
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
&&.MuiButton-root {
|
||||
margin: 4px 12px 4px 0px;
|
||||
padding: 0 12px;
|
||||
min-width: auto;
|
||||
}
|
||||
svg {
|
||||
margin: 0 6px 0 0;
|
||||
}
|
||||
`
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
type Props = {
|
||||
@@ -137,31 +149,25 @@ const Coins = (props: Props): React.ReactElement => {
|
||||
<TableCell component="td">
|
||||
<Row align="end" className={classes.actions}>
|
||||
{granted && (
|
||||
<Button
|
||||
className={classes.send}
|
||||
<StyledButton
|
||||
color="primary"
|
||||
onClick={() => showSendFunds(row.asset.address)}
|
||||
size="small"
|
||||
testId="balance-send-btn"
|
||||
size="md"
|
||||
variant="contained"
|
||||
data-testid="balance-send-btn"
|
||||
>
|
||||
{/* <CallMade alt="Send Transaction" className={classNames(classes.leftIcon, classes.iconSmall)} /> */}
|
||||
Send
|
||||
</Button>
|
||||
<FixedIcon type="arrowSentWhite" />
|
||||
<Text size="xl" color="white">
|
||||
Send
|
||||
</Text>
|
||||
</StyledButton>
|
||||
)}
|
||||
<Button
|
||||
className={classes.receive}
|
||||
color="primary"
|
||||
onClick={showReceiveFunds}
|
||||
size="small"
|
||||
variant="contained"
|
||||
>
|
||||
{/* <CallReceived
|
||||
alt="Receive Transaction"
|
||||
className={classNames(classes.leftIcon, classes.iconSmall)}
|
||||
/> */}
|
||||
Receive
|
||||
</Button>
|
||||
<StyledButton color="primary" onClick={showReceiveFunds} size="md" variant="contained">
|
||||
<FixedIcon type="arrowReceivedWhite" />
|
||||
<Text size="xl" color="white">
|
||||
Receive
|
||||
</Text>
|
||||
</StyledButton>
|
||||
</Row>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { sm, xs } from 'src/theme/variables'
|
||||
import { sm } from 'src/theme/variables'
|
||||
import { createStyles } from '@material-ui/core'
|
||||
|
||||
export const styles = createStyles({
|
||||
@@ -25,23 +25,6 @@ export const styles = createStyles({
|
||||
justifyContent: 'flex-end',
|
||||
visibility: 'hidden',
|
||||
},
|
||||
receive: {
|
||||
width: '95px',
|
||||
minWidth: '95px',
|
||||
marginLeft: sm,
|
||||
borderRadius: xs,
|
||||
'& > span': {
|
||||
fontSize: '14px',
|
||||
},
|
||||
},
|
||||
send: {
|
||||
width: '75px',
|
||||
minWidth: '75px',
|
||||
borderRadius: xs,
|
||||
'& > span': {
|
||||
fontSize: '14px',
|
||||
},
|
||||
},
|
||||
leftIcon: {
|
||||
marginRight: sm,
|
||||
},
|
||||
|
||||
@@ -32,7 +32,7 @@ const SafeInfo = (): React.ReactElement => {
|
||||
hash={safeAddress}
|
||||
name={safeName}
|
||||
explorerUrl={getExplorerInfo(safeAddress)}
|
||||
showIdenticon
|
||||
showAvatar
|
||||
showCopyBtn
|
||||
/>
|
||||
{ethBalance && (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import CircularProgress from '@material-ui/core/CircularProgress'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import cn from 'classnames'
|
||||
import React, { Suspense, useEffect, useState } from 'react'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
@@ -32,12 +31,6 @@ const SendCustomTx = React.lazy(() => import('./screens/ContractInteraction/Send
|
||||
const ReviewCustomTx = React.lazy(() => import('./screens/ContractInteraction/ReviewCustomTx'))
|
||||
|
||||
const useStyles = makeStyles({
|
||||
scalableModalWindow: {
|
||||
height: 'auto',
|
||||
},
|
||||
scalableStaticModalWindow: {
|
||||
height: 'auto',
|
||||
},
|
||||
loaderStyle: {
|
||||
height: '500px',
|
||||
width: '100%',
|
||||
@@ -86,8 +79,6 @@ const SendModal = ({
|
||||
setTx({})
|
||||
}, [activeScreenType, isOpen])
|
||||
|
||||
const scalableModalSize = activeScreen === 'chooseTxType'
|
||||
|
||||
const handleTxCreation = (txInfo: SendCollectibleTxInfo) => {
|
||||
setActiveScreen('sendFundsReviewTx')
|
||||
setTx(txInfo)
|
||||
@@ -117,7 +108,7 @@ const SendModal = ({
|
||||
description="Send Tokens Form"
|
||||
handleClose={onClose}
|
||||
open={isOpen}
|
||||
paperClassName={cn(scalableModalSize ? classes.scalableStaticModalWindow : classes.scalableModalWindow)}
|
||||
paperClassName="smaller-modal-window"
|
||||
title="Send Tokens"
|
||||
>
|
||||
<Suspense
|
||||
|
||||
@@ -148,7 +148,7 @@ const BaseAddressBookInput = ({
|
||||
/>
|
||||
)}
|
||||
getOptionLabel={({ address }) => address}
|
||||
renderOption={({ address, name }) => <EthHashInfo hash={address} name={name} showIdenticon />}
|
||||
renderOption={({ address, name }) => <EthHashInfo hash={address} name={name} showAvatar />}
|
||||
role="listbox"
|
||||
style={{ display: 'flex', flexGrow: 1 }}
|
||||
/>
|
||||
|
||||
+10
-3
@@ -2,9 +2,8 @@ import React, { useEffect, useState } from 'react'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
import { getNetworkInfo } from 'src/config'
|
||||
import { getNetworkInfo, getExplorerInfo } from 'src/config'
|
||||
import { toTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
import AddressInfo from 'src/components/AddressInfo'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Button from 'src/components/layout/Button'
|
||||
import Col from 'src/components/layout/Col'
|
||||
@@ -30,6 +29,7 @@ import {
|
||||
import { useEstimateTransactionGas, EstimationStatus } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
|
||||
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
@@ -52,11 +52,13 @@ type Props = {
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
|
||||
const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactElement => {
|
||||
const explorerUrl = getExplorerInfo(tx.contractAddress as string)
|
||||
const classes = useStyles()
|
||||
const dispatch = useDispatch()
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
|
||||
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
|
||||
const [manualGasLimit, setManualGasLimit] = useState<string | undefined>()
|
||||
|
||||
const [txInfo, setTxInfo] = useState<{
|
||||
txRecipient: string
|
||||
@@ -79,6 +81,7 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE
|
||||
txData: txInfo?.txData,
|
||||
safeTxGas: manualSafeTxGas,
|
||||
manualGasPrice,
|
||||
manualGasLimit,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@@ -119,6 +122,10 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE
|
||||
setManualGasPrice(txParameters.ethGasPrice)
|
||||
}
|
||||
|
||||
if (txParameters.ethGasLimit && gasLimit !== txParameters.ethGasLimit) {
|
||||
setManualGasLimit(txParameters.ethGasLimit)
|
||||
}
|
||||
|
||||
if (newSafeTxGas && oldSafeTxGas !== newSafeTxGas) {
|
||||
setManualSafeTxGas(newSafeTxGas)
|
||||
}
|
||||
@@ -144,7 +151,7 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<AddressInfo safeAddress={tx.contractAddress as string} />
|
||||
<EthHashInfo hash={tx.contractAddress as string} showAvatar showCopyBtn explorerUrl={explorerUrl} />
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
|
||||
@@ -56,6 +56,7 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement =
|
||||
const nftTokens = useSelector(nftTokensSelector)
|
||||
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
|
||||
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
|
||||
const [manualGasLimit, setManualGasLimit] = useState<string | undefined>()
|
||||
|
||||
const txToken = nftTokens.find(
|
||||
({ assetAddress, tokenId }) => assetAddress === tx.assetAddress && tokenId === tx.nftTokenId,
|
||||
@@ -76,6 +77,7 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement =
|
||||
txRecipient: tx.assetAddress,
|
||||
safeTxGas: manualSafeTxGas,
|
||||
manualGasPrice,
|
||||
manualGasLimit,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@@ -133,6 +135,10 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement =
|
||||
setManualGasPrice(txParameters.ethGasPrice)
|
||||
}
|
||||
|
||||
if (txParameters.ethGasLimit && gasLimit !== txParameters.ethGasLimit) {
|
||||
setManualGasLimit(txParameters.ethGasLimit)
|
||||
}
|
||||
|
||||
if (newSafeTxGas && oldSafeTxGas !== newSafeTxGas) {
|
||||
setManualSafeTxGas(newSafeTxGas)
|
||||
}
|
||||
|
||||
@@ -100,6 +100,7 @@ const ReviewSendFundsTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactE
|
||||
const data = useTxData(isSendingNativeToken, tx.amount, tx.recipientAddress, txToken)
|
||||
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
|
||||
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
|
||||
const [manualGasLimit, setManualGasLimit] = useState<string | undefined>()
|
||||
|
||||
const {
|
||||
gasCostFormatted,
|
||||
@@ -117,6 +118,7 @@ const ReviewSendFundsTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactE
|
||||
txAmount: txValue,
|
||||
safeTxGas: manualSafeTxGas,
|
||||
manualGasPrice,
|
||||
manualGasLimit,
|
||||
})
|
||||
|
||||
const submitTx = async (txParameters: TxParameters) => {
|
||||
@@ -171,6 +173,10 @@ const ReviewSendFundsTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactE
|
||||
setManualGasPrice(txParameters.ethGasPrice)
|
||||
}
|
||||
|
||||
if (txParameters.ethGasLimit && gasLimit !== txParameters.ethGasLimit) {
|
||||
setManualGasLimit(txParameters.ethGasLimit)
|
||||
}
|
||||
|
||||
if (newSafeTxGas && oldSafeTxGas !== newSafeTxGas) {
|
||||
setManualSafeTxGas(newSafeTxGas)
|
||||
}
|
||||
@@ -257,17 +263,21 @@ const ReviewSendFundsTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactE
|
||||
</Row>
|
||||
|
||||
{/* Tx Parameters */}
|
||||
<TxParametersDetail
|
||||
txParameters={txParameters}
|
||||
onEdit={toggleEditMode}
|
||||
isTransactionCreation={isCreation}
|
||||
isTransactionExecution={isExecution}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
/>
|
||||
{/* FIXME TxParameters should be updated to be used with spending limits */}
|
||||
{!sameString(tx.txType, 'spendingLimit') && (
|
||||
<TxParametersDetail
|
||||
txParameters={txParameters}
|
||||
onEdit={toggleEditMode}
|
||||
isTransactionCreation={isCreation}
|
||||
isTransactionExecution={isExecution}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
/>
|
||||
)}
|
||||
</Block>
|
||||
|
||||
{/* Disclaimer */}
|
||||
{txEstimationExecutionStatus !== EstimationStatus.LOADING && (
|
||||
{/* FIXME Estimation should be fixed to be used with spending limits */}
|
||||
{!sameString(tx.txType, 'spendingLimit') && txEstimationExecutionStatus !== EstimationStatus.LOADING && (
|
||||
<div className={classes.gasCostsContainer}>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
|
||||
@@ -265,7 +265,7 @@ const SendFunds = ({
|
||||
<EthHashInfo
|
||||
hash={selectedEntry.address}
|
||||
name={selectedEntry.name}
|
||||
showIdenticon
|
||||
showAvatar
|
||||
showCopyBtn
|
||||
explorerUrl={getExplorerInfo(selectedEntry.address)}
|
||||
/>
|
||||
|
||||
@@ -92,7 +92,7 @@ const Balances = (): React.ReactElement => {
|
||||
}))
|
||||
}
|
||||
|
||||
const { assetDivider, assetTab, assetTabActive, assetTabs, controls, receiveModal, tokenControls } = classes
|
||||
const { assetDivider, assetTab, assetTabActive, assetTabs, controls, tokenControls } = classes
|
||||
const { erc721Enabled, sendFunds, showReceive } = state
|
||||
|
||||
return (
|
||||
@@ -174,7 +174,7 @@ const Balances = (): React.ReactElement => {
|
||||
description="Receive Tokens Form"
|
||||
handleClose={() => onHide('Receive')}
|
||||
open={showReceive}
|
||||
paperClassName={receiveModal}
|
||||
paperClassName="receive-modal"
|
||||
title="Receive Tokens"
|
||||
>
|
||||
<ReceiveModal safeAddress={address} safeName={safeName} onClose={() => onHide('Receive')} />
|
||||
|
||||
@@ -58,12 +58,6 @@ export const styles = createStyles({
|
||||
marginLeft: '0',
|
||||
},
|
||||
},
|
||||
receiveModal: {
|
||||
height: 'auto',
|
||||
maxWidth: 'calc(100% - 30px)',
|
||||
minHeight: '544px',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
send: {
|
||||
width: '75px',
|
||||
minWidth: '75px',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// TODO: remove this file. It's no longer used
|
||||
import { screenSm, sm } from 'src/theme/variables'
|
||||
import { createStyles } from '@material-ui/core'
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ export const RemoveModuleModal = ({ onClose, selectedModulePair }: RemoveModuleM
|
||||
const dispatch = useDispatch()
|
||||
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
|
||||
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
|
||||
const [manualGasLimit, setManualGasLimit] = useState<string | undefined>()
|
||||
|
||||
const [, moduleAddress] = selectedModulePair
|
||||
const explorerInfo = getExplorerInfo(moduleAddress)
|
||||
@@ -77,6 +78,7 @@ export const RemoveModuleModal = ({ onClose, selectedModulePair }: RemoveModuleM
|
||||
txAmount: '0',
|
||||
safeTxGas: manualSafeTxGas,
|
||||
manualGasPrice,
|
||||
manualGasLimit,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@@ -113,6 +115,10 @@ export const RemoveModuleModal = ({ onClose, selectedModulePair }: RemoveModuleM
|
||||
setManualGasPrice(txParameters.ethGasPrice)
|
||||
}
|
||||
|
||||
if (txParameters.ethGasLimit && gasLimit !== txParameters.ethGasLimit) {
|
||||
setManualGasLimit(txParameters.ethGasLimit)
|
||||
}
|
||||
|
||||
if (newSafeTxGas && oldSafeTxGas !== newSafeTxGas) {
|
||||
setManualSafeTxGas(newSafeTxGas)
|
||||
}
|
||||
@@ -122,7 +128,7 @@ export const RemoveModuleModal = ({ onClose, selectedModulePair }: RemoveModuleM
|
||||
<Modal
|
||||
description="Remove the selected Module"
|
||||
handleClose={onClose}
|
||||
paperClassName={classes.modal}
|
||||
paperClassName="modal"
|
||||
title="Remove Module"
|
||||
open
|
||||
>
|
||||
|
||||
@@ -101,11 +101,6 @@ export const styles = createStyles({
|
||||
cursor: 'pointer',
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
height: 'auto',
|
||||
maxWidth: 'calc(100% - 30px)',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
gasCostsContainer: {
|
||||
backgroundColor: background,
|
||||
padding: `0 ${lg}`,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
@@ -18,15 +17,6 @@ import { OwnerForm } from './screens/OwnerForm'
|
||||
import { ReviewAddOwner } from './screens/Review'
|
||||
import { ThresholdForm } from './screens/ThresholdForm'
|
||||
|
||||
const styles = createStyles({
|
||||
biggerModalWindow: {
|
||||
width: '775px',
|
||||
height: 'auto',
|
||||
},
|
||||
})
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
export type OwnerValues = {
|
||||
ownerAddress: string
|
||||
ownerName: string
|
||||
@@ -39,7 +29,7 @@ export const sendAddOwner = async (
|
||||
txParameters: TxParameters,
|
||||
dispatch: Dispatch,
|
||||
): Promise<void> => {
|
||||
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const gnosisSafe = getGnosisSafeInstanceAt(safeAddress)
|
||||
const txData = gnosisSafe.methods.addOwnerWithThreshold(values.ownerAddress, values.threshold).encodeABI()
|
||||
|
||||
const txHash = await dispatch(
|
||||
@@ -66,7 +56,6 @@ type Props = {
|
||||
}
|
||||
|
||||
export const AddOwnerModal = ({ isOpen, onClose }: Props): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const [activeScreen, setActiveScreen] = useState('selectOwner')
|
||||
const [values, setValues] = useState<OwnerValues>({ ownerName: '', ownerAddress: '', threshold: '' })
|
||||
const dispatch = useDispatch()
|
||||
@@ -123,7 +112,7 @@ export const AddOwnerModal = ({ isOpen, onClose }: Props): React.ReactElement =>
|
||||
description="Add owner to Safe"
|
||||
handleClose={onClose}
|
||||
open={isOpen}
|
||||
paperClassName={classes.biggerModalWindow}
|
||||
paperClassName="bigger-modal-window"
|
||||
title="Add owner to Safe"
|
||||
>
|
||||
<>
|
||||
|
||||
+8
-2
@@ -45,6 +45,7 @@ export const ReviewAddOwner = ({ onClickBack, onClose, onSubmit, values }: Revie
|
||||
const owners = useSelector(safeOwnersSelector)
|
||||
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
|
||||
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
|
||||
const [manualGasLimit, setManualGasLimit] = useState<string | undefined>()
|
||||
|
||||
const {
|
||||
gasLimit,
|
||||
@@ -60,14 +61,15 @@ export const ReviewAddOwner = ({ onClickBack, onClose, onSubmit, values }: Revie
|
||||
txRecipient: safeAddress,
|
||||
safeTxGas: manualSafeTxGas,
|
||||
manualGasPrice,
|
||||
manualGasLimit,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
let isCurrent = true
|
||||
|
||||
const calculateAddOwnerData = async () => {
|
||||
const calculateAddOwnerData = () => {
|
||||
try {
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const safeInstance = getGnosisSafeInstanceAt(safeAddress)
|
||||
const txData = safeInstance.methods.addOwnerWithThreshold(values.ownerAddress, values.threshold).encodeABI()
|
||||
|
||||
if (isCurrent) {
|
||||
@@ -94,6 +96,10 @@ export const ReviewAddOwner = ({ onClickBack, onClose, onSubmit, values }: Revie
|
||||
setManualGasPrice(txParameters.ethGasPrice)
|
||||
}
|
||||
|
||||
if (txParameters.ethGasLimit && gasLimit !== txParameters.ethGasLimit) {
|
||||
setManualGasLimit(txParameters.ethGasLimit)
|
||||
}
|
||||
|
||||
if (newSafeTxGas && oldSafeTxGas !== newSafeTxGas) {
|
||||
setManualSafeTxGas(newSafeTxGas)
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ export const EditOwnerModal = ({ isOpen, onClose, ownerAddress, selectedOwnerNam
|
||||
description="Edit owner from Safe"
|
||||
handleClose={onClose}
|
||||
open={isOpen}
|
||||
paperClassName={classes.smallerModalWindow}
|
||||
paperClassName="smaller-modal-window"
|
||||
title="Edit owner from Safe"
|
||||
>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
|
||||
@@ -32,7 +32,4 @@ export const styles = createStyles({
|
||||
cursor: 'pointer',
|
||||
},
|
||||
},
|
||||
smallerModalWindow: {
|
||||
height: 'auto',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
@@ -15,15 +14,6 @@ import { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/lo
|
||||
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
|
||||
const styles = createStyles({
|
||||
biggerModalWindow: {
|
||||
width: '775px',
|
||||
height: 'auto',
|
||||
},
|
||||
})
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
type OwnerValues = {
|
||||
ownerAddress: string
|
||||
ownerName: string
|
||||
@@ -39,7 +29,7 @@ export const sendRemoveOwner = async (
|
||||
txParameters: TxParameters,
|
||||
threshold?: number,
|
||||
): Promise<void> => {
|
||||
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const gnosisSafe = getGnosisSafeInstanceAt(safeAddress)
|
||||
const safeOwners = await gnosisSafe.methods.getOwners().call()
|
||||
const index = safeOwners.findIndex(
|
||||
(ownerAddress) => ownerAddress.toLowerCase() === ownerAddressToRemove.toLowerCase(),
|
||||
@@ -78,7 +68,6 @@ export const RemoveOwnerModal = ({
|
||||
ownerAddress,
|
||||
ownerName,
|
||||
}: RemoveOwnerProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const [activeScreen, setActiveScreen] = useState('checkOwner')
|
||||
const [values, setValues] = useState<OwnerValues>({ ownerAddress, ownerName, threshold: '' })
|
||||
const dispatch = useDispatch()
|
||||
@@ -120,7 +109,7 @@ export const RemoveOwnerModal = ({
|
||||
description="Remove owner from Safe"
|
||||
handleClose={onClose}
|
||||
open={isOpen}
|
||||
paperClassName={classes.biggerModalWindow}
|
||||
paperClassName="bigger-modal-window"
|
||||
title="Remove owner from Safe"
|
||||
>
|
||||
<>
|
||||
|
||||
+7
-1
@@ -59,6 +59,7 @@ export const ReviewRemoveOwnerModal = ({
|
||||
const ownersWithAddressBookName = owners ? getOwnersWithNameFromAddressBook(addressBook, owners) : List([])
|
||||
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
|
||||
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
|
||||
const [manualGasLimit, setManualGasLimit] = useState<string | undefined>()
|
||||
|
||||
const {
|
||||
gasLimit,
|
||||
@@ -74,6 +75,7 @@ export const ReviewRemoveOwnerModal = ({
|
||||
txRecipient: safeAddress,
|
||||
safeTxGas: manualSafeTxGas,
|
||||
manualGasPrice,
|
||||
manualGasLimit,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@@ -86,7 +88,7 @@ export const ReviewRemoveOwnerModal = ({
|
||||
|
||||
const calculateRemoveOwnerData = async () => {
|
||||
try {
|
||||
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const gnosisSafe = getGnosisSafeInstanceAt(safeAddress)
|
||||
const safeOwners = await gnosisSafe.methods.getOwners().call()
|
||||
const index = safeOwners.findIndex((owner) => sameAddress(owner, ownerAddress))
|
||||
const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1]
|
||||
@@ -116,6 +118,10 @@ export const ReviewRemoveOwnerModal = ({
|
||||
setManualGasPrice(txParameters.ethGasPrice)
|
||||
}
|
||||
|
||||
if (txParameters.ethGasLimit && gasLimit !== txParameters.ethGasLimit) {
|
||||
setManualGasLimit(txParameters.ethGasLimit)
|
||||
}
|
||||
|
||||
if (newSafeTxGas && oldSafeTxGas !== newSafeTxGas) {
|
||||
setManualSafeTxGas(newSafeTxGas)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
@@ -18,15 +17,6 @@ import { OwnerForm } from 'src/routes/safe/components/Settings/ManageOwners/Repl
|
||||
import { ReviewReplaceOwnerModal } from 'src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
|
||||
const styles = createStyles({
|
||||
biggerModalWindow: {
|
||||
width: '775px',
|
||||
height: 'auto',
|
||||
},
|
||||
})
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
type OwnerValues = {
|
||||
newOwnerAddress: string
|
||||
newOwnerName: string
|
||||
@@ -40,7 +30,7 @@ export const sendReplaceOwner = async (
|
||||
txParameters: TxParameters,
|
||||
threshold?: number,
|
||||
): Promise<void> => {
|
||||
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const gnosisSafe = getGnosisSafeInstanceAt(safeAddress)
|
||||
const safeOwners = await gnosisSafe.methods.getOwners().call()
|
||||
const index = safeOwners.findIndex((ownerAddress) => sameAddress(ownerAddress, ownerAddressToRemove))
|
||||
const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1]
|
||||
@@ -84,7 +74,6 @@ export const ReplaceOwnerModal = ({
|
||||
ownerAddress,
|
||||
ownerName,
|
||||
}: ReplaceOwnerProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const [activeScreen, setActiveScreen] = useState('checkOwner')
|
||||
const [values, setValues] = useState({
|
||||
newOwnerAddress: '',
|
||||
@@ -137,7 +126,7 @@ export const ReplaceOwnerModal = ({
|
||||
description="Replace owner from Safe"
|
||||
handleClose={onClose}
|
||||
open={isOpen}
|
||||
paperClassName={classes.biggerModalWindow}
|
||||
paperClassName="bigger-modal-window"
|
||||
title="Replace owner from Safe"
|
||||
>
|
||||
<>
|
||||
|
||||
+7
-1
@@ -67,6 +67,7 @@ export const ReviewReplaceOwnerModal = ({
|
||||
const ownersWithAddressBookName = owners ? getOwnersWithNameFromAddressBook(addressBook, owners) : List([])
|
||||
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
|
||||
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
|
||||
const [manualGasLimit, setManualGasLimit] = useState<string | undefined>()
|
||||
|
||||
const {
|
||||
gasLimit,
|
||||
@@ -82,12 +83,13 @@ export const ReviewReplaceOwnerModal = ({
|
||||
txRecipient: safeAddress,
|
||||
safeTxGas: manualSafeTxGas,
|
||||
manualGasPrice,
|
||||
manualGasLimit,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
let isCurrent = true
|
||||
const calculateReplaceOwnerData = async () => {
|
||||
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const gnosisSafe = getGnosisSafeInstanceAt(safeAddress)
|
||||
const safeOwners = await gnosisSafe.methods.getOwners().call()
|
||||
const index = safeOwners.findIndex((owner) => owner.toLowerCase() === ownerAddress.toLowerCase())
|
||||
const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1]
|
||||
@@ -113,6 +115,10 @@ export const ReviewReplaceOwnerModal = ({
|
||||
setManualGasPrice(txParameters.ethGasPrice)
|
||||
}
|
||||
|
||||
if (txParameters.ethGasLimit && gasLimit !== txParameters.ethGasLimit) {
|
||||
setManualGasLimit(txParameters.ethGasLimit)
|
||||
}
|
||||
|
||||
if (newSafeTxGas && oldSafeTxGas !== newSafeTxGas) {
|
||||
setManualSafeTxGas(newSafeTxGas)
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export const RemoveSafeModal = ({ isOpen, onClose }: RemoveSafeModalProps): Reac
|
||||
description="Remove the selected Safe"
|
||||
handleClose={onClose}
|
||||
open={isOpen}
|
||||
paperClassName={classes.modal}
|
||||
paperClassName="modal"
|
||||
title="Remove Safe"
|
||||
>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
|
||||
@@ -52,9 +52,4 @@ export const styles = createStyles({
|
||||
cursor: 'pointer',
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
height: 'auto',
|
||||
maxWidth: 'calc(100% - 30px)',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -81,7 +81,7 @@ const Beneficiary = (): ReactElement => {
|
||||
hash={selectedEntry.address}
|
||||
name={selectedEntry.name}
|
||||
showCopyBtn
|
||||
showIdenticon
|
||||
showAvatar
|
||||
textSize="lg"
|
||||
shortenHash={4}
|
||||
explorerUrl={getExplorerInfo(selectedEntry.address)}
|
||||
|
||||
@@ -24,7 +24,7 @@ const AddressInfo = ({ address, cut = 4, title }: AddressInfoProps): ReactElemen
|
||||
hash={address}
|
||||
name={sameString(name, 'UNKNOWN') ? undefined : name}
|
||||
showCopyBtn
|
||||
showIdenticon
|
||||
showAvatar
|
||||
textSize="lg"
|
||||
explorerUrl={explorerUrl}
|
||||
shortenHash={cut}
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import { Icon, Text, Title } from '@gnosis.pm/safe-react-components'
|
||||
import React, { ReactElement, ReactNode, ReactNodeArray } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import GnoModal from 'src/components/Modal'
|
||||
import { useStyles } from 'src/routes/safe/components/Settings/SpendingLimit/style'
|
||||
|
||||
const TitleSection = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 2px solid ${({ theme }) => theme.colors.separator};
|
||||
`
|
||||
|
||||
const StyledButton = styled.button`
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 5px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
|
||||
span {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
:hover {
|
||||
background: ${({ theme }) => theme.colors.separator};
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
`
|
||||
|
||||
const FooterSection = styled.div`
|
||||
border-top: 2px solid ${({ theme }) => theme.colors.separator};
|
||||
padding: 16px 24px;
|
||||
`
|
||||
|
||||
const FooterWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
`
|
||||
|
||||
export interface TopBarProps {
|
||||
title: string
|
||||
titleNote?: string
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const TopBar = ({ title, titleNote, onClose }: TopBarProps): ReactElement => (
|
||||
<TitleSection>
|
||||
<Title size="xs" withoutMargin>
|
||||
{title}
|
||||
{titleNote && (
|
||||
<>
|
||||
{' '}
|
||||
<Text size="lg" color="secondaryLight" as="span">
|
||||
{titleNote}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Title>
|
||||
|
||||
<StyledButton onClick={onClose}>
|
||||
<Icon size="sm" type="cross" />
|
||||
</StyledButton>
|
||||
</TitleSection>
|
||||
)
|
||||
|
||||
interface FooterProps {
|
||||
children: ReactNodeArray
|
||||
}
|
||||
|
||||
const Footer = ({ children }: FooterProps): ReactElement => (
|
||||
<FooterSection>
|
||||
<FooterWrapper>{children}</FooterWrapper>
|
||||
</FooterSection>
|
||||
)
|
||||
|
||||
export interface ModalProps {
|
||||
children: ReactNode
|
||||
description: string
|
||||
handleClose: () => void
|
||||
open: boolean
|
||||
title: string
|
||||
}
|
||||
|
||||
// TODO: this is a potential proposal for `safe-react-components` Modal
|
||||
// By being able to combine components for better flexibility, this way Buttons can be part of the form body
|
||||
const Modal = ({ children, ...props }: ModalProps): ReactElement => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<GnoModal {...props} paperClassName={classes.modal}>
|
||||
{children}
|
||||
</GnoModal>
|
||||
)
|
||||
}
|
||||
|
||||
Modal.TopBar = TopBar
|
||||
Modal.Footer = Footer
|
||||
|
||||
export default Modal
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Button } from '@gnosis.pm/safe-react-components'
|
||||
import { Text } from '@gnosis.pm/safe-react-components'
|
||||
import { FormState, Mutator } from 'final-form'
|
||||
import React, { ReactElement } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import GnoForm from 'src/components/forms/GnoForm'
|
||||
import GnoButton from 'src/components/layout/Button'
|
||||
import { Modal } from 'src/components/Modal'
|
||||
import { Amount, Beneficiary, ResetTime, Token } from 'src/routes/safe/components/Settings/SpendingLimit/FormFields'
|
||||
import Modal from 'src/routes/safe/components/Settings/SpendingLimit/Modal'
|
||||
|
||||
const FormContainer = styled.div`
|
||||
padding: 24px 8px 24px 24px;
|
||||
@@ -24,14 +23,6 @@ const FormContainer = styled.div`
|
||||
'resetTimeOption resetTimeOption';
|
||||
`
|
||||
|
||||
const YetAnotherButton = styled(GnoButton)`
|
||||
&.Mui-disabled {
|
||||
background-color: ${({ theme }) => theme.colors.primary};
|
||||
color: ${({ theme }) => theme.colors.white};
|
||||
opacity: 0.5;
|
||||
}
|
||||
`
|
||||
|
||||
const formMutators: Record<string, Mutator<{ beneficiary: { name: string } }>> = {
|
||||
setBeneficiary: (args, state, utils) => {
|
||||
utils.changeValue(state, 'beneficiary', () => args[0])
|
||||
@@ -55,7 +46,16 @@ const canReview = ({
|
||||
const Create = ({ initialValues, onCancel, onReview }: NewSpendingLimitProps): ReactElement => {
|
||||
return (
|
||||
<>
|
||||
<Modal.TopBar title="New Spending Limit" titleNote="1 of 2" onClose={onCancel} />
|
||||
<Modal.Header onClose={onCancel}>
|
||||
<Modal.Header.Title size="xs" withoutMargin>
|
||||
<>
|
||||
New Spending Limit
|
||||
<Text size="lg" color="secondaryLight" as="span">
|
||||
1 of 2
|
||||
</Text>
|
||||
</>
|
||||
</Modal.Header.Title>
|
||||
</Modal.Header>
|
||||
|
||||
<GnoForm formMutators={formMutators} onSubmit={onReview} initialValues={initialValues}>
|
||||
{(...args) => {
|
||||
@@ -69,21 +69,10 @@ const Create = ({ initialValues, onCancel, onReview }: NewSpendingLimitProps): R
|
||||
</FormContainer>
|
||||
|
||||
<Modal.Footer>
|
||||
<Button color="primary" size="md" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
{/* TODO: replace this with safe-react-components button. */}
|
||||
{/* This is used as "submit" SRC Button does not triggers submission up until the 2nd click */}
|
||||
<YetAnotherButton
|
||||
color="primary"
|
||||
size="medium"
|
||||
variant="contained"
|
||||
type="submit"
|
||||
disabled={!canReview(args[2])}
|
||||
>
|
||||
Review
|
||||
</YetAnotherButton>
|
||||
<Modal.Footer.Buttons
|
||||
cancelButtonProps={{ onClick: onCancel }}
|
||||
confirmButtonProps={{ disabled: !canReview(args[2]), text: 'Review' }}
|
||||
/>
|
||||
</Modal.Footer>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Button, Text } from '@gnosis.pm/safe-react-components'
|
||||
import { Text } from '@gnosis.pm/safe-react-components'
|
||||
import React, { ReactElement, useEffect, useMemo, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Col from 'src/components/layout/Col'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { Modal } from 'src/components/Modal'
|
||||
import { createTransaction, CreateTransactionArgs } from 'src/logic/safe/store/actions/createTransaction'
|
||||
import { SafeRecordProps, SpendingLimit } from 'src/logic/safe/store/models/safe'
|
||||
import {
|
||||
@@ -22,7 +22,6 @@ import { fromTokenUnit, toTokenUnit } from 'src/logic/tokens/utils/humanReadable
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { RESET_TIME_OPTIONS } from 'src/routes/safe/components/Settings/SpendingLimit/FormFields/ResetTime'
|
||||
import { AddressInfo, ResetTimeInfo, TokenInfo } from 'src/routes/safe/components/Settings/SpendingLimit/InfoDisplay'
|
||||
import Modal from 'src/routes/safe/components/Settings/SpendingLimit/Modal'
|
||||
import { useStyles } from 'src/routes/safe/components/Settings/SpendingLimit/style'
|
||||
import { safeParamAddressFromStateSelector, safeSpendingLimitsSelector } from 'src/logic/safe/store/selectors'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
@@ -155,6 +154,7 @@ export const ReviewSpendingLimits = ({ onBack, onClose, txToken, values }: Revie
|
||||
})
|
||||
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
|
||||
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
|
||||
const [manualGasLimit, setManualGasLimit] = useState<string | undefined>()
|
||||
|
||||
const {
|
||||
gasCostFormatted,
|
||||
@@ -172,6 +172,7 @@ export const ReviewSpendingLimits = ({ onBack, onClose, txToken, values }: Revie
|
||||
operation: estimateGasArgs.operation,
|
||||
safeTxGas: manualSafeTxGas,
|
||||
manualGasPrice,
|
||||
manualGasLimit,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@@ -226,6 +227,10 @@ export const ReviewSpendingLimits = ({ onBack, onClose, txToken, values }: Revie
|
||||
setManualGasPrice(txParameters.ethGasPrice)
|
||||
}
|
||||
|
||||
if (txParameters.ethGasLimit && gasLimit !== txParameters.ethGasLimit) {
|
||||
setManualGasLimit(txParameters.ethGasLimit)
|
||||
}
|
||||
|
||||
if (newSafeTxGas && oldSafeTxGas !== newSafeTxGas) {
|
||||
setManualSafeTxGas(newSafeTxGas)
|
||||
}
|
||||
@@ -242,9 +247,16 @@ export const ReviewSpendingLimits = ({ onBack, onClose, txToken, values }: Revie
|
||||
>
|
||||
{(txParameters, toggleEditMode) => (
|
||||
<>
|
||||
<Modal.TopBar title="New Spending Limit" titleNote="2 of 2" onClose={onClose} />
|
||||
<Modal.Header onClose={onClose}>
|
||||
<Modal.Header.Title size="xs" withoutMargin>
|
||||
New Spending Limit
|
||||
<Text size="lg" color="secondaryLight" as="span">
|
||||
2 of 2
|
||||
</Text>
|
||||
</Modal.Header.Title>
|
||||
</Modal.Header>
|
||||
|
||||
<Block className={classes.container}>
|
||||
<Modal.Body>
|
||||
<Col margin="lg">
|
||||
<AddressInfo address={values.beneficiary} title="Beneficiary" />
|
||||
</Col>
|
||||
@@ -286,7 +298,7 @@ export const ReviewSpendingLimits = ({ onBack, onClose, txToken, values }: Revie
|
||||
isTransactionExecution={isExecution}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
/>
|
||||
</Block>
|
||||
</Modal.Body>
|
||||
<div className={classes.gasCostsContainer}>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
@@ -298,23 +310,17 @@ export const ReviewSpendingLimits = ({ onBack, onClose, txToken, values }: Revie
|
||||
</div>
|
||||
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
color="primary"
|
||||
size="md"
|
||||
onClick={() => onBack({ values: {}, txToken: makeToken(), step: CREATE })}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="primary"
|
||||
size="md"
|
||||
variant="contained"
|
||||
onClick={() => handleSubmit(txParameters)}
|
||||
disabled={existentSpendingLimit === undefined || txEstimationExecutionStatus === EstimationStatus.LOADING}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
<Modal.Footer.Buttons
|
||||
cancelButtonProps={{
|
||||
onClick: () => onBack({ values: {}, txToken: makeToken(), step: CREATE }),
|
||||
text: 'Back',
|
||||
}}
|
||||
confirmButtonProps={{
|
||||
onClick: () => handleSubmit(txParameters),
|
||||
disabled:
|
||||
existentSpendingLimit === undefined || txEstimationExecutionStatus === EstimationStatus.LOADING,
|
||||
}}
|
||||
/>
|
||||
</Modal.Footer>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -2,10 +2,10 @@ import { List } from 'immutable'
|
||||
import React, { ReactElement, Reducer, useCallback, useReducer } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import { Modal } from 'src/components/Modal'
|
||||
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector'
|
||||
import Modal from 'src/routes/safe/components/Settings/SpendingLimit/Modal'
|
||||
|
||||
import Create from './Create'
|
||||
import { ReviewSpendingLimits } from './Review'
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
import { Button } from '@gnosis.pm/safe-react-components'
|
||||
import cn from 'classnames'
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Col from 'src/components/layout/Col'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { Modal } from 'src/components/Modal'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import useTokenInfo from 'src/logic/safe/hooks/useTokenInfo'
|
||||
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
|
||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||
import { getDeleteAllowanceTxData } from 'src/logic/safe/utils/spendingLimits'
|
||||
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
|
||||
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants'
|
||||
|
||||
import { RESET_TIME_OPTIONS } from './FormFields/ResetTime'
|
||||
import { AddressInfo, ResetTimeInfo, TokenInfo } from './InfoDisplay'
|
||||
import { SpendingLimitTable } from './LimitsTable/dataFetcher'
|
||||
import Modal from './Modal'
|
||||
import { useStyles } from './style'
|
||||
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
import cn from 'classnames'
|
||||
|
||||
interface RemoveSpendingLimitModalProps {
|
||||
onClose: () => void
|
||||
@@ -41,6 +39,7 @@ export const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendin
|
||||
const dispatch = useDispatch()
|
||||
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
|
||||
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
|
||||
const [manualGasLimit, setManualGasLimit] = useState<string | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
const {
|
||||
@@ -66,6 +65,7 @@ export const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendin
|
||||
txAmount: '0',
|
||||
safeTxGas: manualSafeTxGas,
|
||||
manualGasPrice,
|
||||
manualGasLimit,
|
||||
})
|
||||
|
||||
const removeSelectedSpendingLimit = async (txParameters: TxParameters): Promise<void> => {
|
||||
@@ -103,6 +103,10 @@ export const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendin
|
||||
setManualGasPrice(txParameters.ethGasPrice)
|
||||
}
|
||||
|
||||
if (txParameters.ethGasLimit && gasLimit !== txParameters.ethGasLimit) {
|
||||
setManualGasLimit(txParameters.ethGasLimit)
|
||||
}
|
||||
|
||||
if (newSafeTxGas && oldSafeTxGas !== newSafeTxGas) {
|
||||
setManualSafeTxGas(newSafeTxGas)
|
||||
}
|
||||
@@ -126,9 +130,13 @@ export const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendin
|
||||
{(txParameters, toggleEditMode) => {
|
||||
return (
|
||||
<>
|
||||
<Modal.TopBar title="Remove Spending Limit" onClose={onClose} />
|
||||
<Modal.Header onClose={onClose}>
|
||||
<Modal.Header.Title size="xs" withoutMargin>
|
||||
Remove Spending Limit
|
||||
</Modal.Header.Title>
|
||||
</Modal.Header>
|
||||
|
||||
<Block className={classes.container}>
|
||||
<Modal.Body>
|
||||
<Col margin="lg">
|
||||
<AddressInfo title="Beneficiary" address={spendingLimit.beneficiary} />
|
||||
</Col>
|
||||
@@ -152,7 +160,7 @@ export const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendin
|
||||
isTransactionExecution={isExecution}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
/>
|
||||
</Block>
|
||||
</Modal.Body>
|
||||
|
||||
<Row className={cn(classes.modalDescription, classes.gasCostsContainer)}>
|
||||
<TransactionFees
|
||||
@@ -165,18 +173,15 @@ export const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendin
|
||||
</Row>
|
||||
|
||||
<Modal.Footer>
|
||||
<Button size="md" color="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="error"
|
||||
size="md"
|
||||
variant="contained"
|
||||
onClick={() => removeSelectedSpendingLimit(txParameters)}
|
||||
disabled={txEstimationExecutionStatus === EstimationStatus.LOADING}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
<Modal.Footer.Buttons
|
||||
cancelButtonProps={{ onClick: onClose }}
|
||||
confirmButtonProps={{
|
||||
color: 'error',
|
||||
onClick: () => removeSelectedSpendingLimit(txParameters),
|
||||
disabled: txEstimationExecutionStatus === EstimationStatus.LOADING,
|
||||
text: 'Remove',
|
||||
}}
|
||||
/>
|
||||
</Modal.Footer>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -120,10 +120,6 @@ export const useStyles = makeStyles(
|
||||
cursor: 'pointer',
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
height: 'auto',
|
||||
maxWidth: 'calc(100% - 30px)',
|
||||
},
|
||||
amountInput: {
|
||||
width: '100% !important',
|
||||
},
|
||||
|
||||
@@ -50,6 +50,7 @@ export const ChangeThresholdModal = ({
|
||||
const [data, setData] = useState('')
|
||||
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
|
||||
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
|
||||
const [manualGasLimit, setManualGasLimit] = useState<string | undefined>()
|
||||
const [editedThreshold, setEditedThreshold] = useState<number>(threshold)
|
||||
|
||||
const {
|
||||
@@ -66,12 +67,13 @@ export const ChangeThresholdModal = ({
|
||||
txRecipient: safeAddress,
|
||||
safeTxGas: manualSafeTxGas,
|
||||
manualGasPrice,
|
||||
manualGasLimit,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
let isCurrent = true
|
||||
const calculateChangeThresholdData = async () => {
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const calculateChangeThresholdData = () => {
|
||||
const safeInstance = getGnosisSafeInstanceAt(safeAddress)
|
||||
const txData = safeInstance.methods.changeThreshold(editedThreshold).encodeABI()
|
||||
if (isCurrent) {
|
||||
setData(txData)
|
||||
@@ -111,6 +113,10 @@ export const ChangeThresholdModal = ({
|
||||
setManualGasPrice(txParameters.ethGasPrice)
|
||||
}
|
||||
|
||||
if (txParameters.ethGasLimit && gasLimit !== txParameters.ethGasLimit) {
|
||||
setManualGasLimit(txParameters.ethGasLimit)
|
||||
}
|
||||
|
||||
if (newSafeTxGas && oldSafeTxGas !== newSafeTxGas) {
|
||||
setManualSafeTxGas(newSafeTxGas)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,13 @@ import { getExplorerInfo } from 'src/config'
|
||||
|
||||
import { getNameFromAddressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
|
||||
export const AddressInfo = ({ address }: { address: string }): ReactElement | null => {
|
||||
type Props = {
|
||||
address: string
|
||||
name?: string | undefined
|
||||
avatarUrl?: string | undefined
|
||||
}
|
||||
|
||||
export const AddressInfo = ({ address, name, avatarUrl }: Props): ReactElement | null => {
|
||||
const recipientName = useSelector((state) => getNameFromAddressBookSelector(state, address))
|
||||
|
||||
if (address === '') {
|
||||
@@ -15,8 +21,9 @@ export const AddressInfo = ({ address }: { address: string }): ReactElement | nu
|
||||
return (
|
||||
<EthHashInfo
|
||||
hash={address}
|
||||
name={recipientName === 'UNKNOWN' ? undefined : recipientName}
|
||||
showIdenticon
|
||||
name={recipientName === 'UNKNOWN' ? name : recipientName}
|
||||
showAvatar
|
||||
customAvatar={avatarUrl}
|
||||
showCopyBtn
|
||||
explorerUrl={getExplorerInfo(address)}
|
||||
/>
|
||||
|
||||
@@ -12,7 +12,7 @@ export const OwnerRow = ({ ownerAddress }: { ownerAddress: string }): ReactEleme
|
||||
<EthHashInfo
|
||||
hash={ownerAddress}
|
||||
name={ownerName === 'UNKNOWN' ? '' : ownerName}
|
||||
showIdenticon
|
||||
showAvatar
|
||||
showCopyBtn
|
||||
explorerUrl={getExplorerInfo(ownerAddress)}
|
||||
shortenHash={4}
|
||||
|
||||
@@ -4,7 +4,7 @@ import CircularProgress from '@material-ui/core/CircularProgress'
|
||||
import React, { ReactElement, useContext, useRef } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import CustomIconText from 'src/components/CustomIconText'
|
||||
import { CustomIconText } from 'src/components/CustomIconText'
|
||||
import {
|
||||
isCustomTxInfo,
|
||||
isMultiSendTxInfo,
|
||||
@@ -24,6 +24,7 @@ import { TokenTransferAmount } from './TokenTransferAmount'
|
||||
import { TxsInfiniteScrollContext } from './TxsInfiniteScroll'
|
||||
import { TxLocationContext } from './TxLocationProvider'
|
||||
import { CalculatedVotes } from './TxQueueCollapsed'
|
||||
import { isCancelTxDetails } from './utils'
|
||||
|
||||
const TxInfo = ({ info }: { info: AssetInfo }) => {
|
||||
if (isTokenTransferAsset(info)) {
|
||||
@@ -116,6 +117,8 @@ export const TxCollapsed = ({
|
||||
const { ref, lastItemId } = useContext(TxsInfiniteScrollContext)
|
||||
|
||||
const willBeReplaced = transaction?.txStatus === 'WILL_BE_REPLACED' ? ' will-be-replaced' : ''
|
||||
const onChainRejection =
|
||||
isCancelTxDetails(transaction.txInfo) && txLocation !== 'history' ? ' on-chain-rejection' : ''
|
||||
|
||||
const txCollapsedNonce = (
|
||||
<div className={'tx-nonce' + willBeReplaced}>
|
||||
@@ -124,7 +127,7 @@ export const TxCollapsed = ({
|
||||
)
|
||||
|
||||
const txCollapsedType = (
|
||||
<div className={'tx-type' + willBeReplaced}>
|
||||
<div className={'tx-type' + willBeReplaced + onChainRejection}>
|
||||
<CustomIconText iconUrl={type.icon} text={type.text} />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -46,7 +46,7 @@ export const TxCollapsedActions = ({ transaction }: TxCollapsedActionsProps): Re
|
||||
</span>
|
||||
</Tooltip>
|
||||
{canCancel && (
|
||||
<Tooltip title="Cancel" placement="top">
|
||||
<Tooltip title="Reject" placement="top">
|
||||
<span>
|
||||
<IconButton size="small" type="button" onClick={handleCancelButtonClick} disabled={isPending}>
|
||||
<Icon type="circleCross" color="error" size="sm" />
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import React, { ReactElement, ReactNode } from 'react'
|
||||
|
||||
import { getNetworkInfo } from 'src/config'
|
||||
import { ExpandedTxDetails, TransactionData } from 'src/logic/safe/store/models/types/gateway.d'
|
||||
import {
|
||||
ExpandedTxDetails,
|
||||
isCustomTxInfo,
|
||||
TransactionData,
|
||||
TransactionInfo,
|
||||
} from 'src/logic/safe/store/models/types/gateway.d'
|
||||
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
import {
|
||||
DeleteSpendingLimitDetails,
|
||||
@@ -20,23 +25,39 @@ const { nativeCoin } = getNetworkInfo()
|
||||
type DetailsWithTxInfoProps = {
|
||||
children: ReactNode
|
||||
txData: TransactionData
|
||||
txInfo: TransactionInfo
|
||||
}
|
||||
|
||||
const DetailsWithTxInfo = ({ children, txData }: DetailsWithTxInfoProps): ReactElement => (
|
||||
<>
|
||||
<TxInfoDetails
|
||||
address={txData.to}
|
||||
title={`Send ${txData.value ? fromTokenUnit(txData.value, nativeCoin.decimals) : 'n/a'} ${nativeCoin.symbol} to:`}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
const DetailsWithTxInfo = ({ children, txData, txInfo }: DetailsWithTxInfoProps): ReactElement => {
|
||||
const amount = txData.value ? fromTokenUnit(txData.value, nativeCoin.decimals) : 'n/a'
|
||||
let name
|
||||
let avatarUrl
|
||||
|
||||
if (isCustomTxInfo(txInfo) && txInfo.toInfo) {
|
||||
name = txInfo.toInfo.name
|
||||
avatarUrl = txInfo.toInfo.logoUri
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TxInfoDetails
|
||||
address={txData.to}
|
||||
name={name}
|
||||
avatarUrl={avatarUrl}
|
||||
title={`Send ${amount} ${nativeCoin.symbol} to:`}
|
||||
/>
|
||||
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type TxDataProps = {
|
||||
txData: ExpandedTxDetails['txData']
|
||||
txInfo: TransactionInfo
|
||||
}
|
||||
|
||||
export const TxData = ({ txData }: TxDataProps): ReactElement | null => {
|
||||
export const TxData = ({ txData, txInfo }: TxDataProps): ReactElement | null => {
|
||||
// nothing to render
|
||||
if (!txData) {
|
||||
return null
|
||||
@@ -51,7 +72,7 @@ export const TxData = ({ txData }: TxDataProps): ReactElement | null => {
|
||||
|
||||
// we render the hex encoded data
|
||||
return (
|
||||
<DetailsWithTxInfo txData={txData}>
|
||||
<DetailsWithTxInfo txData={txData} txInfo={txInfo}>
|
||||
<HexEncodedData title="Data (hex encoded)" hexData={txData.hexData} />
|
||||
</DetailsWithTxInfo>
|
||||
)
|
||||
@@ -74,7 +95,7 @@ export const TxData = ({ txData }: TxDataProps): ReactElement | null => {
|
||||
|
||||
// we render the decoded data
|
||||
return (
|
||||
<DetailsWithTxInfo txData={txData}>
|
||||
<DetailsWithTxInfo txData={txData} txInfo={txInfo}>
|
||||
<MethodDetails data={txData.dataDecoded} />
|
||||
</DetailsWithTxInfo>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Icon, Link, Loader, Text } from '@gnosis.pm/safe-react-components'
|
||||
import cn from 'classnames'
|
||||
import React, { ReactElement, useContext } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import {
|
||||
@@ -12,7 +11,6 @@ import {
|
||||
MultiSigExecutionDetails,
|
||||
Transaction,
|
||||
} from 'src/logic/safe/store/models/types/gateway.d'
|
||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { TransactionActions } from './hooks/useTransactionActions'
|
||||
import { useTransactionDetails } from './hooks/useTransactionDetails'
|
||||
import { TxDetailsContainer, Centered, AlignItemsWithMargin } from './styled'
|
||||
@@ -30,34 +28,44 @@ const NormalBreakingText = styled(Text)`
|
||||
`
|
||||
|
||||
const TxDataGroup = ({ txDetails }: { txDetails: ExpandedTxDetails }): ReactElement | null => {
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
|
||||
if (isTransferTxInfo(txDetails.txInfo) || isSettingsChangeTxInfo(txDetails.txInfo)) {
|
||||
return <TxInfo txInfo={txDetails.txInfo} />
|
||||
}
|
||||
|
||||
if (isCancelTxDetails({ executedAt: txDetails.executedAt, txInfo: txDetails.txInfo, safeAddress })) {
|
||||
if (isCancelTxDetails(txDetails.txInfo)) {
|
||||
const txNonce = `${(txDetails.detailedExecutionInfo as MultiSigExecutionDetails).nonce ?? NOT_AVAILABLE}`
|
||||
const isTxExecuted = txDetails.executedAt
|
||||
|
||||
// executed rejection transaction
|
||||
let message = `This is an on-chain rejection that didn't send any funds.
|
||||
This on-chain rejection replaced all transactions with nonce ${txNonce}.`
|
||||
|
||||
if (!isTxExecuted) {
|
||||
// queued rejection transaction
|
||||
message = `This is an on-chain rejection that doesn't send any funds.
|
||||
Executing this on-chain rejection will replace all currently awaiting transactions with nonce ${txNonce}.`
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<NormalBreakingText size="xl">
|
||||
{`This is an empty cancelling transaction that doesn't send any funds.
|
||||
Executing this transaction will replace all currently awaiting transactions with nonce ${
|
||||
(txDetails.detailedExecutionInfo as MultiSigExecutionDetails).nonce ?? NOT_AVAILABLE
|
||||
}.`}
|
||||
</NormalBreakingText>
|
||||
<Link
|
||||
href="https://help.gnosis-safe.io/en/articles/4738501-why-do-i-need-to-pay-for-cancelling-a-transaction"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title="Why do I need to pay for cancelling a transaction?"
|
||||
>
|
||||
<AlignItemsWithMargin>
|
||||
<Text size="xl" as="span" color="primary">
|
||||
Why do I need to pay for cancelling a transaction?
|
||||
</Text>
|
||||
<Icon size="sm" type="externalLink" color="primary" />
|
||||
</AlignItemsWithMargin>
|
||||
</Link>
|
||||
<NormalBreakingText size="xl">{message}</NormalBreakingText>
|
||||
{!isTxExecuted && (
|
||||
<>
|
||||
<br />
|
||||
<Link
|
||||
href="https://help.gnosis-safe.io/en/articles/4738501-why-do-i-need-to-pay-for-cancelling-a-transaction"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title="Why do I need to pay for rejecting a transaction?"
|
||||
>
|
||||
<AlignItemsWithMargin>
|
||||
<Text size="xl" as="span" color="primary">
|
||||
Why do I need to pay for rejecting a transaction?
|
||||
</Text>
|
||||
<Icon size="sm" type="externalLink" color="primary" />
|
||||
</AlignItemsWithMargin>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -66,7 +74,7 @@ const TxDataGroup = ({ txDetails }: { txDetails: ExpandedTxDetails }): ReactElem
|
||||
return null
|
||||
}
|
||||
|
||||
return <TxData txData={txDetails.txData} />
|
||||
return <TxData txData={txDetails.txData} txInfo={txDetails.txInfo} />
|
||||
}
|
||||
|
||||
type TxDetailsProps = {
|
||||
@@ -116,7 +124,7 @@ export const TxDetails = ({ transaction, actions }: TxDetailsProps): ReactElemen
|
||||
'will-be-replaced': transaction.txStatus === 'WILL_BE_REPLACED',
|
||||
})}
|
||||
>
|
||||
<TxOwners detailedExecutionInfo={data.detailedExecutionInfo} />
|
||||
<TxOwners txDetails={data} />
|
||||
</div>
|
||||
{!data.executedAt && txLocation !== 'history' && actions?.isUserAnOwner && (
|
||||
<div className={cn('tx-details-actions', { 'will-be-replaced': transaction.txStatus === 'WILL_BE_REPLACED' })}>
|
||||
|
||||
@@ -41,7 +41,7 @@ export const TxExpandedActions = ({ transaction }: TxExpandedActionsProps): Reac
|
||||
</Button>
|
||||
{canCancel && (
|
||||
<Button size="md" color="error" onClick={handleCancelButtonClick} className="error" disabled={isPending}>
|
||||
Cancel
|
||||
Reject
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
|
||||
import {
|
||||
ExpandedTxDetails,
|
||||
isSettingsChangeTxInfo,
|
||||
isTransferTxInfo,
|
||||
} from 'src/logic/safe/store/models/types/gateway.d'
|
||||
import { TransactionInfo, isSettingsChangeTxInfo, isTransferTxInfo } from 'src/logic/safe/store/models/types/gateway.d'
|
||||
import { TxInfoSettings } from './TxInfoSettings'
|
||||
import { TxInfoTransfer } from './TxInfoTransfer'
|
||||
|
||||
type TxInfoProps = {
|
||||
txInfo: ExpandedTxDetails['txInfo']
|
||||
}
|
||||
|
||||
export const TxInfo = ({ txInfo }: TxInfoProps): ReactElement | null => {
|
||||
export const TxInfo = ({ txInfo }: { txInfo: TransactionInfo }): ReactElement | null => {
|
||||
if (isSettingsChangeTxInfo(txInfo)) {
|
||||
return <TxInfoSettings settingsInfo={txInfo.settingsInfo} />
|
||||
}
|
||||
|
||||
@@ -21,11 +21,20 @@ const SingleRow = styled.div`
|
||||
type TxInfoDetailsProps = {
|
||||
title: string
|
||||
address: string
|
||||
name?: string | undefined
|
||||
avatarUrl?: string | undefined
|
||||
isTransferType?: boolean
|
||||
txInfo?: Transfer
|
||||
}
|
||||
|
||||
export const TxInfoDetails = ({ title, address, isTransferType, txInfo }: TxInfoDetailsProps): ReactElement => {
|
||||
export const TxInfoDetails = ({
|
||||
title,
|
||||
address,
|
||||
isTransferType,
|
||||
txInfo,
|
||||
name,
|
||||
avatarUrl,
|
||||
}: TxInfoDetailsProps): ReactElement => {
|
||||
const recipientName = useSelector((state) => getNameFromAddressBookSelector(state, address))
|
||||
const knownAddress = recipientName !== 'UNKNOWN'
|
||||
|
||||
@@ -59,6 +68,7 @@ export const TxInfoDetails = ({ title, address, isTransferType, txInfo }: TxInfo
|
||||
selectedToken: ZERO_ADDRESS,
|
||||
tokenAmount: '0',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (txInfo) {
|
||||
const isCollectible = txInfo.transferInfo.type === 'ERC721'
|
||||
@@ -76,7 +86,7 @@ export const TxInfoDetails = ({ title, address, isTransferType, txInfo }: TxInfo
|
||||
return (
|
||||
<InfoDetails title={title}>
|
||||
<SingleRow>
|
||||
<AddressInfo address={address} />
|
||||
<AddressInfo address={address} name={name} avatarUrl={avatarUrl} />
|
||||
<EllipsisTransactionDetails
|
||||
address={address}
|
||||
knownAddress={knownAddress}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Text } from '@gnosis.pm/safe-react-components'
|
||||
import { Text, Icon } from '@gnosis.pm/safe-react-components'
|
||||
import React, { ReactElement } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@@ -6,43 +6,55 @@ import Img from 'src/components/layout/Img'
|
||||
import { ExpandedTxDetails, isModuleExecutionDetails } from 'src/logic/safe/store/models/types/gateway.d'
|
||||
import TransactionListActive from './assets/transactions-list-active.svg'
|
||||
import TransactionListInactive from './assets/transactions-list-inactive.svg'
|
||||
import CheckCircleGreen from './assets/check-circle-green.svg'
|
||||
import PlusCircleGreen from './assets/plus-circle-green.svg'
|
||||
import { OwnerRow } from './OwnerRow'
|
||||
import { OwnerList, OwnerListItem } from './styled'
|
||||
|
||||
type TxOwnersProps = {
|
||||
detailedExecutionInfo: ExpandedTxDetails['detailedExecutionInfo']
|
||||
}
|
||||
import { isCancelTxDetails } from './utils'
|
||||
|
||||
const StyledImg = styled(Img)`
|
||||
background-color: transparent;
|
||||
border-radius: 50%;
|
||||
`
|
||||
|
||||
export const TxOwners = ({ detailedExecutionInfo }: TxOwnersProps): ReactElement | null => {
|
||||
export const TxOwners = ({ txDetails }: { txDetails: ExpandedTxDetails }): ReactElement | null => {
|
||||
const { txInfo, detailedExecutionInfo } = txDetails
|
||||
|
||||
if (!detailedExecutionInfo || isModuleExecutionDetails(detailedExecutionInfo)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const confirmationsNeeded = detailedExecutionInfo.confirmationsRequired - detailedExecutionInfo.confirmations.length
|
||||
|
||||
const CreationNode = isCancelTxDetails(txInfo) ? (
|
||||
<OwnerListItem>
|
||||
<span className="icon">
|
||||
<Icon size="sm" type="circleCross" color="error" />
|
||||
</span>
|
||||
<div className="legend">
|
||||
<Text color="error" size="xl" strong>
|
||||
On-chain rejection created
|
||||
</Text>
|
||||
</div>
|
||||
</OwnerListItem>
|
||||
) : (
|
||||
<OwnerListItem>
|
||||
<span className="icon">
|
||||
<Icon size="sm" type="add" color="primary" />
|
||||
</span>
|
||||
<div className="legend">
|
||||
<Text color="primary" size="xl" strong>
|
||||
Created
|
||||
</Text>
|
||||
</div>
|
||||
</OwnerListItem>
|
||||
)
|
||||
|
||||
return (
|
||||
<OwnerList>
|
||||
<OwnerListItem>
|
||||
<span className="icon">
|
||||
<StyledImg alt="" src={PlusCircleGreen} />
|
||||
</span>
|
||||
<div className="legend">
|
||||
<Text color="primary" size="xl" strong>
|
||||
Created
|
||||
</Text>
|
||||
</div>
|
||||
</OwnerListItem>
|
||||
{CreationNode}
|
||||
{detailedExecutionInfo.confirmations.map(({ signer }) => (
|
||||
<OwnerListItem key={signer}>
|
||||
<span className="icon">
|
||||
<StyledImg alt="" src={CheckCircleGreen} />
|
||||
<Icon size="sm" type="circleCheck" color="primary" />
|
||||
</span>
|
||||
<div className="legend">
|
||||
<Text color="primary" size="xl" strong>
|
||||
@@ -55,7 +67,11 @@ export const TxOwners = ({ detailedExecutionInfo }: TxOwnersProps): ReactElement
|
||||
{confirmationsNeeded <= 0 ? (
|
||||
<OwnerListItem>
|
||||
<span className="icon">
|
||||
<StyledImg alt="" src={detailedExecutionInfo.executor ? CheckCircleGreen : TransactionListActive} />
|
||||
{detailedExecutionInfo.executor ? (
|
||||
<Icon type="circleCheck" size="sm" color="primary" />
|
||||
) : (
|
||||
<StyledImg alt="" src={TransactionListActive} />
|
||||
)}
|
||||
</span>
|
||||
<div className="legend">
|
||||
<Text color="primary" size="xl" strong>
|
||||
|
||||
@@ -27,7 +27,7 @@ export const TxSummary = ({ txDetails }: { txDetails: ExpandedTxDetails }): Reac
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
{nonce && (
|
||||
{nonce !== undefined && (
|
||||
<div className="tx-nonce">
|
||||
<Text size="xl" strong as="span">
|
||||
Nonce:{' '}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M0 0H16V16H0z" transform="translate(-273 -201) translate(273 201)"/>
|
||||
<path fill="#F02525" fill-rule="nonzero" d="M8 15c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm0-2c-2.761 0-5-2.239-5-5s2.239-5 5-5 5 2.239 5 5-2.239 5-5 5z" transform="translate(-273 -201) translate(273 201)"/>
|
||||
<path fill="#F02525" d="M9.414 8l1.414 1.414c.391.39.391 1.024 0 1.414-.39.391-1.023.391-1.414 0L8 9.414l-1.414 1.414c-.39.391-1.024.391-1.414 0-.391-.39-.391-1.023 0-1.414L6.586 8 5.172 6.586c-.391-.39-.391-1.024 0-1.414.39-.391 1.023-.391 1.414 0L8 6.586l1.414-1.414c.39-.391 1.024-.391 1.414 0 .391.39.391 1.023 0 1.414L9.414 8z" transform="translate(-273 -201) translate(273 201)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 936 B |
@@ -7,7 +7,6 @@ import { getQueuedTransactionsByNonce } from 'src/logic/safe/store/selectors/gat
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { userAccountSelector } from 'src/logic/wallets/store/selectors'
|
||||
import { TxLocationContext } from 'src/routes/safe/components/Transactions/TxList/TxLocationProvider'
|
||||
import { isCancelTransaction } from 'src/routes/safe/components/Transactions/TxList/utils'
|
||||
import { grantedSelector } from 'src/routes/safe/container/selector'
|
||||
import { AppReduxState } from 'src/store'
|
||||
|
||||
@@ -60,14 +59,7 @@ export const useTransactionActions = (transaction: Transaction): TransactionActi
|
||||
canConfirm,
|
||||
canConfirmThenExecute: txLocation === 'queued.next' && canConfirm && oneToGo,
|
||||
canExecute: txLocation === 'queued.next' && thresholdReached,
|
||||
canCancel: !transactionsByNonce.some(
|
||||
({ txInfo }) =>
|
||||
isCustomTxInfo(txInfo) &&
|
||||
isCancelTransaction({
|
||||
txInfo,
|
||||
safeAddress,
|
||||
}),
|
||||
),
|
||||
canCancel: !transactionsByNonce.some(({ txInfo }) => isCustomTxInfo(txInfo) && txInfo.isCancellation),
|
||||
isUserAnOwner,
|
||||
oneToGo,
|
||||
})
|
||||
|
||||
@@ -37,10 +37,10 @@ export const useTransactionStatus = (transaction: Transaction): TransactionStatu
|
||||
|
||||
switch (transaction.txStatus) {
|
||||
case 'AWAITING_CONFIRMATIONS':
|
||||
text = signaturePending(currentUser) ? 'Awaiting your confirmation' : 'Awaiting confirmations'
|
||||
text = signaturePending(currentUser) ? 'Needs your confirmation' : 'Needs confirmations'
|
||||
break
|
||||
case 'AWAITING_EXECUTION':
|
||||
text = 'Awaiting execution'
|
||||
text = 'Needs execution'
|
||||
break
|
||||
case 'PENDING':
|
||||
case 'PENDING_FAILED':
|
||||
|
||||
@@ -4,13 +4,13 @@ import { useSelector } from 'react-redux'
|
||||
import { Transaction } from 'src/logic/safe/store/models/types/gateway.d'
|
||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import CustomTxIcon from 'src/routes/safe/components/Transactions/TxList/assets/custom.svg'
|
||||
import CircleCrossRed from 'src/routes/safe/components/Transactions/TxList/assets/circle-cross-red.svg'
|
||||
import IncomingTxIcon from 'src/routes/safe/components/Transactions/TxList/assets/incoming.svg'
|
||||
import OutgoingTxIcon from 'src/routes/safe/components/Transactions/TxList/assets/outgoing.svg'
|
||||
import SettingsTxIcon from 'src/routes/safe/components/Transactions/TxList/assets/settings.svg'
|
||||
import { isCancelTransaction } from 'src/routes/safe/components/Transactions/TxList/utils'
|
||||
|
||||
export type TxTypeProps = {
|
||||
icon: string
|
||||
icon: string | null
|
||||
text: string
|
||||
}
|
||||
|
||||
@@ -40,20 +40,8 @@ export const useTransactionType = (tx: Transaction): TxTypeProps => {
|
||||
break
|
||||
}
|
||||
|
||||
// TODO: isCancel
|
||||
// there are two 'cancelling' tx identification
|
||||
// this one is the candidate to remain when the client gateway implements
|
||||
// https://github.com/gnosis/safe-client-gateway/issues/255
|
||||
if (typeof tx.txInfo.isCancellation === 'boolean' && tx.txInfo.isCancellation) {
|
||||
setType({ icon: CustomTxIcon, text: 'Cancelling transaction' })
|
||||
break
|
||||
}
|
||||
|
||||
// TODO: isCancel
|
||||
// remove the following condition when issue#255 is implemented
|
||||
// also remove `isCancelTransaction` function
|
||||
if (isCancelTransaction({ txInfo: tx.txInfo, safeAddress })) {
|
||||
setType({ icon: CustomTxIcon, text: 'Cancelling transaction' })
|
||||
if (tx.txInfo.isCancellation) {
|
||||
setType({ icon: CircleCrossRed, text: 'On-chain rejection' })
|
||||
break
|
||||
}
|
||||
|
||||
@@ -62,6 +50,12 @@ export const useTransactionType = (tx: Transaction): TxTypeProps => {
|
||||
break
|
||||
}
|
||||
|
||||
const toInfo = tx.txInfo.toInfo
|
||||
if (toInfo) {
|
||||
setType({ icon: toInfo.logoUri, text: toInfo.name })
|
||||
break
|
||||
}
|
||||
|
||||
setType({ icon: CustomTxIcon, text: 'Contract interaction' })
|
||||
break
|
||||
}
|
||||
|
||||
@@ -229,6 +229,7 @@ export const ApproveTxModal = ({
|
||||
const oneConfirmationLeft = !thresholdReached && _countingCurrentConfirmation === _threshold
|
||||
const isTheTxReadyToBeExecuted = oneConfirmationLeft ? true : thresholdReached
|
||||
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
|
||||
const [manualGasLimit, setManualGasLimit] = useState<string | undefined>()
|
||||
const {
|
||||
confirmations,
|
||||
data,
|
||||
@@ -262,7 +263,8 @@ export const ApproveTxModal = ({
|
||||
preApprovingOwner: approveAndExecute ? userAddress : undefined,
|
||||
safeTxGas,
|
||||
operation,
|
||||
manualGasPrice: manualGasPrice,
|
||||
manualGasPrice,
|
||||
manualGasLimit,
|
||||
})
|
||||
|
||||
const handleExecuteCheckbox = () => setApproveAndExecute((prevApproveAndExecute) => !prevApproveAndExecute)
|
||||
@@ -312,6 +314,10 @@ export const ApproveTxModal = ({
|
||||
if (newGasPrice && oldGasPrice !== newGasPrice) {
|
||||
setManualGasPrice(newGasPrice.toString())
|
||||
}
|
||||
|
||||
if (txParameters.ethGasLimit && gasLimit !== txParameters.ethGasLimit) {
|
||||
setManualGasLimit(txParameters.ethGasLimit.toString())
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -163,6 +163,32 @@ const failedTransaction = css`
|
||||
}
|
||||
`
|
||||
|
||||
const onChainRejection = css`
|
||||
&.on-chain-rejection {
|
||||
background-color: ${({ theme }) => theme.colors.errorTooltip};
|
||||
border-left: 4px solid ${({ theme }) => theme.colors.error};
|
||||
border-radius: 4px;
|
||||
padding-left: 7px;
|
||||
height: 22px;
|
||||
max-width: 165px;
|
||||
|
||||
> div {
|
||||
height: 17px;
|
||||
align-items: center;
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
letter-spacing: 1px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
margin-left: -2px;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const StyledTransaction = styled.div`
|
||||
${willBeReplaced};
|
||||
${failedTransaction};
|
||||
@@ -175,6 +201,10 @@ export const StyledTransaction = styled.div`
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.tx-type {
|
||||
${onChainRejection};
|
||||
}
|
||||
|
||||
.tx-votes {
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { BigNumber } from 'bignumber.js'
|
||||
|
||||
import { getNetworkInfo } from 'src/config'
|
||||
import {
|
||||
Custom,
|
||||
isCustomTxInfo,
|
||||
isTransferTxInfo,
|
||||
Transaction,
|
||||
@@ -12,7 +11,6 @@ import {
|
||||
|
||||
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { sameString } from 'src/utils/strings'
|
||||
|
||||
export const NOT_AVAILABLE = 'n/a'
|
||||
|
||||
@@ -90,27 +88,11 @@ export const getTxTokenData = (txInfo: Transfer): txTokenData => {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: isCancel
|
||||
// how can we be sure that it's a cancel tx without asking for tx-details?
|
||||
// can the client-gateway service provide info about the tx, Like: `isCancelTransaction: boolean`?
|
||||
// it will be solved as part of: https://github.com/gnosis/safe-client-gateway/issues/255
|
||||
export const isCancelTransaction = ({ txInfo, safeAddress }: { txInfo: Custom; safeAddress: string }): boolean =>
|
||||
sameAddress(txInfo.to, safeAddress) &&
|
||||
sameString(txInfo.dataSize, '0') &&
|
||||
sameString(txInfo.value, '0') &&
|
||||
txInfo.methodName === null
|
||||
|
||||
type IsCancelTxDetailsProps = {
|
||||
executedAt: number | null
|
||||
txInfo: Transaction['txInfo']
|
||||
safeAddress: string
|
||||
}
|
||||
export const isCancelTxDetails = ({ executedAt, txInfo, safeAddress }: IsCancelTxDetailsProps): boolean =>
|
||||
!executedAt &&
|
||||
export const isCancelTxDetails = (txInfo: Transaction['txInfo']): boolean =>
|
||||
// custom transaction
|
||||
isCustomTxInfo(txInfo) &&
|
||||
// verify that it's a cancel tx based on it's info
|
||||
isCancelTransaction({ safeAddress, txInfo })
|
||||
// flag-based identification
|
||||
txInfo.isCancellation
|
||||
|
||||
export const addressInList = (list: string[] = []) => (address: string): boolean =>
|
||||
list.some((ownerAddress) => sameAddress(ownerAddress, address))
|
||||
|
||||
@@ -81,7 +81,7 @@ export const useTransactionParameters = (props?: Props): TxParameters => {
|
||||
useEffect(() => {
|
||||
const getSafeNonce = async () => {
|
||||
if (safeAddress) {
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const safeInstance = getGnosisSafeInstanceAt(safeAddress)
|
||||
const lastTx = await getLastTx(safeAddress)
|
||||
const nonce = await getNewTxNonce(lastTx, safeInstance)
|
||||
setSafeNonce(nonce)
|
||||
|
||||
@@ -130,7 +130,7 @@ describe('DOM > Feature > CREATE a Safe', () => {
|
||||
expect(address).not.toBe(null)
|
||||
expect(address).not.toBe(undefined)
|
||||
|
||||
const gnosisSafe = await getGnosisSafeInstanceAt(address)
|
||||
const gnosisSafe = getGnosisSafeInstanceAt(address)
|
||||
const storedOwners = await gnosisSafe.methods.getOwners().call()
|
||||
expect(storedOwners.length).toEqual(4)
|
||||
const safeThreshold = await gnosisSafe.methods.getThreshold().call()
|
||||
|
||||
@@ -5,7 +5,7 @@ const disabled = '#5D6D74'
|
||||
const errorColor = '#f02525'
|
||||
const fancyColor = '#f02525'
|
||||
const fontColor = '#001428'
|
||||
const headerHeight = '53px'
|
||||
const headerHeight = '52px'
|
||||
const lg = '24px'
|
||||
const marginButtonImg = '12px'
|
||||
const md = '16px'
|
||||
|
||||
@@ -13,7 +13,7 @@ $warning: #ffc05f;
|
||||
$fancy: #f02525;
|
||||
$secondary: #008C73;
|
||||
|
||||
$headerHeight: 53px;
|
||||
$headerHeight: 52px;
|
||||
$marginButtonImg: 12px;
|
||||
|
||||
$lg: 24px;
|
||||
|
||||
@@ -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#80f5db6":
|
||||
"@gnosis.pm/safe-react-components@https://github.com/gnosis/safe-react-components.git#2e427ee":
|
||||
version "0.5.0"
|
||||
resolved "https://github.com/gnosis/safe-react-components.git#80f5db672d417ea410d58c8d713e46e16e3c7e7f"
|
||||
resolved "https://github.com/gnosis/safe-react-components.git#2e427ee36694c7964301fc155b0c080101a34bed"
|
||||
dependencies:
|
||||
classnames "^2.2.6"
|
||||
react-media "^1.10.0"
|
||||
|
||||
Reference in New Issue
Block a user