diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ce638504a0..86efad8a6c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,8 @@ name: Build/Release Desktop app # this will help you specify where to run -on: - push: - branches: - - master +on: + workflow_dispatch env: REACT_APP_BLOCKNATIVE_KEY: ${{ secrets.REACT_APP_BLOCKNATIVE_KEY }} @@ -29,6 +27,19 @@ jobs: - name: Check out Git repository uses: actions/checkout@v2 + # Add cache for yarn directory + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v2 + id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: Patch node gyp on windows to support Visual Studio 2019 if: startsWith(matrix.os, 'windows') shell: powershell @@ -69,21 +80,21 @@ jobs: release: ${{ startsWith(github.ref, 'refs/tags/v') }} - name: 'Upload Artifacts OSX' - if: contains(github.ref, 'master') && startsWith(matrix.os, 'macos') + if: contains(github.ref, 'development') && startsWith(matrix.os, 'macos') uses: actions/upload-artifact@v2 with: name: Desktop OSX path: ./dist/Safe[ ]Multisig*.dmg - name: 'Upload Artifacts Linux' - if: contains(github.ref, 'master') && startsWith(matrix.os, 'ubuntu') + if: contains(github.ref, 'development') && startsWith(matrix.os, 'ubuntu') uses: actions/upload-artifact@v2 with: name: Desktop Linux path: ./dist/Safe[ ]Multisig*.AppImage - name: 'Upload Artifacts Windows' - if: contains(github.ref, 'master') && startsWith(matrix.os, 'windows') + if: contains(github.ref, 'development') && startsWith(matrix.os, 'windows') uses: actions/upload-artifact@v2 with: name: Desktop Windows diff --git a/.travis.yml b/.travis.yml index dcf6ad4bc8..0ee0a3544a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,16 @@ if: (branch = development) OR (branch = master) OR (type = pull_request) OR (tag IS present) sudo: required -dist: xenial -services: - - docker +dist: bionic language: node_js node_js: - '12' os: - linux -env: - global: - - DOCKER_COMPOSE_VERSION=1.22.0 matrix: include: + - env: + - REACT_APP_ENV='production' + if: tag IS present - env: - REACT_APP_NETWORK='mainnet' - STAGING_BUCKET_NAME=${STAGING_MAINNET_BUCKET_NAME} @@ -28,10 +26,12 @@ before_install: - sudo apt-get update - sudo apt-get -y install python-pip python-dev libusb-1.0-0-dev - pip install awscli --upgrade --user - # Install truffle - - yarn global add truffle script: - - bash ./config/travis/build.sh + - yarn lint:check + - yarn prettier:check + - yarn test:coverage + - yarn build + #- bash ./config/travis/build.sh after_success: # Pull Request - Deploy it to a review environment # Travis doesn't do deploy step with pull requests builds diff --git a/package.json b/package.json index 1c70054e30..8d62c911f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "safe-react", - "version": "2.11.1", + "version": "2.12.0", "description": "Allowing crypto users manage funds in a safer way", "website": "https://github.com/gnosis/safe-react#readme", "bugs": { @@ -38,7 +38,7 @@ "release": "electron-builder --mac --linux --windows -p always", "start-mainnet": "REACT_APP_NETWORK=mainnet yarn start", "start": "react-app-rewired start", - "test": "NODE_ENV=test && react-app-rewired test --env=jsdom", + "test": "react-app-rewired test --env=jsdom", "test:coverage": "yarn test --coverage --watchAll=false", "coveralls": "cat ./coverage/lcov.info | coveralls", "storybook": "start-storybook -p 9009 -s public", @@ -164,21 +164,21 @@ ] }, "dependencies": { - "@gnosis.pm/safe-apps-sdk": "https://github.com/gnosis/safe-apps-sdk.git#development", + "@gnosis.pm/safe-apps-sdk": "0.4.0", "@gnosis.pm/safe-contracts": "1.1.1-dev.2", "@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#1bf397f", "@gnosis.pm/util-contracts": "2.0.6", - "@ledgerhq/hw-transport-node-hid": "5.19.1", + "@ledgerhq/hw-transport-node-hid": "5.22.0", "@material-ui/core": "4.11.0", "@material-ui/icons": "4.9.1", "@material-ui/lab": "4.0.0-alpha.56", "@openzeppelin/contracts": "3.1.0", "async-sema": "^3.1.0", - "axios": "0.19.2", + "axios": "0.20.0", "bignumber.js": "9.0.0", - "bnc-onboard": "1.11.1", + "bnc-onboard": "1.13.1", "classnames": "^2.2.6", - "concurrently": "^5.2.0", + "concurrently": "^5.3.0", "connected-react-router": "6.8.0", "coveralls": "^3.1.0", "currency-flags": "2.1.2", @@ -190,20 +190,20 @@ "eth-sig-util": "^2.5.3", "ethereum-blockies-base64": "^1.0.2", "ethereumjs-abi": "0.6.8", - "exponential-backoff": "^3.0.1", + "exponential-backoff": "^3.1.0", "express": "^4.17.1", "final-form": "^4.20.1", "final-form-calculate": "^1.3.1", "history": "4.10.1", - "immortal-db": "^1.0.3", + "immortal-db": "^1.1.0", "immutable": "^4.0.0-rc.12", "js-cookie": "^2.2.1", "lodash.debounce": "^4.0.8", "lodash.memoize": "^4.1.2", - "material-ui-search-bar": "^1.0.0-beta.13", + "material-ui-search-bar": "^1.0.0", "notistack": "https://github.com/gnosis/notistack.git#v0.9.4", - "open": "^7.1.0", - "polished": "3.6.5", + "open": "^7.2.0", + "polished": "3.6.7", "qrcode.react": "1.0.0", "query-string": "6.13.1", "react": "16.13.1", @@ -215,7 +215,7 @@ "react-qr-reader": "^2.2.1", "react-redux": "7.2.1", "react-router-dom": "5.2.0", - "react-scripts": "^3.4.1", + "react-scripts": "^3.4.3", "react-window": "^1.8.5", "recompose": "^0.30.0", "redux": "4.0.5", @@ -223,7 +223,7 @@ "redux-thunk": "^2.3.0", "reselect": "^4.0.0", "semver": "7.3.2", - "styled-components": "^5.1.1", + "styled-components": "^5.2.0", "truffle-contract": "4.0.31", "web3": "1.2.9", "web3-core": "^1.2.11", @@ -236,45 +236,43 @@ "@storybook/addons": "^5.3.19", "@storybook/preset-create-react-app": "^3.1.4", "@storybook/react": "^5.3.19", - "@testing-library/jest-dom": "5.11.2", - "@testing-library/react": "10.4.8", - "@testing-library/user-event": "12.1.0", + "@testing-library/jest-dom": "5.11.4", + "@testing-library/react": "10.4.9", "@typechain/web3-v1": "^1.0.0", "@types/history": "4.6.2", - "@types/jest": "^26.0.9", + "@types/jest": "^26.0.14", "@types/lodash.memoize": "^4.1.6", - "@types/node": "14.6.0", - "@types/react": "^16.9.47", + "@types/node": "14.11.2", + "@types/react": "^16.9.49", "@types/react-dom": "^16.9.6", "@types/react-redux": "^7.1.9", "@types/react-router-dom": "^5.1.5", - "@types/styled-components": "^5.1.2", + "@types/styled-components": "^5.1.3", "@typescript-eslint/eslint-plugin": "3.9.1", "@typescript-eslint/parser": "3.9.1", "autoprefixer": "9.8.6", "cross-env": "^7.0.2", "dotenv": "^8.2.0", "dotenv-expand": "^5.1.0", - "electron": "7.2.4", + "electron": "9.3.0", "electron-builder": "22.8.0", - "electron-notarize": "0.3.0", + "electron-notarize": "1.0.0", "eslint": "6.8.0", "eslint-config-prettier": "6.11.0", "eslint-plugin-import": "2.22.0", "eslint-plugin-jsx-a11y": "^6.3.1", "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-react": "^7.20.5", + "eslint-plugin-react": "^7.20.6", "eslint-plugin-sort-destructure-keys": "1.3.5", "ethereumjs-abi": "0.6.8", "husky": "^4.2.5", - "lint-staged": "10.2.11", + "lint-staged": "10.4.0", "node-sass": "^4.14.1", - "prettier": "2.0.5", + "prettier": "2.1.2", "react-app-rewired": "^2.1.6", "react-docgen-typescript-loader": "^3.7.2", - "truffle": "5.1.36", "typechain": "^2.0.0", "typescript": "3.9.7", - "wait-on": "5.1.0" + "wait-on": "5.2.0" } } diff --git a/public/electron.js b/public/electron.js index 965ab61f8f..d55fa0bcf0 100644 --- a/public/electron.js +++ b/public/electron.js @@ -142,6 +142,10 @@ process.on('uncaughtException',function(error){ app.userAgentFallback = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) old-airport-include/1.0.0 Chrome Electron/7.1.7 Safari/537.36'; +// We have one non-context-aware module in node_modules/usb. This is used by @ledgerhq/hw-transport-node-hid +// This type of modules will be impossible to use after electron 10 +app.allowRendererProcessReuse = false; + app.commandLine.appendSwitch('ignore-certificate-errors'); app.on("ready", () =>{ // Hide the menu diff --git a/src/assets/icons/info.svg b/src/assets/icons/info.svg new file mode 100644 index 0000000000..7becafe5c7 --- /dev/null +++ b/src/assets/icons/info.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/App/ModalReceive.tsx b/src/components/App/ReceiveModal.tsx similarity index 59% rename from src/components/App/ModalReceive.tsx rename to src/components/App/ReceiveModal.tsx index 30df66688b..535a50bd7f 100644 --- a/src/components/App/ModalReceive.tsx +++ b/src/components/App/ReceiveModal.tsx @@ -1,9 +1,8 @@ import IconButton from '@material-ui/core/IconButton' -import { withStyles } from '@material-ui/core/styles' +import { createStyles, makeStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import QRCode from 'qrcode.react' import * as React from 'react' -import { useSelector } from 'react-redux' import CopyBtn from 'src/components/CopyBtn' import EtherscanBtn from 'src/components/EtherscanBtn' @@ -14,72 +13,79 @@ import Col from 'src/components/layout/Col' import Hairline from 'src/components/layout/Hairline' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' -import { safeNameSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { lg, md, screenSm, secondaryText, sm } from 'src/theme/variables' import { copyToClipboard } from 'src/utils/clipboard' -const styles = () => ({ - heading: { - padding: `${md} ${lg}`, - justifyContent: 'space-between', - maxHeight: '75px', - boxSizing: 'border-box', - }, - close: { - height: lg, - width: lg, - fill: secondaryText, - }, - qrContainer: { - backgroundColor: '#fff', - padding: md, - borderRadius: '6px', - border: `1px solid ${secondaryText}`, - }, - annotation: { - margin: lg, - marginBottom: 0, - }, - safeName: { - margin: `${md} 0`, - }, - buttonRow: { - height: '84px', - justifyContent: 'center', - '& > button': { - fontFamily: 'Averta', - fontSize: md, - boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)', +const useStyles = makeStyles( + createStyles({ + heading: { + padding: `${md} ${lg}`, + justifyContent: 'space-between', + maxHeight: '75px', + boxSizing: 'border-box', }, - }, - addressContainer: { - flexDirection: 'column', - justifyContent: 'center', - margin: `${lg} 0`, + close: { + height: lg, + width: lg, + fill: secondaryText, + }, + qrContainer: { + backgroundColor: '#fff', + padding: md, + borderRadius: '6px', + border: `1px solid ${secondaryText}`, + }, + annotation: { + margin: lg, + marginBottom: 0, + }, + safeName: { + margin: `${md} 0`, + }, + buttonRow: { + height: '84px', + justifyContent: 'center', + '& > button': { + fontFamily: 'Averta', + fontSize: md, + boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)', + }, + }, + addressContainer: { + flexDirection: 'column', + justifyContent: 'center', + margin: `${lg} 0`, - [`@media (min-width: ${screenSm}px)`]: { - flexDirection: 'row', + [`@media (min-width: ${screenSm}px)`]: { + flexDirection: 'row', + }, }, - }, - address: { - marginLeft: sm, - marginRight: sm, - maxWidth: '70%', - overflowWrap: 'break-word', + address: { + marginLeft: sm, + marginRight: sm, + maxWidth: '70%', + overflowWrap: 'break-word', - [`@media (min-width: ${screenSm}px)`]: { - maxWidth: 'none', + [`@media (min-width: ${screenSm}px)`]: { + maxWidth: 'none', + }, }, - }, -}) + }), +) + +type Props = { + onClose: () => void + safeAddress: string + safeName: string +} + +const ReceiveModal = ({ onClose, safeAddress, safeName }: Props) => { + const classes = useStyles() -const Receive = ({ classes, onClose }) => { - const safeAddress = useSelector(safeParamAddressFromStateSelector) - const safeName = useSelector(safeNameSelector) return ( <> - + Receive funds @@ -122,4 +128,4 @@ const Receive = ({ classes, onClose }) => { ) } -export default withStyles(styles as any)(Receive) +export default ReceiveModal diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 7d889c67e1..ee90992af8 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -30,7 +30,7 @@ import { currentCurrencySelector, safeFiatBalancesTotalSelector } from 'src/logi import { formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount' import { grantedSelector } from 'src/routes/safe/container/selector' -import Receive from './ModalReceive' +import Receive from './ReceiveModal' import { useSidebarItems } from 'src/components/AppLayout/Sidebar/useSidebarItems' const notificationStyles = { @@ -79,7 +79,8 @@ const App: React.FC = ({ children }) => { const sendFunds = safeActionsState.sendFunds as { isOpen: boolean; selectedToken: string } const formattedTotalBalance = currentSafeBalance ? formatAmountInUsFormat(currentSafeBalance) : '' - const balance = !!formattedTotalBalance && !!currentCurrency ? `${formattedTotalBalance} ${currentCurrency}` : null + const balance = + !!formattedTotalBalance && !!currentCurrency ? `${formattedTotalBalance} ${currentCurrency}` : undefined useEffect(() => { if (matchSafe?.isExact) { @@ -133,14 +134,16 @@ const App: React.FC = ({ children }) => { selectedToken={sendFunds.selectedToken} /> - - - + {safeAddress && safeName && ( + + + + )} diff --git a/src/components/AppLayout/AppLayout.stories.tsx b/src/components/AppLayout/AppLayout.stories.tsx index 778e0dadbb..008684823f 100644 --- a/src/components/AppLayout/AppLayout.stories.tsx +++ b/src/components/AppLayout/AppLayout.stories.tsx @@ -50,7 +50,7 @@ export const Base = (): React.ReactElement => { safeAddress="0xEE63624cC4Dd2355B16b35eFaadF3F7450A9438B" safeName="someName" granted={true} - balance={null} + balance={undefined} onToggleSafeList={() => console.log} onReceiveClick={() => console.log} onNewTransactionClick={() => console.log} diff --git a/src/components/AppLayout/Header/index.tsx b/src/components/AppLayout/Header/index.tsx index fb9bd4654f..5636fbd6dc 100644 --- a/src/components/AppLayout/Header/index.tsx +++ b/src/components/AppLayout/Header/index.tsx @@ -50,7 +50,7 @@ const HeaderComponent = (): React.ReactElement => { } const getProviderInfoBased = () => { - if (!loaded) { + if (!loaded || !provider) { return } diff --git a/src/components/AppLayout/Sidebar/SafeHeader/index.tsx b/src/components/AppLayout/Sidebar/SafeHeader/index.tsx index 82df813fcc..9d3c2f3f6f 100644 --- a/src/components/AppLayout/Sidebar/SafeHeader/index.tsx +++ b/src/components/AppLayout/Sidebar/SafeHeader/index.tsx @@ -79,10 +79,10 @@ const UnStyledButton = styled.button` ` type Props = { - address: string | null - safeName: string + address: string | undefined + safeName: string | undefined granted: boolean - balance: string | null + balance: string | undefined onToggleSafeList: () => void onReceiveClick: () => void onNewTransactionClick: () => void @@ -102,9 +102,7 @@ const SafeHeader = ({ -
- -
+ diff --git a/src/components/AppLayout/Sidebar/index.tsx b/src/components/AppLayout/Sidebar/index.tsx index f59c3b0e37..673b994dd6 100644 --- a/src/components/AppLayout/Sidebar/index.tsx +++ b/src/components/AppLayout/Sidebar/index.tsx @@ -10,14 +10,14 @@ const StyledDivider = styled(Divider)` ` const HelpContainer = styled.div` - height: 58px; + margin-top: auto; ` const HelpCenterLink = styled.a` height: 30px; width: 166px; - padding: 10px 0 0 16px; - margin: 10px 0px; + padding: 6px 0 0 16px; + margin: 14px 0px; text-decoration: none; display: block; @@ -38,9 +38,9 @@ const HelpCenterLink = styled.a` } ` type Props = { - safeAddress: string | null - safeName: string | null - balance: string | null + safeAddress?: string + safeName?: string + balance?: string granted: boolean onToggleSafeList: () => void onReceiveClick: () => void @@ -57,34 +57,32 @@ const Sidebar = ({ onToggleSafeList, onReceiveClick, onNewTransactionClick, -}: Props): React.ReactElement => { - return ( - <> - +}: Props): React.ReactElement => ( + <> + - {items.length ? ( - <> - - - - ) : null} - - + {items.length ? ( + <> - - - - - - ) -} + + + ) : null} + + + + + + + + +) export default Sidebar diff --git a/src/components/AppLayout/index.tsx b/src/components/AppLayout/index.tsx index 20bd8c8907..4ac72c9e21 100644 --- a/src/components/AppLayout/index.tsx +++ b/src/components/AppLayout/index.tsx @@ -28,18 +28,15 @@ const GridTopbarWrapper = styled.nav` const GridSidebarWrapper = styled.aside` width: 200px; - padding: 8px; + padding: 62px 8px 0 8px; height: 100%; background-color: ${({ theme }) => theme.colors.white}; border-right: 2px solid ${({ theme }) => theme.colors.separator}; display: flex; flex-direction: column; box-sizing: border-box; + position: fixed; grid-area: sidebar; - - div:last-of-type { - margin-top: auto; - } ` const GridBodyWrapper = styled.section` @@ -60,9 +57,9 @@ export const FooterWrapper = styled.footer` type Props = { sidebarItems: ListItemType[] - safeAddress: string | null - safeName: string | null - balance: string | null + safeAddress: string | undefined + safeName: string | undefined + balance: string | undefined granted: boolean onToggleSafeList: () => void onReceiveClick: () => void diff --git a/src/components/List/index.tsx b/src/components/List/index.tsx index adec3dacda..f2b7f0cd2b 100644 --- a/src/components/List/index.tsx +++ b/src/components/List/index.tsx @@ -70,6 +70,21 @@ const useStyles = makeStyles((theme: Theme) => width: '100%', maxWidth: 200, backgroundColor: theme.palette.background.paper, + overflowX: 'auto', + margin: '8px 0 -4px 0', + '&::-webkit-scrollbar': { + width: '0.5em', + }, + '&::-webkit-scrollbar-track': { + boxShadow: 'inset 0 0 6px rgba(0, 0, 0, 0.3)', + webkitBoxShadow: 'inset 0 0 6px rgba(0, 0, 0, 0.3)', + borderRadius: '20px', + }, + '&::-webkit-scrollbar-thumb': { + backgroundColor: 'darkgrey', + outline: '1px solid #dadada', + borderRadius: '20px', + }, }, nested: { paddingLeft: theme.spacing(3), diff --git a/src/components/SafeListSidebar/SafeList/index.tsx b/src/components/SafeListSidebar/SafeList/index.tsx index efe0f07070..23ecd43609 100644 --- a/src/components/SafeListSidebar/SafeList/index.tsx +++ b/src/components/SafeListSidebar/SafeList/index.tsx @@ -82,7 +82,7 @@ const useStyles = makeStyles({ }) type Props = { - currentSafe: string | null + currentSafe: string | undefined defaultSafe: DefaultSafe safes: SafeRecord[] onSafeClick: () => void diff --git a/src/components/SafeListSidebar/index.tsx b/src/components/SafeListSidebar/index.tsx index 19fe5ff58f..8aeeb77b3f 100644 --- a/src/components/SafeListSidebar/index.tsx +++ b/src/components/SafeListSidebar/index.tsx @@ -1,7 +1,7 @@ +import React, { useEffect, useMemo, useState } from 'react' import Drawer from '@material-ui/core/Drawer' import SearchIcon from '@material-ui/icons/Search' import SearchBar from 'material-ui-search-bar' -import * as React from 'react' import { connect } from 'react-redux' import SafeList from './SafeList' @@ -16,12 +16,11 @@ import Link from 'src/components/layout/Link' import Row from 'src/components/layout/Row' import { WELCOME_ADDRESS } from 'src/routes/routes' import setDefaultSafe from 'src/logic/safe/store/actions/setDefaultSafe' +import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' import { defaultSafeSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { AppReduxState } from 'src/store' -const { useEffect, useMemo, useState } = React - export const SafeListSidebarContext = React.createContext({ isOpen: false, toggleSidebar: () => {}, @@ -39,12 +38,7 @@ const SafeListSidebar = ({ children, currentSafe, defaultSafe, safes, setDefault const [isOpen, setIsOpen] = useState(false) const [filter, setFilter] = useState('') const classes = useSidebarStyles() - - useEffect(() => { - setTimeout(() => { - setFilter('') - }, 300) - }, [isOpen]) + const { trackEvent } = useAnalytics() const searchClasses = { input: classes.searchInput, @@ -54,6 +48,9 @@ const SafeListSidebar = ({ children, currentSafe, defaultSafe, safes, setDefault } const toggleSidebar = () => { + if (!isOpen) { + trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Safe List Sidebar' }) + } setIsOpen((prevIsOpen) => !prevIsOpen) } @@ -73,6 +70,12 @@ const SafeListSidebar = ({ children, currentSafe, defaultSafe, safes, setDefault const filteredSafes = useMemo(() => filterBy(filter, safes), [safes, filter]) + useEffect(() => { + setTimeout(() => { + setFilter('') + }, 300) + }, [isOpen]) + return ( { +export const cellWidth = (width?: string | number): CellWidth | undefined => { if (!width) { return undefined } diff --git a/src/logic/addressBook/model/addressBook.ts b/src/logic/addressBook/model/addressBook.ts index 732027d033..8575448fc6 100644 --- a/src/logic/addressBook/model/addressBook.ts +++ b/src/logic/addressBook/model/addressBook.ts @@ -1,17 +1,17 @@ -import { Record, RecordOf } from 'immutable' - -export interface AddressBookEntryProps { +export type AddressBookEntry = { address: string name: string - isOwner: boolean } -export type AddressBookEntryRecord = RecordOf - -export const makeAddressBookEntry = Record({ - address: '', - name: '', - isOwner: false, +export const makeAddressBookEntry = ({ + address = '', + name = '', +}: { + address: string + name?: string +}): AddressBookEntry => ({ + address, + name, }) -export type AddressBookEntry = RecordOf +export type AddressBookState = AddressBookEntry[] diff --git a/src/logic/addressBook/store/actions/addAddressBook.ts b/src/logic/addressBook/store/actions/addAddressBook.ts deleted file mode 100644 index 7f524bbdc1..0000000000 --- a/src/logic/addressBook/store/actions/addAddressBook.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createAction } from 'redux-actions' - -export const ADD_ADDRESS_BOOK = 'ADD_ADDRESS_BOOK' - -export const addAddressBook = createAction(ADD_ADDRESS_BOOK, (addressBook, safeAddress) => ({ - addressBook, - safeAddress, -})) diff --git a/src/logic/addressBook/store/actions/addAddressBookEntry.ts b/src/logic/addressBook/store/actions/addAddressBookEntry.ts index 76041d6780..b3d80b5084 100644 --- a/src/logic/addressBook/store/actions/addAddressBookEntry.ts +++ b/src/logic/addressBook/store/actions/addAddressBookEntry.ts @@ -1,7 +1,22 @@ import { createAction } from 'redux-actions' +import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook' export const ADD_ENTRY = 'ADD_ENTRY' -export const addAddressBookEntry = createAction(ADD_ENTRY, (entry) => ({ - entry, -})) +type addAddressBookEntryOptions = { + notifyEntryUpdate: boolean +} + +export const addAddressBookEntry = createAction( + ADD_ENTRY, + (entry: AddressBookEntry, options: addAddressBookEntryOptions) => { + let notifyEntryUpdate = true + if (options) { + notifyEntryUpdate = options.notifyEntryUpdate + } + return { + entry, + shouldAvoidUpdatesNotifications: !notifyEntryUpdate, + } + }, +) diff --git a/src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry.ts b/src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry.ts index f88f9b50ed..f19a00783b 100644 --- a/src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry.ts +++ b/src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry.ts @@ -1,8 +1,8 @@ import { createAction } from 'redux-actions' +import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook' export const ADD_OR_UPDATE_ENTRY = 'ADD_OR_UPDATE_ENTRY' -export const addOrUpdateAddressBookEntry = createAction(ADD_OR_UPDATE_ENTRY, (entryAddress, entry) => ({ - entryAddress, +export const addOrUpdateAddressBookEntry = createAction(ADD_OR_UPDATE_ENTRY, (entry: AddressBookEntry) => ({ entry, })) diff --git a/src/logic/addressBook/store/actions/loadAddressBook.ts b/src/logic/addressBook/store/actions/loadAddressBook.ts index 6486d1cb72..d887d198c3 100644 --- a/src/logic/addressBook/store/actions/loadAddressBook.ts +++ b/src/logic/addressBook/store/actions/loadAddressBook.ts @@ -1,7 +1,8 @@ import { createAction } from 'redux-actions' +import { AddressBookState } from 'src/logic/addressBook/model/addressBook' export const LOAD_ADDRESS_BOOK = 'LOAD_ADDRESS_BOOK' -export const loadAddressBook = createAction(LOAD_ADDRESS_BOOK, (addressBook) => ({ +export const loadAddressBook = createAction(LOAD_ADDRESS_BOOK, (addressBook: AddressBookState) => ({ addressBook, })) diff --git a/src/logic/addressBook/store/actions/loadAddressBookFromStorage.ts b/src/logic/addressBook/store/actions/loadAddressBookFromStorage.ts index 0e40bae602..c4315e3cf9 100644 --- a/src/logic/addressBook/store/actions/loadAddressBookFromStorage.ts +++ b/src/logic/addressBook/store/actions/loadAddressBookFromStorage.ts @@ -1,29 +1,17 @@ -import { List } from 'immutable' - import { loadAddressBook } from 'src/logic/addressBook/store/actions/loadAddressBook' import { buildAddressBook } from 'src/logic/addressBook/store/reducer/addressBook' import { getAddressBookFromStorage } from 'src/logic/addressBook/utils' -import { safesListSelector } from 'src/logic/safe/store/selectors' +import { Dispatch } from 'redux' -const loadAddressBookFromStorage = () => async (dispatch, getState) => { +const loadAddressBookFromStorage = () => async (dispatch: Dispatch): Promise => { try { - const state = getState() let storedAdBk = await getAddressBookFromStorage() if (!storedAdBk) { storedAdBk = [] } - let addressBook = buildAddressBook(storedAdBk) - // Fetch all the current safes, in case that we don't have a safe on the adbk, we add it - const safes = safesListSelector(state) - const adbkEntries = addressBook.keySeq().toArray() - safes.forEach((safe) => { - const { address } = safe - const found = adbkEntries.includes(address) - if (!found) { - addressBook = addressBook.set(address, List([])) - } - }) + const addressBook = buildAddressBook(storedAdBk) + dispatch(loadAddressBook(addressBook)) } catch (err) { // eslint-disable-next-line diff --git a/src/logic/addressBook/store/actions/removeAddressBookEntry.ts b/src/logic/addressBook/store/actions/removeAddressBookEntry.ts index a241e1c121..16571f471e 100644 --- a/src/logic/addressBook/store/actions/removeAddressBookEntry.ts +++ b/src/logic/addressBook/store/actions/removeAddressBookEntry.ts @@ -2,6 +2,6 @@ import { createAction } from 'redux-actions' export const REMOVE_ENTRY = 'REMOVE_ENTRY' -export const removeAddressBookEntry = createAction(REMOVE_ENTRY, (entryAddress) => ({ +export const removeAddressBookEntry = createAction(REMOVE_ENTRY, (entryAddress: string) => ({ entryAddress, })) diff --git a/src/logic/addressBook/store/actions/saveAndUpdateAddressBook.ts b/src/logic/addressBook/store/actions/saveAndUpdateAddressBook.ts deleted file mode 100644 index 960ee92bfe..0000000000 --- a/src/logic/addressBook/store/actions/saveAndUpdateAddressBook.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' -import { updateAddressBookEntry } from 'src/logic/addressBook/store/actions/updateAddressBookEntry' -import { saveAddressBook } from 'src/logic/addressBook/utils' - -const saveAndUpdateAddressBook = (addressBook) => async (dispatch) => { - try { - dispatch(updateAddressBookEntry(makeAddressBookEntry(addressBook))) - await saveAddressBook(addressBook) - } catch (err) { - // eslint-disable-next-line - console.error('Error while loading active tokens from storage:', err) - } -} - -export default saveAndUpdateAddressBook diff --git a/src/logic/addressBook/store/actions/updateAddressBookEntry.ts b/src/logic/addressBook/store/actions/updateAddressBookEntry.ts index 38f8263acb..a426f812aa 100644 --- a/src/logic/addressBook/store/actions/updateAddressBookEntry.ts +++ b/src/logic/addressBook/store/actions/updateAddressBookEntry.ts @@ -1,7 +1,8 @@ import { createAction } from 'redux-actions' +import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook' export const UPDATE_ENTRY = 'UPDATE_ENTRY' -export const updateAddressBookEntry = createAction(UPDATE_ENTRY, (entry) => ({ +export const updateAddressBookEntry = createAction(UPDATE_ENTRY, (entry: AddressBookEntry) => ({ entry, })) diff --git a/src/logic/addressBook/store/middleware/addressBookMiddleware.ts b/src/logic/addressBook/store/middleware/addressBookMiddleware.ts index 9765be48e5..fb7075dd90 100644 --- a/src/logic/addressBook/store/middleware/addressBookMiddleware.ts +++ b/src/logic/addressBook/store/middleware/addressBookMiddleware.ts @@ -2,7 +2,7 @@ import { ADD_ENTRY } from 'src/logic/addressBook/store/actions/addAddressBookEnt import { ADD_OR_UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry' import { REMOVE_ENTRY } from 'src/logic/addressBook/store/actions/removeAddressBookEntry' import { UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/updateAddressBookEntry' -import { addressBookMapSelector } from 'src/logic/addressBook/store/selectors' +import { addressBookSelector } from 'src/logic/addressBook/store/selectors' import { saveAddressBook } from 'src/logic/addressBook/utils' import { enhanceSnackbarForAction, getNotificationsFromTxType } from 'src/logic/notifications' import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar' @@ -16,15 +16,15 @@ const addressBookMiddleware = (store) => (next) => async (action) => { if (watchedActions.includes(action.type)) { const state = store.getState() const { dispatch } = store - const addressBook = addressBookMapSelector(state) - if (addressBook) { + const addressBook = addressBookSelector(state) + if (addressBook.length) { await saveAddressBook(addressBook) } switch (action.type) { case ADD_ENTRY: { - const { isOwner } = action.payload.entry - if (!isOwner) { + const { shouldAvoidUpdatesNotifications } = action.payload + if (!shouldAvoidUpdatesNotifications) { const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.ADDRESSBOOK_NEW_ENTRY) dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded))) } diff --git a/src/logic/addressBook/store/reducer/addressBook.ts b/src/logic/addressBook/store/reducer/addressBook.ts index ea1a46d704..ad23f4fdf2 100644 --- a/src/logic/addressBook/store/reducer/addressBook.ts +++ b/src/logic/addressBook/store/reducer/addressBook.ts @@ -1,130 +1,73 @@ -import { List, Map } from 'immutable' import { handleActions } from 'redux-actions' -import { AddressBookEntry, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' -import { ADD_ADDRESS_BOOK } from 'src/logic/addressBook/store/actions/addAddressBook' +import { AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' import { ADD_ENTRY } from 'src/logic/addressBook/store/actions/addAddressBookEntry' import { ADD_OR_UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry' import { LOAD_ADDRESS_BOOK } from 'src/logic/addressBook/store/actions/loadAddressBook' import { REMOVE_ENTRY } from 'src/logic/addressBook/store/actions/removeAddressBookEntry' import { UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/updateAddressBookEntry' -import { getAddressesListFromAdbk } from 'src/logic/addressBook/utils' -import { sameAddress } from 'src/logic/wallets/ethAddresses' import { checksumAddress } from 'src/utils/checksumAddress' +import { getValidAddressBookName } from 'src/logic/addressBook/utils' export const ADDRESS_BOOK_REDUCER_ID = 'addressBook' -export type AddressBookCollection = List -export type AddressBookState = Map> - -export const buildAddressBook = (storedAdbk) => { - let addressBookBuilt = Map([]) - Object.entries(storedAdbk).forEach((adbkProps: any) => { - const safeAddress = checksumAddress(adbkProps[0]) - const adbkRecords = adbkProps[1].map(makeAddressBookEntry) - const adbkSafeEntries = List(adbkRecords) - addressBookBuilt = addressBookBuilt.set(safeAddress, adbkSafeEntries) +export const buildAddressBook = (storedAddressBook: AddressBookState): AddressBookState => { + return storedAddressBook.map((addressBookEntry) => { + const { address, name } = addressBookEntry + return makeAddressBookEntry({ address: checksumAddress(address), name }) }) - return addressBookBuilt } export default handleActions( { [LOAD_ADDRESS_BOOK]: (state, action) => { const { addressBook } = action.payload - return state.set('addressBook', addressBook) - }, - [ADD_ADDRESS_BOOK]: (state, action) => { - const { addressBook, safeAddress } = action.payload - // Adds the address book if it does not exists - const found = state.getIn(['addressBook', safeAddress]) - if (!found) { - return state.setIn(['addressBook', safeAddress], addressBook) - } - return state + return addressBook }, [ADD_ENTRY]: (state, action) => { const { entry } = action.payload - // Adds the entry to all the safes (if it does not already exists) - const newState = state.withMutations((map) => { - const adbkMap = map.get('addressBook') - - if (adbkMap) { - adbkMap.keySeq().forEach((safeAddress) => { - const safeAddressBook = state.getIn(['addressBook', safeAddress]) + const entryFound = state.find((oldEntry) => oldEntry.address === entry.address) - if (safeAddressBook) { - const adbkAddressList = getAddressesListFromAdbk(safeAddressBook) - const found = adbkAddressList.includes(entry.address) - if (!found) { - const updatedSafeAdbkList = safeAddressBook.push(entry) - map.setIn(['addressBook', safeAddress], updatedSafeAdbkList) - } - } - }) - } - }) - return newState + // Only adds entries with valid names + const validName = getValidAddressBookName(entry.name) + if (!entryFound && validName) { + state.push(entry) + } + return state }, [UPDATE_ENTRY]: (state, action) => { const { entry } = action.payload - - // Updates the entry from all the safes - const newState = state.withMutations((map) => { - map - .get('addressBook') - .keySeq() - .forEach((safeAddress) => { - const entriesList = state.getIn(['addressBook', safeAddress]) - const entryIndex = entriesList.findIndex((entryItem) => sameAddress(entryItem.address, entry.address)) - const updatedEntriesList = entriesList.set(entryIndex, entry) - map.setIn(['addressBook', safeAddress], updatedEntriesList) - }) - }) - - return newState + const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entry.address) + if (entryIndex >= 0) { + state[entryIndex] = entry + } + return state }, [REMOVE_ENTRY]: (state, action) => { const { entryAddress } = action.payload - // Removes the entry from all the safes - const newState = state.withMutations((map) => { - map - .get('addressBook') - .keySeq() - .forEach((safeAddress) => { - const entriesList = state.getIn(['addressBook', safeAddress]) - const entryIndex = entriesList.findIndex((entry) => sameAddress(entry.address, entryAddress)) - const updatedEntriesList = entriesList.remove(entryIndex) - map.setIn(['addressBook', safeAddress], updatedEntriesList) - }) - }) - return newState + const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entryAddress) + state.splice(entryIndex, 1) + return state }, [ADD_OR_UPDATE_ENTRY]: (state, action) => { - const { entry, entryAddress } = action.payload + const { entry } = action.payload - // Adds or Updates the entry to all the safes - return state.withMutations((map) => { - const addressBook = map.get('addressBook') - if (addressBook) { - addressBook.keySeq().forEach((safeAddress) => { - const safeAddressBook = state.getIn(['addressBook', safeAddress]) - const entryIndex = safeAddressBook.findIndex((entryItem) => sameAddress(entryItem.address, entryAddress)) + // Only updates entries with valid names + const validName = getValidAddressBookName(entry.name) + if (!validName) { + return state + } + + const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entry.address) - if (entryIndex !== -1) { - const updatedEntriesList = safeAddressBook.update(entryIndex, (currentEntry) => currentEntry.merge(entry)) - map.setIn(['addressBook', safeAddress], updatedEntriesList) - } else { - const updatedSafeAdbkList = safeAddressBook.push(makeAddressBookEntry(entry)) - map.setIn(['addressBook', safeAddress], updatedSafeAdbkList) - } - }) - } - }) + if (entryIndex >= 0) { + state[entryIndex] = entry + } else { + state.push(entry) + } + return state }, }, - Map({ - addressBook: Map({}), - }), + [], ) diff --git a/src/logic/addressBook/store/reducer/types/addressBook.d.ts b/src/logic/addressBook/store/reducer/types/addressBook.d.ts deleted file mode 100644 index 6f2f4c7aef..0000000000 --- a/src/logic/addressBook/store/reducer/types/addressBook.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { AddressBookEntryRecord, AddressBookEntryProps } from 'src/logic/addressBook/model/addressBook' -import { Map, List } from 'immutable' - -export interface AddressBookReducerState { - addressBook: AddressBookMap -} - -interface AddressBookMapSerialized { - [key: string]: AddressBookEntryProps -} - -interface AddressBookReducerStateSerialized extends AddressBookReducerState { - addressBook: Record -} - -export interface AddressBookMap extends Map { - toJS(): AddressBookMapSerialized - get(key: string, notSetValue: unknown): List -} - -export interface AddressBookReducerMap extends Map { - toJS(): AddressBookReducerStateSerialized - get(key: K): AddressBookReducerState[K] -} diff --git a/src/logic/addressBook/store/selectors/index.ts b/src/logic/addressBook/store/selectors/index.ts index 973b40a6eb..7460909f62 100644 --- a/src/logic/addressBook/store/selectors/index.ts +++ b/src/logic/addressBook/store/selectors/index.ts @@ -1,35 +1,22 @@ import { AppReduxState } from 'src/store' -import { List } from 'immutable' + import { createSelector } from 'reselect' import { ADDRESS_BOOK_REDUCER_ID } from 'src/logic/addressBook/store/reducer/addressBook' -import { AddressBookMap } from 'src/logic/addressBook/store/reducer/types/addressBook.d' -import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' -export const addressBookMapSelector = (state: AppReduxState): AddressBookMap => - state[ADDRESS_BOOK_REDUCER_ID].get('addressBook') +import { AddressBookState } from 'src/logic/addressBook/model/addressBook' -export const getAddressBook = createSelector( - addressBookMapSelector, - safeParamAddressFromStateSelector, - (addressBook, safeAddress) => { - let result = List([]) - if (addressBook) { - result = addressBook.get(safeAddress, List()) - } - return result - }, -) +export const addressBookSelector = (state: AppReduxState): AddressBookState => state[ADDRESS_BOOK_REDUCER_ID] -export const getNameFromAddressBook = createSelector( - getAddressBook, +export const getNameFromAddressBookSelector = createSelector( + addressBookSelector, (_, address) => address, (addressBook, address) => { const adbkEntry = addressBook.find((addressBookItem) => addressBookItem.address === address) + if (adbkEntry) { return adbkEntry.name } - return 'UNKNOWN' }, ) diff --git a/src/logic/addressBook/utils/__tests__/addressBookUtils.test.ts b/src/logic/addressBook/utils/__tests__/addressBookUtils.test.ts new file mode 100644 index 0000000000..4541c2f795 --- /dev/null +++ b/src/logic/addressBook/utils/__tests__/addressBookUtils.test.ts @@ -0,0 +1,312 @@ +import { List } from 'immutable' +import { + checkIfEntryWasDeletedFromAddressBook, + getAddressBookFromStorage, + getAddressesListFromAddressBook, + getNameFromAddressBook, + getOwnersWithNameFromAddressBook, + isValidAddressBookName, + migrateOldAddressBook, + OldAddressBookEntry, + OldAddressBookType, + saveAddressBook, +} from 'src/logic/addressBook/utils/index' +import { buildAddressBook } from 'src/logic/addressBook/store/reducer/addressBook' +import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' + +const getMockAddressBookEntry = (address: string, name: string = 'test'): AddressBookEntry => + makeAddressBookEntry({ + address, + name, + }) + +const getMockOldAddressBookEntry = ({ address = '', name = '', isOwner = false }): OldAddressBookEntry => { + return { + address, + name, + isOwner, + } +} + +describe('getAddressesListFromAdbk', () => { + const entry1 = getMockAddressBookEntry('123456', 'test1') + const entry2 = getMockAddressBookEntry('78910', 'test2') + const entry3 = getMockAddressBookEntry('4781321', 'test3') + + it('It should returns the list of addresses within the addressBook given a safeAddressBook', () => { + // given + const safeAddressBook = [entry1, entry2, entry3] + const expectedResult = [entry1.address, entry2.address, entry3.address] + + // when + const result = getAddressesListFromAddressBook(safeAddressBook) + + // then + expect(result).toStrictEqual(expectedResult) + }) +}) + +describe('getNameFromSafeAddressBook', () => { + const entry1 = getMockAddressBookEntry('123456', 'test1') + const entry2 = getMockAddressBookEntry('78910', 'test2') + const entry3 = getMockAddressBookEntry('4781321', 'test3') + it('It should returns the user name given a safeAddressBook and an user account', () => { + // given + const safeAddressBook = [entry1, entry2, entry3] + const expectedResult = entry2.name + + // when + const result = getNameFromAddressBook(safeAddressBook, entry2.address) + + // then + expect(result).toBe(expectedResult) + }) +}) + +describe('getOwnersWithNameFromAddressBook', () => { + const entry1 = getMockAddressBookEntry('123456', 'test1') + const entry2 = getMockAddressBookEntry('78910', 'test2') + const entry3 = getMockAddressBookEntry('4781321', 'test3') + it('It should returns the list of owners with their names given a safeAddressBook and a list of owners', () => { + // given + const safeAddressBook = [entry1, entry2, entry3] + const ownerList = List([ + { address: entry1.address, name: '' }, + { address: entry2.address, name: '' }, + ]) + const expectedResult = List([ + { address: entry1.address, name: entry1.name }, + { address: entry2.address, name: entry2.name }, + ]) + + // when + const result = getOwnersWithNameFromAddressBook(safeAddressBook, ownerList) + + // then + expect(result).toStrictEqual(expectedResult) + }) +}) + +jest.mock('src/utils/storage/index') +describe('saveAddressBook', () => { + const mockAdd1 = '0x696fd93D725d84acfFf6c62a1fe8C94E1c9E934A' + const mockAdd2 = '0x2C7aC78b01Be0FC66AD29b684ffAb0C93B381D00' + const mockAdd3 = '0x537BD452c3505FC07bA242E437bD29D4E1DC9126' + const entry1 = getMockAddressBookEntry(mockAdd1, 'test1') + const entry2 = getMockAddressBookEntry(mockAdd2, 'test2') + const entry3 = getMockAddressBookEntry(mockAdd3, 'test3') + afterAll(() => { + jest.unmock('src/utils/storage/index') + }) + it('It should save a given addressBook to the localStorage', async () => { + // given + const addressBook: AddressBookState = [entry1, entry2, entry3] + + // when + await saveAddressBook(addressBook) + + const storageUtils = require('src/utils/storage/index') + const spy = storageUtils.loadFromStorage.mockImplementationOnce(() => JSON.stringify(addressBook)) + + const storedAddressBook = await getAddressBookFromStorage() + + // @ts-ignore + let result = buildAddressBook(storedAddressBook) + + // then + expect(result).toStrictEqual(addressBook) + expect(spy).toHaveBeenCalled() + }) +}) + +describe('migrateOldAddressBook', () => { + const safeAddress1 = '0x696fd93D725d84acfFf6c62a1fe8C94E1c9E934A' + const safeAddress2 = '0x2C7aC78b01Be0FC66AD29b684ffAb0C93B381D00' + const mockAdd1 = '0x9163c2F4452E3399CB60AAf737231Af87548DA91' + const mockAdd2 = '0xC4e446Da9C3D37385C86488294C6758c4e25dbD8' + + it('It should receive an addressBook in old format and return the same addressBook in new format', () => { + // given + const entry1 = getMockOldAddressBookEntry({ name: 'test1', address: mockAdd1 }) + const entry2 = getMockOldAddressBookEntry({ name: 'test2', address: mockAdd2 }) + + const oldAddressBook: OldAddressBookType = { + [safeAddress1]: [entry1], + [safeAddress2]: [entry2], + } + + const expectedEntry1 = getMockAddressBookEntry(mockAdd1, 'test1') + const expectedEntry2 = getMockAddressBookEntry(mockAdd2, 'test2') + const expectedResult = [expectedEntry1, expectedEntry2] + + // when + const result = migrateOldAddressBook(oldAddressBook) + + // then + expect(result).toStrictEqual(expectedResult) + }) +}) + +describe('getAddressBookFromStorage', () => { + const safeAddress1 = '0x696fd93D725d84acfFf6c62a1fe8C94E1c9E934A' + const safeAddress2 = '0x2C7aC78b01Be0FC66AD29b684ffAb0C93B381D00' + const mockAdd1 = '0x9163c2F4452E3399CB60AAf737231Af87548DA91' + const mockAdd2 = '0xC4e446Da9C3D37385C86488294C6758c4e25dbD8' + beforeAll(() => { + jest.mock('src/utils/storage/index') + }) + afterAll(() => { + jest.unmock('src/utils/storage/index') + }) + it('It should return null if no addressBook in storage', async () => { + // given + const expectedResult = null + const storageUtils = require('src/utils/storage/index') + const spy = storageUtils.loadFromStorage.mockImplementationOnce(() => null) + + // when + const result = await getAddressBookFromStorage() + + // then + expect(result).toStrictEqual(expectedResult) + expect(spy).toHaveBeenCalled() + }) + it('It should return migrated addressBook if old addressBook in storage', async () => { + // given + const expectedEntry1 = getMockAddressBookEntry(mockAdd1, 'test1') + const expectedEntry2 = getMockAddressBookEntry(mockAdd2, 'test2') + const entry1 = getMockOldAddressBookEntry({ name: 'test1', address: mockAdd1 }) + const entry2 = getMockOldAddressBookEntry({ name: 'test2', address: mockAdd2 }) + const oldAddressBook: OldAddressBookType = { + [safeAddress1]: [entry1], + [safeAddress2]: [entry2], + } + const expectedResult = [expectedEntry1, expectedEntry2] + + const storageUtils = require('src/utils/storage/index') + const spy = storageUtils.loadFromStorage.mockImplementationOnce(() => oldAddressBook) + + // when + const result = await getAddressBookFromStorage() + + // then + expect(result).toStrictEqual(expectedResult) + expect(spy).toHaveBeenCalled() + }) + it('It should return addressBook if addressBook in storage', async () => { + // given + const expectedEntry1 = getMockAddressBookEntry(mockAdd1, 'test1') + const expectedEntry2 = getMockAddressBookEntry(mockAdd2, 'test2') + + const expectedResult = [expectedEntry1, expectedEntry2] + + const storageUtils = require('src/utils/storage/index') + const spy = storageUtils.loadFromStorage.mockImplementationOnce(() => JSON.stringify(expectedResult)) + + // when + const result = await getAddressBookFromStorage() + + // then + expect(result).toStrictEqual(expectedResult) + expect(spy).toHaveBeenCalled() + }) +}) + +describe('isValidAddressBookName', () => { + it('It should return false if given a blacklisted name like UNKNOWN', () => { + // given + const addressNameInput = 'UNKNOWN' + + const expectedResult = false + + // when + const result = isValidAddressBookName(addressNameInput) + + // then + expect(result).toStrictEqual(expectedResult) + }) + it('It should return false if given a blacklisted name like MY WALLET', () => { + // given + const addressNameInput = 'MY WALLET' + + const expectedResult = false + + // when + const result = isValidAddressBookName(addressNameInput) + + // then + expect(result).toStrictEqual(expectedResult) + }) + it('It should return false if given a blacklisted name like OWNER #', () => { + // given + const addressNameInput = 'OWNER #' + + const expectedResult = false + + // when + const result = isValidAddressBookName(addressNameInput) + + // then + expect(result).toStrictEqual(expectedResult) + }) + it('It should return true if the given address name is valid', () => { + // given + const addressNameInput = 'User' + + const expectedResult = true + + // when + const result = isValidAddressBookName(addressNameInput) + + // then + expect(result).toEqual(expectedResult) + }) +}) + +describe('checkIfEntryWasDeletedFromAddressBook', () => { + const mockAdd1 = '0x696fd93D725d84acfFf6c62a1fe8C94E1c9E934A' + const mockAdd2 = '0x2C7aC78b01Be0FC66AD29b684ffAb0C93B381D00' + const mockAdd3 = '0x537BD452c3505FC07bA242E437bD29D4E1DC9126' + const entry1 = getMockAddressBookEntry(mockAdd1, 'test1') + const entry2 = getMockAddressBookEntry(mockAdd2, 'test2') + const entry3 = getMockAddressBookEntry(mockAdd3, 'test3') + it('It should return true if a given entry was deleted from addressBook', () => { + // given + const addressBookEntry = entry1 + const addressBook: AddressBookState = [entry2, entry3] + const safeAlreadyLoaded = true + const expectedResult = true + + // when + const result = checkIfEntryWasDeletedFromAddressBook(addressBookEntry, addressBook, safeAlreadyLoaded) + + // then + expect(result).toEqual(expectedResult) + }) + it('It should return false if a given entry was not deleted from addressBook', () => { + // given + const addressBookEntry = entry1 + const addressBook: AddressBookState = [entry1, entry2, entry3] + const safeAlreadyLoaded = true + const expectedResult = false + + // when + const result = checkIfEntryWasDeletedFromAddressBook(addressBookEntry, addressBook, safeAlreadyLoaded) + + // then + expect(result).toEqual(expectedResult) + }) + it('It should return false if the safe was not already loaded', () => { + // given + const addressBookEntry = entry1 + const addressBook: AddressBookState = [entry1, entry2, entry3] + const safeAlreadyLoaded = false + const expectedResult = false + + // when + const result = checkIfEntryWasDeletedFromAddressBook(addressBookEntry, addressBook, safeAlreadyLoaded) + + // then + expect(result).toEqual(expectedResult) + }) +}) diff --git a/src/logic/addressBook/utils/index.ts b/src/logic/addressBook/utils/index.ts index 22cd11e83f..fed359a9d7 100644 --- a/src/logic/addressBook/utils/index.ts +++ b/src/logic/addressBook/utils/index.ts @@ -1,47 +1,140 @@ import { List } from 'immutable' import { loadFromStorage, saveToStorage } from 'src/utils/storage' -import { AddressBookEntryProps } from './../model/addressBook' +import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' import { SafeOwner } from 'src/logic/safe/store/models/safe' +import { sameAddress } from 'src/logic/wallets/ethAddresses' const ADDRESS_BOOK_STORAGE_KEY = 'ADDRESS_BOOK_STORAGE_KEY' -export const getAddressBookFromStorage = async (): Promise | undefined> => { - const data = await loadFromStorage>(ADDRESS_BOOK_STORAGE_KEY) +export type OldAddressBookEntry = { + address: string + name: string + isOwner: boolean +} - return data +export type OldAddressBookType = { + [safeAddress: string]: [OldAddressBookEntry] } -export const saveAddressBook = async (addressBook) => { +const ADDRESSBOOK_INVALID_NAMES = ['UNKNOWN', 'OWNER #', 'MY WALLET'] + +export const migrateOldAddressBook = (oldAddressBook: OldAddressBookType): AddressBookState => { + const values: AddressBookState = [] + const adbkValues = Object.values(oldAddressBook) + + for (const safeIterator of adbkValues) { + for (const safeAddressBook of safeIterator) { + if (!values.find((entry) => sameAddress(entry.address, safeAddressBook.address))) { + values.push(makeAddressBookEntry({ address: safeAddressBook.address, name: safeAddressBook.name })) + } + } + } + + return values +} + +export const getAddressBookFromStorage = async (): Promise => { + const result: OldAddressBookType | string | undefined = await loadFromStorage(ADDRESS_BOOK_STORAGE_KEY) + + if (!result) { + return null + } + + if (typeof result === 'string') { + return JSON.parse(result) + } + + return migrateOldAddressBook(result as OldAddressBookType) +} + +export const saveAddressBook = async (addressBook: AddressBookState): Promise => { try { - await saveToStorage(ADDRESS_BOOK_STORAGE_KEY, addressBook.toJSON()) + await saveToStorage(ADDRESS_BOOK_STORAGE_KEY, JSON.stringify(addressBook)) } catch (err) { console.error('Error storing addressBook in localstorage', err) } } -export const getAddressesListFromAdbk = (addressBook) => Array.from(addressBook).map((entry: any) => entry.address) +export const getAddressesListFromAddressBook = (addressBook: AddressBookState): string[] => + addressBook.map((entry) => entry.address) + +type GetNameFromAddressBookOptions = { + filterOnlyValidName: boolean +} -export const getNameFromAdbk = (addressBook, userAddress) => { +export const getNameFromAddressBook = ( + addressBook: AddressBookState, + userAddress: string, + options?: GetNameFromAddressBookOptions, +): string | null => { const entry = addressBook.find((addressBookItem) => addressBookItem.address === userAddress) if (entry) { - return entry.name + return options?.filterOnlyValidName ? getValidAddressBookName(entry.name) : entry.name } return null } +export const isValidAddressBookName = (addressBookName: string): boolean => { + const hasInvalidName = ADDRESSBOOK_INVALID_NAMES.find((invalidName) => + addressBookName.toUpperCase().includes(invalidName), + ) + return !hasInvalidName +} + +export const getValidAddressBookName = (addressBookName: string): string | null => { + return isValidAddressBookName(addressBookName) ? addressBookName : null +} + export const getOwnersWithNameFromAddressBook = ( - addressBook: AddressBookEntryProps, + addressBook: AddressBookState, ownerList: List, -): List | [] => { +): List => { if (!ownerList) { - return [] + return List([]) } - const ownersListWithAdbkNames = ownerList.map((owner) => { - const ownerName = getNameFromAdbk(addressBook, owner.address) + return ownerList.map((owner) => { + const ownerName = getNameFromAddressBook(addressBook, owner.address) return { address: owner.address, name: ownerName || owner.name, } }) - return ownersListWithAdbkNames +} + +export const formatAddressListToAddressBookNames = ( + addressBook: AddressBookState, + addresses: string[], +): AddressBookEntry[] => { + if (!addresses.length) { + return [] + } + return addresses.map((address) => { + const ownerName = getNameFromAddressBook(addressBook, address) + return { + address: address, + name: ownerName || '', + } + }) +} + +/** + * If the safe is not loaded, the owner wasn't not deleted + * If the safe is already loaded and the owner has a valid name, will return true if the address is not already on the addressBook + * @param name + * @param address + * @param addressBook + * @param safeAlreadyLoaded + */ +export const checkIfEntryWasDeletedFromAddressBook = ( + { name, address }: AddressBookEntry, + addressBook: AddressBookState, + safeAlreadyLoaded: boolean, +): boolean => { + if (!safeAlreadyLoaded) { + return false + } + + const addressShouldBeOnTheAddressBook = !!getValidAddressBookName(name) + const isAlreadyInAddressBook = !!addressBook.find((entry) => sameAddress(entry.address, address)) + return addressShouldBeOnTheAddressBook && !isAlreadyInAddressBook } diff --git a/src/logic/contractInteraction/sources/ABIService/index.ts b/src/logic/contractInteraction/sources/ABIService/index.ts index c9b62c5f8f..c9dc15b77d 100644 --- a/src/logic/contractInteraction/sources/ABIService/index.ts +++ b/src/logic/contractInteraction/sources/ABIService/index.ts @@ -2,14 +2,19 @@ import { AbiItem } from 'web3-utils' import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3' -export interface AbiItemExtended extends AbiItem { +export interface AllowedAbiItem extends AbiItem { + name: string + type: 'function' +} + +export interface AbiItemExtended extends AllowedAbiItem { action: string methodSignature: string signatureHash: string } export const getMethodSignature = ({ inputs, name }: AbiItem): string => { - const params = inputs.map((x) => x.type).join(',') + const params = inputs?.map((x) => x.type).join(',') return `${name}(${params})` } @@ -35,12 +40,17 @@ export const isAllowedMethod = ({ name, type }: AbiItem): boolean => { } export const getMethodAction = ({ stateMutability }: AbiItem): 'read' | 'write' => { + if (!stateMutability) { + return 'write' + } + return ['view', 'pure'].includes(stateMutability) ? 'read' : 'write' } export const extractUsefulMethods = (abi: AbiItem[]): AbiItemExtended[] => { - return abi - .filter(isAllowedMethod) + const allowedAbiItems = abi.filter(isAllowedMethod) as AllowedAbiItem[] + + return allowedAbiItems .map( (method): AbiItemExtended => ({ action: getMethodAction(method), @@ -48,9 +58,11 @@ export const extractUsefulMethods = (abi: AbiItem[]): AbiItemExtended[] => { ...method, }), ) - .sort(({ name: a }, { name: b }) => (a.toLowerCase() > b.toLowerCase() ? 1 : -1)) + .sort(({ name: a }, { name: b }) => { + return a.toLowerCase() > b.toLowerCase() ? 1 : -1 + }) } export const isPayable = (method: AbiItem | AbiItemExtended): boolean => { - return method.payable + return !!method?.payable } diff --git a/src/logic/contracts/generateBatchRequests.ts b/src/logic/contracts/generateBatchRequests.ts index 95602f2961..2d48979bd8 100644 --- a/src/logic/contracts/generateBatchRequests.ts +++ b/src/logic/contracts/generateBatchRequests.ts @@ -12,7 +12,7 @@ import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3' */ const generateBatchRequests = ({ abi, address, batch, context, methods }: any): any => { const contractInstance: any = new web3.eth.Contract(abi, address) - const localBatch = batch ? null : new web3.BatchRequest() + const localBatch = new web3.BatchRequest() const values = methods.map((methodObject) => { let method, type, args = [] @@ -39,6 +39,8 @@ const generateBatchRequests = ({ abi, address, batch, context, methods }: any): } else { request = contractInstance.methods[method](...args).call.request(resolver) } + + // If batch was provided add to external batch batch ? batch.add(request) : localBatch.add(request) } catch (e) { resolve(null) @@ -46,7 +48,11 @@ const generateBatchRequests = ({ abi, address, batch, context, methods }: any): }) }) - localBatch && localBatch.execute() + // TODO fix this so all batch.execute() are handled here + // If batch was created locally we can already execute it + // If batch was provided we should execute once we finish to generate the batch, + // in the outside function where the batch object is created. + !batch && localBatch.execute() const returnValues = context ? [context, ...values] : values diff --git a/src/logic/contracts/safeContracts.ts b/src/logic/contracts/safeContracts.ts index d2992add06..68d971f92b 100644 --- a/src/logic/contracts/safeContracts.ts +++ b/src/logic/contracts/safeContracts.ts @@ -98,11 +98,9 @@ export const estimateGasForDeployingSafe = async ( return gas * parseInt(gasPrice, 10) } -export const getGnosisSafeInstanceAt = async (safeAddress: string): Promise => { +export const getGnosisSafeInstanceAt = (safeAddress: string): GnosisSafe => { const web3 = getWeb3() - const gnosisSafe = await new web3.eth.Contract(GnosisSafeSol.abi as AbiItem[], safeAddress) as unknown as GnosisSafe - - return gnosisSafe + return new web3.eth.Contract(GnosisSafeSol.abi as AbiItem[], safeAddress) as unknown as GnosisSafe } const cleanByteCodeMetadata = (bytecode: string): string => { diff --git a/src/logic/currencyValues/__tests__/fetchSafeTokens.test.ts b/src/logic/currencyValues/__tests__/fetchSafeTokens.test.ts new file mode 100644 index 0000000000..32bf362ff5 --- /dev/null +++ b/src/logic/currencyValues/__tests__/fetchSafeTokens.test.ts @@ -0,0 +1,56 @@ +import { aNewStore } from 'src/store' +import fetchTokenCurrenciesBalances from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances' +import axios from 'axios' +import { getTxServiceHost } from 'src/config' + +jest.mock('axios') +describe('fetchTokenCurrenciesBalances', () => { + let store + const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf' + const excludeSpamTokens = true + beforeEach(() => { + store = aNewStore() + }) + afterAll(() => { + jest.unmock('axios') + }) + + it('Given a safe address, calls the API and returns token balances', async () => { + // given + const expectedResult = [ + { + balance: '849890000000000000', + balanceUsd: '337.2449', + token: null, + tokenAddress: null, + usdConversion: '396.81', + }, + { + balance: '24698677800000000000', + balanceUsd: '29.3432', + token: { + name: 'Dai', + symbol: 'DAI', + decimals: 18, + logoUri: 'https://gnosis-safe-token-logos.s3.amazonaws.com/0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa.png', + }, + tokenAddress: '0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa', + usdConversion: '1.188', + }, + ] + const apiUrl = getTxServiceHost() + + // @ts-ignore + axios.get.mockImplementationOnce(() => Promise.resolve(expectedResult)) + + // when + const result = await fetchTokenCurrenciesBalances(safeAddress, excludeSpamTokens) + + // then + expect(result).toStrictEqual(expectedResult) + expect(axios.get).toHaveBeenCalled() + expect(axios.get).toBeCalledWith(`${apiUrl}safes/${safeAddress}/balances/usd/?exclude_spam=${excludeSpamTokens}`, { + params: { limit: 3000 }, + }) + }) +}) diff --git a/src/logic/currencyValues/api/fetchTokenCurrenciesBalances.ts b/src/logic/currencyValues/api/fetchTokenCurrenciesBalances.ts index 3ed8fa9d57..c2804708a9 100644 --- a/src/logic/currencyValues/api/fetchTokenCurrenciesBalances.ts +++ b/src/logic/currencyValues/api/fetchTokenCurrenciesBalances.ts @@ -6,17 +6,17 @@ import { TokenProps } from 'src/logic/tokens/store/model/token' export type BalanceEndpoint = { balance: string balanceUsd: string - tokenAddress?: string + tokenAddress: string token?: TokenProps usdConversion: string } -const fetchTokenCurrenciesBalances = (safeAddress?: string): Promise> => { - if (!safeAddress) { - return null - } +const fetchTokenCurrenciesBalances = ( + safeAddress: string, + excludeSpamTokens = true, +): Promise> => { const apiUrl = getTxServiceHost() - const url = `${apiUrl}safes/${safeAddress}/balances/usd/` + const url = `${apiUrl}safes/${safeAddress}/balances/usd/?exclude_spam=${excludeSpamTokens}` return axios.get(url, { params: { diff --git a/src/logic/currencyValues/store/actions/fetchCurrencyValues.ts b/src/logic/currencyValues/store/actions/fetchCurrencyValues.ts deleted file mode 100644 index b109f93e8d..0000000000 --- a/src/logic/currencyValues/store/actions/fetchCurrencyValues.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { List } from 'immutable' -import { batch } from 'react-redux' - -import { setCurrencyBalances } from 'src/logic/currencyValues/store/actions/setCurrencyBalances' -import { setCurrencyRate } from 'src/logic/currencyValues/store/actions/setCurrencyRate' -import { setSelectedCurrency } from 'src/logic/currencyValues/store/actions/setSelectedCurrency' -import { AVAILABLE_CURRENCIES, CurrencyRateValue } from 'src/logic/currencyValues/store/model/currencyValues' -import { loadCurrencyValues } from 'src/logic/currencyValues/store/utils/currencyValuesStorage' -import { Dispatch } from 'redux' - -export const fetchCurrencyValues = (safeAddress: string) => async ( - dispatch: Dispatch, -): Promise => { - try { - const storedCurrencies: Map | unknown = await loadCurrencyValues() - const storedCurrency = storedCurrencies[safeAddress] - if (!storedCurrency) { - return batch(() => { - dispatch(setCurrencyBalances(safeAddress, List([]))) - dispatch(setSelectedCurrency(safeAddress, AVAILABLE_CURRENCIES.USD)) - dispatch(setCurrencyRate(safeAddress, 1)) - }) - } - // Loads the stored state on redux - Object.entries(storedCurrencies).forEach((kv) => { - const safeAddr = kv[0] - const value = kv[1] - - let { currencyRate, selectedCurrency }: CurrencyRateValue = value - - // Fallback for users that got an undefined saved on localStorage - if (!selectedCurrency || selectedCurrency === AVAILABLE_CURRENCIES.USD) { - currencyRate = 1 - selectedCurrency = AVAILABLE_CURRENCIES.USD - } - - batch(() => { - dispatch(setSelectedCurrency(safeAddr, selectedCurrency)) - dispatch(setCurrencyRate(safeAddr, currencyRate)) - }) - }) - } catch (err) { - console.error('Error fetching currency values', err) - } - return Promise.resolve() -} diff --git a/src/logic/currencyValues/store/actions/fetchSelectedCurrency.ts b/src/logic/currencyValues/store/actions/fetchSelectedCurrency.ts new file mode 100644 index 0000000000..42898646b7 --- /dev/null +++ b/src/logic/currencyValues/store/actions/fetchSelectedCurrency.ts @@ -0,0 +1,19 @@ +import { setCurrencyBalances } from 'src/logic/currencyValues/store/actions/setCurrencyBalances' +import { setCurrencyRate } from 'src/logic/currencyValues/store/actions/setCurrencyRate' +import { setSelectedCurrency } from 'src/logic/currencyValues/store/actions/setSelectedCurrency' +import { AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/model/currencyValues' +import { loadSelectedCurrency } from 'src/logic/currencyValues/store/utils/currencyValuesStorage' +import { Dispatch } from 'redux' + +export const fetchSelectedCurrency = (safeAddress: string) => async ( + dispatch: Dispatch, +): Promise => { + try { + const storedSelectedCurrency = await loadSelectedCurrency() + + dispatch(setSelectedCurrency(safeAddress, storedSelectedCurrency || AVAILABLE_CURRENCIES.USD)) + } catch (err) { + console.error('Error fetching currency values', err) + } + return Promise.resolve() +} diff --git a/src/logic/currencyValues/store/actions/setCurrencyBalances.ts b/src/logic/currencyValues/store/actions/setCurrencyBalances.ts index e84800e972..b739016689 100644 --- a/src/logic/currencyValues/store/actions/setCurrencyBalances.ts +++ b/src/logic/currencyValues/store/actions/setCurrencyBalances.ts @@ -1,9 +1,12 @@ import { createAction } from 'redux-actions' +import { BalanceCurrencyList } from 'src/logic/currencyValues/store/model/currencyValues' export const SET_CURRENCY_BALANCES = 'SET_CURRENCY_BALANCES' -// eslint-disable-next-line max-len -export const setCurrencyBalances = createAction(SET_CURRENCY_BALANCES, (safeAddress, currencyBalances) => ({ - safeAddress, - currencyBalances, -})) +export const setCurrencyBalances = createAction( + SET_CURRENCY_BALANCES, + (safeAddress: string, currencyBalances: BalanceCurrencyList) => ({ + safeAddress, + currencyBalances, + }), +) diff --git a/src/logic/currencyValues/store/actions/setSelectedCurrency.ts b/src/logic/currencyValues/store/actions/setSelectedCurrency.ts index ea3f13cafa..3b0774a075 100644 --- a/src/logic/currencyValues/store/actions/setSelectedCurrency.ts +++ b/src/logic/currencyValues/store/actions/setSelectedCurrency.ts @@ -1,12 +1,23 @@ import { createAction } from 'redux-actions' +import { ThunkDispatch } from 'redux-thunk' +import { AnyAction } from 'redux' +import { AppReduxState } from 'src/store' import { AVAILABLE_CURRENCIES } from '../model/currencyValues' +import fetchCurrencyRate from 'src/logic/currencyValues/store/actions/fetchCurrencyRate' export const SET_CURRENT_CURRENCY = 'SET_CURRENT_CURRENCY' -export const setSelectedCurrency = createAction( +const setCurrentCurrency = createAction( SET_CURRENT_CURRENCY, (safeAddress: string, selectedCurrency: AVAILABLE_CURRENCIES) => ({ safeAddress, selectedCurrency, }), ) + +export const setSelectedCurrency = (safeAddress: string, selectedCurrency: AVAILABLE_CURRENCIES) => ( + dispatch: ThunkDispatch, +): void => { + dispatch(setCurrentCurrency(safeAddress, selectedCurrency)) + dispatch(fetchCurrencyRate(safeAddress, selectedCurrency)) +} diff --git a/src/logic/currencyValues/store/middleware/index.ts b/src/logic/currencyValues/store/middleware/index.ts index 92445da4e9..a361f593c8 100644 --- a/src/logic/currencyValues/store/middleware/index.ts +++ b/src/logic/currencyValues/store/middleware/index.ts @@ -1,39 +1,16 @@ -import fetchCurrencyRate from 'src/logic/currencyValues/store/actions/fetchCurrencyRate' -import { SET_CURRENCY_BALANCES } from 'src/logic/currencyValues/store/actions/setCurrencyBalances' -import { SET_CURRENCY_RATE } from 'src/logic/currencyValues/store/actions/setCurrencyRate' import { SET_CURRENT_CURRENCY } from 'src/logic/currencyValues/store/actions/setSelectedCurrency' -import { currencyValuesSelector } from 'src/logic/currencyValues/store/selectors' -import { saveCurrencyValues } from 'src/logic/currencyValues/store/utils/currencyValuesStorage' -import { AVAILABLE_CURRENCIES, CurrencyRateValue } from '../model/currencyValues' -import { Map } from 'immutable' +import { saveSelectedCurrency } from 'src/logic/currencyValues/store/utils/currencyValuesStorage' -const watchedActions = [SET_CURRENT_CURRENCY, SET_CURRENCY_RATE, SET_CURRENCY_BALANCES] +const watchedActions = [SET_CURRENT_CURRENCY] -const currencyValuesStorageMiddleware = (store) => (next) => async (action) => { +const currencyValuesStorageMiddleware = () => (next) => async (action) => { const handledAction = next(action) if (watchedActions.includes(action.type)) { - const state = store.getState() - const { dispatch } = store switch (action.type) { case SET_CURRENT_CURRENCY: { - const { safeAddress, selectedCurrency } = action.payload - dispatch(fetchCurrencyRate(safeAddress, selectedCurrency)) - break - } - case SET_CURRENCY_RATE: - case SET_CURRENCY_BALANCES: { - const currencyValues = currencyValuesSelector(state) - - const currencyValuesWithoutBalances: Map = currencyValues.map((currencyValue) => { - const currencyRate: number = currencyValue.get('currencyRate') - const selectedCurrency: AVAILABLE_CURRENCIES = currencyValue.get('selectedCurrency') - return { - currencyRate, - selectedCurrency, - } - }) + const { selectedCurrency } = action.payload - await saveCurrencyValues(currencyValuesWithoutBalances) + saveSelectedCurrency(selectedCurrency) break } diff --git a/src/logic/currencyValues/store/selectors/index.ts b/src/logic/currencyValues/store/selectors/index.ts index 141654c1af..b17891740f 100644 --- a/src/logic/currencyValues/store/selectors/index.ts +++ b/src/logic/currencyValues/store/selectors/index.ts @@ -16,7 +16,7 @@ export const safeFiatBalancesSelector = createSelector( currencyValuesSelector, safeParamAddressFromStateSelector, (currencyValues, safeAddress): CurrencyReducerMap | undefined => { - if (!currencyValues) return + if (!currencyValues || !safeAddress) return return currencyValues.get(safeAddress) }, ) diff --git a/src/logic/currencyValues/store/utils/currencyValuesStorage.ts b/src/logic/currencyValues/store/utils/currencyValuesStorage.ts index 6a265fc881..ba71634983 100644 --- a/src/logic/currencyValues/store/utils/currencyValuesStorage.ts +++ b/src/logic/currencyValues/store/utils/currencyValuesStorage.ts @@ -1,16 +1,15 @@ import { loadFromStorage, saveToStorage } from 'src/utils/storage' -import { CurrencyRateValue } from '../model/currencyValues' -import { Map } from 'immutable' +import { AVAILABLE_CURRENCIES } from '../model/currencyValues' -const CURRENCY_VALUES_STORAGE_KEY = 'CURRENCY_VALUES_STORAGE_KEY' -export const saveCurrencyValues = async (currencyValues: Map): Promise => { +const SELECTED_CURRENCY_STORAGE_KEY = 'SELECTED_CURRENCY' +export const saveSelectedCurrency = async (selectedCurrency: AVAILABLE_CURRENCIES): Promise => { try { - await saveToStorage(CURRENCY_VALUES_STORAGE_KEY, currencyValues) + await saveToStorage(SELECTED_CURRENCY_STORAGE_KEY, selectedCurrency) } catch (err) { console.error('Error storing currency values info in localstorage', err) } } -export const loadCurrencyValues = async (): Promise | unknown> => { - return (await loadFromStorage(CURRENCY_VALUES_STORAGE_KEY)) || {} +export const loadSelectedCurrency = async (): Promise => { + return await loadFromStorage(SELECTED_CURRENCY_STORAGE_KEY) } diff --git a/src/logic/currentSession/store/actions/addViewedSafe.ts b/src/logic/currentSession/store/actions/addViewedSafe.ts index 7bac5b2a6c..16d9a8a86b 100644 --- a/src/logic/currentSession/store/actions/addViewedSafe.ts +++ b/src/logic/currentSession/store/actions/addViewedSafe.ts @@ -1,6 +1,8 @@ +import { Dispatch } from 'src/logic/safe/store/actions/types.d' + import updateViewedSafes from 'src/logic/currentSession/store/actions/updateViewedSafes' -const addViewedSafe = (safeAddress) => (dispatch) => { +const addViewedSafe = (safeAddress: string) => (dispatch: Dispatch): void => { dispatch(updateViewedSafes(safeAddress)) } diff --git a/src/logic/currentSession/store/actions/loadCurrentSessionFromStorage.ts b/src/logic/currentSession/store/actions/loadCurrentSessionFromStorage.ts index b3dde21741..cd3a42a0e5 100644 --- a/src/logic/currentSession/store/actions/loadCurrentSessionFromStorage.ts +++ b/src/logic/currentSession/store/actions/loadCurrentSessionFromStorage.ts @@ -1,11 +1,12 @@ +import { Dispatch } from 'redux' + import loadCurrentSession from 'src/logic/currentSession/store/actions/loadCurrentSession' -import { makeCurrentSession } from 'src/logic/currentSession/store/model/currentSession' import { getCurrentSessionFromStorage } from 'src/logic/currentSession/utils' -const loadCurrentSessionFromStorage = () => async (dispatch) => { +const loadCurrentSessionFromStorage = () => async (dispatch: Dispatch): Promise => { const currentSession = await getCurrentSessionFromStorage() - dispatch(loadCurrentSession(makeCurrentSession(currentSession ? currentSession : {}))) + dispatch(loadCurrentSession(currentSession)) } export default loadCurrentSessionFromStorage diff --git a/src/logic/currentSession/store/model/currentSession.ts b/src/logic/currentSession/store/model/currentSession.ts deleted file mode 100644 index c5be51cadb..0000000000 --- a/src/logic/currentSession/store/model/currentSession.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Record } from 'immutable' - -export const makeCurrentSession = Record({ - viewedSafes: [], -}) diff --git a/src/logic/currentSession/store/reducer/currentSession.ts b/src/logic/currentSession/store/reducer/currentSession.ts index 228bfc9eef..236da1b928 100644 --- a/src/logic/currentSession/store/reducer/currentSession.ts +++ b/src/logic/currentSession/store/reducer/currentSession.ts @@ -1,4 +1,3 @@ -import { Map } from 'immutable' import { handleActions } from 'redux-actions' import { LOAD_CURRENT_SESSION } from 'src/logic/currentSession/store/actions/loadCurrentSession' @@ -7,20 +6,32 @@ import { saveCurrentSessionToStorage } from 'src/logic/currentSession/utils' export const CURRENT_SESSION_REDUCER_ID = 'currentSession' +export type CurrentSessionState = { + viewedSafes: string[] +} + +export const initialState = { + viewedSafes: [], +} + export default handleActions( { - [LOAD_CURRENT_SESSION]: (state, action) => state.merge(Map(action.payload)), + [LOAD_CURRENT_SESSION]: (state = initialState, action) => ({ + ...state, + ...action.payload, + }), [UPDATE_VIEWED_SAFES]: (state, action) => { const safeAddress = action.payload - - const newState = state.updateIn(['viewedSafes'], (prev) => - prev.includes(safeAddress) ? prev : [...prev, safeAddress], - ) + const viewedSafes = state.viewedSafes + const newState = { + ...state, + viewedSafes: viewedSafes.includes(safeAddress) ? viewedSafes : [...viewedSafes, safeAddress], + } saveCurrentSessionToStorage(newState) return newState }, }, - Map(), + initialState, ) diff --git a/src/logic/currentSession/utils/index.ts b/src/logic/currentSession/utils/index.ts index cbfa1a141d..f3da7fc0eb 100644 --- a/src/logic/currentSession/utils/index.ts +++ b/src/logic/currentSession/utils/index.ts @@ -1,12 +1,14 @@ import { loadFromStorage, saveToStorage } from 'src/utils/storage' +import { CurrentSessionState } from 'src/logic/currentSession/store/reducer/currentSession' const CURRENT_SESSION_STORAGE_KEY = 'CURRENT_SESSION' -export const getCurrentSessionFromStorage = async () => loadFromStorage(CURRENT_SESSION_STORAGE_KEY) +export const getCurrentSessionFromStorage = async (): Promise => + loadFromStorage(CURRENT_SESSION_STORAGE_KEY) export const saveCurrentSessionToStorage = async (currentSession) => { try { - await saveToStorage(CURRENT_SESSION_STORAGE_KEY, currentSession.toJSON()) + await saveToStorage(CURRENT_SESSION_STORAGE_KEY, currentSession) } catch (err) { console.error('Error storing currentSession in localStorage', err) } diff --git a/src/logic/hooks/useDebounce.tsx b/src/logic/hooks/useDebounce.tsx index e9abbb4329..2a19734fe7 100644 --- a/src/logic/hooks/useDebounce.tsx +++ b/src/logic/hooks/useDebounce.tsx @@ -16,7 +16,7 @@ interface DebounceOptions { export const useDebouncedCallback = unknown>( callback: T, delay = 0, - options: DebounceOptions, + options?: DebounceOptions, ): T & { cancel: () => void } => useCallback(debounce(callback, delay, options), [callback, delay, options]) export const useDebounce = (value: T, delay = 0, options?: DebounceOptions): T => { diff --git a/src/logic/notifications/notificationBuilder.tsx b/src/logic/notifications/notificationBuilder.tsx index b49b2aa17c..fd8153852a 100644 --- a/src/logic/notifications/notificationBuilder.tsx +++ b/src/logic/notifications/notificationBuilder.tsx @@ -15,7 +15,7 @@ const setNotificationOrigin = (notification: Notification, origin: string): Noti } const appInfo = getAppInfoFromOrigin(origin) - return { ...notification, message: `${appInfo.name}: ${notification.message}` } + return { ...notification, message: `${appInfo ? appInfo.name : 'Unknown origin'}: ${notification.message}` } } const getStandardTxNotificationsQueue = ( diff --git a/src/logic/safe/hooks/useFetchTokens.tsx b/src/logic/safe/hooks/useFetchTokens.tsx index 32003528b8..2932d16a19 100644 --- a/src/logic/safe/hooks/useFetchTokens.tsx +++ b/src/logic/safe/hooks/useFetchTokens.tsx @@ -3,7 +3,7 @@ import { batch, useDispatch } from 'react-redux' import { useLocation } from 'react-router-dom' import fetchCollectibles from 'src/logic/collectibles/store/actions/fetchCollectibles' -import { fetchCurrencyValues } from 'src/logic/currencyValues/store/actions/fetchCurrencyValues' +import { fetchSelectedCurrency } from 'src/logic/currencyValues/store/actions/fetchSelectedCurrency' import activateAssetsByBalance from 'src/logic/tokens/store/actions/activateAssetsByBalance' import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens' import { fetchTokens } from 'src/logic/tokens/store/actions/fetchTokens' @@ -19,7 +19,7 @@ export const useFetchTokens = (safeAddress: string): void => { batch(() => { // fetch tokens there to get symbols for tokens in TXs list dispatch(fetchTokens()) - dispatch(fetchCurrencyValues(safeAddress)) + dispatch(fetchSelectedCurrency(safeAddress)) dispatch(fetchSafeTokens(safeAddress)) }) } diff --git a/src/logic/safe/hooks/useLoadSafe.tsx b/src/logic/safe/hooks/useLoadSafe.tsx index 048adb856a..6a44a7c499 100644 --- a/src/logic/safe/hooks/useLoadSafe.tsx +++ b/src/logic/safe/hooks/useLoadSafe.tsx @@ -10,7 +10,7 @@ import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTr import fetchSafeCreationTx from 'src/logic/safe/store/actions/fetchSafeCreationTx' import { Dispatch } from 'src/logic/safe/store/actions/types.d' -export const useLoadSafe = (safeAddress: string): void => { +export const useLoadSafe = (safeAddress?: string): void => { const dispatch = useDispatch() useEffect(() => { @@ -22,13 +22,13 @@ export const useLoadSafe = (safeAddress: string): void => { return dispatch(fetchSafeTokens(safeAddress)) }) .then(() => { - dispatch(loadAddressBookFromStorage()) dispatch(fetchSafeCreationTx(safeAddress)) dispatch(fetchTransactions(safeAddress)) return dispatch(addViewedSafe(safeAddress)) }) } } + dispatch(loadAddressBookFromStorage()) fetchData() }, [dispatch, safeAddress]) diff --git a/src/logic/safe/hooks/useSafeScheduledUpdates.tsx b/src/logic/safe/hooks/useSafeScheduledUpdates.tsx index d2415eb385..66c474a210 100644 --- a/src/logic/safe/hooks/useSafeScheduledUpdates.tsx +++ b/src/logic/safe/hooks/useSafeScheduledUpdates.tsx @@ -8,9 +8,9 @@ import { checkAndUpdateSafe } from 'src/logic/safe/store/actions/fetchSafe' import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions' import { TIMEOUT } from 'src/utils/constants' -export const useSafeScheduledUpdates = (safeAddress: string): void => { +export const useSafeScheduledUpdates = (safeAddress?: string): void => { const dispatch = useDispatch() - const timer = useRef(null) + const timer = useRef() useEffect(() => { // using this variable to prevent setting a timeout when the component is already unmounted or the effect @@ -29,7 +29,7 @@ export const useSafeScheduledUpdates = (safeAddress: string): void => { if (mounted) { timer.current = setTimeout(() => { - fetchSafeData(safeAddress) + fetchSafeData(address) }, TIMEOUT * 3) } } diff --git a/src/logic/safe/store/actions/__tests__/transactionHelpers.test.ts b/src/logic/safe/store/actions/__tests__/transactionHelpers.test.ts new file mode 100644 index 0000000000..4076f0027c --- /dev/null +++ b/src/logic/safe/store/actions/__tests__/transactionHelpers.test.ts @@ -0,0 +1,875 @@ +import { getMockedSafeInstance, getMockedTxServiceModel } from 'src/test/utils/safeHelper' + +import { makeTransaction } from 'src/logic/safe/store/models/transaction' +import { TransactionStatus, TransactionTypes } from 'src/logic/safe/store/models/types/transaction' +import makeSafe from 'src/logic/safe/store/models/safe' +import { List, Map, Record } from 'immutable' +import { makeToken, TokenProps } from 'src/logic/tokens/store/model/token' +import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions' +import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' +import { + buildTx, + calculateTransactionStatus, + calculateTransactionType, + generateSafeTxHash, + getRefundParams, + isCancelTransaction, + isCustomTransaction, + isInnerTransaction, + isModifySettingsTransaction, + isMultiSendTransaction, + isOutgoingTransaction, + isPendingTransaction, + isUpgradeTransaction, +} from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers' +import { getERC20DecimalsAndSymbol } from 'src/logic/tokens/utils/tokenHelpers' + +const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf' +const safeAddress2 = '0x344B941b1aAE2e4Be73987212FC4741687Bf0503' +describe('isInnerTransaction', () => { + it('It should return true if the transaction recipient is our given safeAddress and the txValue is 0', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0' }) + + // when + const result = isInnerTransaction(transaction, safeAddress) + + // then + expect(result).toBe(true) + }) + it('It should return false if the transaction recipient is our given safeAddress and the txValue is >0', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '100' }) + + // when + const result = isInnerTransaction(transaction, safeAddress) + + // then + expect(result).toBe(false) + }) + it('It should return false if the transaction recipient is not our given safeAddress', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0' }) + + // when + const result = isInnerTransaction(transaction, safeAddress) + + // then + expect(result).toBe(false) + }) + it('It should return true if the transaction recipient is the given safeAddress and the txValue is 0', () => { + // given + const transaction = makeTransaction({ recipient: safeAddress, value: '0' }) + + // when + const result = isInnerTransaction(transaction, safeAddress) + + // then + expect(result).toBe(true) + }) + it('It should return false if the transaction recipient is the given safeAddress and the txValue is >0', () => { + // given + const transaction = makeTransaction({ recipient: safeAddress, value: '100' }) + + // when + const result = isInnerTransaction(transaction, safeAddress) + + // then + expect(result).toBe(false) + }) + it('It should return false if the transaction recipient is not the given safeAddress', () => { + // given + const transaction = makeTransaction({ recipient: safeAddress2, value: '100' }) + + // when + const result = isInnerTransaction(transaction, safeAddress) + + // then + expect(result).toBe(false) + }) +}) + +describe('isCancelTransaction', () => { + const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf' + it('It should return false if given a inner transaction with empty data', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: null }) + + // when + const result = isCancelTransaction(transaction, safeAddress) + + // then + expect(result).toBe(true) + }) + it('It should return false if given a inner transaction without empty data', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: 'test' }) + + // when + const result = isCancelTransaction(transaction, safeAddress) + + // then + expect(result).toBe(false) + }) +}) + +describe('isPendingTransaction', () => { + it('It should return true if the transaction is on pending status', () => { + // given + const transaction = makeTransaction({ status: TransactionStatus.PENDING }) + const cancelTx = makeTransaction({ data: null }) + + // when + const result = isPendingTransaction(transaction, cancelTx) + + // then + expect(result).toBe(true) + }) + it('It should return true If the transaction is not pending status but the cancellation transaction is', () => { + // given + const transaction = makeTransaction({ status: TransactionStatus.AWAITING_CONFIRMATIONS }) + const cancelTx = makeTransaction({ status: TransactionStatus.PENDING }) + + // when + const result = isPendingTransaction(transaction, cancelTx) + + // then + expect(result).toBe(true) + }) + it('It should return true If the transaction and a cancellation transaction are not pending', () => { + // given + const transaction = makeTransaction({ status: TransactionStatus.CANCELLED }) + const cancelTx = makeTransaction({ status: TransactionStatus.AWAITING_CONFIRMATIONS }) + + // when + const result = isPendingTransaction(transaction, cancelTx) + + // then + expect(result).toBe(false) + }) +}) + +describe('isModifySettingsTransaction', () => { + const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf' + it('It should return true if given an inner transaction without empty data', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: 'test' }) + + // when + const result = isModifySettingsTransaction(transaction, safeAddress) + + // then + expect(result).toBe(true) + }) + it('It should return false if given an inner transaction with empty data', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: null }) + + // when + const result = isModifySettingsTransaction(transaction, safeAddress) + + // then + expect(result).toBe(false) + }) +}) + +describe('isMultiSendTransaction', () => { + it('It should return true if given a transaction without value, the data has multisend data', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: '0x8d80ff0a' }) + + // when + const result = isMultiSendTransaction(transaction) + + // then + expect(result).toBe(true) + }) + it('It should return false if given a transaction without data', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: null }) + + // when + const result = isMultiSendTransaction(transaction) + + // then + expect(result).toBe(false) + }) + it('It should return true if given a transaction without value, the data has not multisend substring', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: 'thisiswrongdata' }) + + // when + const result = isMultiSendTransaction(transaction) + + // then + expect(result).toBe(false) + }) +}) + +describe('isUpgradeTransaction', () => { + it('If should return true if the transaction data is empty', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: null }) + + // when + const result = isUpgradeTransaction(transaction) + + // then + expect(result).toBe(false) + }) + it('It should return false if the transaction data is multisend transaction but does not have upgradeTx function signature encoded in data', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: '0x8d80ff0a' }) + + // when + const result = isUpgradeTransaction(transaction) + + // then + expect(result).toBe(false) + }) + it('It should return true if the transaction data is multisend transaction and has upgradeTx enconded in function signature data', () => { + // given + const upgradeTxData = `0x8d80ff0a000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000f200dfa693da0d16f5e7e78fdcbede8fc6ebea44f1cf000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000247de7edef000000000000000000000000d5d82b6addc9027b22dca772aa68d5d74cdbdf4400dfa693da0d16f5e7e78fdcbede8fc6ebea44f1cf00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f08a032300000000000000000000000034cfac646f301356faa8b21e94227e3583fe3f5f0000000000000000000000000000` + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: upgradeTxData }) + + // when + const result = isUpgradeTransaction(transaction) + + // then + expect(result).toBe(true) + }) +}) + +describe('isOutgoingTransaction', () => { + it('It should return true if the transaction recipient is not a safe address and data is not empty', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', data: 'test' }) + + // when + const result = isOutgoingTransaction(transaction, safeAddress) + + // then + expect(result).toBe(true) + }) + it('It should return true if the transaction has an address equal to the safe address', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: 'test' }) + + // when + const result = isOutgoingTransaction(transaction, safeAddress) + + // then + expect(result).toBe(false) + }) + it('It should return false if the transaction recipient is not a safe address and data is empty', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: null }) + + // when + const result = isOutgoingTransaction(transaction, safeAddress) + + // then + expect(result).toBe(false) + }) +}) + +jest.mock('src/logic/tokens/utils/tokenHelpers') +describe('isCustomTransaction', () => { + afterAll(() => { + jest.unmock('src/logic/tokens/utils/tokenHelpers') + }) + it('It should return true if Is outgoing transaction, is not an erc20 transaction, not an upgrade transaction and not and erc721 transaction', async () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', data: 'test' }) + const txCode = '' + const knownTokens = Map & Readonly>() + const token = makeToken({ + address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', + name: 'OmiseGo', + symbol: 'OMG', + decimals: 18, + logoUri: + 'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true', + }) + knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token) + + const txHelpers = require('src/logic/tokens/utils/tokenHelpers') + + txHelpers.isSendERC20Transaction.mockImplementationOnce(() => false) + txHelpers.isSendERC721Transaction.mockImplementationOnce(() => false) + + // when + const result = await isCustomTransaction(transaction, txCode, safeAddress, knownTokens) + + // then + expect(result).toBe(true) + expect(txHelpers.isSendERC20Transaction).toHaveBeenCalled() + expect(txHelpers.isSendERC721Transaction).toHaveBeenCalled() + }) + it('It should return true if is outgoing transaction, is not SendERC20Transaction, is not isUpgradeTransaction and not isSendERC721Transaction', async () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', data: 'test' }) + const txCode = '' + const knownTokens = Map & Readonly>() + const token = makeToken({ + address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', + name: 'OmiseGo', + symbol: 'OMG', + decimals: 18, + logoUri: + 'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true', + }) + knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token) + + const txHelpers = require('src/logic/tokens/utils/tokenHelpers') + + txHelpers.isSendERC20Transaction.mockImplementationOnce(() => false) + txHelpers.isSendERC721Transaction.mockImplementationOnce(() => false) + + // when + const result = await isCustomTransaction(transaction, txCode, safeAddress, knownTokens) + + // then + expect(result).toBe(true) + expect(txHelpers.isSendERC20Transaction).toHaveBeenCalled() + expect(txHelpers.isSendERC721Transaction).toHaveBeenCalled() + }) + it('It should return false if is outgoing transaction, not SendERC20Transaction, isUpgradeTransaction and not isSendERC721Transaction', async () => { + // given + const upgradeTxData = `0x8d80ff0a000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000f200dfa693da0d16f5e7e78fdcbede8fc6ebea44f1cf000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000247de7edef000000000000000000000000d5d82b6addc9027b22dca772aa68d5d74cdbdf4400dfa693da0d16f5e7e78fdcbede8fc6ebea44f1cf00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f08a032300000000000000000000000034cfac646f301356faa8b21e94227e3583fe3f5f0000000000000000000000000000` + const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', data: upgradeTxData }) + const txCode = '' + const knownTokens = Map & Readonly>() + const token = makeToken({ + address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', + name: 'OmiseGo', + symbol: 'OMG', + decimals: 18, + logoUri: + 'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true', + }) + knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token) + + const txHelpers = require('src/logic/tokens/utils/tokenHelpers') + + txHelpers.isSendERC20Transaction.mockImplementationOnce(() => true) + txHelpers.isSendERC721Transaction.mockImplementationOnce(() => false) + + // when + const result = await isCustomTransaction(transaction, txCode, safeAddress, knownTokens) + + // then + expect(result).toBe(false) + expect(txHelpers.isSendERC20Transaction).toHaveBeenCalled() + }) + it('It should return false if is outgoing transaction, is not SendERC20Transaction, not isUpgradeTransaction and isSendERC721Transaction', async () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', data: 'test' }) + const txCode = '' + const knownTokens = Map & Readonly>() + const token = makeToken({ + address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', + name: 'OmiseGo', + symbol: 'OMG', + decimals: 18, + logoUri: + 'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true', + }) + knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token) + + const txHelpers = require('src/logic/tokens/utils/tokenHelpers') + + txHelpers.isSendERC20Transaction.mockImplementationOnce(() => false) + txHelpers.isSendERC721Transaction.mockImplementationOnce(() => true) + + // when + const result = await isCustomTransaction(transaction, txCode, safeAddress, knownTokens) + + // then + expect(result).toBe(false) + expect(txHelpers.isSendERC20Transaction).toHaveBeenCalled() + expect(txHelpers.isSendERC721Transaction).toHaveBeenCalled() + }) +}) + +describe('getRefundParams', () => { + it('It should return null if given a transaction with the gasPrice == 0', async () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', gasPrice: '0' }) + + // when + const result = await getRefundParams(transaction, getERC20DecimalsAndSymbol) + + // then + expect(result).toBe(null) + }) + it('It should return 0.000000000000020000 if given a transaction with the gasPrice = 100, the baseGas = 100, the txGas = 100 and 18 decimals', async () => { + // given + const gasPrice = '100' + const baseGas = 100 + const safeTxGas = 100 + const decimals = 18 + const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', gasPrice, baseGas, safeTxGas }) + const feeString = (Number(gasPrice) * (Number(baseGas) + Number(safeTxGas))).toString().padStart(decimals, '0') + const whole = feeString.slice(0, feeString.length - decimals) || '0' + const fraction = feeString.slice(feeString.length - decimals) + + const expectedResult = { + fee: `${whole}.${fraction}`, + symbol: 'ETH', + } + + const getTokenInfoMock = jest.fn().mockImplementation(() => { + return { + symbol: 'ETH', + decimals, + } + }) + + // when + const result = await getRefundParams(transaction, getTokenInfoMock) + + // then + expect(result).toStrictEqual(expectedResult) + expect(getTokenInfoMock).toBeCalled() + }) + it('Given a transaction with the gasPrice = 100, the baseGas = 100, the txGas = 100 and 1 decimal, returns 2000.0', async () => { + // given + const gasPrice = '100' + const baseGas = 100 + const safeTxGas = 100 + const decimals = 1 + const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', gasPrice, baseGas, safeTxGas }) + + const expectedResult = { + fee: `2000.0`, + symbol: 'ETH', + } + + const getTokenInfoMock = jest.fn().mockImplementation(() => { + return { + symbol: 'ETH', + decimals, + } + }) + + // when + const result = await getRefundParams(transaction, getTokenInfoMock) + + // then + expect(result).toStrictEqual(expectedResult) + expect(getTokenInfoMock).toBeCalled() + }) + it('It should return 0.50000 if given a transaction with the gasPrice = 100, the baseGas = 100, the txGas = 400 and 5 decimals', async () => { + // given + const gasPrice = '100' + const baseGas = 100 + const safeTxGas = 400 + const decimals = 5 + const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0', gasPrice, baseGas, safeTxGas }) + + const expectedResult = { + fee: `0.50000`, + symbol: 'ETH', + } + + const getTokenInfoMock = jest.fn().mockImplementation(() => { + return { + symbol: 'ETH', + decimals, + } + }) + + // when + const result = await getRefundParams(transaction, getTokenInfoMock) + + // then + expect(result).toStrictEqual(expectedResult) + expect(getTokenInfoMock).toBeCalled() + }) +}) + +describe('getDecodedParams', () => { + it('', () => { + // given + // when + // then + }) +}) + +describe('isTransactionCancelled', () => { + it('', () => { + // given + // when + // then + }) +}) + +describe('calculateTransactionStatus', () => { + it('It should return SUCCESS if the tx is executed and successful', () => { + // given + const transaction = makeTransaction({ isExecuted: true, isSuccessful: true }) + const safe = makeSafe() + const currentUser = safeAddress + + // when + const result = calculateTransactionStatus(transaction, safe, currentUser) + + // then + expect(result).toBe(TransactionStatus.SUCCESS) + }) + it('It should return CANCELLED if the tx is cancelled and successful', () => { + // given + const transaction = makeTransaction({ cancelled: true }) + const safe = makeSafe() + const currentUser = safeAddress + + // when + const result = calculateTransactionStatus(transaction, safe, currentUser) + + // then + expect(result).toBe(TransactionStatus.CANCELLED) + }) + it('It should return AWAITING_EXECUTION if the tx has an amount of confirmations equal to the safe threshold', () => { + // given + const makeUser = Record({ + owner: '', + type: '', + hash: '', + signature: '', + }) + const transaction = makeTransaction({ cancelled: true, confirmations: List([makeUser(), makeUser(), makeUser()]) }) + const safe = makeSafe({ threshold: 3 }) + const currentUser = safeAddress + + // when + const result = calculateTransactionStatus(transaction, safe, currentUser) + + // then + expect(result).toBe(TransactionStatus.CANCELLED) + }) + it('It should return SUCCESS if the tx is the creation transaction', () => { + // given + const transaction = makeTransaction({ creationTx: true, confirmations: List() }) + const safe = makeSafe({ threshold: 3 }) + const currentUser = safeAddress + + // when + const result = calculateTransactionStatus(transaction, safe, currentUser) + + // then + expect(result).toBe(TransactionStatus.SUCCESS) + }) + it('It should return PENDING if the tx is pending', () => { + // given + const transaction = makeTransaction({ confirmations: List(), isPending: true }) + const safe = makeSafe({ threshold: 3 }) + const currentUser = safeAddress + + // when + const result = calculateTransactionStatus(transaction, safe, currentUser) + + // then + expect(result).toBe(TransactionStatus.PENDING) + }) + it('It should return PENDING if the tx has no confirmations', () => { + // given + const transaction = makeTransaction({ confirmations: List(), isPending: false }) + const safe = makeSafe({ threshold: 3 }) + const currentUser = safeAddress + + // when + const result = calculateTransactionStatus(transaction, safe, currentUser) + + // then + expect(result).toBe(TransactionStatus.PENDING) + }) + it('It should return AWAITING_CONFIRMATIONS if the tx has confirmations bellow the threshold, the user is owner and signed', () => { + // given + const userAddress = 'address1' + const userAddress2 = 'address2' + const makeUser = Record({ + owner: '', + type: '', + hash: '', + signature: '', + }) + const transaction = makeTransaction({ confirmations: List([makeUser({ owner: userAddress })]) }) + const safe = makeSafe({ + threshold: 3, + owners: List([ + { name: '', address: userAddress }, + { name: '', address: userAddress2 }, + ]), + }) + const currentUser = userAddress + + // when + const result = calculateTransactionStatus(transaction, safe, currentUser) + + // then + expect(result).toBe(TransactionStatus.AWAITING_CONFIRMATIONS) + }) + it('It should return AWAITING_YOUR_CONFIRMATION if the tx has confirmations bellow the threshold, the user is owner and not signed', () => { + // given + const userAddress = 'address1' + const userAddress2 = 'address2' + const makeUser = Record({ + owner: '', + type: '', + hash: '', + signature: '', + }) + + const transaction = makeTransaction({ confirmations: List([makeUser({ owner: userAddress })]) }) + const safe = makeSafe({ + threshold: 3, + owners: List([ + { name: '', address: userAddress }, + { name: '', address: userAddress2 }, + ]), + }) + const currentUser = userAddress2 + + // when + const result = calculateTransactionStatus(transaction, safe, currentUser) + + // then + expect(result).toBe(TransactionStatus.AWAITING_YOUR_CONFIRMATION) + }) + it('It should return AWAITING_CONFIRMATIONS if the tx has confirmations bellow the threshold, the user is not owner', () => { + // given + const userAddress = 'address1' + const userAddress2 = 'address2' + const makeUser = Record({ + owner: '', + type: '', + hash: '', + signature: '', + }) + + const transaction = makeTransaction({ confirmations: List([makeUser({ owner: userAddress })]) }) + const safe = makeSafe({ threshold: 3, owners: List([{ name: '', address: userAddress }]) }) + const currentUser = userAddress2 + + // when + const result = calculateTransactionStatus(transaction, safe, currentUser) + + // then + expect(result).toBe(TransactionStatus.AWAITING_CONFIRMATIONS) + }) + it('It should return FAILED if the tx is not successful', () => { + // given + const userAddress = 'address1' + const userAddress2 = 'address2' + const makeUser = Record({ + owner: '', + type: '', + hash: '', + signature: '', + }) + + const transaction = makeTransaction({ + confirmations: List([makeUser({ owner: userAddress })]), + isSuccessful: false, + }) + const safe = makeSafe({ threshold: 3, owners: List([{ name: '', address: userAddress }]) }) + const currentUser = userAddress2 + + // when + const result = calculateTransactionStatus(transaction, safe, currentUser) + + // then + expect(result).toBe(TransactionStatus.FAILED) + }) +}) + +describe('calculateTransactionType', () => { + it('It should return TOKEN If the tx is a token transfer transaction', () => { + // given + const transaction = makeTransaction({ isTokenTransfer: true }) + + // when + const result = calculateTransactionType(transaction) + + // then + expect(result).toBe(TransactionTypes.TOKEN) + }) + it('It should return COLLECTIBLE If the tx is a collectible transfer transaction', () => { + // given + const transaction = makeTransaction({ isCollectibleTransfer: true }) + + // when + const result = calculateTransactionType(transaction) + + // then + expect(result).toBe(TransactionTypes.COLLECTIBLE) + }) + it('It should return SETTINGS If the tx is a modifySettings transaction', () => { + // given + const transaction = makeTransaction({ modifySettingsTx: true }) + + // when + const result = calculateTransactionType(transaction) + + // then + expect(result).toBe(TransactionTypes.SETTINGS) + }) + + it('It should return CANCELLATION If the tx is a cancellation transaction', () => { + // given + const transaction = makeTransaction({ isCancellationTx: true }) + + // when + const result = calculateTransactionType(transaction) + + // then + expect(result).toBe(TransactionTypes.CANCELLATION) + }) + + it('It should return CUSTOM If the tx is a custom transaction', () => { + // given + const transaction = makeTransaction({ customTx: true }) + + // when + const result = calculateTransactionType(transaction) + + // then + expect(result).toBe(TransactionTypes.CUSTOM) + }) + it('It should return CUSTOM If the tx is a creation transaction', () => { + // given + const transaction = makeTransaction({ creationTx: true }) + + // when + const result = calculateTransactionType(transaction) + + // then + expect(result).toBe(TransactionTypes.CREATION) + }) + it('It should return UPGRADE If the tx is an upgrade transaction', () => { + // given + const transaction = makeTransaction({ upgradeTx: true }) + + // when + const result = calculateTransactionType(transaction) + + // then + expect(result).toBe(TransactionTypes.UPGRADE) + }) +}) + +describe('buildTx', () => { + it('Returns a valid transaction', async () => { + // given + const cancelTx1 = makeTransaction() + const transaction = getMockedTxServiceModel({ to: safeAddress2, value: '0' }) + const userAddress = 'address1' + const cancellationTxs = List([cancelTx1]) + const token = makeToken({ + address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', + name: 'OmiseGo', + symbol: 'OMG', + decimals: 18, + logoUri: + 'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true', + }) + const knownTokens = Map & Readonly>() + knownTokens.set('0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', token) + const outgoingTxs = List([cancelTx1]) + const safeInstance = makeSafe({ name: 'LOADED SAFE', address: safeAddress }) + const expectedTx = makeTransaction({ + baseGas: 0, + blockNumber: 0, + cancelled: false, + confirmations: List([]), + creationTx: false, + customTx: false, + data: EMPTY_DATA, + dataDecoded: null, + decimals: 18, + decodedParams: null, + executionDate: '', + executionTxHash: '', + executor: '', + gasPrice: '', + gasToken: ZERO_ADDRESS, + isCancellationTx: false, + isCollectibleTransfer: false, + isExecuted: false, + isSuccessful: false, + isTokenTransfer: false, + modifySettingsTx: false, + multiSendTx: false, + nonce: 0, + operation: 0, + origin: '', + recipient: safeAddress2, + refundParams: null, + refundReceiver: ZERO_ADDRESS, + safeTxGas: 0, + safeTxHash: '', + setupData: '', + status: TransactionStatus.FAILED, + submissionDate: '', + symbol: 'ETH', + upgradeTx: false, + value: '0', + fee: '', + }) + + // when + const txResult = await buildTx({ + cancellationTxs, + currentUser: userAddress, + knownTokens, + outgoingTxs, + safe: safeInstance, + tx: transaction, + txCode: null, + }) + + // then + expect(txResult).toStrictEqual(expectedTx) + }) +}) + +describe('updateStoredTransactionsStatus', () => { + it('', () => { + // given + // when + // then + }) +}) + +describe('generateSafeTxHash', () => { + it('It should return a safe transaction hash', () => { + // given + const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf' + const userAddress = 'address1' + const userAddress2 = 'address2' + const userAddress3 = 'address3' + const safeInstance = getMockedSafeInstance({}) + const txArgs = { + baseGas: 100, + data: '', + gasPrice: '1000', + gasToken: '', + nonce: 0, + operation: 0, + refundReceiver: userAddress, + safeInstance, + safeTxGas: 1000, + sender: userAddress2, + sigs: '', + to: userAddress3, + valueInWei: '5000', + } + + // when + const result = generateSafeTxHash(safeAddress, txArgs) + + // then + expect(result).toBe('0x21e6ebc992f959dd0a2a6ce6034c414043c598b7f446c274efb3527c30dec254') + }) +}) diff --git a/src/logic/safe/store/actions/addOrUpdateSafe.ts b/src/logic/safe/store/actions/addOrUpdateSafe.ts new file mode 100644 index 0000000000..8aafa0ea49 --- /dev/null +++ b/src/logic/safe/store/actions/addOrUpdateSafe.ts @@ -0,0 +1,9 @@ +import { createAction } from 'redux-actions' + +import { SafeRecordProps } from '../models/safe' + +export const ADD_OR_UPDATE_SAFE = 'ADD_OR_UPDATE_SAFE' + +export const addOrUpdateSafe = createAction(ADD_OR_UPDATE_SAFE, (safe: SafeRecordProps) => ({ + safe, +})) diff --git a/src/logic/safe/store/actions/addSafe.ts b/src/logic/safe/store/actions/addSafe.ts index 0317a22f53..90851dab69 100644 --- a/src/logic/safe/store/actions/addSafe.ts +++ b/src/logic/safe/store/actions/addSafe.ts @@ -18,15 +18,16 @@ export const buildOwnersFrom = (names: string[], addresses: string[]): List ({ +export const addSafe = createAction(ADD_SAFE, (safe: SafeRecordProps, loadedFromStorage = false) => ({ safe, + loadedFromStorage, })) const saveSafe = (safe: SafeRecordProps) => (dispatch: Dispatch, getState: () => AppReduxState): void => { const state = getState() const safeList = safesListSelector(state) - dispatch(addSafe(safe)) + dispatch(addSafe(safe, true)) if (safeList.size === 0) { dispatch(setDefaultSafe(safe.address)) diff --git a/src/logic/safe/store/actions/allTransactions/loadAllTransactions.ts b/src/logic/safe/store/actions/allTransactions/loadAllTransactions.ts index 25148bdcc3..b054b123e8 100644 --- a/src/logic/safe/store/actions/allTransactions/loadAllTransactions.ts +++ b/src/logic/safe/store/actions/allTransactions/loadAllTransactions.ts @@ -2,7 +2,7 @@ import axios, { AxiosResponse } from 'axios' import { getAllTransactionsUriFrom, getTxServiceHost } from 'src/config' import { checksumAddress } from 'src/utils/checksumAddress' -import { Transaction } from '../../models/types/transactions' +import { Transaction } from '../../models/types/transactions.d' export type ServiceUriParams = { safeAddress: string @@ -30,8 +30,8 @@ const getAllTransactionsUri = (safeAddress: string): string => { const fetchAllTransactions = async ( urlParams: ServiceUriParams, - eTag: string | null, -): Promise<{ responseEtag: string; results: Transaction[]; count?: number }> => { + eTag?: string, +): Promise<{ responseEtag?: string; results: Transaction[]; count?: number }> => { const { safeAddress, limit, offset, orderBy, queued, trusted } = urlParams try { const url = getAllTransactionsUri(safeAddress) diff --git a/src/logic/safe/store/actions/allTransactions/pagination.ts b/src/logic/safe/store/actions/allTransactions/pagination.ts index ec2bf2e9da..a1b2b2372f 100644 --- a/src/logic/safe/store/actions/allTransactions/pagination.ts +++ b/src/logic/safe/store/actions/allTransactions/pagination.ts @@ -1,5 +1,5 @@ import { createAction } from 'redux-actions' -import { Transaction } from '../../models/types/transactions' +import { Transaction } from '../../models/types/transactions.d' export const LOAD_MORE_TRANSACTIONS = 'LOAD_MORE_TRANSACTIONS' diff --git a/src/logic/safe/store/actions/createTransaction.ts b/src/logic/safe/store/actions/createTransaction.ts index 6f0424a730..95ea40e5cb 100644 --- a/src/logic/safe/store/actions/createTransaction.ts +++ b/src/logic/safe/store/actions/createTransaction.ts @@ -5,6 +5,7 @@ import semverSatisfies from 'semver/functions/satisfies' import { ThunkAction } from 'redux-thunk' import { onboardUser } from 'src/components/ConnectButton' +import { decodeMethods } from 'src/logic/contracts/methodIds' import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { getNotificationsFromTxType } from 'src/logic/notifications' import { @@ -100,7 +101,7 @@ interface CreateTransactionArgs { navigateToTransactionsTab?: boolean notifiedTransaction: string operation?: number - origin?: string + origin?: string | null safeAddress: string to: string txData?: string @@ -110,6 +111,7 @@ interface CreateTransactionArgs { type CreateTransactionAction = ThunkAction, AppReduxState, undefined, AnyAction> type ConfirmEventHandler = (safeTxHash: string) => void +type ErrorEventHandler = () => void const createTransaction = ( { @@ -124,6 +126,7 @@ const createTransaction = ( origin = null, }: CreateTransactionArgs, onUserConfirm?: ConfirmEventHandler, + onError?: ErrorEventHandler, ): CreateTransactionAction => async (dispatch: Dispatch, getState: () => AppReduxState): Promise => { const state = getState() @@ -169,6 +172,7 @@ const createTransaction = ( sender: from, sigs, } + const safeTxHash = generateSafeTxHash(safeAddress, txArgs) try { // Here we're checking that safe contract version is greater or equal 1.1.1, but @@ -176,20 +180,19 @@ const createTransaction = ( const canTryOffchainSigning = !isExecution && !smartContractWallet && semverSatisfies(safeVersion, SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES) if (canTryOffchainSigning) { - const signature = await tryOffchainSigning({ ...txArgs, safeAddress }, hardwareWallet) + const signature = await tryOffchainSigning(safeTxHash, { ...txArgs, safeAddress }, hardwareWallet) if (signature) { dispatch(closeSnackbarAction({ key: beforeExecutionKey })) - - await saveTxToHistory({ ...txArgs, signature, origin }) dispatch(enqueueSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded)) - dispatch(fetchTransactions(safeAddress)) + + await saveTxToHistory({ ...txArgs, signature, origin }) + onUserConfirm?.(safeTxHash) return } } - const safeTxHash = generateSafeTxHash(safeAddress, txArgs) const tx = isExecution ? await getExecutionTransaction(txArgs) : await getApprovalTransaction(safeInstance, safeTxHash) @@ -205,6 +208,7 @@ const createTransaction = ( confirmations: [], // this is used to determine if a tx is pending or not. See `calculateTransactionStatus` helper value: txArgs.valueInWei, safeTxHash, + dataDecoded: decodeMethods(txArgs.data), submissionDate: new Date().toISOString(), } const mockedTx = await mockTransaction(txToMock, safeAddress, state) @@ -240,6 +244,8 @@ const createTransaction = ( dispatch(closeSnackbarAction({ key: pendingExecutionKey })) removeTxFromStore(mockedTx, safeAddress, dispatch, state) console.error('Tx error: ', error) + + onError?.() }) .then(async (receipt) => { if (pendingExecutionKey) { diff --git a/src/logic/safe/store/actions/fetchSafe.ts b/src/logic/safe/store/actions/fetchSafe.ts index bd95676e90..a398ffb733 100644 --- a/src/logic/safe/store/actions/fetchSafe.ts +++ b/src/logic/safe/store/actions/fetchSafe.ts @@ -6,7 +6,6 @@ import { getLocalSafe, getSafeName } from 'src/logic/safe/utils' import { enabledFeatures, safeNeedsUpdate } from 'src/logic/safe/utils/safeVersion' import { sameAddress } from 'src/logic/wallets/ethAddresses' import { getBalanceInEtherOf } from 'src/logic/wallets/getWeb3' -import addSafe from 'src/logic/safe/store/actions/addSafe' import addSafeOwner from 'src/logic/safe/store/actions/addSafeOwner' import removeSafeOwner from 'src/logic/safe/store/actions/removeSafeOwner' import updateSafe from 'src/logic/safe/store/actions/updateSafe' @@ -17,8 +16,9 @@ import { ModulePair, SafeOwner, SafeRecordProps } from 'src/logic/safe/store/mod import { Action, Dispatch } from 'redux' import { SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts' import { AppReduxState } from 'src/store' +import { latestMasterContractVersionSelector } from '../selectors' -const buildOwnersFrom = (safeOwners: string[], localSafe: SafeRecordProps): List => { +const buildOwnersFrom = (safeOwners: string[], localSafe?: SafeRecordProps): List => { const ownersList = safeOwners.map((ownerAddress) => { const convertedAdd = checksumAddress(ownerAddress) @@ -85,7 +85,7 @@ export const buildSafe = async ( needsUpdate, featuresEnabled, balances: Map(), - latestIncomingTxBlock: null, + latestIncomingTxBlock: 0, activeAssets: Set(), activeTokens: Set(), blacklistedAssets: Set(), @@ -114,11 +114,12 @@ export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: Dispatch ]) // Converts from [ { address, ownerName} ] to address array - const localOwners = localSafe ? localSafe.owners.map((localOwner) => localOwner.address) : undefined + const localOwners = localSafe ? localSafe.owners.map((localOwner) => localOwner.address) : [] dispatch( updateSafe({ address: safeAddress, + name: localSafe?.name, modules: buildModulesLinkedList(modules?.array, modules?.next), nonce: Number(remoteNonce), threshold: Number(remoteThreshold), @@ -126,30 +127,27 @@ export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: Dispatch ) // If the remote owners does not contain a local address, we remove that local owner - if (localOwners) { - localOwners.forEach((localAddress) => { - const remoteOwnerIndex = remoteOwners.findIndex((remoteAddress) => sameAddress(remoteAddress, localAddress)) - if (remoteOwnerIndex === -1) { - dispatch(removeSafeOwner({ safeAddress, ownerAddress: localAddress })) - } - }) + localOwners.forEach((localAddress) => { + const remoteOwnerIndex = remoteOwners.findIndex((remoteAddress) => sameAddress(remoteAddress, localAddress)) + if (remoteOwnerIndex === -1) { + dispatch(removeSafeOwner({ safeAddress, ownerAddress: localAddress })) + } + }) - // If the remote has an owner that we don't have locally, we add it - remoteOwners.forEach((remoteAddress) => { - const localOwnerIndex = localOwners.findIndex((localAddress) => sameAddress(remoteAddress, localAddress)) - if (localOwnerIndex === -1) { - dispatch( - addSafeOwner({ - safeAddress, - ownerAddress: remoteAddress, - ownerName: 'UNKNOWN', - }), - ) - } - }) - } + // If the remote has an owner that we don't have locally, we add it + remoteOwners.forEach((remoteAddress) => { + const localOwnerIndex = localOwners.findIndex((localAddress) => sameAddress(remoteAddress, localAddress)) + if (localOwnerIndex === -1) { + dispatch( + addSafeOwner({ + safeAddress, + ownerAddress: remoteAddress, + ownerName: 'UNKNOWN', + }), + ) + } + }) } - export default (safeAdd: string) => async ( dispatch: Dispatch, getState: () => AppReduxState, @@ -157,12 +155,15 @@ export default (safeAdd: string) => async ( try { const safeAddress = checksumAddress(safeAdd) const safeName = (await getSafeName(safeAddress)) || 'LOADED SAFE' - const latestMasterContractVersion = getState().safes.get('latestMasterContractVersion') + const latestMasterContractVersion = latestMasterContractVersionSelector(getState()) const safeProps = await buildSafe(safeAddress, safeName, latestMasterContractVersion) - dispatch(addSafe(safeProps)) + // `updateSafe`, as `loadSafesFromStorage` will populate the store previous to this call + // and `addSafe` will only add a newly non-existent safe + // For the case where the safe does not exist in the localStorage, + // `updateSafe` uses a default `notSetValue` to add the Safe to the store + dispatch(updateSafe(safeProps)) } catch (err) { - // eslint-disable-next-line console.error('Error while updating Safe information: ', err) return Promise.resolve() diff --git a/src/logic/safe/store/actions/loadSafesFromStorage.ts b/src/logic/safe/store/actions/loadSafesFromStorage.ts index e4a36dd969..c60098573d 100644 --- a/src/logic/safe/store/actions/loadSafesFromStorage.ts +++ b/src/logic/safe/store/actions/loadSafesFromStorage.ts @@ -1,19 +1,19 @@ -import { addSafe } from './addSafe' +import { Dispatch } from 'redux' import { SAFES_KEY } from 'src/logic/safe/utils' - +import { SafeRecordProps } from 'src/logic/safe/store/models/safe' import { buildSafe } from 'src/logic/safe/store/reducer/safe' - import { loadFromStorage } from 'src/utils/storage' -import { Dispatch } from 'redux' + +import { addSafe } from './addSafe' const loadSafesFromStorage = () => async (dispatch: Dispatch): Promise => { try { - const safes = await loadFromStorage(SAFES_KEY) + const safes = await loadFromStorage>(SAFES_KEY) if (safes) { Object.values(safes).forEach((safeProps) => { - dispatch(addSafe(buildSafe(safeProps))) + dispatch(addSafe(buildSafe(safeProps), true)) }) } } catch (err) { diff --git a/src/logic/safe/store/actions/processTransaction.ts b/src/logic/safe/store/actions/processTransaction.ts index 0791584c75..a4e2aa6e26 100644 --- a/src/logic/safe/store/actions/processTransaction.ts +++ b/src/logic/safe/store/actions/processTransaction.ts @@ -1,4 +1,3 @@ -import { fromJS } from 'immutable' import semverSatisfies from 'semver/functions/satisfies' import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' @@ -20,9 +19,9 @@ import { import { getLastTx, getNewTxNonce, shouldExecuteTransaction } from 'src/logic/safe/store/actions/utils' import { getErrorMessage } from 'src/test/utils/ethereumErrors' -import { makeConfirmation } from '../models/confirmation' import { storeTx } from './createTransaction' -import { TransactionStatus } from '../models/types/transaction' +import { TransactionStatus } from 'src/logic/safe/store/models/types/transaction' +import { makeConfirmation } from 'src/logic/safe/store/models/confirmation' const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddress, tx, userAddress }) => async ( dispatch, @@ -34,7 +33,7 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres const safeInstance = await getGnosisSafeInstanceAt(safeAddress) const lastTx = await getLastTx(safeAddress) - const nonce = await getNewTxNonce(null, lastTx, safeInstance) + const nonce = await getNewTxNonce(undefined, lastTx, safeInstance) const isExecution = approveAndExecute || (await shouldExecuteTransaction(safeInstance, nonce, lastTx)) const safeVersion = await getCurrentSafeVersion(safeInstance) @@ -79,12 +78,14 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres const canTryOffchainSigning = !isExecution && !smartContractWallet && semverSatisfies(safeVersion, SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES) if (canTryOffchainSigning) { - const signature = await tryOffchainSigning({ ...txArgs, safeAddress }, hardwareWallet) + const signature = await tryOffchainSigning(tx.safeTxHash, { ...txArgs, safeAddress }, hardwareWallet) if (signature) { dispatch(closeSnackbarAction(beforeExecutionKey)) await saveTxToHistory({ ...txArgs, signature }) + // TODO: while we wait for the tx to be stored in the service and later update the tx info + // we should update the tx status in the store to disable owners' action buttons dispatch(enqueueSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded)) dispatch(fetchTransactions(safeAddress)) @@ -105,9 +106,7 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres const txToMock: TxToMock = { ...txArgs, - confirmations: txArgs.confirmations, // this is used to determine if a tx is pending or not. See `calculateTransactionStatus` helper value: txArgs.valueInWei, - submissionDate: txArgs.submissionDate, } const mockedTx = await mockTransaction(txToMock, safeAddress, state) @@ -123,10 +122,14 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres await Promise.all([ saveTxToHistory({ ...txArgs, txHash }), storeTx( - mockedTx.updateIn( - ['ownersWithPendingActions', mockedTx.isCancellationTx ? 'reject' : 'confirm'], - (previous) => previous.push(from), - ), + mockedTx.withMutations((record) => { + record + .updateIn( + ['ownersWithPendingActions', mockedTx.isCancellationTx ? 'reject' : 'confirm'], + (previous) => previous.push(from), + ) + .set('status', TransactionStatus.PENDING) + }), safeAddress, dispatch, state, @@ -175,16 +178,20 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres : TransactionStatus.FAILED, ) .updateIn(['ownersWithPendingActions', 'reject'], (prev) => prev.clear()) + .updateIn(['ownersWithPendingActions', 'confirm'], (prev) => prev.clear()) + }) + : mockedTx.withMutations((record) => { + record + .updateIn(['ownersWithPendingActions', toStoreTx.isCancellationTx ? 'reject' : 'confirm'], (previous) => + previous.pop(), + ) + .set('status', TransactionStatus.AWAITING_CONFIRMATIONS) }) - : mockedTx.set('status', TransactionStatus.AWAITING_CONFIRMATIONS) await storeTx( - toStoreTx.withMutations((record) => { - record - .set('confirmations', fromJS([...tx.confirmations, makeConfirmation({ owner: from })])) - .updateIn(['ownersWithPendingActions', toStoreTx.isCancellationTx ? 'reject' : 'confirm'], (previous) => - previous.pop(from), - ) + toStoreTx.update('confirmations', (confirmations) => { + const index = confirmations.findIndex(({ owner }) => owner === from) + return index === -1 ? confirmations.push(makeConfirmation({ owner: from })) : confirmations }), safeAddress, dispatch, diff --git a/src/logic/safe/store/actions/transactions/fetchTransactions/fetchTransactions.ts b/src/logic/safe/store/actions/transactions/fetchTransactions/fetchTransactions.ts index d413879c4f..f4907bed07 100644 --- a/src/logic/safe/store/actions/transactions/fetchTransactions/fetchTransactions.ts +++ b/src/logic/safe/store/actions/transactions/fetchTransactions/fetchTransactions.ts @@ -27,7 +27,7 @@ async function fetchTransactions( txType: TransactionTypes.INCOMING | TransactionTypes.OUTGOING, safeAddress: string, eTag: string | null, -): Promise<{ eTag: string; results: TxServiceModel[] | IncomingTxServiceModel[] }> { +): Promise<{ eTag: string | null; results: TxServiceModel[] | IncomingTxServiceModel[] }> { try { const url = getServiceUrl(txType, safeAddress) const response = await axios.get(url, eTag ? { headers: { 'If-None-Match': eTag } } : undefined) diff --git a/src/logic/safe/store/actions/transactions/fetchTransactions/index.ts b/src/logic/safe/store/actions/transactions/fetchTransactions/index.ts index 07161d800e..09d00a0563 100644 --- a/src/logic/safe/store/actions/transactions/fetchTransactions/index.ts +++ b/src/logic/safe/store/actions/transactions/fetchTransactions/index.ts @@ -39,8 +39,9 @@ export default (safeAddress: string): ThunkAction, AppReduxState, } const incomingTransactions = await loadIncomingTransactions(safeAddress) + const safeIncomingTxs = incomingTransactions.get(safeAddress) - if (incomingTransactions.get(safeAddress).size) { + if (safeIncomingTxs?.size) { dispatch(addIncomingTransactions(incomingTransactions)) } } catch (error) { diff --git a/src/logic/safe/store/actions/transactions/fetchTransactions/loadIncomingTransactions.ts b/src/logic/safe/store/actions/transactions/fetchTransactions/loadIncomingTransactions.ts index 2b3b2e4981..ec79aa234a 100644 --- a/src/logic/safe/store/actions/transactions/fetchTransactions/loadIncomingTransactions.ts +++ b/src/logic/safe/store/actions/transactions/fetchTransactions/loadIncomingTransactions.ts @@ -45,7 +45,12 @@ const batchIncomingTxsTokenDataRequest = (txs: IncomingTxServiceModel[]) => { const batch = new web3ReadOnly.BatchRequest() const whenTxsValues = txs.map((tx) => { - const methods = ['symbol', 'decimals', { method: 'getTransaction', args: [tx.transactionHash], type: 'eth' }] + const methods = [ + 'symbol', + 'decimals', + { method: 'getTransaction', args: [tx.transactionHash], type: 'eth' }, + { method: 'getTransactionReceipt', args: [tx.transactionHash], type: 'eth' }, + ] return generateBatchRequests({ abi: ALTERNATIVE_TOKEN_ABI, @@ -59,17 +64,17 @@ const batchIncomingTxsTokenDataRequest = (txs: IncomingTxServiceModel[]) => { batch.execute() return Promise.all(whenTxsValues).then((txsValues) => - txsValues.map(([tx, symbol, decimals, { gas, gasPrice }]) => [ + txsValues.map(([tx, symbol, decimals, { gasPrice }, { gasUsed }]) => [ tx, symbol === null ? 'ETH' : symbol, decimals === null ? '18' : decimals, - new bn(gas).div(gasPrice).toFixed(), + new bn(gasPrice).times(gasUsed), ]), ) } -let previousETag = null -export const loadIncomingTransactions = async (safeAddress: string) => { +let previousETag: string | null = null +export const loadIncomingTransactions = async (safeAddress: string): Promise>> => { const { eTag, results } = await fetchTransactions(TransactionTypes.INCOMING, safeAddress, previousETag) previousETag = eTag diff --git a/src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions.ts b/src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions.ts index 67f9b4cab9..89d53d9599 100644 --- a/src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions.ts +++ b/src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions.ts @@ -27,8 +27,7 @@ export type TxServiceModel = { blockNumber?: number | null confirmations: ConfirmationServiceModel[] confirmationsRequired: number - creationTx?: boolean | null - data?: string | null + data: string | null dataDecoded?: DataDecoded ethGasPrice: string executionDate?: string | null @@ -40,15 +39,15 @@ export type TxServiceModel = { isExecuted: boolean isSuccessful: boolean modified: string - nonce?: number | null + nonce: number operation: number - origin?: string | null + origin: string | null refundReceiver: string safe: string safeTxGas: number safeTxHash: string signatures: string - submissionDate?: string | null + submissionDate: string | null to: string transactionHash?: string | null value: string @@ -78,7 +77,7 @@ export type BatchProcessTxsProps = OutgoingTxs & { */ const extractCancelAndOutgoingTxs = (safeAddress: string, outgoingTxs: TxServiceModel[]): OutgoingTxs => { return outgoingTxs.reduce( - (acc, transaction) => { + (acc: { cancellationTxs: Record; outgoingTxs: TxServiceModel[] }, transaction) => { if ( isCancelTransaction(transaction, safeAddress) && outgoingTxs.find((tx) => tx.nonce === transaction.nonce && !isCancelTransaction(tx, safeAddress)) @@ -164,7 +163,7 @@ const batchProcessOutgoingTransactions = async ({ // outgoing transactions const outgoingTxsWithData = outgoingTxs.length ? await batchRequestContractCode(outgoingTxs) : [] - const outgoing = [] + const outgoing: Transaction[] = [] for (const [tx, txCode] of outgoingTxsWithData) { outgoing.push( await buildTx({ @@ -182,7 +181,7 @@ const batchProcessOutgoingTransactions = async ({ return { cancel, outgoing } } -let previousETag = null +let previousETag: string | null = null export const loadOutgoingTransactions = async (safeAddress: string): Promise => { const defaultResponse = { cancel: Map(), diff --git a/src/logic/safe/store/actions/transactions/utils/newTransactionsHelpers.ts b/src/logic/safe/store/actions/transactions/utils/newTransactionsHelpers.ts index dbf8c096f4..9a93ed0088 100644 --- a/src/logic/safe/store/actions/transactions/utils/newTransactionsHelpers.ts +++ b/src/logic/safe/store/actions/transactions/utils/newTransactionsHelpers.ts @@ -1,4 +1,4 @@ -import { Transaction, TxType } from 'src/logic/safe/store/models/types/transactions' +import { Transaction, TxType } from 'src/logic/safe/store/models/types/transactions.d' export const isMultiSigTx = (tx: Transaction): boolean => { return TxType[tx.txType] === TxType.MULTISIG_TRANSACTION diff --git a/src/logic/safe/store/actions/transactions/utils/transactionHelpers.ts b/src/logic/safe/store/actions/transactions/utils/transactionHelpers.ts index 5349d7b669..9ac91d11f4 100644 --- a/src/logic/safe/store/actions/transactions/utils/transactionHelpers.ts +++ b/src/logic/safe/store/actions/transactions/utils/transactionHelpers.ts @@ -19,6 +19,7 @@ import { TransactionTypes, TransactionTypeValues, TxArgs, + RefundParams, } from 'src/logic/safe/store/models/types/transaction' import { CANCELLATION_TRANSACTIONS_REDUCER_ID } from 'src/logic/safe/store/reducer/cancellationTransactions' import { SAFE_REDUCER_ID } from 'src/logic/safe/store/reducer/safe' @@ -34,7 +35,7 @@ import { TypedDataUtils } from 'eth-sig-util' import { Token } from 'src/logic/tokens/store/model/token' import { ProviderRecord } from 'src/logic/wallets/store/model/provider' import { SafeRecord } from 'src/logic/safe/store/models/safe' -import { DecodedParams } from 'src/routes/safe/store/models/types/transactions.d' +import { DataDecoded, DecodedParams } from 'src/routes/safe/store/models/types/transactions.d' export const isEmptyData = (data?: string | null): boolean => { return !data || data === EMPTY_DATA @@ -65,15 +66,15 @@ export const isModifySettingsTransaction = (tx: TxServiceModel, safeAddress: str } export const isMultiSendTransaction = (tx: TxServiceModel): boolean => { - return !isEmptyData(tx.data) && tx.data.substring(0, 10) === '0x8d80ff0a' && Number(tx.value) === 0 + return !isEmptyData(tx.data) && tx.data?.substring(0, 10) === '0x8d80ff0a' && Number(tx.value) === 0 } export const isUpgradeTransaction = (tx: TxServiceModel): boolean => { return ( !isEmptyData(tx.data) && isMultiSendTransaction(tx) && - tx.data.substr(308, 8) === '7de7edef' && // 7de7edef - changeMasterCopy (308, 8) - tx.data.substr(550, 8) === 'f08a0323' // f08a0323 - setFallbackHandler (550, 8) + tx.data?.substr(308, 8) === '7de7edef' && // 7de7edef - changeMasterCopy (308, 8) + tx.data?.substr(550, 8) === 'f08a0323' // f08a0323 - setFallbackHandler (550, 8) ) } @@ -83,24 +84,24 @@ export const isOutgoingTransaction = (tx: TxServiceModel, safeAddress: string): export const isCustomTransaction = async ( tx: TxServiceModel, - txCode: string, + txCode: string | null, safeAddress: string, knownTokens: Map, ): Promise => { - return ( - isOutgoingTransaction(tx, safeAddress) && - !(await isSendERC20Transaction(tx, txCode, knownTokens)) && - !isUpgradeTransaction(tx) && - !isSendERC721Transaction(tx, txCode, knownTokens) - ) + const isOutgoing = isOutgoingTransaction(tx, safeAddress) + const isErc20 = await isSendERC20Transaction(tx, txCode, knownTokens) + const isUpgrade = isUpgradeTransaction(tx) + const isErc721 = isSendERC721Transaction(tx, txCode, knownTokens) + + return isOutgoing && !isErc20 && !isUpgrade && !isErc721 } export const getRefundParams = async ( tx: TxServiceModel, tokenInfo: (string) => Promise<{ decimals: number; symbol: string } | null>, -): Promise => { +): Promise => { const txGasPrice = Number(tx.gasPrice) - let refundParams = null + let refundParams: RefundParams | null = null if (txGasPrice > 0) { let refundSymbol = 'ETH' @@ -273,7 +274,6 @@ export const buildTx = async ({ blockNumber: tx.blockNumber, cancelled: isTxCancelled, confirmations, - creationTx: tx.creationTx, customTx: isCustomTx, data: tx.data ? tx.data : EMPTY_DATA, dataDecoded: tx.dataDecoded, @@ -282,6 +282,7 @@ export const buildTx = async ({ executionDate: tx.executionDate, executionTxHash: tx.transactionHash, executor: tx.executor, + fee: tx.fee, gasPrice: tx.gasPrice, gasToken: tx.gasToken || ZERO_ADDRESS, isCancellationTx, @@ -315,6 +316,7 @@ export type TxToMock = TxArgs & { safeTxHash: string value: string submissionDate: string + dataDecoded: DataDecoded | null } export const mockTransaction = (tx: TxToMock, safeAddress: string, state: AppReduxState): Promise => { @@ -325,7 +327,7 @@ export const mockTransaction = (tx: TxToMock, safeAddress: string, state: AppRed return buildTx({ cancellationTxs, - currentUser: null, + currentUser: undefined, knownTokens, outgoingTxs, safe, @@ -344,7 +346,7 @@ export const updateStoredTransactionsStatus = (dispatch: (any) => void, walletRe dispatch( addOrUpdateTransactions({ safeAddress, - transactions: transactions.withMutations((list) => + transactions: transactions.withMutations((list: any[]) => list.map((tx) => tx.set('status', calculateTransactionStatus(tx, safe, walletRecord.account))), ), }), diff --git a/src/logic/safe/store/actions/updateActiveAssets.ts b/src/logic/safe/store/actions/updateActiveAssets.ts index 4f107c17b2..80be2139c0 100644 --- a/src/logic/safe/store/actions/updateActiveAssets.ts +++ b/src/logic/safe/store/actions/updateActiveAssets.ts @@ -1,17 +1,9 @@ -import updateSafe from './updateSafe' +import { Set } from 'immutable' +import updateAssetsList from './updateAssetsList' +import { Dispatch } from 'src/logic/safe/store/actions/types.d' -// the selector uses ownProps argument/router props to get the address of the safe -// so in order to use it I had to recreate the same structure -// const generateMatchProps = (safeAddress: string) => ({ -// match: { -// params: { -// [SAFE_PARAM_ADDRESS]: safeAddress, -// }, -// }, -// }) - -const updateActiveAssets = (safeAddress, activeAssets) => async (dispatch) => { - dispatch(updateSafe({ address: safeAddress, activeAssets })) +const updateActiveAssets = (safeAddress: string, activeAssets: Set) => (dispatch: Dispatch): void => { + dispatch(updateAssetsList({ safeAddress, activeAssets })) } export default updateActiveAssets diff --git a/src/logic/safe/store/actions/updateActiveTokens.ts b/src/logic/safe/store/actions/updateActiveTokens.ts index 922c020f3c..012836c9e1 100644 --- a/src/logic/safe/store/actions/updateActiveTokens.ts +++ b/src/logic/safe/store/actions/updateActiveTokens.ts @@ -1,4 +1,6 @@ -import updateSafe from './updateSafe' +import { Set } from 'immutable' +import updateTokensList from './updateTokensList' +import { Dispatch } from 'src/logic/safe/store/actions/types.d' // the selector uses ownProps argument/router props to get the address of the safe // so in order to use it I had to recreate the same structure @@ -10,8 +12,8 @@ import updateSafe from './updateSafe' // }, // }) -const updateActiveTokens = (safeAddress, activeTokens) => async (dispatch) => { - dispatch(updateSafe({ address: safeAddress, activeTokens })) +const updateActiveTokens = (safeAddress: string, activeTokens: Set) => (dispatch: Dispatch): void => { + dispatch(updateTokensList({ safeAddress, activeTokens })) } export default updateActiveTokens diff --git a/src/logic/safe/store/actions/updateAssetsList.ts b/src/logic/safe/store/actions/updateAssetsList.ts new file mode 100644 index 0000000000..0f098e2868 --- /dev/null +++ b/src/logic/safe/store/actions/updateAssetsList.ts @@ -0,0 +1,7 @@ +import { createAction } from 'redux-actions' + +export const UPDATE_ASSETS_LIST = 'UPDATE_ASSETS_LIST' + +const updateAssetsList = createAction(UPDATE_ASSETS_LIST) + +export default updateAssetsList diff --git a/src/logic/safe/store/actions/updateBlacklistedAssets.ts b/src/logic/safe/store/actions/updateBlacklistedAssets.ts index 76e28b2155..8b52bbfdc4 100644 --- a/src/logic/safe/store/actions/updateBlacklistedAssets.ts +++ b/src/logic/safe/store/actions/updateBlacklistedAssets.ts @@ -1,7 +1,9 @@ -import updateSafe from './updateSafe' +import { Set } from 'immutable' +import updateAssetsList from './updateAssetsList' +import { Dispatch } from 'src/logic/safe/store/actions/types.d' -const updateBlacklistedAssets = (safeAddress, blacklistedAssets) => async (dispatch) => { - dispatch(updateSafe({ address: safeAddress, blacklistedAssets })) +const updateBlacklistedAssets = (safeAddress: string, blacklistedAssets: Set) => (dispatch: Dispatch): void => { + dispatch(updateAssetsList({ safeAddress, blacklistedAssets })) } export default updateBlacklistedAssets diff --git a/src/logic/safe/store/actions/updateBlacklistedTokens.ts b/src/logic/safe/store/actions/updateBlacklistedTokens.ts index 1337184f47..e81293b952 100644 --- a/src/logic/safe/store/actions/updateBlacklistedTokens.ts +++ b/src/logic/safe/store/actions/updateBlacklistedTokens.ts @@ -1,7 +1,9 @@ -import updateSafe from './updateSafe' +import { Set } from 'immutable' +import updateTokensList from './updateTokensList' +import { Dispatch } from 'src/logic/safe/store/actions/types.d' -const updateBlacklistedTokens = (safeAddress, blacklistedTokens) => async (dispatch) => { - dispatch(updateSafe({ address: safeAddress, blacklistedTokens })) +const updateBlacklistedTokens = (safeAddress: string, blacklistedTokens: Set) => (dispatch: Dispatch): void => { + dispatch(updateTokensList({ safeAddress, blacklistedTokens })) } export default updateBlacklistedTokens diff --git a/src/logic/safe/store/actions/updateTokensList.ts b/src/logic/safe/store/actions/updateTokensList.ts new file mode 100644 index 0000000000..91123bd609 --- /dev/null +++ b/src/logic/safe/store/actions/updateTokensList.ts @@ -0,0 +1,7 @@ +import { createAction } from 'redux-actions' + +export const UPDATE_TOKENS_LIST = 'UPDATE_TOKENS_LIST' + +const updateTokenList = createAction(UPDATE_TOKENS_LIST) + +export default updateTokenList diff --git a/src/logic/safe/store/actions/utils.ts b/src/logic/safe/store/actions/utils.ts index 2dee2f096d..74b59b0d3c 100644 --- a/src/logic/safe/store/actions/utils.ts +++ b/src/logic/safe/store/actions/utils.ts @@ -4,7 +4,7 @@ import axios from 'axios' import { buildTxServiceUrl } from 'src/logic/safe/transactions/txHistory' -export const getLastTx = async (safeAddress: string): Promise => { +export const getLastTx = async (safeAddress: string): Promise => { try { const url = buildTxServiceUrl(safeAddress) const response = await axios.get(url, { params: { limit: 1 } }) @@ -17,23 +17,22 @@ export const getLastTx = async (safeAddress: string): Promise => } export const getNewTxNonce = async ( - txNonce: string | null, - lastTx: TxServiceModel, + txNonce: string | undefined, + lastTx: TxServiceModel | null, safeInstance: GnosisSafe, ): Promise => { - if (!Number.isInteger(Number.parseInt(txNonce, 10))) { - return lastTx === null - ? // use current's safe nonce as fallback - (await safeInstance.methods.nonce().call()).toString() - : `${lastTx.nonce + 1}` + if (txNonce) { + return txNonce } - return txNonce + + // use current's safe nonce as fallback + return lastTx ? `${lastTx.nonce + 1}` : (await safeInstance.methods.nonce().call()).toString() } export const shouldExecuteTransaction = async ( safeInstance: GnosisSafe, nonce: string, - lastTx: TxServiceModel, + lastTx: TxServiceModel | null, ): Promise => { const threshold = await safeInstance.methods.getThreshold().call() @@ -45,7 +44,7 @@ export const shouldExecuteTransaction = async ( // by the user using the exec button. const canExecuteCurrentTransaction = lastTx && lastTx.isExecuted - return isFirstTransaction || canExecuteCurrentTransaction + return isFirstTransaction || !!canExecuteCurrentTransaction } return false diff --git a/src/logic/safe/store/middleware/notificationsMiddleware.ts b/src/logic/safe/store/middleware/notificationsMiddleware.ts index c30f5fa142..617bdd3d10 100644 --- a/src/logic/safe/store/middleware/notificationsMiddleware.ts +++ b/src/logic/safe/store/middleware/notificationsMiddleware.ts @@ -85,7 +85,7 @@ const notificationsMiddleware = (store) => (next) => async (action) => { const safes = safesMapSelector(state) const currentSafe = safes.get(safeAddress) - if (!isUserAnOwner(currentSafe, userAddress) || awaitingTransactions.size === 0) { + if (!currentSafe || !isUserAnOwner(currentSafe, userAddress) || awaitingTransactions.size === 0) { break } @@ -103,8 +103,8 @@ const notificationsMiddleware = (store) => (next) => async (action) => { } case ADD_INCOMING_TRANSACTIONS: { action.payload.forEach((incomingTransactions, safeAddress) => { - const { latestIncomingTxBlock } = state.safes.get('safes').get(safeAddress) - const viewedSafes = state.currentSession ? state.currentSession.get('viewedSafes') : [] + const { latestIncomingTxBlock } = state.safes.get('safes').get(safeAddress, {}) + const viewedSafes = state.currentSession['viewedSafes'] const recurringUser = viewedSafes?.includes(safeAddress) const newIncomingTransactions = incomingTransactions.filter((tx) => tx.blockNumber > latestIncomingTxBlock) diff --git a/src/logic/safe/store/middleware/safeStorage.ts b/src/logic/safe/store/middleware/safeStorage.ts index e5b2feef66..ee9152c238 100644 --- a/src/logic/safe/store/middleware/safeStorage.ts +++ b/src/logic/safe/store/middleware/safeStorage.ts @@ -1,4 +1,3 @@ -import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' import { addAddressBookEntry } from 'src/logic/addressBook/store/actions/addAddressBookEntry' import { saveDefaultSafe, saveSafes } from 'src/logic/safe/utils' import { tokensSelector } from 'src/logic/tokens/store/selectors' @@ -12,17 +11,30 @@ import { REMOVE_SAFE_OWNER } from 'src/logic/safe/store/actions/removeSafeOwner' import { REPLACE_SAFE_OWNER } from 'src/logic/safe/store/actions/replaceSafeOwner' import { SET_DEFAULT_SAFE } from 'src/logic/safe/store/actions/setDefaultSafe' import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe' +import { UPDATE_TOKENS_LIST } from 'src/logic/safe/store/actions/updateTokensList' +import { UPDATE_ASSETS_LIST } from 'src/logic/safe/store/actions/updateAssetsList' import { getActiveTokensAddressesForAllSafes, safesMapSelector } from 'src/logic/safe/store/selectors' +import { checksumAddress } from 'src/utils/checksumAddress' +import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' +import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry' +import { checkIfEntryWasDeletedFromAddressBook, isValidAddressBookName } from 'src/logic/addressBook/utils' +import { addressBookSelector } from 'src/logic/addressBook/store/selectors' +import { sameAddress } from 'src/logic/wallets/ethAddresses' +import { updateAddressBookEntry } from 'src/logic/addressBook/store/actions/updateAddressBookEntry' +import { ADD_OR_UPDATE_SAFE } from 'src/logic/safe/store/actions/addOrUpdateSafe' const watchedActions = [ ADD_SAFE, UPDATE_SAFE, REMOVE_SAFE, + ADD_OR_UPDATE_SAFE, ADD_SAFE_OWNER, REMOVE_SAFE_OWNER, REPLACE_SAFE_OWNER, EDIT_SAFE_OWNER, ACTIVATE_TOKEN_FOR_ALL_SAFES, + UPDATE_TOKENS_LIST, + UPDATE_ASSETS_LIST, SET_DEFAULT_SAFE, ] @@ -48,6 +60,7 @@ const safeStorageMware = (store) => (next) => async (action) => { const state = store.getState() const { dispatch } = store const safes = safesMapSelector(state) + const addressBook = addressBookSelector(state) await saveSafes(safes.toJSON()) switch (action.type) { @@ -56,19 +69,60 @@ const safeStorageMware = (store) => (next) => async (action) => { break } case ADD_SAFE: { + const { safe, loadedFromStorage } = action.payload + const safeAlreadyLoaded = + loadedFromStorage || safes.find((safeIterator) => sameAddress(safeIterator.address, safe.address)) + + safe.owners.forEach((owner) => { + const checksumEntry = makeAddressBookEntry({ address: checksumAddress(owner.address), name: owner.name }) + + const ownerWasAlreadyInAddressBook = checkIfEntryWasDeletedFromAddressBook( + checksumEntry, + addressBook, + safeAlreadyLoaded, + ) + + if (!ownerWasAlreadyInAddressBook) { + dispatch(addAddressBookEntry(checksumEntry, { notifyEntryUpdate: false })) + } + const addressAlreadyExists = addressBook.find((entry) => sameAddress(entry.address, checksumEntry.address)) + if (isValidAddressBookName(checksumEntry.name) && addressAlreadyExists) { + dispatch(updateAddressBookEntry(checksumEntry)) + } + }) + const safeWasAlreadyInAddressBook = checkIfEntryWasDeletedFromAddressBook( + { address: safe.address, name: safe.name }, + addressBook, + safeAlreadyLoaded, + ) + + if (!safeWasAlreadyInAddressBook) { + dispatch( + addAddressBookEntry(makeAddressBookEntry({ address: safe.address, name: safe.name }), { + notifyEntryUpdate: true, + }), + ) + } + break + } + case ADD_OR_UPDATE_SAFE: { const { safe } = action.payload - const ownersArray = safe.owners.toJS() - // Adds the owners to the address book - ownersArray.forEach((owner) => { - dispatch(addAddressBookEntry(makeAddressBookEntry({ ...owner, isOwner: true }))) + safe.owners.forEach((owner) => { + const checksumEntry = makeAddressBookEntry({ address: checksumAddress(owner.address), name: owner.name }) + if (isValidAddressBookName(checksumEntry.name)) { + dispatch(addOrUpdateAddressBookEntry(checksumEntry)) + } }) break } case UPDATE_SAFE: { - const { activeTokens } = action.payload + const { activeTokens, name, address } = action.payload if (activeTokens) { recalculateActiveTokens(state) } + if (name) { + dispatch(addOrUpdateAddressBookEntry(makeAddressBookEntry({ name, address }))) + } break } case SET_DEFAULT_SAFE: { diff --git a/src/logic/safe/store/models/types/transaction.ts b/src/logic/safe/store/models/types/transaction.ts index 150233b818..2dbb07116e 100644 --- a/src/logic/safe/store/models/types/transaction.ts +++ b/src/logic/safe/store/models/types/transaction.ts @@ -2,7 +2,7 @@ import { List, Map, RecordOf } from 'immutable' import { Confirmation } from './confirmation' import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d' import { DataDecoded, Transfer } from './transactions' -import { DecodedParams } from 'src/routes/safe/store/models/types/transactions' +import { DecodedParams } from 'src/routes/safe/store/models/types/transactions.d' export enum TransactionTypes { INCOMING = 'incoming', @@ -33,6 +33,7 @@ export enum PendingActionType { REJECT = 'reject', } export type PendingActionValues = PendingActionType[keyof PendingActionType] +export type RefundParams = { fee: string; symbol: string } export type TransactionProps = { baseGas: number @@ -43,7 +44,7 @@ export type TransactionProps = { creator: string creationTx: boolean customTx: boolean - data?: string | null + data: string | null dataDecoded: DataDecoded | null decimals?: (number | string) | null decodedParams: DecodedParams | null @@ -51,7 +52,7 @@ export type TransactionProps = { executionTxHash?: string | null executor: string factoryAddress: string - fee?: string // It will be replace with the new TXs types. + fee: string | null // It will be replace with the new TXs types. gasPrice: string gasToken: string isCancellationTx: boolean @@ -63,18 +64,18 @@ export type TransactionProps = { masterCopy: string modifySettingsTx: boolean multiSendTx: boolean - nonce?: number | null + nonce: number operation: number origin: string | null ownersWithPendingActions: Map> recipient: string - refundParams: any + refundParams: RefundParams | null refundReceiver: string safeTxGas: number safeTxHash: string setupData: string - status?: TransactionStatus - submissionDate?: string | null + status: TransactionStatus + submissionDate: string | null symbol?: string | null transactionHash: string | null transfers?: Transfer[] @@ -87,7 +88,7 @@ export type Transaction = RecordOf export type TxArgs = { baseGas: number - data?: string | null + data: string gasPrice: string gasToken: string nonce: number diff --git a/src/logic/safe/store/reducer/allTransactions.ts b/src/logic/safe/store/reducer/allTransactions.ts index 4e95bb7d7f..dcfcb3e54b 100644 --- a/src/logic/safe/store/reducer/allTransactions.ts +++ b/src/logic/safe/store/reducer/allTransactions.ts @@ -1,7 +1,10 @@ import { handleActions } from 'redux-actions' -import { Transaction } from '../models/types/transactions' -import { LOAD_MORE_TRANSACTIONS, LoadMoreTransactionsAction } from '../actions/allTransactions/pagination' +import { Transaction } from 'src/logic/safe/store/models/types/transactions.d' +import { + LOAD_MORE_TRANSACTIONS, + LoadMoreTransactionsAction, +} from 'src/logic/safe/store/actions/allTransactions/pagination' export const TRANSACTIONS = 'allTransactions' diff --git a/src/logic/safe/store/reducer/safe.ts b/src/logic/safe/store/reducer/safe.ts index 7a1e9b4b69..3d170f8e6d 100644 --- a/src/logic/safe/store/reducer/safe.ts +++ b/src/logic/safe/store/reducer/safe.ts @@ -1,4 +1,4 @@ -import { Map, Set } from 'immutable' +import { Map, Set, List } from 'immutable' import { handleActions } from 'redux-actions' import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from 'src/logic/safe/store/actions/activateTokenForAllSafes' @@ -11,10 +11,14 @@ import { REPLACE_SAFE_OWNER } from 'src/logic/safe/store/actions/replaceSafeOwne import { SET_DEFAULT_SAFE } from 'src/logic/safe/store/actions/setDefaultSafe' import { SET_LATEST_MASTER_CONTRACT_VERSION } from 'src/logic/safe/store/actions/setLatestMasterContractVersion' import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe' +import { UPDATE_TOKENS_LIST } from 'src/logic/safe/store/actions/updateTokensList' +import { UPDATE_ASSETS_LIST } from 'src/logic/safe/store/actions/updateAssetsList' import { makeOwner } from 'src/logic/safe/store/models/owner' import makeSafe, { SafeRecordProps } from 'src/logic/safe/store/models/safe' import { checksumAddress } from 'src/utils/checksumAddress' import { SafeReducerMap } from 'src/routes/safe/store/reducer/types/safe' +import { ADD_OR_UPDATE_SAFE } from 'src/logic/safe/store/actions/addOrUpdateSafe' +import { sameAddress } from 'src/logic/wallets/ethAddresses' export const SAFE_REDUCER_ID = 'safes' export const DEFAULT_SAFE_INITIAL_STATE = 'NOT_ASKED' @@ -37,11 +41,37 @@ export const buildSafe = (storedSafe: SafeRecordProps): SafeRecordProps => { blacklistedTokens, activeAssets, blacklistedAssets, - latestIncomingTxBlock: null, + latestIncomingTxBlock: 0, modules: null, } } +const updateSafeProps = (prevSafe, safe) => { + return prevSafe.withMutations((record) => { + // Every property is updated individually to overcome the issue with nested data being overwritten + const safeProperties = Object.keys(safe) + + // We check each safe property sent in action.payload + safeProperties.forEach((key) => { + if (safe[key] && typeof safe[key] === 'object') { + if (safe[key].length >= 0) { + // If type is array we update the array + record.update(key, () => safe[key]) + } else if (safe[key].size >= 0) { + // If type is Immutable List we replace current List + // If type is Object we do a merge + List.isList(safe[key]) + ? record.update(key, (current) => current.set(safe[key])) + : record.update(key, (current) => current.merge(safe[key])) + } + } else { + // By default we overwrite the value. This is for strings, numbers and unset values + record.set(key, safe[key]) + } + }) + }) +} + export default handleActions( { [UPDATE_SAFE]: (state: SafeReducerMap, action) => { @@ -50,8 +80,8 @@ export default handleActions( return state.updateIn( ['safes', safeAddress], - makeSafe({ name: 'LOADED SAFE', address: safeAddress }), - (prevSafe) => prevSafe.merge(safe), + makeSafe({ name: safe?.name || 'LOADED SAFE', address: safeAddress }), + (prevSafe) => updateSafeProps(prevSafe, safe), ) }, [ACTIVATE_TOKEN_FOR_ALL_SAFES]: (state: SafeReducerMap, action) => { @@ -65,7 +95,7 @@ export default handleActions( const safeActiveTokens = map.getIn(['safes', safeAddress, 'activeTokens']) const activeTokens = safeActiveTokens.add(tokenAddress) - map.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.merge({ activeTokens })) + map.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.mergeDeep({ activeTokens })) }) }) }, @@ -77,11 +107,24 @@ export default handleActions( // with initial props and it would overwrite existing ones if (state.hasIn(['safes', safe.address])) { - return state.updateIn(['safes', safe.address], (prevSafe) => prevSafe.merge(safe)) + return state } return state.setIn(['safes', safe.address], makeSafe(safe)) }, + [ADD_OR_UPDATE_SAFE]: (state: SafeReducerMap, action) => { + const { safe } = action.payload + + if (!state.hasIn(['safes', safe.address])) { + return state.setIn(['safes', safe.address], makeSafe(safe)) + } + + return state.updateIn( + ['safes', safe.address], + makeSafe({ name: 'LOADED SAFE', address: safe.address }), + (prevSafe) => updateSafeProps(prevSafe, safe), + ) + }, [REMOVE_SAFE]: (state: SafeReducerMap, action) => { const safeAddress = action.payload @@ -90,6 +133,14 @@ export default handleActions( [ADD_SAFE_OWNER]: (state: SafeReducerMap, action) => { const { ownerAddress, ownerName, safeAddress } = action.payload + const addressFound = state + .getIn(['safes', safeAddress]) + .owners.find((owner) => sameAddress(owner.address, ownerAddress)) + + if (addressFound) { + return state + } + return state.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.merge({ owners: prevSafe.owners.push(makeOwner({ address: ownerAddress, name: ownerName })), @@ -127,6 +178,24 @@ export default handleActions( return prevSafe.merge({ owners: updatedOwners }) }) }, + [UPDATE_TOKENS_LIST]: (state: SafeReducerMap, action) => { + // Only activeTokens or blackListedTokens is required + const { safeAddress, activeTokens, blacklistedTokens } = action.payload + + const key = activeTokens ? 'activeTokens' : 'blacklistedTokens' + const list = activeTokens ?? blacklistedTokens + + return state.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.set(key, list)) + }, + [UPDATE_ASSETS_LIST]: (state: SafeReducerMap, action) => { + // Only activeAssets or blackListedAssets is required + const { safeAddress, activeAssets, blacklistedAssets } = action.payload + + const key = activeAssets ? 'activeAssets' : 'blacklistedAssets' + const list = activeAssets ?? blacklistedAssets + + return state.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.set(key, list)) + }, [SET_DEFAULT_SAFE]: (state: SafeReducerMap, action) => state.set('defaultSafe', action.payload), [SET_LATEST_MASTER_CONTRACT_VERSION]: (state: SafeReducerMap, action) => state.set('latestMasterContractVersion', action.payload), diff --git a/src/logic/safe/store/selectors/allTransactions.ts b/src/logic/safe/store/selectors/allTransactions.ts index 211a6def44..e8729e6150 100644 --- a/src/logic/safe/store/selectors/allTransactions.ts +++ b/src/logic/safe/store/selectors/allTransactions.ts @@ -12,11 +12,11 @@ export const allTransactionsSelector = createSelector(getTransactionsStateSelect export const safeAllTransactionsSelector = createSelector( safeParamAddressFromStateSelector, allTransactionsSelector, - (safeAddress, transactions) => transactions[safeAddress]?.transactions || [], + (safeAddress, transactions) => (safeAddress ? transactions[safeAddress]?.transactions : []), ) export const safeTotalTransactionsAmountSelector = createSelector( safeParamAddressFromStateSelector, allTransactionsSelector, - (safeAddress, transactions) => transactions[safeAddress]?.totalTransactionsCount || 0, + (safeAddress, transactions) => (safeAddress ? transactions[safeAddress]?.totalTransactionsCount : 0), ) diff --git a/src/logic/safe/store/selectors/index.ts b/src/logic/safe/store/selectors/index.ts index 9c6d3f9c59..df9d6de6a5 100644 --- a/src/logic/safe/store/selectors/index.ts +++ b/src/logic/safe/store/selectors/index.ts @@ -36,7 +36,7 @@ const cancellationTransactionsSelector = (state: AppReduxState) => state[CANCELL const incomingTransactionsSelector = (state: AppReduxState) => state[INCOMING_TRANSACTIONS_REDUCER_ID] -export const safeParamAddressFromStateSelector = (state: AppReduxState): string | null => { +export const safeParamAddressFromStateSelector = (state: AppReduxState): string => { const match = matchPath<{ safeAddress: string }>(state.router.location.pathname, { path: `${SAFELIST_ADDRESS}/:safeAddress`, }) @@ -45,7 +45,7 @@ export const safeParamAddressFromStateSelector = (state: AppReduxState): string return checksumAddress(match.params.safeAddress) } - return null + return '' } export const safeParamAddressSelector = ( @@ -177,16 +177,16 @@ export const safeBlacklistedAssetsSelector = createSelector( ) export const safeActiveAssetsSelectorBySafe = (safeAddress: string, safes: SafesMap): Set => - safes.get(safeAddress).get('activeAssets') + safes.get(safeAddress)?.get('activeAssets') || Set() export const safeBlacklistedAssetsSelectorBySafe = (safeAddress: string, safes: SafesMap): Set => - safes.get(safeAddress).get('blacklistedAssets') + safes.get(safeAddress)?.get('blacklistedAssets') || Set() const baseSafe = makeSafe() export const safeFieldSelector = (field: K) => ( safe: SafeRecord, -): SafeRecordProps[K] | null => (safe ? safe.get(field, baseSafe.get(field)) : null) +): SafeRecordProps[K] | undefined => (safe ? safe.get(field, baseSafe.get(field)) : undefined) export const safeNameSelector = createSelector(safeSelector, safeFieldSelector('name')) diff --git a/src/logic/safe/store/tests/safe.balances.test.ts b/src/logic/safe/store/tests/safe.balances.test.ts new file mode 100644 index 0000000000..4238d4c3dd --- /dev/null +++ b/src/logic/safe/store/tests/safe.balances.test.ts @@ -0,0 +1,59 @@ +import { Set, Map } from 'immutable' +import { aNewStore } from 'src/store' +import updateActiveTokens from 'src/logic/safe/store/actions/updateActiveTokens' +import '@testing-library/jest-dom/extend-expect' +import updateSafe from 'src/logic/safe/store/actions/updateSafe' +import { makeToken } from 'src/logic/tokens/store/model/token' +import { safesMapSelector } from 'src/logic/safe/store/selectors' + +describe('Feature > Balances', () => { + let store + const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf' + beforeEach(async () => { + store = aNewStore() + }) + + it('It should return an updated balance when updates active tokens', async () => { + // given + const tokensAmount = '100' + const token = makeToken({ + address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', + name: 'OmiseGo', + symbol: 'OMG', + decimals: 18, + logoUri: + 'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true', + }) + const balances = Map({ + [token.address]: tokensAmount, + }) + const expectedResult = '100' + + // when + store.dispatch(updateSafe({ address: safeAddress, balances })) + store.dispatch(updateActiveTokens(safeAddress, Set([token.address]))) + + const safe = safesMapSelector(store.getState()).get(safeAddress) + const balanceResult = safe?.get('balances').get(token.address) + const activeTokens = safe?.get('activeTokens') + const tokenIsActive = activeTokens?.has(token.address) + + // then + expect(balanceResult).toBe(expectedResult) + expect(tokenIsActive).toBe(true) + }) + + it('The store should have an updated ether balance after updating the value', async () => { + // given + const etherAmount = '1' + const expectedResult = '1' + + // when + store.dispatch(updateSafe({ address: safeAddress, ethBalance: etherAmount })) + const safe = safesMapSelector(store.getState()).get(safeAddress) + const balanceResult = safe?.get('ethBalance') + + // then + expect(balanceResult).toBe(expectedResult) + }) +}) diff --git a/src/logic/safe/transactions/awaitingTransactions.ts b/src/logic/safe/transactions/awaitingTransactions.ts index ff05bb0ebd..85ea65680a 100644 --- a/src/logic/safe/transactions/awaitingTransactions.ts +++ b/src/logic/safe/transactions/awaitingTransactions.ts @@ -1,8 +1,13 @@ import { List } from 'immutable' import { isPendingTransaction } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers' +import { Transaction } from 'src/logic/safe/store/models/types/transaction' -export const getAwaitingTransactions = (allTransactions = List([]), cancellationTxs, userAccount: string) => { +export const getAwaitingTransactions = ( + allTransactions: List, + cancellationTxs, + userAccount: string, +): List => { return allTransactions.filter((tx) => { const cancelTx = !!tx.nonce && !isNaN(Number(tx.nonce)) ? cancellationTxs.get(`${tx.nonce}`) : null diff --git a/src/logic/safe/transactions/gasNew.ts b/src/logic/safe/transactions/gasNew.ts index 9a54fcb771..91015c6206 100644 --- a/src/logic/safe/transactions/gasNew.ts +++ b/src/logic/safe/transactions/gasNew.ts @@ -25,7 +25,7 @@ const estimateDataGasCosts = (data: string): number => { return accumulator + 16 } - return data.match(/.{2}/g).reduce(reducer, 0) + return data.match(/.{2}/g)?.reduce(reducer, 0) } export const estimateTxGasCosts = async ( @@ -38,6 +38,11 @@ export const estimateTxGasCosts = async ( try { const web3 = getWeb3() const from = await getAccountFrom(web3) + + if (!from) { + return 0 + } + const safeInstance = (new web3.eth.Contract(GnosisSafeSol.abi as AbiItem[], safeAddress) as unknown) as GnosisSafe const nonce = await safeInstance.methods.nonce().call() const threshold = await safeInstance.methods.getThreshold().call() diff --git a/src/logic/safe/transactions/incomingTxHistory.ts b/src/logic/safe/transactions/incomingTxHistory.ts index 6e6b19cbe1..c7d78e5476 100644 --- a/src/logic/safe/transactions/incomingTxHistory.ts +++ b/src/logic/safe/transactions/incomingTxHistory.ts @@ -1,7 +1,7 @@ import { getIncomingTxServiceUriTo, getTxServiceHost } from 'src/config' import { checksumAddress } from 'src/utils/checksumAddress' -export const buildIncomingTxServiceUrl = (safeAddress) => { +export const buildIncomingTxServiceUrl = (safeAddress: string): string => { const host = getTxServiceHost() const address = checksumAddress(safeAddress) const base = getIncomingTxServiceUriTo(address) diff --git a/src/logic/safe/transactions/offchainSigner/EIP712Signer.ts b/src/logic/safe/transactions/offchainSigner/EIP712Signer.ts index 9b77ae2fc5..55fe61d728 100644 --- a/src/logic/safe/transactions/offchainSigner/EIP712Signer.ts +++ b/src/logic/safe/transactions/offchainSigner/EIP712Signer.ts @@ -1,5 +1,6 @@ -import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions' +import { AbstractProvider } from 'web3-core' import { getWeb3 } from 'src/logic/wallets/getWeb3' +import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions' const EIP712_NOT_SUPPORTED_ERROR_MSG = "EIP712 is not supported by user's wallet" @@ -59,7 +60,7 @@ const generateTypedDataFrom = async ({ } export const getEIP712Signer = (version?: string) => async (txArgs) => { - const web3: any = getWeb3() + const web3 = getWeb3() const typedData = await generateTypedDataFrom(txArgs) let method = 'eth_signTypedData_v3' @@ -80,13 +81,14 @@ export const getEIP712Signer = (version?: string) => async (txArgs) => { } return new Promise((resolve, reject) => { - web3.currentProvider.sendAsync(signedTypedData, (err, signature) => { + const provider = web3.currentProvider as AbstractProvider + provider.sendAsync(signedTypedData, (err, signature) => { if (err) { reject(err) return } - if (signature.result == null) { + if (signature?.result == null) { reject(new Error(EIP712_NOT_SUPPORTED_ERROR_MSG)) return } diff --git a/src/logic/safe/transactions/offchainSigner/ethSigner.ts b/src/logic/safe/transactions/offchainSigner/ethSigner.ts index 005a983efd..5daeedba84 100644 --- a/src/logic/safe/transactions/offchainSigner/ethSigner.ts +++ b/src/logic/safe/transactions/offchainSigner/ethSigner.ts @@ -4,26 +4,13 @@ import { AbstractProvider } from 'web3-core/types' const ETH_SIGN_NOT_SUPPORTED_ERROR_MSG = 'ETH_SIGN_NOT_SUPPORTED' -export const ethSigner = async ({ - baseGas, - data, - gasPrice, - gasToken, - nonce, - operation, - refundReceiver, - safeInstance, - safeTxGas, - sender, - to, - valueInWei, -}): Promise => { +type EthSignerArgs = { + safeTxHash: string + sender: string +} + +export const ethSigner = async ({ safeTxHash, sender }: EthSignerArgs): Promise => { const web3 = await getWeb3() - const txHash = await safeInstance.methods - .getTransactionHash(to, valueInWei, data, operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, nonce) - .call({ - from: sender, - }) return new Promise(function (resolve, reject) { const provider = web3.currentProvider as AbstractProvider @@ -31,7 +18,7 @@ export const ethSigner = async ({ { jsonrpc: '2.0', method: 'eth_sign', - params: [sender, txHash], + params: [sender, safeTxHash], id: new Date().getTime(), }, async function (err, signature) { @@ -39,7 +26,7 @@ export const ethSigner = async ({ return reject(err) } - if (signature.result == null) { + if (signature?.result == null) { reject(new Error(ETH_SIGN_NOT_SUPPORTED_ERROR_MSG)) return } diff --git a/src/logic/safe/transactions/offchainSigner/index.ts b/src/logic/safe/transactions/offchainSigner/index.ts index 047c04d74d..6113d568db 100644 --- a/src/logic/safe/transactions/offchainSigner/index.ts +++ b/src/logic/safe/transactions/offchainSigner/index.ts @@ -8,7 +8,7 @@ import { ethSigner } from './ethSigner' const SIGNERS = { EIP712_V3: getEIP712Signer('v3'), EIP712_V4: getEIP712Signer('v4'), - EIP712: getEIP712Signer() as any, + EIP712: getEIP712Signer(), ETH_SIGN: ethSigner, } @@ -18,13 +18,13 @@ const getSignersByWallet = (isHW) => export const SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES = '>=1.1.1' -export const tryOffchainSigning = async (txArgs, isHW) => { +export const tryOffchainSigning = async (safeTxHash: string, txArgs, isHW: boolean): Promise => { let signature const signerByWallet = getSignersByWallet(isHW) for (const signingFunc of signerByWallet) { try { - signature = await signingFunc(txArgs) + signature = await signingFunc({ ...txArgs, safeTxHash }) break } catch (err) { diff --git a/src/logic/safe/transactions/send.ts b/src/logic/safe/transactions/send.ts index aa8b06df82..67bbab2f1a 100644 --- a/src/logic/safe/transactions/send.ts +++ b/src/logic/safe/transactions/send.ts @@ -1,6 +1,6 @@ import { NonPayableTransactionObject } from 'src/types/contracts/types.d' import { TxArgs } from 'src/logic/safe/store/models/types/transaction' -import { GnosisSafe } from 'src/types/contracts/GnosisSafe' +import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d' export const CALL = 0 export const DELEGATE_CALL = 1 diff --git a/src/logic/safe/utils/safeStorage.ts b/src/logic/safe/utils/safeStorage.ts index c782db82c7..d2fccea0ec 100644 --- a/src/logic/safe/utils/safeStorage.ts +++ b/src/logic/safe/utils/safeStorage.ts @@ -2,20 +2,20 @@ import { loadFromStorage, saveToStorage } from 'src/utils/storage' import { SafeRecordProps } from 'src/logic/safe/store/models/safe' export const SAFES_KEY = 'SAFES' -export const TX_KEY = 'TX' export const DEFAULT_SAFE_KEY = 'DEFAULT_SAFE' -export const getSafeName = async (safeAddress: string): Promise => { - const safes = await loadFromStorage(SAFES_KEY) - if (!safes) { - return undefined - } - const safe = safes[safeAddress] +type StoredSafes = Record - return safe ? safe.name : undefined +export const loadStoredSafes = (): Promise => { + return loadFromStorage(SAFES_KEY) +} + +export const getSafeName = async (safeAddress: string): Promise => { + const safes = await loadStoredSafes() + return safes?.[safeAddress]?.name } -export const saveSafes = async (safes) => { +export const saveSafes = async (safes: StoredSafes): Promise => { try { await saveToStorage(SAFES_KEY, safes) } catch (err) { @@ -23,9 +23,9 @@ export const saveSafes = async (safes) => { } } -export const getLocalSafe = async (safeAddress: string): Promise => { - const storedSafes = (await loadFromStorage(SAFES_KEY)) || {} - return storedSafes[safeAddress] || null +export const getLocalSafe = async (safeAddress: string): Promise => { + const storedSafes = await loadStoredSafes() + return storedSafes?.[safeAddress] } export const getDefaultSafe = async (): Promise => { diff --git a/src/logic/safe/utils/safeVersion.ts b/src/logic/safe/utils/safeVersion.ts index f0ab2736bd..5fe5ee3460 100644 --- a/src/logic/safe/utils/safeVersion.ts +++ b/src/logic/safe/utils/safeVersion.ts @@ -11,13 +11,15 @@ export const FEATURES = [ { name: 'ERC1155', validVersion: '>=1.1.1' }, ] -export const safeNeedsUpdate = (currentVersion: string, latestVersion: string): boolean => { +type Feature = typeof FEATURES[number] + +export const safeNeedsUpdate = (currentVersion?: string, latestVersion?: string): boolean => { if (!currentVersion || !latestVersion) { return false } - const current = semverValid(currentVersion) - const latest = semverValid(latestVersion) + const current = semverValid(currentVersion) as string + const latest = semverValid(latestVersion) as string return latest ? semverLessThan(current, latest) : false } @@ -26,7 +28,7 @@ export const getCurrentSafeVersion = (gnosisSafeInstance: GnosisSafe): Promise - FEATURES.reduce((acc, feature) => { + FEATURES.reduce((acc: string[], feature: Feature) => { if (semverSatisfies(version, feature.validVersion)) { acc.push(feature.name) } @@ -44,11 +46,11 @@ export const checkIfSafeNeedsUpdate = async ( lastSafeVersion: string, ): Promise => { if (!gnosisSafeInstance || !lastSafeVersion) { - return null + throw new Error('checkIfSafeNeedsUpdate: No Safe Instance or version provided') } const safeMasterVersion = await getCurrentSafeVersion(gnosisSafeInstance) - const current = semverValid(safeMasterVersion) - const latest = semverValid(lastSafeVersion) + const current = semverValid(safeMasterVersion) as string + const latest = semverValid(lastSafeVersion) as string const needUpdate = safeNeedsUpdate(safeMasterVersion, lastSafeVersion) return { current, latest, needUpdate } @@ -69,7 +71,7 @@ export const getCurrentMasterContractLastVersion = async (): Promise => export const getSafeVersionInfo = async (safeAddress: string): Promise => { try { - const safeMaster = await getGnosisSafeInstanceAt(safeAddress) + const safeMaster = getGnosisSafeInstanceAt(safeAddress) const lastSafeVersion = await getCurrentMasterContractLastVersion() return checkIfSafeNeedsUpdate(safeMaster, lastSafeVersion) } catch (err) { diff --git a/src/logic/tokens/store/actions/fetchSafeTokens.ts b/src/logic/tokens/store/actions/fetchSafeTokens.ts index 79df106730..608256c65a 100644 --- a/src/logic/tokens/store/actions/fetchSafeTokens.ts +++ b/src/logic/tokens/store/actions/fetchSafeTokens.ts @@ -48,7 +48,7 @@ const extractDataFromResult = (currentTokens: TokenState) => ( if (tokenAddress === null) { acc.ethBalance = humanReadableValue(balance, 18) } else { - acc.balances = acc.balances.merge({ [tokenAddress]: humanReadableValue(balance, Number(token.decimals)) }) + acc.balances = acc.balances.merge({ [tokenAddress]: humanReadableValue(balance, Number(token?.decimals)) }) if (currentTokens && !currentTokens.get(tokenAddress)) { acc.tokens = acc.tokens.push(makeToken({ address: tokenAddress, ...token })) @@ -57,7 +57,7 @@ const extractDataFromResult = (currentTokens: TokenState) => ( acc.currencyList = acc.currencyList.push( makeBalanceCurrency({ - currencyName: balanceUsd ? AVAILABLE_CURRENCIES.USD : null, + currencyName: balanceUsd ? AVAILABLE_CURRENCIES.USD : undefined, tokenAddress, balanceInBaseCurrency: balanceUsd, balanceInSelectedCurrency: balanceUsd, diff --git a/src/logic/tokens/store/actions/fetchTokens.ts b/src/logic/tokens/store/actions/fetchTokens.ts index a2156698de..5c5bde4959 100644 --- a/src/logic/tokens/store/actions/fetchTokens.ts +++ b/src/logic/tokens/store/actions/fetchTokens.ts @@ -12,8 +12,10 @@ import { fetchTokenList } from 'src/logic/tokens/api' import { makeToken, Token } from 'src/logic/tokens/store/model/token' import { tokensSelector } from 'src/logic/tokens/store/selectors' import { getWeb3 } from 'src/logic/wallets/getWeb3' -import { store } from 'src/store' +import { AppReduxState, store } from 'src/store' import { ensureOnce } from 'src/utils/singleton' +import { ThunkDispatch } from 'redux-thunk' +import { AnyAction } from 'redux' const createStandardTokenContract = async () => { const web3 = getWeb3() @@ -43,7 +45,7 @@ export const getStandardTokenContract = ensureOnce(createStandardTokenContract) export const getERC721TokenContract = ensureOnce(createERC721TokenContract) -export const containsMethodByHash = async (contractAddress, methodHash) => { +export const containsMethodByHash = async (contractAddress: string, methodHash: string): Promise => { const web3 = getWeb3() const byteCode = await web3.eth.getCode(contractAddress) @@ -57,11 +59,7 @@ const getTokenValues = (tokenAddress) => methods: ['decimals', 'name', 'symbol'], }) -export const getTokenInfos = async (tokenAddress: string): Promise => { - if (!tokenAddress) { - return null - } - +export const getTokenInfos = async (tokenAddress: string): Promise => { const { tokens } = store.getState() const localToken = tokens.get(tokenAddress) @@ -74,7 +72,7 @@ export const getTokenInfos = async (tokenAddress: string): Promise => { const [tokenDecimals, tokenName, tokenSymbol] = await getTokenValues(tokenAddress) if (tokenDecimals === null) { - return null + return undefined } const token = makeToken({ @@ -91,7 +89,10 @@ export const getTokenInfos = async (tokenAddress: string): Promise => { return token } -export const fetchTokens = () => async (dispatch, getState) => { +export const fetchTokens = () => async ( + dispatch: ThunkDispatch, + getState: () => AppReduxState, +): Promise => { try { const currentSavedTokens = tokensSelector(getState()) diff --git a/src/logic/tokens/store/model/token.ts b/src/logic/tokens/store/model/token.ts index fc072be21d..933059aa5e 100644 --- a/src/logic/tokens/store/model/token.ts +++ b/src/logic/tokens/store/model/token.ts @@ -5,7 +5,7 @@ export type TokenProps = { name: string symbol: string decimals: number | string - logoUri?: string | null + logoUri: string balance?: number | string } diff --git a/src/test/logic/token/utils/formatAmount.test.ts b/src/logic/tokens/utils/__tests__/formatAmount.test.ts similarity index 87% rename from src/test/logic/token/utils/formatAmount.test.ts rename to src/logic/tokens/utils/__tests__/formatAmount.test.ts index 94fc60c90d..8dd5a0d663 100644 --- a/src/test/logic/token/utils/formatAmount.test.ts +++ b/src/logic/tokens/utils/__tests__/formatAmount.test.ts @@ -1,8 +1,7 @@ import { formatAmount, formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount' - describe('formatAmount', () => { - it('Given 0 returns 0', () => { + it('Given 0 returns 0', () => { // given const input = '0' const expectedResult = '0' @@ -13,7 +12,7 @@ describe('formatAmount', () => { // then expect(result).toBe(expectedResult) }) - it('Given 1 returns 1', () => { + it('Given 1 returns 1', () => { // given const input = '1' const expectedResult = '1' @@ -24,7 +23,7 @@ describe('formatAmount', () => { // then expect(result).toBe(expectedResult) }) - it('Given a string in format XXXXX.XXX returns a number of format XX,XXX.XXX', () => { + it('Given a string in format XXXXX.XXX returns a number of format XX,XXX.XXX', () => { // given const input = '19797.899' const expectedResult = '19,797.899' @@ -35,7 +34,7 @@ describe('formatAmount', () => { // then expect(result).toBe(expectedResult) }) - it('Given number > 0.001 && < 1000 returns the same number as string', () => { + it('Given number > 0.001 && < 1000 returns the same number as string', () => { // given const input = 999 const expectedResult = '999' @@ -45,7 +44,7 @@ describe('formatAmount', () => { // then expect(result).toBe(expectedResult) }) - it('Given a number between 1000 and 10000 returns a number of format XX,XXX', () => { + it('Given a number between 1000 and 10000 returns a number of format XX,XXX', () => { // given const input = 9999 const expectedResult = '9,999' @@ -55,7 +54,7 @@ describe('formatAmount', () => { // then expect(result).toBe(expectedResult) }) - it('Given a number between 10000 and 100000 returns a number of format XX,XXX', () => { + it('Given a number between 10000 and 100000 returns a number of format XX,XXX', () => { // given const input = 99999 const expectedResult = '99,999' @@ -65,7 +64,7 @@ describe('formatAmount', () => { // then expect(result).toBe(expectedResult) }) - it('Given a number between 100000 and 1000000 returns a number of format XXX,XXX', () => { + it('Given a number between 100000 and 1000000 returns a number of format XXX,XXX', () => { // given const input = 999999 const expectedResult = '999,999' @@ -75,7 +74,7 @@ describe('formatAmount', () => { // then expect(result).toBe(expectedResult) }) - it('Given a number between 10000000 and 100000000 returns a number of format X,XXX,XXX', () => { + it('Given a number between 10000000 and 100000000 returns a number of format X,XXX,XXX', () => { // given const input = 9999999 const expectedResult = '9,999,999' @@ -85,7 +84,7 @@ describe('formatAmount', () => { // then expect(result).toBe(expectedResult) }) - it('Given number < 0.001 returns < 0.001', () => { + it('Given number < 0.001 returns < 0.001', () => { // given const input = 0.000001 const expectedResult = '< 0.001' @@ -95,7 +94,7 @@ describe('formatAmount', () => { // then expect(result).toBe(expectedResult) }) - it('Given number > 10 ** 15 returns > 1000T', () => { + it('Given number > 10 ** 15 returns > 1000T', () => { // given const input = 10 ** 15 * 2 const expectedResult = '> 1000T' @@ -109,7 +108,7 @@ describe('formatAmount', () => { }) describe('FormatsAmountsInUsFormat', () => { - it('Given 0 returns 0.00', () => { + it('Given 0 returns 0.00', () => { // given const input = 0 const expectedResult = '0.00' @@ -120,7 +119,7 @@ describe('FormatsAmountsInUsFormat', () => { // then expect(result).toBe(expectedResult) }) - it('Given 1 returns 1.00', () => { + it('Given 1 returns 1.00', () => { // given const input = 1 const expectedResult = '1.00' @@ -131,9 +130,9 @@ describe('FormatsAmountsInUsFormat', () => { // then expect(result).toBe(expectedResult) }) - it('Given a number in format XXXXX.XX returns a number of format XXX,XXX.XX', () => { + it('Given a number in format XXXXX.XX returns a number of format XXX,XXX.XX', () => { // given - const input = 311137.30 + const input = 311137.3 const expectedResult = '311,137.30' // when @@ -142,7 +141,7 @@ describe('FormatsAmountsInUsFormat', () => { // then expect(result).toBe(expectedResult) }) - it('Given a number in format XXXXX.XXX returns a number of format XX,XXX.XXX', () => { + it('Given a number in format XXXXX.XXX returns a number of format XX,XXX.XXX', () => { // given const input = 19797.899 const expectedResult = '19,797.899' @@ -153,7 +152,7 @@ describe('FormatsAmountsInUsFormat', () => { // then expect(result).toBe(expectedResult) }) - it('Given a number in format XXXXXXXX.XXX returns a number of format XX,XXX,XXX.XXX', () => { + it('Given a number in format XXXXXXXX.XXX returns a number of format XX,XXX,XXX.XXX', () => { // given const input = 19797899.479 const expectedResult = '19,797,899.479' @@ -164,7 +163,7 @@ describe('FormatsAmountsInUsFormat', () => { // then expect(result).toBe(expectedResult) }) - it('Given a number in format XXXXXXXXXXX.XXX returns a number of format XX,XXX,XXX,XXX.XXX', () => { + it('Given a number in format XXXXXXXXXXX.XXX returns a number of format XX,XXX,XXX,XXX.XXX', () => { // given const input = 19797899479.999 const expectedResult = '19,797,899,479.999' @@ -176,4 +175,3 @@ describe('FormatsAmountsInUsFormat', () => { expect(result).toBe(expectedResult) }) }) - diff --git a/src/logic/tokens/utils/__tests__/tokenHelpers.test.ts b/src/logic/tokens/utils/__tests__/tokenHelpers.test.ts new file mode 100644 index 0000000000..7afee4ed9e --- /dev/null +++ b/src/logic/tokens/utils/__tests__/tokenHelpers.test.ts @@ -0,0 +1,174 @@ +import { makeToken } from 'src/logic/tokens/store/model/token' +import { getERC20DecimalsAndSymbol, isERC721Contract, isTokenTransfer } from 'src/logic/tokens/utils/tokenHelpers' +import { getMockedTxServiceModel } from 'src/test/utils/safeHelper' + +describe('isTokenTransfer', () => { + const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf' + it('It should return false if the transaction has no value but but "transfer" function signature is encoded in the data', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: '0xa9059cbb' }) + const expectedResult = true + // when + const result = isTokenTransfer(transaction) + + // then + expect(result).toEqual(expectedResult) + }) + it('It should return false if the transaction has no value but and no "transfer" function signature encoded in data', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: '0xa9055cbb' }) + const expectedResult = false + // when + const result = isTokenTransfer(transaction) + + // then + expect(result).toEqual(expectedResult) + }) + it('It should return false if the transaction has empty data', () => { + // given + const transaction = getMockedTxServiceModel({ to: safeAddress, value: '0', data: null }) + const expectedResult = false + // when + const result = isTokenTransfer(transaction) + + // then + expect(result).toEqual(expectedResult) + }) +}) + +jest.mock('src/logic/tokens/store/actions/fetchTokens') +jest.mock('src/logic/contracts/generateBatchRequests') +jest.mock('console') +describe('getERC20DecimalsAndSymbol', () => { + afterAll(() => { + jest.unmock('src/logic/tokens/store/actions/fetchTokens') + jest.unmock('src/logic/contracts/generateBatchRequests') + jest.unmock('console') + }) + it('It should return DAI information from the store if given a DAI address', async () => { + // given + const tokenAddress = '0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa' + const decimals = Number(18) + const symbol = 'DAI' + const token = makeToken({ + address: tokenAddress, + name: 'Dai', + symbol, + decimals, + logoUri: 'https://gnosis-safe-token-logos.s3.amazonaws.com/0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa.png', + balance: 0, + }) + const expectedResult = { + decimals, + symbol, + } + + const fetchTokens = require('src/logic/tokens/store/actions/fetchTokens') + const spy = fetchTokens.getTokenInfos.mockImplementationOnce(() => token) + + // when + const result = await getERC20DecimalsAndSymbol(tokenAddress) + + // then + expect(result).toEqual(expectedResult) + expect(spy).toHaveBeenCalled() + }) + it('It should return default value decimals: 18, symbol: UNKNOWN if given a token address and if there is an error fetching the data', async () => { + // given + const tokenAddress = '0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa' + const decimals = Number(18) + const symbol = 'UNKNOWN' + + const expectedResult = { + decimals, + symbol, + } + + const fetchTokens = require('src/logic/tokens/store/actions/fetchTokens') + const spy = fetchTokens.getTokenInfos.mockImplementationOnce(() => { + throw new Error() + }) + console.error = jest.fn() + const spyConsole = jest.spyOn(console, 'error').mockImplementation() + + // when + const result = await getERC20DecimalsAndSymbol(tokenAddress) + + // then + expect(result).toEqual(expectedResult) + expect(spy).toHaveBeenCalled() + expect(spyConsole).toHaveBeenCalled() + }) + it("It should fetch token information from the blockchain if given a token address and if the token doesn't exist in redux store", async () => { + // given + const tokenAddress = '0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa' + const decimals = Number(18) + const symbol = 'DAI' + const expectedResult = { + decimals, + symbol, + } + + const fetchTokens = require('src/logic/tokens/store/actions/fetchTokens') + const generateBatchRequests = require('src/logic/contracts/generateBatchRequests') + const spyTokenInfos = fetchTokens.getTokenInfos.mockImplementationOnce(() => null) + + const spyGenerateBatchRequest = generateBatchRequests.default.mockImplementationOnce(() => [decimals, symbol]) + + // when + const result = await getERC20DecimalsAndSymbol(tokenAddress) + + // then + expect(result).toEqual(expectedResult) + expect(spyTokenInfos).toHaveBeenCalled() + expect(spyGenerateBatchRequest).toHaveBeenCalled() + }) +}) + +describe('isERC721Contract', () => { + afterAll(() => { + jest.unmock('src/logic/tokens/store/actions/fetchTokens') + }) + beforeEach(() => { + jest.mock('src/logic/tokens/store/actions/fetchTokens') + }) + it('It should return false if given non-erc721 contract address', async () => { + // given + const contractAddress = '0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa' // DAI Address + const expectedResult = false + + const ERC721Contract = { + at: () => { + throw new Error('Contract is not ERC721') + }, + } + + const fetchTokens = require('src/logic/tokens/store/actions/fetchTokens') + const standardContractSpy = fetchTokens.getStandardTokenContract.mockImplementation(() => ERC721Contract) + + // when + const result = await isERC721Contract(contractAddress) + + // then + expect(result).toEqual(expectedResult) + expect(standardContractSpy).toHaveBeenCalled + }) + it('It should return true if given a Erc721 contract address', async () => { + // given + const contractAddress = '0x014d5883274ab3a9708b0f1e4263df6e90160a30' // dummy ft Address + const ERC721Contract = { + at: (address) => address === contractAddress, + } + const expectedResult = true + + const fetchTokens = require('src/logic/tokens/store/actions/fetchTokens') + const standardContractSpy = fetchTokens.getStandardTokenContract.mockImplementation(() => ERC721Contract) + + // when + const result = await isERC721Contract(contractAddress) + + // then + expect(result).toEqual(expectedResult) + expect(standardContractSpy).toHaveBeenCalled() + }) +}) diff --git a/src/logic/tokens/utils/tokenHelpers.ts b/src/logic/tokens/utils/tokenHelpers.ts index 493aa1e09b..48134bdb2b 100644 --- a/src/logic/tokens/utils/tokenHelpers.ts +++ b/src/logic/tokens/utils/tokenHelpers.ts @@ -35,18 +35,18 @@ export const isAddressAToken = async (tokenAddress: string): Promise => // } catch { // return 'Not a token address' // } - const call = await web3.eth.call({ to: tokenAddress, data: web3.utils.sha3('totalSupply()') }) + const call = await web3.eth.call({ to: tokenAddress, data: web3.utils.sha3('totalSupply()') as string }) return call !== '0x' } export const isTokenTransfer = (tx: TxServiceModel): boolean => { - return !isEmptyData(tx.data) && tx.data.substring(0, 10) === '0xa9059cbb' && Number(tx.value) === 0 + return !isEmptyData(tx.data) && tx.data?.substring(0, 10) === '0xa9059cbb' && Number(tx.value) === 0 } export const isSendERC721Transaction = ( tx: TxServiceModel, - txCode: string, + txCode: string | null, knownTokens: Map, ): boolean => { // "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85" - ens token contract, includes safeTransferFrom @@ -78,7 +78,7 @@ export const getERC20DecimalsAndSymbol = async ( try { const storedTokenInfo = await getTokenInfos(tokenAddress) - if (storedTokenInfo === null) { + if (!storedTokenInfo) { const [tokenDecimals, tokenSymbol] = await generateBatchRequests({ abi: ALTERNATIVE_TOKEN_ABI, address: tokenAddress, @@ -96,7 +96,7 @@ export const getERC20DecimalsAndSymbol = async ( export const isSendERC20Transaction = async ( tx: TxServiceModel, - txCode: string, + txCode: string | null, knownTokens: Map, ): Promise => { let isSendTokenTx = !isSendERC721Transaction(tx, txCode, knownTokens) && isTokenTransfer(tx) @@ -118,8 +118,8 @@ export const isERC721Contract = async (contractAddress: string): Promise { +export const sameAddress = (firstAddress: string | undefined, secondAddress: string | undefined): boolean => { if (!firstAddress) { return false } @@ -20,7 +20,7 @@ export const shortVersionOf = (value: string, cut: number): string => { } const final = value.length - cut - if (value.length < final) { + if (value.length <= cut) { return value } diff --git a/src/logic/wallets/getWeb3.ts b/src/logic/wallets/getWeb3.ts index 17e06a133d..c3c7b0f407 100644 --- a/src/logic/wallets/getWeb3.ts +++ b/src/logic/wallets/getWeb3.ts @@ -96,7 +96,7 @@ const isSmartContractWallet = async (web3Provider: Web3, account: string): Promi } export const getProviderInfo = async (web3Instance: Web3, providerName = 'Wallet'): Promise => { - const account = await getAccountFrom(web3Instance) + const account = (await getAccountFrom(web3Instance)) || '' const network = await getNetworkIdFrom(web3Instance) const smartContractWallet = await isSmartContractWallet(web3Instance, account) const hardwareWallet = isHardwareWallet(providerName) diff --git a/src/logic/wallets/store/middlewares/providerWatcher.ts b/src/logic/wallets/store/middlewares/providerWatcher.ts index 0dedb32def..fa938cf685 100644 --- a/src/logic/wallets/store/middlewares/providerWatcher.ts +++ b/src/logic/wallets/store/middlewares/providerWatcher.ts @@ -16,8 +16,7 @@ export const loadLastUsedProvider = async (): Promise => { return lastUsedProvider } -let watcherInterval = null - +let watcherInterval const providerWatcherMware = (store) => (next) => async (action) => { const handledAction = next(action) diff --git a/src/logic/wallets/tests/ethAddresses.test.ts b/src/logic/wallets/tests/ethAddresses.test.ts new file mode 100644 index 0000000000..5191d21bbf --- /dev/null +++ b/src/logic/wallets/tests/ethAddresses.test.ts @@ -0,0 +1,282 @@ +//@ts-nocheck +import { + isUserAnOwner, + isUserAnOwnerOfAnySafe, + isValidEnsName, + sameAddress, + shortVersionOf, +} from 'src/logic/wallets/ethAddresses' +import makeSafe from 'src/logic/safe/store/models/safe' +import { makeOwner } from 'src/logic/safe/store/models/owner' +import { List } from 'immutable' + +describe('Utility function: sameAddress', () => { + it('It should return false if no address given', () => { + // given + const safeAddress = null + const safeAddress2 = '0x344B941b1aAE2e4Be73987212FC4741687Bf0503' + + // when + const result = sameAddress(safeAddress, safeAddress2) + + // then + expect(result).toBe(false) + }) + it('It should return false if not second address given', () => { + // given + const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf' + const safeAddress2 = null + + // when + const result = sameAddress(safeAddress, safeAddress2) + + // then + expect(result).toBe(false) + }) + it('It should return true if two equal addresses given', () => { + // given + const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf' + + // when + const result = sameAddress(safeAddress, safeAddress) + + // then + expect(result).toBe(true) + }) + it('If should return false if two different addresses given', () => { + // given + const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf' + const safeAddress2 = '0x344B941b1aAE2e4Be73987212FC4741687Bf0503' + + // when + const result = sameAddress(safeAddress, safeAddress2) + + // then + expect(result).toBe(false) + }) +}) + +describe('Utility function: shortVersionOf', () => { + it('It should return Unknown if no address given', () => { + // given + const safeAddress = null + const cut = 5 + const expectedResult = 'Unknown' + + // when + const result = shortVersionOf(safeAddress, cut) + + // then + expect(result).toBe(expectedResult) + }) + it('It should return 0x344...f0503 if given 0x344B941b1aAE2e4Be73987212FC4741687Bf0503 and a cut = 5', () => { + // given + const safeAddress = '0x344B941b1aAE2e4Be73987212FC4741687Bf0503' + const cut = 5 + const expectedResult = `0x344...f0503` + + // when + const result = shortVersionOf(safeAddress, cut) + + // then + expect(result).toBe(expectedResult) + }) + it('If should return the same address if a cut value bigger than the address length given', () => { + // given + const safeAddress = '0x344B941b1aAE2e4Be73987212FC4741687Bf0503' + const cut = safeAddress.length + const expectedResult = safeAddress + + // when + const result = shortVersionOf(safeAddress, cut) + + // then + expect(result).toBe(expectedResult) + }) +}) + +describe('Utility function: isUserAnOwner', () => { + it("Should return false if there's no Safe", () => { + // given + const userAddress = 'address1' + const safeInstance = null + const expectedResult = false + + // when + const result = isUserAnOwner(safeInstance, userAddress) + + // then + expect(result).toBe(expectedResult) + }) + it("Should return false if there's no `userAccount`", () => { + // given + const userAddress = null + const owners = List([makeOwner({ address: userAddress })]) + const safeInstance = makeSafe({ owners }) + const expectedResult = false + + // when + const result = isUserAnOwner(safeInstance, userAddress) + + // then + expect(result).toBe(expectedResult) + }) + it('Should return false if there are no owners for the Safe', () => { + // given + const userAddress = 'address1' + const owners = null + const safeInstance = makeSafe({ owners }) + const expectedResult = false + + // when + const result = isUserAnOwner(safeInstance, userAddress) + + // then + expect(result).toBe(expectedResult) + }) + it("Should return true if `userAccount` is not in the list of Safe's owners", () => { + // given + const userAddress = 'address1' + const owners = List([makeOwner({ address: userAddress })]) + const safeInstance = makeSafe({ owners }) + const expectedResult = true + + // when + const result = isUserAnOwner(safeInstance, userAddress) + + // then + expect(result).toBe(expectedResult) + }) + it("Should return false if `userAccount` is not in the list of Safe's owners", () => { + // given + const userAddress = 'address1' + const userAddress2 = 'address2' + const owners = List([makeOwner({ address: userAddress })]) + const safeInstance = makeSafe({ owners }) + const expectedResult = false + + // when + const result = isUserAnOwner(safeInstance, userAddress2) + + // then + expect(result).toBe(expectedResult) + }) +}) + +describe('Utility function: isUserAnOwnerOfAnySafe', () => { + it('Should return true if given a list of safes, one of them has an owner equal to the userAccount', () => { + // given + const userAddress = 'address1' + const userAddress2 = 'address2' + const owners1 = List([makeOwner({ address: userAddress })]) + const owners2 = List([makeOwner({ address: userAddress2 })]) + const safeInstance = makeSafe({ owners: owners1 }) + const safeInstance2 = makeSafe({ owners: owners2 }) + const safesList = List([safeInstance, safeInstance2]) + const expectedResult = true + + // when + const result = isUserAnOwnerOfAnySafe(safesList, userAddress) + + // then + expect(result).toBe(expectedResult) + }) + it('It should return false if given a list of safes, none of them has an owner equal to the userAccount', () => { + // given + const userAddress = 'address1' + const userAddress2 = 'address2' + const userAddress3 = 'address3' + const owners1 = List([makeOwner({ address: userAddress3 })]) + const owners2 = List([makeOwner({ address: userAddress2 })]) + const safeInstance = makeSafe({ owners: owners1 }) + const safeInstance2 = makeSafe({ owners: owners2 }) + const safesList = List([safeInstance, safeInstance2]) + const expectedResult = false + + // when + const result = isUserAnOwnerOfAnySafe(safesList, userAddress) + + // then + expect(result).toBe(expectedResult) + }) +}) + +describe('Utility function: isValidEnsName', () => { + it('If should return false if given no ens name', () => { + // given + const ensName = null + const expectedResult = false + + // when + const result = isValidEnsName(ensName) + + // then + expect(result).toBe(expectedResult) + }) + it('It should return false for an ens without extension in format [value].[eth|test|xyz|luxe]', () => { + // given + const ensName = 'test' + const expectedResult = false + + // when + const result = isValidEnsName(ensName) + + // then + expect(result).toBe(expectedResult) + }) + it('It should return false for an ens without the format [value].[eth|test|xyz|luxe]', () => { + // given + const ensName = 'test.et12312' + const expectedResult = false + + // when + const result = isValidEnsName(ensName) + + // then + expect(result).toBe(expectedResult) + }) + it('It should return true for an ens in format [value].eth', () => { + // given + const ensName = 'test.eth' + const expectedResult = true + + // when + const result = isValidEnsName(ensName) + + // then + expect(result).toBe(expectedResult) + }) + it('It should return true for ens in format [value].test', () => { + // given + const ensName = 'test.test' + const expectedResult = true + + // when + const result = isValidEnsName(ensName) + + // then + expect(result).toBe(expectedResult) + }) + it('It should return true for an ens in the format [value].xyz', () => { + // given + const ensName = 'test.xyz' + const expectedResult = true + + // when + const result = isValidEnsName(ensName) + + // then + expect(result).toBe(expectedResult) + }) + it('It should return true for an ens in format [value].luxe', () => { + // given + const ensName = 'test.luxe' + const expectedResult = true + + // when + const result = isValidEnsName(ensName) + + // then + expect(result).toBe(expectedResult) + }) +}) diff --git a/src/logic/wallets/transactionDataCheck/images/contractData.png b/src/logic/wallets/transactionDataCheck/images/contractData.png new file mode 100644 index 0000000000..441a3de167 Binary files /dev/null and b/src/logic/wallets/transactionDataCheck/images/contractData.png differ diff --git a/src/logic/wallets/transactionDataCheck.ts b/src/logic/wallets/transactionDataCheck/index.ts similarity index 80% rename from src/logic/wallets/transactionDataCheck.ts rename to src/logic/wallets/transactionDataCheck/index.ts index 4ca7235ea2..b4a03bbfbb 100644 --- a/src/logic/wallets/transactionDataCheck.ts +++ b/src/logic/wallets/transactionDataCheck/index.ts @@ -1,15 +1,16 @@ import { loadFromStorage, saveToStorage } from 'src/utils/storage' import { WALLET_PROVIDER } from 'src/logic/wallets/getWeb3' +import contractDataImage from './images/contractData.png' const USER_ENABLED_LEDGER_TX_DATA = 'USER_ENABLED_LEDGER_TX_DATA' function transactionDataCheck(): any { return async (stateAndHelpers) => { const { wallet } = stateAndHelpers const isTransactionDataEnabled = await loadFromStorage(USER_ENABLED_LEDGER_TX_DATA) - if (wallet && wallet.name === WALLET_PROVIDER.LEDGER && !isTransactionDataEnabled) { + if (wallet && wallet.name.toUpperCase() === WALLET_PROVIDER.LEDGER && !isTransactionDataEnabled) { return { heading: 'Allow Transaction Data', // edit modal heading here - description: 'Please allow transaction data on your Ledger device.', // edit modal description that is displayed here. You can include html strings here and they will be rendered as html elements. + description: `

Important: In order to sign transactions with your Ledger device, you will have to activate the "Contract Data" setting in the Ethereum app on your Ledger.

`, // edit modal description that is displayed here. You can include html strings here and they will be rendered as html elements. eventCode: 'allowTransactionData', button: { text: 'Done', diff --git a/src/routes/load/components/DetailsForm/index.tsx b/src/routes/load/components/DetailsForm/index.tsx index e332554920..acd1cd94df 100644 --- a/src/routes/load/components/DetailsForm/index.tsx +++ b/src/routes/load/components/DetailsForm/index.tsx @@ -10,7 +10,13 @@ import { StepperPageFormProps } from 'src/components/Stepper' import AddressInput from 'src/components/forms/AddressInput' import Field from 'src/components/forms/Field' import TextField from 'src/components/forms/TextField' -import { mustBeEthereumAddress, noErrorsOn, required } from 'src/components/forms/validator' +import { + mustBeEthereumAddress, + noErrorsOn, + required, + composeValidators, + minMaxLength, +} from 'src/components/forms/validator' import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' import Paragraph from 'src/components/layout/Paragraph' @@ -109,7 +115,7 @@ const DetailsForm = ({ errors, form }: DetailsFormProps): React.ReactElement => placeholder="Name of the Safe" text="Safe name" type="text" - validate={required} + validate={composeValidators(required, minMaxLength(1, 50))} testId="load-safe-name-field" /> @@ -120,6 +126,8 @@ const DetailsForm = ({ errors, form }: DetailsFormProps): React.ReactElement => fieldMutator={(val) => { form.mutators.setValue(FIELD_LOAD_ADDRESS, val) }} + // eslint-disable-next-line + // @ts-ignore inputAdornment={ noErrorsOn(FIELD_LOAD_ADDRESS, errors) && { endAdornment: ( @@ -156,12 +164,15 @@ const DetailsForm = ({ errors, form }: DetailsFormProps): React.ReactElement => ) } -const DetailsPage = () => (controls: React.ReactNode, { errors, form }: StepperPageFormProps): React.ReactElement => ( - <> - - - - -) +const DetailsPage = () => + function LoadSafeDetails(controls: React.ReactNode, { errors, form }: StepperPageFormProps): React.ReactElement { + return ( + <> + + + + + ) + } export default DetailsPage diff --git a/src/routes/load/components/OwnerList/index.tsx b/src/routes/load/components/OwnerList/index.tsx index 57f38276eb..a172328f4d 100644 --- a/src/routes/load/components/OwnerList/index.tsx +++ b/src/routes/load/components/OwnerList/index.tsx @@ -1,5 +1,5 @@ import TableContainer from '@material-ui/core/TableContainer' -import { withStyles } from '@material-ui/core/styles' +import { makeStyles } from '@material-ui/core/styles' import React, { useEffect, useState } from 'react' import CopyBtn from 'src/components/CopyBtn' @@ -8,7 +8,7 @@ import Identicon from 'src/components/Identicon' import OpenPaper from 'src/components/Stepper/OpenPaper' import Field from 'src/components/forms/Field' import TextField from 'src/components/forms/TextField' -import { required } from 'src/components/forms/validator' +import { composeValidators, minMaxLength, required } from 'src/components/forms/validator' import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' import Hairline from 'src/components/layout/Hairline' @@ -17,57 +17,13 @@ import Row from 'src/components/layout/Row' import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { FIELD_LOAD_ADDRESS, THRESHOLD } from 'src/routes/load/components/fields' import { getOwnerAddressBy, getOwnerNameBy } from 'src/routes/open/components/fields' -import { border, disabled, extraSmallFontSize, lg, md, screenSm, sm } from 'src/theme/variables' - -const styles = () => ({ - details: { - padding: lg, - borderRight: `solid 1px ${border}`, - height: '100%', - }, - owners: { - display: 'flex', - justifyContent: 'flex-start', - }, - ownerName: { - marginBottom: '15px', - minWidth: '100%', - [`@media (min-width: ${screenSm}px)`]: { - marginBottom: '0', - minWidth: '0', - }, - }, - ownerAddresses: { - alignItems: 'center', - marginLeft: `${sm}`, - }, - address: { - paddingLeft: '6px', - marginRight: sm, - }, - open: { - paddingLeft: sm, - width: 'auto', - '&:hover': { - cursor: 'pointer', - }, - }, - title: { - padding: `${md} ${lg}`, - }, - owner: { - padding: `0 ${lg}`, - marginBottom: '12px', - }, - header: { - padding: `${sm} ${lg}`, - color: disabled, - fontSize: extraSmallFontSize, - }, - name: { - marginRight: `${sm}`, - }, -}) + +import { useSelector } from 'react-redux' +import { addressBookSelector } from 'src/logic/addressBook/store/selectors' + +import { formatAddressListToAddressBookNames } from 'src/logic/addressBook/utils' +import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook' +import { styles } from './styles' const calculateSafeValues = (owners, threshold, values) => { const initialValues = { ...values } @@ -78,9 +34,20 @@ const calculateSafeValues = (owners, threshold, values) => { return initialValues } +const useAddressBookForOwnersNames = (ownersList: string[]): AddressBookEntry[] => { + const addressBook = useSelector(addressBookSelector) + + return formatAddressListToAddressBookNames(addressBook, ownersList) +} + +const useStyles = makeStyles(styles) + const OwnerListComponent = (props) => { - const [owners, setOwners] = useState([]) - const { classes, updateInitialProps, values } = props + const [owners, setOwners] = useState([]) + const classes = useStyles() + const { updateInitialProps, values } = props + + const ownersWithNames = useAddressBookForOwnersNames(owners) useEffect(() => { let isCurrent = true @@ -121,47 +88,51 @@ const OwnerListComponent = (props) => {
- {owners.map((address, index) => ( - - - - - - - - - {address} - - - - - - - ))} + {ownersWithNames.map(({ address, name }, index) => { + const ownerName = name || `Owner #${index + 1}` + return ( + + + + + + + + + {address} + + + + + + + ) + })} ) } -const OwnerListPage = withStyles(styles as any)(OwnerListComponent) - -const OwnerList = ({ updateInitialProps }, network) => (controls, { values }) => ( - <> - - - - -) +const OwnerList = ({ updateInitialProps }, network) => + function LoadSafeOwnerList(controls, { values }): React.ReactElement { + return ( + <> + + + + + ) + } export default OwnerList diff --git a/src/routes/load/components/OwnerList/styles.ts b/src/routes/load/components/OwnerList/styles.ts new file mode 100644 index 0000000000..c5f1e71081 --- /dev/null +++ b/src/routes/load/components/OwnerList/styles.ts @@ -0,0 +1,52 @@ +import { border, disabled, extraSmallFontSize, lg, md, screenSm, sm } from 'src/theme/variables' +import { createStyles } from '@material-ui/core' + +export const styles = createStyles({ + details: { + padding: lg, + borderRight: `solid 1px ${border}`, + height: '100%', + }, + owners: { + display: 'flex', + justifyContent: 'flex-start', + }, + ownerName: { + marginBottom: '15px', + minWidth: '100%', + [`@media (min-width: ${screenSm}px)`]: { + marginBottom: '0', + minWidth: '0', + }, + }, + ownerAddresses: { + alignItems: 'center', + marginLeft: `${sm}`, + }, + address: { + paddingLeft: '6px', + marginRight: sm, + }, + open: { + paddingLeft: sm, + width: 'auto', + '&:hover': { + cursor: 'pointer', + }, + }, + title: { + padding: `${md} ${lg}`, + }, + owner: { + padding: `0 ${lg}`, + marginBottom: '12px', + }, + header: { + padding: `${sm} ${lg}`, + color: disabled, + fontSize: extraSmallFontSize, + }, + name: { + marginRight: `${sm}`, + }, +}) diff --git a/src/routes/load/components/ReviewInformation/index.tsx b/src/routes/load/components/ReviewInformation/index.tsx index b52f3b6500..49a7588f9e 100644 --- a/src/routes/load/components/ReviewInformation/index.tsx +++ b/src/routes/load/components/ReviewInformation/index.tsx @@ -136,15 +136,15 @@ const ReviewComponent = ({ userAddress, values }: Props): React.ReactElement => ) } -const Review = ({ userAddress }: { userAddress: string }) => ( - controls: React.ReactNode, - { values }: { values: Record }, -): React.ReactElement => ( - <> - - - - -) +const Review = ({ userAddress }: { userAddress: string }) => + function ReviewPage(controls: React.ReactNode, { values }: { values: Record }): React.ReactElement { + return ( + <> + + + + + ) + } export default Review diff --git a/src/routes/load/container/Load.tsx b/src/routes/load/container/Load.tsx index 689f460e81..a3452bfd1e 100644 --- a/src/routes/load/container/Load.tsx +++ b/src/routes/load/container/Load.tsx @@ -6,17 +6,16 @@ import { FIELD_LOAD_ADDRESS, FIELD_LOAD_NAME } from '../components/fields' import Page from 'src/components/layout/Page' import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' -import { SAFES_KEY, saveSafes } from 'src/logic/safe/utils' +import { saveSafes, loadStoredSafes } from 'src/logic/safe/utils' import { getNamesFrom, getOwnersFrom } from 'src/routes/open/utils/safeDataExtractor' import { SAFELIST_ADDRESS } from 'src/routes/routes' import { buildSafe } from 'src/logic/safe/store/actions/fetchSafe' import { history } from 'src/store' -import { loadFromStorage } from 'src/utils/storage' import { SafeOwner, SafeRecordProps } from 'src/logic/safe/store/models/safe' import { List } from 'immutable' import { checksumAddress } from 'src/utils/checksumAddress' -import { addSafe } from 'src/logic/safe/store/actions/addSafe' import { networkSelector, providerNameSelector, userAccountSelector } from 'src/logic/wallets/store/selectors' +import { addOrUpdateSafe } from 'src/logic/safe/store/actions/addOrUpdateSafe' export const loadSafe = async ( safeName: string, @@ -27,7 +26,7 @@ export const loadSafe = async ( const safeProps = await buildSafe(safeAddress, safeName) safeProps.owners = owners - const storedSafes = (await loadFromStorage(SAFES_KEY)) || {} + const storedSafes = (await loadStoredSafes()) || {} storedSafes[safeAddress] = safeProps @@ -47,8 +46,8 @@ const Load = (): React.ReactElement => { const network = useSelector(networkSelector) const userAddress = useSelector(userAccountSelector) - const addSafeHandler = (safe: SafeRecordProps) => { - dispatch(addSafe(safe)) + const addSafeHandler = async (safe: SafeRecordProps) => { + await dispatch(addOrUpdateSafe(safe)) } const onLoadSafeSubmit = async (values: LoadFormValues) => { let safeAddress = values[FIELD_LOAD_ADDRESS] @@ -63,7 +62,7 @@ const Load = (): React.ReactElement => { safeAddress = checksumAddress(safeAddress) const ownerNames = getNamesFrom(values) - const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) + const gnosisSafe = getGnosisSafeInstanceAt(safeAddress) const ownerAddresses = await gnosisSafe.methods.getOwners().call() const owners = getOwnersFrom(ownerNames, ownerAddresses.slice().sort()) diff --git a/src/routes/open/components/Layout.tsx b/src/routes/open/components/Layout.tsx index 7a43d73522..0aef861fa8 100644 --- a/src/routes/open/components/Layout.tsx +++ b/src/routes/open/components/Layout.tsx @@ -19,22 +19,43 @@ import { import Welcome from 'src/routes/welcome/components/Layout' import { history } from 'src/store' import { secondary, sm } from 'src/theme/variables' +import { networkSelector, providerNameSelector, userAccountSelector } from 'src/logic/wallets/store/selectors' +import { useSelector } from 'react-redux' +import { addressBookSelector } from 'src/logic/addressBook/store/selectors' +import { getNameFromAddressBook } from 'src/logic/addressBook/utils' const { useEffect } = React const getSteps = () => ['Name', 'Owners and confirmations', 'Review'] -const initialValuesFrom = (userAccount, safeProps) => { +type SafeProps = { + name: string + ownerAddresses: any + ownerNames: string + threshold: string +} + +type InitialValuesForm = { + owner0Address?: string + owner0Name?: string + confirmations: string + safeName?: string +} + +const useInitialValuesFrom = (userAccount: string, safeProps?: SafeProps): InitialValuesForm => { + const addressBook = useSelector(addressBookSelector) + const ownerName = getNameFromAddressBook(addressBook, userAccount, { filterOnlyValidName: true }) + if (!safeProps) { return { - [getOwnerNameBy(0)]: 'My Wallet', + [getOwnerNameBy(0)]: ownerName || 'My Wallet', [getOwnerAddressBy(0)]: userAccount, [FIELD_CONFIRMATIONS]: '1', } } let obj = {} const { name, ownerAddresses, ownerNames, threshold } = safeProps - // eslint-disable-next-line no-restricted-syntax + for (const [index, value] of ownerAddresses.entries()) { const safeName = ownerNames[index] ? ownerNames[index] : 'My Wallet' obj = { @@ -66,8 +87,17 @@ const formMutators = { }, } -const Layout = (props) => { - const { network, onCallSafeContractSubmit, provider, safeProps, userAccount } = props +type LayoutProps = { + onCallSafeContractSubmit: (formValues: unknown) => void + safeProps?: SafeProps +} + +const Layout = (props: LayoutProps): React.ReactElement => { + const { onCallSafeContractSubmit, safeProps } = props + + const provider = useSelector(providerNameSelector) + const network = useSelector(networkSelector) + const userAccount = useSelector(userAccountSelector) useEffect(() => { if (provider) { @@ -77,7 +107,7 @@ const Layout = (props) => { const steps = getSteps() - const initialValues = initialValuesFrom(userAccount, safeProps) + const initialValues = useInitialValuesFrom(userAccount, safeProps) return ( <> diff --git a/src/routes/open/components/ReviewInformation/index.tsx b/src/routes/open/components/ReviewInformation/index.tsx index c918189b1d..c5d6c4c1ce 100644 --- a/src/routes/open/components/ReviewInformation/index.tsx +++ b/src/routes/open/components/ReviewInformation/index.tsx @@ -1,7 +1,6 @@ import TableContainer from '@material-ui/core/TableContainer' -import { withStyles } from '@material-ui/core/styles' import classNames from 'classnames' -import * as React from 'react' +import React, { useEffect, useState } from 'react' import { FIELD_CONFIRMATIONS, FIELD_NAME, getNumOwnersFrom } from '../fields' @@ -18,73 +17,16 @@ import { estimateGasForDeployingSafe } from 'src/logic/contracts/safeContracts' import { formatAmount } from 'src/logic/tokens/utils/formatAmount' import { getWeb3 } from 'src/logic/wallets/getWeb3' import { getAccountsFrom, getNamesFrom } from 'src/routes/open/utils/safeDataExtractor' -import { background, border, lg, screenSm, sm } from 'src/theme/variables' +import { useStyles } from './styles' -const { useEffect, useState } = React +type ReviewComponentProps = { + userAccount: string + values: any +} -const styles = () => ({ - root: { - minHeight: '300px', - [`@media (min-width: ${screenSm}px)`]: { - flexDirection: 'row', - }, - }, - detailsColumn: { - minWidth: '100%', - [`@media (min-width: ${screenSm}px)`]: { - minWidth: '0', - }, - }, - ownersColumn: { - minWidth: '100%', - [`@media (min-width: ${screenSm}px)`]: { - minWidth: '0', - }, - }, - details: { - padding: lg, - borderRight: `solid 1px ${border}`, - height: '100%', - }, - info: { - backgroundColor: background, - flexDirection: 'column', - justifyContent: 'center', - padding: lg, - textAlign: 'center', - }, - owners: { - padding: lg, - }, - name: { - textOverflow: 'ellipsis', - overflow: 'hidden', - }, - userName: { - whiteSpace: 'nowrap', - }, - owner: { - alignItems: 'center', - minWidth: 'fit-content', - padding: sm, - paddingLeft: lg, - }, - user: { - justifyContent: 'left', - '& > p': { - marginRight: sm, - }, - }, - open: { - paddingLeft: sm, - width: 'auto', - '&:hover': { - cursor: 'pointer', - }, - }, -}) +const ReviewComponent = ({ userAccount, values }: ReviewComponentProps) => { + const classes = useStyles() -const ReviewComponent = ({ classes, userAccount, values }: any) => { const [gasCosts, setGasCosts] = useState('< 0.001') const names = getNamesFrom(values) const addresses = getAccountsFrom(values) @@ -198,14 +140,15 @@ const ReviewComponent = ({ classes, userAccount, values }: any) => { ) } -const ReviewPage = withStyles(styles as any)(ReviewComponent) - -const Review = () => (controls, { values }) => ( - <> - - - - -) +const Review = () => + function ReviewPage(controls, props): React.ReactElement { + return ( + <> + + + + + ) + } export default Review diff --git a/src/routes/open/components/ReviewInformation/styles.ts b/src/routes/open/components/ReviewInformation/styles.ts new file mode 100644 index 0000000000..c6bbd15a67 --- /dev/null +++ b/src/routes/open/components/ReviewInformation/styles.ts @@ -0,0 +1,66 @@ +import { createStyles, makeStyles } from '@material-ui/core/styles' +import { background, border, lg, screenSm, sm } from 'src/theme/variables' + +export const useStyles = makeStyles( + createStyles({ + root: { + minHeight: '300px', + [`@media (min-width: ${screenSm}px)`]: { + flexDirection: 'row', + }, + }, + detailsColumn: { + minWidth: '100%', + [`@media (min-width: ${screenSm}px)`]: { + minWidth: '0', + }, + }, + ownersColumn: { + minWidth: '100%', + [`@media (min-width: ${screenSm}px)`]: { + minWidth: '0', + }, + }, + details: { + padding: lg, + borderRight: `solid 1px ${border}`, + height: '100%', + }, + info: { + backgroundColor: background, + flexDirection: 'column', + justifyContent: 'center', + padding: lg, + textAlign: 'center', + }, + owners: { + padding: lg, + }, + name: { + textOverflow: 'ellipsis', + overflow: 'hidden', + }, + userName: { + whiteSpace: 'nowrap', + }, + owner: { + alignItems: 'center', + minWidth: 'fit-content', + padding: sm, + paddingLeft: lg, + }, + user: { + justifyContent: 'left', + '& > p': { + marginRight: sm, + }, + }, + open: { + paddingLeft: sm, + width: 'auto', + '&:hover': { + cursor: 'pointer', + }, + }, + }), +) diff --git a/src/routes/open/components/SafeNameForm/index.tsx b/src/routes/open/components/SafeNameForm/index.tsx index 01d6e8f48d..82ebd94425 100644 --- a/src/routes/open/components/SafeNameForm/index.tsx +++ b/src/routes/open/components/SafeNameForm/index.tsx @@ -1,16 +1,16 @@ -import { withStyles } from '@material-ui/core/styles' +import { createStyles, makeStyles } from '@material-ui/core/styles' import * as React from 'react' import OpenPaper from 'src/components/Stepper/OpenPaper' import Field from 'src/components/forms/Field' import TextField from 'src/components/forms/TextField' -import { required } from 'src/components/forms/validator' +import { composeValidators, minMaxLength, required } from 'src/components/forms/validator' import Block from 'src/components/layout/Block' import Paragraph from 'src/components/layout/Paragraph' import { FIELD_NAME } from 'src/routes/open/components/fields' import { secondary, sm } from 'src/theme/variables' -const styles = () => ({ +const styles = createStyles({ root: { display: 'flex', maxWidth: '440px', @@ -28,51 +28,56 @@ const styles = () => ({ }, }) -const SafeName = ({ classes, safeName }) => ( - <> - - - You are about to create a new Gnosis Safe wallet with one or more owners. First, let's give your new wallet - a name. This name is only stored locally and will never be shared with Gnosis or any third parties. - - - - - - - - By continuing you consent to the{' '} - - terms of use - {' '} - and{' '} - - privacy policy - - . - - - -) +const useSafeNameStyles = makeStyles(styles) -const SafeNameForm = withStyles(styles as any)(SafeName) +const SafeNameForm = ({ safeName }: { safeName: string }): React.ReactElement => { + const classes = useSafeNameStyles() -const SafeNamePage = () => (controls, { values }) => { - const { safeName } = values return ( - - - + <> + + + You are about to create a new Gnosis Safe wallet with one or more owners. First, let's give your new + wallet a name. This name is only stored locally and will never be shared with Gnosis or any third parties. + + + + + + + + By continuing you consent to the{' '} + + terms of use + {' '} + and{' '} + + privacy policy + + . + + + ) } -export default SafeNamePage +const SafeNamePageComponent = () => + function SafeNamePage(controls, { values }): React.ReactElement { + const { safeName } = values + return ( + + + + ) + } + +export default SafeNamePageComponent diff --git a/src/routes/open/components/SafeOwnersConfirmationsForm/index.tsx b/src/routes/open/components/SafeOwnersConfirmationsForm/index.tsx index 58d2e20426..6951f19a3a 100644 --- a/src/routes/open/components/SafeOwnersConfirmationsForm/index.tsx +++ b/src/routes/open/components/SafeOwnersConfirmationsForm/index.tsx @@ -1,10 +1,8 @@ import InputAdornment from '@material-ui/core/InputAdornment' import MenuItem from '@material-ui/core/MenuItem' -import { withStyles } from '@material-ui/core/styles' +import { makeStyles } from '@material-ui/core/styles' import CheckCircle from '@material-ui/icons/CheckCircle' import * as React from 'react' -import { withRouter } from 'react-router-dom' - import { styles } from './style' import { getAddressValidator } from './validators' @@ -16,7 +14,14 @@ import AddressInput from 'src/components/forms/AddressInput' import Field from 'src/components/forms/Field' import SelectField from 'src/components/forms/SelectField' import TextField from 'src/components/forms/TextField' -import { composeValidators, minValue, mustBeInteger, noErrorsOn, required } from 'src/components/forms/validator' +import { + composeValidators, + minValue, + mustBeInteger, + noErrorsOn, + required, + minMaxLength, +} from 'src/components/forms/validator' import Block from 'src/components/layout/Block' import Button from 'src/components/layout/Button' import Col from 'src/components/layout/Col' @@ -31,6 +36,9 @@ import { getOwnerNameBy, } from 'src/routes/open/components/fields' import { getAccountsFrom } from 'src/routes/open/utils/safeDataExtractor' +import { useSelector } from 'react-redux' +import { addressBookSelector } from 'src/logic/addressBook/store/selectors' +import { getNameFromAddressBook } from 'src/logic/addressBook/utils' const { useState } = React @@ -56,10 +64,14 @@ export const calculateValuesAfterRemoving = (index, notRemovedOwners, values) => return initialValues } -const SafeOwners = (props) => { - const { classes, errors, form, otherAccounts, values } = props +const useStyles = makeStyles(styles) + +const SafeOwnersForm = (props): React.ReactElement => { + const { errors, form, otherAccounts, values } = props + const classes = useStyles() const validOwners = getNumOwnersFrom(values) + const addressBook = useSelector(addressBookSelector) const [numOwners, setNumOwners] = useState(validOwners) const [qrModalOpen, setQrModalOpen] = useState(false) @@ -118,6 +130,7 @@ const SafeOwners = (props) => { {[...Array(Number(numOwners))].map((x, index) => { const addressName = getOwnerAddressBy(index) + const ownerName = getOwnerNameBy(index) return ( @@ -125,19 +138,27 @@ const SafeOwners = (props) => { { - form.mutators.setValue(addressName, val) + fieldMutator={(newOwnerAddress) => { + const newOwnerName = getNameFromAddressBook(addressBook, newOwnerAddress, { + filterOnlyValidName: true, + }) + form.mutators.setValue(addressName, newOwnerAddress) + if (newOwnerName) { + form.mutators.setValue(ownerName, newOwnerName) + } }} + // eslint-disable-next-line + // @ts-ignore inputAdornment={ noErrorsOn(addressName, errors) && { endAdornment: ( @@ -215,20 +236,21 @@ const SafeOwners = (props) => { ) } -const SafeOwnersForm = withStyles(styles as any)(withRouter(SafeOwners)) - -const SafeOwnersPage = ({ updateInitialProps }) => (controls, { errors, form, values }) => ( - <> - - - - -) +const SafeOwnersPage = ({ updateInitialProps }) => + function OpenSafeOwnersPage(controls, { errors, form, values }) { + return ( + <> + + + + + ) + } export default SafeOwnersPage diff --git a/src/routes/open/components/SafeOwnersConfirmationsForm/style.ts b/src/routes/open/components/SafeOwnersConfirmationsForm/style.ts index 1e4e6a3ba0..61c73caf94 100644 --- a/src/routes/open/components/SafeOwnersConfirmationsForm/style.ts +++ b/src/routes/open/components/SafeOwnersConfirmationsForm/style.ts @@ -1,6 +1,7 @@ import { disabled, extraSmallFontSize, lg, md, screenSm, sm } from 'src/theme/variables' +import { createStyles } from '@material-ui/core' -export const styles = () => ({ +export const styles = createStyles({ root: { display: 'flex', }, diff --git a/src/routes/open/container/Open.tsx b/src/routes/open/container/Open.tsx index e1f95690ad..360d388438 100644 --- a/src/routes/open/container/Open.tsx +++ b/src/routes/open/container/Open.tsx @@ -2,15 +2,9 @@ import { Loader } from '@gnosis.pm/safe-react-components' import queryString from 'query-string' import React, { useEffect, useState } from 'react' import ReactGA from 'react-ga' -import { connect } from 'react-redux' -import { withRouter, RouteComponentProps } from 'react-router-dom' - -import Opening from '../../opening' -import Layout from '../components/Layout' - -import actions from './actions' -import selector from './selector' - +import { useDispatch, useSelector } from 'react-redux' +import Opening from 'src/routes/opening' +import Layout from 'src/routes/open/components/Layout' import Page from 'src/components/layout/Page' import { getSafeDeploymentTransaction } from 'src/logic/contracts/safeContracts' import { checkReceiptStatus } from 'src/logic/wallets/ethTransactions' @@ -25,6 +19,9 @@ import { SAFELIST_ADDRESS, WELCOME_ADDRESS } from 'src/routes/routes' import { buildSafe } from 'src/logic/safe/store/actions/fetchSafe' import { history } from 'src/store' import { loadFromStorage, removeFromStorage, saveToStorage } from 'src/utils/storage' +import { userAccountSelector } from 'src/logic/wallets/store/selectors' +import { SafeRecordProps } from 'src/logic/safe/store/models/safe' +import { addOrUpdateSafe } from 'src/logic/safe/store/actions/addOrUpdateSafe' const SAFE_PENDING_CREATION_STORAGE_KEY = 'SAFE_PENDING_CREATION_STORAGE_KEY' @@ -39,13 +36,15 @@ const validateQueryParams = (ownerAddresses, ownerNames, threshold, safeName) => if (Number.isNaN(Number(threshold))) { return false } - if (threshold > ownerAddresses.length) { - return false - } - return true + return threshold <= ownerAddresses.length } -export const getSafeProps = async (safeAddress, safeName, ownersNames, ownerAddresses) => { +export const getSafeProps = async ( + safeAddress: string, + safeName: string, + ownersNames: string[], + ownerAddresses: string[], +): Promise => { const safeProps = await buildSafe(safeAddress, safeName) const owners = getOwnersFrom(ownersNames, ownerAddresses) safeProps.owners = owners @@ -81,19 +80,14 @@ export const createSafe = (values, userAccount) => { return promiEvent } -interface OwnProps extends RouteComponentProps { - userAccount: string - network: string - provider: string - addSafe: any -} - -const Open = ({ addSafe, network, provider, userAccount }: OwnProps): React.ReactElement => { +const Open = (): React.ReactElement => { const [loading, setLoading] = useState(false) const [showProgress, setShowProgress] = useState(false) const [creationTxPromise, setCreationTxPromise] = useState() const [safeCreationPendingInfo, setSafeCreationPendingInfo] = useState() const [safePropsFromUrl, setSafePropsFromUrl] = useState() + const userAccount = useSelector(userAccountSelector) + const dispatch = useDispatch() useEffect(() => { // #122: Allow to migrate an old Multisig by passing the parameters to the URL. @@ -141,14 +135,15 @@ const Open = ({ addSafe, network, provider, userAccount }: OwnProps): React.Reac setShowProgress(true) } - const onSafeCreated = async (safeAddress) => { + const onSafeCreated = async (safeAddress): Promise => { const pendingCreation = await loadFromStorage<{ txHash: string }>(SAFE_PENDING_CREATION_STORAGE_KEY) const name = getSafeNameFrom(pendingCreation) const ownersNames = getNamesFrom(pendingCreation) const ownerAddresses = getAccountsFrom(pendingCreation) const safeProps = await getSafeProps(safeAddress, name, ownersNames, ownerAddresses) - addSafe(safeProps) + + await dispatch(addOrUpdateSafe(safeProps)) ReactGA.event({ category: 'User', @@ -161,7 +156,7 @@ const Open = ({ addSafe, network, provider, userAccount }: OwnProps): React.Reac pathname: `${SAFELIST_ADDRESS}/${safeProps.address}/balances`, state: { name, - tx: pendingCreation.txHash, + tx: pendingCreation?.txHash, }, } @@ -177,7 +172,7 @@ const Open = ({ addSafe, network, provider, userAccount }: OwnProps): React.Reac const onRetry = async () => { const values = await loadFromStorage<{ txHash: string }>(SAFE_PENDING_CREATION_STORAGE_KEY) - delete values.txHash + delete values?.txHash await saveToStorage(SAFE_PENDING_CREATION_STORAGE_KEY, values) setSafeCreationPendingInfo(values) createSafeProxy() @@ -194,21 +189,14 @@ const Open = ({ addSafe, network, provider, userAccount }: OwnProps): React.Reac creationTxHash={safeCreationPendingInfo?.txHash} onCancel={onCancel} onRetry={onRetry} - onSuccess={onSafeCreated as any} - provider={provider} + onSuccess={onSafeCreated} submittedPromise={creationTxPromise} /> ) : ( - + )} ) } -export default connect(selector, actions)(withRouter(Open)) +export default Open diff --git a/src/routes/open/container/actions.ts b/src/routes/open/container/actions.ts deleted file mode 100644 index 918dbf4469..0000000000 --- a/src/routes/open/container/actions.ts +++ /dev/null @@ -1,5 +0,0 @@ -import addSafe from 'src/logic/safe/store/actions/addSafe' - -export default { - addSafe, -} diff --git a/src/routes/open/container/selector.ts b/src/routes/open/container/selector.ts deleted file mode 100644 index c0163f1034..0000000000 --- a/src/routes/open/container/selector.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createStructuredSelector } from 'reselect' - -import { networkSelector, providerNameSelector, userAccountSelector } from 'src/logic/wallets/store/selectors' - -export default createStructuredSelector({ - provider: providerNameSelector, - network: networkSelector, - userAccount: userAccountSelector, -}) diff --git a/src/routes/opening/index.tsx b/src/routes/opening/index.tsx index 3b7f3d5b0e..d976284625 100644 --- a/src/routes/opening/index.tsx +++ b/src/routes/opening/index.tsx @@ -2,7 +2,7 @@ import { Loader, Stepper } from '@gnosis.pm/safe-react-components' import React, { useEffect, useState } from 'react' import styled from 'styled-components' -import { ErrorFooter } from './components/Footer' +import { ErrorFooter } from 'src/routes/opening/components/Footer' import { isConfirmationStep, steps } from './steps' import Button from 'src/components/layout/Button' @@ -13,6 +13,8 @@ import { initContracts } from 'src/logic/contracts/safeContracts' import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions' import { getWeb3 } from 'src/logic/wallets/getWeb3' import { background, connected } from 'src/theme/variables' +import { providerNameSelector } from 'src/logic/wallets/store/selectors' +import { useSelector } from 'react-redux' const loaderDotsSvg = require('./assets/loader-dots.svg') const successSvg = require('./assets/success.svg') @@ -102,16 +104,17 @@ const BackButton = styled(Button)` // onCancel: () => void // } -const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, provider, submittedPromise }: any) => { +const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, submittedPromise }): React.ReactElement => { const [loading, setLoading] = useState(true) const [stepIndex, setStepIndex] = useState(0) - const [safeCreationTxHash, setSafeCreationTxHash] = useState() - const [createdSafeAddress, setCreatedSafeAddress] = useState() + const [safeCreationTxHash, setSafeCreationTxHash] = useState('') + const [createdSafeAddress, setCreatedSafeAddress] = useState('') const [error, setError] = useState(false) const [intervalStarted, setIntervalStarted] = useState(false) const [waitingSafeDeployed, setWaitingSafeDeployed] = useState(false) const [continueButtonDisabled, setContinueButtonDisabled] = useState(false) + const provider = useSelector(providerNameSelector) const confirmationStep = isConfirmationStep(stepIndex) @@ -242,7 +245,7 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, provider useEffect(() => { let interval - const awaitUntilSafeIsDeployed = async () => { + const awaitUntilSafeIsDeployed = async (safeCreationTxHash: string) => { try { const web3 = getWeb3() const receipt = await web3.eth.getTransactionReceipt(safeCreationTxHash) @@ -283,7 +286,9 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, provider return } - awaitUntilSafeIsDeployed() + if (typeof safeCreationTxHash === 'string') { + awaitUntilSafeIsDeployed(safeCreationTxHash) + } return () => { clearInterval(interval) @@ -294,7 +299,7 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, provider return } - let FooterComponent = null + let FooterComponent if (error) { FooterComponent = ErrorFooter } else if (steps[stepIndex].footerComponent) { diff --git a/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.tsx b/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.tsx index 974be3e491..c8c5073a8a 100644 --- a/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.tsx +++ b/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.tsx @@ -19,8 +19,8 @@ import Col from 'src/components/layout/Col' import Hairline from 'src/components/layout/Hairline' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' -import { getAddressBook } from 'src/logic/addressBook/store/selectors' -import { getAddressesListFromAdbk } from 'src/logic/addressBook/utils' +import { addressBookSelector } from 'src/logic/addressBook/store/selectors' +import { getAddressesListFromAddressBook } from 'src/logic/addressBook/utils' export const CREATE_ENTRY_INPUT_NAME_ID = 'create-entry-input-name' export const CREATE_ENTRY_INPUT_ADDRESS_ID = 'create-entry-input-address' @@ -42,8 +42,8 @@ const CreateEditEntryModalComponent = ({ } } - const addressBook = useSelector(getAddressBook) - const addressBookAddressesList = getAddressesListFromAdbk(addressBook) + const addressBook = useSelector(addressBookSelector) + const addressBookAddressesList = getAddressesListFromAddressBook(addressBook) const entryDoesntExist = uniqueAddress(addressBookAddressesList) const formMutators = { diff --git a/src/routes/safe/components/AddressBook/columns.ts b/src/routes/safe/components/AddressBook/columns.ts index 6548e18a85..79ddf5317b 100644 --- a/src/routes/safe/components/AddressBook/columns.ts +++ b/src/routes/safe/components/AddressBook/columns.ts @@ -1,4 +1,5 @@ import { List } from 'immutable' +import { TableCellProps } from '@material-ui/core/TableCell/TableCell' export const ADDRESS_BOOK_ROW_ID = 'address-book-row' export const TX_TABLE_ADDRESS_BOOK_ID = 'idAddressBook' @@ -9,7 +10,17 @@ export const EDIT_ENTRY_BUTTON = 'edit-entry-btn' export const REMOVE_ENTRY_BUTTON = 'remove-entry-btn' export const SEND_ENTRY_BUTTON = 'send-entry-btn' -export const generateColumns = () => { +type AddressBookColumn = { + id: string + order: boolean + disablePadding?: boolean + label: string + width?: number + custom?: boolean + align?: TableCellProps['align'] +} + +export const generateColumns = (): List => { const nameColumn = { id: AB_NAME_ID, order: false, diff --git a/src/routes/safe/components/AddressBook/index.tsx b/src/routes/safe/components/AddressBook/index.tsx index a467479762..42597e6439 100644 --- a/src/routes/safe/components/AddressBook/index.tsx +++ b/src/routes/safe/components/AddressBook/index.tsx @@ -1,10 +1,8 @@ import TableCell from '@material-ui/core/TableCell' import TableContainer from '@material-ui/core/TableContainer' import TableRow from '@material-ui/core/TableRow' -import { withStyles } from '@material-ui/core/styles' -// import CallMade from '@material-ui/icons/CallMade' +import { makeStyles } from '@material-ui/core/styles' import cn from 'classnames' -// import classNames from 'classnames/bind' import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' @@ -18,11 +16,11 @@ import ButtonLink from 'src/components/layout/ButtonLink' import Col from 'src/components/layout/Col' import Img from 'src/components/layout/Img' import Row from 'src/components/layout/Row' -import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' +import { AddressBookEntry, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' import { addAddressBookEntry } from 'src/logic/addressBook/store/actions/addAddressBookEntry' import { removeAddressBookEntry } from 'src/logic/addressBook/store/actions/removeAddressBookEntry' import { updateAddressBookEntry } from 'src/logic/addressBook/store/actions/updateAddressBookEntry' -import { getAddressBook } from 'src/logic/addressBook/store/selectors' +import { addressBookSelector } from 'src/logic/addressBook/store/selectors' import { isUserAnOwnerOfAnySafe } from 'src/logic/wallets/ethAddresses' import CreateEditEntryModal from 'src/routes/safe/components/AddressBook/CreateEditEntryModal' import DeleteEntryModal from 'src/routes/safe/components/AddressBook/DeleteEntryModal' @@ -38,21 +36,40 @@ import SendModal from 'src/routes/safe/components/Balances/SendModal' import OwnerAddressTableCell from 'src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell' import RenameOwnerIcon from 'src/routes/safe/components/Settings/ManageOwners/assets/icons/rename-owner.svg' import RemoveOwnerIcon from 'src/routes/safe/components/Settings/assets/icons/bin.svg' -import RemoveOwnerIconDisabled from 'src/routes/safe/components/Settings/assets/icons/disabled-bin.svg' import { addressBookQueryParamsSelector, safesListSelector } from 'src/logic/safe/store/selectors' import { checksumAddress } from 'src/utils/checksumAddress' +import { grantedSelector } from 'src/routes/safe/container/selector' +import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' +import { getValidAddressBookName } from 'src/logic/addressBook/utils' -const AddressBookTable = ({ classes }) => { +const useStyles = makeStyles(styles) + +interface AddressBookSelectedEntry extends AddressBookEntry { + isNew?: boolean +} + +const AddressBookTable = (): React.ReactElement => { + const classes = useStyles() const columns = generateColumns() const autoColumns = columns.filter((c) => !c.custom) const dispatch = useDispatch() const safesList = useSelector(safesListSelector) const entryAddressToEditOrCreateNew = useSelector(addressBookQueryParamsSelector) - const addressBook = useSelector(getAddressBook) - const [selectedEntry, setSelectedEntry] = useState(null) + const addressBook = useSelector(addressBookSelector) + const granted = useSelector(grantedSelector) + const [selectedEntry, setSelectedEntry] = useState<{ + entry?: AddressBookSelectedEntry + index?: number + isOwnerAddress?: boolean + } | null>(null) const [editCreateEntryModalOpen, setEditCreateEntryModalOpen] = useState(false) const [deleteEntryModalOpen, setDeleteEntryModalOpen] = useState(false) const [sendFundsModalOpen, setSendFundsModalOpen] = useState(false) + const { trackEvent } = useAnalytics() + + useEffect(() => { + trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'AddressBook' }) + }, [trackEvent]) useEffect(() => { if (entryAddressToEditOrCreateNew) { @@ -63,11 +80,10 @@ const AddressBookTable = ({ classes }) => { useEffect(() => { if (entryAddressToEditOrCreateNew) { const checksumEntryAdd = checksumAddress(entryAddressToEditOrCreateNew) - const key = addressBook.findKey((entry) => entry.address === checksumEntryAdd) - if (key >= 0) { + const oldEntryIndex = addressBook.findIndex((entry) => entry.address === checksumEntryAdd) + if (oldEntryIndex >= 0) { // Edit old entry - const value = addressBook.get(key) - setSelectedEntry({ entry: value, index: key }) + setSelectedEntry({ entry: addressBook[oldEntryIndex], index: oldEntryIndex }) } else { // Create new entry setSelectedEntry({ @@ -101,7 +117,7 @@ const AddressBookTable = ({ classes }) => { } const deleteEntryModalHandler = () => { - const entryAddress = checksumAddress(selectedEntry.entry.address) + const entryAddress = selectedEntry && selectedEntry.entry ? checksumAddress(selectedEntry.entry.address) : '' setSelectedEntry(null) setDeleteEntryModalOpen(false) dispatch(removeAddressBookEntry(entryAddress)) @@ -132,7 +148,7 @@ const AddressBookTable = ({ classes }) => { defaultRowsPerPage={25} disableLoadingOnEmptyTable label="Owners" - size={addressBook?.size || 0} + size={addressBook?.length || 0} > {(sortedData) => sortedData.map((row, index) => { @@ -145,20 +161,22 @@ const AddressBookTable = ({ classes }) => { key={index} tabIndex={-1} > - {autoColumns.map((column: any) => ( - - {column.id === AB_ADDRESS_ID ? ( - - ) : ( - row[column.id] - )} - - ))} + {autoColumns.map((column) => { + return ( + + {column.id === AB_ADDRESS_ID ? ( + + ) : ( + getValidAddressBookName(row[column.id]) + )} + + ) + })} Edit entry { setSelectedEntry({ entry: row, @@ -171,33 +189,29 @@ const AddressBookTable = ({ classes }) => { /> Remove entry { - if (!userOwner) { - setSelectedEntry({ entry: row }) - setDeleteEntryModalOpen(true) - } + setSelectedEntry({ entry: row }) + setDeleteEntryModalOpen(true) }} - src={userOwner ? RemoveOwnerIconDisabled : RemoveOwnerIcon} + src={RemoveOwnerIcon} testId={REMOVE_ENTRY_BUTTON} /> - + {granted ? ( + + ) : null} @@ -230,4 +244,4 @@ const AddressBookTable = ({ classes }) => { ) } -export default withStyles(styles as any)(AddressBookTable) +export default AddressBookTable diff --git a/src/routes/safe/components/AddressBook/style.ts b/src/routes/safe/components/AddressBook/style.ts index b676dd63e6..f0e6c98031 100644 --- a/src/routes/safe/components/AddressBook/style.ts +++ b/src/routes/safe/components/AddressBook/style.ts @@ -1,6 +1,7 @@ import { lg, marginButtonImg, md, sm } from 'src/theme/variables' +import { createStyles } from '@material-ui/core' -export const styles = () => ({ +export const styles = createStyles({ formContainer: { minHeight: '250px', }, @@ -38,6 +39,9 @@ export const styles = () => ({ cursor: 'pointer', marginBottom: marginButtonImg, }, + editEntryButtonNonOwner: { + cursor: 'pointer', + }, removeEntryButton: { marginLeft: lg, marginRight: lg, @@ -50,6 +54,11 @@ export const styles = () => ({ marginBottom: marginButtonImg, cursor: 'default', }, + removeEntryButtonNonOwner: { + marginLeft: lg, + marginRight: lg, + cursor: 'pointer', + }, message: { padding: `${md} 0`, maxHeight: '54px', diff --git a/src/routes/safe/components/AllTransactions/index.tsx b/src/routes/safe/components/AllTransactions/index.tsx index 816819e4f1..ba208e2159 100644 --- a/src/routes/safe/components/AllTransactions/index.tsx +++ b/src/routes/safe/components/AllTransactions/index.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react' import { useTransactions } from 'src/routes/safe/container/hooks/useTransactions' import { ButtonLink, Loader } from '@gnosis.pm/safe-react-components' -import { Transaction } from 'src/logic/safe/store/models/types/transactions' +import { Transaction } from 'src/logic/safe/store/models/types/transactions.d' const Transactions = (): React.ReactElement => { const [currentPage, setCurrentPage] = useState(0) @@ -38,7 +38,7 @@ const Transactions = (): React.ReactElement => { {transactionsByPage.map((tx: Transaction, index) => { let txHash = '' if ('transactionHash' in tx) { - txHash = tx.transactionHash + txHash = tx.transactionHash as string } if ('txHash' in tx) { txHash = tx.txHash diff --git a/src/routes/safe/components/Apps/AddAppForm/AppAgreement.tsx b/src/routes/safe/components/Apps/AddAppForm/AppAgreement.tsx index cc1ba29ae4..929a3fd216 100644 --- a/src/routes/safe/components/Apps/AddAppForm/AppAgreement.tsx +++ b/src/routes/safe/components/Apps/AddAppForm/AppAgreement.tsx @@ -14,7 +14,7 @@ const AppAgreement = (): React.ReactElement => { const { visited } = useFormState({ subscription: { visited: true } }) // trick to prevent having the field validated by default. Not sure why this happens in this form - const validate = !visited.agreementAccepted ? undefined : required + const validate = !visited?.agreementAccepted ? undefined : required return ( void }): React.ReactElement => { +export const AppInfoUpdater = ({ onAppInfo }: { onAppInfo: (appInfo: SafeApp) => void }): null => { const { input: { value: appUrl }, } = useField('appUrl', { subscription: { value: true } }) @@ -52,7 +52,7 @@ const AppUrl = ({ appList }: { appList: SafeApp[] }): React.ReactElement => { const { visited } = useFormState({ subscription: { visited: true } }) // trick to prevent having the field validated by default. Not sure why this happens in this form - const validate = !visited.appUrl ? undefined : composeValidators(required, validateUrl, uniqueApp(appList)) + const validate = !visited?.appUrl ? undefined : composeValidators(required, validateUrl, uniqueApp(appList)) return ( diff --git a/src/routes/safe/components/Apps/AddAppForm/SubmitButtonStatus.tsx b/src/routes/safe/components/Apps/AddAppForm/SubmitButtonStatus.tsx index dcd6d129dc..b79a888fcf 100644 --- a/src/routes/safe/components/Apps/AddAppForm/SubmitButtonStatus.tsx +++ b/src/routes/safe/components/Apps/AddAppForm/SubmitButtonStatus.tsx @@ -9,14 +9,14 @@ interface SubmitButtonStatusProps { onSubmitButtonStatusChange: (disabled: boolean) => void } -const SubmitButtonStatus = ({ appInfo, onSubmitButtonStatusChange }: SubmitButtonStatusProps): React.ReactElement => { +const SubmitButtonStatus = ({ appInfo, onSubmitButtonStatusChange }: SubmitButtonStatusProps): null => { const { valid, validating, visited } = useFormState({ subscription: { valid: true, validating: true, visited: true }, }) React.useEffect(() => { // if non visited, fields were not evaluated yet. Then, the default value is considered invalid - const fieldsVisited = visited.agreementAccepted && visited.appUrl + const fieldsVisited = visited?.agreementAccepted && visited.appUrl onSubmitButtonStatusChange(validating || !valid || !fieldsVisited || !isAppManifestValid(appInfo)) }, [validating, valid, visited, onSubmitButtonStatusChange, appInfo]) diff --git a/src/routes/safe/components/Apps/AddAppForm/index.tsx b/src/routes/safe/components/Apps/AddAppForm/index.tsx index a88b3f4a4d..292f522f53 100644 --- a/src/routes/safe/components/Apps/AddAppForm/index.tsx +++ b/src/routes/safe/components/Apps/AddAppForm/index.tsx @@ -40,7 +40,7 @@ const INITIAL_VALUES: AddAppFormValues = { } const APP_INFO: SafeApp = { - id: undefined, + id: '', url: '', name: '', iconUrl: appsIconSvg, diff --git a/src/routes/safe/components/Apps/components/AppFrame.tsx b/src/routes/safe/components/Apps/components/AppFrame.tsx index 31ddeaefbd..9d021a8125 100644 --- a/src/routes/safe/components/Apps/components/AppFrame.tsx +++ b/src/routes/safe/components/Apps/components/AppFrame.tsx @@ -54,7 +54,7 @@ const AppFrame = forwardRef(function AppFrameC const redirectToBalance = () => history.push(`${SAFELIST_ADDRESS}/${safeAddress}/balances`) if (!selectedApp) { - return null + return
} if (!consentReceived) { diff --git a/src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx b/src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx index 5c75f84705..013532882e 100644 --- a/src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx +++ b/src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx @@ -31,7 +31,7 @@ const isTxValid = (t: Transaction): boolean => { } const isAddressValid = mustBeEthereumAddress(t.to) === undefined - return isAddressValid && t.data && typeof t.data === 'string' + return isAddressValid && !!t.data && typeof t.data === 'string' } const Wrapper = styled.div` @@ -69,8 +69,8 @@ type OwnProps = { safeAddress: string safeName: string ethBalance: string - onCancel: () => void onUserConfirm: (safeTxHash: string) => void + onTxReject: () => void onClose: () => void } @@ -81,15 +81,20 @@ const ConfirmTransactionModal = ({ safeAddress, ethBalance, safeName, - onCancel, onUserConfirm, onClose, -}: OwnProps): React.ReactElement => { + onTxReject, +}: OwnProps): React.ReactElement | null => { const dispatch = useDispatch() if (!isOpen) { return null } + const handleTxRejection = () => { + onTxReject() + onClose() + } + const handleUserConfirmation = (safeTxHash: string): void => { onUserConfirm(safeTxHash) onClose() @@ -111,9 +116,9 @@ const ConfirmTransactionModal = ({ navigateToTransactionsTab: false, }, handleUserConfirmation, + handleTxRejection, ), ) - onClose() } const areTxsMalformed = txs.some((t) => !isTxValid(t)) @@ -133,27 +138,25 @@ const ConfirmTransactionModal = ({ <> - {txs.map((tx, index) => { - return ( - - } title={`Transaction ${index + 1}`}> - -
- Value -
- Ether - {humanReadableValue(tx.value, 18)} ETH -
-
-
- Data (hex encoded)* - {tx.data} + {txs.map((tx, index) => ( + + } title={`Transaction ${index + 1}`}> + +
+ Value +
+ Ether + {humanReadableValue(tx.value, 18)} ETH
- - - - ) - })} +
+
+ Data (hex encoded)* + {tx.data} +
+
+
+
+ ))} ) @@ -164,13 +167,13 @@ const ConfirmTransactionModal = ({ footer={ } - onClose={onClose} + onClose={handleTxRejection} /> ) } diff --git a/src/routes/safe/components/Apps/components/ManageApps.tsx b/src/routes/safe/components/Apps/components/ManageApps.tsx index 9d0de55daa..26ccb49416 100644 --- a/src/routes/safe/components/Apps/components/ManageApps.tsx +++ b/src/routes/safe/components/Apps/components/ManageApps.tsx @@ -14,6 +14,8 @@ type Props = { onAppRemoved: (appId: string) => void } +type AppListItem = SafeApp & { checked: boolean } + const ManageApps = ({ appList, onAppAdded, onAppToggle, onAppRemoved }: Props): React.ReactElement => { const [isOpen, setIsOpen] = useState(false) const [isSubmitDisabled, setIsSubmitDisabled] = useState(true) @@ -28,7 +30,7 @@ const ManageApps = ({ appList, onAppAdded, onAppToggle, onAppRemoved }: Props): const closeModal = () => setIsOpen(false) - const getItemList = () => + const getItemList = (): AppListItem[] => appList.map((a) => { return { ...a, checked: !a.disabled } }) diff --git a/src/routes/safe/components/Apps/hooks/useAppList.ts b/src/routes/safe/components/Apps/hooks/useAppList.ts index dc08c91920..fc48a71682 100644 --- a/src/routes/safe/components/Apps/hooks/useAppList.ts +++ b/src/routes/safe/components/Apps/hooks/useAppList.ts @@ -42,7 +42,7 @@ const useAppList = (): UseAppListReturnType => { } }) - let apps = [] + let apps: SafeApp[] = [] // using the appURL to recover app info for (let index = 0; index < list.length; index++) { try { diff --git a/src/routes/safe/components/Apps/hooks/useIframeMessageHandler.ts b/src/routes/safe/components/Apps/hooks/useIframeMessageHandler.ts index 59cab2d4c5..a1a0e69523 100644 --- a/src/routes/safe/components/Apps/hooks/useIframeMessageHandler.ts +++ b/src/routes/safe/components/Apps/hooks/useIframeMessageHandler.ts @@ -8,16 +8,18 @@ import { INTERFACE_MESSAGES, RequestId, Transaction, + LowercaseNetworks, } from '@gnosis.pm/safe-apps-sdk' import { useDispatch, useSelector } from 'react-redux' import { useEffect, useCallback, MutableRefObject } from 'react' +import { getTxServiceHost } from 'src/config/' import { safeEthBalanceSelector, safeNameSelector, safeParamAddressFromStateSelector, } from 'src/logic/safe/store/selectors' import { networkSelector } from 'src/logic/wallets/store/selectors' -import { SafeApp } from 'src/routes/safe/components/Apps/types' +import { SafeApp } from 'src/routes/safe/components/Apps/types.d' type InterfaceMessageProps = { messageId: T @@ -44,7 +46,7 @@ const useIframeMessageHandler = ( selectedApp: SafeApp | undefined, openConfirmationModal: (txs: Transaction[], requestId: RequestId) => void, closeModal: () => void, - iframeRef: MutableRefObject, + iframeRef: MutableRefObject, ): ReturnType => { const { enqueueSnackbar, closeSnackbar } = useSnackbar() const safeName = useSelector(safeNameSelector) @@ -60,8 +62,8 @@ const useIframeMessageHandler = ( requestId: requestId || Math.trunc(window.performance.now()), } - if (iframeRef?.current && selectedApp) { - iframeRef.current.contentWindow.postMessage(requestWithMessage, selectedApp.url) + if (iframeRef && selectedApp) { + iframeRef.current?.contentWindow?.postMessage(requestWithMessage, selectedApp.url) } }, [iframeRef, selectedApp], @@ -77,21 +79,30 @@ const useIframeMessageHandler = ( switch (msg.data.messageId) { case SDK_MESSAGES.SEND_TRANSACTIONS: { - openConfirmationModal(msg.data.data, requestId) + if (msg.data.data) { + openConfirmationModal(msg.data.data, requestId) + } break } case SDK_MESSAGES.SAFE_APP_SDK_INITIALIZED: { - const message = { + const safeInfoMessage = { messageId: INTERFACE_MESSAGES.ON_SAFE_INFO, data: { - safeAddress, - network: network, - ethBalance, + safeAddress: safeAddress as string, + network: network.toLowerCase() as LowercaseNetworks, + ethBalance: ethBalance as string, + }, + } + const envInfoMessage = { + messageId: INTERFACE_MESSAGES.ENV_INFO, + data: { + txServiceUrl: getTxServiceHost(), }, } - sendMessageToIframe(message) + sendMessageToIframe(safeInfoMessage) + sendMessageToIframe(envInfoMessage) break } default: { @@ -104,7 +115,7 @@ const useIframeMessageHandler = ( if (message.origin === window.origin) { return } - if (!selectedApp.url.includes(message.origin)) { + if (!selectedApp?.url.includes(message.origin)) { console.error(`ThirdPartyApp: A message was received from an unknown origin ${message.origin}`) return } diff --git a/src/routes/safe/components/Apps/index.tsx b/src/routes/safe/components/Apps/index.tsx index 2cd3031543..d34d8dd4db 100644 --- a/src/routes/safe/components/Apps/index.tsx +++ b/src/routes/safe/components/Apps/index.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useRef, useState, useMemo } from 'react' -import { INTERFACE_MESSAGES, Transaction, RequestId } from '@gnosis.pm/safe-apps-sdk' +import { INTERFACE_MESSAGES, Transaction, RequestId, LowercaseNetworks } from '@gnosis.pm/safe-apps-sdk' import { Card, IconText, Loader, Menu, Title } from '@gnosis.pm/safe-react-components' import { useSelector } from 'react-redux' import styled, { css } from 'styled-components' @@ -7,6 +7,7 @@ import styled, { css } from 'styled-components' import ManageApps from './components/ManageApps' import AppFrame from './components/AppFrame' import { useAppList } from './hooks/useAppList' +import { SafeApp } from './types.d' import LCL from 'src/components/ListContentLayout' import { networkSelector } from 'src/logic/wallets/store/selectors' @@ -19,6 +20,7 @@ import { import { isSameURL } from 'src/utils/url' import { useIframeMessageHandler } from './hooks/useIframeMessageHandler' import ConfirmTransactionModal from './components/ConfirmTransactionModal' +import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' const centerCSS = css` display: flex; @@ -62,8 +64,9 @@ const Apps = (): React.ReactElement => { const [confirmTransactionModal, setConfirmTransactionModal] = useState( INITIAL_CONFIRM_TX_MODAL_STATE, ) - const iframeRef = useRef() + const iframeRef = useRef(null) + const { trackEvent } = useAnalytics() const granted = useSelector(grantedSelector) const safeAddress = useSelector(safeParamAddressFromStateSelector) const safeName = useSelector(safeNameSelector) @@ -99,6 +102,13 @@ const Apps = (): React.ReactElement => { ) } + const onTxReject = () => { + sendMessageToIframe( + { messageId: INTERFACE_MESSAGES.TRANSACTION_REJECTED, data: {} }, + confirmTransactionModal.requestId, + ) + } + const onSelectApp = useCallback( (appId) => { if (selectedAppId === appId) { @@ -111,6 +121,7 @@ const Apps = (): React.ReactElement => { [selectedAppId], ) + // Auto Select app first App useEffect(() => { const selectFirstEnabledApp = () => { const firstEnabledApp = appList.find((a) => !a.disabled) @@ -124,7 +135,14 @@ const Apps = (): React.ReactElement => { if (initialSelect || currentAppWasDisabled) { selectFirstEnabledApp() } - }, [appList, selectedApp, selectedAppId]) + }, [appList, selectedApp, selectedAppId, trackEvent]) + + // track GA + useEffect(() => { + if (selectedApp) { + trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Apps', label: selectedApp.name }) + } + }, [selectedApp, trackEvent]) const handleIframeLoad = useCallback(() => { const iframe = iframeRef.current @@ -136,14 +154,14 @@ const Apps = (): React.ReactElement => { sendMessageToIframe({ messageId: INTERFACE_MESSAGES.ON_SAFE_INFO, data: { - safeAddress, - network, - ethBalance, + safeAddress: safeAddress as string, + network: network.toLowerCase() as LowercaseNetworks, + ethBalance: ethBalance as string, }, }) }, [ethBalance, network, safeAddress, selectedApp, sendMessageToIframe]) - if (loadingAppList || !appList.length) { + if (loadingAppList || !appList.length || !safeAddress) { return ( @@ -189,14 +207,14 @@ const Apps = (): React.ReactElement => { ) diff --git a/src/routes/safe/components/Apps/types.d.ts b/src/routes/safe/components/Apps/types.d.ts index 4608012bb9..db8b49d112 100644 --- a/src/routes/safe/components/Apps/types.d.ts +++ b/src/routes/safe/components/Apps/types.d.ts @@ -1,5 +1,5 @@ export type SafeApp = { - id: string | undefined + id: string url: string name: string iconUrl: string diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts index cda21c9a13..beeba0c089 100644 --- a/src/routes/safe/components/Apps/utils.ts +++ b/src/routes/safe/components/Apps/utils.ts @@ -1,7 +1,7 @@ import axios from 'axios' import memoize from 'lodash.memoize' -import { SafeApp } from './types' +import { SafeApp } from './types.d' import { getGnosisSafeAppsUrl } from 'src/config/index' import { getContentFromENS } from 'src/logic/wallets/getWeb3' @@ -38,6 +38,10 @@ export const staticAppsList: Array<{ url: string; disabled: boolean }> = [ { url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmQovvfYYMUXjZfNbysQDUEXR8nr55iJRwcYgJQGJR7KEA`, disabled: false }, // TX-Builder { url: `${gnosisAppsUrl}/tx-builder`, disabled: false }, + // Wallet-Connect + { url: `${gnosisAppsUrl}/walletConnect`, disabled: false }, + // Yearn Vaults + { url: `${process.env.REACT_APP_IPFS_GATEWAY}/Qme9HuPPhgCtgfj1CktvaDKhTesMueGCV2Kui1Sqna3Xs9`, disabled: false }, ] export const getAppInfoFromOrigin = (origin: string): Record | null => { @@ -62,8 +66,8 @@ export const isAppManifestValid = (appInfo: SafeApp): boolean => !appInfo.error export const getAppInfoFromUrl = memoize( - async (appUrl?: string): Promise => { - let res = { id: undefined, url: appUrl, name: 'unknown', iconUrl: appsIconSvg, error: true, description: '' } + async (appUrl: string): Promise => { + let res = { id: '', url: appUrl, name: 'unknown', iconUrl: appsIconSvg, error: true, description: '' } if (!appUrl?.length) { return res diff --git a/src/routes/safe/components/Balances/Coins/index.tsx b/src/routes/safe/components/Balances/Coins/index.tsx index ac2feaa48e..82bfc97a23 100644 --- a/src/routes/safe/components/Balances/Coins/index.tsx +++ b/src/routes/safe/components/Balances/Coins/index.tsx @@ -1,13 +1,15 @@ +import React, { useEffect, useMemo } from 'react' +import { useSelector } from 'react-redux' +import { List } from 'immutable' import TableCell from '@material-ui/core/TableCell' +import Tooltip from '@material-ui/core/Tooltip' import TableContainer from '@material-ui/core/TableContainer' import TableRow from '@material-ui/core/TableRow' -import { makeStyles } from '@material-ui/core/styles' -import { List } from 'immutable' -import React from 'react' -import { useSelector } from 'react-redux' +import { Skeleton } from '@material-ui/lab' -import { styles } from './styles' +import InfoIcon from 'src/assets/icons/info.svg' +import Img from 'src/components/layout/Img' import Table from 'src/components/Table' import { cellWidth } from 'src/components/Table/TableHead' import Button from 'src/components/layout/Button' @@ -28,27 +30,38 @@ import { BalanceData, } from 'src/routes/safe/components/Balances/dataFetcher' import { extendedSafeTokensSelector, grantedSelector } from 'src/routes/safe/container/selector' -import { Skeleton } from '@material-ui/lab' +import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' +import { makeStyles } from '@material-ui/core/styles' +import { styles } from './styles' -const useStyles = makeStyles(styles as any) +const useStyles = makeStyles(styles) type Props = { showReceiveFunds: () => void showSendFunds: (tokenAddress: string) => void } -export type BalanceDataRow = List<{ - asset: { - name: string - address: string - logoUri: string +type CurrencyTooltipProps = { + valueWithCurrency: string + balanceWithSymbol: string +} + +const CurrencyTooltip = (props: CurrencyTooltipProps): React.ReactElement | null => { + const { balanceWithSymbol, valueWithCurrency } = props + const classes = useStyles() + const balance = balanceWithSymbol.replace(/[^\d.-]/g, '') + const value = valueWithCurrency.replace(/[^\d.-]/g, '') + if (!Number(value) && Number(balance)) { + return ( + + + Info Tooltip + + + ) } - assetOrder: string - balance: string - balanceOrder: number - fixed: boolean - value: string -}> + return null +} const Coins = (props: Props): React.ReactElement => { const { showReceiveFunds, showSendFunds } = props @@ -60,11 +73,16 @@ const Coins = (props: Props): React.ReactElement => { const activeTokens = useSelector(extendedSafeTokensSelector) const currencyValues = useSelector(safeFiatBalancesListSelector) const granted = useSelector(grantedSelector) - const [filteredData, setFilteredData] = React.useState>(List()) + const { trackEvent } = useAnalytics() + + useEffect(() => { + trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Coins' }) + }, [trackEvent]) - React.useMemo(() => { - setFilteredData(getBalanceData(activeTokens, selectedCurrency, currencyValues, currencyRate)) - }, [activeTokens, selectedCurrency, currencyValues, currencyRate]) + const filteredData: List = useMemo( + () => getBalanceData(activeTokens, selectedCurrency, currencyValues, currencyRate), + [activeTokens, selectedCurrency, currencyValues, currencyRate], + ) return ( @@ -96,10 +114,16 @@ const Coins = (props: Props): React.ReactElement => { // If there are no values for that row but we have balances, we display as '0.00 {CurrencySelected}' // In case we don't have balances, we display a skeleton const showCurrencyValueRow = row[id] || row[BALANCE_TABLE_BALANCE_ID] - + const valueWithCurrency = row[id] ? row[id] : `0.00 ${selectedCurrency}` cellItem = showCurrencyValueRow && selectedCurrency ? ( -
{row[id] ? row[id] : `0.00 ${selectedCurrency}`}
+
+ {valueWithCurrency} + +
) : ( ) diff --git a/src/routes/safe/components/Balances/Coins/styles.ts b/src/routes/safe/components/Balances/Coins/styles.ts index 94e0781a90..5df6bf5138 100644 --- a/src/routes/safe/components/Balances/Coins/styles.ts +++ b/src/routes/safe/components/Balances/Coins/styles.ts @@ -1,9 +1,15 @@ import { sm, xs } from 'src/theme/variables' +import { createStyles } from '@material-ui/core' -export const styles = () => ({ +export const styles = createStyles({ iconSmall: { fontSize: 16, }, + tooltipInfo: { + position: 'relative', + top: '3px', + left: '3px', + }, hide: { '&:hover': { backgroundColor: '#fff3e2', diff --git a/src/routes/safe/components/Balances/Collectibles/index.tsx b/src/routes/safe/components/Balances/Collectibles/index.tsx index f8153d6d94..c06b7227f1 100644 --- a/src/routes/safe/components/Balances/Collectibles/index.tsx +++ b/src/routes/safe/components/Balances/Collectibles/index.tsx @@ -1,6 +1,6 @@ +import React, { useEffect } from 'react' import Card from '@material-ui/core/Card' import { makeStyles } from '@material-ui/core/styles' -import React from 'react' import { useSelector } from 'react-redux' import Item from './components/Item' @@ -10,6 +10,7 @@ import { activeNftAssetsListSelector, nftTokensSelector } from 'src/logic/collec import SendModal from 'src/routes/safe/components/Balances/SendModal' import { safeSelector } from 'src/logic/safe/store/selectors' import { fontColor, lg, screenSm, screenXs } from 'src/theme/variables' +import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' const useStyles = makeStyles({ cardInner: { @@ -74,13 +75,18 @@ const useStyles = makeStyles({ }, } as any) -const Collectibles = () => { +const Collectibles = (): React.ReactElement => { const classes = useStyles() const [selectedToken, setSelectedToken] = React.useState({}) const [sendNFTsModalOpen, setSendNFTsModalOpen] = React.useState(false) - const { address, ethBalance, name } = useSelector(safeSelector) + const { address, ethBalance, name } = useSelector(safeSelector) || {} const nftTokens = useSelector(nftTokensSelector) const activeAssetsList = useSelector(activeNftAssetsListSelector) + const { trackEvent } = useAnalytics() + + useEffect(() => { + trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Collectibles' }) + }, [trackEvent]) const handleItemSend = (nftToken) => { setSelectedToken(nftToken) diff --git a/src/routes/safe/components/Balances/SendModal/SafeInfo/index.tsx b/src/routes/safe/components/Balances/SendModal/SafeInfo/index.tsx index 5cd13863f7..d2b49300f0 100644 --- a/src/routes/safe/components/Balances/SendModal/SafeInfo/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/SafeInfo/index.tsx @@ -5,7 +5,7 @@ import AddressInfo from 'src/components/AddressInfo' import { safeSelector } from 'src/logic/safe/store/selectors' const SafeInfo = () => { - const { address: safeAddress = '', ethBalance, name: safeName } = useSelector(safeSelector) + const { address: safeAddress = '', ethBalance, name: safeName } = useSelector(safeSelector) || {} return } diff --git a/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx index 6079daefc4..db9b9c90d2 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx @@ -1,8 +1,6 @@ import MuiTextField from '@material-ui/core/TextField' -import { withStyles } from '@material-ui/core/styles' import makeStyles from '@material-ui/core/styles/makeStyles' import Autocomplete from '@material-ui/lab/Autocomplete' -import { List } from 'immutable' import React, { useEffect, useState } from 'react' import { useSelector } from 'react-redux' import { trimSpaces } from 'src/utils/strings' @@ -11,10 +9,10 @@ import { styles } from './style' import Identicon from 'src/components/Identicon' import { mustBeEthereumAddress, mustBeEthereumContractAddress } from 'src/components/forms/validator' -import { getAddressBook } from 'src/logic/addressBook/store/selectors' +import { addressBookSelector } from 'src/logic/addressBook/store/selectors' import { getAddressFromENS } from 'src/logic/wallets/getWeb3' import { isValidEnsName } from 'src/logic/wallets/ethAddresses' -import { AddressBookEntryRecord } from 'src/logic/addressBook/model/addressBook' +import { AddressBookEntry, AddressBookState } from 'src/logic/addressBook/model/addressBook' export interface AddressBookProps { fieldMutator: (address: string) => void @@ -22,9 +20,9 @@ export interface AddressBookProps { pristine: boolean recipientAddress?: string setSelectedEntry: ( - entry: { address?: string; name?: string } | React.SetStateAction<{ address: string; name: string }>, + entry: { address?: string; name?: string } | React.SetStateAction<{ address?: string; name? }> | null, ) => void - setIsValidAddress: (valid?: boolean) => void + setIsValidAddress: (valid: boolean) => void } const useStyles = makeStyles(styles) @@ -45,12 +43,10 @@ const textFieldInputStyle = makeStyles(() => ({ }, })) -const filterAddressBookWithContractAddresses = async ( - addressBook: List, -): Promise> => { +const filterAddressBookWithContractAddresses = async (addressBook: AddressBookState): Promise => { const abFlags = await Promise.all( addressBook.map( - async ({ address }: AddressBookEntryRecord): Promise => { + async ({ address }: AddressBookEntry): Promise => { return (await mustBeEthereumContractAddress(address)) === undefined }, ), @@ -59,11 +55,6 @@ const filterAddressBookWithContractAddresses = async ( return addressBook.filter((_, index) => abFlags[index]) } -interface FilteredAddressBookEntry { - name: string - address: string -} - const AddressBookInput = ({ fieldMutator, isCustomTx, @@ -71,22 +62,23 @@ const AddressBookInput = ({ recipientAddress, setIsValidAddress, setSelectedEntry, -}: AddressBookProps) => { +}: AddressBookProps): React.ReactElement => { const classes = useStyles() - const addressBook = useSelector(getAddressBook) + const addressBook = useSelector(addressBookSelector) const [isValidForm, setIsValidForm] = useState(true) const [validationText, setValidationText] = useState('') const [inputTouched, setInputTouched] = useState(false) const [blurred, setBlurred] = useState(pristine) - const [adbkList, setADBKList] = useState>(List([])) + const [adbkList, setADBKList] = useState([]) const [inputAddValue, setInputAddValue] = useState(recipientAddress) const onAddressInputChanged = async (value: string): Promise => { const normalizedAddress = trimSpaces(value) + const isENSDomain = isValidEnsName(normalizedAddress) setInputAddValue(normalizedAddress) let resolvedAddress = normalizedAddress - let isValidText + let addressErrorMessage if (inputTouched && !normalizedAddress) { setIsValidForm(false) setValidationText('Required') @@ -94,13 +86,14 @@ const AddressBookInput = ({ return } if (normalizedAddress) { - if (isValidEnsName(normalizedAddress)) { + if (isENSDomain) { resolvedAddress = await getAddressFromENS(normalizedAddress) setInputAddValue(resolvedAddress) } - isValidText = mustBeEthereumAddress(resolvedAddress) - if (isCustomTx && isValidText === undefined) { - isValidText = await mustBeEthereumContractAddress(resolvedAddress) + + addressErrorMessage = mustBeEthereumAddress(resolvedAddress) + if (isCustomTx && addressErrorMessage === undefined) { + addressErrorMessage = await mustBeEthereumContractAddress(resolvedAddress) } // First removes the entries that are not contracts if the operation is custom tx @@ -110,18 +103,31 @@ const AddressBookInput = ({ const { address, name } = adbkEntry return ( name.toLowerCase().includes(normalizedAddress.toLowerCase()) || - address.toLowerCase().includes(normalizedAddress.toLowerCase()) + address.toLowerCase().includes(resolvedAddress.toLowerCase()) ) }) setADBKList(filteredADBK) - if (!isValidText) { - setSelectedEntry({ address: normalizedAddress }) + if (!addressErrorMessage) { + // base case if isENSDomain we set the domain as the name + // if address does not exist in address book we use blank name + let addressName = isENSDomain ? normalizedAddress : '' + + // if address is valid, and is in the address book, then we use the stored values + if (filteredADBK.length === 1) { + const addressBookContact = filteredADBK[0] + addressName = addressBookContact.name ?? addressName + } + + setSelectedEntry({ + name: addressName, + address: resolvedAddress, + }) } } - setIsValidForm(isValidText === undefined) - setValidationText(isValidText) + setIsValidForm(addressErrorMessage === undefined) + setValidationText(addressErrorMessage) fieldMutator(resolvedAddress) - setIsValidAddress(isValidText === undefined) + setIsValidAddress(addressErrorMessage === undefined) } useEffect(() => { @@ -157,14 +163,14 @@ const AddressBookInput = ({ optionsArray.filter((item) => { const inputLowerCase = inputValue.toLowerCase() const foundName = item.name.toLowerCase().includes(inputLowerCase) - const foundAddress = item.address.toLowerCase().includes(inputLowerCase) + const foundAddress = item.address?.toLowerCase().includes(inputLowerCase) return foundName || foundAddress }) } freeSolo getOptionLabel={(adbkEntry) => adbkEntry.address || ''} id="free-solo-demo" - onChange={(_, value: FilteredAddressBookEntry) => { + onChange={(_, value: AddressBookEntry) => { let address = '' let name = '' if (value) { @@ -180,7 +186,7 @@ const AddressBookInput = ({ setBlurred(false) }} open={!blurred} - options={adbkList.toArray()} + options={adbkList} renderInput={(params) => ( { const { address, name } = adbkEntry + + if (!address) { + return + } + return (
@@ -232,4 +243,4 @@ const AddressBookInput = ({ ) } -export default withStyles(styles as any)(AddressBookInput) +export default AddressBookInput diff --git a/src/routes/safe/components/Balances/SendModal/screens/ChooseTxType/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ChooseTxType/index.tsx index 596eff593a..371dfb7a0f 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ChooseTxType/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ChooseTxType/index.tsx @@ -1,13 +1,9 @@ import IconButton from '@material-ui/core/IconButton' -import { makeStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import classNames from 'classnames/bind' import * as React from 'react' import { useSelector } from 'react-redux' -import Collectible from '../assets/collectibles.svg' -import Token from '../assets/token.svg' - import { mustBeEthereumContractAddress } from 'src/components/forms/validator' import Button from 'src/components/layout/Button' import Col from 'src/components/layout/Col' @@ -15,55 +11,25 @@ import Hairline from 'src/components/layout/Hairline' import Img from 'src/components/layout/Img' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' +import { safeFeaturesEnabledSelector } from 'src/logic/safe/store/selectors' +import { useStyles } from 'src/routes/safe/components/Balances/SendModal/screens/ChooseTxType/style' import ContractInteractionIcon from 'src/routes/safe/components/Transactions/TxsTable/TxType/assets/custom.svg' -import { safeSelector } from 'src/logic/safe/store/selectors' -import { lg, md, sm } from 'src/theme/variables' -const useStyles = makeStyles({ - heading: { - padding: `${md} ${lg}`, - justifyContent: 'space-between', - boxSizing: 'border-box', - maxHeight: '75px', - }, - manage: { - fontSize: lg, - }, - disclaimer: { - marginBottom: `-${md}`, - paddingTop: md, - textAlign: 'center', - }, - disclaimerText: { - fontSize: md, - }, - closeIcon: { - height: '35px', - width: '35px', - }, - buttonColumn: { - padding: '52px 0', - '& > button': { - fontSize: md, - fontFamily: 'Averta', - }, - }, - firstButton: { - boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)', - marginBottom: 15, - }, - iconSmall: { - fontSize: 16, - }, - leftIcon: { - marginRight: sm, - }, -}) +import Collectible from '../assets/collectibles.svg' +import Token from '../assets/token.svg' + +type ActiveScreen = 'sendFunds' | 'sendCollectible' | 'contractInteraction' + +interface ChooseTxTypeProps { + onClose: () => void + recipientAddress: string + setActiveScreen: React.Dispatch> +} -const ChooseTxType = ({ onClose, recipientAddress, setActiveScreen }) => { +const ChooseTxType = ({ onClose, recipientAddress, setActiveScreen }: ChooseTxTypeProps): React.ReactElement => { const classes = useStyles() - const { featuresEnabled } = useSelector(safeSelector) - const erc721Enabled = featuresEnabled.includes('ERC721') + const featuresEnabled = useSelector(safeFeaturesEnabledSelector) + const erc721Enabled = featuresEnabled?.includes('ERC721') const [disableContractInteraction, setDisableContractInteraction] = React.useState(!!recipientAddress) React.useEffect(() => { diff --git a/src/routes/safe/components/Balances/SendModal/screens/ChooseTxType/style.ts b/src/routes/safe/components/Balances/SendModal/screens/ChooseTxType/style.ts new file mode 100644 index 0000000000..490db3ccf1 --- /dev/null +++ b/src/routes/safe/components/Balances/SendModal/screens/ChooseTxType/style.ts @@ -0,0 +1,45 @@ +import { createStyles, makeStyles } from '@material-ui/core/styles' +import { lg, md, sm } from 'src/theme/variables' + +export const useStyles = makeStyles( + createStyles({ + heading: { + padding: `${md} ${lg}`, + justifyContent: 'space-between', + boxSizing: 'border-box', + maxHeight: '75px', + }, + manage: { + fontSize: lg, + }, + disclaimer: { + marginBottom: `-${md}`, + paddingTop: md, + textAlign: 'center', + }, + disclaimerText: { + fontSize: md, + }, + closeIcon: { + height: '35px', + width: '35px', + }, + buttonColumn: { + padding: '52px 0', + '& > button': { + fontSize: md, + fontFamily: 'Averta', + }, + }, + firstButton: { + boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)', + marginBottom: 15, + }, + iconSmall: { + fontSize: 16, + }, + leftIcon: { + marginRight: sm, + }, + }), +) diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/EthAddressInput/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/EthAddressInput/index.tsx index c51b58d010..3a1f20d0f3 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/EthAddressInput/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/EthAddressInput/index.tsx @@ -11,6 +11,7 @@ import { mustBeEthereumAddress, mustBeEthereumContractAddress, required, + Validator, } from 'src/components/forms/validator' import Col from 'src/components/layout/Col' import Row from 'src/components/layout/Row' @@ -34,13 +35,17 @@ const EthAddressInput = ({ text, }: EthAddressInputProps): React.ReactElement => { const classes = useStyles() - const validatorsList = [isRequired && required, mustBeEthereumAddress, isContract && mustBeEthereumContractAddress] - const validate = composeValidators(...validatorsList.filter((_) => _)) + const validatorsList = [ + isRequired && required, + mustBeEthereumAddress, + isContract && mustBeEthereumContractAddress, + ] as Validator[] + const validate = composeValidators(...validatorsList.filter((validator) => validator)) const { pristine } = useFormState({ subscription: { pristine: true } }) const { input: { value }, } = useField('contractAddress', { subscription: { value: true } }) - const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string } | null>({ + const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string | null } | null>({ address: value, name: '', }) diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/EthValue/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/EthValue/index.tsx index 48482ae7cc..afda621363 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/EthValue/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/EthValue/index.tsx @@ -20,14 +20,18 @@ const useStyles = makeStyles(styles) interface EthValueProps { onSetMax: (ethBalance: string) => void } -const EthValue = ({ onSetMax }: EthValueProps) => { +const EthValue = ({ onSetMax }: EthValueProps): React.ReactElement | null => { const classes = useStyles() - const { ethBalance } = useSelector(safeSelector) + const { ethBalance } = useSelector(safeSelector) || {} const { input: { value: method }, } = useField('selectedMethod', { subscription: { value: true } }) const disabled = !isPayable(method) + if (!ethBalance) { + return null + } + return disabled ? null : ( <> diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/MethodsDropdown/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/MethodsDropdown/index.tsx index b804ea13cd..82ebacdfe1 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/MethodsDropdown/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/MethodsDropdown/index.tsx @@ -16,7 +16,7 @@ import { NO_CONTRACT } from 'src/routes/safe/components/Balances/SendModal/scree import CheckIcon from 'src/routes/safe/components/CurrencyDropdown/img/check.svg' import { useDropdownStyles } from 'src/routes/safe/components/CurrencyDropdown/style' import { DropdownListTheme } from 'src/theme/mui' -import { extractUsefulMethods } from 'src/logic/contractInteraction/sources/ABIService' +import { extractUsefulMethods, AbiItemExtended } from 'src/logic/contractInteraction/sources/ABIService' const MENU_WIDTH = '452px' @@ -24,7 +24,7 @@ interface MethodsDropdownProps { onChange: (method: AbiItem) => void } -const MethodsDropdown = ({ onChange }: MethodsDropdownProps) => { +const MethodsDropdown = ({ onChange }: MethodsDropdownProps): React.ReactElement | null => { const classes = useDropdownStyles({ buttonWidth: MENU_WIDTH }) const { input: { value: abi }, @@ -34,8 +34,8 @@ const MethodsDropdown = ({ onChange }: MethodsDropdownProps) => { initialValues: { selectedMethod: selectedMethodByDefault }, } = useFormState({ subscription: { initialValues: true } }) const [selectedMethod, setSelectedMethod] = React.useState(selectedMethodByDefault ? selectedMethodByDefault : {}) - const [methodsList, setMethodsList] = React.useState([]) - const [methodsListFiltered, setMethodsListFiltered] = React.useState([]) + const [methodsList, setMethodsList] = React.useState([]) + const [methodsListFiltered, setMethodsListFiltered] = React.useState([]) const [anchorEl, setAnchorEl] = React.useState(null) const [searchParams, setSearchParams] = React.useState('') @@ -50,7 +50,7 @@ const MethodsDropdown = ({ onChange }: MethodsDropdownProps) => { }, [abi]) React.useEffect(() => { - setMethodsListFiltered(methodsList.filter(({ name }) => name.toLowerCase().includes(searchParams.toLowerCase()))) + setMethodsListFiltered(methodsList.filter(({ name }) => name?.toLowerCase().includes(searchParams.toLowerCase()))) }, [methodsList, searchParams]) const handleClick = (event) => { diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/RenderInputParams/InputComponent/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/RenderInputParams/InputComponent/index.tsx index bbad4ed095..9c8af66c8e 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/RenderInputParams/InputComponent/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/RenderInputParams/InputComponent/index.tsx @@ -15,7 +15,7 @@ type Props = { placeholder: string } -const InputComponent = ({ type, keyValue, placeholder }: Props): React.ReactElement => { +const InputComponent = ({ type, keyValue, placeholder }: Props): React.ReactElement | null => { if (!type) { return null } diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/RenderInputParams/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/RenderInputParams/index.tsx index 70cc2d791e..47b3b6c6d6 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/RenderInputParams/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/RenderInputParams/index.tsx @@ -7,18 +7,18 @@ import InputComponent from './InputComponent' import { generateFormFieldKey } from '../utils' import { AbiItemExtended } from 'src/logic/contractInteraction/sources/ABIService' -const RenderInputParams = (): React.ReactElement => { +const RenderInputParams = (): React.ReactElement | null => { const { meta: { valid: validABI }, } = useField('abi', { subscription: { valid: true, value: true } }) const { input: { value: method }, }: { input: { value: AbiItemExtended } } = useField('selectedMethod', { subscription: { value: true } }) - const renderInputs = validABI && !!method && method.inputs.length + const renderInputs = validABI && !!method && method.inputs?.length return !renderInputs ? null : ( <> - {method.inputs.map(({ name, type }, index) => { + {method.inputs?.map(({ name, type }, index) => { const placeholder = name ? `${name} (${type})` : type const key = generateFormFieldKey(type, method.signatureHash, index) diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx index 99c00ff832..5d2d1c1064 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx @@ -40,11 +40,11 @@ type Props = { tx: TransactionReviewType } -const ContractInteractionReview = ({ onClose, onPrev, tx }: Props) => { +const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactElement => { const { enqueueSnackbar, closeSnackbar } = useSnackbar() const classes = useStyles() const dispatch = useDispatch() - const { address: safeAddress } = useSelector(safeSelector) + const { address: safeAddress } = useSelector(safeSelector) || {} const [gasCosts, setGasCosts] = useState('< 0.001') useEffect(() => { @@ -54,7 +54,7 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props) => { const { fromWei, toBN } = getWeb3().utils const txData = tx.data ? tx.data.trim() : '' - const estimatedGasCosts = await estimateTxGasCosts(safeAddress, tx.contractAddress, txData) + const estimatedGasCosts = await estimateTxGasCosts(safeAddress as string, tx.contractAddress as string, txData) const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether') const formattedGasCosts = formatAmount(gasCostsAsEth) @@ -102,7 +102,7 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props) => { - + @@ -129,11 +129,11 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props) => { - {tx.selectedMethod.name} + {tx.selectedMethod?.name} - {tx.selectedMethod.inputs.map(({ name, type }, index) => { - const key = generateFormFieldKey(type, tx.selectedMethod.signatureHash, index) + {tx.selectedMethod?.inputs?.map(({ name, type }, index) => { + const key = generateFormFieldKey(type, tx.selectedMethod?.signatureHash || '', index) const value: string = getValueFromTxInputs(key, type, tx) return ( diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ReviewCustomTx/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ReviewCustomTx/index.tsx index c27dfa5f22..b47b8f2f97 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ReviewCustomTx/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ReviewCustomTx/index.tsx @@ -1,7 +1,6 @@ import IconButton from '@material-ui/core/IconButton' import { makeStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' -import { useSnackbar } from 'notistack' import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' @@ -39,10 +38,9 @@ type Props = { const useStyles = makeStyles(styles) const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => { - const { enqueueSnackbar, closeSnackbar } = useSnackbar() const classes = useStyles() const dispatch = useDispatch() - const { address: safeAddress } = useSelector(safeSelector) + const { address: safeAddress } = useSelector(safeSelector) || {} const [gasCosts, setGasCosts] = useState('< 0.001') useEffect(() => { @@ -52,7 +50,7 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => { const { fromWei, toBN } = getWeb3().utils const txData = tx.data ? tx.data.trim() : '' - const estimatedGasCosts = await estimateTxGasCosts(safeAddress, tx.contractAddress, txData) + const estimatedGasCosts = await estimateTxGasCosts(safeAddress as string, tx.contractAddress as string, txData) const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether') const formattedGasCosts = formatAmount(gasCostsAsEth) @@ -76,14 +74,12 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => { dispatch( createTransaction({ - safeAddress, - to: txRecipient, + safeAddress: safeAddress as string, + to: txRecipient as string, valueInWei: txValue, txData, notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, - enqueueSnackbar, - closeSnackbar, - } as any), + }), ) onClose() @@ -118,15 +114,15 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => { - + {tx.contractAddress} - - + + diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/SendCustomTx/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/SendCustomTx/index.tsx index c481f080c7..99bbd1008f 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/SendCustomTx/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/SendCustomTx/index.tsx @@ -52,9 +52,9 @@ const useStyles = makeStyles(styles) const SendCustomTx: React.FC = ({ initialValues, onClose, onNext, contractAddress, switchMethod, isABI }) => { const classes = useStyles() - const { ethBalance } = useSelector(safeSelector) + const { ethBalance } = useSelector(safeSelector) || {} const [qrModalOpen, setQrModalOpen] = useState(false) - const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string } | null>({ + const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string | null } | null>({ address: contractAddress || initialValues.contractAddress, name: '', }) @@ -230,7 +230,7 @@ const SendCustomTx: React.FC = ({ initialValues, onClose, onNext, contrac placeholder="Value*" text="Value*" type="text" - validate={composeValidators(mustBeFloat, maxValue(ethBalance), minValue(0))} + validate={composeValidators(mustBeFloat, maxValue(ethBalance || '0'), minValue(0))} /> diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/index.tsx index 5d8048ef02..31d28b3f14 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/index.tsx @@ -49,7 +49,7 @@ const ContractInteraction: React.FC = ({ isABI, }) => { const classes = useStyles() - const { address: safeAddress = '' } = useSelector(safeSelector) + const { address: safeAddress = '' } = useSelector(safeSelector) || {} let setCallResults React.useMemo(() => { diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style.ts b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style.ts index 14fdac1441..9b18ef70e5 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style.ts +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style.ts @@ -26,6 +26,7 @@ export const styles = createStyles({ }, formContainer: { padding: `${md} ${lg}`, + wordBreak: 'break-word', }, value: { marginLeft: sm, diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils/index.ts b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils/index.ts index c6a41b9146..52922fd024 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils/index.ts +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils/index.ts @@ -59,7 +59,7 @@ export const formMutators: Record { const modified = - state.lastFormState.values.selectedMethod && state.lastFormState.values.selectedMethod.name !== args[0].name + state.lastFormState?.values.selectedMethod && state.lastFormState.values.selectedMethod.name !== args[0].name if (modified) { utils.changeValue(state, 'callResults', () => '') @@ -115,8 +115,8 @@ export const createTxObject = ( ): ContractSendMethod => { const web3 = getWeb3() const contract: any = new web3.eth.Contract([method], contractAddress) - const { inputs, name, signatureHash } = method - const args = inputs.map(extractMethodArgs(signatureHash, values)) + const { inputs, name = '', signatureHash } = method + const args = inputs?.map(extractMethodArgs(signatureHash, values)) || [] return contract.methods[name](...args) } diff --git a/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx index a87afba9d3..1b7cb68d8b 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx @@ -43,7 +43,7 @@ const ReviewCollectible = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx const classes = useStyles() const shortener = textShortener() const dispatch = useDispatch() - const { address: safeAddress } = useSelector(safeSelector) + const { address: safeAddress } = useSelector(safeSelector) || {} const nftTokens = useSelector(nftTokensSelector) const [gasCosts, setGasCosts] = useState('< 0.001') const txToken = nftTokens.find( @@ -66,7 +66,7 @@ const ReviewCollectible = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx const tokenInstance = await ERC721Token.at(tx.assetAddress) const txData = tokenInstance.contract.methods[methodToCall](...params).encodeABI() - const estimatedGasCosts = await estimateTxGasCosts(safeAddress, tx.recipientAddress, txData) + const estimatedGasCosts = await estimateTxGasCosts(safeAddress as string, tx.recipientAddress, txData) const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether') const formattedGasCosts = formatAmount(gasCostsAsEth) @@ -148,7 +148,7 @@ const ReviewCollectible = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx {txToken.name} - {shortener(txToken.name)} (Token ID: {shortener(txToken.tokenId)}) + {shortener(txToken.name)} (Token ID: {shortener(txToken.tokenId as string)}) )} diff --git a/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/index.tsx index b6de2579c6..0ee764bbe5 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/index.tsx @@ -3,7 +3,7 @@ import { makeStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import { BigNumber } from 'bignumber.js' import { withSnackbar } from 'notistack' -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState, useMemo } from 'react' import { useDispatch, useSelector } from 'react-redux' import ArrowDown from '../assets/arrow-down.svg' @@ -39,14 +39,14 @@ const useStyles = makeStyles(styles as any) const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }) => { const classes = useStyles() const dispatch = useDispatch() - const { address: safeAddress } = useSelector(safeSelector) + const { address: safeAddress } = useSelector(safeSelector) || {} const tokens = useSelector(extendedSafeTokensSelector) const [gasCosts, setGasCosts] = useState('< 0.001') const [data, setData] = useState('') - const txToken = tokens.find((token) => token.address === tx.token) - const isSendingETH = txToken.address === ETH_ADDRESS - const txRecipient = isSendingETH ? tx.recipientAddress : txToken.address + const txToken = useMemo(() => tokens.find((token) => token.address === tx.token), [tokens, tx.token]) + const isSendingETH = txToken?.address === ETH_ADDRESS + const txRecipient = isSendingETH ? tx.recipientAddress : txToken?.address useEffect(() => { let isCurrent = true @@ -54,18 +54,22 @@ const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }) => { const estimateGas = async () => { const { fromWei, toBN } = getWeb3().utils + if (!txToken) { + return + } + let txData = EMPTY_DATA if (!isSendingETH) { const StandardToken = await getHumanFriendlyToken() - const tokenInstance = await StandardToken.at(txToken.address) + const tokenInstance = await StandardToken.at(txToken.address as string) const decimals = await tokenInstance.decimals() const txAmount = new BigNumber(tx.amount).times(10 ** decimals.toNumber()).toString() txData = tokenInstance.contract.methods.transfer(tx.recipientAddress, txAmount).encodeABI() } - const estimatedGasCosts = await estimateTxGasCosts(safeAddress, txRecipient, txData) + const estimatedGasCosts = await estimateTxGasCosts(safeAddress as string, txRecipient, txData) const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether') const formattedGasCosts = formatAmount(gasCostsAsEth) @@ -80,7 +84,7 @@ const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }) => { return () => { isCurrent = false } - }, [isSendingETH, safeAddress, tx.amount, tx.recipientAddress, txRecipient, txToken.address]) + }, [isSendingETH, safeAddress, tx.amount, tx.recipientAddress, txRecipient, txToken]) const submitTx = async () => { const web3 = getWeb3() @@ -155,9 +159,14 @@ const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }) => { - {txToken.name} - - {tx.amount} {txToken.symbol} + {txToken?.name + + {tx.amount} {txToken?.symbol} diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/index.tsx index 637085a707..14c54f06cb 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/index.tsx @@ -20,8 +20,8 @@ import Col from 'src/components/layout/Col' import Hairline from 'src/components/layout/Hairline' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' -import { getAddressBook } from 'src/logic/addressBook/store/selectors' -import { getNameFromAdbk } from 'src/logic/addressBook/utils' +import { addressBookSelector } from 'src/logic/addressBook/store/selectors' +import { getNameFromAddressBook } from 'src/logic/addressBook/utils' import { nftTokensSelector, safeActiveSelectorMap } from 'src/logic/collectibles/store/selectors' import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo' import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput' @@ -41,19 +41,25 @@ const formMutators = { }, } -const useStyles = makeStyles(styles as any) +const useStyles = makeStyles(styles) -const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, selectedToken = {} }) => { +const SendCollectible = ({ + initialValues, + onClose, + onNext, + recipientAddress, + selectedToken = {}, +}): React.ReactElement => { const classes = useStyles() const nftAssets = useSelector(safeActiveSelectorMap) const nftTokens = useSelector(nftTokensSelector) - const addressBook = useSelector(getAddressBook) - const [selectedEntry, setSelectedEntry] = useState({ + const addressBook = useSelector(addressBookSelector) + const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string | null } | null>({ address: recipientAddress || initialValues.recipientAddress, name: '', }) const [pristine, setPristine] = useState(true) - const [isValidAddress, setIsValidAddress] = useState(true) + const [isValidAddress, setIsValidAddress] = useState(false) React.useMemo(() => { if (selectedEntry === null && pristine) { @@ -64,7 +70,7 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel const handleSubmit = (values) => { // If the input wasn't modified, there was no mutation of the recipientAddress if (!values.recipientAddress) { - values.recipientAddress = selectedEntry.address + values.recipientAddress = selectedEntry?.address } values.assetName = nftAssets[values.assetAddress].name @@ -97,7 +103,7 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel if (scannedAddress.startsWith('ethereum:')) { scannedAddress = scannedAddress.replace('ethereum:', '') } - const scannedName = addressBook ? getNameFromAdbk(addressBook, scannedAddress) : '' + const scannedName = addressBook ? getNameFromAddressBook(addressBook, scannedAddress) : '' mutators.setRecipient(scannedAddress) setSelectedEntry({ name: scannedName, @@ -129,7 +135,7 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
{ if (e.keyCode !== 9) { - setSelectedEntry(null) + setSelectedEntry({ address: '', name: 'string' }) } }} role="listbox" @@ -150,7 +156,7 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel setSelectedEntry(null)} + onClick={() => setSelectedEntry({ address: '', name: 'string' })} weight="bolder" > {selectedEntry.name} @@ -158,7 +164,7 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel setSelectedEntry(null)} + onClick={() => setSelectedEntry({ address: '', name: 'string' })} weight="bolder" > {selectedEntry.address} diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/style.ts b/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/style.ts index 44d7d9bb97..776dea0432 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/style.ts +++ b/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/style.ts @@ -1,6 +1,7 @@ import { lg, md, secondaryText } from 'src/theme/variables' +import { createStyles } from '@material-ui/core' -export const styles = () => ({ +export const styles = createStyles({ heading: { padding: `${md} ${lg}`, justifyContent: 'flex-start', diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx index 8b78fc79b4..27db52e9e4 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx @@ -25,8 +25,8 @@ import Col from 'src/components/layout/Col' import Hairline from 'src/components/layout/Hairline' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' -import { getAddressBook } from 'src/logic/addressBook/store/selectors' -import { getNameFromAdbk } from 'src/logic/addressBook/utils' +import { addressBookSelector } from 'src/logic/addressBook/store/selectors' +import { getNameFromAddressBook } from 'src/logic/addressBook/utils' import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo' import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput' @@ -48,17 +48,35 @@ const formMutators = { const useStyles = makeStyles(styles as any) -const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedToken = '' }): React.ReactElement => { +type SendFundsProps = { + initialValues: { + amount?: string + recipientAddress?: string + token?: string + } + onClose: () => void + onNext: (txInfo: unknown) => void + recipientAddress: string + selectedToken: string +} + +const SendFunds = ({ + initialValues, + onClose, + onNext, + recipientAddress, + selectedToken = '', +}: SendFundsProps): React.ReactElement => { const classes = useStyles() const tokens = useSelector(extendedSafeTokensSelector) - const addressBook = useSelector(getAddressBook) - const [selectedEntry, setSelectedEntry] = useState({ + const addressBook = useSelector(addressBookSelector) + const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string | null } | null>({ address: recipientAddress || initialValues.recipientAddress, name: '', }) const [pristine, setPristine] = useState(true) - const [isValidAddress, setIsValidAddress] = useState(true) + const [isValidAddress, setIsValidAddress] = useState(false) React.useMemo(() => { if (selectedEntry === null && pristine) { @@ -70,7 +88,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT const submitValues = values // If the input wasn't modified, there was no mutation of the recipientAddress if (!values.recipientAddress) { - submitValues.recipientAddress = selectedEntry.address + submitValues.recipientAddress = selectedEntry?.address } onNext(submitValues) } @@ -100,10 +118,10 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT if (scannedAddress.startsWith('ethereum:')) { scannedAddress = scannedAddress.replace('ethereum:', '') } - const scannedName = addressBook ? getNameFromAdbk(addressBook, scannedAddress) : '' + const scannedName = addressBook ? getNameFromAddressBook(addressBook, scannedAddress) : '' mutators.setRecipient(scannedAddress) setSelectedEntry({ - name: scannedName, + name: scannedName || '', address: scannedAddress, }) closeQrModal() @@ -130,7 +148,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
{ if (e.keyCode !== 9) { - setSelectedEntry(null) + setSelectedEntry({ address: '', name: 'string' }) } }} role="listbox" @@ -151,7 +169,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT setSelectedEntry(null)} + onClick={() => setSelectedEntry({ address: '', name: 'string' })} weight="bolder" > {selectedEntry.name} @@ -159,7 +177,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT setSelectedEntry(null)} + onClick={() => setSelectedEntry({ address: '', name: 'string' })} weight="bolder" > {selectedEntry.address} @@ -204,7 +222,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT Amount mutators.setMax(selectedTokenRecord.balance)} + onClick={() => mutators.setMax(selectedTokenRecord?.balance)} weight="bold" testId="send-max-btn" > @@ -230,7 +248,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT required, mustBeFloat, minValue(0, false), - maxValue(selectedTokenRecord?.balance), + maxValue(selectedTokenRecord?.balance || 0), )} /> diff --git a/src/routes/safe/components/Balances/Tokens/actions.ts b/src/routes/safe/components/Balances/Tokens/actions.ts deleted file mode 100644 index 8307f06915..0000000000 --- a/src/routes/safe/components/Balances/Tokens/actions.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { addToken } from 'src/logic/tokens/store/actions/addToken' -import fetchTokens from 'src/logic/tokens/store/actions/fetchTokens' -import activateTokenForAllSafes from 'src/logic/safe/store/actions/activateTokenForAllSafes' -import updateActiveTokens from 'src/logic/safe/store/actions/updateActiveTokens' -import updateBlacklistedTokens from 'src/logic/safe/store/actions/updateBlacklistedTokens' - -export default { - fetchTokens, - addToken, - updateActiveTokens, - updateBlacklistedTokens, - activateTokenForAllSafes, -} diff --git a/src/routes/safe/components/Balances/Tokens/index.tsx b/src/routes/safe/components/Balances/Tokens/index.tsx index 6b4e59c3f9..a25c876289 100644 --- a/src/routes/safe/components/Balances/Tokens/index.tsx +++ b/src/routes/safe/components/Balances/Tokens/index.tsx @@ -1,11 +1,10 @@ import IconButton from '@material-ui/core/IconButton' -import { withStyles } from '@material-ui/core/styles' +import { makeStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import React, { useState } from 'react' -import { connect, useSelector } from 'react-redux' +import { useSelector } from 'react-redux' -import actions from './actions' import { styles } from './style' import Hairline from 'src/components/layout/Hairline' @@ -16,27 +15,27 @@ import { orderedTokenListSelector } from 'src/logic/tokens/store/selectors' import AddCustomAssetComponent from 'src/routes/safe/components/Balances/Tokens/screens/AddCustomAsset' import AddCustomToken from 'src/routes/safe/components/Balances/Tokens/screens/AddCustomToken' import AssetsList from 'src/routes/safe/components/Balances/Tokens/screens/AssetsList' -import TokenList from 'src/routes/safe/components/Balances/Tokens/screens/TokenList' + import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector' import { safeBlacklistedTokensSelector } from 'src/logic/safe/store/selectors' +import { TokenList } from 'src/routes/safe/components/Balances/Tokens/screens/TokenList' export const MANAGE_TOKENS_MODAL_CLOSE_BUTTON_TEST_ID = 'manage-tokens-close-modal-btn' -const Tokens = (props) => { - const { - activateTokenForAllSafes, - addToken, - classes, - fetchTokens, - modalScreen, - onClose, - safeAddress, - updateActiveTokens, - updateBlacklistedTokens, - } = props +const useStyles = makeStyles(styles) + +type Props = { + safeAddress: string + modalScreen: string + onClose: () => void +} + +const Tokens = (props: Props): React.ReactElement => { + const { modalScreen, onClose, safeAddress } = props const tokens = useSelector(orderedTokenListSelector) const activeTokens = useSelector(extendedSafeTokensSelector) const blacklistedTokens = useSelector(safeBlacklistedTokensSelector) + const classes = useStyles() const [activeScreen, setActiveScreen] = useState(modalScreen) return ( @@ -54,26 +53,20 @@ const Tokens = (props) => { )} {activeScreen === 'assetsList' && } {activeScreen === 'addCustomToken' && ( )} {activeScreen === 'addCustomAsset' && ( @@ -83,6 +76,4 @@ const Tokens = (props) => { ) } -const TokenComponent = withStyles(styles as any)(Tokens) - -export default connect(undefined, actions)(TokenComponent) +export default Tokens diff --git a/src/routes/safe/components/Balances/Tokens/screens/AddCustomToken/index.tsx b/src/routes/safe/components/Balances/Tokens/screens/AddCustomToken/index.tsx index 3f57ffa365..c5053040f4 100644 --- a/src/routes/safe/components/Balances/Tokens/screens/AddCustomToken/index.tsx +++ b/src/routes/safe/components/Balances/Tokens/screens/AddCustomToken/index.tsx @@ -1,4 +1,4 @@ -import { withStyles } from '@material-ui/core/styles' +import { makeStyles } from '@material-ui/core/styles' import React, { useState } from 'react' import { FormSpy } from 'react-final-form' @@ -22,6 +22,12 @@ import Row from 'src/components/layout/Row' import TokenPlaceholder from 'src/routes/safe/components/Balances/assets/token_placeholder.svg' import { checksumAddress } from 'src/utils/checksumAddress' import { Checkbox } from '@gnosis.pm/safe-react-components' +import { useDispatch } from 'react-redux' +import { addToken } from 'src/logic/tokens/store/actions/addToken' +import updateActiveTokens from 'src/logic/safe/store/actions/updateActiveTokens' +import activateTokenForAllSafes from 'src/logic/safe/store/actions/activateTokenForAllSafes' +import { Token } from 'src/logic/tokens/store/model/token' +import { List, Set } from 'immutable' export const ADD_CUSTOM_TOKEN_ADDRESS_INPUT_TEST_ID = 'add-custom-token-address-input' export const ADD_CUSTOM_TOKEN_SYMBOLS_INPUT_TEST_ID = 'add-custom-token-symbols-input' @@ -35,20 +41,22 @@ const INITIAL_FORM_STATE = { logoUri: '', } -const AddCustomToken = (props) => { - const { - activateTokenForAllSafes, - activeTokens, - addToken, - classes, - onClose, - parentList, - safeAddress, - setActiveScreen, - tokens, - updateActiveTokens, - } = props +const useStyles = makeStyles(styles) + +type Props = { + activeTokens: List + onClose: () => void + parentList: string + safeAddress: string + setActiveScreen: (screen: string) => void + tokens: List +} + +const AddCustomToken = (props: Props): React.ReactElement => { + const { activeTokens, onClose, parentList, safeAddress, setActiveScreen, tokens } = props const [formValues, setFormValues] = useState(INITIAL_FORM_STATE) + const classes = useStyles() + const dispatch = useDispatch() const handleSubmit = (values) => { const address = checksumAddress(values.address) @@ -59,12 +67,12 @@ const AddCustomToken = (props) => { name: values.symbol, } - addToken(token) + dispatch(addToken(token)) if (values.showForAllSafes) { - activateTokenForAllSafes(token.address) + dispatch(activateTokenForAllSafes(token.address)) } else { - const activeTokensAddresses = activeTokens.map(({ address }) => address) - updateActiveTokens(safeAddress, activeTokensAddresses.push(token.address)) + const activeTokensAddresses = Set(activeTokens.map(({ address }) => address)) + dispatch(updateActiveTokens(safeAddress, activeTokensAddresses.add(token.address))) } onClose() @@ -203,6 +211,4 @@ const AddCustomToken = (props) => { ) } -const AddCustomTokenComponent = withStyles(styles as any)(AddCustomToken) - -export default AddCustomTokenComponent +export default AddCustomToken diff --git a/src/routes/safe/components/Balances/Tokens/screens/AddCustomToken/style.ts b/src/routes/safe/components/Balances/Tokens/screens/AddCustomToken/style.ts index 6c8f0b357e..793edae931 100644 --- a/src/routes/safe/components/Balances/Tokens/screens/AddCustomToken/style.ts +++ b/src/routes/safe/components/Balances/Tokens/screens/AddCustomToken/style.ts @@ -1,6 +1,7 @@ import { lg, md } from 'src/theme/variables' +import { createStyles } from '@material-ui/core' -export const styles = () => ({ +export const styles = createStyles({ title: { padding: `${lg} 0 20px`, fontSize: md, diff --git a/src/routes/safe/components/Balances/Tokens/screens/AssetsList/index.tsx b/src/routes/safe/components/Balances/Tokens/screens/AssetsList/index.tsx index 3a3ccb6c36..1e87da0a6e 100644 --- a/src/routes/safe/components/Balances/Tokens/screens/AssetsList/index.tsx +++ b/src/routes/safe/components/Balances/Tokens/screens/AssetsList/index.tsx @@ -4,7 +4,7 @@ import { makeStyles } from '@material-ui/core/styles' import Search from '@material-ui/icons/Search' import cn from 'classnames' import SearchBar from 'material-ui-search-bar' -import React, { useEffect, useState } from 'react' +import React, { useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { FixedSizeList } from 'react-window' @@ -55,11 +55,6 @@ const AssetsList = (props) => { const [blacklistedAssetsAddresses, setBlacklistedAssetsAddresses] = useState(blacklistedAssets) const nftAssetsList = useSelector(nftAssetsListSelector) - useEffect(() => { - dispatch(updateActiveAssets(safeAddress, activeAssetsAddresses)) - dispatch(updateBlacklistedAssets(safeAddress, blacklistedAssetsAddresses)) - }, [activeAssetsAddresses, blacklistedAssetsAddresses, dispatch, safeAddress]) - const onCancelSearch = () => { setFilterValue('') } @@ -73,19 +68,22 @@ const AssetsList = (props) => { } const onSwitch = (asset) => () => { - const { address } = asset - const activeAssetsAddressesResult = activeAssetsAddresses.contains(address) - ? activeAssetsAddresses.remove(address) - : activeAssetsAddresses.add(address) - const blacklistedAssetsAddressesResult = activeAssetsAddresses.has(address) - ? blacklistedAssetsAddresses.add(address) - : blacklistedAssetsAddresses.remove(address) - setActiveAssetsAddresses(activeAssetsAddressesResult) - setBlacklistedAssetsAddresses(blacklistedAssetsAddressesResult) - return { - activeAssetsAddresses: activeAssetsAddressesResult, - blacklistedAssetsAddresses: blacklistedAssetsAddressesResult, + let newActiveAssetsAddresses + let newBlacklistedAssetsAddresses + if (activeAssetsAddresses.has(asset.address)) { + newActiveAssetsAddresses = activeAssetsAddresses.delete(asset.address) + newBlacklistedAssetsAddresses = blacklistedAssetsAddresses.add(asset.address) + } else { + newActiveAssetsAddresses = activeAssetsAddresses.add(asset.address) + newBlacklistedAssetsAddresses = blacklistedAssetsAddresses.delete(asset.address) } + + // Set local state + setActiveAssetsAddresses(newActiveAssetsAddresses) + setBlacklistedAssetsAddresses(newBlacklistedAssetsAddresses) + // Dispatch to global state + dispatch(updateActiveAssets(safeAddress, newActiveAssetsAddresses)) + dispatch(updateBlacklistedAssets(safeAddress, newBlacklistedAssetsAddresses)) } const createItemData = (assetsList) => { diff --git a/src/routes/safe/components/Balances/Tokens/screens/TokenList/index.tsx b/src/routes/safe/components/Balances/Tokens/screens/TokenList/index.tsx index 819f7284bb..999dd9418f 100644 --- a/src/routes/safe/components/Balances/Tokens/screens/TokenList/index.tsx +++ b/src/routes/safe/components/Balances/Tokens/screens/TokenList/index.tsx @@ -1,11 +1,11 @@ import CircularProgress from '@material-ui/core/CircularProgress' import MuiList from '@material-ui/core/List' -import { withStyles } from '@material-ui/core/styles' +import { makeStyles } from '@material-ui/core/styles' import Search from '@material-ui/icons/Search' import cn from 'classnames' -import { Set } from 'immutable' +import { List, Set } from 'immutable' import SearchBar from 'material-ui-search-bar' -import * as React from 'react' +import React, { useState } from 'react' import { FixedSizeList } from 'react-window' import TokenRow from './TokenRow' @@ -17,10 +17,14 @@ import Button from 'src/components/layout/Button' import Divider from 'src/components/layout/Divider' import Hairline from 'src/components/layout/Hairline' import Row from 'src/components/layout/Row' +import { Token } from 'src/logic/tokens/store/model/token' +import { useDispatch } from 'react-redux' +import updateBlacklistedTokens from 'src/logic/safe/store/actions/updateBlacklistedTokens' +import updateActiveTokens from 'src/logic/safe/store/actions/updateActiveTokens' export const ADD_CUSTOM_TOKEN_BUTTON_TEST_ID = 'add-custom-token-btn' -const filterBy = (filter, tokens) => +const filterBy = (filter: string, tokens: List): List => tokens.filter( (token) => !filter || @@ -28,163 +32,129 @@ const filterBy = (filter, tokens) => token.name.toLowerCase().includes(filter.toLowerCase()), ) -// OPTIMIZATION IDEA (Thanks Andre) -// Calculate active tokens on component mount, store it in component state -// After user closes modal, dispatch an action so we don't have 100500 actions -// And selectors don't recalculate - -class Tokens extends React.Component { - renderCount = 0 - - state = { - filter: '', - activeTokensAddresses: Set([]), - initialActiveTokensAddresses: Set([]), - blacklistedTokensAddresses: Set([]), - activeTokensCalculated: false, - blacklistedTokensCalculated: false, - } - - static getDerivedStateFromProps(nextProps, prevState) { - // I moved this logic here because if placed in ComponentDidMount - // the user would see Switches switch and this method fires before the component mounts - - if (!prevState.activeTokensCalculated) { - const { activeTokens } = nextProps - - return { - activeTokensAddresses: Set(activeTokens.map(({ address }) => address)), - initialActiveTokensAddresses: Set(activeTokens.map(({ address }) => address)), - activeTokensCalculated: true, - } - } - - if (!prevState.blacklistedTokensCalculated) { - const { blacklistedTokens } = nextProps - - return { - blacklistedTokensAddresses: blacklistedTokens, - blacklistedTokensCalculated: true, - } - } - - return null - } +const useStyles = makeStyles(styles) - componentWillUnmount() { - const { activeTokensAddresses, blacklistedTokensAddresses } = this.state - const { safeAddress, updateActiveTokens, updateBlacklistedTokens } = this.props +type Props = { + setActiveScreen: (newScreen: string) => void + tokens: List + activeTokens: List + blacklistedTokens: Set + safeAddress: string +} - updateActiveTokens(safeAddress, activeTokensAddresses) - updateBlacklistedTokens(safeAddress, blacklistedTokensAddresses) +export const TokenList = (props: Props): React.ReactElement => { + const classes = useStyles() + const { setActiveScreen, tokens, activeTokens, blacklistedTokens, safeAddress } = props + const [activeTokensAddresses, setActiveTokensAddresses] = useState(Set(activeTokens.map(({ address }) => address))) + const [blacklistedTokensAddresses, setBlacklistedTokensAddresses] = useState>(blacklistedTokens) + const [filter, setFilter] = useState('') + const dispatch = useDispatch() + + const searchClasses = { + input: classes.searchInput, + root: classes.searchRoot, + iconButton: classes.searchIcon, + searchContainer: classes.searchContainer, } - onCancelSearch = () => { + const onCancelSearch = () => { + setFilter('') this.setState(() => ({ filter: '' })) } - onChangeSearchBar = (value) => { - this.setState(() => ({ filter: value })) + const onChangeSearchBar = (value: string) => { + setFilter(value) } - onSwitch = (token) => () => { - this.setState((prevState: any) => { - const activeTokensAddresses = prevState.activeTokensAddresses.has(token.address) - ? prevState.activeTokensAddresses.remove(token.address) - : prevState.activeTokensAddresses.add(token.address) - - let { blacklistedTokensAddresses } = prevState - if (activeTokensAddresses.has(token.address)) { - blacklistedTokensAddresses = prevState.blacklistedTokensAddresses.remove(token.address) - } else if (prevState.initialActiveTokensAddresses.has(token.address)) { - blacklistedTokensAddresses = prevState.blacklistedTokensAddresses.add(token.address) - } - - return { ...prevState, activeTokensAddresses, blacklistedTokensAddresses } - }) + const onSwitch = (token: Token) => () => { + let newActiveTokensAddresses + let newBlacklistedTokensAddresses + if (activeTokensAddresses.has(token.address)) { + newActiveTokensAddresses = activeTokensAddresses.delete(token.address) + newBlacklistedTokensAddresses = blacklistedTokensAddresses.add(token.address) + } else { + newActiveTokensAddresses = activeTokensAddresses.add(token.address) + newBlacklistedTokensAddresses = blacklistedTokensAddresses.delete(token.address) + } + + // Set local state + setActiveTokensAddresses(newActiveTokensAddresses) + setBlacklistedTokensAddresses(newBlacklistedTokensAddresses) + // Dispatch to global state + dispatch(updateActiveTokens(safeAddress, newActiveTokensAddresses)) + dispatch(updateBlacklistedTokens(safeAddress, newBlacklistedTokensAddresses)) } - createItemData = (tokens, activeTokensAddresses) => ({ - tokens, - activeTokensAddresses, - onSwitch: this.onSwitch, - }) + const createItemData = ( + tokens: List, + activeTokensAddresses: Set, + ): { tokens: List; activeTokensAddresses: Set; onSwitch: (token: Token) => void } => { + return { + tokens, + activeTokensAddresses, + onSwitch: onSwitch, + } + } - getItemKey = (index, { tokens }) => { - const token = tokens.get(index) + const switchToAddCustomTokenScreen = () => setActiveScreen('addCustomToken') - return token.address + const getItemKey = (index: number, { tokens }): string => { + return tokens.get(index).address } - render() { - const { classes, setActiveScreen, tokens } = this.props - const { activeTokensAddresses, filter } = this.state - const searchClasses = { - input: classes.searchInput, - root: classes.searchRoot, - iconButton: classes.searchIcon, - searchContainer: classes.searchContainer, - } - const switchToAddCustomTokenScreen = () => setActiveScreen('addCustomToken') - - const filteredTokens = filterBy(filter, tokens) - const itemData = this.createItemData(filteredTokens, activeTokensAddresses) - - return ( - <> - - - - } - value={filter} - /> - - - - - - + const filteredTokens = filterBy(filter, tokens) + const itemData = createItemData(filteredTokens, activeTokensAddresses) + + return ( + <> + + + + } + value={filter} + /> + + + + + + + + {!tokens.size && ( + + - {!tokens.size && ( - - - - )} - {tokens.size > 0 && ( - - - {TokenRow} - - - )} - - ) - } + )} + {tokens.size > 0 && ( + + + {TokenRow} + + + )} + + ) } - -const TokenComponent = withStyles(styles as any)(Tokens) - -export default TokenComponent diff --git a/src/routes/safe/components/Balances/Tokens/screens/TokenList/style.ts b/src/routes/safe/components/Balances/Tokens/screens/TokenList/style.ts index 68f7322fb9..ac7f8c3414 100644 --- a/src/routes/safe/components/Balances/Tokens/screens/TokenList/style.ts +++ b/src/routes/safe/components/Balances/Tokens/screens/TokenList/style.ts @@ -1,6 +1,7 @@ import { border, md, mediumFontSize, secondaryText, sm, xs } from 'src/theme/variables' +import { createStyles } from '@material-ui/core' -export const styles = () => ({ +export const styles = createStyles({ root: { minHeight: '52px', }, diff --git a/src/routes/safe/components/Balances/Tokens/style.ts b/src/routes/safe/components/Balances/Tokens/style.ts index a660c44aa6..b37e7b7f05 100644 --- a/src/routes/safe/components/Balances/Tokens/style.ts +++ b/src/routes/safe/components/Balances/Tokens/style.ts @@ -1,6 +1,7 @@ import { lg, md } from 'src/theme/variables' +import { createStyles } from '@material-ui/core' -export const styles = () => ({ +export const styles = createStyles({ heading: { padding: `${md} ${lg}`, justifyContent: 'space-between', diff --git a/src/routes/safe/components/Balances/dataFetcher.ts b/src/routes/safe/components/Balances/dataFetcher.ts index 16737cc747..e19c215a66 100644 --- a/src/routes/safe/components/Balances/dataFetcher.ts +++ b/src/routes/safe/components/Balances/dataFetcher.ts @@ -42,7 +42,9 @@ const getTokenPriceInCurrency = ( export interface BalanceData { asset: { name: string; logoUri: string; address: string; symbol: string } + assetOrder: string balance: string + balanceOrder: number fixed: boolean value: string } @@ -61,7 +63,7 @@ export const getBalanceData = ( symbol: token.symbol, }, assetOrder: token.name, - [BALANCE_TABLE_BALANCE_ID]: `${formatAmountInUsFormat(token.balance.toString())} ${token.symbol}`, + [BALANCE_TABLE_BALANCE_ID]: `${formatAmountInUsFormat(token.balance?.toString() || '0')} ${token.symbol}`, balanceOrder: Number(token.balance), [FIXED]: token.symbol === 'ETH', [BALANCE_TABLE_VALUE_ID]: getTokenPriceInCurrency(token, currencySelected, currencyValues, currencyRate), diff --git a/src/routes/safe/components/Balances/index.tsx b/src/routes/safe/components/Balances/index.tsx index 21c7fb7ee4..1f5489c6af 100644 --- a/src/routes/safe/components/Balances/index.tsx +++ b/src/routes/safe/components/Balances/index.tsx @@ -2,7 +2,7 @@ import { makeStyles } from '@material-ui/core/styles' import React, { useEffect, useState } from 'react' import { useSelector } from 'react-redux' -import Receive from 'src/components/App/ModalReceive' +import Receive from 'src/components/App/ReceiveModal' import Tokens from './Tokens' import { styles } from './style' @@ -15,7 +15,11 @@ import Row from 'src/components/layout/Row' import { SAFELIST_ADDRESS } from 'src/routes/routes' import SendModal from 'src/routes/safe/components/Balances/SendModal' import CurrencyDropdown from 'src/routes/safe/components/CurrencyDropdown' -import { safeFeaturesEnabledSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' +import { + safeFeaturesEnabledSelector, + safeParamAddressFromStateSelector, + safeNameSelector, +} from 'src/logic/safe/store/selectors' import { wrapInSuspense } from 'src/utils/wrapInSuspense' import { useFetchTokens } from 'src/logic/safe/hooks/useFetchTokens' @@ -33,7 +37,7 @@ const INITIAL_STATE = { showManageCollectibleModal: false, sendFunds: { isOpen: false, - selectedToken: undefined, + selectedToken: '', }, showReceive: false, } @@ -49,11 +53,12 @@ const Balances = (): React.ReactElement => { const address = useSelector(safeParamAddressFromStateSelector) const featuresEnabled = useSelector(safeFeaturesEnabledSelector) + const safeName = useSelector(safeNameSelector) - useFetchTokens(address) + useFetchTokens(address as string) useEffect(() => { - const erc721Enabled = featuresEnabled && featuresEnabled.includes('ERC721') + const erc721Enabled = Boolean(featuresEnabled?.includes('ERC721')) setState((prevState) => ({ ...prevState, @@ -84,7 +89,7 @@ const Balances = (): React.ReactElement => { ...prevState, sendFunds: { isOpen: false, - selectedToken: undefined, + selectedToken: '', }, })) } @@ -224,7 +229,7 @@ const Balances = (): React.ReactElement => { paperClassName={receiveModal} title="Receive Tokens" > - onHide('Receive')} /> + onHide('Receive')} /> ) diff --git a/src/routes/safe/components/CurrencyDropdown/index.tsx b/src/routes/safe/components/CurrencyDropdown/index.tsx index 63cf986b12..862c02516e 100644 --- a/src/routes/safe/components/CurrencyDropdown/index.tsx +++ b/src/routes/safe/components/CurrencyDropdown/index.tsx @@ -22,9 +22,9 @@ import { setImageToPlaceholder } from '../Balances/utils' import Img from 'src/components/layout/Img/index' import etherIcon from 'src/assets/icons/icon_etherTokens.svg' -const CurrencyDropdown = (): React.ReactElement => { +const CurrencyDropdown = (): React.ReactElement | null => { const currenciesList = Object.values(AVAILABLE_CURRENCIES) - const safeAddress = useSelector(safeParamAddressFromStateSelector) + const safeAddress = useSelector(safeParamAddressFromStateSelector) as string const dispatch = useDispatch() const [anchorEl, setAnchorEl] = useState(null) const selectedCurrency = useSelector(currentCurrencySelector) @@ -48,7 +48,11 @@ const CurrencyDropdown = (): React.ReactElement => { handleClose() } - return !selectedCurrency ? null : ( + if (!selectedCurrency) { + return null + } + + return ( <> - - - - )} - - - - - - ) - } + )} + + )} + + + + )) + } + + + + {granted && ( + <> + + + + + + + + )} + + + + + + ) } -export default withStyles(styles as any)(ManageOwners) +export default ManageOwners diff --git a/src/routes/safe/components/Settings/ManageOwners/style.ts b/src/routes/safe/components/Settings/ManageOwners/style.ts index 21cb5f9ebb..957a20d016 100644 --- a/src/routes/safe/components/Settings/ManageOwners/style.ts +++ b/src/routes/safe/components/Settings/ManageOwners/style.ts @@ -1,6 +1,7 @@ import { lg, sm } from 'src/theme/variables' +import { createStyles } from '@material-ui/core' -export const styles = () => ({ +export const styles = createStyles({ formContainer: { minHeight: '420px', }, diff --git a/src/routes/safe/components/Settings/RemoveSafeModal/actions.ts b/src/routes/safe/components/Settings/RemoveSafeModal/actions.ts deleted file mode 100644 index ee6a86680e..0000000000 --- a/src/routes/safe/components/Settings/RemoveSafeModal/actions.ts +++ /dev/null @@ -1,5 +0,0 @@ -import removeSafe from 'src/logic/safe/store/actions/removeSafe' - -export default { - removeSafe, -} diff --git a/src/routes/safe/components/Settings/RemoveSafeModal/index.tsx b/src/routes/safe/components/Settings/RemoveSafeModal/index.tsx index dc48db3390..e301c1b04d 100644 --- a/src/routes/safe/components/Settings/RemoveSafeModal/index.tsx +++ b/src/routes/safe/components/Settings/RemoveSafeModal/index.tsx @@ -1,5 +1,5 @@ import IconButton from '@material-ui/core/IconButton' -import { withStyles } from '@material-ui/core/styles' +import { makeStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import OpenInNew from '@material-ui/icons/OpenInNew' import classNames from 'classnames' @@ -27,8 +27,11 @@ const openIconStyle = { color: secondary, } -const RemoveSafeComponent = ({ classes, isOpen, onClose }) => { - const safeAddress = useSelector(safeParamAddressFromStateSelector) +const useStyles = makeStyles(styles) + +const RemoveSafeComponent = ({ isOpen, onClose }) => { + const classes = useStyles() + const safeAddress = useSelector(safeParamAddressFromStateSelector) as string const safeName = useSelector(safeNameSelector) const dispatch = useDispatch() const etherScanLink = getEtherScanLink('address', safeAddress) @@ -104,4 +107,4 @@ const RemoveSafeComponent = ({ classes, isOpen, onClose }) => { ) } -export const RemoveSafeModal = withStyles(styles as any)(RemoveSafeComponent) +export const RemoveSafeModal = RemoveSafeComponent diff --git a/src/routes/safe/components/Settings/RemoveSafeModal/style.ts b/src/routes/safe/components/Settings/RemoveSafeModal/style.ts index 445bf832a1..2d41fb79ff 100644 --- a/src/routes/safe/components/Settings/RemoveSafeModal/style.ts +++ b/src/routes/safe/components/Settings/RemoveSafeModal/style.ts @@ -1,6 +1,7 @@ +import { createStyles } from '@material-ui/core/styles' import { background, error, lg, md, sm } from 'src/theme/variables' -export const styles = () => ({ +export const styles = createStyles({ heading: { boxSizing: 'border-box', justifyContent: 'space-between', diff --git a/src/routes/safe/components/Settings/SafeDetails/index.tsx b/src/routes/safe/components/Settings/SafeDetails/index.tsx index 14a7b11dca..c04ed03a6b 100644 --- a/src/routes/safe/components/Settings/SafeDetails/index.tsx +++ b/src/routes/safe/components/Settings/SafeDetails/index.tsx @@ -1,5 +1,5 @@ import { makeStyles } from '@material-ui/core/styles' -import React from 'react' +import React, { useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux' import { styles } from './style' @@ -29,6 +29,7 @@ import { safeNeedsUpdateSelector, safeParamAddressFromStateSelector, } from 'src/logic/safe/store/selectors' +import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' export const SAFE_NAME_INPUT_TEST_ID = 'safe-name-input' export const SAFE_NAME_SUBMIT_BTN_TEST_ID = 'change-safe-name-btn' @@ -44,6 +45,7 @@ const SafeDetails = (): React.ReactElement => { const safeName = useSelector(safeNameSelector) const safeNeedsUpdate = useSelector(safeNeedsUpdateSelector) const safeCurrentVersion = useSelector(safeCurrentVersionSelector) + const { trackEvent } = useAnalytics() const [isModalOpen, setModalOpen] = React.useState(false) const safeAddress = useSelector(safeParamAddressFromStateSelector) @@ -63,6 +65,10 @@ const SafeDetails = (): React.ReactElement => { setModalOpen(true) } + useEffect(() => { + trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Settings', label: 'Details' }) + }, [trackEvent]) + return ( <> diff --git a/src/routes/safe/components/Settings/ThresholdSettings/index.tsx b/src/routes/safe/components/Settings/ThresholdSettings/index.tsx index 36ae420a9d..ccc3f4e57d 100644 --- a/src/routes/safe/components/Settings/ThresholdSettings/index.tsx +++ b/src/routes/safe/components/Settings/ThresholdSettings/index.tsx @@ -1,6 +1,5 @@ -import { withStyles } from '@material-ui/core/styles' -import { withSnackbar } from 'notistack' -import React, { useState } from 'react' +import { makeStyles } from '@material-ui/core/styles' +import React, { useState, useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux' import ChangeThreshold from './ChangeThreshold' @@ -22,12 +21,16 @@ import { safeParamAddressFromStateSelector, safeThresholdSelector, } from 'src/logic/safe/store/selectors' +import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' -const ThresholdSettings = ({ classes, closeSnackbar, enqueueSnackbar }) => { +const useStyles = makeStyles(styles) + +const ThresholdSettings = (): React.ReactElement => { + const classes = useStyles() const [isModalOpen, setModalOpen] = useState(false) const dispatch = useDispatch() const threshold = useSelector(safeThresholdSelector) - const safeAddress = useSelector(safeParamAddressFromStateSelector) + const safeAddress = useSelector(safeParamAddressFromStateSelector) as string const owners = useSelector(safeOwnersSelector) const granted = useSelector(grantedSelector) @@ -46,21 +49,25 @@ const ThresholdSettings = ({ classes, closeSnackbar, enqueueSnackbar }) => { valueInWei: '0', txData, notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX, - enqueueSnackbar, - closeSnackbar, - } as any), + }), ) } + const { trackEvent } = useAnalytics() + + useEffect(() => { + trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Settings', label: 'Owners' }) + }, [trackEvent]) + return ( <> Required confirmations Any transaction requires the confirmation of: - {threshold} out of {owners.size} owners + {threshold} out of {owners?.size || 0} owners - {owners.size > 1 && granted && ( + {owners && owners.size > 1 && granted && (
{ @@ -65,8 +66,8 @@ function getPendingOwnersConfirmations( const ownersWithNoConfirmationsSorted = ownersWithNoConfirmations .map((owner) => ({ - hasPendingAcceptActions: confirmationPendingActions.includes(owner), - hasPendingRejectActions: confirmationRejectActions.includes(owner), + hasPendingAcceptActions: !!confirmationPendingActions?.includes(owner), + hasPendingRejectActions: !!confirmationRejectActions?.includes(owner), owner, })) // Reorders the list of unconfirmed owners, owners with pendingActions should be first @@ -119,7 +120,7 @@ const OwnersColumn = ({ } else { showOlderTxAnnotation = (thresholdReached && !canExecute) || (cancelThresholdReached && !canExecuteCancel) } - const owners = useSelector(safeOwnersSelector) + const owners = useSelector(safeOwnersSelector) as List const threshold = useSelector(safeThresholdSelector) const userAddress = useSelector(userAccountSelector) const [ownersWhoConfirmed, currentUserAlreadyConfirmed] = getOwnersConfirmations(tx, userAddress) @@ -142,6 +143,7 @@ const OwnersColumn = ({ displayButtonRow = false } + // TODO: simplify this whole logic around tx status, it's getting hard to maintain and follow const showConfirmBtn = !tx.isExecuted && tx.status !== 'pending' && @@ -151,7 +153,8 @@ const OwnersColumn = ({ !currentUserAlreadyConfirmed && !thresholdReached - const showExecuteBtn = canExecute && !tx.isExecuted && thresholdReached + const showExecuteBtn = + canExecute && !tx.isExecuted && thresholdReached && tx.status !== 'pending' && cancelTx.status !== 'pending' const showRejectBtn = !cancelTx.isExecuted && @@ -163,7 +166,13 @@ const OwnersColumn = ({ !cancelThresholdReached && displayButtonRow - const showExecuteRejectBtn = !cancelTx.isExecuted && !tx.isExecuted && canExecuteCancel && cancelThresholdReached + const showExecuteRejectBtn = + !cancelTx.isExecuted && + !tx.isExecuted && + canExecuteCancel && + cancelThresholdReached && + tx.status !== 'pending' && + cancelTx.status !== 'pending' const txThreshold = cancelTx.isExecuted ? tx.confirmations.size : threshold const cancelThreshold = tx.isExecuted ? cancelTx.confirmations.size : threshold diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/RejectTxModal/index.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/RejectTxModal/index.tsx index 3aa449fa8a..e5372d4c15 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/RejectTxModal/index.tsx +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/RejectTxModal/index.tsx @@ -34,7 +34,7 @@ type Props = { const RejectTxModal = ({ isOpen, onClose, tx }: Props): React.ReactElement => { const [gasCosts, setGasCosts] = useState('< 0.001') const dispatch = useDispatch() - const safeAddress = useSelector(safeParamAddressFromStateSelector) + const safeAddress = useSelector(safeParamAddressFromStateSelector) as string const classes = useStyles() useEffect(() => { diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/CustomDescription.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/CustomDescription.tsx index a4a8199c16..0ed87bec5f 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/CustomDescription.tsx +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/CustomDescription.tsx @@ -1,4 +1,4 @@ -import { IconText, Text } from '@gnosis.pm/safe-react-components' +import { IconText, Text, EthHashInfo } from '@gnosis.pm/safe-react-components' import { makeStyles } from '@material-ui/core/styles' import React from 'react' import styled from 'styled-components' @@ -12,18 +12,18 @@ import { MultiSendDetails, } from 'src/routes/safe/store/actions/transactions/utils/multiSendDecodedDetails' import Bold from 'src/components/layout/Bold' -import OwnerAddressTableCell from 'src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell' -import EtherscanLink from 'src/components/EtherscanLink' import { humanReadableValue } from 'src/logic/tokens/utils/humanReadableValue' import Collapse from 'src/components/Collapse' import { useSelector } from 'react-redux' -import { getNameFromAddressBook } from 'src/logic/addressBook/store/selectors' +import { getNameFromAddressBookSelector } from 'src/logic/addressBook/store/selectors' import Paragraph from 'src/components/layout/Paragraph' import LinkWithRef from 'src/components/layout/Link' import { shortVersionOf } from 'src/logic/wallets/ethAddresses' import { Transaction } from 'src/logic/safe/store/models/types/transaction' import { DataDecoded } from 'src/routes/safe/store/models/types/transactions.d' import DividerLine from 'src/components/DividerLine' +import { isArrayParameter } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils' +import { getNetwork } from 'src/config' export const TRANSACTIONS_DESC_CUSTOM_VALUE_TEST_ID = 'tx-description-custom-value' export const TRANSACTIONS_DESC_CUSTOM_DATA_TEST_ID = 'tx-description-custom-data' @@ -34,9 +34,14 @@ const useStyles = makeStyles(styles) const TxDetailsMethodName = styled(Text)` text-indent: 4px; ` -const TxDetailsMethodParam = styled.div` - text-indent: 8px; - display: flex; +const TxDetailsMethodParam = styled.div<{ isArrayParameter: boolean }>` + padding-left: 8px; + display: ${({ isArrayParameter }) => (isArrayParameter ? 'block' : 'flex')}; + align-items: center; + + p:first-of-type { + margin-right: ${({ isArrayParameter }) => (isArrayParameter ? '0' : '4px')}; + } ` const TxDetailsContent = styled.div` padding: 8px 8px 8px 16px; @@ -46,6 +51,10 @@ const TxInfo = styled.div` padding: 8px 8px 8px 16px; ` +const StyledMethodName = styled(Text)` + white-space: nowrap; +` + const TxInfoDetails = ({ data }: { data: DataDecoded }): React.ReactElement => ( @@ -53,10 +62,10 @@ const TxInfoDetails = ({ data }: { data: DataDecoded }): React.ReactElement => ( {data.parameters.map((param, index) => ( - - + + {param.name}({param.type}): - + ))} @@ -76,7 +85,7 @@ const MultiSendCustomDataAction = ({ tx, order }: { tx: MultiSendDetails; order: Send {humanReadableValue(tx.value)} ETH to: - + {!!tx.data && } @@ -167,17 +176,21 @@ interface GenericCustomDataProps { const GenericCustomData = ({ amount = '0', data, recipient, storedTx }: GenericCustomDataProps): React.ReactElement => { const classes = useStyles() - const recipientName = useSelector((state) => getNameFromAddressBook(state, recipient)) + const recipientName = useSelector((state) => getNameFromAddressBookSelector(state, recipient)) return ( Send {amount} to: - {recipientName ? ( - - ) : ( - - )} + + {!!storedTx?.dataDecoded && } diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/SettingsDescription.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/SettingsDescription.tsx index 974382b91e..596f3224e1 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/SettingsDescription.tsx +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/SettingsDescription.tsx @@ -1,7 +1,7 @@ import { useSelector } from 'react-redux' import React from 'react' -import { getNameFromAddressBook } from 'src/logic/addressBook/store/selectors' +import { getNameFromAddressBookSelector } from 'src/logic/addressBook/store/selectors' import Block from 'src/components/layout/Block' import Bold from 'src/components/layout/Bold' import OwnerAddressTableCell from 'src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell' @@ -21,7 +21,7 @@ interface RemovedOwnerProps { } const RemovedOwner = ({ removedOwner }: RemovedOwnerProps): React.ReactElement => { - const ownerChangedName = useSelector((state) => getNameFromAddressBook(state, removedOwner)) + const ownerChangedName = useSelector((state) => getNameFromAddressBookSelector(state, removedOwner)) return ( @@ -40,7 +40,7 @@ interface AddedOwnerProps { } const AddedOwner = ({ addedOwner }: AddedOwnerProps): React.ReactElement => { - const ownerChangedName = useSelector((state) => getNameFromAddressBook(state, addedOwner)) + const ownerChangedName = useSelector((state) => getNameFromAddressBookSelector(state, addedOwner)) return ( diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/TransferDescription.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/TransferDescription.tsx index 2e206a1f21..4fc0be9780 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/TransferDescription.tsx +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/TransferDescription.tsx @@ -2,7 +2,7 @@ import React from 'react' import { useSelector } from 'react-redux' import { TRANSACTIONS_DESC_SEND_TEST_ID } from './index' -import { getNameFromAddressBook } from 'src/logic/addressBook/store/selectors' +import { getNameFromAddressBookSelector } from 'src/logic/addressBook/store/selectors' import Block from 'src/components/layout/Block' import Bold from 'src/components/layout/Bold' import OwnerAddressTableCell from 'src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell' @@ -14,7 +14,7 @@ interface TransferDescriptionProps { } const TransferDescription = ({ amount = '', recipient }: TransferDescriptionProps): React.ReactElement => { - const recipientName = useSelector((state) => getNameFromAddressBook(state, recipient)) + const recipientName = useSelector((state) => getNameFromAddressBookSelector(state, recipient)) return ( Send {amount} to: diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/Value.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/Value.tsx index fed5bafe3f..7a0108e2c4 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/Value.tsx +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/Value.tsx @@ -1,21 +1,15 @@ -import { Text } from '@gnosis.pm/safe-react-components' -import { makeStyles } from '@material-ui/core/styles' +import { Text, EthHashInfo } from '@gnosis.pm/safe-react-components' import React from 'react' import styled from 'styled-components' -import { styles } from './styles' - +import { getNetwork } from 'src/config' import { isAddress, isArrayParameter, } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils' -import { useWindowDimensions } from 'src/logic/hooks/useWindowDimensions' -import SafeEtherscanLink from 'src/components/EtherscanLink' - -const useStyles = makeStyles(styles) const NestedWrapper = styled.div` - text-indent: 24px; + padding-left: 4px; ` const StyledText = styled(Text)` @@ -28,53 +22,38 @@ interface RenderValueProps { value: string | string[] } -const EtherscanLink = ({ method, type, value }: RenderValueProps): React.ReactElement => { - const classes = useStyles() - const [cut, setCut] = React.useState(undefined) - const { width } = useWindowDimensions() - - React.useEffect(() => { - if (width <= 900) { - setCut(4) - } else if (width <= 1024) { - setCut(8) - } else { - setCut(12) - } - }, [width]) +const GenericValue = ({ method, type, value }: RenderValueProps): React.ReactElement => { + const getTextValue = (value: string) => {value} - if (isArrayParameter(type)) { - return ( + const getArrayValue = (parentId: string, value: string[] | string) => ( +
+ [ - {(value as string[]).map((value, index) => ( - - ))} + {(value as string[]).map((currentValue, index) => { + const key = `${parentId}-value-${index}` + return ( +
+ {Array.isArray(currentValue) ? getArrayValue(key, currentValue) : getTextValue(currentValue)} +
+ ) + })}
- ) - } - - return -} + ] +
+ ) -const GenericValue = ({ method, type, value }: RenderValueProps): React.ReactElement => { - if (isArrayParameter(type)) { - return ( - - {(value as string[]).map((value, index) => ( - - {value} - - ))} - - ) + if (isArrayParameter(type) || Array.isArray(value)) { + return getArrayValue(method, value) } - return {value as string} + return getTextValue(value as string) } const Value = ({ type, ...props }: RenderValueProps): React.ReactElement => { if (isAddress(type)) { - return + return ( + + ) } return diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/utils.ts b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/utils.ts index 15dabaaa70..d204b86318 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/utils.ts +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/utils.ts @@ -12,7 +12,7 @@ const getSafeVersion = (data) => { } interface TxData { - data?: string + data?: string | null recipient?: string module?: string action?: string @@ -34,12 +34,12 @@ export const getTxData = (tx: Transaction): TxData => { if (tx.decodedParams) { if (tx.isTokenTransfer) { - const { to } = tx.decodedParams.transfer + const { to } = tx.decodedParams.transfer || {} txData.recipient = to txData.isTokenTransfer = true } else if (tx.isCollectibleTransfer) { const { safeTransferFrom, transfer, transferFrom } = tx.decodedParams - const { to, value } = safeTransferFrom || transferFrom || transfer + const { to, value } = safeTransferFrom || transferFrom || transfer || {} txData.recipient = to txData.tokenId = value txData.isCollectibleTransfer = true diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/index.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/index.tsx index 8298fb3c02..2bc2c2bb2b 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/index.tsx +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/index.tsx @@ -21,6 +21,7 @@ import Hairline from 'src/components/layout/Hairline' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import Span from 'src/components/layout/Span' +import { getWeb3 } from 'src/logic/wallets/getWeb3' import { INCOMING_TX_TYPES } from 'src/logic/safe/store/models/incomingTransaction' import { safeNonceSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors' import { Transaction, TransactionTypes } from 'src/logic/safe/store/models/types/transaction' @@ -34,12 +35,14 @@ interface ExpandedTxProps { } const ExpandedTx = ({ cancelTx, tx }: ExpandedTxProps): React.ReactElement => { + const { fromWei, toBN } = getWeb3().utils + const classes = useStyles() const nonce = useSelector(safeNonceSelector) - const threshold = useSelector(safeThresholdSelector) - const [openModal, setOpenModal] = useState(null) + const threshold = useSelector(safeThresholdSelector) as number + const [openModal, setOpenModal] = useState<'approveTx' | 'executeRejectTx' | 'rejectTx'>() const openApproveModal = () => setOpenModal('approveTx') - const closeModal = () => setOpenModal(null) + const closeModal = () => setOpenModal(undefined) const isIncomingTx = !!INCOMING_TX_TYPES[tx.type] const isCreationTx = tx.type === TransactionTypes.CREATION @@ -60,7 +63,7 @@ const ExpandedTx = ({ cancelTx, tx }: ExpandedTxProps): React.ReactElement => { <> - +
Hash: @@ -85,7 +88,7 @@ const ExpandedTx = ({ cancelTx, tx }: ExpandedTxProps): React.ReactElement => { {!isCreationTx ? ( Fee: - {tx.fee ? tx.fee : 'n/a'} + {tx.fee ? fromWei(toBN(tx.fee)) + ' ETH' : 'n/a'} ) : null} diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/style.ts b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/style.ts index f149263b7d..d76074bef5 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/style.ts +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/style.ts @@ -1,6 +1,10 @@ import { border, lg, md } from 'src/theme/variables' const cssStyles = { + col: { + wordBreak: 'break-word', + whiteSpace: 'normal', + }, expandedTxBlock: { borderBottom: `2px solid ${border}`, }, diff --git a/src/routes/safe/components/Transactions/TxsTable/TxType/index.tsx b/src/routes/safe/components/Transactions/TxsTable/TxType/index.tsx index fbe5a86079..28c54f67f7 100644 --- a/src/routes/safe/components/Transactions/TxsTable/TxType/index.tsx +++ b/src/routes/safe/components/Transactions/TxsTable/TxType/index.tsx @@ -35,7 +35,7 @@ const typeToLabel = { } interface TxTypeProps { - origin?: string + origin: string | null txType: keyof typeof typeToLabel } @@ -45,7 +45,11 @@ const TxType = ({ origin, txType }: TxTypeProps): React.ReactElement => { const [forceCustom, setForceCustom] = useState(false) useEffect(() => { - const getAppInfo = async () => { + const getAppInfo = async (origin: string | null) => { + if (!origin) { + return + } + const parsedOrigin = getAppInfoFromOrigin(origin) if (!parsedOrigin) { @@ -60,11 +64,7 @@ const TxType = ({ origin, txType }: TxTypeProps): React.ReactElement => { setLoading(false) } - if (!origin) { - return - } - - getAppInfo() + getAppInfo(origin) }, [origin, txType]) if (forceCustom || !origin) { diff --git a/src/routes/safe/components/Transactions/TxsTable/columns.tsx b/src/routes/safe/components/Transactions/TxsTable/columns.tsx index 4f385bf644..39eca8d5db 100644 --- a/src/routes/safe/components/Transactions/TxsTable/columns.tsx +++ b/src/routes/safe/components/Transactions/TxsTable/columns.tsx @@ -50,7 +50,10 @@ export const getIncomingTxAmount = (tx: Transaction, formatted = true): string = return `1 ${tx.symbol}` } - return getAmountWithSymbol(tx, formatted) + return getAmountWithSymbol( + { decimals: tx.decimals as string, symbol: tx.symbol as string, value: tx.value }, + formatted, + ) } export const getTxAmount = (tx: Transaction, formatted = true): string => { @@ -65,10 +68,10 @@ export const getTxAmount = (tx: Transaction, formatted = true): string => { return NOT_AVAILABLE } - return getAmountWithSymbol({ decimals, symbol, value }, formatted) + return getAmountWithSymbol({ decimals: decimals as string, symbol: symbol as string, value }, formatted) } -interface TableData { +export interface TableData { amount: string cancelTx?: Transaction date: string @@ -81,15 +84,15 @@ interface TableData { const getIncomingTxTableData = (tx: Transaction): TableData => ({ [TX_TABLE_ID]: tx.blockNumber?.toString() ?? '', - [TX_TABLE_TYPE_ID]: , - [TX_TABLE_DATE_ID]: formatDate(tx.executionDate), - [buildOrderFieldFrom(TX_TABLE_DATE_ID)]: getTime(parseISO(tx.executionDate)), + [TX_TABLE_TYPE_ID]: , + [TX_TABLE_DATE_ID]: formatDate(tx.executionDate || '0'), + [buildOrderFieldFrom(TX_TABLE_DATE_ID)]: getTime(parseISO(tx.executionDate || '0')), [TX_TABLE_AMOUNT_ID]: getIncomingTxAmount(tx), [TX_TABLE_STATUS_ID]: tx.status, [TX_TABLE_RAW_TX_ID]: tx, }) -const getTransactionTableData = (tx: Transaction, cancelTx: Transaction): TableData => { +const getTransactionTableData = (tx: Transaction, cancelTx?: Transaction): TableData => { const txDate = tx.submissionDate return { diff --git a/src/routes/safe/components/Transactions/TxsTable/index.tsx b/src/routes/safe/components/Transactions/TxsTable/index.tsx index 57033ba43e..522c277a08 100644 --- a/src/routes/safe/components/Transactions/TxsTable/index.tsx +++ b/src/routes/safe/components/Transactions/TxsTable/index.tsx @@ -7,7 +7,7 @@ import { withStyles } from '@material-ui/core/styles' import ExpandLess from '@material-ui/icons/ExpandLess' import ExpandMore from '@material-ui/icons/ExpandMore' import cn from 'classnames' -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import { useSelector } from 'react-redux' import ExpandedTxComponent from './ExpandedTx' @@ -21,6 +21,7 @@ import Block from 'src/components/layout/Block' import Row from 'src/components/layout/Row' import { safeCancellationTransactionsSelector } from 'src/logic/safe/store/selectors' import { extendedTransactionsSelector } from 'src/logic/safe/store/selectors/transactions' +import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' export const TRANSACTION_ROW_TEST_ID = 'transaction-row' @@ -28,6 +29,11 @@ const TxsTable = ({ classes }) => { const [expandedTx, setExpandedTx] = useState(null) const cancellationTransactions = useSelector(safeCancellationTransactionsSelector) const transactions = useSelector(extendedTransactionsSelector) + const { trackEvent } = useAnalytics() + + useEffect(() => { + trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Transactions' }) + }, [trackEvent]) const handleTxExpand = (safeTxHash) => { setExpandedTx((prevTx) => (prevTx === safeTxHash ? null : safeTxHash)) @@ -38,8 +44,8 @@ const TxsTable = ({ classes }) => { const filteredData = getTxTableData(transactions, cancellationTransactions) .sort((tx1, tx2) => { // First order by nonce - const aNonce = tx1.tx.nonce - const bNonce = tx1.tx.nonce + const aNonce = tx1.tx?.nonce + const bNonce = tx1.tx?.nonce if (aNonce && bNonce) { const difference = aNonce - bNonce if (difference !== 0) { diff --git a/src/routes/safe/components/Transactions/TxsTable/test/column.test.ts b/src/routes/safe/components/Transactions/TxsTable/test/column.test.ts index 89b0a4998b..75f9fffb38 100644 --- a/src/routes/safe/components/Transactions/TxsTable/test/column.test.ts +++ b/src/routes/safe/components/Transactions/TxsTable/test/column.test.ts @@ -1,6 +1,6 @@ import { List, Map } from 'immutable' import { makeTransaction } from 'src/logic/safe/store/models/transaction' -import { getTxTableData, TX_TABLE_RAW_CANCEL_TX_ID } from 'src/routes/safe/components/Transactions/TxsTable/columns' +import { getTxTableData, TX_TABLE_RAW_CANCEL_TX_ID, TableData } from 'src/routes/safe/components/Transactions/TxsTable/columns' describe('TxsTable Columns > getTxTableData', () => { it('should include CancelTx object inside TxTableData', () => { @@ -10,7 +10,7 @@ describe('TxsTable Columns > getTxTableData', () => { // When const txTableData = getTxTableData(List([mockedTransaction]), Map( { '1': mockedCancelTransaction })) - const txRow = txTableData.first() + const txRow = txTableData.first() as TableData // Then expect(txRow[TX_TABLE_RAW_CANCEL_TX_ID]).toEqual(mockedCancelTransaction) @@ -22,7 +22,7 @@ describe('TxsTable Columns > getTxTableData', () => { // When const txTableData = getTxTableData(List([mockedTransaction]), Map( { '2': mockedCancelTransaction })) - const txRow = txTableData.first() + const txRow = txTableData.first() as TableData // Then expect(txRow[TX_TABLE_RAW_CANCEL_TX_ID]).toBeUndefined() diff --git a/src/routes/safe/container/hooks/useTransactions.tsx b/src/routes/safe/container/hooks/useTransactions.ts similarity index 97% rename from src/routes/safe/container/hooks/useTransactions.tsx rename to src/routes/safe/container/hooks/useTransactions.ts index a08f649d64..e9699ab6ac 100644 --- a/src/routes/safe/container/hooks/useTransactions.tsx +++ b/src/routes/safe/container/hooks/useTransactions.ts @@ -7,7 +7,7 @@ import { safeAllTransactionsSelector, safeTotalTransactionsAmountSelector, } from 'src/logic/safe/store/selectors/allTransactions' -import { Transaction } from 'src/logic/safe/store/models/types/transactions' +import { Transaction } from 'src/logic/safe/store/models/types/transactions.d' import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' type Props = { @@ -19,7 +19,7 @@ export const useTransactions = (props: Props): { transactions: Transaction[]; to const { offset, limit } = props const dispatch = useDispatch() const transactions = useSelector(safeAllTransactionsSelector) - const safeAddress = useSelector(safeParamAddressFromStateSelector) + const safeAddress = useSelector(safeParamAddressFromStateSelector) as string const totalTransactionsCount = useSelector(safeTotalTransactionsAmountSelector) useEffect(() => { async function loadNewTxs() { diff --git a/src/routes/safe/container/index.tsx b/src/routes/safe/container/index.tsx index c504a71fb8..2ca98cbf4b 100644 --- a/src/routes/safe/container/index.tsx +++ b/src/routes/safe/container/index.tsx @@ -29,7 +29,7 @@ const Container = (): React.ReactElement => { title: null, body: null, footer: null, - onClose: null, + onClose: () => {}, }) const safeAddress = useSelector(safeParamAddressFromStateSelector) @@ -42,7 +42,7 @@ const Container = (): React.ReactElement => { const closeGenericModal = () => { if (modal.onClose) { - modal.onClose() + modal.onClose?.() } setModal({ @@ -50,7 +50,7 @@ const Container = (): React.ReactElement => { title: null, body: null, footer: null, - onClose: null, + onClose: () => {}, }) } @@ -59,22 +59,26 @@ const Container = (): React.ReactElement => { wrapInSuspense(, null)} /> wrapInSuspense(, null)} /> - wrapInSuspense(, null)} /> - wrapInSuspense(, null)} /> + wrapInSuspense(, null)} /> wrapInSuspense(, null)} + /> + wrapInSuspense(, null)} /> - + {modal.isOpen && } diff --git a/src/routes/safe/container/selector.ts b/src/routes/safe/container/selector.ts index 821790cd0d..f27581d9a6 100644 --- a/src/routes/safe/container/selector.ts +++ b/src/routes/safe/container/selector.ts @@ -33,7 +33,7 @@ export const extendedSafeTokensSelector = createSelector( const extendedTokens = Map().withMutations((map) => { safeTokens.forEach((tokenAddress) => { const baseToken = tokensList.get(tokenAddress) - const tokenBalance = balances.get(tokenAddress) + const tokenBalance = balances?.get(tokenAddress) if (baseToken) { map.set(tokenAddress, baseToken.set('balance', tokenBalance || '0')) diff --git a/src/routes/safe/store/actions/transactions/__tests__/utils.test.ts b/src/routes/safe/store/actions/transactions/__tests__/utils.test.ts new file mode 100644 index 0000000000..db0b015c06 --- /dev/null +++ b/src/routes/safe/store/actions/transactions/__tests__/utils.test.ts @@ -0,0 +1,170 @@ +import { getLastTx, getNewTxNonce, shouldExecuteTransaction } from 'src/logic/safe/store/actions/utils' +import { getMockedSafeInstance, getMockedTxServiceModel } from 'src/test/utils/safeHelper' +import axios from 'axios' +import { buildTxServiceUrl } from 'src/logic/safe/transactions' + +describe('shouldExecuteTransaction', () => { + it('It should return false if given a safe with a threshold > 1', async () => { + // given + const nonce = '0' + const threshold = '2' + const safeInstance = getMockedSafeInstance({ threshold }) + const lastTx = getMockedTxServiceModel({}) + + // when + const result = await shouldExecuteTransaction(safeInstance, nonce, lastTx) + + // then + expect(result).toBe(false) + }) + it('It should return true if given a safe with a threshold === 1 and the previous transaction is already executed', async () => { + // given + const nonce = '0' + const threshold = '1' + const safeInstance = getMockedSafeInstance({ threshold }) + const lastTx = getMockedTxServiceModel({}) + + // when + const result = await shouldExecuteTransaction(safeInstance, nonce, lastTx) + + // then + expect(result).toBe(true) + }) + it('It should return true if given a safe with a threshold === 1 and the previous transaction is already executed', async () => { + // given + const nonce = '10' + const threshold = '1' + const safeInstance = getMockedSafeInstance({ threshold }) + const lastTx = getMockedTxServiceModel({ isExecuted: true }) + + // when + const result = await shouldExecuteTransaction(safeInstance, nonce, lastTx) + + // then + expect(result).toBe(true) + }) + it('It should return false if given a safe with a threshold === 1 and the previous transaction is not yet executed', async () => { + // given + const nonce = '10' + const threshold = '1' + const safeInstance = getMockedSafeInstance({ threshold }) + const lastTx = getMockedTxServiceModel({ isExecuted: false }) + + // when + const result = await shouldExecuteTransaction(safeInstance, nonce, lastTx) + + // then + expect(result).toBe(false) + }) +}) + +describe('getNewTxNonce', () => { + it('It should return 2 if given the last transaction with nonce 1', async () => { + // given + const safeInstance = getMockedSafeInstance({}) + const lastTx = getMockedTxServiceModel({ nonce: 1 }) + const expectedResult = '2' + + // when + const result = await getNewTxNonce(undefined, lastTx, safeInstance) + + // then + expect(result).toBe(expectedResult) + }) + it('It should return 0 if given a safe with nonce 0 and no transactions should use safe contract instance for retrieving nonce', async () => { + // given + const safeNonce = '0' + const safeInstance = getMockedSafeInstance({ nonce: safeNonce }) + const expectedResult = '0' + const mockFnCall = jest.fn().mockImplementation(() => safeNonce) + const mockFnNonce = jest.fn().mockImplementation(() => ({ call: mockFnCall })) + + safeInstance.methods.nonce = mockFnNonce + + // when + const result = await getNewTxNonce(undefined, null, safeInstance) + + // then + expect(result).toBe(expectedResult) + expect(mockFnNonce).toHaveBeenCalled() + expect(mockFnCall).toHaveBeenCalled() + mockFnNonce.mockRestore() + mockFnCall.mockRestore() + }) + it('Given a Safe and the last transaction, should return nonce of the last transaction + 1', async () => { + // given + const safeInstance = getMockedSafeInstance({}) + const expectedResult = '11' + const lastTx = getMockedTxServiceModel({ nonce: 10 }) + + // when + const result = await getNewTxNonce(undefined, lastTx, safeInstance) + + // then + expect(result).toBe(expectedResult) + }) + it('Given a pre-calculated nonce number should return it', async () => { + // given + const safeInstance = getMockedSafeInstance({}) + const expectedResult = '114' + const nextNonce = '114' + + // when + const result = await getNewTxNonce(nextNonce, null, safeInstance) + + // then + expect(result).toBe(expectedResult) + }) +}) + +jest.mock('axios') +jest.mock('console') +describe('getLastTx', () => { + afterAll(() => { + jest.unmock('axios') + jest.unmock('console') + }) + const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf' + it('It should return the last transaction for a given a safe address', async () => { + // given + const lastTx = getMockedTxServiceModel({ nonce: 1 }) + const url = buildTxServiceUrl(safeAddress) + + // when + // @ts-ignore + axios.get.mockImplementationOnce(() => { + return { + data: { + results: [lastTx], + }, + } + }) + + const result = await getLastTx(safeAddress) + + // then + expect(result).toStrictEqual(lastTx) + expect(axios.get).toHaveBeenCalled() + expect(axios.get).toBeCalledWith(url, { params: { limit: 1 } }) + }) + it('If should return null If catches an error getting last transaction', async () => { + // given + const lastTx = null + const url = buildTxServiceUrl(safeAddress) + + // when + // @ts-ignore + axios.get.mockImplementationOnce(() => { + throw new Error() + }) + console.error = jest.fn() + const result = await getLastTx(safeAddress) + const spyConsole = jest.spyOn(console, 'error').mockImplementation() + + // then + expect(result).toStrictEqual(lastTx) + expect(axios.get).toHaveBeenCalled() + expect(axios.get).toBeCalledWith(url, { params: { limit: 1 } }) + expect(spyConsole).toHaveBeenCalled() + }) +}) diff --git a/src/routes/safe/store/actions/transactions/utils/multiSendDecodedDetails.ts b/src/routes/safe/store/actions/transactions/utils/multiSendDecodedDetails.ts index 461051c49b..b03c475126 100644 --- a/src/routes/safe/store/actions/transactions/utils/multiSendDecodedDetails.ts +++ b/src/routes/safe/store/actions/transactions/utils/multiSendDecodedDetails.ts @@ -56,7 +56,7 @@ export const extractMultiSendDetails = (parameter: Parameter): MultiSendDetails[ export const extractMultiSendDataDecoded = (tx: Transaction): MultiSendDataDecoded => { const transfersDetails = tx.transfers?.map(extractTransferDetails) - const txDetails = extractMultiSendDetails(tx.dataDecoded?.parameters[0]) + const txDetails = tx.dataDecoded?.parameters[0] ? extractMultiSendDetails(tx.dataDecoded?.parameters[0]) : undefined return { txDetails, transfersDetails } } diff --git a/src/routes/safe/store/actions/transactions/utils/transferDetails.ts b/src/routes/safe/store/actions/transactions/utils/transferDetails.ts index 6b00126f12..12363dff05 100644 --- a/src/routes/safe/store/actions/transactions/utils/transferDetails.ts +++ b/src/routes/safe/store/actions/transactions/utils/transferDetails.ts @@ -20,7 +20,7 @@ const isIncomingTransfer = (transfer: Transfer): boolean => { export const extractERC20TransferDetails = (transfer: Transfer): ERC20TransferDetails => { const erc20TransferDetails = { tokenAddress: transfer.tokenInfo?.address || TxConstants.UNKNOWN, - value: humanReadableValue(transfer.value, transfer.tokenInfo?.decimals), + value: humanReadableValue(transfer.value || 0, transfer.tokenInfo?.decimals), name: transfer.tokenInfo?.name || transfer.tokenInfo?.symbol || TxConstants.UNKNOWN, txHash: transfer.transactionHash, } @@ -59,7 +59,7 @@ export const extractERC721TransferDetails = (transfer: Transfer): ERC721Transfer export const extractETHTransferDetails = (transfer: Transfer): ETHTransferDetails => { const ethTransferDetails = { - value: humanReadableValue(transfer.value), + value: humanReadableValue(transfer.value || 0), txHash: transfer.transactionHash, } if (isIncomingTransfer(transfer)) { diff --git a/src/store/index.ts b/src/store/index.ts index 8fc2340868..0b5d7a8d6d 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -6,7 +6,6 @@ import thunk from 'redux-thunk' import addressBookMiddleware from 'src/logic/addressBook/store/middleware/addressBookMiddleware' import addressBook, { ADDRESS_BOOK_REDUCER_ID } from 'src/logic/addressBook/store/reducer/addressBook' -import { AddressBookReducerMap } from 'src/logic/addressBook/store/reducer/types/addressBook.d' import { NFT_ASSETS_REDUCER_ID, NFT_TOKENS_REDUCER_ID, @@ -19,7 +18,10 @@ import currencyValues, { CURRENCY_VALUES_KEY, CurrencyValuesState, } from 'src/logic/currencyValues/store/reducer/currencyValues' -import currentSession, { CURRENT_SESSION_REDUCER_ID } from 'src/logic/currentSession/store/reducer/currentSession' +import currentSession, { + CURRENT_SESSION_REDUCER_ID, + CurrentSessionState, +} from 'src/logic/currentSession/store/reducer/currentSession' import notifications, { NOTIFICATIONS_REDUCER_ID } from 'src/logic/notifications/store/reducer/notifications' import tokens, { TOKEN_REDUCER_ID, TokenState } from 'src/logic/tokens/store/reducer/tokens' import providerWatcher from 'src/logic/wallets/store/middlewares/providerWatcher' @@ -38,6 +40,7 @@ import transactions, { TRANSACTIONS_REDUCER_ID } from 'src/logic/safe/store/redu import { NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/OpenSea' import { SafeReducerMap } from 'src/routes/safe/store/reducer/types/safe' import allTransactions, { TRANSACTIONS, TransactionsState } from '../logic/safe/store/reducer/allTransactions' +import { AddressBookState } from 'src/logic/addressBook/model/addressBook' export const history = createHashHistory() @@ -85,8 +88,8 @@ export type AppReduxState = CombinedState<{ [NOTIFICATIONS_REDUCER_ID]: Map [CURRENCY_VALUES_KEY]: CurrencyValuesState [COOKIES_REDUCER_ID]: Map - [ADDRESS_BOOK_REDUCER_ID]: AddressBookReducerMap - [CURRENT_SESSION_REDUCER_ID]: Map + [ADDRESS_BOOK_REDUCER_ID]: AddressBookState + [CURRENT_SESSION_REDUCER_ID]: CurrentSessionState [TRANSACTIONS]: TransactionsState router: RouterState }> diff --git a/src/test/safe.dom.balances.ts b/src/test/safe.dom.balances.ts deleted file mode 100644 index 142228fce0..0000000000 --- a/src/test/safe.dom.balances.ts +++ /dev/null @@ -1,72 +0,0 @@ -// -import { waitForElement } from '@testing-library/react' -import { Set, Map } from 'immutable' -import { aNewStore } from 'src/store' -import { sleep } from 'src/utils/timer' -import { aMinedSafe } from 'src/test/builder/safe.redux.builder' -import { sendTokenTo, sendEtherTo } from 'src/test/utils/tokenMovements' -import { renderSafeView } from 'src/test/builder/safe.dom.utils' -import { dispatchAddTokenToList } from 'src/test/utils/transactions/moveTokens.helper' -// import { calculateBalanceOf } from 'src/routes/safe/store/actions/fetchTokenBalances' -import updateActiveTokens from 'src/logic/safe/store/actions/updateActiveTokens' -import '@testing-library/jest-dom/extend-expect' -import updateSafe from 'src/logic/safe/store/actions/updateSafe' -import { BALANCE_ROW_TEST_ID } from 'src/routes/safe/components/Balances' -import { getBalanceInEtherOf } from 'src/logic/wallets/getWeb3' - -describe('DOM > Feature > Balances', () => { - let store - let safeAddress - beforeEach(async () => { - store = aNewStore() - safeAddress = await aMinedSafe(store) - }) - - it('Updates token balances automatically', async () => { - const tokensAmount = '100' - const tokenAddress = await sendTokenTo(safeAddress, tokensAmount) - await dispatchAddTokenToList(store, tokenAddress) - - const SafeDom = await renderSafeView(store, safeAddress) - - // Activate token - const safeTokenBalance = undefined - // const safeTokenBalance = await calculateBalanceOf(tokenAddress, safeAddress, 18) - // expect(safeTokenBalance).toBe(tokensAmount) - - const balances = Map({ - [tokenAddress]: safeTokenBalance, - }) - store.dispatch(updateActiveTokens(safeAddress, Set([tokenAddress]))) - store.dispatch(updateSafe({ address: safeAddress, balances })) - await sleep(1000) - - const balanceRows = SafeDom.getAllByTestId(BALANCE_ROW_TEST_ID) - expect(balanceRows.length).toBe(2) - - await waitForElement(() => SafeDom.getByText(`${tokensAmount} OMG`)) - - await sendTokenTo(safeAddress, tokensAmount) - - await waitForElement(() => SafeDom.getByText(`${parseInt(tokensAmount, 10) * 2} OMG`)) - }) - - it('Updates ether balance automatically', async () => { - const etherAmount = '1' - await sendEtherTo(safeAddress, etherAmount) - - const SafeDom = await renderSafeView(store, safeAddress) - - const safeEthBalance = await getBalanceInEtherOf(safeAddress) - expect(safeEthBalance).toBe(etherAmount) - - const balanceRows = SafeDom.getAllByTestId(BALANCE_ROW_TEST_ID) - expect(balanceRows.length).toBe(1) - - await waitForElement(() => SafeDom.getByText(`${etherAmount} ETH`)) - - await sendEtherTo(safeAddress, etherAmount) - - await waitForElement(() => SafeDom.getByText(`${parseInt(etherAmount, 10) * 2} ETH`)) - }) -}) diff --git a/src/test/tokens.dom.adding.ts b/src/test/tokens.dom.adding.ts index fa1c5b0f1e..79f770af1a 100644 --- a/src/test/tokens.dom.adding.ts +++ b/src/test/tokens.dom.adding.ts @@ -1,83 +1,82 @@ -// -import { fireEvent } from '@testing-library/react' -import { getWeb3 } from 'src/logic/wallets/getWeb3' -import { getFirstTokenContract } from 'src/test/utils/tokenMovements' -import { aNewStore } from 'src/store' -import { aMinedSafe } from 'src/test/builder/safe.redux.builder' -import { renderSafeView } from 'src/test/builder/safe.dom.utils' -import { sleep } from 'src/utils/timer' -import { clickOnManageTokens, clickOnAddCustomToken } from 'src/test/utils/DOMNavigation' -import * as fetchTokensModule from 'src/logic/tokens/store/actions/fetchTokens' -import { - ADD_CUSTOM_TOKEN_ADDRESS_INPUT_TEST_ID, - ADD_CUSTOM_TOKEN_SYMBOLS_INPUT_TEST_ID, - ADD_CUSTOM_TOKEN_DECIMALS_INPUT_TEST_ID, - ADD_CUSTOM_TOKEN_FORM, -} from 'src/routes/safe/components/Balances/Tokens/screens/AddCustomToken' -import { BALANCE_ROW_TEST_ID } from 'src/routes/safe/components/Balances/' -import '@testing-library/jest-dom/extend-expect' +// import { fireEvent } from '@testing-library/react' +// import { getWeb3 } from 'src/logic/wallets/getWeb3' +// import { getFirstTokenContract } from 'src/test/utils/tokenMovements' +// import { aNewStore } from 'src/store' +// import { aMinedSafe } from 'src/test/builder/safe.redux.builder' +// import { renderSafeView } from 'src/test/builder/safe.dom.utils' +// import { sleep } from 'src/utils/timer' +// import { clickOnManageTokens, clickOnAddCustomToken } from 'src/test/utils/DOMNavigation' +// import * as fetchTokensModule from 'src/logic/tokens/store/actions/fetchTokens' +// import { +// ADD_CUSTOM_TOKEN_ADDRESS_INPUT_TEST_ID, +// ADD_CUSTOM_TOKEN_SYMBOLS_INPUT_TEST_ID, +// ADD_CUSTOM_TOKEN_DECIMALS_INPUT_TEST_ID, +// ADD_CUSTOM_TOKEN_FORM, +// } from 'src/routes/safe/components/Balances/Tokens/screens/AddCustomToken' +// import { BALANCE_ROW_TEST_ID } from 'src/routes/safe/components/Balances/' +// import '@testing-library/jest-dom/extend-expect' +export const TODO = 'TODO' +// // https://github.com/testing-library/@testing-library/react/issues/281 +// const originalError = console.error +// beforeAll(() => { +// console.error = (...args) => { +// if (/Warning.*not wrapped in act/.test(args[0])) { +// return +// } +// originalError.call(console, ...args) +// } +// }) -// https://github.com/testing-library/@testing-library/react/issues/281 -const originalError = console.error -beforeAll(() => { - console.error = (...args) => { - if (/Warning.*not wrapped in act/.test(args[0])) { - return - } - originalError.call(console, ...args) - } -}) +// afterAll(() => { +// console.error = originalError +// }) -afterAll(() => { - console.error = originalError -}) +// describe('DOM > Feature > Add custom ERC 20 Tokens', () => { +// let web3 +// let accounts +// let erc20Token -describe('DOM > Feature > Add custom ERC 20 Tokens', () => { - let web3 - let accounts - let erc20Token +// beforeAll(async () => { +// web3 = getWeb3() +// accounts = await web3.eth.getAccounts() +// erc20Token = await getFirstTokenContract(web3, accounts[0]) +// }) - beforeAll(async () => { - web3 = getWeb3() - accounts = await web3.eth.getAccounts() - erc20Token = await getFirstTokenContract(web3, accounts[0]) - }) +// it('adds and displays an erc 20 token after filling the form', async () => { +// // GIVEN +// const store = aNewStore() +// const safeAddress = await aMinedSafe(store) +// await store.dispatch(fetchTokensModule.fetchTokens() as any) +// const TokensDom = renderSafeView(store, safeAddress) +// await sleep(400) - it('adds and displays an erc 20 token after filling the form', async () => { - // GIVEN - const store = aNewStore() - const safeAddress = await aMinedSafe(store) - await store.dispatch(fetchTokensModule.fetchTokens() as any) - const TokensDom = renderSafeView(store, safeAddress) - await sleep(400) +// // WHEN +// clickOnManageTokens(TokensDom) +// clickOnAddCustomToken(TokensDom) +// await sleep(200) - // WHEN - clickOnManageTokens(TokensDom) - clickOnAddCustomToken(TokensDom) - await sleep(200) +// // Fill address +// const addTokenForm = TokensDom.getByTestId(ADD_CUSTOM_TOKEN_FORM) +// const addressInput = TokensDom.getByTestId(ADD_CUSTOM_TOKEN_ADDRESS_INPUT_TEST_ID) +// fireEvent.change(addressInput, { target: { value: erc20Token.address } }) +// await sleep(500) - // Fill address - const addTokenForm = TokensDom.getByTestId(ADD_CUSTOM_TOKEN_FORM) - const addressInput = TokensDom.getByTestId(ADD_CUSTOM_TOKEN_ADDRESS_INPUT_TEST_ID) - fireEvent.change(addressInput, { target: { value: erc20Token.address } }) - await sleep(500) +// // Check if it loaded symbol/decimals correctly +// const symbolInput: any = TokensDom.getByTestId(ADD_CUSTOM_TOKEN_SYMBOLS_INPUT_TEST_ID) +// const decimalsInput: any = TokensDom.getByTestId(ADD_CUSTOM_TOKEN_DECIMALS_INPUT_TEST_ID) - // Check if it loaded symbol/decimals correctly - const symbolInput: any = TokensDom.getByTestId(ADD_CUSTOM_TOKEN_SYMBOLS_INPUT_TEST_ID) - const decimalsInput: any = TokensDom.getByTestId(ADD_CUSTOM_TOKEN_DECIMALS_INPUT_TEST_ID) +// const tokenSymbol = await erc20Token.symbol() +// const tokenDecimals = await erc20Token.decimals() +// expect(symbolInput.value).toBe(tokenSymbol) +// expect(decimalsInput.value).toBe(tokenDecimals.toString()) - const tokenSymbol = await erc20Token.symbol() - const tokenDecimals = await erc20Token.decimals() - expect(symbolInput.value).toBe(tokenSymbol) - expect(decimalsInput.value).toBe(tokenDecimals.toString()) +// // Submit form +// fireEvent.submit(addTokenForm) +// await sleep(300) - // Submit form - fireEvent.submit(addTokenForm) - await sleep(300) - - // check if token is displayed - const balanceRows = TokensDom.getAllByTestId(BALANCE_ROW_TEST_ID) - expect(balanceRows.length).toBe(2) - expect(balanceRows[1]).toHaveTextContent(tokenSymbol) - }) -}) +// // check if token is displayed +// const balanceRows = TokensDom.getAllByTestId(BALANCE_ROW_TEST_ID) +// expect(balanceRows.length).toBe(2) +// expect(balanceRows[1]).toHaveTextContent(tokenSymbol) +// }) +// }) diff --git a/src/test/tokens.dom.enabling.ts b/src/test/tokens.dom.enabling.ts index 1f22bff7b2..0a3ea15c5f 100644 --- a/src/test/tokens.dom.enabling.ts +++ b/src/test/tokens.dom.enabling.ts @@ -1,92 +1,91 @@ -// -import { waitForElement } from '@testing-library/react' -import { List } from 'immutable' -import { getWeb3 } from 'src/logic/wallets/getWeb3' -import { getFirstTokenContract, getSecondTokenContract } from 'src/test/utils/tokenMovements' -import { aNewStore } from 'src/store' -import { aMinedSafe } from 'src/test/builder/safe.redux.builder' -import { renderSafeView } from 'src/test/builder/safe.dom.utils' -import { sleep } from 'src/utils/timer' -import saveTokens from 'src/logic/tokens/store/actions/saveTokens' -import { clickOnManageTokens, closeManageTokensModal, toggleToken } from './utils/DOMNavigation' -import { BALANCE_ROW_TEST_ID } from 'src/routes/safe/components/Balances' -import { makeToken } from 'src/logic/tokens/store/model/token' -import '@testing-library/jest-dom/extend-expect' -import { getActiveTokens } from 'src/logic/tokens/utils/tokensStorage' +// import { waitForElement } from '@testing-library/react' +// import { List } from 'immutable' +// import { getWeb3 } from 'src/logic/wallets/getWeb3' +// import { getFirstTokenContract, getSecondTokenContract } from 'src/test/utils/tokenMovements' +// import { aNewStore } from 'src/store' +// import { aMinedSafe } from 'src/test/builder/safe.redux.builder' +// import { renderSafeView } from 'src/test/builder/safe.dom.utils' +// import { sleep } from 'src/utils/timer' +// import saveTokens from 'src/logic/tokens/store/actions/saveTokens' +// import { clickOnManageTokens, closeManageTokensModal, toggleToken } from './utils/DOMNavigation' +// import { BALANCE_ROW_TEST_ID } from 'src/routes/safe/components/Balances' +// import { makeToken } from 'src/logic/tokens/store/model/token' +// import '@testing-library/jest-dom/extend-expect' +// import { getActiveTokens } from 'src/logic/tokens/utils/tokensStorage' +export const TODO = 'TODO' +// describe('DOM > Feature > Enable and disable default tokens', () => { +// let web3 +// let accounts +// let firstErc20Token +// let secondErc20Token +// let testTokens -describe('DOM > Feature > Enable and disable default tokens', () => { - let web3 - let accounts - let firstErc20Token - let secondErc20Token - let testTokens +// beforeAll(async () => { +// web3 = getWeb3() +// accounts = await web3.eth.getAccounts() - beforeAll(async () => { - web3 = getWeb3() - accounts = await web3.eth.getAccounts() +// firstErc20Token = await getFirstTokenContract(web3, accounts[0]) +// secondErc20Token = await getSecondTokenContract(web3, accounts[0]) +// testTokens = List([ +// makeToken({ +// address: firstErc20Token.address, +// name: 'First Token Example', +// symbol: 'FTE', +// decimals: 18, +// logoUri: 'https://upload.wikimedia.org/wikipedia/commons/c/c0/Earth_simple_icon.png', +// }), +// makeToken({ +// address: secondErc20Token.address, +// name: 'Second Token Example', +// symbol: 'STE', +// decimals: 18, +// logoUri: 'https://upload.wikimedia.org/wikipedia/commons/c/c0/Earth_simple_icon.png', +// }), +// ]) +// }) - firstErc20Token = await getFirstTokenContract(web3, accounts[0]) - secondErc20Token = await getSecondTokenContract(web3, accounts[0]) - testTokens = List([ - makeToken({ - address: firstErc20Token.address, - name: 'First Token Example', - symbol: 'FTE', - decimals: 18, - logoUri: 'https://upload.wikimedia.org/wikipedia/commons/c/c0/Earth_simple_icon.png', - }), - makeToken({ - address: secondErc20Token.address, - name: 'Second Token Example', - symbol: 'STE', - decimals: 18, - logoUri: 'https://upload.wikimedia.org/wikipedia/commons/c/c0/Earth_simple_icon.png', - }), - ]) - }) +// it('allows to enable and disable tokens, stores active ones in the local storage', async () => { +// // GIVEN +// const store = aNewStore() +// const safeAddress = await aMinedSafe(store) +// await store.dispatch(saveTokens(testTokens)) - it('allows to enable and disable tokens, stores active ones in the local storage', async () => { - // GIVEN - const store = aNewStore() - const safeAddress = await aMinedSafe(store) - await store.dispatch(saveTokens(testTokens)) +// // WHEN +// const TokensDom = await renderSafeView(store, safeAddress) - // WHEN - const TokensDom = await renderSafeView(store, safeAddress) +// // Check if only ETH is enabled +// let balanceRows = await waitForElement(() => TokensDom.getAllByTestId(BALANCE_ROW_TEST_ID)) +// expect(balanceRows.length).toBe(1) - // Check if only ETH is enabled - let balanceRows = await waitForElement(() => TokensDom.getAllByTestId(BALANCE_ROW_TEST_ID)) - expect(balanceRows.length).toBe(1) +// // THEN +// clickOnManageTokens(TokensDom) +// await toggleToken(TokensDom, 'FTE') +// await toggleToken(TokensDom, 'STE') +// closeManageTokensModal(TokensDom) - // THEN - clickOnManageTokens(TokensDom) - await toggleToken(TokensDom, 'FTE') - await toggleToken(TokensDom, 'STE') - closeManageTokensModal(TokensDom) +// // Wait for active tokens to save +// await sleep(1500) - // Wait for active tokens to save - await sleep(1500) +// // Check if tokens were enabled +// balanceRows = TokensDom.getAllByTestId(BALANCE_ROW_TEST_ID) +// expect(balanceRows.length).toBe(3) +// expect(balanceRows[1]).toHaveTextContent('FTE') +// expect(balanceRows[2]).toHaveTextContent('STE') +// const tokensFromStorage = await getActiveTokens() - // Check if tokens were enabled - balanceRows = TokensDom.getAllByTestId(BALANCE_ROW_TEST_ID) - expect(balanceRows.length).toBe(3) - expect(balanceRows[1]).toHaveTextContent('FTE') - expect(balanceRows[2]).toHaveTextContent('STE') - const tokensFromStorage = await getActiveTokens() +// expect(Object.keys(tokensFromStorage)).toContain(firstErc20Token.address) +// expect(Object.keys(tokensFromStorage)).toContain(secondErc20Token.address) - expect(Object.keys(tokensFromStorage)).toContain(firstErc20Token.address) - expect(Object.keys(tokensFromStorage)).toContain(secondErc20Token.address) +// // disable tokens +// clickOnManageTokens(TokensDom) +// await toggleToken(TokensDom, 'FTE') +// await toggleToken(TokensDom, 'STE') +// closeManageTokensModal(TokensDom) +// await sleep(1500) - // disable tokens - clickOnManageTokens(TokensDom) - await toggleToken(TokensDom, 'FTE') - await toggleToken(TokensDom, 'STE') - closeManageTokensModal(TokensDom) - await sleep(1500) - - // check if tokens were disabled - balanceRows = TokensDom.getAllByTestId(BALANCE_ROW_TEST_ID) - expect(balanceRows.length).toBe(1) - expect(balanceRows[0]).toHaveTextContent('ETH') - }) -}) +// // check if tokens were disabled +// balanceRows = TokensDom.getAllByTestId(BALANCE_ROW_TEST_ID) +// expect(balanceRows.length).toBe(1) +// expect(balanceRows[0]).toHaveTextContent('ETH') +// }) +// }) diff --git a/src/test/utils/safeHelper.ts b/src/test/utils/safeHelper.ts new file mode 100644 index 0000000000..c260537c06 --- /dev/null +++ b/src/test/utils/safeHelper.ts @@ -0,0 +1,163 @@ +//@ts-nocheck +import { NonPayableTransactionObject } from 'src/types/contracts/types.d' +import { PromiEvent } from 'web3-core' +import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d' +import { ContractOptions, ContractSendMethod, DeployOptions, EventData, PastEventOptions } from 'web3-eth-contract' +import { + ConfirmationServiceModel, + TxServiceModel, +} from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions' +import { DataDecoded } from 'src/routes/safe/store/models/types/transactions.d' +import { List, Map } from 'immutable' +import { PendingActionValues } from 'src/logic/safe/store/models/types/transaction' + +const mockNonPayableTransactionObject = (callResult?: string): NonPayableTransactionObject => { + return { + arguments: [], + call: (tx?) => new Promise((resolve) => resolve(callResult || '')), + encodeABI: (tx?) => '', + estimateGas: (tx?) => new Promise((resolve) => resolve(1000)), + send: () => { return {} as PromiEvent} + } +} + +type SafeMethodsProps = { + threshold?: string + nonce?: string + isOwnerUserAddress?: string, + name?: string, + version?: string +} + +export const getMockedSafeInstance = (safeProps: SafeMethodsProps): GnosisSafe => { + const { threshold = '1', nonce = '0', isOwnerUserAddress, name = 'safeName', version = '1.0.0' } = safeProps + return { + defaultAccount: undefined, + defaultBlock: undefined, + defaultChain: undefined, + defaultCommon: undefined, + defaultHardfork: undefined, + handleRevert: false, + options: undefined, + transactionBlockTimeout: 0, + transactionConfirmationBlocks: 0, + transactionPollingTimeout: 0, + clone(): GnosisSafe { + return undefined; + }, + constructor(jsonInterface: any[], address?: string, options?: ContractOptions): GnosisSafe { + return undefined; + }, + deploy(options: DeployOptions): ContractSendMethod { + return undefined; + }, + getPastEvents(event: string, options?: PastEventOptions | ((error: Error, event: EventData) => void), callback?: (error: Error, event: EventData) => void): Promise { + return undefined; + }, + once(event: "AddedOwner" | "ExecutionFromModuleSuccess" | "EnabledModule" | "ChangedMasterCopy" | "ExecutionFromModuleFailure" | "RemovedOwner" | "ApproveHash" | "DisabledModule" | "SignMsg" | "ExecutionSuccess" | "ChangedThreshold" | "ExecutionFailure", cb: any): void { + }, + events: { } as any, + methods: { + NAME: (): NonPayableTransactionObject => mockNonPayableTransactionObject(name) as NonPayableTransactionObject, + VERSION: (): NonPayableTransactionObject => mockNonPayableTransactionObject(version) as NonPayableTransactionObject, + addOwnerWithThreshold: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + approvedHashes: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + changeMasterCopy: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + changeThreshold: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + disableModule: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + domainSeparator: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + enableModule: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + execTransactionFromModule: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + execTransactionFromModuleReturnData: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + getModules: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + getThreshold: (): NonPayableTransactionObject => mockNonPayableTransactionObject(threshold) as NonPayableTransactionObject, + isOwner: (): NonPayableTransactionObject => mockNonPayableTransactionObject(isOwnerUserAddress) as NonPayableTransactionObject, + nonce: (): NonPayableTransactionObject => mockNonPayableTransactionObject(nonce) as NonPayableTransactionObject, + removeOwner: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + setFallbackHandler: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + signedMessages: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + swapOwner: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + setup: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + execTransaction: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + requiredTxGas: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + approveHash: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + signMessage: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + isValidSignature: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + getMessageHash: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + encodeTransactionData: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + getTransactionHash: (): NonPayableTransactionObject => mockNonPayableTransactionObject() as NonPayableTransactionObject, + } as any + } +} + +type TransactionProps = { + baseGas?: number + blockNumber?: number | null + confirmations?: ConfirmationServiceModel[] + confirmationsRequired?: number + creationTx?: boolean | null + data?: string | null + dataDecoded?: DataDecoded + ethGasPrice?: string + executionDate?: string | null + executor?: string + fee?: string + gasPrice?: string + gasToken?: string + gasUsed?: number + isExecuted?: boolean + isSuccessful?: boolean + modified?: string + nonce?: number | null + operation?: number + origin?: string | null + ownersWithPendingActions?: Map>, + recipient?: string, + refundParams?: string, + refundReceiver?: string + safe?: string + safeTxGas?: number + safeTxHash?: string + signatures?: string + submissionDate?: string | null + to?: string + transactionHash?: string | null + value?: string +} + + +export const getMockedTxServiceModel = (txProps: TransactionProps): TxServiceModel => { + return { + baseGas: 0, + confirmations: [], + confirmationsRequired: 0, + creationTx: false, + data: null, + ethGasPrice: '', + executionDate: '', + executor: '', + fee: '', + gasPrice: '', + gasToken: '', + gasUsed: 0, + isExecuted: false, + isSuccessful: false, + modified: '', + nonce: 0, + operation: 0, + origin: '', + ownersWithPendingActions: Map(), + recipient: '', + refundParams: '', + refundReceiver: '', + safe: '', + safeTxGas: 0, + safeTxHash: '', + signatures: '', + submissionDate: '', + to: '', + transactionHash: '', + value: '', + ...txProps + } +} diff --git a/src/test/utils/tokenMovements.ts b/src/test/utils/tokenMovements.ts index 85ffe7e064..e15863d868 100644 --- a/src/test/utils/tokenMovements.ts +++ b/src/test/utils/tokenMovements.ts @@ -56,13 +56,13 @@ export const getFirstTokenContract = undefined //ensureOnce(createTokenOMGContra export const getSecondTokenContract = undefined //ensureOnce(createTokenRDNContract) export const get6DecimalsTokenContract = undefined //ensureOnce(create6DecimalsTokenContract) -export const sendTokenTo = async (safe, value, tokenContract?: any) => { - const web3 = getWeb3() - const accounts = await web3.eth.getAccounts() +// export const sendTokenTo = async (safe, value, tokenContract?: any) => { +// const web3 = getWeb3() +// const accounts = await web3.eth.getAccounts() - const OMGToken = tokenContract || (await getFirstTokenContract(web3, accounts[0])) - const nativeValue = toNative(value, 18) - await OMGToken.transfer(safe, nativeValue.valueOf(), { from: accounts[0], gas: '5000000' }) +// const OMGToken = tokenContract || (await getFirstTokenContract(web3, accounts[0])) +// const nativeValue = toNative(value, 18) +// await OMGToken.transfer(safe, nativeValue.valueOf(), { from: accounts[0], gas: '5000000' }) - return OMGToken.address -} +// return OMGToken.address +// } diff --git a/src/utils/checksumAddress.ts b/src/utils/checksumAddress.ts index 674509a37c..b0cd1fdca4 100644 --- a/src/utils/checksumAddress.ts +++ b/src/utils/checksumAddress.ts @@ -1,6 +1,5 @@ import { getWeb3 } from 'src/logic/wallets/getWeb3' export const checksumAddress = (address: string): string => { - if (!address) return null return getWeb3().utils.toChecksumAddress(address) } diff --git a/src/utils/clipboard.ts b/src/utils/clipboard.ts index e2b45a91a2..d540a71cca 100644 --- a/src/utils/clipboard.ts +++ b/src/utils/clipboard.ts @@ -1,15 +1,15 @@ -export const copyToClipboard = (text) => { +export const copyToClipboard = (text: string): void => { const range = document.createRange() range.selectNodeContents(document.body) - document.getSelection().addRange(range) + document?.getSelection()?.addRange(range) - function listener(e) { - e.clipboardData.setData('text/plain', text) + function listener(e: ClipboardEvent) { + e.clipboardData?.setData('text/plain', text) e.preventDefault() } document.addEventListener('copy', listener) document.execCommand('copy') document.removeEventListener('copy', listener) - document.getSelection().removeAllRanges() + document?.getSelection()?.removeAllRanges() } diff --git a/src/utils/fetch.ts b/src/utils/fetch.ts deleted file mode 100644 index faf9f84e19..0000000000 --- a/src/utils/fetch.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const enhancedFetch = async (url, errMsg) => { - const header = new Headers({ - 'Access-Control-Allow-Origin': '*', - }) - - const sentData: any = { - mode: 'cors', - header, - } - - const response = await fetch(url, sentData) - if (!response.ok) { - return Promise.reject(new Error(errMsg)) - } - - return Promise.resolve(response.json()) -} diff --git a/src/utils/googleAnalytics.ts b/src/utils/googleAnalytics.ts index d66c860260..091f6a40bb 100644 --- a/src/utils/googleAnalytics.ts +++ b/src/utils/googleAnalytics.ts @@ -1,12 +1,14 @@ import { useCallback, useEffect, useState } from 'react' -import GoogleAnalytics from 'react-ga' +import GoogleAnalytics, { EventArgs } from 'react-ga' import { getGoogleAnalyticsTrackingID } from 'src/config' import { COOKIES_KEY } from 'src/logic/cookies/model/cookie' import { loadFromCookie } from 'src/logic/cookies/utils' +export const SAFE_NAVIGATION_EVENT = 'Safe Navigation' + let analyticsLoaded = false -export const loadGoogleAnalytics = () => { +export const loadGoogleAnalytics = (): void => { if (analyticsLoaded) { return } @@ -22,7 +24,12 @@ export const loadGoogleAnalytics = () => { } } -export const useAnalytics = () => { +type UseAnalyticsResponse = { + trackPage: (path: string) => void + trackEvent: (event: EventArgs) => void +} + +export const useAnalytics = (): UseAnalyticsResponse => { const [analyticsAllowed, setAnalyticsAllowed] = useState(false) useEffect(() => { @@ -37,18 +44,24 @@ export const useAnalytics = () => { }, []) const trackPage = useCallback( - (page, options = {}) => { + (page) => { if (!analyticsAllowed || !analyticsLoaded) { return } - GoogleAnalytics.set({ - page, - ...options, - }) GoogleAnalytics.pageview(page) }, [analyticsAllowed], ) - return { trackPage } + const trackEvent = useCallback( + (event: EventArgs) => { + if (!analyticsAllowed || !analyticsLoaded) { + return + } + GoogleAnalytics.event(event) + }, + [analyticsAllowed], + ) + + return { trackPage, trackEvent } } diff --git a/src/utils/intercom.ts b/src/utils/intercom.ts index 8c2f7ee949..6fb98cbf97 100644 --- a/src/utils/intercom.ts +++ b/src/utils/intercom.ts @@ -13,7 +13,7 @@ export const loadIntercom = () => { s.async = true s.src = `https://widget.intercom.io/widget/${APP_ID}` const x = d.getElementsByTagName('script')[0] - x.parentNode.insertBefore(s, x) + x?.parentNode?.insertBefore(s, x) s.onload = () => { ;(window as any).Intercom('boot', { diff --git a/src/utils/storage/signatures.ts b/src/utils/storage/signatures.ts deleted file mode 100644 index 1684fea41c..0000000000 --- a/src/utils/storage/signatures.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Map } from 'immutable' - -import { loadFromStorage, saveToStorage } from 'src/utils/storage' - -const getSignaturesKeyFrom = (safeAddress) => `TXS-SIGNATURES-${safeAddress}` - -export const storeSignature = async (safeAddress, nonce, signature) => { - const signaturesKey = getSignaturesKeyFrom(safeAddress) - const subjects = Map(await loadFromStorage(signaturesKey)) || Map() - - try { - const key = `${nonce}` - const existingSignatures = subjects.get(key) - const signatures = existingSignatures ? existingSignatures + signature : signature - const updatedSubjects = subjects.set(key, signatures) - await saveToStorage(signaturesKey, updatedSubjects) - } catch (err) { - console.error('Error storing signatures in localstorage', err) - } -} - -export const getSignaturesFrom = (safeAddress, nonce) => { - const key = getSignaturesKeyFrom(safeAddress) - const data = loadFromStorage(key) - - const signatures = data ? Map(data as any) : Map() - const txSigs = signatures.get(String(nonce)) || '' - - return `0x${txSigs}` -} diff --git a/src/utils/storage/transactions.ts b/src/utils/storage/transactions.ts deleted file mode 100644 index 855f5f3456..0000000000 --- a/src/utils/storage/transactions.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Map } from 'immutable' - -import { loadFromStorage, saveToStorage } from 'src/utils/storage' - -const getSubjectKeyFrom = (safeAddress) => `TXS-SUBJECTS-${safeAddress}` - -export const storeSubject = async (safeAddress, nonce, subject) => { - const key = getSubjectKeyFrom(safeAddress) - const subjects = Map(await loadFromStorage(key)) || Map() - - try { - const updatedSubjects = subjects.set(nonce, subject) - saveToStorage(key, updatedSubjects) - } catch (err) { - console.error('Error storing transaction subject in localstorage', err) - } -} diff --git a/src/utils/strings.ts b/src/utils/strings.ts index f7d41dedcb..ec142be70e 100644 --- a/src/utils/strings.ts +++ b/src/utils/strings.ts @@ -16,15 +16,7 @@ export const textShortener = ({ charsEnd = 10, charsStart = 10, ellipsis = '...' * @param text * @returns {string|?string} */ - (text = null) => { - if (typeof text !== 'string') { - throw new TypeError(` A string is required. ${typeof text} was provided instead.`) - } - - if (!text) { - return '' - } - + (text = ''): string => { const amountOfCharsToKeep = charsEnd + charsStart const finalStringLength = amountOfCharsToKeep + ellipsis.length diff --git a/tsconfig.json b/tsconfig.json index ede637e72e..61c5bcc10c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,7 @@ "noImplicitAny": false, "allowSyntheticDefaultImports": true, "strict": false, + "strictNullChecks": true, "forceConsistentCasingInFileNames": true, "module": "esnext", "moduleResolution": "node", diff --git a/yarn.lock b/yarn.lock index 1a75bcdf73..6c51521608 100644 --- a/yarn.lock +++ b/yarn.lock @@ -186,11 +186,10 @@ lodash "^4.17.19" "@babel/helper-explode-assignable-expression@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.10.4.tgz#40a1cd917bff1288f699a94a75b37a1a2dbd8c7c" - integrity sha512-4K71RyRQNPRrR85sr5QY4X3VwG4wtVoXZB9+L3r1Gp38DhELyHCtovqydRi7c1Ovb17eRGiQ/FD5s8JdU0Uy5A== + version "7.11.4" + resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.11.4.tgz#2d8e3470252cc17aba917ede7803d4a7a276a41b" + integrity sha512-ux9hm3zR4WV1Y3xXxXkdG/0gxF9nvI0YVmKVhvK9AfMoaQkemL3sJpXw+Xbz65azo8qJiEz2XVDUpK3KYhH3ZQ== dependencies: - "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" "@babel/helper-function-name@^7.10.4": @@ -263,14 +262,13 @@ lodash "^4.17.19" "@babel/helper-remap-async-to-generator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.10.4.tgz#fce8bea4e9690bbe923056ded21e54b4e8b68ed5" - integrity sha512-86Lsr6NNw3qTNl+TBcF1oRZMaVzJtbWTyTko+CQL/tvNvcGYEFKbLXDPxtW0HKk3McNOk4KzY55itGWCAGK5tg== + version "7.11.4" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.11.4.tgz#4474ea9f7438f18575e30b0cac784045b402a12d" + integrity sha512-tR5vJ/vBa9wFy3m5LLv2faapJLnDFxNWff2SAYkSE4rLUdbp7CdObYFgI7wK4T/Mj4UzpjPwzR8Pzmr5m7MHGA== dependencies: "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/helper-wrap-function" "^7.10.4" "@babel/template" "^7.10.4" - "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" "@babel/helper-replace-supers@^7.10.4": @@ -1486,9 +1484,10 @@ "@ethersproject/rlp" "^5.0.0" "@ethersproject/signing-key" "^5.0.0" -"@gnosis.pm/safe-apps-sdk@https://github.com/gnosis/safe-apps-sdk.git#development": - version "0.3.1" - resolved "https://github.com/gnosis/safe-apps-sdk.git#15c93481812dee9987ad52edd2589a49675d688d" +"@gnosis.pm/safe-apps-sdk@0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-apps-sdk/-/safe-apps-sdk-0.4.0.tgz#26c821513c995b9dc023ebbdfe103a832e731521" + integrity sha512-hUt/Siz5kSu9jgvMZXejQsxQiUo/NIow67KNAQGfMt7D0S1YoyvpCGAgSliNelY/bP7EanBhhStOnItnu7DwUA== "@gnosis.pm/safe-contracts@1.1.1-dev.2": version "1.1.1-dev.2" @@ -1524,7 +1523,7 @@ resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5" integrity sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ== -"@hapi/address@^4.0.1": +"@hapi/address@^4.1.0": version "4.1.0" resolved "https://registry.yarnpkg.com/@hapi/address/-/address-4.1.0.tgz#d60c5c0d930e77456fdcde2598e77302e2955e1d" integrity sha512-SkszZf13HVgGmChdHo/PxchnSaCJ6cetVqLzyciudzZRT0jcOouIF/Q93mgjw8cce+D+4F4C1Z/WrfFN+O3VHQ== @@ -1561,17 +1560,6 @@ "@hapi/hoek" "8.x.x" "@hapi/topo" "3.x.x" -"@hapi/joi@^17.1.1": - version "17.1.1" - resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-17.1.1.tgz#9cc8d7e2c2213d1e46708c6260184b447c661350" - integrity sha512-p4DKeZAoeZW4g3u7ZeRo+vCDuSDgSvtsB/NpfjXEHTUjSeINAi/RrVOWiVQ1isaoLzMvFEhe8n5065mQq1AdQg== - dependencies: - "@hapi/address" "^4.0.1" - "@hapi/formula" "^2.0.0" - "@hapi/hoek" "^9.0.0" - "@hapi/pinpoint" "^2.0.0" - "@hapi/topo" "^5.0.0" - "@hapi/pinpoint@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@hapi/pinpoint/-/pinpoint-2.0.0.tgz#805b40d4dbec04fc116a73089494e00f073de8df" @@ -1749,7 +1737,18 @@ "@types/yargs" "^15.0.0" chalk "^3.0.0" -"@ledgerhq/devices@^5.19.1", "@ledgerhq/devices@^5.22.0": +"@jest/types@^26.3.0": + version "26.3.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.3.0.tgz#97627bf4bdb72c55346eef98e3b3f7ddc4941f71" + integrity sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^15.0.0" + chalk "^4.0.0" + +"@ledgerhq/devices@^5.22.0": version "5.22.0" resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-5.22.0.tgz#18595f3545b57cf60e50d6e9d83095dda21f575f" integrity sha512-oJxhee/zlHmIx66zvQQTSpIsHOiiLjALemTX9oUtB4xQwFvoiptPnBCeTDTM9teode7wzk7oE9qdUAZuat+nCg== @@ -1758,7 +1757,7 @@ "@ledgerhq/logs" "^5.22.0" rxjs "^6.6.2" -"@ledgerhq/errors@^5.19.1", "@ledgerhq/errors@^5.22.0": +"@ledgerhq/errors@^5.22.0": version "5.22.0" resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-5.22.0.tgz#7327fc152d4896ddc26aada0943065db21c14880" integrity sha512-XDT0meBn39+q+JWzUFXmiFbVYLTy+uHRFMb9napcxyZ0Q/MdKkle9/vkgtvRHjPIkGobklXpyefsgH3BZQHukA== @@ -1773,7 +1772,7 @@ bignumber.js "^9.0.0" rlp "^2.2.6" -"@ledgerhq/hw-transport-node-hid-noevents@^5.19.1": +"@ledgerhq/hw-transport-node-hid-noevents@^5.22.0": version "5.22.0" resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-node-hid-noevents/-/hw-transport-node-hid-noevents-5.22.0.tgz#b5a42a71664fe69bf5fae579854d10e4815a794e" integrity sha512-6sxrqTcBEGvhVDOS5Vy3mKZgaVOKbHplxm4o/3PmtugJcvpEBvDNGXNh3PMWPtHXXYQ5E5C/qWh7Y+gYshMmTg== @@ -1784,16 +1783,16 @@ "@ledgerhq/logs" "^5.22.0" node-hid "^1.3.0" -"@ledgerhq/hw-transport-node-hid@5.19.1": - version "5.19.1" - resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-node-hid/-/hw-transport-node-hid-5.19.1.tgz#75017ad106a1c8c809ffb255a73f87ea5bfdd51d" - integrity sha512-8+FvB8MPYfRs6DqxwFjFxe5+7l4uOuwWHP7WXh4lcv26fU3jVOTghOr82YQwE+LPiYeQntViLwsLmp9DUiPQQA== +"@ledgerhq/hw-transport-node-hid@5.22.0": + version "5.22.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-node-hid/-/hw-transport-node-hid-5.22.0.tgz#00f573bd9163b9c553071af3f2a52aa19d4674c0" + integrity sha512-TrSQEGiYXBW8FQS2QEAmk/g+vwcKQ2MZVjPiIaqSCGnwVgmKOXfMetPjwgwr8k6XiQ7YMRdpsXa0GpIvTowqRA== dependencies: - "@ledgerhq/devices" "^5.19.1" - "@ledgerhq/errors" "^5.19.1" - "@ledgerhq/hw-transport" "^5.19.1" - "@ledgerhq/hw-transport-node-hid-noevents" "^5.19.1" - "@ledgerhq/logs" "^5.19.1" + "@ledgerhq/devices" "^5.22.0" + "@ledgerhq/errors" "^5.22.0" + "@ledgerhq/hw-transport" "^5.22.0" + "@ledgerhq/hw-transport-node-hid-noevents" "^5.22.0" + "@ledgerhq/logs" "^5.22.0" lodash "^4.17.19" node-hid "^1.3.0" usb "^1.6.3" @@ -1808,7 +1807,7 @@ "@ledgerhq/logs" "^5.22.0" u2f-api "0.2.7" -"@ledgerhq/hw-transport@^5.19.1", "@ledgerhq/hw-transport@^5.22.0": +"@ledgerhq/hw-transport@^5.22.0": version "5.22.0" resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-5.22.0.tgz#d627948b43005ec9e7dfe85adf9aa01e130de280" integrity sha512-MFfkVGYMYnr6fI4XGnJQNLd36JIrRpvd5WBmVSDhCO3UKUER2fJ9koVBGc97o7yXtE5IAlJKF+nR9HZJIa0lRQ== @@ -1817,7 +1816,7 @@ "@ledgerhq/errors" "^5.22.0" events "^3.2.0" -"@ledgerhq/logs@^5.19.1", "@ledgerhq/logs@^5.22.0": +"@ledgerhq/logs@^5.22.0": version "5.22.0" resolved "https://registry.yarnpkg.com/@ledgerhq/logs/-/logs-5.22.0.tgz#a54d6b5b391cdb4c2eacc9500feb04b90475c361" integrity sha512-jV4mJxD1aieORm+sK9bYakQd9GMLd7KAxgt2IaxhrTU+QD5Ne47mxQOTys9p7f5w25ujs3R+Px2t3KiMRASHtg== @@ -2477,21 +2476,21 @@ dependencies: defer-to-connect "^1.0.1" -"@testing-library/dom@^7.17.1": - version "7.22.2" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.22.2.tgz#6deaa828500993cc94bdd62875c251b5b5b70d69" - integrity sha512-taxURh+4Lwr//uC1Eghat95aMnTlI4G4ETosnZK0wliwHWdutLDVKIvHXAOYdXGdzrBAy1wNhSGmNBbZ72ml4g== +"@testing-library/dom@^7.22.3": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.23.0.tgz#c54c0fa53705ad867bcefb52fc0c96487fbc10f6" + integrity sha512-H5m090auYH+obdZmsaYLrSWC5OauWD2CvNbz88KBxQJoXgkJzbU0DpAG8BS7Evj5WqCC3nAAKrLS6vw0ljUYLg== dependencies: "@babel/runtime" "^7.10.3" "@types/aria-query" "^4.2.0" aria-query "^4.2.2" - dom-accessibility-api "^0.5.0" - pretty-format "^25.5.0" + dom-accessibility-api "^0.5.1" + pretty-format "^26.4.2" -"@testing-library/jest-dom@5.11.2": - version "5.11.2" - resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.11.2.tgz#c49de331555c70127b5d7fc97344ad5265f4c54c" - integrity sha512-s+rWJx+lanEGKqvOl4qJR0rGjCrxsEjj9qjxFlg4NV4/FRD7fnUUAWPHqwpyafNHfLYArs58FADgdn4UKmjFmw== +"@testing-library/jest-dom@5.11.4": + version "5.11.4" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.11.4.tgz#f325c600db352afb92995c2576022b35621ddc99" + integrity sha512-6RRn3epuweBODDIv3dAlWjOEHQLpGJHB2i912VS3JQtsD22+ENInhdDNl4ZZQiViLlIfFinkSET/J736ytV9sw== dependencies: "@babel/runtime" "^7.9.2" "@types/testing-library__jest-dom" "^5.9.1" @@ -2499,25 +2498,16 @@ chalk "^3.0.0" css "^3.0.0" css.escape "^1.5.1" - jest-diff "^25.1.0" - jest-matcher-utils "^25.1.0" lodash "^4.17.15" redent "^3.0.0" -"@testing-library/react@10.4.8": - version "10.4.8" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-10.4.8.tgz#5eb730291b8fd81cdb2d8877770d060b044ae4a4" - integrity sha512-clgpFR6QHiRRcdhFfAKDhH8UXpNASyfkkANhtCsCVBnai+O+mK1rGtMES+Apc7ql5Wyxu7j8dcLiC4pV5VblHA== +"@testing-library/react@10.4.9": + version "10.4.9" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-10.4.9.tgz#9faa29c6a1a217bf8bbb96a28bd29d7a847ca150" + integrity sha512-pHZKkqUy0tmiD81afs8xfiuseXfU/N7rAX3iKjeZYje86t9VaB0LrxYVa+OOsvkrveX5jCK3IjajVn2MbePvqA== dependencies: "@babel/runtime" "^7.10.3" - "@testing-library/dom" "^7.17.1" - -"@testing-library/user-event@12.1.0": - version "12.1.0" - resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-12.1.0.tgz#a2597419466a93e338c91baa7bb22d4da0309d1d" - integrity sha512-aH/XuNFpPD6dA+fh754EGqKeAzpH66HpLJYkv9vOAih2yGmTM8JiZ8uisQDGWRPkc6sxE2zCqDwLR4ZskhRCxw== - dependencies: - "@babel/runtime" "^7.10.2" + "@testing-library/dom" "^7.22.3" "@toruslabs/eccrypto@^1.1.4": version "1.1.5" @@ -2548,14 +2538,14 @@ loglevel "^1.6.8" "@toruslabs/torus-embed@^1.8.2": - version "1.8.2" - resolved "https://registry.yarnpkg.com/@toruslabs/torus-embed/-/torus-embed-1.8.2.tgz#6652b8f751c5f041749ccbfcaa0c08ced5f4f278" - integrity sha512-SlApK4BavoQYNenoQxjUs9/rrqrGDK5+Z9coABA6J7pLcbSL7QnBl8bKwTTYhI9Hri2GRbUM8XzNNpZfy5RiIQ== + version "1.8.3" + resolved "https://registry.yarnpkg.com/@toruslabs/torus-embed/-/torus-embed-1.8.3.tgz#3c1e5c6ca755628381529402650f00e5c0e4d407" + integrity sha512-wI+mDF3oj6QsHPcLrApVEXmddBcIzrB5JMdxR/V5Jag2Rlk3bRFf7VkxI4mXz0+Qf+He6+fa2VXWCITZMlaDeQ== dependencies: "@chaitanyapotti/random-id" "^1.0.3" "@toruslabs/fetch-node-details" "^2.3.0" "@toruslabs/http-helpers" "^1.3.4" - "@toruslabs/torus.js" "^2.2.4" + "@toruslabs/torus.js" "^2.2.5" create-hash "^1.2.0" deepmerge "^4.2.2" eth-json-rpc-errors "^2.0.2" @@ -2571,7 +2561,7 @@ safe-event-emitter "^1.0.1" web3 "^0.20.7" -"@toruslabs/torus.js@^2.2.4": +"@toruslabs/torus.js@^2.2.5": version "2.2.5" resolved "https://registry.yarnpkg.com/@toruslabs/torus.js/-/torus.js-2.2.5.tgz#8994ae7727d980e2c0600b1154d547260ea52ec4" integrity sha512-fxrIQmtNo4p3uEy5KdiIrZiB32KGPtaV70PoPg/vQB4IL/gjrQSYSIcC0VyP04yBfjHLccJe/HKOhlofpKcjAg== @@ -2831,7 +2821,14 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" -"@types/jest@*", "@types/jest@^26.0.9": +"@types/istanbul-reports@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz#508b13aa344fa4976234e75dddcc34925737d821" + integrity sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/jest@*": version "26.0.10" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.10.tgz#8faf7e9756c033c39014ae76a7329efea00ea607" integrity sha512-i2m0oyh8w/Lum7wWK/YOZJakYF8Mx08UaKA1CtbmFeDquVhAEdA7znacsVSf2hJ1OQ/OfVMGN90pw/AtzF8s/Q== @@ -2839,6 +2836,14 @@ jest-diff "^25.2.1" pretty-format "^25.2.1" +"@types/jest@^26.0.14": + version "26.0.14" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.14.tgz#078695f8f65cb55c5a98450d65083b2b73e5a3f3" + integrity sha512-Hz5q8Vu0D288x3iWXePSn53W7hAjP0H7EQ6QvDO9c7t46mR0lNOLlfuwQ+JkVxuhygHzlzPX+0jKdA3ZgSh+Vg== + dependencies: + jest-diff "^25.2.1" + pretty-format "^25.2.1" + "@types/json-schema@^7.0.3", "@types/json-schema@^7.0.4": version "7.0.5" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" @@ -2878,10 +2883,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.27.tgz#a151873af5a5e851b51b3b065c9e63390a9e0eb1" integrity sha512-kVrqXhbclHNHGu9ztnAwSncIgJv/FaxmzXJvGXNdcCpV1b8u1/Mi6z6m0vwy0LzKeXFTPLH0NzwmoJ3fNCIq0g== -"@types/node@14.6.0": - version "14.6.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.0.tgz#7d4411bf5157339337d7cff864d9ff45f177b499" - integrity sha512-mikldZQitV94akrc4sCcSjtJfsTKt4p+e/s0AGscVA6XArQ9kFclP+ZiYUMnq987rc6QlYxXv/EivqlfSLxpKA== +"@types/node@14.11.2": + version "14.11.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256" + integrity sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA== "@types/node@^10.12.18", "@types/node@^10.3.2": version "10.17.28" @@ -3003,10 +3008,10 @@ "@types/prop-types" "*" csstype "^3.0.2" -"@types/react@^16.9.47": - version "16.9.47" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.47.tgz#fb092936f0b56425f874d0ff1b08051fdf70c1ba" - integrity sha512-dAJO4VbrjYqTUwFiQqAKjLyHHl4RSTNnRyPdX3p16MPbDKvow51wxATUPxoe2QsiXNMEYrOjc2S6s92VjG+1VQ== +"@types/react@^16.9.49": + version "16.9.49" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.49.tgz#09db021cf8089aba0cdb12a49f8021a69cce4872" + integrity sha512-DtLFjSj0OYAdVLBbyjhuV9CdGVHCkHn2R+xr3XkBvK2rS1Y1tkc14XSGjYgm5Fjjr90AxH9tiSzc1pCFMGO06g== dependencies: "@types/prop-types" "*" csstype "^3.0.2" @@ -3040,10 +3045,10 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== -"@types/styled-components@^5.1.2": - version "5.1.2" - resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-5.1.2.tgz#652af475b4af917b355ea1c3068acae63d46455f" - integrity sha512-HNocYLfrsnNNm8NTS/W53OERSjRA8dx5Bn6wBd2rXXwt4Z3s+oqvY6/PbVt3e6sgtzI63GX//WiWiRhWur08qQ== +"@types/styled-components@^5.1.3": + version "5.1.3" + resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-5.1.3.tgz#6fab3d9c8f7d9a15cbb89d379d850c985002f363" + integrity sha512-HGpirof3WOhiX17lb61Q/tpgqn48jxO8EfZkdJ8ueYqwLbK2AHQe/G08DasdA2IdKnmwOIP1s9X2bopxKXgjRw== dependencies: "@types/hoist-non-react-statics" "*" "@types/react" "*" @@ -3725,9 +3730,9 @@ aes-js@3.1.2, aes-js@^3.1.1: integrity sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ== aggregate-error@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.0.1.tgz#db2fe7246e536f40d9b5442a39e117d7dd6a24e0" - integrity sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA== + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== dependencies: clean-stack "^2.0.0" indent-string "^4.0.0" @@ -4273,12 +4278,12 @@ axe-core@^3.5.4: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.5.5.tgz#84315073b53fa3c0c51676c588d59da09a192227" integrity sha512-5P0QZ6J5xGikH780pghEdbEKijCTrruK9KxtPZCFWUpef0f6GipO+xEZ5GKCb020mmqgbiNO6TcA55CriL784Q== -axios@0.19.2, axios@^0.19.2: - version "0.19.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" - integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== +axios@0.20.0: + version "0.20.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.20.0.tgz#057ba30f04884694993a8cd07fa394cff11c50bd" + integrity sha512-ANA4rr2BDcmmAQLOKft2fufrtuvlqR+cXNNinUmvfeSNCOF98PZL+7M/v1zIdGo7OLjEA9J2gXJL+j4zGsl0bA== dependencies: - follow-redirects "1.5.10" + follow-redirects "^1.10.0" axios@^0.18.0: version "0.18.1" @@ -4288,6 +4293,13 @@ axios@^0.18.0: follow-redirects "1.5.10" is-buffer "^2.0.2" +axios@^0.19.2: + version "0.19.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" + integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== + dependencies: + follow-redirects "1.5.10" + axobject-query@^2.0.2, axobject-query@^2.1.2: version "2.2.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" @@ -5345,10 +5357,10 @@ bn.js@^5.1.1, bn.js@^5.1.2: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b" integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ== -bnc-onboard@1.11.1: - version "1.11.1" - resolved "https://registry.yarnpkg.com/bnc-onboard/-/bnc-onboard-1.11.1.tgz#da463b0af063d8dc10d9cfbdcadbd40ee8383597" - integrity sha512-KuPEuVQGr4/oGysFXsJoVCpgyijEjWh6FHZeyP7azf2g6wUW3U7553+TPT72IMyIg47N4h7UfJFLzIZKFM9QMQ== +bnc-onboard@1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/bnc-onboard/-/bnc-onboard-1.13.1.tgz#281629e15c01ab47425f9494b33c4ce28a9db01e" + integrity sha512-Mv06oWNjkjDNU3vR8l/aJFKTBya5lj6vPfYcl9ONpyyaoY/8neZS4icQrFeRv9nDvENVz6esxcYAyX62f0+rwA== dependencies: "@ledgerhq/hw-app-eth" "^5.21.0" "@ledgerhq/hw-transport-u2f" "^5.21.0" @@ -6138,7 +6150,7 @@ cli-table3@0.5.1: optionalDependencies: colors "^1.1.2" -cli-truncate@2.1.0, cli-truncate@^2.1.0: +cli-truncate@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== @@ -6338,10 +6350,10 @@ commander@^4.0.1, commander@^4.1.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== -commander@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" - integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== +commander@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.1.0.tgz#f8d722b78103141006b66f4c7ba1e97315ba75bc" + integrity sha512-wl7PNrYWd2y5mp1OK/LhTlv8Ff4kQJQRXXAvF+uU/TPNiVJUxZLRYGj/B0y/lPGAVcSbJqH2Za/cvHmrPMC8mA== common-tags@^1.8.0: version "1.8.0" @@ -6405,7 +6417,7 @@ concat-stream@^1.5.0, concat-stream@^1.5.1, concat-stream@^1.6.2: readable-stream "^2.2.2" typedarray "^0.0.6" -concurrently@^5.2.0: +concurrently@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-5.3.0.tgz#7500de6410d043c912b2da27de3202cb489b1e7b" integrity sha512-8MhqOB6PWlBfA2vJ8a0bSFKATOdWlHiQlk11IfmQBPaHVP8oP2gsh2MObE6UR3hqDHqvaIvLTyceNW6obVuFHQ== @@ -6619,6 +6631,17 @@ cosmiconfig@^6.0.0: path-type "^4.0.0" yaml "^1.7.2" +cosmiconfig@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3" + integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + coveralls@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/coveralls/-/coveralls-3.1.0.tgz#13c754d5e7a2dd8b44fe5269e21ca394fb4d615b" @@ -7474,10 +7497,10 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dom-accessibility-api@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.0.tgz#fddffd04e178796e241436c3f21be2f89c91afac" - integrity sha512-eCVf9n4Ni5UQAFc2+fqfMPHdtiX7DA0rLakXgNBZfXNJzEbNo3MQIYd+zdYpFBqAaGYVrkd8leNSLGPrG4ODmA== +dom-accessibility-api@^0.5.1: + version "0.5.2" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.2.tgz#ef3cdb5d3f0d599d8f9c8b18df2fb63c9793739d" + integrity sha512-k7hRNKAiPJXD2aBqfahSo4/01cTsKWXf+LqJgglnkN2Nz8TsxXKQBXHhKe0Ye9fEfHEZY49uSA5Sr3AqP/sWKA== dom-converter@^0.2: version "0.2.0" @@ -7663,9 +7686,9 @@ ejs@^2.7.4: integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== ejs@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.3.tgz#514d967a8894084d18d3d47bd169a1c0560f093d" - integrity sha512-wmtrUGyfSC23GC/B1SMv2ogAUgbQEtDmTIhfqielrG5ExIM9TP4UoYdi90jLF1aTcsWCJNEO0UrgKzP0y3nTSg== + version "3.1.5" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.5.tgz#aed723844dc20acb4b170cd9ab1017e476a0d93b" + integrity sha512-dldq3ZfFtgVTJMLjOe+/3sROTzALlL9E34V4/sDtUd/KlBSS0s6U1/+WPE1B4sj9CXHJpL1M6rhNJnc9Wbal9w== dependencies: jake "^10.6.1" @@ -7699,13 +7722,13 @@ electron-log@4.2.4: resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-4.2.4.tgz#a13e42a9fc42ca2cc7d2603c3746352efa82112e" integrity sha512-CXbDU+Iwi+TjKzugKZmTRIORIPe3uQRqgChUl19fkW/reFUn5WP7dt+cNGT3bkLV8xfPilpkPFv33HgtmLLewQ== -electron-notarize@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/electron-notarize/-/electron-notarize-0.3.0.tgz#b93c606306eac558b250c78ff95273ddb9fedf0a" - integrity sha512-tuDw8H0gcDOalNLv6RM2CwGvUXU60MPGZRDEmd0ppX+yP5XqL8Ec2DuXyz9J7WQSA3aRCfzIgH8C5CAivDYWMw== +electron-notarize@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/electron-notarize/-/electron-notarize-1.0.0.tgz#bc925b1ccc3f79e58e029e8c4706572b01a9fd8f" + integrity sha512-dsib1IAquMn0onCrNMJ6gtEIZn/azG8hZMCYOuZIMVMUeRMgBYHK1s5TK9P8xAcrAjh/2aN5WYHzgVSWX314og== dependencies: debug "^4.1.1" - fs-extra "^8.1.0" + fs-extra "^9.0.1" electron-publish@22.8.0: version "22.8.0" @@ -7756,10 +7779,10 @@ electron-updater@4.3.4: lodash.isequal "^4.5.0" semver "^7.3.2" -electron@7.2.4: - version "7.2.4" - resolved "https://registry.yarnpkg.com/electron/-/electron-7.2.4.tgz#9fc0446dae23ead897af8742470cb18da55c6ce9" - integrity sha512-Z+R692uTzXgP8AHrabE+kkrMlQJ6pnAYoINenwj9QSqaD2YbO8IuXU9DMCcUY0+VpA91ee09wFZJNUKYPMnCKg== +electron@9.3.0: + version "9.3.0" + resolved "https://registry.yarnpkg.com/electron/-/electron-9.3.0.tgz#a4f3dc17f31acc6797eb4c2c4bd0d0e25efb939b" + integrity sha512-7zPLEZ+kOjVJqfawMQ0vVuZZRqvZIeiID3tbjjbVybbxXIlFMpZ2jogoh7PV3rLrtm+dKRfu7Qc4E7ob1d0FqQ== dependencies: "@electron/get" "^1.0.1" "@types/node" "^12.0.12" @@ -7888,7 +7911,7 @@ enhanced-resolve@^4.1.0, enhanced-resolve@^4.3.0: memory-fs "^0.5.0" tapable "^1.0.0" -enquirer@^2.3.5: +enquirer@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== @@ -7941,6 +7964,24 @@ es-abstract@^1.17.0, es-abstract@^1.17.0-next.0, es-abstract@^1.17.0-next.1, es- string.prototype.trimend "^1.0.1" string.prototype.trimstart "^1.0.1" +es-abstract@^1.18.0-next.0: + version "1.18.0-next.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.0.tgz#b302834927e624d8e5837ed48224291f2c66e6fc" + integrity sha512-elZXTZXKn51hUBdJjSZGYRujuzilgXo8vSPQzjGYXLvSlGiCo8VO8ZGV3kjo9a0WNJJ57hENagwbtlRuHuzkcQ== + dependencies: + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.2.0" + is-negative-zero "^2.0.0" + is-regex "^1.1.1" + object-inspect "^1.8.0" + object-keys "^1.1.1" + object.assign "^4.1.0" + string.prototype.trimend "^1.0.1" + string.prototype.trimstart "^1.0.1" + es-array-method-boxes-properly@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" @@ -8203,7 +8244,7 @@ eslint-plugin-react@7.19.0: string.prototype.matchall "^4.0.2" xregexp "^4.3.0" -eslint-plugin-react@^7.20.5: +eslint-plugin-react@^7.20.6: version "7.20.6" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.20.6.tgz#4d7845311a93c463493ccfa0a19c9c5d0fd69f60" integrity sha512-kidMTE5HAEBSLu23CUDvj8dc3LdBU0ri1scwHBZjI41oDv4tjsWZKU7MQccFzH1QYPYhsnTF2ovh7JlcIcmxgg== @@ -8893,11 +8934,16 @@ eventemitter3@4.0.0: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb" integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg== -eventemitter3@4.0.4, eventemitter3@^4.0.0: +eventemitter3@4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.4.tgz#b5463ace635a083d018bdc7c917b4c5f10a85384" integrity sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ== +eventemitter3@^4.0.0: + version "4.0.5" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.5.tgz#51d81e4f1ccc8311a04f0c20121ea824377ea6d9" + integrity sha512-QR0rh0YiPuxuDQ6+T9GAO/xWTExXpxIes1Nl9RykNGTnE1HJmkuEfxJH9cubjIOQZ/GH4qNBR4u8VSHaKiWs4g== + events@^3.0.0, events@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379" @@ -8936,7 +8982,7 @@ execa@^1.0.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@^4.0.1: +execa@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.3.tgz#0a34dabbad6d66100bd6f2c576c8669403f317f2" integrity sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A== @@ -8986,7 +9032,7 @@ expect@^24.9.0: jest-message-util "^24.9.0" jest-regex-util "^24.9.0" -exponential-backoff@^3.0.1: +exponential-backoff@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.0.tgz#9409c7e579131f8bd4b32d7d8094a911040f2e68" integrity sha512-oBuz5SYz5zzyuHINoe9ooePwSu0xApKWgeNzok4hZ5YKXFh9zrQBEM15CXqoZkJJPuI2ArvqjPQd8UKJA753XA== @@ -9451,7 +9497,7 @@ follow-redirects@1.5.10: dependencies: debug "=3.1.0" -follow-redirects@^1.0.0: +follow-redirects@^1.0.0, follow-redirects@^1.10.0: version "1.13.0" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db" integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA== @@ -10511,10 +10557,10 @@ immer@1.10.0: resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d" integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg== -immortal-db@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/immortal-db/-/immortal-db-1.0.3.tgz#cd88a1e8ba53646ccc8d7363fd1ee4717ad049c3" - integrity sha512-KWmEx/5KZumg++Yrj/+LH0vERDf1mXR5UFKKhLla0pwd7r/FttKz80ccO1sHyd5+eoSK2wb/N2WCFxWz9O6JKw== +immortal-db@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/immortal-db/-/immortal-db-1.1.0.tgz#b0bbff61262bcbc964952954aeb169462e4b6c5c" + integrity sha512-RwtZT+FEdXrLQeHHKvQQx6SKlQelrcH7x1SLh5lQVcOZFtUNYPjc/ZaU52SsFI/T5rey+VdM87pxVOGKhuZLVw== dependencies: idb-keyval "^3.2.0" js-cookie "^2.2.1" @@ -10539,7 +10585,7 @@ import-fresh@^2.0.0: caller-path "^2.0.0" resolve-from "^3.0.0" -import-fresh@^3.0.0, import-fresh@^3.1.0: +import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== @@ -11011,6 +11057,11 @@ is-natural-number@^4.0.1: resolved "https://registry.yarnpkg.com/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8" integrity sha1-q5124dtM7VHjXeDHLr7PCfc0zeg= +is-negative-zero@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461" + integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE= + is-npm@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" @@ -11084,7 +11135,7 @@ is-plain-object@^3.0.0: resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-3.0.1.tgz#662d92d24c0aa4302407b0d45d21f2251c85f85b" integrity sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g== -is-regex@^1.0.4, is-regex@^1.1.0: +is-regex@^1.0.4, is-regex@^1.1.0, is-regex@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== @@ -11381,7 +11432,7 @@ jest-diff@^24.9.0: jest-get-type "^24.9.0" pretty-format "^24.9.0" -jest-diff@^25.1.0, jest-diff@^25.2.1, jest-diff@^25.5.0: +jest-diff@^25.2.1: version "25.5.0" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.5.0.tgz#1dd26ed64f96667c068cef026b677dfa01afcfa9" integrity sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A== @@ -11513,16 +11564,6 @@ jest-matcher-utils@^24.9.0: jest-get-type "^24.9.0" pretty-format "^24.9.0" -jest-matcher-utils@^25.1.0: - version "25.5.0" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-25.5.0.tgz#fbc98a12d730e5d2453d7f1ed4a4d948e34b7867" - integrity sha512-VWI269+9JS5cpndnpCwm7dy7JtGQT30UHfrnM3mXl22gHGt/b7NkjBqXfbhZ8V4B7ANUsjK18PlSBmG0YH7gjw== - dependencies: - chalk "^3.0.0" - jest-diff "^25.5.0" - jest-get-type "^25.2.6" - pretty-format "^25.5.0" - jest-message-util@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-24.9.0.tgz#527f54a1e380f5e202a8d1149b0ec872f43119e3" @@ -11732,6 +11773,17 @@ jest@24.9.0: import-local "^2.0.0" jest-cli "^24.9.0" +joi@^17.1.1: + version "17.2.1" + resolved "https://registry.yarnpkg.com/joi/-/joi-17.2.1.tgz#e5140fdf07e8fecf9bc977c2832d1bdb1e3f2a0a" + integrity sha512-YT3/4Ln+5YRpacdmfEfrrKh50/kkgX3LgBltjqnlMPIYiZ4hxXZuVJcxmsvxsdeHg9soZfE3qXxHC2tMpCCBOA== + dependencies: + "@hapi/address" "^4.1.0" + "@hapi/formula" "^2.0.0" + "@hapi/hoek" "^9.0.0" + "@hapi/pinpoint" "^2.0.0" + "@hapi/topo" "^5.0.0" + js-base64@^2.1.8: version "2.6.4" resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4" @@ -12328,20 +12380,20 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= -lint-staged@10.2.11: - version "10.2.11" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.2.11.tgz#713c80877f2dc8b609b05bc59020234e766c9720" - integrity sha512-LRRrSogzbixYaZItE2APaS4l2eJMjjf5MbclRZpLJtcQJShcvUzKXsNeZgsLIZ0H0+fg2tL4B59fU9wHIHtFIA== +lint-staged@10.4.0: + version "10.4.0" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.4.0.tgz#d18628f737328e0bbbf87d183f4020930e9a984e" + integrity sha512-uaiX4U5yERUSiIEQc329vhCTDDwUcSvKdRLsNomkYLRzijk3v8V9GWm2Nz0RMVB87VcuzLvtgy6OsjoH++QHIg== dependencies: - chalk "^4.0.0" - cli-truncate "2.1.0" - commander "^5.1.0" - cosmiconfig "^6.0.0" + chalk "^4.1.0" + cli-truncate "^2.1.0" + commander "^6.0.0" + cosmiconfig "^7.0.0" debug "^4.1.1" dedent "^0.7.0" - enquirer "^2.3.5" - execa "^4.0.1" - listr2 "^2.1.0" + enquirer "^2.3.6" + execa "^4.0.3" + listr2 "^2.6.0" log-symbols "^4.0.0" micromatch "^4.0.2" normalize-path "^3.0.0" @@ -12349,10 +12401,10 @@ lint-staged@10.2.11: string-argv "0.3.1" stringify-object "^3.3.0" -listr2@^2.1.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/listr2/-/listr2-2.6.0.tgz#788a3d202978a1b8582062952cbc49272c8e206a" - integrity sha512-nwmqTJYQQ+AsKb4fCXH/6/UmLCEDL1jkRAdSn9M6cEUzoRGrs33YD/3N86gAZQnGZ6hxV18XSdlBcJ1GTmetJA== +listr2@^2.6.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-2.6.2.tgz#4912eb01e1e2dd72ec37f3895a56bf2622d6f36a" + integrity sha512-6x6pKEMs8DSIpA/tixiYY2m/GcbgMplMVmhQAaLFxEtNSKLeWTGjtmU57xvv6QCm2XcqzyNXL/cTSVf4IChCRA== dependencies: chalk "^4.1.0" cli-truncate "^2.1.0" @@ -12739,7 +12791,7 @@ matcher@^3.0.0: dependencies: escape-string-regexp "^4.0.0" -material-ui-search-bar@^1.0.0-beta.13: +material-ui-search-bar@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/material-ui-search-bar/-/material-ui-search-bar-1.0.0.tgz#2652dd5bdc4cb043cffb7144d9c296c120702e62" integrity sha512-lCNuzMLPBVukVAkcnYKLXHneozsuKZREZNOcc8z9S9scXHqxJzhC9hOS3OC3/YJ+NJEB5lZB9zg1gryBaXEu8w== @@ -13178,9 +13230,9 @@ mocha@8.0.1: yargs-unparser "1.6.0" mock-fs@^4.1.0: - version "4.12.0" - resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.12.0.tgz#a5d50b12d2d75e5bec9dac3b67ffe3c41d31ade4" - integrity sha512-/P/HtrlvBxY4o/PzXY9cCNBrdylDNxg7gnrv2sMNxj+UJ2m8jSpl0/A6fuJeNAWr99ZvGWH8XCbE0vmnM5KupQ== + version "4.13.0" + resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.13.0.tgz#31c02263673ec3789f90eb7b6963676aa407a598" + integrity sha512-DD0vOdofJdoaRNtnWcrXe6RQbpHkPPmtqGq14uRX0F8ZKJ5nv89CVTYl/BZdppDxBDaV0hl75htg3abpEWlPZA== moment@2.24.0: version "2.24.0" @@ -13353,9 +13405,9 @@ no-case@^3.0.3: tslib "^1.10.0" node-abi@^2.18.0, node-abi@^2.7.0: - version "2.18.0" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.18.0.tgz#1f5486cfd7d38bd4f5392fa44a4ad4d9a0dffbf4" - integrity sha512-yi05ZoiuNNEbyT/xXfSySZE+yVnQW6fxPZuFbLyS1s6b5Kw3HzV2PHOM4XR+nsjzkHxByK+2Wg+yCQbe35l8dw== + version "2.19.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.19.0.tgz#11614ff22dd64dad3501074bf656e6923539e17a" + integrity sha512-rpKqVe24p9GvMTgtqUXdLR1WQJBGVlkYPU10qHKv9/1i9V/k04MmFLVK2WcHBf1WKKY+ZsdvARPi8F4tfJ4opA== dependencies: semver "^5.4.1" @@ -13675,7 +13727,7 @@ object-hash@^2.0.1: resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.0.3.tgz#d12db044e03cd2ca3d77c0570d87225b02e1e6ea" integrity sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg== -object-inspect@^1.7.0: +object-inspect@^1.7.0, object-inspect@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== @@ -13836,7 +13888,7 @@ open@^6.3.0: dependencies: is-wsl "^1.1.0" -open@^7.0.0, open@^7.0.2, open@^7.1.0: +open@^7.0.0, open@^7.0.2: version "7.1.0" resolved "https://registry.yarnpkg.com/open/-/open-7.1.0.tgz#68865f7d3cb238520fa1225a63cf28bcf8368a1c" integrity sha512-lLPI5KgOwEYCDKXf4np7y1PBEkj7HYIyP2DY8mVDRnx0VIIu6bNrRB0R66TuO7Mack6EnTNLm4uvcl1UoklTpA== @@ -13844,6 +13896,14 @@ open@^7.0.0, open@^7.0.2, open@^7.1.0: is-docker "^2.0.0" is-wsl "^2.1.1" +open@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/open/-/open-7.2.0.tgz#212959bd7b0ce2e8e3676adc76e3cf2f0a2498b4" + integrity sha512-4HeyhxCvBTI5uBePsAdi55C5fmqnWZ2e2MlmvWi5KW5tdH5rxoiv/aMtbeVxKZc3eWkT1GymMnLG8XC4Rq4TDQ== + dependencies: + is-docker "^2.0.0" + is-wsl "^2.1.1" + opencollective-postinstall@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259" @@ -14395,6 +14455,13 @@ polished@3.6.5, polished@^3.3.1: dependencies: "@babel/runtime" "^7.9.2" +polished@3.6.7: + version "3.6.7" + resolved "https://registry.yarnpkg.com/polished/-/polished-3.6.7.tgz#44cbd0047f3187d83db0c479ef0c7d5583af5fb6" + integrity sha512-b4OViUOihwV0icb9PHmWbR+vPqaSzSAEbgLskvb7ANPATVXGiYv/TQFHQo65S53WU9i5EQ1I03YDOJW7K0bmYg== + dependencies: + "@babel/runtime" "^7.9.2" + popper.js@1.16.1-lts: version "1.16.1-lts" resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1-lts.tgz#cf6847b807da3799d80ee3d6d2f90df8a3f50b05" @@ -15152,10 +15219,10 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" -prettier@2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4" - integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg== +prettier@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.2.tgz#3050700dae2e4c8b67c4c3f666cdb8af405e1ce5" + integrity sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg== prettier@^1.14.2: version "1.19.1" @@ -15195,6 +15262,16 @@ pretty-format@^25.2.1, pretty-format@^25.5.0: ansi-styles "^4.0.0" react-is "^16.12.0" +pretty-format@^26.4.2: + version "26.4.2" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.4.2.tgz#d081d032b398e801e2012af2df1214ef75a81237" + integrity sha512-zK6Gd8zDsEiVydOCGLkoBoZuqv8VTiHyAbKznXe/gaph/DAeZOmit9yMfgIz5adIgAMMs5XfoYSwAX3jcCO1tA== + dependencies: + "@jest/types" "^26.3.0" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^16.12.0" + pretty-hrtime@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" @@ -15486,9 +15563,9 @@ querystring@0.2.0, querystring@^0.2.0: integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= querystringify@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" - integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== quick-lru@^1.0.0: version "1.1.0" @@ -15882,7 +15959,7 @@ react-router@5.2.0: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-scripts@^3.4.1: +react-scripts@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-3.4.3.tgz#21de5eb93de41ee92cd0b85b0e1298d0bb2e6c51" integrity sha512-oSnoWmii/iKdeQiwaO6map1lUaZLmG0xIUyb/HwCVFLT7gNbj8JZ9RmpvMCZ4fB98ZUMRfNmp/ft8uy/xD1RLA== @@ -17172,12 +17249,12 @@ shellwords@^0.1.1: integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== side-channel@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.2.tgz#df5d1abadb4e4bf4af1cd8852bf132d2f7876947" - integrity sha512-7rL9YlPHg7Ancea1S96Pa8/QWb4BtXL/TZvS6B8XFetGBeuhAsfmUspK6DokBeZ64+Kj9TCNRD/30pVz1BvQNA== + version "1.0.3" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.3.tgz#cdc46b057550bbab63706210838df5d4c19519c3" + integrity sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g== dependencies: - es-abstract "^1.17.0-next.1" - object-inspect "^1.7.0" + es-abstract "^1.18.0-next.0" + object-inspect "^1.8.0" signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.3" @@ -17929,10 +18006,10 @@ style-loader@^1.0.0: loader-utils "^2.0.0" schema-utils "^2.6.6" -styled-components@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.1.1.tgz#96dfb02a8025794960863b9e8e365e3b6be5518d" - integrity sha512-1ps8ZAYu2Husx+Vz8D+MvXwEwvMwFv+hqqUwhNlDN5ybg6A+3xyW1ECrAgywhvXapNfXiz79jJyU0x22z0FFTg== +styled-components@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.2.0.tgz#6dcb5aa8a629c84b8d5ab34b7167e3e0c6f7ed74" + integrity sha512-9qE8Vgp8C5cpGAIdFaQVAl89Zgx1TDM4Yf4tlHbO9cPijtpSXTMLHy9lmP0lb+yImhgPFb1AmZ1qMUubmg3HLg== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/traverse" "^7.4.5" @@ -18493,15 +18570,6 @@ truffle-interface-adapter@^0.2.5: lodash "^4.17.13" web3 "1.2.1" -truffle@5.1.36: - version "5.1.36" - resolved "https://registry.yarnpkg.com/truffle/-/truffle-5.1.36.tgz#d49c9e0c20558bdee76f442663f81367f62c5559" - integrity sha512-BXfDrRJmxECsHFu1ZHeQNDdv3OA3vmwQ6Wp5m9yaE0swKcHS+gd8sBdxQBoliiAI0xvUAsD62PRGowqFfT1CLg== - dependencies: - app-module-path "^2.2.0" - mocha "8.0.1" - original-require "1.0.1" - truffle@^5.1.21: version "5.1.40" resolved "https://registry.yarnpkg.com/truffle/-/truffle-5.1.40.tgz#f93a147612b3b083354808dd0abbe6723bd04f7f" @@ -18653,9 +18721,9 @@ type@^1.0.1: integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== type@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/type/-/type-2.0.0.tgz#5f16ff6ef2eb44f260494dae271033b29c09a9c3" - integrity sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow== + version "2.1.0" + resolved "https://registry.yarnpkg.com/type/-/type-2.1.0.tgz#9bdc22c648cf8cf86dd23d32336a41cfb6475e3f" + integrity sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA== typechain@^2.0.0: version "2.0.0" @@ -19095,13 +19163,13 @@ w3c-xmlserializer@^1.1.2: webidl-conversions "^4.0.2" xml-name-validator "^3.0.0" -wait-on@5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-5.1.0.tgz#b697f21c6fea0908b9c7ad6ed56ace4736768b66" - integrity sha512-JM0kgaE+V0nCDvSl72iM05W8NDt2E2M56WC5mzR7M+T+k6xjt2yYpyom+xA8RasSunFGzbxIpAXbVzXqtweAnA== +wait-on@5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-5.2.0.tgz#6711e74422523279714a36d52cf49fb47c9d9597" + integrity sha512-U1D9PBgGw2XFc6iZqn45VBubw02VsLwnZWteQ1au4hUVHasTZuFSKRzlTB2dqgLhji16YVI8fgpEpwUdCr8B6g== dependencies: - "@hapi/joi" "^17.1.1" axios "^0.19.2" + joi "^17.1.1" lodash "^4.17.19" minimist "^1.2.5" rxjs "^6.5.5" @@ -20562,7 +20630,7 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^1.7.2: +yaml@^1.10.0, yaml@^1.7.2: version "1.10.0" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==