diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ce638504..86efad8a 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/package.json b/package.json index 505cc361..5058ef7b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "safe-react", - "version": "2.11.0", + "version": "2.11.1", "description": "Allowing crypto users manage funds in a safer way", "website": "https://github.com/gnosis/safe-react#readme", "bugs": { @@ -164,7 +164,7 @@ ] }, "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", @@ -178,7 +178,7 @@ "bignumber.js": "9.0.0", "bnc-onboard": "1.12.0", "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,19 +190,19 @@ "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", + "open": "^7.2.0", "polished": "3.6.6", "qrcode.react": "1.0.0", "query-string": "6.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", @@ -255,15 +255,15 @@ "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", @@ -272,9 +272,9 @@ "prettier": "2.1.1", "react-app-rewired": "^2.1.6", "react-docgen-typescript-loader": "^3.7.2", - "truffle": "5.1.36", + "truffle": "5.1.41", "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 965ab61f..d55fa0bc 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 00000000..7becafe5 --- /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 30df6668..535a50bd 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`, - - [`@media (min-width: ${screenSm}px)`]: { - flexDirection: 'row', + close: { + height: lg, + width: lg, + fill: secondaryText, }, - }, - address: { - marginLeft: sm, - marginRight: sm, - maxWidth: '70%', - overflowWrap: 'break-word', - - [`@media (min-width: ${screenSm}px)`]: { - maxWidth: 'none', + 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', + }, + }, + address: { + marginLeft: sm, + marginRight: sm, + maxWidth: '70%', + overflowWrap: 'break-word', + + [`@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 7d889c67..ee90992a 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 778e0dad..00868482 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 fb9bd465..5636fbd6 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 82df813f..9d3c2f3f 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 f59c3b0e..673b994d 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 20bd8c89..4ac72c9e 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 adec3dac..f2b7f0cd 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 efe0f070..23ecd436 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/Table/TableHead.tsx b/src/components/Table/TableHead.tsx index 6d341fa6..94e8b1c1 100644 --- a/src/components/Table/TableHead.tsx +++ b/src/components/Table/TableHead.tsx @@ -8,7 +8,7 @@ interface CellWidth { maxWidth: string } -export const cellWidth = (width: string | number): CellWidth | undefined => { +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 732027d0..34554330 100644 --- a/src/logic/addressBook/model/addressBook.ts +++ b/src/logic/addressBook/model/addressBook.ts @@ -6,12 +6,10 @@ export interface AddressBookEntryProps { isOwner: boolean } -export type AddressBookEntryRecord = RecordOf - export const makeAddressBookEntry = Record({ address: '', name: '', isOwner: false, }) -export type AddressBookEntry = RecordOf +export type AddressBookEntryRecord = RecordOf diff --git a/src/logic/addressBook/store/reducer/addressBook.ts b/src/logic/addressBook/store/reducer/addressBook.ts index ea1a46d7..e1ce17d6 100644 --- a/src/logic/addressBook/store/reducer/addressBook.ts +++ b/src/logic/addressBook/store/reducer/addressBook.ts @@ -1,27 +1,31 @@ import { List, Map } from 'immutable' import { handleActions } from 'redux-actions' -import { AddressBookEntry, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' +import { + AddressBookEntryRecord, + AddressBookEntryProps, + makeAddressBookEntry, +} from 'src/logic/addressBook/model/addressBook' import { ADD_ADDRESS_BOOK } from 'src/logic/addressBook/store/actions/addAddressBook' 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 { getAddressesListFromSafeAddressBook } from 'src/logic/addressBook/utils' import { sameAddress } from 'src/logic/wallets/ethAddresses' import { checksumAddress } from 'src/utils/checksumAddress' export const ADDRESS_BOOK_REDUCER_ID = 'addressBook' -export type AddressBookCollection = List +export type AddressBookCollection = List export type AddressBookState = Map> -export const buildAddressBook = (storedAdbk) => { - let addressBookBuilt = Map([]) +export const buildAddressBook = (storedAdbk: AddressBookEntryProps[]): Map => { + let addressBookBuilt: Map = Map([]) Object.entries(storedAdbk).forEach((adbkProps: any) => { const safeAddress = checksumAddress(adbkProps[0]) - const adbkRecords = adbkProps[1].map(makeAddressBookEntry) + const adbkRecords: AddressBookEntryRecord[] = adbkProps[1].map(makeAddressBookEntry) const adbkSafeEntries = List(adbkRecords) addressBookBuilt = addressBookBuilt.set(safeAddress, adbkSafeEntries) }) @@ -55,7 +59,7 @@ export default handleActions( const safeAddressBook = state.getIn(['addressBook', safeAddress]) if (safeAddressBook) { - const adbkAddressList = getAddressesListFromAdbk(safeAddressBook) + const adbkAddressList = getAddressesListFromSafeAddressBook(safeAddressBook) const found = adbkAddressList.includes(entry.address) if (!found) { const updatedSafeAdbkList = safeAddressBook.push(entry) diff --git a/src/logic/addressBook/store/selectors/index.ts b/src/logic/addressBook/store/selectors/index.ts index 973b40a6..b03fbdf1 100644 --- a/src/logic/addressBook/store/selectors/index.ts +++ b/src/logic/addressBook/store/selectors/index.ts @@ -4,6 +4,7 @@ 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 { AddressBookEntryRecord } from 'src/logic/addressBook/model/addressBook' import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' export const addressBookMapSelector = (state: AppReduxState): AddressBookMap => @@ -13,8 +14,8 @@ export const getAddressBook = createSelector( addressBookMapSelector, safeParamAddressFromStateSelector, (addressBook, safeAddress) => { - let result = List([]) - if (addressBook) { + let result: List = List([]) + if (addressBook && safeAddress) { result = addressBook.get(safeAddress, List()) } return result 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 00000000..f9574c29 --- /dev/null +++ b/src/logic/addressBook/utils/__tests__/addressBookUtils.test.ts @@ -0,0 +1,102 @@ +import { Map, List } from 'immutable' +import { + getAddressBookFromStorage, + getAddressesListFromSafeAddressBook, + getNameFromSafeAddressBook, + getOwnersWithNameFromAddressBook, + saveAddressBook, +} from 'src/logic/addressBook/utils/index' +import { buildAddressBook } from 'src/logic/addressBook/store/reducer/addressBook' +import { AddressBookEntryRecord, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' + +const getMockAddressBookEntry = ( + address: string, + name: string = 'test', + isOwner: boolean = false, +): AddressBookEntryRecord => + makeAddressBookEntry({ + 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 = List([entry1, entry2, entry3]) + const expectedResult = [entry1.address, entry2.address, entry3.address] + + // when + const result = getAddressesListFromSafeAddressBook(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 = List([entry1, entry2, entry3]) + const expectedResult = entry2.name + + // when + const result = getNameFromSafeAddressBook(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 = List([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) + }) +}) + +describe('saveAddressBook', () => { + const safeAddress1 = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf' + const safeAddress2 = '0x344B941b1aAE2e4Be73987212FC4741687Bf0503' + const entry1 = getMockAddressBookEntry('123456', 'test1') + const entry2 = getMockAddressBookEntry('78910', 'test2') + const entry3 = getMockAddressBookEntry('4781321', 'test3') + it('It should save a given addressBook to the localStorage', async () => { + // given + const addressBook = Map({ [safeAddress1]: List([entry1, entry2]), [safeAddress2]: List([entry3]) }) + + // when + // @ts-ignore + await saveAddressBook(addressBook) + const storedAdBk = await getAddressBookFromStorage() + // @ts-ignore + let result = buildAddressBook(storedAdBk) + + // then + expect(result).toStrictEqual(addressBook) + }) +}) diff --git a/src/logic/addressBook/utils/index.ts b/src/logic/addressBook/utils/index.ts index 22cd11e8..8b636892 100644 --- a/src/logic/addressBook/utils/index.ts +++ b/src/logic/addressBook/utils/index.ts @@ -1,27 +1,28 @@ import { List } from 'immutable' import { loadFromStorage, saveToStorage } from 'src/utils/storage' -import { AddressBookEntryProps } from './../model/addressBook' +import { AddressBookEntryRecord, AddressBookEntryProps } from '../model/addressBook' import { SafeOwner } from 'src/logic/safe/store/models/safe' +import { AddressBookCollection } from '../store/reducer/addressBook' +import { AddressBookMap } from '../store/reducer/types/addressBook' const ADDRESS_BOOK_STORAGE_KEY = 'ADDRESS_BOOK_STORAGE_KEY' export const getAddressBookFromStorage = async (): Promise | undefined> => { - const data = await loadFromStorage>(ADDRESS_BOOK_STORAGE_KEY) - - return data + return await loadFromStorage>(ADDRESS_BOOK_STORAGE_KEY) } -export const saveAddressBook = async (addressBook) => { +export const saveAddressBook = async (addressBook: AddressBookMap): Promise => { try { - await saveToStorage(ADDRESS_BOOK_STORAGE_KEY, addressBook.toJSON()) + await saveToStorage(ADDRESS_BOOK_STORAGE_KEY, addressBook.toJS()) } catch (err) { console.error('Error storing addressBook in localstorage', err) } } -export const getAddressesListFromAdbk = (addressBook) => Array.from(addressBook).map((entry: any) => entry.address) +export const getAddressesListFromSafeAddressBook = (addressBook: AddressBookCollection): string[] => + Array.from(addressBook).map((entry: AddressBookEntryRecord) => entry.address) -export const getNameFromAdbk = (addressBook, userAddress) => { +export const getNameFromSafeAddressBook = (addressBook: AddressBookCollection, userAddress: string): string | null => { const entry = addressBook.find((addressBookItem) => addressBookItem.address === userAddress) if (entry) { return entry.name @@ -30,18 +31,17 @@ export const getNameFromAdbk = (addressBook, userAddress) => { } export const getOwnersWithNameFromAddressBook = ( - addressBook: AddressBookEntryProps, + addressBook: AddressBookCollection, ownerList: List, ): List | [] => { if (!ownerList) { return [] } - const ownersListWithAdbkNames = ownerList.map((owner) => { - const ownerName = getNameFromAdbk(addressBook, owner.address) + return ownerList.map((owner) => { + const ownerName = getNameFromSafeAddressBook(addressBook, owner.address) return { address: owner.address, name: ownerName || owner.name, } }) - return ownersListWithAdbkNames } diff --git a/src/logic/contractInteraction/sources/ABIService/index.ts b/src/logic/contractInteraction/sources/ABIService/index.ts index c9b62c5f..c9dc15b7 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 95602f29..2d48979b 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/currencyValues/__tests__/fetchSafeTokens.test.ts b/src/logic/currencyValues/__tests__/fetchSafeTokens.test.ts new file mode 100644 index 00000000..246a2664 --- /dev/null +++ b/src/logic/currencyValues/__tests__/fetchSafeTokens.test.ts @@ -0,0 +1,53 @@ +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' + beforeEach(async () => { + 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) + + // then + expect(result).toStrictEqual(expectedResult) + expect(axios.get).toHaveBeenCalled() + expect(axios.get).toBeCalledWith(`${apiUrl}safes/${safeAddress}/balances/usd/`, { params: { limit: 3000 } }) + }) +}) diff --git a/src/logic/currencyValues/api/fetchTokenCurrenciesBalances.ts b/src/logic/currencyValues/api/fetchTokenCurrenciesBalances.ts index 3ed8fa9d..c2804708 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 b109f93e..00000000 --- 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 00000000..42898646 --- /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 e84800e9..b7390166 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 ea3f13ca..3b0774a0 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 92445da4..a361f593 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 { selectedCurrency } = action.payload - const currencyValuesWithoutBalances: Map = currencyValues.map((currencyValue) => { - const currencyRate: number = currencyValue.get('currencyRate') - const selectedCurrency: AVAILABLE_CURRENCIES = currencyValue.get('selectedCurrency') - return { - currencyRate, - selectedCurrency, - } - }) - - 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 141654c1..b1789174 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 6a265fc8..ba716349 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 7bac5b2a..16d9a8a8 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 b3dde217..cd3a42a0 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 c5be51ca..00000000 --- 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 228bfc9e..236da1b9 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 cbfa1a14..f3da7fc0 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 e9abbb43..2a19734f 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 b49b2aa1..fd815385 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 32003528..2932d16a 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 048adb85..9d6e15df 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(() => { diff --git a/src/logic/safe/hooks/useSafeScheduledUpdates.tsx b/src/logic/safe/hooks/useSafeScheduledUpdates.tsx index d2415eb3..66c474a2 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 00000000..4076f002 --- /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/allTransactions/loadAllTransactions.ts b/src/logic/safe/store/actions/allTransactions/loadAllTransactions.ts index 25148bdc..d98b59b6 100644 --- a/src/logic/safe/store/actions/allTransactions/loadAllTransactions.ts +++ b/src/logic/safe/store/actions/allTransactions/loadAllTransactions.ts @@ -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/createTransaction.ts b/src/logic/safe/store/actions/createTransaction.ts index 6f0424a7..76ded3a5 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 RejectEventHandler = () => void const createTransaction = ( { @@ -124,6 +126,7 @@ const createTransaction = ( origin = null, }: CreateTransactionArgs, onUserConfirm?: ConfirmEventHandler, + onUserReject?: RejectEventHandler, ): CreateTransactionAction => async (dispatch: Dispatch, getState: () => AppReduxState): Promise => { const state = getState() @@ -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,21 @@ const createTransaction = ( dispatch(closeSnackbarAction({ key: pendingExecutionKey })) removeTxFromStore(mockedTx, safeAddress, dispatch, state) console.error('Tx error: ', error) + + // Different wallets return different error messages in this case. This is an assumption that if + // error message includes "user" word, the tx was rejected by user + + let errorIncludesUserWord = false + if (typeof error === 'string') { + errorIncludesUserWord = (error as string).includes('User') || (error as string).includes('user') + } + if (error.message) { + errorIncludesUserWord = error.message.includes('User') || error.message.includes('user') + } + + if (errorIncludesUserWord) { + onUserReject?.() + } }) .then(async (receipt) => { if (pendingExecutionKey) { diff --git a/src/logic/safe/store/actions/fetchSafe.ts b/src/logic/safe/store/actions/fetchSafe.ts index bd95676e..6821a997 100644 --- a/src/logic/safe/store/actions/fetchSafe.ts +++ b/src/logic/safe/store/actions/fetchSafe.ts @@ -18,7 +18,7 @@ import { Action, Dispatch } from 'redux' import { SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts' import { AppReduxState } from 'src/store' -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(), diff --git a/src/logic/safe/store/actions/loadSafesFromStorage.ts b/src/logic/safe/store/actions/loadSafesFromStorage.ts index e4a36dd9..de752b0e 100644 --- a/src/logic/safe/store/actions/loadSafesFromStorage.ts +++ b/src/logic/safe/store/actions/loadSafesFromStorage.ts @@ -1,15 +1,15 @@ -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) => { diff --git a/src/logic/safe/store/actions/processTransaction.ts b/src/logic/safe/store/actions/processTransaction.ts index 0791584c..ffe0b2d2 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) @@ -85,6 +84,8 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres 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 d413879c..f4907bed 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 07161d80..09d00a05 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 2b3b2e49..ec79aa23 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 67f9b4ca..89d53d95 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/transactionHelpers.ts b/src/logic/safe/store/actions/transactions/utils/transactionHelpers.ts index 5349d7b6..9ac91d11 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/updateActiveTokens.ts b/src/logic/safe/store/actions/updateActiveTokens.ts index 922c020f..851fef1f 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 { Dispatch } from 'redux' // 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,7 +12,7 @@ import updateSafe from './updateSafe' // }, // }) -const updateActiveTokens = (safeAddress, activeTokens) => async (dispatch) => { +const updateActiveTokens = (safeAddress: string, activeTokens: Set) => (dispatch: Dispatch): void => { dispatch(updateSafe({ address: safeAddress, activeTokens })) } diff --git a/src/logic/safe/store/actions/updateBlacklistedTokens.ts b/src/logic/safe/store/actions/updateBlacklistedTokens.ts index 1337184f..b1b1039e 100644 --- a/src/logic/safe/store/actions/updateBlacklistedTokens.ts +++ b/src/logic/safe/store/actions/updateBlacklistedTokens.ts @@ -1,6 +1,8 @@ import updateSafe from './updateSafe' +import { Dispatch } from 'redux' +import { Set } from 'immutable' -const updateBlacklistedTokens = (safeAddress, blacklistedTokens) => async (dispatch) => { +const updateBlacklistedTokens = (safeAddress: string, blacklistedTokens: Set) => (dispatch: Dispatch): void => { dispatch(updateSafe({ address: safeAddress, blacklistedTokens })) } diff --git a/src/logic/safe/store/actions/utils.ts b/src/logic/safe/store/actions/utils.ts index 2dee2f09..74b59b0d 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 c30f5fa1..a9fbf648 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 } @@ -104,7 +104,7 @@ 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 viewedSafes = state.currentSession['viewedSafes'] const recurringUser = viewedSafes?.includes(safeAddress) const newIncomingTransactions = incomingTransactions.filter((tx) => tx.blockNumber > latestIncomingTxBlock) diff --git a/src/logic/safe/store/models/types/transaction.ts b/src/logic/safe/store/models/types/transaction.ts index 150233b8..1ecd114a 100644 --- a/src/logic/safe/store/models/types/transaction.ts +++ b/src/logic/safe/store/models/types/transaction.ts @@ -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/safe.ts b/src/logic/safe/store/reducer/safe.ts index 7a1e9b4b..becddb83 100644 --- a/src/logic/safe/store/reducer/safe.ts +++ b/src/logic/safe/store/reducer/safe.ts @@ -37,7 +37,7 @@ export const buildSafe = (storedSafe: SafeRecordProps): SafeRecordProps => { blacklistedTokens, activeAssets, blacklistedAssets, - latestIncomingTxBlock: null, + latestIncomingTxBlock: 0, modules: null, } } @@ -65,7 +65,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,7 +77,7 @@ 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)) diff --git a/src/logic/safe/store/selectors/allTransactions.ts b/src/logic/safe/store/selectors/allTransactions.ts index 211a6def..e8729e61 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 9c6d3f9c..df9d6de6 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 00000000..619df7aa --- /dev/null +++ b/src/logic/safe/store/tests/safe.balances.test.ts @@ -0,0 +1,62 @@ +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(updateActiveTokens(safeAddress, Set([token.address]))) + store.dispatch(updateSafe({ address: safeAddress, balances })) + + const safe = safesMapSelector(store.getState()).get(safeAddress) + //@ts-ignore + const balanceResult = safe.get('balances').get(token.address) + //@ts-ignore + 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) + //@ts-ignore + 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 ff05bb0e..85ea6568 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 9a54fcb7..91015c62 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 6e6b19cb..c7d78e54 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/ethSigner.ts b/src/logic/safe/transactions/offchainSigner/ethSigner.ts index 005a983e..08f0dc86 100644 --- a/src/logic/safe/transactions/offchainSigner/ethSigner.ts +++ b/src/logic/safe/transactions/offchainSigner/ethSigner.ts @@ -39,7 +39,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/utils/safeStorage.ts b/src/logic/safe/utils/safeStorage.ts index c782db82..3ef09f7c 100644 --- a/src/logic/safe/utils/safeStorage.ts +++ b/src/logic/safe/utils/safeStorage.ts @@ -2,11 +2,18 @@ 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' +type StoredSafes = Record + +export const loadStoredSafes = async (): Promise => { + const safes = await loadFromStorage(SAFES_KEY) + + return safes +} + export const getSafeName = async (safeAddress: string): Promise => { - const safes = await loadFromStorage(SAFES_KEY) + const safes = await loadStoredSafes() if (!safes) { return undefined } @@ -23,9 +30,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 f0ab2736..38c47e69 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 } diff --git a/src/logic/tokens/store/actions/fetchSafeTokens.ts b/src/logic/tokens/store/actions/fetchSafeTokens.ts index 79df1067..608256c6 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 a2156698..5c5bde49 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 fc072be2..933059aa 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 94fc60c9..8dd5a0d6 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 00000000..7afee4ed --- /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 493aa1e0..48134bdb 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 17e06a13..c3c7b0f4 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 0dedb32d..fa938cf6 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 00000000..5191d21b --- /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 00000000..441a3de1 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 4ca7235e..b4a03bbf 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 e3325549..acd1cd94 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 57f38276..80fdfb9c 100644 --- a/src/routes/load/components/OwnerList/index.tsx +++ b/src/routes/load/components/OwnerList/index.tsx @@ -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' @@ -79,7 +79,7 @@ const calculateSafeValues = (owners, threshold, values) => { } const OwnerListComponent = (props) => { - const [owners, setOwners] = useState([]) + const [owners, setOwners] = useState([]) const { classes, updateInitialProps, values } = props useEffect(() => { @@ -132,7 +132,7 @@ const OwnerListComponent = (props) => { placeholder="Owner Name*" text="Owner Name" type="text" - validate={required} + validate={composeValidators(required, minMaxLength(1, 50))} testId={`load-safe-owner-name-${index}`} /> @@ -156,12 +156,15 @@ const OwnerListComponent = (props) => { 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/container/Load.tsx b/src/routes/load/container/Load.tsx index 689f460e..49809c26 100644 --- a/src/routes/load/container/Load.tsx +++ b/src/routes/load/container/Load.tsx @@ -6,12 +6,11 @@ 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' @@ -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 diff --git a/src/routes/open/components/SafeNameForm/index.tsx b/src/routes/open/components/SafeNameForm/index.tsx index 01d6e8f4..82ebd944 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 58d2e204..da0e67c4 100644 --- a/src/routes/open/components/SafeOwnersConfirmationsForm/index.tsx +++ b/src/routes/open/components/SafeOwnersConfirmationsForm/index.tsx @@ -16,7 +16,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' @@ -129,7 +136,7 @@ const SafeOwners = (props) => { placeholder="Owner Name*" text="Owner Name" type="text" - validate={required} + validate={composeValidators(required, minMaxLength(1, 50))} testId={`create-safe-owner-name-field-${index}`} /> @@ -138,6 +145,8 @@ const SafeOwners = (props) => { fieldMutator={(val) => { form.mutators.setValue(addressName, val) }} + // eslint-disable-next-line + // @ts-ignore inputAdornment={ noErrorsOn(addressName, errors) && { endAdornment: ( @@ -217,18 +226,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/container/Open.tsx b/src/routes/open/container/Open.tsx index e1f95690..3865e166 100644 --- a/src/routes/open/container/Open.tsx +++ b/src/routes/open/container/Open.tsx @@ -161,7 +161,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 +177,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() diff --git a/src/routes/opening/index.tsx b/src/routes/opening/index.tsx index 3b7f3d5b..efe99000 100644 --- a/src/routes/opening/index.tsx +++ b/src/routes/opening/index.tsx @@ -105,7 +105,7 @@ const BackButton = styled(Button)` const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, provider, submittedPromise }: any) => { const [loading, setLoading] = useState(true) const [stepIndex, setStepIndex] = useState(0) - const [safeCreationTxHash, setSafeCreationTxHash] = useState() + const [safeCreationTxHash, setSafeCreationTxHash] = useState('') const [createdSafeAddress, setCreatedSafeAddress] = useState() const [error, setError] = useState(false) @@ -242,7 +242,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 +283,9 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, provider return } - awaitUntilSafeIsDeployed() + if (typeof safeCreationTxHash === 'string') { + awaitUntilSafeIsDeployed(safeCreationTxHash) + } return () => { clearInterval(interval) @@ -294,7 +296,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 974be3e4..628a7af0 100644 --- a/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.tsx +++ b/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.tsx @@ -20,7 +20,7 @@ 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 { getAddressesListFromSafeAddressBook } 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' @@ -43,7 +43,7 @@ const CreateEditEntryModalComponent = ({ } const addressBook = useSelector(getAddressBook) - const addressBookAddressesList = getAddressesListFromAdbk(addressBook) + const addressBookAddressesList = getAddressesListFromSafeAddressBook(addressBook) const entryDoesntExist = uniqueAddress(addressBookAddressesList) const formMutators = { diff --git a/src/routes/safe/components/AddressBook/index.tsx b/src/routes/safe/components/AddressBook/index.tsx index 8239f4a1..9362b09e 100644 --- a/src/routes/safe/components/AddressBook/index.tsx +++ b/src/routes/safe/components/AddressBook/index.tsx @@ -50,7 +50,7 @@ const AddressBookTable = ({ classes }) => { const safesList = useSelector(safesListSelector) const entryAddressToEditOrCreateNew = useSelector(addressBookQueryParamsSelector) const addressBook = useSelector(getAddressBook) - const [selectedEntry, setSelectedEntry] = useState(null) + const [selectedEntry, setSelectedEntry] = useState(null) const [editCreateEntryModalOpen, setEditCreateEntryModalOpen] = useState(false) const [deleteEntryModalOpen, setDeleteEntryModalOpen] = useState(false) const [sendFundsModalOpen, setSendFundsModalOpen] = useState(false) @@ -70,7 +70,7 @@ const AddressBookTable = ({ classes }) => { if (entryAddressToEditOrCreateNew) { const checksumEntryAdd = checksumAddress(entryAddressToEditOrCreateNew) const key = addressBook.findKey((entry) => entry.address === checksumEntryAdd) - if (key >= 0) { + if (key && key >= 0) { // Edit old entry const value = addressBook.get(key) setSelectedEntry({ entry: value, index: key }) diff --git a/src/routes/safe/components/AllTransactions/index.tsx b/src/routes/safe/components/AllTransactions/index.tsx index 816819e4..955f5ecf 100644 --- a/src/routes/safe/components/AllTransactions/index.tsx +++ b/src/routes/safe/components/AllTransactions/index.tsx @@ -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 cc1ba29a..929a3fd2 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 dcd6d129..b79a888f 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 a88b3f4a..292f522f 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 31ddeaef..9d021a81 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 5c75f847..c9e279db 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` @@ -71,6 +71,7 @@ type OwnProps = { ethBalance: string onCancel: () => void onUserConfirm: (safeTxHash: string) => void + onUserTxReject: () => void onClose: () => void } @@ -84,7 +85,8 @@ const ConfirmTransactionModal = ({ onCancel, onUserConfirm, onClose, -}: OwnProps): React.ReactElement => { + onUserTxReject, +}: OwnProps): React.ReactElement | null => { const dispatch = useDispatch() if (!isOpen) { return null @@ -111,6 +113,7 @@ const ConfirmTransactionModal = ({ navigateToTransactionsTab: false, }, handleUserConfirmation, + onUserTxReject, ), ) onClose() @@ -133,27 +136,25 @@ const ConfirmTransactionModal = ({ <> - {txs.map((tx, index) => { - return ( - - } title={`Transaction ${index + 1}`}> - -
- Value -
- Ether - {humanReadableValue(tx.value, 18)} ETH -
+ {txs.map((tx, index) => ( + + } title={`Transaction ${index + 1}`}> + +
+ Value +
+ Ether + {humanReadableValue(tx.value, 18)} ETH
-
- Data (hex encoded)* - {tx.data} -
- - - - ) - })} +
+
+ Data (hex encoded)* + {tx.data} +
+
+
+
+ ))} ) diff --git a/src/routes/safe/components/Apps/components/ManageApps.tsx b/src/routes/safe/components/Apps/components/ManageApps.tsx index 9d0de55d..26ccb494 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 dc08c919..fc48a716 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 59cab2d4..aa165f4d 100644 --- a/src/routes/safe/components/Apps/hooks/useIframeMessageHandler.ts +++ b/src/routes/safe/components/Apps/hooks/useIframeMessageHandler.ts @@ -8,6 +8,7 @@ import { INTERFACE_MESSAGES, RequestId, Transaction, + LowercaseNetworks, } from '@gnosis.pm/safe-apps-sdk' import { useDispatch, useSelector } from 'react-redux' import { useEffect, useCallback, MutableRefObject } from 'react' @@ -44,7 +45,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 +61,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,7 +78,9 @@ 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 } @@ -85,9 +88,9 @@ const useIframeMessageHandler = ( const message = { messageId: INTERFACE_MESSAGES.ON_SAFE_INFO, data: { - safeAddress, - network: network, - ethBalance, + safeAddress: safeAddress as string, + network: network.toLowerCase() as LowercaseNetworks, + ethBalance: ethBalance as string, }, } @@ -104,7 +107,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 d54dd1b0..3d7acdd1 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' @@ -63,7 +64,7 @@ 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) @@ -101,6 +102,13 @@ const Apps = (): React.ReactElement => { ) } + const onUserTxReject = () => { + sendMessageToIframe( + { messageId: INTERFACE_MESSAGES.TRANSACTION_REJECTED, data: {} }, + confirmTransactionModal.requestId, + ) + } + const onSelectApp = useCallback( (appId) => { if (selectedAppId === appId) { @@ -146,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 ( @@ -199,14 +207,15 @@ 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 4608012b..db8b49d1 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 187f3be0..00559f92 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' @@ -27,11 +27,11 @@ export const staticAppsList: Array<{ url: string; disabled: boolean }> = [ // Compound { url: `${gnosisAppsUrl}/compound`, disabled: false }, // Idle - { url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmTpbdjjMxgjvuGDN62tLEF3qPSk8GBLAYGR9fMM6ftWKi`, disabled: false }, + { url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmZ3oug89a3BaVqdJrJEA8CKmLF4M8snuAnphR6z1yq8V8`, disabled: false }, // request { url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmTBBaiDQyGa17DJ7DdviyHbc51fTVgf6Z5PW5w2YUTkgR`, disabled: false }, // Sablier - { url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmabPEk7g4zaytFefp6fE4nz8f85QMJoWmRQQZypvJViNG`, disabled: false }, + { url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmeHa5CS6eAMRvQfTBwWfcXKrXZ7itZTpWSM6625ZZ522N`, disabled: false }, // Synthetix { url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmXLxxczMH4MBEYDeeN9zoiHDzVkeBmB5rBjA3UniPEFcA`, disabled: false }, // OpenZeppelin @@ -62,8 +62,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 d9b1bc63..82bfc97a 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 } from 'react' +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 { 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,28 +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 @@ -61,16 +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 ( @@ -102,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 94e0781a..5df6bf51 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 d3a88e23..c06b7227 100644 --- a/src/routes/safe/components/Balances/Collectibles/index.tsx +++ b/src/routes/safe/components/Balances/Collectibles/index.tsx @@ -79,7 +79,7 @@ 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() diff --git a/src/routes/safe/components/Balances/SendModal/SafeInfo/index.tsx b/src/routes/safe/components/Balances/SendModal/SafeInfo/index.tsx index 5cd13863..d2b49300 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 6079daef..74e41694 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx @@ -22,9 +22,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?: string }> | null, ) => void - setIsValidAddress: (valid?: boolean) => void + setIsValidAddress: (valid: boolean) => void } const useStyles = makeStyles(styles) @@ -157,7 +157,7 @@ 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 }) } @@ -212,6 +212,11 @@ const AddressBookInput = ({ )} renderOption={(adbkEntry) => { const { address, name } = adbkEntry + + if (!address) { + return + } + return (
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 596eff59..19d4ff90 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ChooseTxType/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ChooseTxType/index.tsx @@ -62,8 +62,8 @@ const useStyles = makeStyles({ const ChooseTxType = ({ onClose, recipientAddress, setActiveScreen }) => { const classes = useStyles() - const { featuresEnabled } = useSelector(safeSelector) - const erc721Enabled = featuresEnabled.includes('ERC721') + const { featuresEnabled } = useSelector(safeSelector) || {} + const erc721Enabled = featuresEnabled?.includes('ERC721') const [disableContractInteraction, setDisableContractInteraction] = React.useState(!!recipientAddress) React.useEffect(() => { 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 c51b58d0..3a1f20d0 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 48482ae7..afda6213 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 b804ea13..82ebacdf 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 bbad4ed0..9c8af66c 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 70cc2d79..47b3b6c6 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 99c00ff8..5d2d1c10 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 c27dfa5f..b47b8f2f 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 c481f080..99bbd100 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 5d8048ef..31d28b3f 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 14fdac14..9b18ef70 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 c6a41b91..52922fd0 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 a87afba9..1b7cb68d 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 b6de2579..0ee764bb 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 637085a7..55fab95b 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/index.tsx @@ -21,7 +21,7 @@ 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 { getNameFromSafeAddressBook } 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' @@ -53,7 +53,7 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel name: '', }) const [pristine, setPristine] = useState(true) - const [isValidAddress, setIsValidAddress] = useState(true) + const [isValidAddress, setIsValidAddress] = useState(false) React.useMemo(() => { if (selectedEntry === null && pristine) { @@ -97,10 +97,10 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel if (scannedAddress.startsWith('ethereum:')) { scannedAddress = scannedAddress.replace('ethereum:', '') } - const scannedName = addressBook ? getNameFromAdbk(addressBook, scannedAddress) : '' + const scannedName = addressBook ? getNameFromSafeAddressBook(addressBook, scannedAddress) : '' mutators.setRecipient(scannedAddress) setSelectedEntry({ - name: scannedName, + name: scannedName || '', address: scannedAddress, }) closeQrModal() @@ -129,7 +129,7 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
{ if (e.keyCode !== 9) { - setSelectedEntry(null) + setSelectedEntry({ address: '', name: 'string' }) } }} role="listbox" @@ -150,7 +150,7 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel setSelectedEntry(null)} + onClick={() => setSelectedEntry({ address: '', name: 'string' })} weight="bolder" > {selectedEntry.name} @@ -158,7 +158,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/SendFunds/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx index 8b78fc79..11b293d8 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx @@ -26,7 +26,7 @@ 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 { getNameFromSafeAddressBook } 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,7 +48,25 @@ 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) @@ -58,7 +76,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT }) const [pristine, setPristine] = useState(true) - const [isValidAddress, setIsValidAddress] = useState(true) + const [isValidAddress, setIsValidAddress] = useState(false) React.useMemo(() => { if (selectedEntry === null && pristine) { @@ -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 ? getNameFromSafeAddressBook(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 8307f069..00000000 --- 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 6b4e59c3..a25c8762 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 3f57ffa3..c5053040 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 6c8f0b35..793edae9 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/TokenList/index.tsx b/src/routes/safe/components/Balances/Tokens/screens/TokenList/index.tsx index 819f7284..73629ad8 100644 --- a/src/routes/safe/components/Balances/Tokens/screens/TokenList/index.tsx +++ b/src/routes/safe/components/Balances/Tokens/screens/TokenList/index.tsx @@ -1,9 +1,9 @@ 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 { FixedSizeList } from 'react-window' @@ -17,10 +17,15 @@ 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 { useEffect, useState } from 'react' +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 +33,128 @@ 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 +const useStyles = makeStyles(styles) -class Tokens extends React.Component { - renderCount = 0 +type Props = { + setActiveScreen: (newScreen: string) => void + tokens: List + activeTokens: List + blacklistedTokens: Set + safeAddress: string +} - state = { - filter: '', - activeTokensAddresses: Set([]), - initialActiveTokensAddresses: Set([]), - blacklistedTokensAddresses: Set([]), - activeTokensCalculated: false, - blacklistedTokensCalculated: false, - } +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() - 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, - } + useEffect(() => { + return () => { + dispatch(updateActiveTokens(safeAddress, activeTokensAddresses)) + dispatch(updateBlacklistedTokens(safeAddress, blacklistedTokensAddresses)) } + }, [dispatch, safeAddress, activeTokensAddresses, blacklistedTokensAddresses]) - if (!prevState.blacklistedTokensCalculated) { - const { blacklistedTokens } = nextProps - - return { - blacklistedTokensAddresses: blacklistedTokens, - blacklistedTokensCalculated: true, - } - } - - return null + const searchClasses = { + input: classes.searchInput, + root: classes.searchRoot, + iconButton: classes.searchIcon, + searchContainer: classes.searchContainer, } - componentWillUnmount() { - const { activeTokensAddresses, blacklistedTokensAddresses } = this.state - const { safeAddress, updateActiveTokens, updateBlacklistedTokens } = this.props - - updateActiveTokens(safeAddress, activeTokensAddresses) - updateBlacklistedTokens(safeAddress, blacklistedTokensAddresses) - } - - 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 } - }) - } - - createItemData = (tokens, activeTokensAddresses) => ({ - tokens, - activeTokensAddresses, - onSwitch: this.onSwitch, - }) - - getItemKey = (index, { tokens }) => { - const token = tokens.get(index) - - return token.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 onSwitch = (token: Token) => () => { + if (activeTokensAddresses.has(token.address)) { + const newTokens = activeTokensAddresses.remove(token.address) + setActiveTokensAddresses(newTokens) + setBlacklistedTokensAddresses(blacklistedTokensAddresses.add(token.address)) + } else { + setActiveTokensAddresses(activeTokensAddresses.add(token.address)) + setBlacklistedTokensAddresses(blacklistedTokensAddresses.remove(token.address)) } - const switchToAddCustomTokenScreen = () => setActiveScreen('addCustomToken') - - const filteredTokens = filterBy(filter, tokens) - const itemData = this.createItemData(filteredTokens, activeTokensAddresses) - - return ( - <> - - - - } - value={filter} - /> - - - - - - - - {!tokens.size && ( - - - - )} - {tokens.size > 0 && ( - - - {TokenRow} - - - )} - - ) } + + const createItemData = ( + tokens: List, + activeTokensAddresses: Set, + ): { tokens: List; activeTokensAddresses: Set; onSwitch: (token: Token) => void } => { + return { + tokens, + activeTokensAddresses, + onSwitch: onSwitch, + } + } + + const switchToAddCustomTokenScreen = () => setActiveScreen('addCustomToken') + + const getItemKey = (index: number, { tokens }): string => { + return tokens.get(index).address + } + + const filteredTokens = filterBy(filter, tokens) + const itemData = createItemData(filteredTokens, activeTokensAddresses) + + return ( + <> + + + + } + value={filter} + /> + + + + + + + + {!tokens.size && ( + + + + )} + {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 68f7322f..ac7f8c34 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 a660c44a..b37e7b7f 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 16737cc7..e19c215a 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 21c7fb7e..1f5489c6 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 63cf986b..862c0251 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 ( <>
{ @@ -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 3aa449fa..e5372d4c 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/Value.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/Value.tsx index fed5bafe..409440a5 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/Value.tsx +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/Value.tsx @@ -30,7 +30,7 @@ interface RenderValueProps { const EtherscanLink = ({ method, type, value }: RenderValueProps): React.ReactElement => { const classes = useStyles() - const [cut, setCut] = React.useState(undefined) + const [cut, setCut] = React.useState(0) const { width } = useWindowDimensions() React.useEffect(() => { 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 15dabaaa..d204b863 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 8298fb3c..7bd08ad8 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 @@ -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/TxType/index.tsx b/src/routes/safe/components/Transactions/TxsTable/TxType/index.tsx index fbe5a860..28c54f67 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 4f385bf6..39eca8d5 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 e7e43cdc..522c277a 100644 --- a/src/routes/safe/components/Transactions/TxsTable/index.tsx +++ b/src/routes/safe/components/Transactions/TxsTable/index.tsx @@ -44,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 89b0a499..75f9fffb 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 98% rename from src/routes/safe/container/hooks/useTransactions.tsx rename to src/routes/safe/container/hooks/useTransactions.ts index a08f649d..d8c08424 100644 --- a/src/routes/safe/container/hooks/useTransactions.tsx +++ b/src/routes/safe/container/hooks/useTransactions.ts @@ -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 c504a71f..2ca98cbf 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 821790cd..f27581d9 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 00000000..db0b015c --- /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 461051c4..b03c4751 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 6b00126f..12363dff 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 8fc23408..cd67c3f6 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -19,6 +19,7 @@ import currencyValues, { CURRENCY_VALUES_KEY, CurrencyValuesState, } from 'src/logic/currencyValues/store/reducer/currencyValues' +import { CurrentSessionState } from 'src/logic/currentSession/store/reducer/currentSession' import currentSession, { CURRENT_SESSION_REDUCER_ID } 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' @@ -37,7 +38,7 @@ import safe, { SAFE_REDUCER_ID } from 'src/logic/safe/store/reducer/safe' import transactions, { TRANSACTIONS_REDUCER_ID } from 'src/logic/safe/store/reducer/transactions' 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 allTransactions, { TRANSACTIONS, TransactionsState } from 'src/logic/safe/store/reducer/allTransactions' export const history = createHashHistory() @@ -86,7 +87,7 @@ export type AppReduxState = CombinedState<{ [CURRENCY_VALUES_KEY]: CurrencyValuesState [COOKIES_REDUCER_ID]: Map [ADDRESS_BOOK_REDUCER_ID]: AddressBookReducerMap - [CURRENT_SESSION_REDUCER_ID]: Map + [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 142228fc..00000000 --- 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 fa1c5b0f..79f770af 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 1f22bff7..0a3ea15c 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 00000000..3969301f --- /dev/null +++ b/src/test/utils/safeHelper.ts @@ -0,0 +1,163 @@ +//@ts-nocheck +import { NonPayableTransactionObject } from 'src/types/contracts/types' +import { PromiEvent } from 'web3-core' +import { GnosisSafe } from 'src/types/contracts/GnosisSafe' +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' +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 85ffe7e0..e15863d8 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 674509a3..b0cd1fdc 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 e2b45a91..d540a71c 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 faf9f84e..00000000 --- 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/intercom.ts b/src/utils/intercom.ts index 8c2f7ee9..6fb98cbf 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 1684fea4..00000000 --- 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 855f5f34..00000000 --- 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 f7d41ded..ec142be7 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 ede637e7..61c5bcc1 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 12eda847..130de018 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" @@ -2567,14 +2555,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" @@ -2590,7 +2578,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== @@ -3759,9 +3747,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" @@ -3831,6 +3819,13 @@ ansi-colors@4.1.1, ansi-colors@^4.1.1: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== +ansi-colors@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-1.1.0.tgz#6374b4dd5d4718ff3ce27a671a3b1cad077132a9" + integrity sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA== + dependencies: + ansi-wrap "^0.1.0" + ansi-colors@^3.0.0: version "3.2.4" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" @@ -3848,6 +3843,13 @@ ansi-escapes@^4.2.1, ansi-escapes@^4.3.0: dependencies: type-fest "^0.11.0" +ansi-gray@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-gray/-/ansi-gray-0.1.1.tgz#2962cf54ec9792c48510a3deb524436861ef7251" + integrity sha1-KWLPVOyXksSFEKPetSRDaGHvclE= + dependencies: + ansi-wrap "0.1.0" + ansi-html@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" @@ -3900,6 +3902,11 @@ ansi-to-html@^0.6.11: dependencies: entities "^1.1.2" +ansi-wrap@0.1.0, ansi-wrap@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" + integrity sha1-qCJQ3bABXponyoLoLqYDu/pF768= + any-promise@1.3.0, any-promise@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" @@ -3964,11 +3971,23 @@ app-root-dir@^1.0.2: resolved "https://registry.yarnpkg.com/app-root-dir/-/app-root-dir-1.0.2.tgz#38187ec2dea7577fff033ffcb12172692ff6e118" integrity sha1-OBh+wt6nV3//Az/8sSFyaS/24Rg= +append-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/append-buffer/-/append-buffer-1.0.2.tgz#d8220cf466081525efea50614f3de6514dfa58f1" + integrity sha1-2CIM9GYIFSXv6lBhTz3mUU36WPE= + dependencies: + buffer-equal "^1.0.0" + aproba@^1.0.3, aproba@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== +archy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" + integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA= + are-we-there-yet@~1.1.2: version "1.1.5" resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" @@ -4010,11 +4029,25 @@ arr-diff@^4.0.0: resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= -arr-flatten@^1.1.0: +arr-filter@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/arr-filter/-/arr-filter-1.1.2.tgz#43fdddd091e8ef11aa4c45d9cdc18e2dff1711ee" + integrity sha1-Q/3d0JHo7xGqTEXZzcGOLf8XEe4= + dependencies: + make-iterator "^1.0.0" + +arr-flatten@^1.0.1, arr-flatten@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== +arr-map@^2.0.0, arr-map@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/arr-map/-/arr-map-2.0.2.tgz#3a77345ffc1cf35e2a91825601f9e58f2e24cac4" + integrity sha1-Onc0X/wc814qkYJWAfnljy4kysQ= + dependencies: + make-iterator "^1.0.0" + arr-union@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" @@ -4034,6 +4067,11 @@ array-back@^2.0.0: dependencies: typical "^2.6.1" +array-each@^1.0.0, array-each@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/array-each/-/array-each-1.0.1.tgz#a794af0c05ab1752846ee753a1f211a05ba0c44f" + integrity sha1-p5SvDAWrF1KEbudTofIRoFugxE8= + array-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" @@ -4063,6 +4101,35 @@ array-includes@^3.0.3, array-includes@^3.1.1: es-abstract "^1.17.0" is-string "^1.0.5" +array-initial@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/array-initial/-/array-initial-1.1.0.tgz#2fa74b26739371c3947bd7a7adc73be334b3d795" + integrity sha1-L6dLJnOTccOUe9enrcc74zSz15U= + dependencies: + array-slice "^1.0.0" + is-number "^4.0.0" + +array-last@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/array-last/-/array-last-1.3.0.tgz#7aa77073fec565ddab2493f5f88185f404a9d336" + integrity sha512-eOCut5rXlI6aCOS7Z7kCplKRKyiFQ6dHFBem4PwlwKeNFk2/XxTrhRh5T9PyaEWGy/NHTZWbY+nsZlNFJu9rYg== + dependencies: + is-number "^4.0.0" + +array-slice@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-1.1.0.tgz#e368ea15f89bc7069f7ffb89aec3a6c7d4ac22d4" + integrity sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w== + +array-sort@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-sort/-/array-sort-1.0.0.tgz#e4c05356453f56f53512a7d1d6123f2c54c0a88a" + integrity sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg== + dependencies: + default-compare "^1.0.0" + get-value "^2.0.6" + kind-of "^5.0.2" + array-union@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" @@ -4184,6 +4251,16 @@ astral-regex@^2.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== +async-done@^1.2.0, async-done@^1.2.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/async-done/-/async-done-1.3.2.tgz#5e15aa729962a4b07414f528a88cdf18e0b290a2" + integrity sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.2" + process-nextick-args "^2.0.0" + stream-exhaust "^1.0.1" + async-each@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" @@ -4216,6 +4293,13 @@ async-sema@^3.1.0: resolved "https://registry.yarnpkg.com/async-sema/-/async-sema-3.1.0.tgz#3a813beb261e4cc58b19213916a48e931e21d21e" integrity sha512-+JpRq3r0zjpRLDruS6q/nC4V5tzsaiu07521677Mdi5i+AkaU/aNJH38rYHJVQ4zvz+SSkjgc8FUI7qIZrR+3g== +async-settle@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async-settle/-/async-settle-1.0.0.tgz#1d0a914bb02575bec8a8f3a74e5080f72b2c0c6b" + integrity sha1-HQqRS7Aldb7IqPOnTlCA9yssDGs= + dependencies: + async-done "^1.2.2" + async@0.9.x: version "0.9.2" resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" @@ -5225,6 +5309,21 @@ babylon@^6.18.0: resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== +bach@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/bach/-/bach-1.2.0.tgz#4b3ce96bf27134f79a1b414a51c14e34c3bd9880" + integrity sha1-Szzpa/JxNPeaG0FKUcFONMO9mIA= + dependencies: + arr-filter "^1.1.1" + arr-flatten "^1.0.1" + arr-map "^2.0.0" + array-each "^1.0.0" + array-initial "^1.0.0" + array-last "^1.1.1" + async-done "^1.2.2" + async-settle "^1.0.0" + now-and-later "^2.0.0" + backoff@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/backoff/-/backoff-2.5.0.tgz#f616eda9d3e4b66b8ca7fca79f695722c5f8e26f" @@ -5682,6 +5781,11 @@ buffer-crc32@~0.2.3: resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= +buffer-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe" + integrity sha1-WWFrSYME1Var1GaWayLu2j7KX74= + buffer-fill@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" @@ -5909,6 +6013,11 @@ camelcase@^2.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= +camelcase@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" + integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo= + camelcase@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" @@ -6044,7 +6153,7 @@ chokidar@3.3.1: optionalDependencies: fsevents "~2.1.2" -chokidar@^2.0.4, chokidar@^2.1.8: +chokidar@^2.0.0, chokidar@^2.0.4, chokidar@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== @@ -6207,6 +6316,15 @@ clipboard@^2.0.0: select "^1.1.2" tiny-emitter "^2.0.0" +cliui@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" + integrity sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0= + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrap-ansi "^2.0.0" + cliui@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" @@ -6225,6 +6343,11 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" +clone-buffer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" + integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg= + clone-deep@^0.2.4: version "0.2.4" resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-0.2.4.tgz#4e73dd09e9fb971cc38670c5dced9c1896481cc6" @@ -6252,11 +6375,25 @@ clone-response@^1.0.2: dependencies: mimic-response "^1.0.0" +clone-stats@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" + integrity sha1-s3gt/4u1R04Yuba/D9/ngvh3doA= + clone@^2.0.0, clone@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= +cloneable-readable@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/cloneable-readable/-/cloneable-readable-1.1.3.tgz#120a00cb053bfb63a222e709f9683ea2e11d8cec" + integrity sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ== + dependencies: + inherits "^2.0.1" + process-nextick-args "^2.0.0" + readable-stream "^2.3.5" + clsx@^1.0.4, clsx@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" @@ -6281,6 +6418,15 @@ code-point-at@^1.0.0: resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= +collection-map@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-map/-/collection-map-1.0.0.tgz#aea0f06f8d26c780c2b75494385544b2255af18c" + integrity sha1-rqDwb40mx4DCt1SUOFVEsiVa8Yw= + dependencies: + arr-map "^2.0.2" + for-own "^1.0.0" + make-iterator "^1.0.0" + collection-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" @@ -6321,6 +6467,11 @@ color-string@^1.5.2: color-name "^1.0.0" simple-swizzle "^0.2.2" +color-support@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + color@^3.0.0: version "3.1.2" resolved "https://registry.yarnpkg.com/color/-/color-3.1.2.tgz#68148e7f85d41ad7649c5fa8c8106f098d229e10" @@ -6437,7 +6588,7 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@^1.5.0, concat-stream@^1.5.1, concat-stream@^1.6.2: +concat-stream@^1.5.0, concat-stream@^1.5.1, concat-stream@^1.6.0, concat-stream@^1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== @@ -6447,7 +6598,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== @@ -6584,6 +6735,14 @@ copy-descriptor@^0.1.0: resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= +copy-props@^2.0.1: + version "2.0.4" + resolved "https://registry.yarnpkg.com/copy-props/-/copy-props-2.0.4.tgz#93bb1cadfafd31da5bb8a9d4b41f471ec3a72dfe" + integrity sha512-7cjuUME+p+S3HZlbllgsn2CDwS+5eCCX16qBgNC4jgSTf49qR1VKy/Zhl400m0IQXl/bPGEVqncgUUMjrr4s8A== + dependencies: + each-props "^1.3.0" + is-plain-object "^2.0.1" + copy-to-clipboard@^3.0.8: version "3.3.1" resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz#115aa1a9998ffab6196f93076ad6da3b913662ae" @@ -7174,7 +7333,7 @@ decamelize-keys@^1.0.0: decamelize "^1.1.0" map-obj "^1.0.0" -decamelize@^1.1.0, decamelize@^1.1.2, decamelize@^1.2.0: +decamelize@^1.1.0, decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= @@ -7288,6 +7447,13 @@ deepmerge@^4.2.2: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== +default-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/default-compare/-/default-compare-1.0.0.tgz#cb61131844ad84d84788fb68fd01681ca7781a2f" + integrity sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ== + dependencies: + kind-of "^5.0.2" + default-gateway@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b" @@ -7296,6 +7462,11 @@ default-gateway@^4.2.0: execa "^1.0.0" ip-regex "^2.1.0" +default-resolution@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/default-resolution/-/default-resolution-2.0.0.tgz#bcb82baa72ad79b426a76732f1a81ad6df26d684" + integrity sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ= + defer-to-connect@^1.0.1: version "1.1.3" resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" @@ -7393,6 +7564,11 @@ detect-browser@5.1.0: resolved "https://registry.yarnpkg.com/detect-browser/-/detect-browser-5.1.0.tgz#0c51c66b747ad8f98a6832bf3026a5a23a7850ff" integrity sha512-WKa9p+/MNwmTiS+V2AS6eGxic+807qvnV3hC+4z2GTY+F42h1n8AynVTMMc4EJBC32qMs6yjOTpeDEQQt/AVqQ== +detect-file@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" + integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc= + detect-indent@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" @@ -7685,6 +7861,14 @@ duplexify@^3.4.2, duplexify@^3.6.0: readable-stream "^2.0.0" stream-shift "^1.0.0" +each-props@^1.3.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/each-props/-/each-props-1.3.2.tgz#ea45a414d16dd5cfa419b1a81720d5ca06892333" + integrity sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA== + dependencies: + is-plain-object "^2.0.1" + object.defaults "^1.1.0" + ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" @@ -7716,9 +7900,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" @@ -7752,13 +7936,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" @@ -7809,10 +7993,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" @@ -7994,6 +8178,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" @@ -8021,7 +8223,7 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" -es5-ext@^0.10.35, es5-ext@^0.10.50: +es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50: version "0.10.53" resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1" integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q== @@ -8040,7 +8242,7 @@ es6-error@^4.1.1: resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== -es6-iterator@2.0.3, es6-iterator@~2.0.3: +es6-iterator@2.0.3, es6-iterator@^2.0.1, es6-iterator@^2.0.3, es6-iterator@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c= @@ -8067,6 +8269,16 @@ es6-symbol@^3.1.1, es6-symbol@~3.1.3: d "^1.0.1" ext "^1.1.2" +es6-weak-map@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.3.tgz#b6da1f16cc2cc0d9be43e6bdbfc5e7dfcdf31d53" + integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA== + dependencies: + d "1" + es5-ext "^0.10.46" + es6-iterator "^2.0.3" + es6-symbol "^3.1.1" + escalade@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.0.2.tgz#6a580d70edb87880f22b4c91d0d56078df6962c4" @@ -8256,7 +8468,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== @@ -8946,11 +9158,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" @@ -9027,6 +9244,13 @@ expand-template@^2.0.3: resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== +expand-tilde@^2.0.0, expand-tilde@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" + integrity sha1-l+gBqgUt8CRU3kawK/YhZCzchQI= + dependencies: + homedir-polyfill "^1.0.1" + expect@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/expect/-/expect-24.9.0.tgz#b75165b4817074fa4a157794f46fe9f1ba15b6ca" @@ -9039,7 +9263,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== @@ -9102,7 +9326,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@~3.0.2: +extend@^3.0.0, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -9157,6 +9381,16 @@ fake-merkle-patricia-tree@^1.0.1: dependencies: checkpoint-store "^1.1.0" +fancy-log@^1.3.2: + version "1.3.3" + resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.3.tgz#dbc19154f558690150a23953a0adbd035be45fc7" + integrity sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw== + dependencies: + ansi-gray "^0.1.1" + color-support "^1.1.3" + parse-node-version "^1.0.0" + time-stamp "^1.0.0" + fast-deep-equal@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" @@ -9194,6 +9428,11 @@ fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== +fast-levenshtein@^1.0.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-1.1.4.tgz#e6a754cc8f15e58987aa9cbd27af66fd6f4e5af9" + integrity sha1-5qdUzI8V5YmHqpy9J69m/W9OWvk= + fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" @@ -9458,6 +9697,42 @@ find-versions@^3.2.0: dependencies: semver-regex "^2.0.0" +findup-sync@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc" + integrity sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw= + dependencies: + detect-file "^1.0.0" + is-glob "^3.1.0" + micromatch "^3.0.4" + resolve-dir "^1.0.1" + +findup-sync@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" + integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg== + dependencies: + detect-file "^1.0.0" + is-glob "^4.0.0" + micromatch "^3.0.4" + resolve-dir "^1.0.1" + +fined@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/fined/-/fined-1.2.0.tgz#d00beccf1aa2b475d16d423b0238b713a2c4a37b" + integrity sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng== + dependencies: + expand-tilde "^2.0.2" + is-plain-object "^2.0.3" + object.defaults "^1.1.0" + object.pick "^1.2.0" + parse-filepath "^1.0.1" + +flagged-respawn@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/flagged-respawn/-/flagged-respawn-1.0.1.tgz#e7de6f1279ddd9ca9aac8a5971d618606b3aab41" + integrity sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q== + flat-cache@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" @@ -9484,7 +9759,7 @@ flatten@^1.0.2: resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b" integrity sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg== -flush-write-stream@^1.0.0: +flush-write-stream@^1.0.0, flush-write-stream@^1.0.2: version "1.1.1" resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w== @@ -9533,6 +9808,13 @@ for-own@^0.1.3: dependencies: for-in "^1.0.1" +for-own@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b" + integrity sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs= + dependencies: + for-in "^1.0.1" + forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -9677,6 +9959,14 @@ fs-minipass@^2.0.0: dependencies: minipass "^3.0.0" +fs-mkdirp-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz#0b7815fc3201c6a69e14db98ce098c16935259eb" + integrity sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes= + dependencies: + graceful-fs "^4.1.11" + through2 "^2.0.3" + fs-write-stream-atomic@^1.0.8: version "1.0.10" resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" @@ -9765,6 +10055,11 @@ gensync@^1.0.0-beta.1: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== +get-caller-file@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" + integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== + get-caller-file@^2.0.1: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -9859,11 +10154,40 @@ glob-parent@^5.0.0, glob-parent@~5.1.0: dependencies: is-glob "^4.0.1" +glob-stream@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-6.1.0.tgz#7045c99413b3eb94888d83ab46d0b404cc7bdde4" + integrity sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ= + dependencies: + extend "^3.0.0" + glob "^7.1.1" + glob-parent "^3.1.0" + is-negated-glob "^1.0.0" + ordered-read-streams "^1.0.0" + pumpify "^1.3.5" + readable-stream "^2.1.5" + remove-trailing-separator "^1.0.1" + to-absolute-glob "^2.0.0" + unique-stream "^2.0.2" + glob-to-regexp@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= +glob-watcher@^5.0.3: + version "5.0.5" + resolved "https://registry.yarnpkg.com/glob-watcher/-/glob-watcher-5.0.5.tgz#aa6bce648332924d9a8489be41e3e5c52d4186dc" + integrity sha512-zOZgGGEHPklZNjZQaZ9f41i7F2YwE+tS5ZHrDhbBCk3stwahn5vQxnFmBJZHoYdusR6R1bLSXeGUy/BhctwKzw== + dependencies: + anymatch "^2.0.0" + async-done "^1.2.0" + chokidar "^2.0.0" + is-negated-glob "^1.0.0" + just-debounce "^1.0.0" + normalize-path "^3.0.0" + object.defaults "^1.1.0" + glob@7.1.6, glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@~7.1.1, glob@~7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" @@ -9903,6 +10227,26 @@ global-modules@2.0.0: dependencies: global-prefix "^3.0.0" +global-modules@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" + integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg== + dependencies: + global-prefix "^1.0.1" + is-windows "^1.0.1" + resolve-dir "^1.0.0" + +global-prefix@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" + integrity sha1-2/dDxsFJklk8ZVVoy2btMsASLr4= + dependencies: + expand-tilde "^2.0.2" + homedir-polyfill "^1.0.1" + ini "^1.3.4" + is-windows "^1.0.1" + which "^1.2.14" + global-prefix@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" @@ -9995,6 +10339,13 @@ globule@^1.0.0: lodash "~4.17.10" minimatch "~3.0.2" +glogg@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/glogg/-/glogg-1.0.2.tgz#2d7dd702beda22eb3bffadf880696da6d846313f" + integrity sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA== + dependencies: + sparkles "^1.0.0" + good-listener@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" @@ -10039,7 +10390,7 @@ got@^7.1.0: url-parse-lax "^1.0.0" url-to-options "^1.0.1" -graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.2: +graceful-fs@^4.0.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.2: version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== @@ -10059,6 +10410,47 @@ gud@^1.0.0: resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0" integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw== +gulp-cli@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/gulp-cli/-/gulp-cli-2.3.0.tgz#ec0d380e29e52aa45e47977f0d32e18fd161122f" + integrity sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A== + dependencies: + ansi-colors "^1.0.1" + archy "^1.0.0" + array-sort "^1.0.0" + color-support "^1.1.3" + concat-stream "^1.6.0" + copy-props "^2.0.1" + fancy-log "^1.3.2" + gulplog "^1.0.0" + interpret "^1.4.0" + isobject "^3.0.1" + liftoff "^3.1.0" + matchdep "^2.0.0" + mute-stdout "^1.0.0" + pretty-hrtime "^1.0.0" + replace-homedir "^1.0.0" + semver-greatest-satisfied-range "^1.1.0" + v8flags "^3.2.0" + yargs "^7.1.0" + +gulp@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/gulp/-/gulp-4.0.2.tgz#543651070fd0f6ab0a0650c6a3e6ff5a7cb09caa" + integrity sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA== + dependencies: + glob-watcher "^5.0.3" + gulp-cli "^2.2.0" + undertaker "^1.2.1" + vinyl-fs "^3.0.0" + +gulplog@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/gulplog/-/gulplog-1.0.0.tgz#e28c4d45d05ecbbed818363ce8f9c5926229ffe5" + integrity sha1-4oxNRdBey77YGDY86PnFkmIp/+U= + dependencies: + glogg "^1.0.0" + gzip-size@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274" @@ -10277,6 +10669,13 @@ home-or-tmp@^2.0.0: os-homedir "^1.0.0" os-tmpdir "^1.0.1" +homedir-polyfill@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" + integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== + dependencies: + parse-passwd "^1.0.0" + hosted-git-info@^2.1.4: version "2.8.8" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" @@ -10564,10 +10963,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" @@ -10759,7 +11158,7 @@ internal-slot@^1.0.2: has "^1.0.3" side-channel "^1.0.2" -interpret@^1.0.0: +interpret@^1.0.0, interpret@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== @@ -10776,6 +11175,11 @@ invariant@^2.2.2, invariant@^2.2.3, invariant@^2.2.4: dependencies: loose-envify "^1.0.0" +invert-kv@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" + integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY= + ip-regex@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" @@ -10801,6 +11205,14 @@ is-absolute-url@^3.0.3: resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q== +is-absolute@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576" + integrity sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA== + dependencies: + is-relative "^1.0.0" + is-windows "^1.0.1" + is-accessor-descriptor@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" @@ -11064,6 +11476,16 @@ 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-negated-glob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2" + integrity sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI= + +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" @@ -11076,6 +11498,11 @@ is-number@^3.0.0: dependencies: kind-of "^3.0.2" +is-number@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" + integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ== + is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -11137,7 +11564,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== @@ -11156,6 +11583,13 @@ is-regexp@^1.0.0: resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" integrity sha1-/S2INUXEa6xaYz57mgnof6LLUGk= +is-relative@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d" + integrity sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA== + dependencies: + is-unc-path "^1.0.0" + is-resolvable@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" @@ -11210,17 +11644,29 @@ is-typedarray@1.0.0, is-typedarray@^1.0.0, is-typedarray@~1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= -is-utf8@^0.2.0: +is-unc-path@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-1.0.0.tgz#d731e8898ed090a12c352ad2eaed5095ad322c9d" + integrity sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ== + dependencies: + unc-path-regex "^0.1.2" + +is-utf8@^0.2.0, is-utf8@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= +is-valid-glob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa" + integrity sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao= + is-window@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-window/-/is-window-1.0.2.tgz#2c896ca53db97de45d3c33133a65d8c9f563480d" integrity sha1-LIlspT25feRdPDMTOmXYyfVjSA0= -is-windows@^1.0.2: +is-windows@^1.0.1, is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== @@ -11775,6 +12221,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" @@ -12144,6 +12601,11 @@ just-curry-it@^3.1.0: resolved "https://registry.yarnpkg.com/just-curry-it/-/just-curry-it-3.1.0.tgz#ab59daed308a58b847ada166edd0a2d40766fbc5" integrity sha512-mjzgSOFzlrurlURaHVjnQodyPNvrHrf1TbQP2XU9NSqBtHQPuHZ+Eb6TAJP7ASeJN9h9K0KXoRTs8u6ouHBKvg== +just-debounce@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/just-debounce/-/just-debounce-1.0.0.tgz#87fccfaeffc0b68cd19d55f6722943f929ea35ea" + integrity sha1-h/zPrv/AtozRnVX2cilD+SnqNeo= + keccak256@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/keccak256/-/keccak256-1.0.0.tgz#1ba55ce78ed3d63fb7091d045469007da984171d" @@ -12211,7 +12673,7 @@ kind-of@^4.0.0: dependencies: is-buffer "^1.1.5" -kind-of@^5.0.0: +kind-of@^5.0.0, kind-of@^5.0.2: version "5.1.0" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== @@ -12253,6 +12715,14 @@ last-call-webpack-plugin@^3.0.0: lodash "^4.17.5" webpack-sources "^1.1.0" +last-run@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/last-run/-/last-run-1.1.1.tgz#45b96942c17b1c79c772198259ba943bebf8ca5b" + integrity sha1-RblpQsF7HHnHchmCWbqUO+v4yls= + dependencies: + default-resolution "^2.0.0" + es6-weak-map "^2.0.1" + latest-version@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" @@ -12286,11 +12756,32 @@ lazy-val@^1.0.4: resolved "https://registry.yarnpkg.com/lazy-val/-/lazy-val-1.0.4.tgz#882636a7245c2cfe6e0a4e3ba6c5d68a137e5c65" integrity sha512-u93kb2fPbIrfzBuLjZE+w+fJbUUMhNDXxNmMfaqNgpfQf1CO5ZSe2LfsnBqVAk7i/2NF48OSoRj+Xe2VT+lE8Q== +lazystream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4" + integrity sha1-9plf4PggOS9hOWvolGJAe7dxaOQ= + dependencies: + readable-stream "^2.0.5" + +lcid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" + integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU= + dependencies: + invert-kv "^1.0.0" + lcov-parse@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lcov-parse/-/lcov-parse-1.0.0.tgz#eb0d46b54111ebc561acb4c408ef9363bdc8f7e0" integrity sha1-6w1GtUER68VhrLTECO+TY73I9+A= +lead@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lead/-/lead-1.0.0.tgz#6f14f99a37be3a9dd784f5495690e5903466ee42" + integrity sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI= + dependencies: + flush-write-stream "^1.0.2" + left-pad@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e" @@ -12366,6 +12857,20 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +liftoff@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-3.1.0.tgz#c9ba6081f908670607ee79062d700df062c52ed3" + integrity sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog== + dependencies: + extend "^3.0.0" + findup-sync "^3.0.0" + fined "^1.0.1" + flagged-respawn "^1.0.0" + is-plain-object "^2.0.4" + object.map "^1.0.0" + rechoir "^0.6.2" + resolve "^1.1.7" + lines-and-columns@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" @@ -12728,6 +13233,13 @@ make-dir@^3.0.0, make-dir@^3.0.2: dependencies: semver "^6.0.0" +make-iterator@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.1.tgz#29b33f312aa8f547c4a5e490f56afcec99133ad6" + integrity sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw== + dependencies: + kind-of "^6.0.2" + makeerror@1.0.x: version "1.0.11" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" @@ -12740,7 +13252,7 @@ mamacro@^0.0.3: resolved "https://registry.yarnpkg.com/mamacro/-/mamacro-0.0.3.tgz#ad2c9576197c9f1abf308d0787865bd975a3f3e4" integrity sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA== -map-cache@^0.2.2: +map-cache@^0.2.0, map-cache@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= @@ -12775,6 +13287,16 @@ markdown-to-jsx@^6.11.4: prop-types "^15.6.2" unquote "^1.1.0" +matchdep@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/matchdep/-/matchdep-2.0.0.tgz#c6f34834a0d8dbc3b37c27ee8bbcb27c7775582e" + integrity sha1-xvNINKDY28OzfCfui7yyfHd1WC4= + dependencies: + findup-sync "^2.0.0" + micromatch "^3.0.4" + resolve "^1.4.0" + stack-trace "0.0.10" + matcher@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/matcher/-/matcher-3.0.0.tgz#bd9060f4c5b70aa8041ccc6f80368760994f30ca" @@ -12782,7 +13304,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== @@ -12943,7 +13465,7 @@ microevent.ts@~0.1.1: resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0" integrity sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g== -micromatch@^3.1.10, micromatch@^3.1.4: +micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== @@ -13221,9 +13743,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" @@ -13310,6 +13832,11 @@ multihashes@^0.4.15, multihashes@~0.4.15: multibase "^0.7.0" varint "^5.0.0" +mute-stdout@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mute-stdout/-/mute-stdout-1.0.1.tgz#acb0300eb4de23a7ddeec014e3e96044b3472331" + integrity sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg== + mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" @@ -13396,9 +13923,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" @@ -13623,6 +14150,13 @@ normalize-url@^4.1.0: prop-types "^15.7.2" react-is "^16.9.0" +now-and-later@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/now-and-later/-/now-and-later-2.0.1.tgz#8e579c8685764a7cc02cb680380e94f43ccb1f7c" + integrity sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ== + dependencies: + once "^1.3.2" + npm-conf@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/npm-conf/-/npm-conf-1.1.3.tgz#256cc47bd0e218c259c4e9550bf413bc2192aff9" @@ -13718,7 +14252,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== @@ -13768,6 +14302,26 @@ object.assign@4.1.0, object.assign@^4.1.0: has-symbols "^1.0.0" object-keys "^1.0.11" +object.assign@^4.0.4: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd" + integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.18.0-next.0" + has-symbols "^1.0.1" + object-keys "^1.1.1" + +object.defaults@^1.0.0, object.defaults@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/object.defaults/-/object.defaults-1.1.0.tgz#3a7f868334b407dea06da16d88d5cd29e435fecf" + integrity sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8= + dependencies: + array-each "^1.0.1" + array-slice "^1.0.0" + for-own "^1.0.0" + isobject "^3.0.0" + object.entries@^1.1.0, object.entries@^1.1.1, object.entries@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.2.tgz#bc73f00acb6b6bb16c203434b10f9a7e797d3add" @@ -13795,13 +14349,29 @@ object.getownpropertydescriptors@^2.0.3, object.getownpropertydescriptors@^2.1.0 define-properties "^1.1.3" es-abstract "^1.17.0-next.1" -object.pick@^1.3.0: +object.map@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object.map/-/object.map-1.0.1.tgz#cf83e59dc8fcc0ad5f4250e1f78b3b81bd801d37" + integrity sha1-z4Plncj8wK1fQlDh94s7gb2AHTc= + dependencies: + for-own "^1.0.0" + make-iterator "^1.0.0" + +object.pick@^1.2.0, object.pick@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= dependencies: isobject "^3.0.1" +object.reduce@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object.reduce/-/object.reduce-1.0.1.tgz#6fe348f2ac7fa0f95ca621226599096825bb03ad" + integrity sha1-b+NI8qx/oPlcpiEiZZkJaCW7A60= + dependencies: + for-own "^1.0.0" + make-iterator "^1.0.0" + object.values@^1.1.0, object.values@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e" @@ -13851,7 +14421,7 @@ on-headers@~1.0.2: resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== -once@^1.3.0, once@^1.3.1, once@^1.4.0: +once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= @@ -13879,7 +14449,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== @@ -13887,6 +14457,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" @@ -13924,6 +14502,13 @@ optionator@^0.8.1, optionator@^0.8.3: type-check "~0.3.2" word-wrap "~1.2.3" +ordered-read-streams@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz#77c0cb37c41525d64166d990ffad7ec6a0e1363e" + integrity sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4= + dependencies: + readable-stream "^2.0.1" + original-require@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/original-require/-/original-require-1.0.1.tgz#0f130471584cd33511c5ec38c8d59213f9ac5e20" @@ -13946,6 +14531,13 @@ os-homedir@^1.0.0: resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= +os-locale@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" + integrity sha1-IPnxeuKe00XoveWDsT0gCYA8FNk= + dependencies: + lcid "^1.0.0" + os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -14126,6 +14718,15 @@ parse-entities@^1.1.2: is-decimal "^1.0.0" is-hexadecimal "^1.0.0" +parse-filepath@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891" + integrity sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE= + dependencies: + is-absolute "^1.0.0" + map-cache "^0.2.0" + path-root "^0.1.1" + parse-headers@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/parse-headers/-/parse-headers-2.0.3.tgz#5e8e7512383d140ba02f0c7aa9f49b4399c92515" @@ -14156,6 +14757,16 @@ parse-json@^5.0.0: json-parse-better-errors "^1.0.1" lines-and-columns "^1.1.6" +parse-node-version@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" + integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA== + +parse-passwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" + integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= + parse5@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" @@ -14236,6 +14847,18 @@ path-parse@^1.0.6: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== +path-root-regex@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/path-root-regex/-/path-root-regex-0.1.2.tgz#bfccdc8df5b12dc52c8b43ec38d18d72c04ba96d" + integrity sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0= + +path-root@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/path-root/-/path-root-0.1.1.tgz#9a4a6814cac1c0cd73360a95f32083c8ea4745b7" + integrity sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc= + dependencies: + path-root-regex "^0.1.0" + path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" @@ -15255,7 +15878,7 @@ pretty-format@^26.4.2: ansi-styles "^4.0.0" react-is "^16.12.0" -pretty-hrtime@^1.0.3: +pretty-hrtime@^1.0.0, pretty-hrtime@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE= @@ -15279,7 +15902,7 @@ private@^0.1.6, private@^0.1.8, private@~0.1.5: resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg== -process-nextick-args@~2.0.0: +process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== @@ -15426,7 +16049,7 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -pumpify@^1.3.3: +pumpify@^1.3.3, pumpify@^1.3.5: version "1.5.1" resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ== @@ -15546,9 +16169,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" @@ -15942,7 +16565,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== @@ -16142,7 +16765,7 @@ read-pkg@^4.0.1: parse-json "^4.0.0" pify "^3.0.0" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -16445,7 +17068,24 @@ relateurl@^0.2.7: resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= -remove-trailing-separator@^1.0.1: +remove-bom-buffer@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz#c2bf1e377520d324f623892e33c10cac2c252b53" + integrity sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ== + dependencies: + is-buffer "^1.1.5" + is-utf8 "^0.2.1" + +remove-bom-stream@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz#05f1a593f16e42e1fb90ebf59de8e569525f9523" + integrity sha1-BfGlk/FuQuH7kOv1nejlaVJflSM= + dependencies: + remove-bom-buffer "^3.0.0" + safe-buffer "^5.1.0" + through2 "^2.0.3" + +remove-trailing-separator@^1.0.1, remove-trailing-separator@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= @@ -16478,6 +17118,20 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" +replace-ext@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.1.tgz#2d6d996d04a15855d967443631dd5f77825b016a" + integrity sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw== + +replace-homedir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/replace-homedir/-/replace-homedir-1.0.0.tgz#e87f6d513b928dde808260c12be7fec6ff6e798c" + integrity sha1-6H9tUTuSjd6AgmDBK+f+xv9ueYw= + dependencies: + homedir-polyfill "^1.0.1" + is-absolute "^1.0.0" + remove-trailing-separator "^1.1.0" + request-promise-core@1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f" @@ -16530,6 +17184,11 @@ require-from-string@^2.0.0: resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= + require-main-filename@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" @@ -16557,6 +17216,14 @@ resolve-cwd@^2.0.0: dependencies: resolve-from "^3.0.0" +resolve-dir@^1.0.0, resolve-dir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" + integrity sha1-eaQGRMNivoLybv/nOcm7U4IEb0M= + dependencies: + expand-tilde "^2.0.0" + global-modules "^1.0.0" + resolve-from@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" @@ -16572,6 +17239,13 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== +resolve-options@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/resolve-options/-/resolve-options-1.1.0.tgz#32bb9e39c06d67338dc9378c0d6d6074566ad131" + integrity sha1-MrueOcBtZzONyTeMDW1gdFZq0TE= + dependencies: + value-or-function "^3.0.0" + resolve-pathname@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" @@ -16610,7 +17284,7 @@ resolve@1.15.0: dependencies: path-parse "^1.0.6" -resolve@^1.1.6, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.15.1, resolve@^1.17.0, resolve@^1.3.2, resolve@^1.8.1, resolve@~1.17.0: +resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.15.1, resolve@^1.17.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.8.1, resolve@~1.17.0: version "1.17.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== @@ -16981,6 +17655,13 @@ semver-diff@^3.1.1: dependencies: semver "^6.3.0" +semver-greatest-satisfied-range@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz#13e8c2658ab9691cb0cd71093240280d36f77a5b" + integrity sha1-E+jCZYq5aRywzXEJMkAoDTb3els= + dependencies: + sver-compat "^1.5.0" + semver-regex@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-2.0.0.tgz#a93c2c5844539a770233379107b38c7b4ac9d338" @@ -17232,12 +17913,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" @@ -17484,6 +18165,11 @@ space-separated-tokens@^1.0.0: resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899" integrity sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA== +sparkles@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.1.tgz#008db65edce6c50eec0c5e228e1945061dd0437c" + integrity sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw== + spawn-command@^0.0.2-1: version "0.0.2-1" resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0" @@ -17631,6 +18317,11 @@ stable@^0.1.8: resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== +stack-trace@0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= + stack-utils@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8" @@ -17692,6 +18383,11 @@ stream-each@^1.1.0: end-of-stream "^1.1.0" stream-shift "^1.0.0" +stream-exhaust@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/stream-exhaust/-/stream-exhaust-1.0.2.tgz#acdac8da59ef2bc1e17a2c0ccf6c320d120e555d" + integrity sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw== + stream-http@^2.7.2: version "2.8.3" resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" @@ -17744,7 +18440,7 @@ string-length@^3.1.0: astral-regex "^1.0.0" strip-ansi "^5.2.0" -string-width@^1.0.1: +string-width@^1.0.1, string-width@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= @@ -18047,6 +18743,14 @@ supports-color@^6.1.0: dependencies: has-flag "^3.0.0" +sver-compat@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/sver-compat/-/sver-compat-1.5.0.tgz#3cf87dfeb4d07b4a3f14827bc186b3fd0c645cd8" + integrity sha1-PPh9/rTQe0o/FIJ7wYaz/QxkXNg= + dependencies: + es6-iterator "^2.0.1" + es6-symbol "^3.1.1" + svg-parser@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" @@ -18321,7 +19025,15 @@ throttle-debounce@^2.1.0: resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.2.1.tgz#fbd933ae6793448816f7d5b3cae259d464c98137" integrity sha512-i9hAVld1f+woAiyNGqWelpDD5W1tpMroL3NofTz9xzwq6acWBlO2dC8k5EFSZepU6oOINtV5Q3aSPoRg7o4+fA== -through2@^2.0.0, through2@^2.0.3: +through2-filter@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254" + integrity sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA== + dependencies: + through2 "~2.0.0" + xtend "~4.0.0" + +through2@^2.0.0, through2@^2.0.3, through2@~2.0.0: version "2.0.5" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== @@ -18339,6 +19051,11 @@ thunky@^1.0.2: resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== +time-stamp@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3" + integrity sha1-dkpaEa9QVhkhsTPztE5hhofg9cM= + timed-out@^4.0.0, timed-out@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" @@ -18383,6 +19100,14 @@ tmpl@1.0.x: resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE= +to-absolute-glob@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1865f43d9e74b0822db9f145b78cff7d0f7c849b" + integrity sha1-GGX0PZ50sIItufFFt4z/fQ98hJs= + dependencies: + is-absolute "^1.0.0" + is-negated-glob "^1.0.0" + to-arraybuffer@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" @@ -18466,6 +19191,13 @@ to-space-case@^1.0.0: dependencies: to-no-case "^1.0.0" +to-through@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-through/-/to-through-2.0.0.tgz#fc92adaba072647bc0b67d6b03664aa195093af6" + integrity sha1-/JKtq6ByZHvAtn1rA2ZKoZUJOvY= + dependencies: + through2 "^2.0.3" + toggle-selection@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" @@ -18553,10 +19285,10 @@ 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== +truffle@5.1.41: + version "5.1.41" + resolved "https://registry.yarnpkg.com/truffle/-/truffle-5.1.41.tgz#662a0f2816c4e5a12bae25c0b68d908478ff4606" + integrity sha512-6vphA82Os7HvrzqkMy0o2kxP0SYsf7glHE8U8jk15lbUNOy76SrBLmTi7at7xFkIq6LMgv03YRf0EFEN/qwAxg== dependencies: app-module-path "^2.2.0" mocha "8.0.1" @@ -18713,9 +19445,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" @@ -18780,11 +19512,37 @@ unbzip2-stream@^1.0.9: buffer "^5.2.1" through "^2.3.8" +unc-path-regex@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" + integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= + underscore@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961" integrity sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg== +undertaker-registry@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/undertaker-registry/-/undertaker-registry-1.0.1.tgz#5e4bda308e4a8a2ae584f9b9a4359a499825cc50" + integrity sha1-XkvaMI5KiirlhPm5pDWaSZglzFA= + +undertaker@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/undertaker/-/undertaker-1.3.0.tgz#363a6e541f27954d5791d6fa3c1d321666f86d18" + integrity sha512-/RXwi5m/Mu3H6IHQGww3GNt1PNXlbeCuclF2QYR14L/2CHPz3DFZkvB5hZ0N/QUkiXWCACML2jXViIQEQc2MLg== + dependencies: + arr-flatten "^1.0.1" + arr-map "^2.0.0" + bach "^1.0.0" + collection-map "^1.0.0" + es6-weak-map "^2.0.1" + fast-levenshtein "^1.0.0" + last-run "^1.1.0" + object.defaults "^1.0.0" + object.reduce "^1.0.0" + undertaker-registry "^1.0.0" + unfetch@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.1.0.tgz#6ec2dd0de887e58a4dee83a050ded80ffc4137db" @@ -18847,6 +19605,14 @@ unique-slug@^2.0.0: dependencies: imurmurhash "^0.1.4" +unique-stream@^2.0.2: + version "2.3.1" + resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-2.3.1.tgz#c65d110e9a4adf9a6c5948b28053d9a8d04cbeac" + integrity sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A== + dependencies: + json-stable-stringify-without-jsonify "^1.0.1" + through2-filter "^3.0.0" + unique-string@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" @@ -19097,6 +19863,13 @@ v8-compile-cache@^2.0.3: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745" integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ== +v8flags@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-3.2.0.tgz#b243e3b4dfd731fa774e7492128109a0fe66d656" + integrity sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg== + dependencies: + homedir-polyfill "^1.0.1" + validate-npm-package-license@^3.0.1: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" @@ -19110,6 +19883,11 @@ value-equal@^1.0.1: resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== +value-or-function@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/value-or-function/-/value-or-function-3.0.0.tgz#1c243a50b595c1be54a754bfece8563b9ff8d813" + integrity sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM= + varint@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/varint/-/varint-5.0.0.tgz#d826b89f7490732fabc0c0ed693ed475dcb29ebf" @@ -19134,6 +19912,54 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +vinyl-fs@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-3.0.3.tgz#c85849405f67428feabbbd5c5dbdd64f47d31bc7" + integrity sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng== + dependencies: + fs-mkdirp-stream "^1.0.0" + glob-stream "^6.1.0" + graceful-fs "^4.0.0" + is-valid-glob "^1.0.0" + lazystream "^1.0.0" + lead "^1.0.0" + object.assign "^4.0.4" + pumpify "^1.3.5" + readable-stream "^2.3.3" + remove-bom-buffer "^3.0.0" + remove-bom-stream "^1.2.0" + resolve-options "^1.1.0" + through2 "^2.0.0" + to-through "^2.0.0" + value-or-function "^3.0.0" + vinyl "^2.0.0" + vinyl-sourcemap "^1.1.0" + +vinyl-sourcemap@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz#92a800593a38703a8cdb11d8b300ad4be63b3e16" + integrity sha1-kqgAWTo4cDqM2xHYswCtS+Y7PhY= + dependencies: + append-buffer "^1.0.2" + convert-source-map "^1.5.0" + graceful-fs "^4.1.6" + normalize-path "^2.1.1" + now-and-later "^2.0.0" + remove-bom-buffer "^3.0.0" + vinyl "^2.0.0" + +vinyl@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.0.tgz#d85b07da96e458d25b2ffe19fece9f2caa13ed86" + integrity sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg== + dependencies: + clone "^2.1.1" + clone-buffer "^1.0.0" + clone-stats "^1.0.0" + cloneable-readable "^1.0.0" + remove-trailing-separator "^1.0.1" + replace-ext "^1.0.0" + vm-browserify@^1.0.1: version "1.1.2" resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" @@ -19155,13 +19981,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" @@ -20203,6 +21029,7 @@ websocket@^1.0.31: dependencies: debug "^2.2.0" es5-ext "^0.10.50" + gulp "^4.0.2" nan "^2.14.0" typedarray-to-buffer "^3.1.5" yaeti "^0.0.6" @@ -20247,6 +21074,11 @@ whatwg-url@^7.0.0: tr46 "^1.0.1" webidl-conversions "^4.0.2" +which-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" + integrity sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8= + which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" @@ -20264,7 +21096,7 @@ which@2.0.2, which@^2.0.1: dependencies: isexe "^2.0.0" -which@^1.2.9, which@^1.3.0, which@^1.3.1: +which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== @@ -20444,6 +21276,14 @@ workerpool@6.0.0: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.0.0.tgz#85aad67fa1a2c8ef9386a1b43539900f61d03d58" integrity sha512-fU2OcNA/GVAJLLyKUoHkAgIhKb0JoCpSjLC/G2vYKxUjVmQwGbRVeoPJ1a8U4pnVofz4AQV5Y/NEw8oKqxEBtA== +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrap-ansi@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" @@ -20597,6 +21437,11 @@ xtend@~2.1.1: dependencies: object-keys "~0.4.0" +y18n@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" + integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= + y18n@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" @@ -20635,6 +21480,14 @@ yargs-parser@13.1.2, yargs-parser@^13.1.2: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@5.0.0-security.0: + version "5.0.0-security.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0-security.0.tgz#4ff7271d25f90ac15643b86076a2ab499ec9ee24" + integrity sha512-T69y4Ps64LNesYxeYGYPvfoMTt/7y1XtfpIslUeK4um+9Hu7hlGoRtaDLvdXb7+/tfq4opVa2HRY5xGip022rQ== + dependencies: + camelcase "^3.0.0" + object.assign "^4.1.0" + yargs-parser@^10.0.0: version "10.1.0" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" @@ -20692,6 +21545,25 @@ yargs@^15.3.1: y18n "^4.0.0" yargs-parser "^18.1.2" +yargs@^7.1.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.1.tgz#67f0ef52e228d4ee0d6311acede8850f53464df6" + integrity sha512-huO4Fr1f9PmiJJdll5kwoS2e4GqzGSsMT3PPMpOwoVkOK8ckqAewMTZyA6LXVQWflleb/Z8oPBEvNsMft0XE+g== + dependencies: + camelcase "^3.0.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^1.4.0" + read-pkg-up "^1.0.1" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^1.0.2" + which-module "^1.0.0" + y18n "^3.2.1" + yargs-parser "5.0.0-security.0" + yauzl@^2.10.0, yauzl@^2.4.2: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"