diff --git a/.dockerignore b/.dockerignore index 8740a1f92c..5d9a9785a5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,3 +4,4 @@ .DS_Store **/.idea **/dist +*Dockerfile* diff --git a/.env.example b/.env.example index 1b318d4386..add2b488e4 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,8 @@ +# Define environment +REACT_APP_ENV= + # For all environments +REACT_APP_GATEWAY_URL= REACT_APP_GOOGLE_ANALYTICS= REACT_APP_INFURA_TOKEN= REACT_APP_IPFS_GATEWAY=https://ipfs.io/ipfs @@ -21,5 +25,5 @@ REACT_APP_LATEST_SAFE_VERSION= REACT_APP_APP_VERSION=$npm_package_version # Contracts Addresses -REACT_APP_SPENDING_LIMIT_MODULE_ADDRESS=0x9e9Bf12b5a66c0f0A7435835e0365477E121B110 +REACT_APP_SPENDING_LIMIT_MODULE_ADDRESS= diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 47db697743..398d0affae 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -4,17 +4,15 @@ about: Create an issue to fix a bug --- ## Description ## Environment - - Browser: + - Browser: Chrome - Wallet: MetaMask - - Safe: - - Environment: - - production (rinkeby) + - Chain: Rinkeby ## Steps to reproduce 1. Go to diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index b3fb99df0e..a2056046f0 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -1,17 +1,24 @@ --- name: Feature request about: Create a feature request for the Gnosis Safe - --- -## Overview + + +## Overview ## Requirements -## Screens - - Figma: - - Zeplin: +## Designs ## Links diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 5cd936265b..051318b065 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,4 +5,6 @@ Resolves # ## How to test it +## Analytics changes + ## Screenshots diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000000..9aa698fa10 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,70 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ dev ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ dev ] + schedule: + - cron: '37 9 * * 6' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index fc396a7c62..e446892903 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,4 +6,4 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: gnosis/safe-react-eslint-plus-action@main + - uses: gnosis/safe-react-eslint-plus-action@v3.5.0 diff --git a/Dockerfile b/Dockerfile index eb57b09d2a..4f4039b4ba 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,12 @@ FROM node:14 -RUN apt-get update && apt-get install -y libusb-1.0-0 libusb-1.0-0-dev libudev-dev +RUN apt-get update \ + && apt-get install -y libusb-1.0-0 libusb-1.0-0-dev libudev-dev \ + && rm -rf /var/lib/apt/lists/* WORKDIR /app -COPY package.json ./ - -COPY yarn.lock ./ +COPY package.json yarn.lock . COPY src/logic/contracts/artifacts ./src/logic/contracts/artifacts diff --git a/README.md b/README.md index 76c215e5ae..3eb15ff763 100644 --- a/README.md +++ b/README.md @@ -62,12 +62,24 @@ Install dependencies for the project: yarn install ``` -To launch the app with all configured chains: +To launch the dev version of the app locally: ``` yarn start ``` +Alternatively, to run the production version of the app: + +``` +yarn build +mv build app +python -m SimpleHTTPServer 3000 +``` + +And open http://localhost:3000/app in the browser. + +### Docker + If you prefer to use Docker: ``` diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000000..ac4188718d --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,18 @@ +server { + listen 80; + + location / { + root /usr/share/nginx/html; + try_files $uri /index.html; + index index.html index.htm; + } + + location ^~ /app { + alias /usr/share/nginx/html; + try_files $uri /index.html; + index index.html index.htm; + } + + + include /etc/nginx/extra-conf.d/*.conf; +} diff --git a/package.json b/package.json index 71c0cd4f2e..f19d7e7bb1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "safe-react", - "version": "3.17.2", + "version": "3.19.0", "description": "Allowing crypto users manage funds in a safer way", "website": "https://github.com/gnosis/safe-react#readme", "homepage": "/", @@ -89,7 +89,7 @@ "@gnosis.pm/safe-apps-sdk-v1": "npm:@gnosis.pm/safe-apps-sdk@0.4.2", "@gnosis.pm/safe-core-sdk": "^1.3.0", "@gnosis.pm/safe-deployments": "^1.8.0", - "@gnosis.pm/safe-react-components": "^0.9.0", + "@gnosis.pm/safe-react-components": "^0.9.7", "@gnosis.pm/safe-react-gateway-sdk": "2.8.3", "@material-ui/core": "^4.12.3", "@material-ui/icons": "^4.11.0", diff --git a/patches/@gnosis.pm+safe-core-sdk+1.3.0.patch b/patches/@gnosis.pm+safe-core-sdk+1.3.0.patch deleted file mode 100644 index e7226cefd9..0000000000 --- a/patches/@gnosis.pm+safe-core-sdk+1.3.0.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/node_modules/@gnosis.pm/safe-core-sdk/package.json b/node_modules/@gnosis.pm/safe-core-sdk/package.json -index 560a5ec..7abf6a5 100644 ---- a/node_modules/@gnosis.pm/safe-core-sdk/package.json -+++ b/node_modules/@gnosis.pm/safe-core-sdk/package.json -@@ -92,7 +92,7 @@ - }, - "dependencies": { - "@gnosis.pm/safe-core-sdk-types": "^0.1.1", -- "@gnosis.pm/safe-deployments": "^1.7.0", -+ "@gnosis.pm/safe-deployments": "^1.8.0", - "ethereumjs-util": "^7.1.3", - "semver": "^7.3.5" - } diff --git a/prod.Dockerfile b/prod.Dockerfile new file mode 100644 index 0000000000..9f2f856d52 --- /dev/null +++ b/prod.Dockerfile @@ -0,0 +1,29 @@ +FROM node:14 as react-build-step + +# Grab needed environment variables from .env.example +ENV REACT_APP_ENV=production + +RUN apt-get update \ + && apt-get install -y libusb-1.0-0 libusb-1.0-0-dev libudev-dev \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY package.json yarn.lock . +COPY src/logic/contracts/artifacts ./src/logic/contracts/artifacts + +RUN yarn install + +COPY . . + +RUN yarn build + +# Deploy the build +FROM nginx:1-alpine + +COPY ./docker/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=react-build-step /app/build /usr/share/nginx/html/ + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/public/resources/og-image.png b/public/resources/og-image.png new file mode 100644 index 0000000000..fd68fbc01f Binary files /dev/null and b/public/resources/og-image.png differ diff --git a/public/third-party-cookies-check/index.html b/public/third-party-cookies-check/index.html new file mode 100644 index 0000000000..5affcd4997 --- /dev/null +++ b/public/third-party-cookies-check/index.html @@ -0,0 +1,43 @@ + + + + + + + Third Party Cookies Test + + + + + + diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index a93bfab110..40da08f03d 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -17,15 +17,12 @@ import { currentSafeWithNames } from 'src/logic/safe/store/selectors' import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors' import Modal from 'src/components/Modal' import SendModal from 'src/routes/safe/components/Balances/SendModal' -import { useLoadSafe } from 'src/logic/safe/hooks/useLoadSafe' -import { useSafeScheduledUpdates } from 'src/logic/safe/hooks/useSafeScheduledUpdates' import useSafeActions from 'src/logic/safe/hooks/useSafeActions' import { formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount' import { grantedSelector } from 'src/routes/safe/container/selector' import ReceiveModal from './ReceiveModal' import { useSidebarItems } from 'src/components/AppLayout/Sidebar/useSidebarItems' import useAddressBookSync from 'src/logic/addressBook/hooks/useAddressBookSync' -import { extractSafeAddress } from 'src/routes/routes' const notificationStyles = { success: { @@ -55,13 +52,12 @@ const App: React.FC = ({ children }) => { const classes = useStyles() const { toggleSidebar } = useContext(SafeListSidebarContext) const { name: safeName, totalFiatBalance: currentSafeBalance } = useSelector(currentSafeWithNames) - const addressFromUrl = extractSafeAddress() const { safeActionsState, onShow, onHide, showSendFunds, hideSendFunds } = useSafeActions() const currentCurrency = useSelector(currentCurrencySelector) const granted = useSelector(grantedSelector) const sidebarItems = useSidebarItems() - useLoadSafe(addressFromUrl) // load initially - useSafeScheduledUpdates(addressFromUrl) // load every X seconds + const { address: safeAddress } = useSelector(currentSafeWithNames) + useAddressBookSync() const sendFunds = safeActionsState.sendFunds @@ -95,7 +91,7 @@ const App: React.FC = ({ children }) => { { selectedToken={sendFunds.selectedToken} /> - {addressFromUrl && ( + {safeAddress && ( { paperClassName="receive-modal" title="Receive Tokens" > - + )} diff --git a/src/components/AppLayout/Header/components/ProviderDetails/UserDetails.tsx b/src/components/AppLayout/Header/components/ProviderDetails/UserDetails.tsx index 10e1894e88..301dd1d45e 100644 --- a/src/components/AppLayout/Header/components/ProviderDetails/UserDetails.tsx +++ b/src/components/AppLayout/Header/components/ProviderDetails/UserDetails.tsx @@ -37,6 +37,10 @@ const styles = createStyles({ padding: '9px', lineHeight: 1, }, + ens: { + paddingBottom: md, + fontWeight: 'bold', + }, details: { padding: `0 ${md}`, height: '20px', @@ -98,6 +102,7 @@ type Props = { openDashboard?: (() => void | null) | boolean provider?: string userAddress: string + ensName: string } const useStyles = makeStyles(styles) @@ -108,6 +113,7 @@ export const UserDetails = ({ openDashboard, provider, userAddress, + ensName, }: Props): React.ReactElement => { const connectedNetwork = useSelector(networkSelector) const classes = useStyles() @@ -122,6 +128,11 @@ export const UserDetails = ({ )} + {ensName && ( + + {ensName} + + )} {userAddress ? ( { const classes = useStyles() const currentNetwork = useSelector(networkSelector) + const ensName = useSelector(userEnsSelector) const chain = getChainById(currentNetwork) const addressColor = connected ? 'text' : 'warning' return ( @@ -90,16 +85,22 @@ const ProviderInfo = ({ connected, provider, userAddress }: ProviderInfoProps): {provider} {chain?.chainName && ` @ ${chain.chainName}`} -
+
{connected ? ( - + ensName ? ( + + {ensName} + + ) : ( + + ) ) : ( Connection Error diff --git a/src/components/AppLayout/Header/index.tsx b/src/components/AppLayout/Header/index.tsx index b1782813cc..75aad66807 100644 --- a/src/components/AppLayout/Header/index.tsx +++ b/src/components/AppLayout/Header/index.tsx @@ -12,6 +12,7 @@ import { loadedSelector, providerNameSelector, userAccountSelector, + userEnsSelector, } from 'src/logic/wallets/store/selectors' import { removeProvider } from 'src/logic/wallets/store/actions' import onboard from 'src/logic/wallets/onboard' @@ -21,6 +22,7 @@ const HeaderComponent = (): React.ReactElement => { const provider = useSelector(providerNameSelector) const chainId = useSelector(currentChainId) const userAddress = useSelector(userAccountSelector) + const ensName = useSelector(userEnsSelector) const loaded = useSelector(loadedSelector) const available = useSelector(availableSelector) const dispatch = useDispatch() @@ -65,6 +67,7 @@ const HeaderComponent = (): React.ReactElement => { openDashboard={openDashboard()} provider={provider} userAddress={userAddress} + ensName={ensName} /> ) } diff --git a/src/components/AppLayout/Sidebar/DevTools/index.tsx b/src/components/AppLayout/Sidebar/DevTools/index.tsx index 9a97c9ba75..fb146bfc7b 100644 --- a/src/components/AppLayout/Sidebar/DevTools/index.tsx +++ b/src/components/AppLayout/Sidebar/DevTools/index.tsx @@ -1,4 +1,4 @@ -import { ReactElement } from 'react' +import { ReactElement, useMemo } from 'react' import { useSelector } from 'react-redux' import styled from 'styled-components' import { fireEvent, screen, waitForElementToBeRemoved } from '@testing-library/react' @@ -6,6 +6,7 @@ import { Button } from '@gnosis.pm/safe-react-components' import List from '@material-ui/core/List' import ListItem from '@material-ui/core/ListItem' import ListItemText from '@material-ui/core/ListItemText' +import throttle from 'lodash/throttle' import { currentSafe, currentSafeEthBalance } from 'src/logic/safe/store/selectors' import { extractSafeAddress } from 'src/routes/routes' @@ -80,6 +81,8 @@ const DevTools = (): ReactElement => { return hasFunds } + const throttledCreatedQueuedTx = useMemo(() => throttle(createQueuedTx, 1000), []) + return ( <> @@ -98,7 +101,7 @@ const DevTools = (): ReactElement => { createQueuedTx(safeAddress, threshold)} + onClick={() => throttledCreatedQueuedTx(safeAddress, threshold)} size="md" variant="bordered" disabled={!isGranted || !hasSufficientFunds()} diff --git a/src/components/AppLayout/Sidebar/SafeHeader/index.tsx b/src/components/AppLayout/Sidebar/SafeHeader/index.tsx index 400622990c..a24bb19396 100644 --- a/src/components/AppLayout/Sidebar/SafeHeader/index.tsx +++ b/src/components/AppLayout/Sidebar/SafeHeader/index.tsx @@ -9,6 +9,7 @@ import { CopyToClipboardBtn, ExplorerButton, } from '@gnosis.pm/safe-react-components' +import { useRouteMatch } from 'react-router-dom' import ButtonHelper from 'src/components/ButtonHelper' import FlexSpacer from 'src/components/FlexSpacer' @@ -17,7 +18,7 @@ import { border, fontColor } from 'src/theme/variables' import { ChainInfo } from '@gnosis.pm/safe-react-gateway-sdk' import PrefixedEthHashInfo from 'src/components/PrefixedEthHashInfo' import { copyShortNameSelector } from 'src/logic/appearance/selectors' -import { extractShortChainName } from 'src/routes/routes' +import { ADDRESSED_ROUTE, extractShortChainName } from 'src/routes/routes' export const TOGGLE_SIDEBAR_BTN_TESTID = 'TOGGLE_SIDEBAR_BTN' @@ -129,7 +130,9 @@ const SafeHeader = ({ const copyChainPrefix = useSelector(copyShortNameSelector) const shortName = extractShortChainName() - if (!address) { + const hasSafeOpen = useRouteMatch(ADDRESSED_ROUTE) + + if (!address || !hasSafeOpen) { return ( diff --git a/src/components/AppLayout/Sidebar/index.tsx b/src/components/AppLayout/Sidebar/index.tsx index ab1887d1a3..f99269c858 100644 --- a/src/components/AppLayout/Sidebar/index.tsx +++ b/src/components/AppLayout/Sidebar/index.tsx @@ -1,4 +1,4 @@ -import { lazy } from 'react' +import { lazy, useMemo } from 'react' import styled from 'styled-components' import { Divider, IconText } from '@gnosis.pm/safe-react-components' @@ -66,41 +66,43 @@ const Sidebar = ({ onToggleSafeList, onReceiveClick, onNewTransactionClick, -}: Props): React.ReactElement => ( - <> - +}: Props): React.ReactElement => { + const devTools = useMemo(() => lazyLoad('./DevTools'), []) + const debugToggle = useMemo(() => lazyLoad('./DebugToggle'), []) + return ( + <> + - {items.length ? ( - <> - - - - ) : null} - - {!IS_PRODUCTION && safeAddress && ( + {items.length ? ( <> - {lazyLoad('./DevTools')} + - )} - - {!IS_PRODUCTION && lazyLoad('./DebugToggle')} - - + ) : null} + + {!IS_PRODUCTION && safeAddress && ( + <> + + {devTools} + + )} + {!IS_PRODUCTION && debugToggle} + - - - - - -) + + + + + + ) +} export default Sidebar diff --git a/src/components/ChainIndicator/index.tsx b/src/components/ChainIndicator/index.tsx index 65ebe7b170..5b954610c8 100644 --- a/src/components/ChainIndicator/index.tsx +++ b/src/components/ChainIndicator/index.tsx @@ -7,7 +7,7 @@ import { ChainId } from 'src/config/chain.d' interface Props { chainId: ChainId - noLabel?: boolean + hideCircle?: boolean } const Wrapper = styled.span` @@ -16,14 +16,13 @@ const Wrapper = styled.span` vertical-align: text-bottom; margin-right: 0.15em; } -} ` -const ChainIndicator = ({ chainId, noLabel }: Props): React.ReactElement => { +const ChainIndicator = ({ chainId, hideCircle }: Props): React.ReactElement => { return ( - - {!noLabel && getChainById(chainId).chainName} + {!hideCircle && } + {getChainById(chainId).chainName} ) } diff --git a/src/components/DecodeTxs/__tests__/index.ts b/src/components/DecodeTxs/__tests__/index.ts index f7d67b8d9b..cef4ee3347 100644 --- a/src/components/DecodeTxs/__tests__/index.ts +++ b/src/components/DecodeTxs/__tests__/index.ts @@ -1,4 +1,4 @@ -import { getByteLength } from '..' +import { getByteLength } from '../../../utils/getByteLength' describe('DecodeTxs tests', () => { it('should calculate the byte length of a single hex string', () => { diff --git a/src/components/DecodeTxs/index.tsx b/src/components/DecodeTxs/index.tsx index b5cdbec2ed..5b3100d994 100644 --- a/src/components/DecodeTxs/index.tsx +++ b/src/components/DecodeTxs/index.tsx @@ -1,19 +1,28 @@ -import { ReactElement } from 'react' +import { ReactElement, useState } from 'react' import styled from 'styled-components' import { Transaction } from '@gnosis.pm/safe-apps-sdk-v1' -import { - DecodedDataResponse, - DecodedDataBasicParameter, - DecodedDataParameterValue, -} from '@gnosis.pm/safe-react-gateway-sdk' +import { DecodedDataBasicParameter, DecodedDataParameterValue } from '@gnosis.pm/safe-react-gateway-sdk' import get from 'lodash/get' -import { Text, CopyToClipboardBtn, IconText, FixedIcon } from '@gnosis.pm/safe-react-components' -import { hexToBytes } from 'web3-utils' - +import { + Text, + CopyToClipboardBtn, + IconText, + Accordion, + AccordionSummary, + AccordionDetails, +} from '@gnosis.pm/safe-react-components' + +import Paragraph from 'src/components/layout/Paragraph' +import Row from 'src/components/layout/Row' import { getExplorerInfo } from 'src/config' -import { DecodedTxDetail } from 'src/routes/safe/components/Apps/components/ConfirmTxModal' +import { DecodedTxDetailType } from 'src/routes/safe/components/Apps/components/ConfirmTxModal' import PrefixedEthHashInfo from '../PrefixedEthHashInfo' +import { getByteLength } from 'src/utils/getByteLength' import { getInteractionTitle } from 'src/routes/safe/components/Transactions/helpers/utils' +import { + DecodedTxDetail, + isDataDecodedParameterValue, +} from 'src/routes/safe/components/Apps/components/ConfirmTxModal/DecodedTxDetail' const FlexWrapper = styled.div<{ margin: number }>` display: flex; @@ -24,6 +33,12 @@ const FlexWrapper = styled.div<{ margin: number }>` } ` +const StyledAccordionSummary = styled(AccordionSummary)` + & .MuiAccordionSummary-content { + justify-content: space-between; + } +` + const BasicTxInfoWrapper = styled.div` margin-bottom: 15px; @@ -32,44 +47,10 @@ const BasicTxInfoWrapper = styled.div` } ` -const TxList = styled.div` - width: 100%; - max-height: 260px; - overflow-y: auto; - border-top: 2px solid ${({ theme }) => theme.colors.separator}; -` - -const TxListItem = styled.div` - display: flex; - justify-content: space-between; - - padding: 0 24px; - height: 50px; - border-bottom: 2px solid ${({ theme }) => theme.colors.separator}; - - :hover { - cursor: pointer; - } -` const ElementWrapper = styled.div` margin-bottom: 15px; ` -export const getByteLength = (data: string | string[]): number => { - try { - if (!Array.isArray(data)) { - data = data.split(',') - } - // Return the sum of the byte sizes of each hex string - return data.reduce((result, hex) => { - const bytes = hexToBytes(hex) - return result + bytes.length - }, 0) - } catch (err) { - return 0 - } -} - export const BasicTxInfo = ({ txRecipient, txData, @@ -85,9 +66,11 @@ export const BasicTxInfo = ({ {/* TO */} <> - - {getInteractionTitle(txValue)} - + + + {getInteractionTitle(txValue)} + + <> {/* Data */} - - Data (hex encoded): - + + + Data (hex encoded): + + - {txData ? getByteLength(txData) : 0} bytes + + {txData ? getByteLength(txData) : 0} bytes + @@ -155,38 +142,33 @@ export const getParameterElement = (parameter: DecodedDataBasicParameter, index: ) } -const SingleTx = ({ - decodedData, - onTxItemClick, -}: { - decodedData: DecodedDataResponse | null - onTxItemClick: (decodedTxDetails: DecodedDataResponse) => void -}): ReactElement | null => { +const SingleTx = ({ decodedData }: { decodedData: DecodedTxDetailType }): ReactElement | null => { + const [isAccordionExpanded, setIsAccordionExpanded] = useState(false) + + const onChangeExpand = () => { + setIsAccordionExpanded((prev) => !prev) + } + if (!decodedData) { return null } + const method = isDataDecodedParameterValue(decodedData) ? decodedData.dataDecoded?.method : decodedData.method + return ( - - onTxItemClick(decodedData)}> + + - - - {decodedData.method} - - - - + {method} + + + + + ) } -const MultiSendTx = ({ - decodedData, - onTxItemClick, -}: { - decodedData: DecodedDataResponse | null - onTxItemClick: (decodedTxDetails: DecodedDataParameterValue) => void -}): ReactElement | null => { +const MultiSendTx = ({ decodedData }: { decodedData: DecodedTxDetailType }): ReactElement | null => { const txs: DecodedDataParameterValue[] | undefined = get(decodedData, 'parameters[0].valueDecoded') if (!txs) { @@ -194,31 +176,19 @@ const MultiSendTx = ({ } return ( - + <> {txs.map((tx, index) => ( - onTxItemClick(tx)}> - - - - {tx.dataDecoded && {tx.dataDecoded.method}} - - - + ))} - + ) } type Props = { txs: Transaction[] - decodedData: DecodedDataResponse | null - onTxItemClick: (decodedTxDetails: DecodedTxDetail) => void + decodedData: DecodedTxDetailType } -export const DecodeTxs = ({ txs, decodedData, onTxItemClick }: Props): ReactElement => { - return txs.length > 1 ? ( - - ) : ( - - ) +export const DecodeTxs = ({ txs, decodedData }: Props): ReactElement => { + return txs.length > 1 ? : } diff --git a/src/components/Divider/index.tsx b/src/components/Divider/index.tsx index fb579ff1f4..a068f031f5 100644 --- a/src/components/Divider/index.tsx +++ b/src/components/Divider/index.tsx @@ -3,6 +3,7 @@ import styled from 'styled-components' import { Icon, Divider as DividerSRC } from '@gnosis.pm/safe-react-components' const Wrapper = styled.div` + position: relative; display: flex; align-items: center; margin: 8px 0; @@ -15,13 +16,25 @@ const StyledDivider = styled(DividerSRC)` width: 100%; ` +const StyledIcon = styled(Icon)` + position: absolute; + left: 50%; + transform: translateX(-50%); + padding: 0 20px; + background: white; + + & svg { + margin: 0; + } +` + type Props = { withArrow?: boolean } const Divider = ({ withArrow }: Props): ReactElement => ( - {withArrow && } + {withArrow && } ) diff --git a/src/components/ExecuteCheckbox/index.tsx b/src/components/ExecuteCheckbox/index.tsx index 3cdedee961..cb120e219d 100644 --- a/src/components/ExecuteCheckbox/index.tsx +++ b/src/components/ExecuteCheckbox/index.tsx @@ -1,7 +1,26 @@ import { ReactElement } from 'react' +import { Tooltip } from '@gnosis.pm/safe-react-components' import { Checkbox, FormControlLabel } from '@material-ui/core' +import styled from 'styled-components' + +import { sm } from 'src/theme/variables' import Row from 'src/components/layout/Row' -import Paragraph from '../layout/Paragraph' +import Img from 'src/components/layout/Img' +import InfoIcon from 'src/assets/icons/info.svg' + +const StyledRow = styled(Row)` + align-items: center; + margin-bottom: ${sm}; +` + +const StyledFormControlLabel = styled(FormControlLabel)` + margin-right: ${sm}; + + & .MuiFormControlLabel-label { + font-size: 16px; + letter-spacing: 0; + } +` interface ExecuteCheckboxProps { onChange: (val: boolean) => unknown @@ -12,16 +31,21 @@ const ExecuteCheckbox = ({ onChange }: ExecuteCheckboxProps): ReactElement => { onChange(e.target.checked) } return ( - - - If you want to sign the transaction now but manually execute it later, click on the checkbox below. - - } + + } label="Execute transaction" data-testid="execute-checkbox" /> - + + + Info Tooltip + + + ) } diff --git a/src/components/ReviewInfoText/index.tsx b/src/components/ReviewInfoText/index.tsx index c041b2a34b..e7a4f63891 100644 --- a/src/components/ReviewInfoText/index.tsx +++ b/src/components/ReviewInfoText/index.tsx @@ -5,26 +5,26 @@ import { useSelector } from 'react-redux' import Paragraph from 'src/components/layout/Paragraph' import { currentSafe } from 'src/logic/safe/store/selectors' import { getLastTxNonce } from 'src/logic/safe/store/selectors/gatewayTransactions' -import { lg, sm } from 'src/theme/variables' -import { TransactionFees } from '../TransactionsFees' +import { lg } from 'src/theme/variables' import { getRecommendedNonce } from 'src/logic/safe/api/fetchSafeTxGasEstimation' import { extractSafeAddress } from 'src/routes/routes' -import { ComponentProps, useEffect, useState } from 'react' +import { useEffect, useState } from 'react' +import { TransactionFailText } from '../TransactionFailText' +import { EstimationStatus } from 'src/logic/hooks/useEstimateTransactionGas' -type CustomReviewInfoTextProps = { +const ReviewInfoTextWrapper = styled.div` + padding: 0 ${lg}; +` + +type ReviewInfoTextProps = { + txEstimationExecutionStatus: EstimationStatus + isExecution: boolean + isCreation: boolean safeNonce?: string testId?: string } -type ReviewInfoTextProps = ComponentProps & CustomReviewInfoTextProps - -const ReviewInfoTextWrapper = styled.div` - background-color: ${({ theme }) => theme.colors.background}; - padding: ${sm} ${lg}; -` - export const ReviewInfoText = ({ - gasCostFormatted, isCreation, isExecution, safeNonce: txParamsSafeNonce = '', @@ -37,6 +37,7 @@ export const ReviewInfoText = ({ const storeNextNonce = `${lastTxNonce && lastTxNonce + 1}` const safeAddress = extractSafeAddress() const [recommendedNonce, setRecommendedNonce] = useState(storeNextNonce) + const transactionAction = isCreation ? 'create' : isExecution ? 'execute' : 'approve' useEffect(() => { const fetchRecommendedNonce = async () => { @@ -62,7 +63,7 @@ export const ReviewInfoText = ({ const transactionsToGo = safeNonceNumber - nonce return ( - + {transactionsToGo < 0 ? ( `Nonce ${txParamsSafeNonce} has already been used. Your transaction will fail. Please use nonce ${recommendedNonce}.` ) : ( @@ -83,13 +84,18 @@ export const ReviewInfoText = ({ return ( {warningMessage() || ( - + <> + + You're about to {transactionAction} a transaction and will have to confirm it with your currently + connected wallet. + + )} + ) } diff --git a/src/components/SafeListSidebar/SafeList/SafeListItem.tsx b/src/components/SafeListSidebar/SafeList/SafeListItem.tsx index 11db73e6b1..10e043165e 100644 --- a/src/components/SafeListSidebar/SafeList/SafeListItem.tsx +++ b/src/components/SafeListSidebar/SafeList/SafeListItem.tsx @@ -85,7 +85,7 @@ const SafeListItem = ({ useEffect(() => { if (isCurrentSafe && shouldScrollToSafe) { - safeRef?.current?.scrollIntoView({ behavior: 'smooth' }) + safeRef?.current?.scrollIntoView({ block: 'center' }) } }, [isCurrentSafe, shouldScrollToSafe]) diff --git a/src/components/TransactionFailText/index.test.tsx b/src/components/TransactionFailText/index.test.tsx new file mode 100644 index 0000000000..102b84f288 --- /dev/null +++ b/src/components/TransactionFailText/index.test.tsx @@ -0,0 +1,83 @@ +import { render, screen, act } from 'src/utils/test-utils' +import { EstimationStatus } from 'src/logic/hooks/useEstimateTransactionGas' +import { TransactionFailText, _ErrorMessage } from '.' + +jest.mock('src/logic/wallets/store/selectors', () => { + const original = jest.requireActual('src/logic/wallets/store/selectors') + return { + ...original, + shouldSwitchWalletChain: () => false, + } +}) + +jest.mock('src/routes/safe/container/selector', () => { + const original = jest.requireActual('src/routes/safe/container/selector') + return { + ...original, + grantedSelector: () => true, + } +}) + +describe('TransactionFailText', () => { + beforeEach(() => { + jest.resetAllMocks() + }) + + it('shows the create & execute error', async () => { + await act(async () => { + render() + }) + + expect(screen.getByAltText('Info Tooltip')).toBeDefined() + expect(screen.getByText(`${_ErrorMessage.general} ${_ErrorMessage.creation}`)).toBeDefined() + }) + + it('shows the execution error when execution an existing tx', async () => { + await act(async () => { + render() + }) + + expect(screen.getByAltText('Info Tooltip')).toBeDefined() + expect(screen.getByText(`${_ErrorMessage.general} ${_ErrorMessage.execution}`)).toBeDefined() + }) + + it('shows a wrong chain error', async () => { + const sel = require('src/logic/wallets/store/selectors') + ;(sel.shouldSwitchWalletChain as jest.Mocked) = jest.fn(() => true) + + await act(async () => { + render() + }) + + expect(screen.getByAltText('Info Tooltip')).toBeDefined() + expect(screen.getByText(_ErrorMessage.wrongChain)).toBeDefined() + }) + + it('shows an owner error', async () => { + const sel = require('src/routes/safe/container/selector') + ;(sel.grantedSelector as jest.Mocked) = jest.fn(() => false) + + await act(async () => { + render() + }) + + expect(screen.getByAltText('Info Tooltip')).toBeDefined() + expect(screen.getByText(_ErrorMessage.notOwner)).toBeDefined() + }) + + it('renders null if neither execution nor creation error', async () => { + await act(async () => { + render() + }) + + expect(() => screen.getByAltText('Info Tooltip')).toThrow() + }) + + it('renders null if estimation status is not failure', async () => { + await act(async () => { + render() + }) + + expect(() => screen.getByAltText('Info Tooltip')).toThrow() + }) +}) diff --git a/src/components/TransactionFailText/index.tsx b/src/components/TransactionFailText/index.tsx index 22a608196f..770bfdc0fb 100644 --- a/src/components/TransactionFailText/index.tsx +++ b/src/components/TransactionFailText/index.tsx @@ -1,15 +1,22 @@ import { createStyles, makeStyles } from '@material-ui/core' import { sm } from 'src/theme/variables' -import { EstimationStatus } from 'src/logic/hooks/useEstimateTransactionGas' import Row from 'src/components/layout/Row' import Paragraph from 'src/components/layout/Paragraph' import Img from 'src/components/layout/Img' import InfoIcon from 'src/assets/icons/info_red.svg' import { useSelector } from 'react-redux' -import { currentSafeThreshold } from 'src/logic/safe/store/selectors' import { shouldSwitchWalletChain } from 'src/logic/wallets/store/selectors' import { grantedSelector } from 'src/routes/safe/container/selector' +import { EstimationStatus } from 'src/logic/hooks/useEstimateTransactionGas' + +enum ErrorMessage { + general = 'This transaction will most likely fail.', + creation = 'To save gas costs, avoid creating the transaction.', + execution = 'To save gas costs, reject this transaction.', + notOwner = `You are currently not an owner of this Safe and won't be able to submit this transaction.`, + wrongChain = 'Your wallet is connected to the wrong chain.', +} const styles = createStyles({ executionWarningRow: { @@ -24,36 +31,28 @@ const styles = createStyles({ const useStyles = makeStyles(styles) type TransactionFailTextProps = { - txEstimationExecutionStatus: EstimationStatus isExecution: boolean + isCreation: boolean + estimationStatus: EstimationStatus } export const TransactionFailText = ({ - txEstimationExecutionStatus, isExecution, + isCreation, + estimationStatus, }: TransactionFailTextProps): React.ReactElement | null => { const classes = useStyles() - const threshold = useSelector(currentSafeThreshold) const isWrongChain = useSelector(shouldSwitchWalletChain) - const isGranted = useSelector(grantedSelector) + const isOwner = useSelector(grantedSelector) - if (txEstimationExecutionStatus !== EstimationStatus.FAILURE) { - return null - } + const showError = + isWrongChain || (isExecution && estimationStatus === EstimationStatus.FAILURE) || (isCreation && !isOwner) + if (!showError) return null - let errorDesc = 'To save gas costs, avoid creating the transaction.' - if (isExecution) { - errorDesc = - threshold && threshold > 1 - ? `To save gas costs, reject this transaction` - : `To save gas costs, avoid executing the transaction.` - } + const errorDesc = isCreation ? ErrorMessage.creation : ErrorMessage.execution + const defaultMsg = `${ErrorMessage.general} ${errorDesc}` - const error = isGranted - ? `This transaction will most likely fail. ${errorDesc}` - : isWrongChain - ? 'Your wallet is connected to the wrong chain.' - : "You are currently not an owner of this Safe and won't be able to submit this transaction." + const error = isWrongChain ? ErrorMessage.wrongChain : isCreation && !isOwner ? ErrorMessage.notOwner : defaultMsg return ( @@ -64,3 +63,6 @@ export const TransactionFailText = ({ ) } + +// For tests +export const _ErrorMessage = ErrorMessage diff --git a/src/components/TransactionsFees/index.tsx b/src/components/TransactionsFees/index.tsx deleted file mode 100644 index 79ca5aef3b..0000000000 --- a/src/components/TransactionsFees/index.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { EstimationStatus } from 'src/logic/hooks/useEstimateTransactionGas' -import Paragraph from 'src/components/layout/Paragraph' -import { getNativeCurrency } from 'src/config' -import { TransactionFailText } from 'src/components/TransactionFailText' -import { Text } from '@gnosis.pm/safe-react-components' -import useCanTxExecute from 'src/logic/hooks/useCanTxExecute' -import { providerSelector } from 'src/logic/wallets/store/selectors' -import { useSelector } from 'react-redux' -import { currentSafe } from 'src/logic/safe/store/selectors' -import { checkIfOffChainSignatureIsPossible } from 'src/logic/safe/safeTxSigner' - -type TransactionFailTextProps = { - txEstimationExecutionStatus: EstimationStatus - gasCostFormatted?: string - isExecution: boolean - isCreation: boolean -} - -export const TransactionFees = ({ - gasCostFormatted, - isExecution, - isCreation, - txEstimationExecutionStatus, -}: TransactionFailTextProps): React.ReactElement | null => { - const { currentVersion: safeVersion } = useSelector(currentSafe) - const { smartContractWallet } = useSelector(providerSelector) - const canTxExecute = useCanTxExecute(isExecution) - const isOffChainSignature = checkIfOffChainSignatureIsPossible(canTxExecute, smartContractWallet, safeVersion) - - const nativeCurrency = getNativeCurrency() - let transactionAction - if (txEstimationExecutionStatus === EstimationStatus.LOADING) { - return null - } - if (isCreation) { - transactionAction = 'create' - } else if (isExecution) { - transactionAction = 'execute' - } else { - transactionAction = 'approve' - } - - return ( - <> - {gasCostFormatted != null && ( - - You're about to {transactionAction} a transaction and will have to confirm it with your currently - connected wallet.{' '} - {!isOffChainSignature && ( - <> - Make sure you have - - {' '} - {gasCostFormatted} {nativeCurrency.symbol}{' '} - - in this wallet to fund the associated transaction fee. - - )} - - )} - - - ) -} diff --git a/src/components/forms/validator.test.ts b/src/components/forms/validator.test.ts index a8c47d6690..a635081b4c 100644 --- a/src/components/forms/validator.test.ts +++ b/src/components/forms/validator.test.ts @@ -3,7 +3,6 @@ import { mustBeInteger, mustBeFloat, maxValue, - mustBeUrl, minValue, mustBeEthereumAddress, mustBeAddressHash, @@ -64,7 +63,7 @@ describe('Forms > Validators', () => { describe('minValue validator', () => { const getMinValueErrMsg = (minValue: number, inclusive = true): string => - `Should be greater than ${inclusive ? 'or equal to ' : ''}${minValue}` + `Must be greater than ${inclusive ? 'or equal to ' : ''}${minValue}` it('Returns undefined for a number greater than minimum', () => { const minimum = Math.random() @@ -92,18 +91,6 @@ describe('Forms > Validators', () => { }) }) - describe('mustBeUrl validator', () => { - const MUST_BE_URL_ERR_MSG = 'Please, provide a valid url' - - it('Returns undefined for a valid url', () => { - expect(mustBeUrl('https://gnosis-safe.io')).toBeUndefined() - }) - - it('Returns an error message for an valid url', () => { - expect(mustBeUrl('gnosis-safe')).toEqual(MUST_BE_URL_ERR_MSG) - }) - }) - describe('maxValue validator', () => { const getMaxValueErrMsg = (maxValue: number): string => `Maximum value is ${maxValue}` diff --git a/src/components/forms/validator.ts b/src/components/forms/validator.ts index 766b0af5a8..a18ac04818 100644 --- a/src/components/forms/validator.ts +++ b/src/components/forms/validator.ts @@ -34,17 +34,6 @@ export const mustBeInteger = (value: string): ValidatorReturnType => export const mustBeFloat = (value: string): ValidatorReturnType => value && Number.isNaN(Number(value)) ? 'Must be a number' : undefined -const regexQuery = - /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/i -const url = new RegExp(regexQuery) -export const mustBeUrl = (value: string): ValidatorReturnType => { - if (url.test(value)) { - return undefined - } - - return 'Please, provide a valid url' -} - export const minValue = (min: number | string, inclusive = true) => (value: string): ValidatorReturnType => { @@ -56,7 +45,7 @@ export const minValue = return undefined } - return `Should be greater than ${inclusive ? 'or equal to ' : ''}${min}` + return `Must be greater than ${inclusive ? 'or equal to ' : ''}${min}` } export const maxValue = diff --git a/src/components/layout/Paragraph/index.module.scss b/src/components/layout/Paragraph/index.module.scss index 9fccfd2c82..a0b9c1469d 100644 --- a/src/components/layout/Paragraph/index.module.scss +++ b/src/components/layout/Paragraph/index.module.scss @@ -46,6 +46,14 @@ color: white; } +.black400 { + color: $black400; +} + +.black600 { + color: $black600; +} + .capitalize { text-transform: capitalize; } diff --git a/src/components/layout/Typography/index.ts b/src/components/layout/Typography/index.ts new file mode 100644 index 0000000000..0ee299217c --- /dev/null +++ b/src/components/layout/Typography/index.ts @@ -0,0 +1,8 @@ +import styled from 'styled-components' +import Paragraph from 'src/components/layout/Paragraph' + +export const Overline = styled(Paragraph)` + font-size: 11px; + line-height: 14px; + letter-spacing: 1px; +` diff --git a/src/logic/exceptions/registry.ts b/src/logic/exceptions/registry.ts index 1898b58498..baf0f04346 100644 --- a/src/logic/exceptions/registry.ts +++ b/src/logic/exceptions/registry.ts @@ -29,6 +29,7 @@ enum ErrorCodes { _615 = '615: Failed to retrieve last transaction from server', _616 = '616: Failed to retrieve recommended nonce', _617 = '617: Error fetching safeTxGas', + _618 = '618: Error fetching fee history', _700 = '700: Failed to read from local/session storage', _701 = '701: Failed to write to local/session storage', _702 = '702: Failed to remove from local/session storage', diff --git a/src/logic/hooks/__tests__/useCanTxExecute.test.ts b/src/logic/hooks/__tests__/useCanTxExecute.test.ts index 9b4048cda0..29cc70d312 100644 --- a/src/logic/hooks/__tests__/useCanTxExecute.test.ts +++ b/src/logic/hooks/__tests__/useCanTxExecute.test.ts @@ -1,191 +1,70 @@ -import { calculateCanTxExecute } from '../useCanTxExecute' +import useCanTxExecute from '../useCanTxExecute' +import * as redux from 'react-redux' + +const mockedRedux = redux as jest.Mocked & { useSelector: any } + +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux') + return { + ...original, + useSelector: () => ({ threshold: 2 }), + } +}) describe('useCanTxExecute tests', () => { - describe('calculateCanTxExecute tests', () => { - beforeEach(() => { - threshold = 1 - isExecution = false - currentSafeNonce = 8 - recommendedNonce = 8 - txConfirmations = 0 - preApprovingOwner = '' - manualSafeNonce = recommendedNonce - }) - // to be overriden as necessary - let threshold - let preApprovingOwner - let txConfirmations - let currentSafeNonce - let recommendedNonce - let isExecution - let manualSafeNonce - it(`should return true if isExecution`, () => { - // given - isExecution = true - - // when - const result = calculateCanTxExecute( - currentSafeNonce, - preApprovingOwner, - threshold, - txConfirmations, - recommendedNonce, - isExecution, - ) - - // then - expect(result).toBe(true) - }) - it(`should return true if single owner and edited nonce is same as safeNonce`, () => { - // given - threshold = 1 - currentSafeNonce = 8 - recommendedNonce = 12 - manualSafeNonce = 8 - - // when - const result = calculateCanTxExecute( - currentSafeNonce, - preApprovingOwner, - threshold, - txConfirmations, - recommendedNonce, - undefined, - manualSafeNonce, - ) - - // then - expect(result).toBe(true) - }) - it(`should return false if single owner and edited nonce is different than safeNonce`, () => { - // given - threshold = 1 - currentSafeNonce = 8 - recommendedNonce = 8 - manualSafeNonce = 20 - - // when - const result = calculateCanTxExecute( - currentSafeNonce, - preApprovingOwner, - threshold, - txConfirmations, - recommendedNonce, - undefined, - manualSafeNonce, - ) - - // then - expect(result).toBe(false) - }) - it(`should return true if single owner and recommendedNonce is same as safeNonce`, () => { - // given - threshold = 1 - currentSafeNonce = 8 - recommendedNonce = 8 - - // when - const result = calculateCanTxExecute( - currentSafeNonce, - preApprovingOwner, - threshold, - txConfirmations, - recommendedNonce, - ) - - // then - expect(result).toBe(true) - }) - it(`should return false if single owner and recommendedNonce is greater than safeNonce and no edited nonce`, () => { - // given - threshold = 1 - currentSafeNonce = 8 - recommendedNonce = 11 - manualSafeNonce = undefined - - // when - const result = calculateCanTxExecute( - currentSafeNonce, - preApprovingOwner, - threshold, - txConfirmations, - recommendedNonce, - undefined, - manualSafeNonce, - ) - - // then - expect(result).toBe(false) - }) - it(`should return false if single owner and recommendedNonce is different than safeNonce`, () => { - // given - threshold = 1 - currentSafeNonce = 8 - recommendedNonce = 12 - - // when - const result = calculateCanTxExecute( - currentSafeNonce, - preApprovingOwner, - threshold, - txConfirmations, - recommendedNonce, - ) - - // then - expect(result).toBe(false) - }) - it(`should return true if the safe threshold is reached for the transaction`, () => { - // given - threshold = 3 - txConfirmations = 3 - - // when - const result = calculateCanTxExecute( - currentSafeNonce, - preApprovingOwner, - threshold, - txConfirmations, - recommendedNonce, - ) - - // then - expect(result).toBe(true) - }) - it(`should return false if the number of confirmations does not meet the threshold and there is no preApprovingOwner`, () => { - // given - threshold = 5 - txConfirmations = 4 - - // when - const result = calculateCanTxExecute( - currentSafeNonce, - preApprovingOwner, - threshold, - txConfirmations, - recommendedNonce, - ) - - // then - expect(result).toBe(false) - }) - it(`should return true if the number of confirmations is one bellow the threshold but there is a preApprovingOwner`, () => { - // given - threshold = 5 - preApprovingOwner = '0x29B1b813b6e84654Ca698ef5d7808E154364900B' - txConfirmations = 4 - - // when - const result = calculateCanTxExecute( - currentSafeNonce, - preApprovingOwner, - threshold, - txConfirmations, - recommendedNonce, - ) - - // then - expect(result).toBe(true) - }) + it(`should return true if owner of a 1/1 Safe`, () => { + mockedRedux.useSelector = jest.fn(() => ({ threshold: 1 })) + + const result = useCanTxExecute('0x000', 0) + expect(result).toBe(true) + }) + + it(`should return false if not an owner and not enough sigs`, () => { + mockedRedux.useSelector = jest.fn(() => ({ threshold: 1 })) + + const result = useCanTxExecute('', 0) + expect(result).toBe(false) + }) + + it(`should return true if 2/2 sigs`, () => { + mockedRedux.useSelector = jest.fn(() => ({ threshold: 2 })) + + const result = useCanTxExecute('', 2) + expect(result).toBe(true) + }) + + it(`should return true if 1/2 sigs and an owner`, () => { + mockedRedux.useSelector = jest.fn(() => ({ threshold: 2 })) + + const result = useCanTxExecute('0x000', 1) + expect(result).toBe(true) + }) + + it(`should return false if 1/3 sigs and an owner`, () => { + mockedRedux.useSelector = jest.fn(() => ({ threshold: 3 })) + + const result = useCanTxExecute('0x000', 1) + expect(result).toBe(false) + }) + + it(`should return false if 2/3 sigs and not an owner`, () => { + mockedRedux.useSelector = jest.fn(() => ({ threshold: 3 })) + + const result = useCanTxExecute('', 2) + expect(result).toBe(false) + }) + + it(`should return true if 3/10 sigs and threshold 3 passed from an arg`, () => { + mockedRedux.useSelector = jest.fn(() => ({ threshold: 10 })) + + const result = useCanTxExecute('', 3, 3) + expect(result).toBe(true) + }) + + it(`should return false if 3/3 sigs and threshold 10 passed from an arg`, () => { + mockedRedux.useSelector = jest.fn(() => ({ threshold: 3 })) + + const result = useCanTxExecute('', 3, 10) + expect(result).toBe(false) }) }) diff --git a/src/logic/hooks/useCanTxExecute.tsx b/src/logic/hooks/useCanTxExecute.tsx index 602ad044ca..f5cae74c7f 100644 --- a/src/logic/hooks/useCanTxExecute.tsx +++ b/src/logic/hooks/useCanTxExecute.tsx @@ -1,69 +1,39 @@ import { useSelector } from 'react-redux' -import { extractSafeAddress } from 'src/routes/routes' import { currentSafe } from '../safe/store/selectors' -import useGetRecommendedNonce from './useGetRecommendedNonce' -export const calculateCanTxExecute = ( - currentSafeNonce: number, - preApprovingOwner: string, - threshold: number, - txConfirmations: number, - recommendedNonce?: number, - isExecution?: boolean, // when executing from the TxList - manualSafeNonce?: number, -): boolean => { - if (isExecution) return true +type UseCanTxExecuteType = ( + preApprovingOwner?: string, + txConfirmations?: number, + existingTxThreshold?: number, + txNonce?: string, +) => boolean + +const useCanTxExecute: UseCanTxExecuteType = ( + preApprovingOwner = '', + txConfirmations = 0, + existingTxThreshold, + txNonce, +) => { + const safeInfo = useSelector(currentSafe) - // Single owner - if (threshold === 1) { - // nonce was changed manually to be executed - if (manualSafeNonce) { - return manualSafeNonce === currentSafeNonce - } - // is next tx - return recommendedNonce === currentSafeNonce + if (txNonce && parseInt(txNonce, 10) !== safeInfo.nonce) { + return false } + // A tx might have been created with a threshold that is different than the current policy + // If an existing tx threshold isn't passed, take the current safe threshold + const threshold = existingTxThreshold ?? safeInfo.threshold + if (txConfirmations >= threshold) { return true } // When having a preApprovingOwner it is needed one less confirmation to execute the tx - if (preApprovingOwner && txConfirmations) { + if (preApprovingOwner) { return txConfirmations + 1 === threshold } return false } -type UseCanTxExecuteType = ( - isExecution?: boolean, - manualSafeNonce?: number, - preApprovingOwner?: string, - txConfirmations?: number, -) => boolean - -const useCanTxExecute: UseCanTxExecuteType = ( - isExecution = false, - manualSafeNonce, - preApprovingOwner = '', - txConfirmations = 0, -) => { - const { threshold } = useSelector(currentSafe) - - const safeAddress = extractSafeAddress() - const recommendedNonce = useGetRecommendedNonce(safeAddress) - const { nonce: currentSafeNonce } = useSelector(currentSafe) - - return calculateCanTxExecute( - currentSafeNonce, - preApprovingOwner, - threshold, - txConfirmations, - recommendedNonce, - isExecution, - manualSafeNonce, - ) -} - export default useCanTxExecute diff --git a/src/logic/hooks/useEstimateSafeCreationGas.tsx b/src/logic/hooks/useEstimateSafeCreationGas.tsx index d39e4031b7..5088c774bd 100644 --- a/src/logic/hooks/useEstimateSafeCreationGas.tsx +++ b/src/logic/hooks/useEstimateSafeCreationGas.tsx @@ -4,7 +4,7 @@ import { estimateGasForDeployingSafe } from 'src/logic/contracts/safeContracts' import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' import { formatAmount } from 'src/logic/tokens/utils/formatAmount' -import { calculateGasPrice } from 'src/logic/wallets/ethTransactions' +import { calculateGasPrice, getFeesPerGas, setMaxPrioFeePerGas } from 'src/logic/wallets/ethTransactions' import { userAccountSelector } from '../wallets/store/selectors' import { getNativeCurrency } from 'src/config' @@ -19,6 +19,8 @@ type SafeCreationEstimationResult = { gasCostFormatted: string // Cost of gas in format '< | > 100' gasLimit: number // Minimum gas requited to execute the Tx gasPrice: string + gasMaxPrioFee: number + gasMaxPrioFeeFormatted: string } const estimateGas = async ( @@ -27,11 +29,14 @@ const estimateGas = async ( safeCreationSalt: number, addresses: string[], ): Promise => { - const [gasEstimation, gasPrice] = await Promise.all([ + const [gasEstimation, gasPrice, feesPerGas] = await Promise.all([ estimateGasForDeployingSafe(addresses, numOwners, userAccount, safeCreationSalt), calculateGasPrice(), + getFeesPerGas(), ]) + const estimatedGasCosts = gasEstimation * parseInt(gasPrice, 10) + const maxPrioFeePerGas = setMaxPrioFeePerGas(feesPerGas.maxPriorityFeePerGas, parseInt(gasPrice, 10)) const nativeCurrency = getNativeCurrency() const gasCost = fromTokenUnit(estimatedGasCosts, nativeCurrency.decimals) const gasCostFormatted = formatAmount(gasCost) @@ -41,6 +46,8 @@ const estimateGas = async ( gasEstimation, gasCostFormatted, gasLimit: gasEstimation, + gasMaxPrioFee: maxPrioFeePerGas, + gasMaxPrioFeeFormatted: formatAmount(maxPrioFeePerGas.toString()), } } @@ -54,6 +61,8 @@ export const useEstimateSafeCreationGas = ({ gasCostFormatted: '< 0.001', gasLimit: 0, gasPrice: '0', + gasMaxPrioFee: 0, + gasMaxPrioFeeFormatted: '0', }) const userAccount = useSelector(userAccountSelector) // Serialize the addresses array so that it doesn't trigger the effect due to the dependencies diff --git a/src/logic/hooks/useEstimateTransactionGas.tsx b/src/logic/hooks/useEstimateTransactionGas.tsx index b10a73294d..e4777883c3 100644 --- a/src/logic/hooks/useEstimateTransactionGas.tsx +++ b/src/logic/hooks/useEstimateTransactionGas.tsx @@ -13,7 +13,7 @@ import { } 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 { calculateGasPrice, setMaxPrioFeePerGas, getFeesPerGas } from 'src/logic/wallets/ethTransactions' import { currentSafe } from 'src/logic/safe/store/selectors' import { providerSelector } from 'src/logic/wallets/store/selectors' import { Confirmation } from 'src/logic/safe/store/models/types/confirmation' @@ -28,20 +28,15 @@ export enum EstimationStatus { SUCCESS = 'SUCCESS', } -const DEFAULT_MAX_GAS_FEE = String(3.5e9) // 3.5 GWEI -const DEFAULT_MAX_PRIO_FEE = String(2.5e9) // 2.5 GWEI - export const checkIfTxIsApproveAndExecution = ( threshold: number, txConfirmations: number, txType?: string, preApprovingOwner?: string, ): boolean => { - if (preApprovingOwner) { - return txConfirmations + 1 === threshold || isSpendingLimit(txType) - } - - return threshold === 1 + if (txConfirmations === threshold) return false + if (!preApprovingOwner) return false + return txConfirmations + 1 === threshold || isSpendingLimit(txType) } export const checkIfTxIsCreation = (txConfirmations: number, txType?: string): boolean => @@ -60,7 +55,6 @@ type UseEstimateTransactionGasProps = { manualMaxPrioFee?: string manualGasLimit?: string manualSafeNonce?: number // Edited nonce - isExecution?: boolean // If called from the TransactionList "next transaction" } export type TransactionGasEstimationResult = { @@ -116,7 +110,7 @@ export const calculateTotalGasCost = ( gasMaxPrioFee: string, decimals: number, ): [string, string] => { - const totalPricePerGas = parseFloat(gasPrice) + parseFloat(gasMaxPrioFee || '0') + const totalPricePerGas = parseInt(gasPrice, 10) + parseInt(gasMaxPrioFee || '0', 10) const estimatedGasCosts = parseInt(gasLimit, 10) * totalPricePerGas const gasCost = fromTokenUnit(estimatedGasCosts, decimals) const formattedGasCost = formatAmount(gasCost) @@ -136,7 +130,6 @@ export const useEstimateTransactionGas = ({ manualMaxPrioFee, manualGasLimit, manualSafeNonce, - isExecution, }: UseEstimateTransactionGasProps): TransactionGasEstimationResult => { const [gasEstimation, setGasEstimation] = useState( getDefaultGasEstimation({ @@ -151,7 +144,7 @@ export const useEstimateTransactionGas = ({ const { address: safeAddress = '', threshold = 1, currentVersion: safeVersion = '' } = useSelector(currentSafe) ?? {} const { account: from, smartContractWallet, name: providerName } = useSelector(providerSelector) - const canTxExecute = useCanTxExecute(isExecution, manualSafeNonce, preApprovingOwner, txConfirmations?.size) + const canTxExecute = useCanTxExecute(preApprovingOwner, txConfirmations?.size) useEffect(() => { const estimateGas = async () => { @@ -161,14 +154,17 @@ export const useEstimateTransactionGas = ({ const isOffChainSignature = checkIfOffChainSignatureIsPossible(canTxExecute, smartContractWallet, safeVersion) const isCreation = checkIfTxIsCreation(txConfirmations?.size || 0, txType) + const { maxPriorityFeePerGas, maxFeePerGas } = await getFeesPerGas() + const maxPrioFeePerGas = setMaxPrioFeePerGas(maxPriorityFeePerGas, maxFeePerGas) + if (isOffChainSignature && !isCreation) { setGasEstimation( getDefaultGasEstimation({ txEstimationExecutionStatus: EstimationStatus.SUCCESS, - gasPrice: fromWei(DEFAULT_MAX_GAS_FEE, 'gwei'), - gasPriceFormatted: DEFAULT_MAX_GAS_FEE, - gasMaxPrioFee: fromWei(DEFAULT_MAX_PRIO_FEE, 'gwei'), - gasMaxPrioFeeFormatted: DEFAULT_MAX_PRIO_FEE, + gasPrice: fromWei(maxFeePerGas.toString(), 'gwei'), + gasPriceFormatted: maxFeePerGas.toString(), + gasMaxPrioFee: fromWei(maxPrioFeePerGas.toString(), 'gwei'), + gasMaxPrioFeeFormatted: maxPrioFeePerGas.toString(), isCreation, isOffChainSignature, }), @@ -222,9 +218,9 @@ export const useEstimateTransactionGas = ({ const gasMaxPrioFee = isMaxFeeParam() ? manualMaxPrioFee ? toWei(manualMaxPrioFee, 'gwei') - : DEFAULT_MAX_PRIO_FEE + : setMaxPrioFeePerGas(maxPriorityFeePerGas, parseInt(gasPrice)).toString() : '0' - const gasMaxPrioFeeFormatted = fromWei(gasMaxPrioFee, 'gwei') + const gasMaxPrioFeeFormatted = fromWei(gasMaxPrioFee.toString(), 'gwei') const gasLimit = manualGasLimit || ethGasLimitEstimation.toString() const [gasCost, gasCostFormatted] = calculateTotalGasCost( gasLimit, @@ -273,10 +269,10 @@ export const useEstimateTransactionGas = ({ setGasEstimation( getDefaultGasEstimation({ txEstimationExecutionStatus: EstimationStatus.FAILURE, - gasPrice: DEFAULT_MAX_GAS_FEE, - gasPriceFormatted: fromWei(DEFAULT_MAX_GAS_FEE, 'gwei'), - gasMaxPrioFee: DEFAULT_MAX_PRIO_FEE, - gasMaxPrioFeeFormatted: fromWei(DEFAULT_MAX_PRIO_FEE, 'gwei'), + gasPrice: maxFeePerGas.toString(), + gasPriceFormatted: fromWei(maxFeePerGas.toString(), 'gwei'), + gasMaxPrioFee: maxPrioFeePerGas.toString(), + gasMaxPrioFeeFormatted: fromWei(maxPrioFeePerGas.toString(), 'gwei'), }), ) } diff --git a/src/logic/hooks/useGaEvents.ts b/src/logic/hooks/useGaEvents.ts new file mode 100644 index 0000000000..afc22d5582 --- /dev/null +++ b/src/logic/hooks/useGaEvents.ts @@ -0,0 +1,35 @@ +import { useEffect } from 'react' +import { matchPath, useLocation } from 'react-router-dom' + +import { useAnalytics } from 'src/utils/googleAnalytics' +import { isDeeplinkedTx } from 'src/routes/safe/components/Transactions/TxList/utils' +import { extractSafeAddress, getPrefixedSafeAddressSlug, SAFE_ROUTES, TRANSACTION_ID_SLUG } from 'src/routes/routes' + +const useGaEvents = (): void => { + const { trackPage } = useAnalytics() + const location = useLocation() + const { pathname, search } = location + // Google Analytics + useEffect(() => { + let trackedPath = pathname + const address = extractSafeAddress() + + // Anonymize safe address + if (address) { + trackedPath = trackedPath.replace(getPrefixedSafeAddressSlug(), 'SAFE_ADDRESS') + } + + // Anonymize deeplinked transaction + if (isDeeplinkedTx()) { + const match = matchPath(pathname, { + path: SAFE_ROUTES.TRANSACTIONS_SINGULAR, + }) + + trackedPath = trackedPath.replace(match?.params[TRANSACTION_ID_SLUG], 'TRANSACTION_ID') + } + + trackPage(trackedPath + search) + }, [pathname, search, trackPage]) +} + +export default useGaEvents diff --git a/src/logic/safe/store/actions/__tests__/fetchSafe.test.ts b/src/logic/safe/store/actions/__tests__/fetchSafe.test.ts index 5f3ea3faa9..eb40e5b457 100644 --- a/src/logic/safe/store/actions/__tests__/fetchSafe.test.ts +++ b/src/logic/safe/store/actions/__tests__/fetchSafe.test.ts @@ -153,12 +153,21 @@ describe('fetchSafe', () => { expect(store.getActions()).toEqual(expectedActions) }) - it('should not dispatch updateSafe if `remoteSafeInfo` is not present', async () => { + it('should dispatch updateSafe if `remoteSafeInfo` is not present', async () => { jest.spyOn(global.console, 'error').mockImplementationOnce(() => {}) mockedGateway.getSafeInfo.mockImplementationOnce(async () => { throw new Error('-- test -- no resource available') }) - const expectedActions = [] + + const expectedActions = [ + { + type: 'UPDATE_SAFE', + payload: { + address: '0xe414604Ad49602C0b9c0b08D0781ECF96740786a', + owners: [], + }, + }, + ] const store = mockStore( Map({ diff --git a/src/logic/safe/store/actions/__tests__/utils.test.ts b/src/logic/safe/store/actions/__tests__/utils.test.ts index 1123574621..8666f2c0c7 100644 --- a/src/logic/safe/store/actions/__tests__/utils.test.ts +++ b/src/logic/safe/store/actions/__tests__/utils.test.ts @@ -1,7 +1,7 @@ import { FEATURES } from '@gnosis.pm/safe-react-gateway-sdk' import { ChainId } from 'src/config/chain.d' -import { buildSafeOwners, extractRemoteSafeInfo, shouldExecuteTransaction } from 'src/logic/safe/store/actions/utils' +import { buildSafeOwners, extractRemoteSafeInfo, canExecuteCreatedTx } from 'src/logic/safe/store/actions/utils' import { SafeRecordProps } from 'src/logic/safe/store/models/safe' import { getMockedSafeInstance, getMockedStoredTServiceModel } from 'src/test/utils/safeHelper' import { LocalTransactionStatus } from '../../models/types/gateway.d' @@ -14,7 +14,7 @@ import { const lastTxFromStore = getMockedStoredTServiceModel() -describe('shouldExecuteTransaction', () => { +describe('canExecuteCreatedTx', () => { it('It should return false if given a safe with a threshold > 1', async () => { // given const nonce = '0' @@ -22,7 +22,7 @@ describe('shouldExecuteTransaction', () => { const safeInstance = getMockedSafeInstance({ threshold }) // when - const result = await shouldExecuteTransaction(safeInstance, nonce, lastTxFromStore) + const result = await canExecuteCreatedTx(safeInstance, nonce, lastTxFromStore) // then expect(result).toBe(false) @@ -35,7 +35,7 @@ describe('shouldExecuteTransaction', () => { const lastTxFromStoreExecuted = { ...lastTxFromStore, txStatus: LocalTransactionStatus.SUCCESS } // when - const result = await shouldExecuteTransaction(safeInstance, nonce, lastTxFromStoreExecuted) + const result = await canExecuteCreatedTx(safeInstance, nonce, lastTxFromStoreExecuted) // then expect(result).toBe(true) @@ -48,7 +48,7 @@ describe('shouldExecuteTransaction', () => { const lastTxFromStoreExecuted = { ...lastTxFromStore, txStatus: LocalTransactionStatus.SUCCESS } // when - const result = await shouldExecuteTransaction(safeInstance, nonce, lastTxFromStoreExecuted) + const result = await canExecuteCreatedTx(safeInstance, nonce, lastTxFromStoreExecuted) // then expect(result).toBe(true) @@ -61,7 +61,7 @@ describe('shouldExecuteTransaction', () => { const lastTxFromStoreExecuted = { ...lastTxFromStore, txStatus: LocalTransactionStatus.FAILED } // when - const result = await shouldExecuteTransaction(safeInstance, nonce, lastTxFromStoreExecuted) + const result = await canExecuteCreatedTx(safeInstance, nonce, lastTxFromStoreExecuted) // then expect(result).toBe(false) diff --git a/src/logic/safe/store/actions/createTransaction.ts b/src/logic/safe/store/actions/createTransaction.ts index edaa2ec63c..448b209be6 100644 --- a/src/logic/safe/store/actions/createTransaction.ts +++ b/src/logic/safe/store/actions/createTransaction.ts @@ -17,7 +17,7 @@ import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions' import { providerSelector } from 'src/logic/wallets/store/selectors' import { generateSafeTxHash } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers' -import { getNonce, shouldExecuteTransaction } from 'src/logic/safe/store/actions/utils' +import { getNonce, canExecuteCreatedTx } from 'src/logic/safe/store/actions/utils' import fetchTransactions from './transactions/fetchTransactions' import { AppReduxState } from 'src/store' import { Dispatch, DispatchReturn } from './types' @@ -102,24 +102,17 @@ export class TxSender { dispatch: Dispatch safeInstance: GnosisSafe safeVersion: string - approveAndExecute: boolean + txId: string // On transaction completion (either confirming or executing) async onComplete(signature?: string, confirmCallback?: ConfirmEventHandler): Promise { - const { txArgs, safeTxHash, txProps, dispatch, notifications, isFinalization, approveAndExecute = false } = this + const { txArgs, safeTxHash, txProps, dispatch, notifications, isFinalization } = this + // Propose the tx to the backend + // 1) If signing + // 2) If creating a new tx (no txId yet) let txDetails: TransactionDetails | null = null - - const isOffChainSigning = !isFinalization && signature - const isOnChainSigning = isFinalization && !signature - - // If 1/? threshold and owner chooses to execute created tx immediately - const isImmediateExecution = isOnChainSigning && !approveAndExecute - - // Propose the tx to the backend if an owner and - // 1) It's a confirmation w/o exection - // 2) It's a creation + execution w/o pre-approved signatures - if (isOffChainSigning || isImmediateExecution) { + if (!isFinalization || !this.txId) { try { txDetails = await saveTxToHistory({ ...txArgs, signature, origin }) } catch (err) { @@ -128,6 +121,12 @@ export class TxSender { } } + // If threshold reached except for last sig, and owner chooses to execute the created tx immediately + // we retrieve txId of newly created tx from the proposal response + if (isFinalization && txDetails) { + dispatch(addPendingTransaction({ id: txDetails.txId })) + } + notifications.closePending() // This is used to communicate the safeTxHash to a Safe App caller @@ -142,7 +141,7 @@ export class TxSender { } async onError(err: Error & { code: number }, errorCallback?: ErrorEventHandler): Promise { - const { txArgs, isFinalization, from, safeTxHash, txProps, dispatch, notifications, safeInstance } = this + const { txArgs, isFinalization, from, txProps, dispatch, notifications, safeInstance, txId } = this logError(Errors._803, err.message) @@ -150,8 +149,9 @@ export class TxSender { notifications.closePending() - if (isFinalization && safeTxHash) { - dispatch(removePendingTransaction({ safeTxHash })) + // Existing transaction was being finalised (txId exists) + if (isFinalization && txId) { + dispatch(removePendingTransaction({ id: txId })) } const executeDataUsedSignatures = safeInstance.methods @@ -185,14 +185,18 @@ export class TxSender { } async sendTx(): Promise { - const { txArgs, isFinalization, from, safeTxHash, txProps, dispatch } = this + const { txArgs, isFinalization, from, safeTxHash, txProps, dispatch, txId } = this const tx = isFinalization ? getExecutionTransaction(txArgs) : getApprovalTransaction(this.safeInstance, safeTxHash) const sendParams = createSendParams(from, txProps.ethParameters || {}) const promiEvent = tx.send(sendParams) + // When signing on-chain don't mark as pending as it is never removed if (isFinalization) { - dispatch(addPendingTransaction({ safeTxHash })) + // Finalising existing transaction (txId exists) + if (txId) { + dispatch(addPendingTransaction({ id: txId })) + } aboutToExecuteTx.setNonce(txArgs.nonce) } @@ -286,8 +290,7 @@ export const createTransaction = ( // Execute right away? sender.isFinalization = - !props.delayExecution && - (await shouldExecuteTransaction(sender.safeInstance, sender.nonce, getLastTransaction(state))) + !props.delayExecution && (await canExecuteCreatedTx(sender.safeInstance, sender.nonce, getLastTransaction(state))) // Prepare a TxArgs object sender.txArgs = { diff --git a/src/logic/safe/store/actions/fetchSafe.ts b/src/logic/safe/store/actions/fetchSafe.ts index 0c3fc1a43c..1fa6c069f1 100644 --- a/src/logic/safe/store/actions/fetchSafe.ts +++ b/src/logic/safe/store/actions/fetchSafe.ts @@ -78,7 +78,7 @@ export const fetchSafe = // If the network has changed while the safe was being loaded, // ignore the result - if (remoteSafeInfo?.chainId !== chainId) { + if (remoteSafeInfo && remoteSafeInfo?.chainId !== chainId) { return } @@ -102,7 +102,7 @@ export const fetchSafe = } } - const owners = buildSafeOwners(remoteSafeInfo?.owners) + const owners = buildSafeOwners(remoteSafeInfo?.owners || []) return dispatch(updateSafe({ address, ...safeInfo, owners })) } diff --git a/src/logic/safe/store/actions/processTransaction.ts b/src/logic/safe/store/actions/processTransaction.ts index d3a6f852d6..8050283f3e 100644 --- a/src/logic/safe/store/actions/processTransaction.ts +++ b/src/logic/safe/store/actions/processTransaction.ts @@ -3,7 +3,7 @@ import { AnyAction } from 'redux' import { ThunkAction } from 'redux-thunk' import { Operation } from '@gnosis.pm/safe-react-gateway-sdk' -import { generateSignaturesFromTxConfirmations, getPreValidatedSignatures } from 'src/logic/safe/safeTxSigner' +import { generateSignaturesFromTxConfirmations } from 'src/logic/safe/safeTxSigner' import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions' import { AppReduxState } from 'src/store' import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' @@ -11,8 +11,6 @@ import { Dispatch, DispatchReturn } from './types' import { Confirmation } from 'src/logic/safe/store/models/types/confirmation' import { TxSender } from './createTransaction' import { logError, Errors } from 'src/logic/exceptions/CodedException' -import { getLastTransaction } from '../selectors/gatewayTransactions' -import { shouldExecuteTransaction } from './utils' interface ProcessTransactionArgs { approveAndExecute: boolean @@ -34,8 +32,8 @@ interface ProcessTransactionArgs { gasToken: string refundReceiver: string } - userAddress: string ethParameters?: Pick + preApprovingOwner?: string thresholdReached: boolean } @@ -50,6 +48,9 @@ export const processTransaction = (props: ProcessTransactionArgs): ProcessTransa const { tx, approveAndExecute } = props + // Set specific transaction being finalised + sender.txId = tx.id + const txProps = { navigateToTransactionsTab: false, notifiedTransaction: props.notifiedTransaction, @@ -72,14 +73,7 @@ export const processTransaction = (props: ProcessTransactionArgs): ProcessTransa return } - // Execute right away? - sender.isFinalization = - approveAndExecute || - (await shouldExecuteTransaction(sender.safeInstance, sender.nonce, getLastTransaction(state))) - - sender.approveAndExecute = approveAndExecute - - const preApprovingOwner = approveAndExecute && !props.thresholdReached ? props.userAddress : undefined + sender.isFinalization = approveAndExecute && !!(props.thresholdReached || props.preApprovingOwner) sender.txArgs = { ...tx, // Merge previous tx with new data @@ -88,9 +82,10 @@ export const processTransaction = (props: ProcessTransactionArgs): ProcessTransa data: txProps.txData, gasPrice: tx.gasPrice || '0', sender: sender.from, - sigs: - generateSignaturesFromTxConfirmations(tx.confirmations, preApprovingOwner) || - getPreValidatedSignatures(sender.from), + sigs: generateSignaturesFromTxConfirmations( + tx.confirmations, + approveAndExecute ? props.preApprovingOwner : undefined, + ), } sender.safeTxHash = tx.safeTxHash diff --git a/src/logic/safe/store/actions/utils.ts b/src/logic/safe/store/actions/utils.ts index b98c7b88bf..e17be2a92a 100644 --- a/src/logic/safe/store/actions/utils.ts +++ b/src/logic/safe/store/actions/utils.ts @@ -17,7 +17,7 @@ import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { logError, Errors } from 'src/logic/exceptions/CodedException' import { getRecommendedNonce } from '../../api/fetchSafeTxGasEstimation' -export const shouldExecuteTransaction = async ( +export const canExecuteCreatedTx = async ( safeInstance: GnosisSafe, nonce: string, lastTx: Transaction | null, diff --git a/src/logic/safe/store/middleware/gatewayTransactionsMiddleware.ts b/src/logic/safe/store/middleware/gatewayTransactionsMiddleware.ts index ab1a869f11..a188d3c25f 100644 --- a/src/logic/safe/store/middleware/gatewayTransactionsMiddleware.ts +++ b/src/logic/safe/store/middleware/gatewayTransactionsMiddleware.ts @@ -5,7 +5,7 @@ import { ADD_HISTORY_TRANSACTIONS } from 'src/logic/safe/store/actions/transacti import { isTransactionSummary } from 'src/logic/safe/store/models/types/gateway.d' import { removePendingTransaction } from 'src/logic/safe/store/actions/pendingTransactions' import { Dispatch } from 'src/logic/safe/store/actions/types' -import { getSafeTxHashFromId, isTxPending } from 'src/logic/safe/store/selectors/pendingTransactions' +import { isTxPending } from 'src/logic/safe/store/selectors/pendingTransactions' import { HistoryPayload } from 'src/logic/safe/store/reducer/gatewayTransactions' export const gatewayTransactionsMiddleware = @@ -22,10 +22,10 @@ export const gatewayTransactionsMiddleware = continue } - const safeTxHash = getSafeTxHashFromId(value.transaction.id) + const { id } = value.transaction - if (isTxPending(store.getState(), safeTxHash)) { - store.dispatch(removePendingTransaction({ safeTxHash })) + if (isTxPending(store.getState(), id)) { + store.dispatch(removePendingTransaction({ id })) } } diff --git a/src/logic/safe/store/middleware/notificationsMiddleware.ts b/src/logic/safe/store/middleware/notificationsMiddleware.ts index 71d8fb0df1..31aa6ad030 100644 --- a/src/logic/safe/store/middleware/notificationsMiddleware.ts +++ b/src/logic/safe/store/middleware/notificationsMiddleware.ts @@ -23,7 +23,7 @@ import { store as reduxStore } from 'src/store/index' import { HistoryPayload } from 'src/logic/safe/store/reducer/gatewayTransactions' import { history, extractSafeAddress, generateSafeRoute, ADDRESSED_ROUTE, SAFE_ROUTES } from 'src/routes/routes' import { getShortName } from 'src/config' -import { getSafeTxHashFromId, isTxPending } from 'src/logic/safe/store/selectors/pendingTransactions' +import { isTxPending } from 'src/logic/safe/store/selectors/pendingTransactions' const watchedActions = [ADD_OR_UPDATE_SAFE, ADD_QUEUED_TRANSACTIONS, ADD_HISTORY_TRANSACTIONS] @@ -106,10 +106,7 @@ const notificationsMiddleware = const safesMap = safesAsMap(state) const currentSafe = safesMap.get(safeAddress) - const hasPendingTx = transactions.some((tx) => { - const safeTxHash = getSafeTxHashFromId(tx.id) - return isTxPending(state, safeTxHash) - }) + const hasPendingTx = transactions.some(({ id }) => isTxPending(state, id)) if ( hasPendingTx || diff --git a/src/logic/safe/store/middleware/pendingTransactionsMiddleware.ts b/src/logic/safe/store/middleware/pendingTransactionsMiddleware.ts index f050d23307..f070d1456b 100644 --- a/src/logic/safe/store/middleware/pendingTransactionsMiddleware.ts +++ b/src/logic/safe/store/middleware/pendingTransactionsMiddleware.ts @@ -9,7 +9,7 @@ import { } from 'src/logic/safe/store/actions/pendingTransactions' import { PENDING_TRANSACTIONS_ID, PendingTransactionPayload } from 'src/logic/safe/store/reducer/pendingTransactions' import { Dispatch } from 'src/logic/safe/store/actions/types' -import { allPendingTxs } from 'src/logic/safe/store/selectors/pendingTransactions' +import { allPendingTxIds } from 'src/logic/safe/store/selectors/pendingTransactions' // Share updated statuses between tabs/windows // Test env and Safari don't support BroadcastChannel @@ -57,7 +57,7 @@ export const pendingTransactionsMiddleware = } const state = getState() - session.setItem(PENDING_TRANSACTIONS_ID, allPendingTxs(state)) + session.setItem(PENDING_TRANSACTIONS_ID, allPendingTxIds(state)) break } default: diff --git a/src/logic/safe/store/models/types/transaction.ts b/src/logic/safe/store/models/types/transaction.ts index 264a8322bd..598f95d4c9 100644 --- a/src/logic/safe/store/models/types/transaction.ts +++ b/src/logic/safe/store/models/types/transaction.ts @@ -17,7 +17,7 @@ export type TxArgs = { refundReceiver: string safeInstance: GnosisSafe safeTxGas: string - sender?: string + sender: string sigs: string to: string valueInWei: string diff --git a/src/logic/safe/store/reducer/pendingTransactions.ts b/src/logic/safe/store/reducer/pendingTransactions.ts index 6705fd5405..eaaa804e7d 100644 --- a/src/logic/safe/store/reducer/pendingTransactions.ts +++ b/src/logic/safe/store/reducer/pendingTransactions.ts @@ -7,13 +7,12 @@ import { _getChainId } from 'src/config' export const PENDING_TRANSACTIONS_ID = 'pendingTransactions' -type SafeTxHash = string -export type PendingTransactionsState = Record> +export type PendingTransactionsState = Record> const initialPendingTxsState = session.getItem(PENDING_TRANSACTIONS_ID) || {} export type PendingTransactionPayload = { - safeTxHash: string + id: string isBroadcast?: boolean } @@ -24,11 +23,11 @@ export const pendingTransactionsReducer = handleActions, ) => { const chainId = _getChainId() - const { safeTxHash } = action.payload + const { id } = action.payload return { ...state, - [chainId]: { ...state[chainId], [safeTxHash]: true }, + [chainId]: { ...state[chainId], [id]: true }, } }, [PENDING_TRANSACTIONS_ACTIONS.REMOVE]: ( @@ -36,10 +35,10 @@ export const pendingTransactionsReducer = handleActions, ) => { const chainId = _getChainId() - const { safeTxHash } = action.payload + const { id } = action.payload - // Omit safeTxHash from the pending transactions on current chain - const { [safeTxHash]: _, ...newChainState } = state[chainId] || {} + // Omit id from the pending transactions on current chain + const { [id]: _, ...newChainState } = state[chainId] || {} return { ...state, diff --git a/src/logic/safe/store/selectors/pendingTransactions.ts b/src/logic/safe/store/selectors/pendingTransactions.ts index aa65f2bcfe..95124c28c2 100644 --- a/src/logic/safe/store/selectors/pendingTransactions.ts +++ b/src/logic/safe/store/selectors/pendingTransactions.ts @@ -2,52 +2,49 @@ import { TransactionStatus } from '@gnosis.pm/safe-react-gateway-sdk' import { createSelector } from 'reselect' import { AppReduxState } from 'src/store' -import { - isMultiSigExecutionDetails, - LocalTransactionStatus, - Transaction, -} from 'src/logic/safe/store/models/types/gateway.d' +import { LocalTransactionStatus, Transaction } from 'src/logic/safe/store/models/types/gateway.d' import { PendingTransactionsState, PENDING_TRANSACTIONS_ID } from 'src/logic/safe/store/reducer/pendingTransactions' import { currentChainId } from 'src/logic/config/store/selectors' import { ChainId } from 'src/config/chain' +import { getTransactionByAttribute } from 'src/logic/safe/store/selectors/gatewayTransactions' -export const allPendingTxs = (state: AppReduxState): PendingTransactionsState => { +export const allPendingTxIds = (state: AppReduxState): PendingTransactionsState => { return state[PENDING_TRANSACTIONS_ID] } -const pendingTxsByChain = createSelector( - allPendingTxs, +export const pendingTxIdsByChain = createSelector( + allPendingTxIds, currentChainId, (statuses, chainId): PendingTransactionsState[ChainId] => { return statuses[chainId] }, ) -export const isTxPending = createSelector( - pendingTxsByChain, - (_: AppReduxState, safeTxHash: string) => safeTxHash, - (pendingTxs: PendingTransactionsState[ChainId], safeTxHash: string): boolean => { - return pendingTxs ? !!pendingTxs?.[safeTxHash] : false +export const pendingTxByChain = createSelector( + (state: AppReduxState) => state, + pendingTxIdsByChain, + (state: AppReduxState, pendingTxIds: PendingTransactionsState[ChainId]): Transaction | undefined => { + if (!pendingTxIds) { + return + } + + const pendingTxId = Object.keys(pendingTxIds)[0] + return getTransactionByAttribute(state, { attributeValue: pendingTxId, attributeName: 'id' }) }, ) -// @FIXME: this is a dirty hack. -// Ask backend to add safeTxHash in tx list items. -export const getSafeTxHashFromId = (id: string): string => { - return id.split('_').pop() || '' -} +export const isTxPending = createSelector( + pendingTxIdsByChain, + (_: AppReduxState, id: string) => id, + (pendingTxs: PendingTransactionsState[ChainId], id: string): boolean => { + return pendingTxs ? !!pendingTxs?.[id] : false + }, +) export const selectTxStatus = createSelector( - pendingTxsByChain, + pendingTxIdsByChain, (_: AppReduxState, tx: Transaction) => tx, (pendingTxs: PendingTransactionsState[ChainId], tx: Transaction): TransactionStatus => { - const { detailedExecutionInfo } = tx.txDetails || {} - - const safeTxHash = - detailedExecutionInfo && isMultiSigExecutionDetails(detailedExecutionInfo) - ? detailedExecutionInfo.safeTxHash - : getSafeTxHashFromId(tx.id) - - return !!pendingTxs?.[safeTxHash] ? LocalTransactionStatus.PENDING : tx.txStatus + return !!pendingTxs?.[tx.id] ? LocalTransactionStatus.PENDING : tx.txStatus }, ) diff --git a/src/logic/safe/transactions/offchainSigner/index.ts b/src/logic/safe/transactions/offchainSigner/index.ts index 46c1c7a36d..b7cbcbde57 100644 --- a/src/logic/safe/transactions/offchainSigner/index.ts +++ b/src/logic/safe/transactions/offchainSigner/index.ts @@ -33,8 +33,12 @@ const getSupportedSigners = (isHW: boolean, safeVersion: string) => { return signers } -const isKeystoneError = (err: Error): boolean => { - return err.message.startsWith('#ktek_error') +const isKeystoneError = (err: unknown): boolean => { + if (err instanceof Error) { + return err.message?.startsWith('#ktek_error') + } + + return false } export const tryOffChainSigning = async ( diff --git a/src/logic/safe/transactions/txHistory.ts b/src/logic/safe/transactions/txHistory.ts index fb96c98a3f..67b7e98aa4 100644 --- a/src/logic/safe/transactions/txHistory.ts +++ b/src/logic/safe/transactions/txHistory.ts @@ -1,14 +1,21 @@ +import { MultisigTransactionRequest, proposeTransaction, TransactionDetails } from '@gnosis.pm/safe-react-gateway-sdk' + import { GnosisSafe } from 'src/types/contracts/gnosis_safe.d' import { _getChainId } from 'src/config' import { checksumAddress } from 'src/utils/checksumAddress' -import { proposeTransaction, TransactionDetails } from '@gnosis.pm/safe-react-gateway-sdk' import { GATEWAY_URL } from 'src/utils/constants' +import { TxArgs } from '../store/models/types/transaction' + +type ProposeTxBody = Omit & { + safeInstance: GnosisSafe + data: string | number[] +} -const calculateBodyFrom = async ( - safeInstance: GnosisSafe, +const calculateBodyFrom = async ({ + safeInstance, to, - valueInWei, + value, data, operation, nonce, @@ -20,14 +27,14 @@ const calculateBodyFrom = async ( sender, origin, signature, -) => { +}: ProposeTxBody): Promise => { const safeTxHash = await safeInstance.methods - .getTransactionHash(to, valueInWei, data, operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, nonce) + .getTransactionHash(to, value, data, operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver || '', nonce) .call() return { to: checksumAddress(to), - value: valueInWei, + value, data, operation, nonce: nonce.toString(), @@ -43,10 +50,7 @@ const calculateBodyFrom = async ( } } -interface SaveTxToHistoryArgs { - safeInstance: GnosisSafe - [key: string]: any -} +type SaveTxToHistoryTypes = TxArgs & { origin?: string | null; signature?: string } export const saveTxToHistory = async ({ baseGas, @@ -63,24 +67,24 @@ export const saveTxToHistory = async ({ signature, to, valueInWei, -}: SaveTxToHistoryArgs): Promise => { +}: SaveTxToHistoryTypes): Promise => { const address = checksumAddress(safeInstance.options.address) - const body = await calculateBodyFrom( + const body = await calculateBodyFrom({ safeInstance, to, - valueInWei, + value: valueInWei, data, operation, - nonce, + nonce: nonce.toString(), safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, sender, - origin || null, + origin: origin ? origin : null, signature, - ) + }) const txDetails = await proposeTransaction(GATEWAY_URL, _getChainId(), address, body) return txDetails } diff --git a/src/logic/wallets/__tests__/ethTransactions.test.ts b/src/logic/wallets/__tests__/ethTransactions.test.ts new file mode 100644 index 0000000000..c44223417e --- /dev/null +++ b/src/logic/wallets/__tests__/ethTransactions.test.ts @@ -0,0 +1,102 @@ +import { setMaxPrioFeePerGas, getFeesPerGas } from 'src/logic/wallets/ethTransactions' + +describe('setMaxPrioFeePerGas', () => { + it('should return maxPriorityFeePerGas input if less than/equal to maxFeePerGas', () => { + expect(setMaxPrioFeePerGas(10, 20)).toEqual(10) + expect(setMaxPrioFeePerGas(20, 20)).toEqual(20) + }) + it('should return maxFeePerGas input if maxPriorityFeePerGas is greater', () => { + expect(setMaxPrioFeePerGas(30, 20)).toEqual(20) + }) +}) + +describe('getFeeHistory', () => { + const web3 = require('src/logic/wallets/getWeb3') + const web3Utils = require('web3-utils') + + beforeEach(() => { + jest.restoreAllMocks() + }) + + it('should return the default maxFeePerGas/maxPriorityFeeGas if getFeeHistory threw', async () => { + jest.spyOn(web3, 'getWeb3ReadOnly').mockImplementationOnce(() => ({ + eth: { + getFeeHistory: jest.fn(() => { + throw new Error() + }), + }, + })) + + expect(await getFeesPerGas()).toStrictEqual({ + maxFeePerGas: 3.5e9, + maxPriorityFeePerGas: 2.5e9, + }) + }) + it('should return the default maxFeePerGas/maxPriorityFeeGas if hexToNumber threw', async () => { + jest.spyOn(web3Utils, 'hexToNumber').mockImplementationOnce(() => { + throw new Error() + }) + + expect(await getFeesPerGas()).toStrictEqual({ + maxFeePerGas: 3.5e9, + maxPriorityFeePerGas: 2.5e9, + }) + }) + it('should return the default maxFeePerGas/maxPriorityFeeGas if maxPriorityFeePerGas is not a number', async () => { + jest.spyOn(web3, 'getWeb3ReadOnly').mockImplementationOnce(() => ({ + eth: { + getFeeHistory: jest.fn(() => { + return { + baseFeePerGas: ['0xc86b1ba', '0xc35e3b0'], + gasUsedRatio: [0.39920479014424254], + oldestBlock: '0x9a9e7a', + reward: [['not a hex value']], + } + }), + }, + })) + + expect(await getFeesPerGas()).toStrictEqual({ + maxFeePerGas: 3.5e9, + maxPriorityFeePerGas: 2.5e9, + }) + }) + it('should return the default maxFeePerGas/maxPriorityFeeGas if baseFeePerGas is not a number', async () => { + jest.spyOn(web3, 'getWeb3ReadOnly').mockImplementationOnce(() => ({ + eth: { + getFeeHistory: jest.fn(() => { + return { + baseFeePerGas: ['not a hex value'], + gasUsedRatio: [0.39920479014424254], + oldestBlock: '0x9a9e7a', + reward: [['0x28ef440e']], + } + }), + }, + })) + + expect(await getFeesPerGas()).toStrictEqual({ + maxFeePerGas: 3.5e9, + maxPriorityFeePerGas: 2.5e9, + }) + }) + it('should return maxFeePerGas (baseFeePerGas + maxPriorityFeePerGas)/maxPriorityFeePerGas if getFeeHistory returns a result and it was parsed correctly', async () => { + jest.spyOn(web3, 'getWeb3ReadOnly').mockImplementationOnce(() => ({ + eth: { + getFeeHistory: jest.fn(() => { + return { + baseFeePerGas: ['0x28ef440e'], + gasUsedRatio: [0.39920479014424254], + oldestBlock: '0x9a9e7a', + reward: [['0x28ef440e']], + } + }), + }, + })) + + expect(await getFeesPerGas()).toStrictEqual({ + maxFeePerGas: 1373538332, + maxPriorityFeePerGas: 686769166, + }) + }) +}) diff --git a/src/logic/wallets/__tests__/getWeb3.test.ts b/src/logic/wallets/__tests__/getWeb3.test.ts index cbf7b40228..bb9e6c0d7f 100644 --- a/src/logic/wallets/__tests__/getWeb3.test.ts +++ b/src/logic/wallets/__tests__/getWeb3.test.ts @@ -1,5 +1,5 @@ import Web3 from 'web3' -import { isTxPendingError, isSmartContractWallet } from 'src/logic/wallets/getWeb3' +import { isTxPendingError, isSmartContractWallet, getWeb3ReadOnly } from 'src/logic/wallets/getWeb3' describe('src/logic/wallets/getWeb3', () => { describe('isTxPendingError', () => { @@ -14,51 +14,44 @@ describe('src/logic/wallets/getWeb3', () => { }) }) + jest.mock('src/logic/wallets/getWeb3', () => ({ + getWeb3ReadOnly: jest.fn(), + })) + describe('isSmartContractWallet', () => { + const web3ReadOnly = getWeb3ReadOnly() const address = '0x66fb75feC6b40119e023564dF954c8794Cd876F0' it('checks if an address is a contract', async () => { - const web3Provider = { - eth: { - getCode: jest.fn(() => Promise.resolve('Solidity code')), - }, - } as unknown as Web3 - const result = await isSmartContractWallet(web3Provider, address) - expect(web3Provider.eth.getCode).toHaveBeenCalledWith(address) + web3ReadOnly.eth.getCode = jest.fn(() => Promise.resolve('Solidity code')) + + const result = await isSmartContractWallet(address) + expect(web3ReadOnly.eth.getCode).toHaveBeenCalledWith(address) expect(result).toBe(true) }) it('returns false for EoA addresses', async () => { - const web3Provider = { - eth: { - getCode: jest.fn(() => Promise.resolve('0x00000000000000000000')), - }, - } as unknown as Web3 - const result = await isSmartContractWallet(web3Provider, address) - expect(web3Provider.eth.getCode).toHaveBeenCalledWith(address) + web3ReadOnly.eth.getCode = jest.fn(() => Promise.resolve('0x00000000000000000000')) + + const result = await isSmartContractWallet(address) + expect(web3ReadOnly.eth.getCode).toHaveBeenCalledWith(address) expect(result).toBe(false) }) it('returns false for empty addresses', async () => { - const web3Provider = { - eth: { - getCode: jest.fn(() => Promise.resolve('Solidity code')), - }, - } as unknown as Web3 + web3ReadOnly.eth.getCode = jest.fn(() => Promise.resolve('Solidity code')) + const emptyAddress = '' - const result = await isSmartContractWallet(web3Provider, emptyAddress) - expect(web3Provider.eth.getCode).not.toHaveBeenCalled() + const result = await isSmartContractWallet(emptyAddress) + expect(web3ReadOnly.eth.getCode).not.toHaveBeenCalled() expect(result).toBe(false) }) it('returns false if contract code cannot be fetched', async () => { - const web3Provider = { - eth: { - getCode: jest.fn(() => Promise.reject('No code')), - }, - } as unknown as Web3 - const result = await isSmartContractWallet(web3Provider, address) - expect(web3Provider.eth.getCode).toHaveBeenCalledWith(address) + web3ReadOnly.eth.getCode = jest.fn(() => Promise.reject('No code')) + + const result = await isSmartContractWallet(address) + expect(web3ReadOnly.eth.getCode).toHaveBeenCalledWith(address) expect(result).toBe(false) }) }) diff --git a/src/logic/wallets/ethTransactions.ts b/src/logic/wallets/ethTransactions.ts index 7d8112b5be..ddd5aed00e 100644 --- a/src/logic/wallets/ethTransactions.ts +++ b/src/logic/wallets/ethTransactions.ts @@ -1,18 +1,67 @@ import { EthAdapterTransaction } from '@gnosis.pm/safe-core-sdk' +import { GasPriceOracle } from '@gnosis.pm/safe-react-gateway-sdk' import axios from 'axios' import { BigNumber } from 'bignumber.js' +import { FeeHistoryResult } from 'web3-eth' +import { hexToNumber } from 'web3-utils' + import { getSDKWeb3ReadOnly, getWeb3, getWeb3ReadOnly } from 'src/logic/wallets/getWeb3' import { getFixedGasPrice, getGasPriceOracles } from 'src/config' -import { CodedException, Errors } from '../exceptions/CodedException' -import { GasPriceOracle } from '@gnosis.pm/safe-react-gateway-sdk' +import { CodedException, Errors, logError } from 'src/logic/exceptions/CodedException' export const EMPTY_DATA = '0x' +const DEFAULT_MAX_GAS_FEE = 3.5e9 // 3.5 GWEI +const DEFAULT_MAX_PRIO_FEE = 2.5e9 // 2.5 GWEI + const fetchGasPrice = async (gasPriceOracle: GasPriceOracle): Promise => { const { uri, gasParameter, gweiFactor } = gasPriceOracle const { data: response } = await axios.get(uri) const data = response.data || response.result || response // Sometimes the data comes with a data parameter - return new BigNumber(data[gasParameter]).multipliedBy(gweiFactor).toString() + + const gasPrice = new BigNumber(data[gasParameter]).multipliedBy(gweiFactor) + if (gasPrice.isNaN()) { + throw new Error('Fetched gas price is NaN') + } + return gasPrice.toString() +} + +export const setMaxPrioFeePerGas = (maxPriorityFeePerGas: number, maxFeePerGas: number): number => { + return maxPriorityFeePerGas > maxFeePerGas ? maxFeePerGas : maxPriorityFeePerGas +} + +export const getFeesPerGas = async (): Promise<{ + maxFeePerGas: number + maxPriorityFeePerGas: number +}> => { + let blocks: FeeHistoryResult | undefined + let maxPriorityFeePerGas: number | undefined + let baseFeePerGas: number | undefined + + const web3 = getWeb3ReadOnly() + + try { + // Lastest block, 50th reward percentile + blocks = await web3.eth.getFeeHistory(1, 'latest', [50]) + + // hexToNumber can throw if not parsing a valid hex string + baseFeePerGas = hexToNumber(blocks.baseFeePerGas[0]) + maxPriorityFeePerGas = hexToNumber(blocks.reward[0][0]) + } catch (err) { + logError(Errors._618, err.message) + } + + if (!blocks || !maxPriorityFeePerGas || isNaN(maxPriorityFeePerGas) || !baseFeePerGas || isNaN(baseFeePerGas)) { + return { + maxFeePerGas: DEFAULT_MAX_GAS_FEE, + maxPriorityFeePerGas: DEFAULT_MAX_PRIO_FEE, + } + } + + return { + maxFeePerGas: baseFeePerGas + maxPriorityFeePerGas, + maxPriorityFeePerGas, + } } export const calculateGasPrice = async (): Promise => { diff --git a/src/logic/wallets/getWeb3.ts b/src/logic/wallets/getWeb3.ts index 499af40042..0f6d2f4fa3 100644 --- a/src/logic/wallets/getWeb3.ts +++ b/src/logic/wallets/getWeb3.ts @@ -79,13 +79,13 @@ export const getChainIdFrom = (web3Provider: Web3): Promise => { const isHardwareWallet = (walletName: string) => sameAddress(WALLET_PROVIDER.LEDGER, walletName) || sameAddress(WALLET_PROVIDER.TREZOR, walletName) -export const isSmartContractWallet = async (web3Provider: Web3, account: string): Promise => { +export const isSmartContractWallet = async (account: string): Promise => { if (!account) { return false } let contractCode = '' try { - contractCode = await web3Provider.eth.getCode(account) + contractCode = await getWeb3ReadOnly().eth.getCode(account) } catch (e) { // ignore } @@ -96,7 +96,7 @@ export const getProviderInfo = async (web3Instance: Web3, providerName = 'Wallet const account = (await getAccountFrom(web3Instance)) || '' const ensDomain = account ? await reverseENSLookup(account) : '' const network = await getChainIdFrom(web3Instance) - const smartContractWallet = await isSmartContractWallet(web3Instance, account) + const smartContractWallet = await isSmartContractWallet(account) const hardwareWallet = isHardwareWallet(providerName) const available = Boolean(account) @@ -147,8 +147,6 @@ export const reverseENSLookup = async (address: string): Promise => { return verifiedAddress === address ? name : '' } -export const removeTld = (name: string): string => name.replace(/\.[^.]+$/, '') - export const getContentFromENS = (name: string): Promise => web3.eth.ens.getContenthash(name) export const isTxPendingError = (err: Error): boolean => { diff --git a/src/logic/wallets/store/selectors/index.ts b/src/logic/wallets/store/selectors/index.ts index b412337e10..80946bc79e 100644 --- a/src/logic/wallets/store/selectors/index.ts +++ b/src/logic/wallets/store/selectors/index.ts @@ -12,6 +12,11 @@ export const userAccountSelector = createSelector(providerSelector, (provider: P return account || '' }) +export const userEnsSelector = createSelector(providerSelector, (provider: ProviderState): string => { + const ensName = provider.get('ensDomain') + return ensName || '' +}) + export const providerNameSelector = createSelector(providerSelector, (provider: ProviderState): string | undefined => { const name = provider.get('name') return name ? name.toLowerCase() : undefined diff --git a/src/routes/CreateSafePage/components/SafeCreationProcess.tsx b/src/routes/CreateSafePage/components/SafeCreationProcess.tsx index 7ab96ad22e..d2b51a74d2 100644 --- a/src/routes/CreateSafePage/components/SafeCreationProcess.tsx +++ b/src/routes/CreateSafePage/components/SafeCreationProcess.tsx @@ -24,6 +24,7 @@ import { FIELD_NEW_SAFE_PROXY_SALT, FIELD_NEW_SAFE_GAS_PRICE, FIELD_SAFE_OWNER_ENS_LIST, + FIELD_NEW_SAFE_GAS_MAX_PRIO_FEE, } from '../fields/createSafeFields' import { getSafeInfo } from 'src/logic/safe/utils/safeInformation' import { buildSafe } from 'src/logic/safe/store/actions/fetchSafe' @@ -109,11 +110,13 @@ function SafeCreationProcess(): ReactElement { const safeCreationSalt = safeCreationFormValues[FIELD_NEW_SAFE_PROXY_SALT] const gasLimit = safeCreationFormValues[FIELD_NEW_SAFE_GAS_LIMIT] const gasPrice = safeCreationFormValues[FIELD_NEW_SAFE_GAS_PRICE] + const gasMaxPrioFee = safeCreationFormValues[FIELD_NEW_SAFE_GAS_MAX_PRIO_FEE] const deploymentTx = getSafeDeploymentTransaction(ownerAddresses, confirmations, safeCreationSalt) const sendParams = createSendParams(userAddressAccount, { ethGasLimit: gasLimit.toString(), ethGasPriceInGWei: gasPrice, + ethMaxPrioFeeInGWei: gasMaxPrioFee.toString(), }) deploymentTx diff --git a/src/routes/CreateSafePage/fields/createSafeFields.tsx b/src/routes/CreateSafePage/fields/createSafeFields.tsx index c7157b983d..e3d7603376 100644 --- a/src/routes/CreateSafePage/fields/createSafeFields.tsx +++ b/src/routes/CreateSafePage/fields/createSafeFields.tsx @@ -7,6 +7,7 @@ export const FIELD_MAX_OWNER_NUMBER = 'maxOwnerNumber' export const FIELD_NEW_SAFE_PROXY_SALT = 'safeCreationSalt' export const FIELD_NEW_SAFE_GAS_LIMIT = 'gasLimit' export const FIELD_NEW_SAFE_GAS_PRICE = 'gasPrice' +export const FIELD_NEW_SAFE_GAS_MAX_PRIO_FEE = 'gasMaxPrioFee' export const FIELD_NEW_SAFE_CREATION_TX_HASH = 'safeCreationTxHash' export type OwnerFieldItem = { @@ -24,6 +25,7 @@ export type CreateSafeFormValues = { [FIELD_NEW_SAFE_PROXY_SALT]: number [FIELD_NEW_SAFE_GAS_LIMIT]: number [FIELD_NEW_SAFE_GAS_PRICE]: string + [FIELD_NEW_SAFE_GAS_MAX_PRIO_FEE]: number [FIELD_NEW_SAFE_CREATION_TX_HASH]?: string } diff --git a/src/routes/CreateSafePage/steps/NameNewSafeStep.tsx b/src/routes/CreateSafePage/steps/NameNewSafeStep.tsx index c221f7d061..6a1c6443a8 100644 --- a/src/routes/CreateSafePage/steps/NameNewSafeStep.tsx +++ b/src/routes/CreateSafePage/steps/NameNewSafeStep.tsx @@ -18,7 +18,7 @@ import { } from '../fields/createSafeFields' import { useStepper } from 'src/components/Stepper/stepperContext' import NetworkLabel from 'src/components/NetworkLabel/NetworkLabel' -import { removeTld, reverseENSLookup } from 'src/logic/wallets/getWeb3' +import { reverseENSLookup } from 'src/logic/wallets/getWeb3' export const nameNewSafeStepLabel = 'Name' @@ -45,10 +45,9 @@ function NameNewSafeStep(): ReactElement { owners.map(async ({ addressFieldName }) => { const address = formValues[addressFieldName] const ensName = await reverseENSLookup(address) - const ensDomain = removeTld(ensName) return { address, - name: ensDomain, + name: ensName, } }), ) diff --git a/src/routes/CreateSafePage/steps/OwnersAndConfirmationsNewSafeStep.tsx b/src/routes/CreateSafePage/steps/OwnersAndConfirmationsNewSafeStep.tsx index ed47105989..e29d0e2c0a 100644 --- a/src/routes/CreateSafePage/steps/OwnersAndConfirmationsNewSafeStep.tsx +++ b/src/routes/CreateSafePage/steps/OwnersAndConfirmationsNewSafeStep.tsx @@ -38,7 +38,7 @@ import { import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper' import { currentNetworkAddressBookAsMap } from 'src/logic/addressBook/store/selectors' import NetworkLabel from 'src/components/NetworkLabel/NetworkLabel' -import { removeTld, reverseENSLookup } from 'src/logic/wallets/getWeb3' +import { reverseENSLookup } from 'src/logic/wallets/getWeb3' export const ownersAndConfirmationsNewSafeStepLabel = 'Owners and Confirmations' @@ -89,9 +89,8 @@ function OwnersAndConfirmationsNewSafeStep(): ReactElement { const getENSName = async (address: string): Promise => { const ensName = await reverseENSLookup(address) - const ensDomain = removeTld(ensName) const newOwnersWithENSName: Record = Object.assign(ownersWithENSName, { - [address]: ensDomain, + [address]: ensName, }) createSafeForm.change(FIELD_SAFE_OWNER_ENS_LIST, newOwnersWithENSName) } diff --git a/src/routes/CreateSafePage/steps/ReviewNewSafeStep.tsx b/src/routes/CreateSafePage/steps/ReviewNewSafeStep.tsx index 0a988c7525..1495694e73 100644 --- a/src/routes/CreateSafePage/steps/ReviewNewSafeStep.tsx +++ b/src/routes/CreateSafePage/steps/ReviewNewSafeStep.tsx @@ -19,6 +19,7 @@ import { FIELD_NEW_SAFE_THRESHOLD, FIELD_SAFE_OWNER_ENS_LIST, FIELD_SAFE_OWNERS_LIST, + FIELD_NEW_SAFE_GAS_MAX_PRIO_FEE, } from '../fields/createSafeFields' import { getExplorerInfo, getNativeCurrency } from 'src/config' import { useEstimateSafeCreationGas } from 'src/logic/hooks/useEstimateSafeCreationGas' @@ -52,7 +53,7 @@ function ReviewNewSafeStep(): ReactElement | null { const safeCreationSalt = createSafeFormValues[FIELD_NEW_SAFE_PROXY_SALT] const ownerAddresses = owners.map(({ addressFieldName }) => createSafeFormValues[addressFieldName]) - const { gasCostFormatted, gasLimit, gasPrice } = useEstimateSafeCreationGas({ + const { gasCostFormatted, gasLimit, gasPrice, gasMaxPrioFee } = useEstimateSafeCreationGas({ addresses: ownerAddresses, numOwners: numberOfOwners, safeCreationSalt, @@ -62,7 +63,8 @@ function ReviewNewSafeStep(): ReactElement | null { useEffect(() => { createSafeForm.change(FIELD_NEW_SAFE_GAS_LIMIT, gasLimit) createSafeForm.change(FIELD_NEW_SAFE_GAS_PRICE, gasPrice) - }, [gasLimit, gasPrice, createSafeForm]) + createSafeForm.change(FIELD_NEW_SAFE_GAS_MAX_PRIO_FEE, gasMaxPrioFee) + }, [gasLimit, gasPrice, createSafeForm, gasMaxPrioFee]) return ( diff --git a/src/routes/LoadSafePage/steps/LoadSafeAddressStep.tsx b/src/routes/LoadSafePage/steps/LoadSafeAddressStep.tsx index a80dabd6e8..30e34f04c9 100644 --- a/src/routes/LoadSafePage/steps/LoadSafeAddressStep.tsx +++ b/src/routes/LoadSafePage/steps/LoadSafeAddressStep.tsx @@ -30,7 +30,7 @@ import { import NetworkLabel from 'src/components/NetworkLabel/NetworkLabel' import { getLoadSafeName } from '../fields/utils' import { currentChainId } from 'src/logic/config/store/selectors' -import { removeTld, reverseENSLookup } from 'src/logic/wallets/getWeb3' +import { reverseENSLookup } from 'src/logic/wallets/getWeb3' export const loadSafeAddressStepLabel = 'Name and address' @@ -74,8 +74,7 @@ function LoadSafeAddressStep(): ReactElement { const ownersWithENSName = await Promise.all( owners.map(async ({ value: address }) => { const ensName = await reverseENSLookup(address) - const ensDomain = removeTld(ensName) - return makeAddressBookEntry({ address, name: ensDomain, chainId }) + return makeAddressBookEntry({ address, name: ensName, chainId }) }), ) diff --git a/src/routes/index.tsx b/src/routes/index.tsx index cd0df60745..5082c1cbb5 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,31 +1,26 @@ import React from 'react' import { Loader } from '@gnosis.pm/safe-react-components' -import { useEffect } from 'react' import { useSelector } from 'react-redux' -import { matchPath, Redirect, Route, Switch, useLocation } from 'react-router-dom' +import { Redirect, Route, Switch, useLocation } from 'react-router-dom' import { LoadingContainer } from 'src/components/LoaderContainer' -import { useAnalytics } from 'src/utils/googleAnalytics' import { lastViewedSafe } from 'src/logic/currentSession/store/selectors' import { generateSafeRoute, - getPrefixedSafeAddressSlug, LOAD_SPECIFIC_SAFE_ROUTE, OPEN_SAFE_ROUTE, ADDRESSED_ROUTE, SAFE_ROUTES, WELCOME_ROUTE, - hasPrefixedSafeAddressInUrl, ROOT_ROUTE, LOAD_SAFE_ROUTE, getNetworkRootRoutes, - TRANSACTION_ID_SLUG, + extractSafeAddress, } from './routes' import { getShortName } from 'src/config' import { setChainId } from 'src/logic/config/utils' -import { isDeeplinkedTx } from './safe/components/Transactions/TxList/utils' -import { useAddressedRouteKey } from './safe/container/hooks/useAddressedRouteKey' import { setChainIdFromUrl } from 'src/utils/history' +import useGaEvents from 'src/logic/hooks/useGaEvents' const Welcome = React.lazy(() => import('./welcome/Welcome')) const CreateSafePage = React.lazy(() => import('./CreateSafePage/CreateSafePage')) @@ -34,33 +29,11 @@ const SafeContainer = React.lazy(() => import('./safe/container')) const Routes = (): React.ReactElement => { const location = useLocation() - const { pathname, search } = location + const { pathname } = location const defaultSafe = useSelector(lastViewedSafe) - const { trackPage } = useAnalytics() - // Component key that changes when addressed route slug changes - const { key } = useAddressedRouteKey() - - // Google Analytics - useEffect(() => { - let trackedPath = pathname - - // Anonymize safe address - if (hasPrefixedSafeAddressInUrl()) { - trackedPath = trackedPath.replace(getPrefixedSafeAddressSlug(), 'SAFE_ADDRESS') - } - - // Anonymize deeplinked transaction - if (isDeeplinkedTx()) { - const match = matchPath(pathname, { - path: SAFE_ROUTES.TRANSACTIONS_SINGULAR, - }) - - trackedPath = trackedPath.replace(match?.params[TRANSACTION_ID_SLUG], 'TRANSACTION_ID') - } - - trackPage(trackedPath + search) - }, [pathname, search, trackPage]) + // Anonymize and track page views + useGaEvents() return ( @@ -132,7 +105,9 @@ const Routes = (): React.ReactElement => { render={() => { // Routes with a shortName prefix const validShortName = setChainIdFromUrl(pathname) - return validShortName ? : + // Safe address is used as a key to re-render the entire SafeContainer + const safeAddress = extractSafeAddress() + return validShortName ? : }} /> diff --git a/src/routes/routes.test.tsx b/src/routes/routes.test.tsx index 1debe26f7d..f0112816c1 100644 --- a/src/routes/routes.test.tsx +++ b/src/routes/routes.test.tsx @@ -2,7 +2,6 @@ import { generateSafeRoute, generatePrefixedAddressRoutes, getPrefixedSafeAddressSlug, - hasPrefixedSafeAddressInUrl, SAFE_ROUTES, WELCOME_ROUTE, extractPrefixedSafeAddress, @@ -13,7 +12,6 @@ import { import { Route, Switch } from 'react-router' import { render } from 'src/utils/test-utils' import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' -import Root from 'src/components/Root' const validSafeAddress = '0xF5A2915982BC8b0dEDda9cEF79297A83081Fe88f' @@ -96,26 +94,6 @@ describe('extractPrefixedSafeAddress', () => { }) }) -describe('hasPrefixedSafeAddressInUrl', () => { - it('returns true if the chain-specific address exists in the URL', () => { - history.push(`/eth:${validSafeAddress}`) - - expect(hasPrefixedSafeAddressInUrl()).toBe(true) - }) - - it('returns false if the chain-specific address in the URL is malformed', () => { - history.push(`/n0TaR3aLSHORTname:4xIOHAS89asasd`) - - expect(hasPrefixedSafeAddressInUrl()).toBe(false) - }) - - it("returns false if the chain-specific address does't exist in the URL", () => { - history.push(WELCOME_ROUTE) - - expect(hasPrefixedSafeAddressInUrl()).toBe(false) - }) -}) - // Not testing extractShortChainName or extractSafeAddress because // they return from { [key]: extractPrefixedSafeAddress()[key] } diff --git a/src/routes/routes.ts b/src/routes/routes.ts index edac6983c1..90bfce7e9b 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -95,14 +95,6 @@ export const extractPrefixedSafeAddress = ( } } -export const hasPrefixedSafeAddressInUrl = (): boolean => { - const match = matchPath(history.location.pathname, { - // Routes that have addresses in URL - path: [ADDRESSED_ROUTE, LOAD_SPECIFIC_SAFE_ROUTE], - }) - return !!match?.params?.[SAFE_ADDRESS_SLUG] -} - export const extractShortChainName = (): ShortName => extractPrefixedSafeAddress().shortName export const extractSafeAddress = (): string => extractPrefixedSafeAddress().safeAddress diff --git a/src/routes/safe/components/Apps/communicator.ts b/src/routes/safe/components/Apps/communicator.ts index d4a4c4a747..f4dd43e1b7 100644 --- a/src/routes/safe/components/Apps/communicator.ts +++ b/src/routes/safe/components/Apps/communicator.ts @@ -8,7 +8,7 @@ import { MessageFormatter, RequestId, } from '@gnosis.pm/safe-apps-sdk' -import { trackError, Errors } from 'src/logic/exceptions/CodedException' +import { Errors, logError } from 'src/logic/exceptions/CodedException' import { SafeApp } from './types' type MessageHandler = ( @@ -35,6 +35,10 @@ class AppCommunicator { } private isValidMessage = (msg: SDKMessageEvent): boolean => { + if (msg.data.hasOwnProperty('isCookieEnabled')) { + return true + } + // @ts-expect-error .parent doesn't exist on some possible types const sentFromIframe = msg.source.parent === window.parent const knownMethod = Object.values(Methods).includes(msg.data.method) @@ -71,7 +75,7 @@ class AppCommunicator { } } catch (err) { this.send(err.message, msg.data.id, true) - trackError(Errors._901, err.message, { + logError(Errors._901, err.message, { contexts: { safeApp: this.app, request: msg.data, diff --git a/src/routes/safe/components/Apps/components/AppFrame.tsx b/src/routes/safe/components/Apps/components/AppFrame.tsx index d9b93abc78..7128c773fd 100644 --- a/src/routes/safe/components/Apps/components/AppFrame.tsx +++ b/src/routes/safe/components/Apps/components/AppFrame.tsx @@ -32,6 +32,8 @@ import { logError, Errors } from 'src/logic/exceptions/CodedException' import { addressBookEntryName } from 'src/logic/addressBook/store/selectors' import { useSignMessageModal } from '../hooks/useSignMessageModal' import { SignMessageModal } from './SignMessageModal' +import { useThirdPartyCookies } from '../hooks/useThirdPartyCookies' +import { ThirdPartyCookiesWarning } from './ThirdPartyCookiesWarning' const AppWrapper = styled.div` display: flex; @@ -100,6 +102,7 @@ const AppFrame = ({ appUrl }: Props): ReactElement => { const [isLoadingSlow, setIsLoadingSlow] = useState(false) const errorTimer = useRef() const [, setAppLoadError] = useState(false) + const { thirdPartyCookiesDisabled, setThirdPartyCookiesDisabled } = useThirdPartyCookies() useEffect(() => { const clearTimeouts = () => { @@ -307,6 +310,7 @@ const AppFrame = ({ appUrl }: Props): ReactElement => { return ( + {thirdPartyCookiesDisabled && setThirdPartyCookiesDisabled(false)} />} {appIsLoading && ( diff --git a/src/routes/safe/components/Apps/components/ConfirmTxModal/DecodedTxDetail.tsx b/src/routes/safe/components/Apps/components/ConfirmTxModal/DecodedTxDetail.tsx index f7e5e1a69e..0b3a797954 100644 --- a/src/routes/safe/components/Apps/components/ConfirmTxModal/DecodedTxDetail.tsx +++ b/src/routes/safe/components/Apps/components/ConfirmTxModal/DecodedTxDetail.tsx @@ -4,28 +4,26 @@ import { DecodedDataParameterValue, DecodedDataResponse } from '@gnosis.pm/safe- import { getNativeCurrency } from 'src/config' import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' -import { md, lg } from 'src/theme/variables' -import ModalTitle from 'src/components/ModalTitle' -import Hairline from 'src/components/layout/Hairline' import { BasicTxInfo, getParameterElement } from 'src/components/DecodeTxs' +import { DecodedTxDetailType } from 'src/routes/safe/components/Apps/components/ConfirmTxModal/index' const Container = styled.div` - max-width: 480px; - padding: ${md} ${lg}; word-break: break-word; + + & > div:last-child { + margin: 0; + } ` -function isDataDecodedParameterValue(arg: any): arg is DecodedDataParameterValue { - return arg.operation !== undefined +export function isDataDecodedParameterValue(arg: DecodedTxDetailType): arg is DecodedDataParameterValue { + return arg ? arg.hasOwnProperty('operation') : false } type Props = { - hideDecodedTxData: () => void - onClose: () => void decodedTxData: DecodedDataParameterValue | DecodedDataResponse } -export const DecodedTxDetail = ({ hideDecodedTxData, onClose, decodedTxData: tx }: Props): ReactElement => { +export const DecodedTxDetail = ({ decodedTxData: tx }: Props): ReactElement => { const nativeCurrency = getNativeCurrency() let body // If we are dealing with a multiSend @@ -45,17 +43,5 @@ export const DecodedTxDetail = ({ hideDecodedTxData, onClose, decodedTxData: tx body = <>{tx.parameters.map((p, index) => getParameterElement(p, index))} } - return ( - <> - - - - - {body} - - ) + return {body} } diff --git a/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx b/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx index b8f7d7b0d4..83a8689d51 100644 --- a/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx +++ b/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx @@ -4,7 +4,6 @@ import { ReactElement, useEffect, useMemo, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import styled from 'styled-components' import { toBN } from 'web3-utils' -import { DecodedDataResponse } from '@gnosis.pm/safe-react-gateway-sdk' import { createTransaction } from 'src/logic/safe/store/actions/createTransaction' import { getMultisendContractAddress } from 'src/logic/contracts/safeContracts' @@ -22,9 +21,11 @@ import Hairline from 'src/components/layout/Hairline' import Divider from 'src/components/Divider' import PrefixedEthHashInfo from 'src/components/PrefixedEthHashInfo' -import { ConfirmTxModalProps, DecodedTxDetail } from '.' +import { ConfirmTxModalProps, DecodedTxDetailType } from '.' import { grantedSelector } from 'src/routes/safe/container/selector' import { TxModalWrapper } from 'src/routes/safe/components/Transactions/helpers/TxModalWrapper' +import Paragraph from 'src/components/layout/Paragraph' +import Row from 'src/components/layout/Row' const Container = styled.div` max-width: 480px; @@ -32,7 +33,7 @@ const Container = styled.div` ` const DecodeTxsWrapper = styled.div` - margin: 24px -24px; + margin: 0; ` const StyledBlock = styled(Block)` @@ -51,8 +52,6 @@ const StyledBlock = styled(Block)` type Props = ConfirmTxModalProps & { onReject: () => void - showDecodedTxData: (decodedTxDetails: DecodedTxDetail) => void - hidden: boolean // used to prevent re-rendering the modal each time a tx is inspected } const parseTxValue = (value: string | number): string => { @@ -65,15 +64,13 @@ export const ReviewConfirm = ({ safeAddress, ethBalance, safeName, - hidden, onUserConfirm, onClose, onReject, requestId, - showDecodedTxData, }: Props): ReactElement => { const isMultiSend = txs.length > 1 - const [decodedData, setDecodedData] = useState(null) + const [decodedData, setDecodedData] = useState() const dispatch = useDispatch() const nativeCurrency = getNativeCurrency() const explorerUrl = getExplorerInfo(safeAddress) @@ -95,12 +92,18 @@ export const ReviewConfirm = ({ // Decode tx data. useEffect(() => { + let isCurrent = true const decodeTxData = async () => { const res = await fetchTxDecoder(txData) - setDecodedData(res) + if (res && isCurrent) { + setDecodedData(res) + } } decodeTxData() + return () => { + isCurrent = false + } }, [txData]) const handleUserConfirmation = (safeTxHash: string): void => { @@ -133,21 +136,34 @@ export const ReviewConfirm = ({ return ( -