From f8ccff0796f15d010b03bee54e6caad3e3122c68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Longoni?= Date: Tue, 30 Mar 2021 15:46:41 -0300 Subject: [PATCH 01/14] (Fix) Layout inaccuracies (#2072) Co-authored-by: lukasgnosis Co-authored-by: Daniel Sanchez --- package.json | 2 +- .../AppLayout/Header/components/Layout.tsx | 4 +- .../ProviderDetails/ConnectDetails.tsx | 32 ++++++------- .../ProviderDetails/UserDetails.tsx | 14 ++++-- .../AppLayout/Sidebar/SafeHeader/index.tsx | 4 +- src/components/AppLayout/index.tsx | 10 ++-- src/components/ConnectButton/index.tsx | 2 +- src/components/CookiesBanner/index.tsx | 2 +- .../safe/components/AddressBook/index.tsx | 27 ++++++++--- .../safe/components/AddressBook/style.ts | 16 ++----- .../safe/components/Balances/Coins/index.tsx | 48 +++++++++++-------- .../safe/components/Balances/Coins/styles.ts | 19 +------- src/theme/variables.js | 2 +- src/theme/variables.scss | 2 +- yarn.lock | 4 +- 15 files changed, 92 insertions(+), 96 deletions(-) diff --git a/package.json b/package.json index dd954f4a..f3ef8608 100644 --- a/package.json +++ b/package.json @@ -161,7 +161,7 @@ "@gnosis.pm/safe-apps-sdk": "1.0.3", "@gnosis.pm/safe-apps-sdk-v1": "npm:@gnosis.pm/safe-apps-sdk@0.4.2", "@gnosis.pm/safe-contracts": "1.1.1-dev.2", - "@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#80f5db6", + "@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#a68a67e", "@gnosis.pm/util-contracts": "2.0.6", "@ledgerhq/hw-transport-node-hid-singleton": "5.45.0", "@material-ui/core": "^4.11.0", diff --git a/src/components/AppLayout/Header/components/Layout.tsx b/src/components/AppLayout/Header/components/Layout.tsx index 85051334..fae9690d 100644 --- a/src/components/AppLayout/Header/components/Layout.tsx +++ b/src/components/AppLayout/Header/components/Layout.tsx @@ -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', diff --git a/src/components/AppLayout/Header/components/ProviderDetails/ConnectDetails.tsx b/src/components/AppLayout/Header/components/ProviderDetails/ConnectDetails.tsx index 0b52d4e5..e3ebe602 100644 --- a/src/components/AppLayout/Header/components/ProviderDetails/ConnectDetails.tsx +++ b/src/components/AppLayout/Header/components/ProviderDetails/ConnectDetails.tsx @@ -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 }) => ( - <> -
- - - Connect a Wallet - - -
- - + + + + Connect a Wallet + + + + + - + ) export default withStyles(styles as any)(ConnectDetails) diff --git a/src/components/AppLayout/Header/components/ProviderDetails/UserDetails.tsx b/src/components/AppLayout/Header/components/ProviderDetails/UserDetails.tsx index d50fa6cf..1598904f 100644 --- a/src/components/AppLayout/Header/components/ProviderDetails/UserDetails.tsx +++ b/src/components/AppLayout/Header/components/ProviderDetails/UserDetails.tsx @@ -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 ( - <> + {connected ? ( - + ) : ( )} @@ -185,11 +189,11 @@ export const UserDetails = ({ variant="contained" data-testid="disconnect-btn" > - + Disconnect - + ) } diff --git a/src/components/AppLayout/Sidebar/SafeHeader/index.tsx b/src/components/AppLayout/Sidebar/SafeHeader/index.tsx index 149f7d84..fad57d55 100644 --- a/src/components/AppLayout/Sidebar/SafeHeader/index.tsx +++ b/src/components/AppLayout/Sidebar/SafeHeader/index.tsx @@ -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 = ({ {balance} - + New transaction diff --git a/src/components/AppLayout/index.tsx b/src/components/AppLayout/index.tsx index e70a31b4..72fb20a0 100644 --- a/src/components/AppLayout/index.tsx +++ b/src/components/AppLayout/index.tsx @@ -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` diff --git a/src/components/ConnectButton/index.tsx b/src/components/ConnectButton/index.tsx index 4d3c23dd..dc9bbd2e 100644 --- a/src/components/ConnectButton/index.tsx +++ b/src/components/ConnectButton/index.tsx @@ -73,7 +73,7 @@ export const onConnectButtonClick = async () => { } const ConnectButton = (props): React.ReactElement => ( - ) diff --git a/src/components/CookiesBanner/index.tsx b/src/components/CookiesBanner/index.tsx index 9219aab6..781ae6de 100644 --- a/src/components/CookiesBanner/index.tsx +++ b/src/components/CookiesBanner/index.tsx @@ -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', diff --git a/src/routes/safe/components/AddressBook/index.tsx b/src/routes/safe/components/AddressBook/index.tsx index 7460f663..38570218 100644 --- a/src/routes/safe/components/AddressBook/index.tsx +++ b/src/routes/safe/components/AddressBook/index.tsx @@ -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 ? ( - + + + Send + + ) : null} diff --git a/src/routes/safe/components/AddressBook/style.ts b/src/routes/safe/components/AddressBook/style.ts index a8314a31..e6611ec0 100644 --- a/src/routes/safe/components/AddressBook/style.ts +++ b/src/routes/safe/components/AddressBook/style.ts @@ -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, }, diff --git a/src/routes/safe/components/Balances/Coins/index.tsx b/src/routes/safe/components/Balances/Coins/index.tsx index 20335252..81f92e87 100644 --- a/src/routes/safe/components/Balances/Coins/index.tsx +++ b/src/routes/safe/components/Balances/Coins/index.tsx @@ -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 => { {granted && ( - + + + Send + + )} - + + + + Receive + + diff --git a/src/routes/safe/components/Balances/Coins/styles.ts b/src/routes/safe/components/Balances/Coins/styles.ts index ae4ae6ac..bd221514 100644 --- a/src/routes/safe/components/Balances/Coins/styles.ts +++ b/src/routes/safe/components/Balances/Coins/styles.ts @@ -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, }, diff --git a/src/theme/variables.js b/src/theme/variables.js index 71f4625d..66811e2a 100644 --- a/src/theme/variables.js +++ b/src/theme/variables.js @@ -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' diff --git a/src/theme/variables.scss b/src/theme/variables.scss index ad157751..f94de36c 100644 --- a/src/theme/variables.scss +++ b/src/theme/variables.scss @@ -13,7 +13,7 @@ $warning: #ffc05f; $fancy: #f02525; $secondary: #008C73; -$headerHeight: 53px; +$headerHeight: 52px; $marginButtonImg: 12px; $lg: 24px; diff --git a/yarn.lock b/yarn.lock index 00a9ec29..56873e07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1596,9 +1596,9 @@ solc "0.5.14" truffle "^5.1.21" -"@gnosis.pm/safe-react-components@https://github.com/gnosis/safe-react-components.git#80f5db6": +"@gnosis.pm/safe-react-components@https://github.com/gnosis/safe-react-components.git#a68a67e": version "0.5.0" - resolved "https://github.com/gnosis/safe-react-components.git#80f5db672d417ea410d58c8d713e46e16e3c7e7f" + resolved "https://github.com/gnosis/safe-react-components.git#a68a67e634d0be091856ebba9f6874eebb767cd7" dependencies: classnames "^2.2.6" react-media "^1.10.0" From 2669471e13ca52b4eb8d25b5c034de2faab20bd5 Mon Sep 17 00:00:00 2001 From: Mikhail Mikheev Date: Wed, 31 Mar 2021 17:36:18 +0300 Subject: [PATCH 02/14] Bug: Safe apps passed value converted to ether to createTransaction action instead of wei (#2115) * dont convert transaction value to ether for createTransaction Co-authored-by: Daniel Sanchez --- .../components/ConfirmTxModal/ReviewConfirm.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx b/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx index 0a775471..3a56ebcb 100644 --- a/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx +++ b/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx @@ -13,6 +13,7 @@ import { MULTI_SEND_ADDRESS } from 'src/logic/contracts/safeContracts' import { DELEGATE_CALL, TX_NOTIFICATION_TYPES, CALL } from 'src/logic/safe/transactions' import { encodeMultiSendCall } from 'src/logic/safe/transactions/multisend' import { getNetworkInfo } from 'src/config' +import { web3ReadOnly } from 'src/logic/wallets/getWeb3' import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' import { TransactionFees } from 'src/components/TransactionsFees' import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters' @@ -58,6 +59,10 @@ type Props = ConfirmTxModalProps & { hidden: boolean // used to prevent re-rendering the modal each time a tx is inspected } +const parseTxValue = (value: string | number): string => { + return web3ReadOnly.utils.toBN(value).toString() +} + export const ReviewConfirm = ({ app, txs, @@ -86,10 +91,9 @@ export const ReviewConfirm = ({ isMultiSend, ]) const txValue: string | undefined = useMemo( - () => (isMultiSend ? '0' : txs[0]?.value && fromTokenUnit(txs[0]?.value, nativeCoin.decimals)), + () => (isMultiSend ? '0' : txs[0]?.value && parseTxValue(txs[0]?.value)), [txs, isMultiSend], ) - const operation = useMemo(() => (isMultiSend ? DELEGATE_CALL : CALL), [isMultiSend]) const [manualSafeTxGas, setManualSafeTxGas] = useState(0) const [manualGasPrice, setManualGasPrice] = useState() @@ -199,7 +203,11 @@ export const ReviewConfirm = ({ {/* Txs decoded */} - + From 4e96152e2166d80635d224e02fbe462b482d350f Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 1 Apr 2021 05:54:14 -0300 Subject: [PATCH 03/14] Replace AddressInfo by EthHashInfo (#2103) --- src/components/AddressInfo/index.tsx | 78 ------------------- .../ConfirmTxModal/ReviewConfirm.tsx | 28 +++++-- .../ContractInteraction/Review/index.tsx | 7 +- 3 files changed, 26 insertions(+), 87 deletions(-) delete mode 100644 src/components/AddressInfo/index.tsx diff --git a/src/components/AddressInfo/index.tsx b/src/components/AddressInfo/index.tsx deleted file mode 100644 index 8e08f2b7..00000000 --- a/src/components/AddressInfo/index.tsx +++ /dev/null @@ -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 ( - -
- -
-
- {safeName && ( - - {safeName} - - )} -
- - {safeAddress} - - - -
- {ethBalance && ( - - - Balance: {`${ethBalance} ${nativeCoin.symbol}`} - - - )} -
-
- ) -} - -export default AddressInfo diff --git a/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx b/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx index 3a56ebcb..04e70266 100644 --- a/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx +++ b/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx @@ -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(null) const dispatch = useDispatch() + const explorerUrl = getExplorerInfo(safeAddress) const txRecipient: string | undefined = useMemo(() => (isMultiSend ? MULTI_SEND_ADDRESS : txs[0]?.to), [ txs, @@ -198,7 +213,11 @@ export const ReviewConfirm = ({ {/* Safe */} - + + + Balance: + {`${ethBalance} ${nativeCoin.symbol}`} + @@ -212,9 +231,7 @@ export const ReviewConfirm = ({ - {!isMultiSend && } - {/* Warning gas estimation */} {params?.safeTxGas && (
@@ -227,7 +244,6 @@ export const ReviewConfirm = ({ />
)} - {/* Tx Parameters */} { + const explorerUrl = getExplorerInfo(tx.contractAddress as string) const classes = useStyles() const dispatch = useDispatch() const safeAddress = useSelector(safeParamAddressFromStateSelector) @@ -144,7 +145,7 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE - + From 15ae933a18c2fbd6c092e3cd95445678247621c6 Mon Sep 17 00:00:00 2001 From: Fernando Date: Thu, 1 Apr 2021 17:47:15 -0300 Subject: [PATCH 04/14] (Feature) - v3 Decoded Tx - Generic Modal (#2054) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Agustín Longoni Co-authored-by: Daniel Sanchez --- src/components/App/index.tsx | 8 +- src/components/Modal/index.stories.tsx | 174 +++++++++++ src/components/Modal/index.tsx | 290 +++++++++++++++--- .../CreateEditEntryModal/index.tsx | 2 +- .../AddressBook/CreateEditEntryModal/style.ts | 3 - .../AddressBook/DeleteEntryModal/index.tsx | 2 +- .../AddressBook/DeleteEntryModal/style.ts | 3 - .../components/Balances/SendModal/index.tsx | 11 +- src/routes/safe/components/Balances/index.tsx | 4 +- src/routes/safe/components/Balances/style.ts | 6 - src/routes/safe/components/Layout/style.ts | 1 + .../Settings/Advanced/RemoveModuleModal.tsx | 2 +- .../components/Settings/Advanced/style.ts | 5 - .../ManageOwners/AddOwnerModal/index.tsx | 13 +- .../ManageOwners/EditOwnerModal/index.tsx | 2 +- .../ManageOwners/EditOwnerModal/style.ts | 3 - .../ManageOwners/RemoveOwnerModal/index.tsx | 13 +- .../ManageOwners/ReplaceOwnerModal/index.tsx | 13 +- .../Settings/RemoveSafeModal/index.tsx | 2 +- .../Settings/RemoveSafeModal/style.ts | 5 - .../Settings/SpendingLimit/Modal/index.tsx | 102 ------ .../SpendingLimit/NewLimitModal/Create.tsx | 43 +-- .../SpendingLimit/NewLimitModal/Review.tsx | 46 +-- .../SpendingLimit/NewLimitModal/index.tsx | 2 +- .../SpendingLimit/RemoveLimitModal.tsx | 49 ++- .../Settings/SpendingLimit/style.ts | 4 - 26 files changed, 495 insertions(+), 313 deletions(-) create mode 100644 src/components/Modal/index.stories.tsx delete mode 100644 src/routes/safe/components/Settings/SpendingLimit/Modal/index.tsx diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index ae64a97d..158725fe 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -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" > diff --git a/src/components/Modal/index.stories.tsx b/src/components/Modal/index.stories.tsx new file mode 100644 index 00000000..df2fb808 --- /dev/null +++ b/src/components/Modal/index.stories.tsx @@ -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: , + 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: {}, + description: 'safe-react-component exposed with a few styles added to personalize the modal header', + }, + ], + }, + { + title: 'Modal.Header', + component: {}, + 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: {}, + parameters: { + children: 'whatever is required to be rendered in the footer. Usually buttons.', + }, + compositionElements: [ + { + title: 'Modal.Footer.Buttons', + component: , + description: 'standard two buttons wrapped implementation. One "Cancel" and one "Submit" button.', + }, + ], + }, + ], +} + +const SimpleFormModal = ({ title, description, handleClose, handleSubmit, isOpen, children }) => ( + + {/* header */} + + {title} + + + + {() => ( + <> + {/* body */} + {children} + + {/* footer */} + + + + + )} + + +) + +const Username = () => ( + +) + +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 ( +
+ + {/* Modal with Form */} + + {/* Form Fields */} + + +
+ ) +} + +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 ( +
+ + {/* Modal */} + + {/* Header */} + + {title} + + + {/* Body */} + + You are about to remove something + + + {/* Footer */} + + + + +
+ ) +} diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx index d9a2ea46..5a6dcc33 100644 --- a/src/components/Modal/index.tsx +++ b/src/components/Modal/index.tsx @@ -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, reason: 'backdropClick' | 'escapeKeyDown'): void - }['bivarianceHack'] - modalClassName?: string + handleClose?: (event: Record, 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 ( - -
{children}
-
+
{children}
+ ) } 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 => ( + + {children} + +) + +interface HeaderProps { + children?: ReactNode + onClose?: (event: any) => void +} + +const Header = ({ children, onClose }: HeaderProps): ReactElement => { + return ( + + {children} + + {onClose && ( + + )} + + ) +} + +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 => ( + + {children} + +) + +/*** 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 & { + 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 ( + <> + + {cancelText} + + + {confirmText} + + + ) +} + +interface FooterProps { + children: ReactNode | ReactNodeArray +} + +const Footer = ({ children }: FooterProps): ReactElement => ( + {children} +) + +Footer.Buttons = Buttons + +interface ModalProps { + children: ReactNode + description: string + handleClose: () => void + open: boolean + title: string +} + +export const Modal = ({ children, ...props }: ModalProps): ReactElement => { + return ( + + {children} + + ) +} + +Modal.Header = Header +Modal.Body = Body +Modal.Footer = Footer diff --git a/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.tsx b/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.tsx index bb7aa3e8..cc37955f 100644 --- a/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.tsx +++ b/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.tsx @@ -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'} > diff --git a/src/routes/safe/components/AddressBook/CreateEditEntryModal/style.ts b/src/routes/safe/components/AddressBook/CreateEditEntryModal/style.ts index 839697a1..8aca604c 100644 --- a/src/routes/safe/components/AddressBook/CreateEditEntryModal/style.ts +++ b/src/routes/safe/components/AddressBook/CreateEditEntryModal/style.ts @@ -23,8 +23,5 @@ export const useStyles = makeStyles( height: '84px', justifyContent: 'center', }, - smallerModalWindow: { - height: 'auto', - }, }), ) diff --git a/src/routes/safe/components/AddressBook/DeleteEntryModal/index.tsx b/src/routes/safe/components/AddressBook/DeleteEntryModal/index.tsx index 9a1432a7..91390839 100644 --- a/src/routes/safe/components/AddressBook/DeleteEntryModal/index.tsx +++ b/src/routes/safe/components/AddressBook/DeleteEntryModal/index.tsx @@ -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" > diff --git a/src/routes/safe/components/AddressBook/DeleteEntryModal/style.ts b/src/routes/safe/components/AddressBook/DeleteEntryModal/style.ts index 3ea5fab9..4952235f 100644 --- a/src/routes/safe/components/AddressBook/DeleteEntryModal/style.ts +++ b/src/routes/safe/components/AddressBook/DeleteEntryModal/style.ts @@ -27,7 +27,4 @@ export const styles = () => ({ buttonCancel: { color: '#008c73', }, - smallerModalWindow: { - height: 'auto', - }, }) diff --git a/src/routes/safe/components/Balances/SendModal/index.tsx b/src/routes/safe/components/Balances/SendModal/index.tsx index 7623d9fa..c90f1690 100644 --- a/src/routes/safe/components/Balances/SendModal/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/index.tsx @@ -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" > { })) } - 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" > onHide('Receive')} /> diff --git a/src/routes/safe/components/Balances/style.ts b/src/routes/safe/components/Balances/style.ts index bcdaf782..2f408901 100644 --- a/src/routes/safe/components/Balances/style.ts +++ b/src/routes/safe/components/Balances/style.ts @@ -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', diff --git a/src/routes/safe/components/Layout/style.ts b/src/routes/safe/components/Layout/style.ts index ed951783..71f6b493 100644 --- a/src/routes/safe/components/Layout/style.ts +++ b/src/routes/safe/components/Layout/style.ts @@ -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' diff --git a/src/routes/safe/components/Settings/Advanced/RemoveModuleModal.tsx b/src/routes/safe/components/Settings/Advanced/RemoveModuleModal.tsx index 042f47d3..ca38680a 100644 --- a/src/routes/safe/components/Settings/Advanced/RemoveModuleModal.tsx +++ b/src/routes/safe/components/Settings/Advanced/RemoveModuleModal.tsx @@ -122,7 +122,7 @@ export const RemoveModuleModal = ({ onClose, selectedModulePair }: RemoveModuleM diff --git a/src/routes/safe/components/Settings/Advanced/style.ts b/src/routes/safe/components/Settings/Advanced/style.ts index 69925f4f..7cfc93d1 100644 --- a/src/routes/safe/components/Settings/Advanced/style.ts +++ b/src/routes/safe/components/Settings/Advanced/style.ts @@ -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}`, diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx index 864e1918..e376e73b 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx @@ -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 @@ -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({ 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" > <> diff --git a/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.tsx index 914da3c2..d213c512 100644 --- a/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.tsx @@ -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" > diff --git a/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/style.ts b/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/style.ts index 968f88d2..73340afa 100644 --- a/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/style.ts +++ b/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/style.ts @@ -32,7 +32,4 @@ export const styles = createStyles({ cursor: 'pointer', }, }, - smallerModalWindow: { - height: 'auto', - }, }) diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx index 0875cac1..add0309d 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx @@ -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 @@ -78,7 +68,6 @@ export const RemoveOwnerModal = ({ ownerAddress, ownerName, }: RemoveOwnerProps): React.ReactElement => { - const classes = useStyles() const [activeScreen, setActiveScreen] = useState('checkOwner') const [values, setValues] = useState({ 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" > <> diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx index 74cd9f81..0ef75508 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx @@ -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 @@ -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" > <> diff --git a/src/routes/safe/components/Settings/RemoveSafeModal/index.tsx b/src/routes/safe/components/Settings/RemoveSafeModal/index.tsx index 4d2f8b72..a846751d 100644 --- a/src/routes/safe/components/Settings/RemoveSafeModal/index.tsx +++ b/src/routes/safe/components/Settings/RemoveSafeModal/index.tsx @@ -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" > diff --git a/src/routes/safe/components/Settings/RemoveSafeModal/style.ts b/src/routes/safe/components/Settings/RemoveSafeModal/style.ts index 2d41fb79..b034d782 100644 --- a/src/routes/safe/components/Settings/RemoveSafeModal/style.ts +++ b/src/routes/safe/components/Settings/RemoveSafeModal/style.ts @@ -52,9 +52,4 @@ export const styles = createStyles({ cursor: 'pointer', }, }, - modal: { - height: 'auto', - maxWidth: 'calc(100% - 30px)', - overflow: 'hidden', - }, }) diff --git a/src/routes/safe/components/Settings/SpendingLimit/Modal/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/Modal/index.tsx deleted file mode 100644 index b546f02f..00000000 --- a/src/routes/safe/components/Settings/SpendingLimit/Modal/index.tsx +++ /dev/null @@ -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 => ( - - - {title} - {titleNote && ( - <> - {' '} - <Text size="lg" color="secondaryLight" as="span"> - {titleNote} - </Text> - </> - )} - - - - - - -) - -interface FooterProps { - children: ReactNodeArray -} - -const Footer = ({ children }: FooterProps): ReactElement => ( - - {children} - -) - -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 ( - - {children} - - ) -} - -Modal.TopBar = TopBar -Modal.Footer = Footer - -export default Modal diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Create.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Create.tsx index 2c048203..ab417c26 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Create.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Create.tsx @@ -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> = { setBeneficiary: (args, state, utils) => { utils.changeValue(state, 'beneficiary', () => args[0]) @@ -55,7 +46,16 @@ const canReview = ({ const Create = ({ initialValues, onCancel, onReview }: NewSpendingLimitProps): ReactElement => { return ( <> - + + + <> + New Spending Limit + + 1 of 2 + + + + {(...args) => { @@ -69,21 +69,10 @@ const Create = ({ initialValues, onCancel, onReview }: NewSpendingLimitProps): R - - - {/* TODO: replace this with safe-react-components button. */} - {/* This is used as "submit" SRC Button does not triggers submission up until the 2nd click */} - - Review - + ) diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx index 9d803ded..ff4feb57 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx @@ -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' @@ -242,9 +241,16 @@ export const ReviewSpendingLimits = ({ onBack, onClose, txToken, values }: Revie > {(txParameters, toggleEditMode) => ( <> - + + + New Spending Limit + + 2 of 2 + + + - + @@ -286,7 +292,7 @@ export const ReviewSpendingLimits = ({ onBack, onClose, txToken, values }: Revie isTransactionExecution={isExecution} isOffChainSignature={isOffChainSignature} /> - +
- - - + onBack({ values: {}, txToken: makeToken(), step: CREATE }), + text: 'Back', + }} + confirmButtonProps={{ + onClick: () => handleSubmit(txParameters), + disabled: + existentSpendingLimit === undefined || txEstimationExecutionStatus === EstimationStatus.LOADING, + }} + /> )} diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/index.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/index.tsx index 7cb5b9a5..84666696 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/index.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/index.tsx @@ -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' diff --git a/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx b/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx index 6ad0ec6e..7e9002c6 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx @@ -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 @@ -126,9 +124,13 @@ export const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendin {(txParameters, toggleEditMode) => { return ( <> - + + + Remove Spending Limit + + - + @@ -152,7 +154,7 @@ export const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendin isTransactionExecution={isExecution} isOffChainSignature={isOffChainSignature} /> - + - - + removeSelectedSpendingLimit(txParameters), + disabled: txEstimationExecutionStatus === EstimationStatus.LOADING, + text: 'Remove', + }} + /> ) diff --git a/src/routes/safe/components/Settings/SpendingLimit/style.ts b/src/routes/safe/components/Settings/SpendingLimit/style.ts index 0b8a9568..8df8e247 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/style.ts +++ b/src/routes/safe/components/Settings/SpendingLimit/style.ts @@ -120,10 +120,6 @@ export const useStyles = makeStyles( cursor: 'pointer', }, }, - modal: { - height: 'auto', - maxWidth: 'calc(100% - 30px)', - }, amountInput: { width: '100% !important', }, From d07816fcacc52fc3b157ccfd97a08e119f34dbb2 Mon Sep 17 00:00:00 2001 From: katspaugh Date: Tue, 6 Apr 2021 15:55:55 +0200 Subject: [PATCH 05/14] Update the readme (#2125) --- .env.example | 1 - readme.md | 22 ++++++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index 266bbc27..ba240349 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/readme.md b/readme.md index dbd62775..a7bf34d3 100644 --- a/readme.md +++ b/readme.md @@ -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 From 8b047e8c5721a30c3feddb72f4a85d0cf76fa0e5 Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Wed, 7 Apr 2021 11:50:02 +0200 Subject: [PATCH 06/14] [Feature] Use backend gas estimation (#2112) * Use safeTxGas backend estimation * Check transaction execution result and show warning if fails * Fix estimation for safes with just one required signature * Hide advanced parameters for sending funds with spending limit * Update variable names to be more clear --- src/logic/contracts/safeContracts.ts | 28 +- src/logic/hooks/useEstimateTransactionGas.tsx | 188 +++++------- .../safe/api/fetchSafeTxGasEstimation.ts | 25 ++ .../safe/store/actions/createTransaction.ts | 6 +- .../safe/store/actions/processTransaction.ts | 2 +- src/logic/safe/transactions/gas.ts | 279 +++++++++--------- src/logic/safe/utils/upgradeSafe.ts | 2 +- src/logic/wallets/ethTransactions.ts | 8 +- .../load/components/OwnerList/index.tsx | 2 +- .../ConfirmTxModal/ReviewConfirm.tsx | 6 + .../ContractInteraction/Review/index.tsx | 6 + .../screens/ReviewCollectible/index.tsx | 6 + .../screens/ReviewSendFundsTx/index.tsx | 26 +- .../Settings/Advanced/RemoveModuleModal.tsx | 6 + .../ManageOwners/AddOwnerModal/index.tsx | 2 +- .../AddOwnerModal/screens/Review/index.tsx | 10 +- .../ManageOwners/RemoveOwnerModal/index.tsx | 2 +- .../RemoveOwnerModal/screens/Review/index.tsx | 8 +- .../ManageOwners/ReplaceOwnerModal/index.tsx | 2 +- .../screens/Review/index.tsx | 8 +- .../SpendingLimit/NewLimitModal/Review.tsx | 6 + .../SpendingLimit/RemoveLimitModal.tsx | 6 + .../ChangeThreshold/index.tsx | 10 +- .../TxList/modals/ApproveTxModal.tsx | 8 +- .../hooks/useTransactionParameters.ts | 2 +- src/test/safe.dom.create.tsx | 2 +- 26 files changed, 356 insertions(+), 300 deletions(-) create mode 100644 src/logic/safe/api/fetchSafeTxGasEstimation.ts diff --git a/src/logic/contracts/safeContracts.ts b/src/logic/contracts/safeContracts.ts index 17345da0..5ba70c7c 100644 --- a/src/logic/contracts/safeContracts.ts +++ b/src/logic/contracts/safeContracts.ts @@ -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 => { 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,23 @@ 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 +} \ No newline at end of file diff --git a/src/logic/hooks/useEstimateTransactionGas.tsx b/src/logic/hooks/useEstimateTransactionGas.tsx index 567d7659..6e9799ae 100644 --- a/src/logic/hooks/useEstimateTransactionGas.tsx +++ b/src/logic/hooks/useEstimateTransactionGas.tsx @@ -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 - 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 => { - 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({ 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 diff --git a/src/logic/safe/api/fetchSafeTxGasEstimation.ts b/src/logic/safe/api/fetchSafeTxGasEstimation.ts new file mode 100644 index 00000000..b0a4759e --- /dev/null +++ b/src/logic/safe/api/fetchSafeTxGasEstimation.ts @@ -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 => { + const url = `${getSafeServiceBaseUrl(checksumAddress(safeAddress))}/multisig-transactions/estimations/` + + return axios.post(url, body).then(({ data }) => data.safeTxGas) +} diff --git a/src/logic/safe/store/actions/createTransaction.ts b/src/logic/safe/store/actions/createTransaction.ts index b2e6b347..1b11c715 100644 --- a/src/logic/safe/store/actions/createTransaction.ts +++ b/src/logic/safe/store/actions/createTransaction.ts @@ -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 diff --git a/src/logic/safe/store/actions/processTransaction.ts b/src/logic/safe/store/actions/processTransaction.ts index 28934b23..709805ef 100644 --- a/src/logic/safe/store/actions/processTransaction.ts +++ b/src/logic/safe/store/actions/processTransaction.ts @@ -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) diff --git a/src/logic/safe/transactions/gas.ts b/src/logic/safe/transactions/gas.ts index 9c79dd1d..94773b2b 100644 --- a/src/logic/safe/transactions/gas.ts +++ b/src/logic/safe/transactions/gas.ts @@ -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 => { - 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 => { +export const estimateSafeTxGas = async ({ + safeAddress, + txData, + txRecipient, + txAmount, + operation, + safeTxGas, +}: SafeTxGasEstimationProps): Promise => { 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 + 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 => { + 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 => { - 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 => { + 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, diff --git a/src/logic/safe/utils/upgradeSafe.ts b/src/logic/safe/utils/upgradeSafe.ts index 53caa747..3bbe6336 100644 --- a/src/logic/safe/utils/upgradeSafe.ts +++ b/src/logic/safe/utils/upgradeSafe.ts @@ -49,7 +49,7 @@ export const getEncodedMultiSendCallData = (txs: MultiSendTx[], web3: Web3): str } export const getUpgradeSafeTransactionHash = async (safeAddress: string): Promise => { - 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 = [ diff --git a/src/logic/wallets/ethTransactions.ts b/src/logic/wallets/ethTransactions.ts index bfedd1a0..bf1fd7b3 100644 --- a/src/logic/wallets/ethTransactions.ts +++ b/src/logic/wallets/ethTransactions.ts @@ -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 => { 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 => { - 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) } diff --git a/src/routes/load/components/OwnerList/index.tsx b/src/routes/load/components/OwnerList/index.tsx index d6f6fc74..7888d8d5 100644 --- a/src/routes/load/components/OwnerList/index.tsx +++ b/src/routes/load/components/OwnerList/index.tsx @@ -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() diff --git a/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx b/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx index 04e70266..016d165d 100644 --- a/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx +++ b/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx @@ -112,6 +112,7 @@ export const ReviewConfirm = ({ const operation = useMemo(() => (isMultiSend ? DELEGATE_CALL : CALL), [isMultiSend]) const [manualSafeTxGas, setManualSafeTxGas] = useState(0) const [manualGasPrice, setManualGasPrice] = useState() + const [manualGasLimit, setManualGasLimit] = useState() const { gasLimit, @@ -129,6 +130,7 @@ export const ReviewConfirm = ({ txAmount: txValue, safeTxGas: manualSafeTxGas, manualGasPrice, + manualGasLimit, }) useEffect(() => { @@ -191,6 +193,10 @@ export const ReviewConfirm = ({ setManualGasPrice(txParameters.ethGasPrice) } + if (txParameters.ethGasLimit && gasLimit !== txParameters.ethGasLimit) { + setManualGasLimit(txParameters.ethGasLimit) + } + if (newSafeTxGas && oldSafeTxGas !== newSafeTxGas) { setManualSafeTxGas(newSafeTxGas) } diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx index f0fb4546..f02e60f2 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx @@ -58,6 +58,7 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE const safeAddress = useSelector(safeParamAddressFromStateSelector) const [manualSafeTxGas, setManualSafeTxGas] = useState(0) const [manualGasPrice, setManualGasPrice] = useState() + const [manualGasLimit, setManualGasLimit] = useState() const [txInfo, setTxInfo] = useState<{ txRecipient: string @@ -80,6 +81,7 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE txData: txInfo?.txData, safeTxGas: manualSafeTxGas, manualGasPrice, + manualGasLimit, }) useEffect(() => { @@ -120,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) } diff --git a/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx index ea68ea2a..16e3abc5 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx @@ -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() + const [manualGasLimit, setManualGasLimit] = useState() 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) } diff --git a/src/routes/safe/components/Balances/SendModal/screens/ReviewSendFundsTx/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ReviewSendFundsTx/index.tsx index 1e2ab83c..aee98644 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ReviewSendFundsTx/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ReviewSendFundsTx/index.tsx @@ -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() + const [manualGasLimit, setManualGasLimit] = useState() 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 {/* Tx Parameters */} - + {/* FIXME TxParameters should be updated to be used with spending limits */} + {!sameString(tx.txType, 'spendingLimit') && ( + + )} {/* Disclaimer */} - {txEstimationExecutionStatus !== EstimationStatus.LOADING && ( + {/* FIXME Estimation should be fixed to be used with spending limits */} + {!sameString(tx.txType, 'spendingLimit') && txEstimationExecutionStatus !== EstimationStatus.LOADING && (
() + const [manualGasLimit, setManualGasLimit] = useState() 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) } diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx index e376e73b..26d3e878 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx @@ -29,7 +29,7 @@ export const sendAddOwner = async ( txParameters: TxParameters, dispatch: Dispatch, ): Promise => { - const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) + const gnosisSafe = getGnosisSafeInstanceAt(safeAddress) const txData = gnosisSafe.methods.addOwnerWithThreshold(values.ownerAddress, values.threshold).encodeABI() const txHash = await dispatch( diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx index 1acbd6ef..c606396f 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx @@ -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() + const [manualGasLimit, setManualGasLimit] = useState() 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) } diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx index add0309d..4f89c537 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx @@ -29,7 +29,7 @@ export const sendRemoveOwner = async ( txParameters: TxParameters, threshold?: number, ): Promise => { - 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(), diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx index 7d77084c..268f5729 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx @@ -59,6 +59,7 @@ export const ReviewRemoveOwnerModal = ({ const ownersWithAddressBookName = owners ? getOwnersWithNameFromAddressBook(addressBook, owners) : List([]) const [manualSafeTxGas, setManualSafeTxGas] = useState(0) const [manualGasPrice, setManualGasPrice] = useState() + const [manualGasLimit, setManualGasLimit] = useState() 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) } diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx index 0ef75508..0f4cc8ac 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx @@ -30,7 +30,7 @@ export const sendReplaceOwner = async ( txParameters: TxParameters, threshold?: number, ): Promise => { - 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] diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.tsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.tsx index 52327599..2c45a342 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.tsx @@ -67,6 +67,7 @@ export const ReviewReplaceOwnerModal = ({ const ownersWithAddressBookName = owners ? getOwnersWithNameFromAddressBook(addressBook, owners) : List([]) const [manualSafeTxGas, setManualSafeTxGas] = useState(0) const [manualGasPrice, setManualGasPrice] = useState() + const [manualGasLimit, setManualGasLimit] = useState() 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) } diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx index ff4feb57..8330c121 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx @@ -154,6 +154,7 @@ export const ReviewSpendingLimits = ({ onBack, onClose, txToken, values }: Revie }) const [manualSafeTxGas, setManualSafeTxGas] = useState(0) const [manualGasPrice, setManualGasPrice] = useState() + const [manualGasLimit, setManualGasLimit] = useState() const { gasCostFormatted, @@ -171,6 +172,7 @@ export const ReviewSpendingLimits = ({ onBack, onClose, txToken, values }: Revie operation: estimateGasArgs.operation, safeTxGas: manualSafeTxGas, manualGasPrice, + manualGasLimit, }) useEffect(() => { @@ -225,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) } diff --git a/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx b/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx index 7e9002c6..89b9ef5a 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx @@ -39,6 +39,7 @@ export const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendin const dispatch = useDispatch() const [manualSafeTxGas, setManualSafeTxGas] = useState(0) const [manualGasPrice, setManualGasPrice] = useState() + const [manualGasLimit, setManualGasLimit] = useState() useEffect(() => { const { @@ -64,6 +65,7 @@ export const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendin txAmount: '0', safeTxGas: manualSafeTxGas, manualGasPrice, + manualGasLimit, }) const removeSelectedSpendingLimit = async (txParameters: TxParameters): Promise => { @@ -101,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) } diff --git a/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx b/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx index 0560b57a..ff2c2785 100644 --- a/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx +++ b/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx @@ -50,6 +50,7 @@ export const ChangeThresholdModal = ({ const [data, setData] = useState('') const [manualSafeTxGas, setManualSafeTxGas] = useState(0) const [manualGasPrice, setManualGasPrice] = useState() + const [manualGasLimit, setManualGasLimit] = useState() const [editedThreshold, setEditedThreshold] = useState(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) } diff --git a/src/routes/safe/components/Transactions/TxList/modals/ApproveTxModal.tsx b/src/routes/safe/components/Transactions/TxList/modals/ApproveTxModal.tsx index c8b24649..9816a7cd 100644 --- a/src/routes/safe/components/Transactions/TxList/modals/ApproveTxModal.tsx +++ b/src/routes/safe/components/Transactions/TxList/modals/ApproveTxModal.tsx @@ -229,6 +229,7 @@ export const ApproveTxModal = ({ const oneConfirmationLeft = !thresholdReached && _countingCurrentConfirmation === _threshold const isTheTxReadyToBeExecuted = oneConfirmationLeft ? true : thresholdReached const [manualGasPrice, setManualGasPrice] = useState() + const [manualGasLimit, setManualGasLimit] = useState() 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 ( diff --git a/src/routes/safe/container/hooks/useTransactionParameters.ts b/src/routes/safe/container/hooks/useTransactionParameters.ts index 1185c765..fb7215d9 100644 --- a/src/routes/safe/container/hooks/useTransactionParameters.ts +++ b/src/routes/safe/container/hooks/useTransactionParameters.ts @@ -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) diff --git a/src/test/safe.dom.create.tsx b/src/test/safe.dom.create.tsx index d686009f..7c9d8475 100644 --- a/src/test/safe.dom.create.tsx +++ b/src/test/safe.dom.create.tsx @@ -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() From e48891ba2bb32a2ef7f83a550d5aceff93d43059 Mon Sep 17 00:00:00 2001 From: Mikhail Mikheev Date: Wed, 7 Apr 2021 14:21:30 +0400 Subject: [PATCH 07/14] Add Aave 2 safe app (#2127) --- src/routes/safe/components/Apps/utils.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts index fa66d807..46cbe08c 100644 --- a/src/routes/safe/components/Apps/utils.ts +++ b/src/routes/safe/components/Apps/utils.ts @@ -34,6 +34,12 @@ export const staticAppsList: Array = [ disabled: false, networks: [ETHEREUM_NETWORK.MAINNET], }, + // Aave v2 + { + url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmVg7aXr5S8sT2iUdUwdkfTJNknmB7rcE3t92HiGoVsYDj`, + disabled: false, + networks: [ETHEREUM_NETWORK.MAINNET], + }, //Balancer Exchange { url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmRb2VfPVYBrv6gi2zDywgVgTg3A19ZCRMqwL13Ez5f5AS`, From c96e3192ff4b36715298e7b6a2d66e3fe1f3d6c1 Mon Sep 17 00:00:00 2001 From: Fernando Date: Wed, 7 Apr 2021 07:40:33 -0300 Subject: [PATCH 08/14] (Feature) Proper rejections (#2096) * remove `isCancelTransaction` utility function in favor of `txInfo.isCancellation` flag provided by client-gateway * replace "cancel" concept in favor of "reject" * add circle-cross-red icon to "On-chain rejection" transaction info Adjust owner's list text color * identify queued on-chain rejection * apply styles to on-chain rejection type identifier * update awaiting messages wording * fix styles on styles to on-chain rejection * replace local svg with SRC `Icon` component wherever is possible --- package.json | 2 +- src/components/CustomIconText/index.tsx | 6 +- src/components/TransactionFailText/index.tsx | 2 +- .../safe/store/models/types/gateway.d.ts | 2 +- .../Transactions/TxList/TxCollapsed.tsx | 7 ++- .../TxList/TxCollapsedActions.tsx | 2 +- .../Transactions/TxList/TxDetails.tsx | 58 +++++++++++-------- .../Transactions/TxList/TxExpandedActions.tsx | 2 +- .../Transactions/TxList/TxOwners.tsx | 56 +++++++++++------- .../Transactions/TxList/TxSummary.tsx | 2 +- .../TxList/assets/circle-cross-red.svg | 11 ++++ .../TxList/hooks/useTransactionActions.ts | 10 +--- .../TxList/hooks/useTransactionStatus.ts | 4 +- .../TxList/hooks/useTransactionType.ts | 18 +----- .../components/Transactions/TxList/styled.tsx | 30 ++++++++++ .../components/Transactions/TxList/utils.ts | 24 +------- yarn.lock | 4 +- 17 files changed, 134 insertions(+), 106 deletions(-) create mode 100644 src/routes/safe/components/Transactions/TxList/assets/circle-cross-red.svg diff --git a/package.json b/package.json index c8f82367..82ed3cfe 100644 --- a/package.json +++ b/package.json @@ -161,7 +161,7 @@ "@gnosis.pm/safe-apps-sdk": "1.0.3", "@gnosis.pm/safe-apps-sdk-v1": "npm:@gnosis.pm/safe-apps-sdk@0.4.2", "@gnosis.pm/safe-contracts": "1.1.1-dev.2", - "@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#a68a67e", + "@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#7ebc414", "@gnosis.pm/util-contracts": "2.0.6", "@ledgerhq/hw-transport-node-hid-singleton": "5.45.0", "@material-ui/core": "^4.11.0", diff --git a/src/components/CustomIconText/index.tsx b/src/components/CustomIconText/index.tsx index 31fc13ba..cc059de1 100644 --- a/src/components/CustomIconText/index.tsx +++ b/src/components/CustomIconText/index.tsx @@ -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,9 @@ const Icon = styled.img` margin-right: 9px; ` -const CustomIconText = ({ iconUrl, text }: { iconUrl: string; text?: string }) => ( +export const CustomIconText = ({ iconUrl, text }: { iconUrl: string; text?: string }): ReactElement => ( {text && {text}} ) - -export default CustomIconText diff --git a/src/components/TransactionFailText/index.tsx b/src/components/TransactionFailText/index.tsx index d61664cc..25c6b56b 100644 --- a/src/components/TransactionFailText/index.tsx +++ b/src/components/TransactionFailText/index.tsx @@ -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.` } diff --git a/src/logic/safe/store/models/types/gateway.d.ts b/src/logic/safe/store/models/types/gateway.d.ts index c462971a..cf45553d 100644 --- a/src/logic/safe/store/models/types/gateway.d.ts +++ b/src/logic/safe/store/models/types/gateway.d.ts @@ -236,7 +236,7 @@ type MultiSigExecutionDetails = { type DetailedExecutionInfo = ModuleExecutionDetails | MultiSigExecutionDetails type ExpandedTxDetails = { - executedAt: number + executedAt: number | null txStatus: TransactionStatus txInfo: TransactionInfo txData: TransactionData | null diff --git a/src/routes/safe/components/Transactions/TxList/TxCollapsed.tsx b/src/routes/safe/components/Transactions/TxList/TxCollapsed.tsx index 4223be90..4e12f101 100644 --- a/src/routes/safe/components/Transactions/TxList/TxCollapsed.tsx +++ b/src/routes/safe/components/Transactions/TxList/TxCollapsed.tsx @@ -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 = (
@@ -124,7 +127,7 @@ export const TxCollapsed = ({ ) const txCollapsedType = ( -
+
) diff --git a/src/routes/safe/components/Transactions/TxList/TxCollapsedActions.tsx b/src/routes/safe/components/Transactions/TxList/TxCollapsedActions.tsx index 1e753ecd..4751cc37 100644 --- a/src/routes/safe/components/Transactions/TxList/TxCollapsedActions.tsx +++ b/src/routes/safe/components/Transactions/TxList/TxCollapsedActions.tsx @@ -46,7 +46,7 @@ export const TxCollapsedActions = ({ transaction }: TxCollapsedActionsProps): Re {canCancel && ( - + diff --git a/src/routes/safe/components/Transactions/TxList/TxDetails.tsx b/src/routes/safe/components/Transactions/TxList/TxDetails.tsx index 789afd82..675f59c1 100644 --- a/src/routes/safe/components/Transactions/TxList/TxDetails.tsx +++ b/src/routes/safe/components/Transactions/TxList/TxDetails.tsx @@ -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 } - 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 ( <> - - {`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 - }.`} - - - - - Why do I need to pay for cancelling a transaction? - - - - + {message} + {!isTxExecuted && ( + <> +
+ + + + Why do I need to pay for rejecting a transaction? + + + + + + )} ) } @@ -116,7 +124,7 @@ export const TxDetails = ({ transaction, actions }: TxDetailsProps): ReactElemen 'will-be-replaced': transaction.txStatus === 'WILL_BE_REPLACED', })} > - +
{!data.executedAt && txLocation !== 'history' && actions?.isUserAnOwner && (
diff --git a/src/routes/safe/components/Transactions/TxList/TxExpandedActions.tsx b/src/routes/safe/components/Transactions/TxList/TxExpandedActions.tsx index 71e0c28c..81f7d804 100644 --- a/src/routes/safe/components/Transactions/TxList/TxExpandedActions.tsx +++ b/src/routes/safe/components/Transactions/TxList/TxExpandedActions.tsx @@ -41,7 +41,7 @@ export const TxExpandedActions = ({ transaction }: TxExpandedActionsProps): Reac {canCancel && ( )} diff --git a/src/routes/safe/components/Transactions/TxList/TxOwners.tsx b/src/routes/safe/components/Transactions/TxList/TxOwners.tsx index 6b97e081..22b45998 100644 --- a/src/routes/safe/components/Transactions/TxList/TxOwners.tsx +++ b/src/routes/safe/components/Transactions/TxList/TxOwners.tsx @@ -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) ? ( + + + + +
+ + On-chain rejection created + +
+
+ ) : ( + + + + +
+ + Created + +
+
+ ) + return ( - - - - -
- - Created - -
-
+ {CreationNode} {detailedExecutionInfo.confirmations.map(({ signer }) => ( - +
@@ -55,7 +67,11 @@ export const TxOwners = ({ detailedExecutionInfo }: TxOwnersProps): ReactElement {confirmationsNeeded <= 0 ? ( - + {detailedExecutionInfo.executor ? ( + + ) : ( + + )}
diff --git a/src/routes/safe/components/Transactions/TxList/TxSummary.tsx b/src/routes/safe/components/Transactions/TxList/TxSummary.tsx index 84fb6cc7..4bc50b77 100644 --- a/src/routes/safe/components/Transactions/TxList/TxSummary.tsx +++ b/src/routes/safe/components/Transactions/TxList/TxSummary.tsx @@ -27,7 +27,7 @@ export const TxSummary = ({ txDetails }: { txDetails: ExpandedTxDetails }): Reac )}
- {nonce && ( + {nonce !== undefined && (
Nonce:{' '} diff --git a/src/routes/safe/components/Transactions/TxList/assets/circle-cross-red.svg b/src/routes/safe/components/Transactions/TxList/assets/circle-cross-red.svg new file mode 100644 index 00000000..ff8f6306 --- /dev/null +++ b/src/routes/safe/components/Transactions/TxList/assets/circle-cross-red.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/routes/safe/components/Transactions/TxList/hooks/useTransactionActions.ts b/src/routes/safe/components/Transactions/TxList/hooks/useTransactionActions.ts index 5e3b7566..3609d6bc 100644 --- a/src/routes/safe/components/Transactions/TxList/hooks/useTransactionActions.ts +++ b/src/routes/safe/components/Transactions/TxList/hooks/useTransactionActions.ts @@ -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, }) diff --git a/src/routes/safe/components/Transactions/TxList/hooks/useTransactionStatus.ts b/src/routes/safe/components/Transactions/TxList/hooks/useTransactionStatus.ts index e64637b7..4df71af3 100644 --- a/src/routes/safe/components/Transactions/TxList/hooks/useTransactionStatus.ts +++ b/src/routes/safe/components/Transactions/TxList/hooks/useTransactionStatus.ts @@ -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': diff --git a/src/routes/safe/components/Transactions/TxList/hooks/useTransactionType.ts b/src/routes/safe/components/Transactions/TxList/hooks/useTransactionType.ts index ca04efbc..e33b81b7 100644 --- a/src/routes/safe/components/Transactions/TxList/hooks/useTransactionType.ts +++ b/src/routes/safe/components/Transactions/TxList/hooks/useTransactionType.ts @@ -4,10 +4,10 @@ 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 @@ -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 } diff --git a/src/routes/safe/components/Transactions/TxList/styled.tsx b/src/routes/safe/components/Transactions/TxList/styled.tsx index de8d7f19..9058bc80 100644 --- a/src/routes/safe/components/Transactions/TxList/styled.tsx +++ b/src/routes/safe/components/Transactions/TxList/styled.tsx @@ -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; } diff --git a/src/routes/safe/components/Transactions/TxList/utils.ts b/src/routes/safe/components/Transactions/TxList/utils.ts index d461e8cc..13b15057 100644 --- a/src/routes/safe/components/Transactions/TxList/utils.ts +++ b/src/routes/safe/components/Transactions/TxList/utils.ts @@ -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)) diff --git a/yarn.lock b/yarn.lock index 56873e07..b65e2fb0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1596,9 +1596,9 @@ solc "0.5.14" truffle "^5.1.21" -"@gnosis.pm/safe-react-components@https://github.com/gnosis/safe-react-components.git#a68a67e": +"@gnosis.pm/safe-react-components@https://github.com/gnosis/safe-react-components.git#7ebc414": version "0.5.0" - resolved "https://github.com/gnosis/safe-react-components.git#a68a67e634d0be091856ebba9f6874eebb767cd7" + resolved "https://github.com/gnosis/safe-react-components.git#7ebc414ae975d60846704c5a8db5e61c30348d12" dependencies: classnames "^2.2.6" react-media "^1.10.0" From 9f3ff69a75401cc87c883bab84b8894ced758b76 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 7 Apr 2021 08:10:48 -0300 Subject: [PATCH 09/14] Known Addresses V1 (#2113) * Tx Custom: Add toInfo (icon+name) in table row. * Custom tx: add to info if available * update EthHashInfo API usage Co-authored-by: Fernando --- package.json | 2 +- .../ProviderInfo/ProviderAccessible.tsx | 4 +- src/components/CustomIconText/index.tsx | 6 ++- src/components/DecodeTxs/index.tsx | 4 +- .../SafeList/AddressWrapper.tsx | 2 +- src/logic/contracts/safeContracts.ts | 15 +++--- .../ConfirmTxModal/ReviewConfirm.tsx | 2 +- .../Balances/SendModal/SafeInfo/index.tsx | 2 +- .../screens/AddressBookInput/index.tsx | 2 +- .../ContractInteraction/Review/index.tsx | 2 +- .../SendModal/screens/SendFunds/index.tsx | 2 +- .../SpendingLimit/FormFields/Beneficiary.tsx | 2 +- .../SpendingLimit/InfoDisplay/AddressInfo.tsx | 2 +- .../Transactions/TxList/AddressInfo.tsx | 13 +++-- .../Transactions/TxList/OwnerRow.tsx | 2 +- .../components/Transactions/TxList/TxData.tsx | 47 ++++++++++++++----- .../Transactions/TxList/TxDetails.tsx | 2 +- .../components/Transactions/TxList/TxInfo.tsx | 12 +---- .../Transactions/TxList/TxInfoDetails.tsx | 14 +++++- .../TxList/hooks/useTransactionType.ts | 8 +++- yarn.lock | 4 +- 21 files changed, 93 insertions(+), 56 deletions(-) diff --git a/package.json b/package.json index 82ed3cfe..4eaefeb1 100644 --- a/package.json +++ b/package.json @@ -161,7 +161,7 @@ "@gnosis.pm/safe-apps-sdk": "1.0.3", "@gnosis.pm/safe-apps-sdk-v1": "npm:@gnosis.pm/safe-apps-sdk@0.4.2", "@gnosis.pm/safe-contracts": "1.1.1-dev.2", - "@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#7ebc414", + "@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", diff --git a/src/components/AppLayout/Header/components/ProviderInfo/ProviderAccessible.tsx b/src/components/AppLayout/Header/components/ProviderInfo/ProviderAccessible.tsx index 563c3abb..e4720113 100644 --- a/src/components/AppLayout/Header/components/ProviderInfo/ProviderAccessible.tsx +++ b/src/components/AppLayout/Header/components/ProviderInfo/ProviderAccessible.tsx @@ -89,8 +89,8 @@ const ProviderInfo = ({ connected, provider, userAddress }: ProviderInfoProps): diff --git a/src/components/CustomIconText/index.tsx b/src/components/CustomIconText/index.tsx index cc059de1..5cbb94ab 100644 --- a/src/components/CustomIconText/index.tsx +++ b/src/components/CustomIconText/index.tsx @@ -12,9 +12,11 @@ const Icon = styled.img` margin-right: 9px; ` -export const CustomIconText = ({ iconUrl, text }: { iconUrl: string; text?: string }): ReactElement => ( +type Props = { iconUrl: string | null | undefined; text?: string } + +export const CustomIconText = ({ iconUrl, text }: Props): ReactElement => ( - + {iconUrl && } {text && {text}} ) diff --git a/src/components/DecodeTxs/index.tsx b/src/components/DecodeTxs/index.tsx index 8b242e99..703ef001 100644 --- a/src/components/DecodeTxs/index.tsx +++ b/src/components/DecodeTxs/index.tsx @@ -69,7 +69,7 @@ export const BasicTxInfo = ({ { return (
- +
{`${formatAmount(safe.ethBalance)} ${nativeCoin.name}`} diff --git a/src/logic/contracts/safeContracts.ts b/src/logic/contracts/safeContracts.ts index 5ba70c7c..8001d894 100644 --- a/src/logic/contracts/safeContracts.ts +++ b/src/logic/contracts/safeContracts.ts @@ -123,23 +123,22 @@ export const estimateGasForDeployingSafe = async ( data: proxyFactoryData, from: userAccount, to: proxyFactoryMaster.options.address, - }).then(value => value * 2) + }).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 = () => { +export const getSpendingLimitContract = () => { const web3 = getWeb3() - return (new web3.eth.Contract( - SpendingLimitModule.abi as AbiItem[], - SPENDING_LIMIT_MODULE_ADDRESS, - ) as unknown) as AllowanceModule -} \ No newline at end of file + return (new web3.eth.Contract( + SpendingLimitModule.abi as AbiItem[], + SPENDING_LIMIT_MODULE_ADDRESS, + ) as unknown) as AllowanceModule +} diff --git a/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx b/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx index 016d165d..06e4fcc4 100644 --- a/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx +++ b/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx @@ -219,7 +219,7 @@ export const ReviewConfirm = ({ {/* Safe */} - + Balance: {`${ethBalance} ${nativeCoin.symbol}`} diff --git a/src/routes/safe/components/Balances/SendModal/SafeInfo/index.tsx b/src/routes/safe/components/Balances/SendModal/SafeInfo/index.tsx index 36691422..27f5ad53 100644 --- a/src/routes/safe/components/Balances/SendModal/SafeInfo/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/SafeInfo/index.tsx @@ -32,7 +32,7 @@ const SafeInfo = (): React.ReactElement => { hash={safeAddress} name={safeName} explorerUrl={getExplorerInfo(safeAddress)} - showIdenticon + showAvatar showCopyBtn /> {ethBalance && ( diff --git a/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx index 0bc42e7f..1e97da33 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx @@ -148,7 +148,7 @@ const BaseAddressBookInput = ({ /> )} getOptionLabel={({ address }) => address} - renderOption={({ address, name }) => } + renderOption={({ address, name }) => } role="listbox" style={{ display: 'flex', flexGrow: 1 }} /> diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx index f02e60f2..d39700c2 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx @@ -151,7 +151,7 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE - + diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx index 8a8db233..8e5524d3 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx @@ -265,7 +265,7 @@ const SendFunds = ({ diff --git a/src/routes/safe/components/Settings/SpendingLimit/FormFields/Beneficiary.tsx b/src/routes/safe/components/Settings/SpendingLimit/FormFields/Beneficiary.tsx index 0e960d6e..4002bc5c 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/FormFields/Beneficiary.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/FormFields/Beneficiary.tsx @@ -81,7 +81,7 @@ const Beneficiary = (): ReactElement => { hash={selectedEntry.address} name={selectedEntry.name} showCopyBtn - showIdenticon + showAvatar textSize="lg" shortenHash={4} explorerUrl={getExplorerInfo(selectedEntry.address)} diff --git a/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/AddressInfo.tsx b/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/AddressInfo.tsx index 9e98c1fc..c18fdd30 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/AddressInfo.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/AddressInfo.tsx @@ -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} diff --git a/src/routes/safe/components/Transactions/TxList/AddressInfo.tsx b/src/routes/safe/components/Transactions/TxList/AddressInfo.tsx index 46199746..45f4b654 100644 --- a/src/routes/safe/components/Transactions/TxList/AddressInfo.tsx +++ b/src/routes/safe/components/Transactions/TxList/AddressInfo.tsx @@ -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 ( diff --git a/src/routes/safe/components/Transactions/TxList/OwnerRow.tsx b/src/routes/safe/components/Transactions/TxList/OwnerRow.tsx index ebfca691..6cd2cab5 100644 --- a/src/routes/safe/components/Transactions/TxList/OwnerRow.tsx +++ b/src/routes/safe/components/Transactions/TxList/OwnerRow.tsx @@ -12,7 +12,7 @@ export const OwnerRow = ({ ownerAddress }: { ownerAddress: string }): ReactEleme ( - <> - - {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)) { + name = txInfo.toInfo.name + avatarUrl = txInfo.toInfo.logoUri + } + + return ( + <> + + + {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 ( - + ) @@ -74,7 +95,7 @@ export const TxData = ({ txData }: TxDataProps): ReactElement | null => { // we render the decoded data return ( - + ) diff --git a/src/routes/safe/components/Transactions/TxList/TxDetails.tsx b/src/routes/safe/components/Transactions/TxList/TxDetails.tsx index 675f59c1..fdc043ca 100644 --- a/src/routes/safe/components/Transactions/TxList/TxDetails.tsx +++ b/src/routes/safe/components/Transactions/TxList/TxDetails.tsx @@ -74,7 +74,7 @@ const TxDataGroup = ({ txDetails }: { txDetails: ExpandedTxDetails }): ReactElem return null } - return + return } type TxDetailsProps = { diff --git a/src/routes/safe/components/Transactions/TxList/TxInfo.tsx b/src/routes/safe/components/Transactions/TxList/TxInfo.tsx index 9a485d9a..725d6780 100644 --- a/src/routes/safe/components/Transactions/TxList/TxInfo.tsx +++ b/src/routes/safe/components/Transactions/TxList/TxInfo.tsx @@ -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 } diff --git a/src/routes/safe/components/Transactions/TxList/TxInfoDetails.tsx b/src/routes/safe/components/Transactions/TxList/TxInfoDetails.tsx index 2ef7d24a..8c0a84a0 100644 --- a/src/routes/safe/components/Transactions/TxList/TxInfoDetails.tsx +++ b/src/routes/safe/components/Transactions/TxList/TxInfoDetails.tsx @@ -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 ( - + { break } + const toInfo = tx.txInfo.toInfo + if (toInfo) { + setType({ icon: toInfo.logoUri, text: toInfo.name }) + break + } + setType({ icon: CustomTxIcon, text: 'Contract interaction' }) break } diff --git a/yarn.lock b/yarn.lock index b65e2fb0..79aa2e77 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1596,9 +1596,9 @@ solc "0.5.14" truffle "^5.1.21" -"@gnosis.pm/safe-react-components@https://github.com/gnosis/safe-react-components.git#7ebc414": +"@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#7ebc414ae975d60846704c5a8db5e61c30348d12" + resolved "https://github.com/gnosis/safe-react-components.git#2e427ee36694c7964301fc155b0c080101a34bed" dependencies: classnames "^2.2.6" react-media "^1.10.0" From 36ba59799fd5ee55e61502abc7490e37f215ac7e Mon Sep 17 00:00:00 2001 From: Mikhail Mikheev Date: Thu, 8 Apr 2021 15:40:40 +0400 Subject: [PATCH 10/14] Revert "Add Aave 2 safe app (#2127)" This reverts commit e48891ba2bb32a2ef7f83a550d5aceff93d43059. --- src/routes/safe/components/Apps/utils.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts index 46cbe08c..fa66d807 100644 --- a/src/routes/safe/components/Apps/utils.ts +++ b/src/routes/safe/components/Apps/utils.ts @@ -34,12 +34,6 @@ export const staticAppsList: Array = [ disabled: false, networks: [ETHEREUM_NETWORK.MAINNET], }, - // Aave v2 - { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmVg7aXr5S8sT2iUdUwdkfTJNknmB7rcE3t92HiGoVsYDj`, - disabled: false, - networks: [ETHEREUM_NETWORK.MAINNET], - }, //Balancer Exchange { url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmRb2VfPVYBrv6gi2zDywgVgTg3A19ZCRMqwL13Ez5f5AS`, From ad472f3d5f01f8c72c6151f7dab12da72279b063 Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Thu, 8 Apr 2021 17:52:50 +0200 Subject: [PATCH 11/14] Revert "Add fix to swap symbol and name for sidechains" This reverts commit 45d8157f9047ce5845caf130603810e0b719a907. --- .../safe/api/fetchTokenCurrenciesBalances.ts | 25 ++----------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/src/logic/safe/api/fetchTokenCurrenciesBalances.ts b/src/logic/safe/api/fetchTokenCurrenciesBalances.ts index 2fcc4a93..d1185a3d 100644 --- a/src/logic/safe/api/fetchTokenCurrenciesBalances.ts +++ b/src/logic/safe/api/fetchTokenCurrenciesBalances.ts @@ -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) } From 98719f8e6f8bb628bdca66fb4e67b44bdd916b4d Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Thu, 8 Apr 2021 18:40:13 +0200 Subject: [PATCH 12/14] Fix Volta token name in configuration --- src/config/networks/volta.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config/networks/volta.ts b/src/config/networks/volta.ts index 154ba9e8..5450752b 100644 --- a/src/config/networks/volta.ts +++ b/src/config/networks/volta.ts @@ -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, }, From 70983c6c012d2c13f318ce320624cceb58ae8c28 Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Thu, 8 Apr 2021 18:45:06 +0200 Subject: [PATCH 13/14] Update rinkeby safe transaction url --- src/config/networks/rinkeby.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/networks/rinkeby.ts b/src/config/networks/rinkeby.ts index 3ce8179f..fddbf2af 100644 --- a/src/config/networks/rinkeby.ts +++ b/src/config/networks/rinkeby.ts @@ -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', From 228ab703ff1704840f7a5f4ffa8dc3cb0fcbe8a4 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 12 Apr 2021 04:45:59 -0300 Subject: [PATCH 14/14] Fix: toInfo could be null (#2154) * validate if txInfo is defined * set toInfo as optional --- src/logic/safe/store/models/types/gateway.d.ts | 2 +- src/routes/safe/components/Transactions/TxList/TxData.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/logic/safe/store/models/types/gateway.d.ts b/src/logic/safe/store/models/types/gateway.d.ts index cf45553d..1212328b 100644 --- a/src/logic/safe/store/models/types/gateway.d.ts +++ b/src/logic/safe/store/models/types/gateway.d.ts @@ -133,7 +133,7 @@ type BaseCustom = { dataSize: string value: string isCancellation: boolean - toInfo: AddressInfo + toInfo?: AddressInfo } type Custom = BaseCustom & { diff --git a/src/routes/safe/components/Transactions/TxList/TxData.tsx b/src/routes/safe/components/Transactions/TxList/TxData.tsx index f559d721..82be9fb6 100644 --- a/src/routes/safe/components/Transactions/TxList/TxData.tsx +++ b/src/routes/safe/components/Transactions/TxList/TxData.tsx @@ -33,7 +33,7 @@ const DetailsWithTxInfo = ({ children, txData, txInfo }: DetailsWithTxInfoProps) let name let avatarUrl - if (isCustomTxInfo(txInfo)) { + if (isCustomTxInfo(txInfo) && txInfo.toInfo) { name = txInfo.toInfo.name avatarUrl = txInfo.toInfo.logoUri }