Merge pull request #1397 from gnosis/release/v2.12.0

Release v2.12.0
This commit is contained in:
Daniel Sanchez 2020-09-28 18:05:05 +02:00 committed by GitHub
commit 21bd4a2eb7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
233 changed files with 5065 additions and 2540 deletions

View File

@ -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

View File

@ -1,18 +1,16 @@
if: (branch = development) OR (branch = master) OR (type = pull_request) OR (tag IS present)
sudo: required
dist: xenial
services:
- docker
dist: bionic
language: node_js
node_js:
- '12'
os:
- linux
env:
global:
- DOCKER_COMPOSE_VERSION=1.22.0
matrix:
include:
- env:
- REACT_APP_ENV='production'
if: tag IS present
- env:
- REACT_APP_NETWORK='mainnet'
- STAGING_BUCKET_NAME=${STAGING_MAINNET_BUCKET_NAME}
@ -28,10 +26,12 @@ before_install:
- sudo apt-get update
- sudo apt-get -y install python-pip python-dev libusb-1.0-0-dev
- pip install awscli --upgrade --user
# Install truffle
- yarn global add truffle
script:
- bash ./config/travis/build.sh
- yarn lint:check
- yarn prettier:check
- yarn test:coverage
- yarn build
#- bash ./config/travis/build.sh
after_success:
# Pull Request - Deploy it to a review environment
# Travis doesn't do deploy step with pull requests builds

View File

@ -1,6 +1,6 @@
{
"name": "safe-react",
"version": "2.11.1",
"version": "2.12.0",
"description": "Allowing crypto users manage funds in a safer way",
"website": "https://github.com/gnosis/safe-react#readme",
"bugs": {
@ -38,7 +38,7 @@
"release": "electron-builder --mac --linux --windows -p always",
"start-mainnet": "REACT_APP_NETWORK=mainnet yarn start",
"start": "react-app-rewired start",
"test": "NODE_ENV=test && react-app-rewired test --env=jsdom",
"test": "react-app-rewired test --env=jsdom",
"test:coverage": "yarn test --coverage --watchAll=false",
"coveralls": "cat ./coverage/lcov.info | coveralls",
"storybook": "start-storybook -p 9009 -s public",
@ -164,21 +164,21 @@
]
},
"dependencies": {
"@gnosis.pm/safe-apps-sdk": "https://github.com/gnosis/safe-apps-sdk.git#development",
"@gnosis.pm/safe-apps-sdk": "0.4.0",
"@gnosis.pm/safe-contracts": "1.1.1-dev.2",
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#1bf397f",
"@gnosis.pm/util-contracts": "2.0.6",
"@ledgerhq/hw-transport-node-hid": "5.19.1",
"@ledgerhq/hw-transport-node-hid": "5.22.0",
"@material-ui/core": "4.11.0",
"@material-ui/icons": "4.9.1",
"@material-ui/lab": "4.0.0-alpha.56",
"@openzeppelin/contracts": "3.1.0",
"async-sema": "^3.1.0",
"axios": "0.19.2",
"axios": "0.20.0",
"bignumber.js": "9.0.0",
"bnc-onboard": "1.11.1",
"bnc-onboard": "1.13.1",
"classnames": "^2.2.6",
"concurrently": "^5.2.0",
"concurrently": "^5.3.0",
"connected-react-router": "6.8.0",
"coveralls": "^3.1.0",
"currency-flags": "2.1.2",
@ -190,20 +190,20 @@
"eth-sig-util": "^2.5.3",
"ethereum-blockies-base64": "^1.0.2",
"ethereumjs-abi": "0.6.8",
"exponential-backoff": "^3.0.1",
"exponential-backoff": "^3.1.0",
"express": "^4.17.1",
"final-form": "^4.20.1",
"final-form-calculate": "^1.3.1",
"history": "4.10.1",
"immortal-db": "^1.0.3",
"immortal-db": "^1.1.0",
"immutable": "^4.0.0-rc.12",
"js-cookie": "^2.2.1",
"lodash.debounce": "^4.0.8",
"lodash.memoize": "^4.1.2",
"material-ui-search-bar": "^1.0.0-beta.13",
"material-ui-search-bar": "^1.0.0",
"notistack": "https://github.com/gnosis/notistack.git#v0.9.4",
"open": "^7.1.0",
"polished": "3.6.5",
"open": "^7.2.0",
"polished": "3.6.7",
"qrcode.react": "1.0.0",
"query-string": "6.13.1",
"react": "16.13.1",
@ -215,7 +215,7 @@
"react-qr-reader": "^2.2.1",
"react-redux": "7.2.1",
"react-router-dom": "5.2.0",
"react-scripts": "^3.4.1",
"react-scripts": "^3.4.3",
"react-window": "^1.8.5",
"recompose": "^0.30.0",
"redux": "4.0.5",
@ -223,7 +223,7 @@
"redux-thunk": "^2.3.0",
"reselect": "^4.0.0",
"semver": "7.3.2",
"styled-components": "^5.1.1",
"styled-components": "^5.2.0",
"truffle-contract": "4.0.31",
"web3": "1.2.9",
"web3-core": "^1.2.11",
@ -236,45 +236,43 @@
"@storybook/addons": "^5.3.19",
"@storybook/preset-create-react-app": "^3.1.4",
"@storybook/react": "^5.3.19",
"@testing-library/jest-dom": "5.11.2",
"@testing-library/react": "10.4.8",
"@testing-library/user-event": "12.1.0",
"@testing-library/jest-dom": "5.11.4",
"@testing-library/react": "10.4.9",
"@typechain/web3-v1": "^1.0.0",
"@types/history": "4.6.2",
"@types/jest": "^26.0.9",
"@types/jest": "^26.0.14",
"@types/lodash.memoize": "^4.1.6",
"@types/node": "14.6.0",
"@types/react": "^16.9.47",
"@types/node": "14.11.2",
"@types/react": "^16.9.49",
"@types/react-dom": "^16.9.6",
"@types/react-redux": "^7.1.9",
"@types/react-router-dom": "^5.1.5",
"@types/styled-components": "^5.1.2",
"@types/styled-components": "^5.1.3",
"@typescript-eslint/eslint-plugin": "3.9.1",
"@typescript-eslint/parser": "3.9.1",
"autoprefixer": "9.8.6",
"cross-env": "^7.0.2",
"dotenv": "^8.2.0",
"dotenv-expand": "^5.1.0",
"electron": "7.2.4",
"electron": "9.3.0",
"electron-builder": "22.8.0",
"electron-notarize": "0.3.0",
"electron-notarize": "1.0.0",
"eslint": "6.8.0",
"eslint-config-prettier": "6.11.0",
"eslint-plugin-import": "2.22.0",
"eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.20.5",
"eslint-plugin-react": "^7.20.6",
"eslint-plugin-sort-destructure-keys": "1.3.5",
"ethereumjs-abi": "0.6.8",
"husky": "^4.2.5",
"lint-staged": "10.2.11",
"lint-staged": "10.4.0",
"node-sass": "^4.14.1",
"prettier": "2.0.5",
"prettier": "2.1.2",
"react-app-rewired": "^2.1.6",
"react-docgen-typescript-loader": "^3.7.2",
"truffle": "5.1.36",
"typechain": "^2.0.0",
"typescript": "3.9.7",
"wait-on": "5.1.0"
"wait-on": "5.2.0"
}
}

View File

@ -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

52
src/assets/icons/info.svg Normal file
View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g>
<g>
<path d="M256,0C114.497,0,0,114.507,0,256c0,141.503,114.507,256,256,256c141.503,0,256-114.507,256-256
C512,114.497,397.492,0,256,0z M256,472c-119.393,0-216-96.615-216-216c0-119.393,96.615-216,216-216
c119.393,0,216,96.615,216,216C472,375.393,375.384,472,256,472z" fill="#ff0000"/>
</g>
</g>
<g>
<g>
<path d="M256,214.33c-11.046,0-20,8.954-20,20v128.793c0,11.046,8.954,20,20,20s20-8.955,20-20.001V234.33
C276,223.284,267.046,214.33,256,214.33z" fill="#ff0000" />
</g>
</g>
<g>
<g>
<circle cx="256" cy="162.84" r="27" fill="#ff0000"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -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 (
<>
<Row align="center" className={classes.heading} grow>
<Paragraph className={classes.manage} noMargin size="xl" weight="bolder">
<Paragraph noMargin size="xl" weight="bolder">
Receive funds
</Paragraph>
<IconButton disableRipple onClick={onClose}>
@ -122,4 +128,4 @@ const Receive = ({ classes, onClose }) => {
)
}
export default withStyles(styles as any)(Receive)
export default ReceiveModal

View File

@ -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}
/>
<Modal
description="Receive Tokens Form"
handleClose={onReceiveHide}
open={safeActionsState.showReceive as boolean}
title="Receive Tokens"
>
<Receive onClose={onReceiveHide} />
</Modal>
{safeAddress && safeName && (
<Modal
description="Receive Tokens Form"
handleClose={onReceiveHide}
open={safeActionsState.showReceive as boolean}
title="Receive Tokens"
>
<Receive onClose={onReceiveHide} safeAddress={safeAddress} safeName={safeName} />
</Modal>
)}
</>
</SnackbarProvider>
<CookiesBanner />

View File

@ -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}

View File

@ -50,7 +50,7 @@ const HeaderComponent = (): React.ReactElement => {
}
const getProviderInfoBased = () => {
if (!loaded) {
if (!loaded || !provider) {
return <ProviderDisconnected />
}

View File

@ -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 = ({
<Container>
<IdenticonContainer>
<FlexSpacer />
<div>
<FixedIcon type="notConnected" />
</div>
<FixedIcon type="notConnected" />
<UnStyledButton onClick={onToggleSafeList} data-testid={TOGGLE_SIDEBAR_BTN_TESTID}>
<Icon size="md" type="circleDropdown" />
</UnStyledButton>

View File

@ -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 (
<>
<SafeHeader
address={safeAddress}
safeName={safeName}
granted={granted}
balance={balance}
onToggleSafeList={onToggleSafeList}
onReceiveClick={onReceiveClick}
onNewTransactionClick={onNewTransactionClick}
/>
}: Props): React.ReactElement => (
<>
<SafeHeader
address={safeAddress}
safeName={safeName}
granted={granted}
balance={balance}
onToggleSafeList={onToggleSafeList}
onReceiveClick={onReceiveClick}
onNewTransactionClick={onNewTransactionClick}
/>
{items.length ? (
<>
<StyledDivider />
<List items={items} />
</>
) : null}
<HelpContainer>
{items.length ? (
<>
<StyledDivider />
<HelpCenterLink href="https://help.gnosis-safe.io/en/" target="_blank" title="Help Center of Gnosis Safe">
<IconText text="HELP CENTER" iconSize="md" textSize="md" color="placeHolder" iconType="question" />
</HelpCenterLink>
</HelpContainer>
</>
)
}
<List items={items} />
</>
) : null}
<HelpContainer>
<StyledDivider />
<HelpCenterLink href="https://help.gnosis-safe.io/en/" target="_blank" title="Help Center of Gnosis Safe">
<IconText text="HELP CENTER" iconSize="md" textSize="md" color="placeHolder" iconType="question" />
</HelpCenterLink>
</HelpContainer>
</>
)
export default Sidebar

View File

@ -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

View File

@ -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),

View File

@ -82,7 +82,7 @@ const useStyles = makeStyles({
})
type Props = {
currentSafe: string | null
currentSafe: string | undefined
defaultSafe: DefaultSafe
safes: SafeRecord[]
onSafeClick: () => void

View File

@ -1,7 +1,7 @@
import React, { useEffect, useMemo, useState } from 'react'
import Drawer from '@material-ui/core/Drawer'
import SearchIcon from '@material-ui/icons/Search'
import SearchBar from 'material-ui-search-bar'
import * as React from 'react'
import { connect } from 'react-redux'
import SafeList from './SafeList'
@ -16,12 +16,11 @@ import Link from 'src/components/layout/Link'
import Row from 'src/components/layout/Row'
import { WELCOME_ADDRESS } from 'src/routes/routes'
import setDefaultSafe from 'src/logic/safe/store/actions/setDefaultSafe'
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
import { defaultSafeSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { AppReduxState } from 'src/store'
const { useEffect, useMemo, useState } = React
export const SafeListSidebarContext = React.createContext({
isOpen: false,
toggleSidebar: () => {},
@ -39,12 +38,7 @@ const SafeListSidebar = ({ children, currentSafe, defaultSafe, safes, setDefault
const [isOpen, setIsOpen] = useState(false)
const [filter, setFilter] = useState('')
const classes = useSidebarStyles()
useEffect(() => {
setTimeout(() => {
setFilter('')
}, 300)
}, [isOpen])
const { trackEvent } = useAnalytics()
const searchClasses = {
input: classes.searchInput,
@ -54,6 +48,9 @@ const SafeListSidebar = ({ children, currentSafe, defaultSafe, safes, setDefault
}
const toggleSidebar = () => {
if (!isOpen) {
trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Safe List Sidebar' })
}
setIsOpen((prevIsOpen) => !prevIsOpen)
}
@ -73,6 +70,12 @@ const SafeListSidebar = ({ children, currentSafe, defaultSafe, safes, setDefault
const filteredSafes = useMemo(() => filterBy(filter, safes), [safes, filter])
useEffect(() => {
setTimeout(() => {
setFilter('')
}, 300)
}, [isOpen])
return (
<SafeListSidebarContext.Provider value={{ isOpen, toggleSidebar }}>
<Drawer

View File

@ -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
}

View File

@ -1,17 +1,17 @@
import { Record, RecordOf } from 'immutable'
export interface AddressBookEntryProps {
export type AddressBookEntry = {
address: string
name: string
isOwner: boolean
}
export type AddressBookEntryRecord = RecordOf<AddressBookEntryProps>
export const makeAddressBookEntry = Record<AddressBookEntryProps>({
address: '',
name: '',
isOwner: false,
export const makeAddressBookEntry = ({
address = '',
name = '',
}: {
address: string
name?: string
}): AddressBookEntry => ({
address,
name,
})
export type AddressBookEntry = RecordOf<AddressBookEntryProps>
export type AddressBookState = AddressBookEntry[]

View File

@ -1,8 +0,0 @@
import { createAction } from 'redux-actions'
export const ADD_ADDRESS_BOOK = 'ADD_ADDRESS_BOOK'
export const addAddressBook = createAction(ADD_ADDRESS_BOOK, (addressBook, safeAddress) => ({
addressBook,
safeAddress,
}))

View File

@ -1,7 +1,22 @@
import { createAction } from 'redux-actions'
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
export const ADD_ENTRY = 'ADD_ENTRY'
export const addAddressBookEntry = createAction(ADD_ENTRY, (entry) => ({
entry,
}))
type addAddressBookEntryOptions = {
notifyEntryUpdate: boolean
}
export const addAddressBookEntry = createAction(
ADD_ENTRY,
(entry: AddressBookEntry, options: addAddressBookEntryOptions) => {
let notifyEntryUpdate = true
if (options) {
notifyEntryUpdate = options.notifyEntryUpdate
}
return {
entry,
shouldAvoidUpdatesNotifications: !notifyEntryUpdate,
}
},
)

View File

@ -1,8 +1,8 @@
import { createAction } from 'redux-actions'
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
export const ADD_OR_UPDATE_ENTRY = 'ADD_OR_UPDATE_ENTRY'
export const addOrUpdateAddressBookEntry = createAction(ADD_OR_UPDATE_ENTRY, (entryAddress, entry) => ({
entryAddress,
export const addOrUpdateAddressBookEntry = createAction(ADD_OR_UPDATE_ENTRY, (entry: AddressBookEntry) => ({
entry,
}))

View File

@ -1,7 +1,8 @@
import { createAction } from 'redux-actions'
import { AddressBookState } from 'src/logic/addressBook/model/addressBook'
export const LOAD_ADDRESS_BOOK = 'LOAD_ADDRESS_BOOK'
export const loadAddressBook = createAction(LOAD_ADDRESS_BOOK, (addressBook) => ({
export const loadAddressBook = createAction(LOAD_ADDRESS_BOOK, (addressBook: AddressBookState) => ({
addressBook,
}))

View File

@ -1,29 +1,17 @@
import { List } from 'immutable'
import { loadAddressBook } from 'src/logic/addressBook/store/actions/loadAddressBook'
import { buildAddressBook } from 'src/logic/addressBook/store/reducer/addressBook'
import { getAddressBookFromStorage } from 'src/logic/addressBook/utils'
import { safesListSelector } from 'src/logic/safe/store/selectors'
import { Dispatch } from 'redux'
const loadAddressBookFromStorage = () => async (dispatch, getState) => {
const loadAddressBookFromStorage = () => async (dispatch: Dispatch): Promise<void> => {
try {
const state = getState()
let storedAdBk = await getAddressBookFromStorage()
if (!storedAdBk) {
storedAdBk = []
}
let addressBook = buildAddressBook(storedAdBk)
// Fetch all the current safes, in case that we don't have a safe on the adbk, we add it
const safes = safesListSelector(state)
const adbkEntries = addressBook.keySeq().toArray()
safes.forEach((safe) => {
const { address } = safe
const found = adbkEntries.includes(address)
if (!found) {
addressBook = addressBook.set(address, List([]))
}
})
const addressBook = buildAddressBook(storedAdBk)
dispatch(loadAddressBook(addressBook))
} catch (err) {
// eslint-disable-next-line

View File

@ -2,6 +2,6 @@ import { createAction } from 'redux-actions'
export const REMOVE_ENTRY = 'REMOVE_ENTRY'
export const removeAddressBookEntry = createAction(REMOVE_ENTRY, (entryAddress) => ({
export const removeAddressBookEntry = createAction(REMOVE_ENTRY, (entryAddress: string) => ({
entryAddress,
}))

View File

@ -1,15 +0,0 @@
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { updateAddressBookEntry } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
import { saveAddressBook } from 'src/logic/addressBook/utils'
const saveAndUpdateAddressBook = (addressBook) => async (dispatch) => {
try {
dispatch(updateAddressBookEntry(makeAddressBookEntry(addressBook)))
await saveAddressBook(addressBook)
} catch (err) {
// eslint-disable-next-line
console.error('Error while loading active tokens from storage:', err)
}
}
export default saveAndUpdateAddressBook

View File

@ -1,7 +1,8 @@
import { createAction } from 'redux-actions'
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
export const UPDATE_ENTRY = 'UPDATE_ENTRY'
export const updateAddressBookEntry = createAction(UPDATE_ENTRY, (entry) => ({
export const updateAddressBookEntry = createAction(UPDATE_ENTRY, (entry: AddressBookEntry) => ({
entry,
}))

View File

@ -2,7 +2,7 @@ import { ADD_ENTRY } from 'src/logic/addressBook/store/actions/addAddressBookEnt
import { ADD_OR_UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
import { REMOVE_ENTRY } from 'src/logic/addressBook/store/actions/removeAddressBookEntry'
import { UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
import { addressBookMapSelector } from 'src/logic/addressBook/store/selectors'
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
import { saveAddressBook } from 'src/logic/addressBook/utils'
import { enhanceSnackbarForAction, getNotificationsFromTxType } from 'src/logic/notifications'
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
@ -16,15 +16,15 @@ const addressBookMiddleware = (store) => (next) => async (action) => {
if (watchedActions.includes(action.type)) {
const state = store.getState()
const { dispatch } = store
const addressBook = addressBookMapSelector(state)
if (addressBook) {
const addressBook = addressBookSelector(state)
if (addressBook.length) {
await saveAddressBook(addressBook)
}
switch (action.type) {
case ADD_ENTRY: {
const { isOwner } = action.payload.entry
if (!isOwner) {
const { shouldAvoidUpdatesNotifications } = action.payload
if (!shouldAvoidUpdatesNotifications) {
const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.ADDRESSBOOK_NEW_ENTRY)
dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded)))
}

View File

@ -1,130 +1,73 @@
import { List, Map } from 'immutable'
import { handleActions } from 'redux-actions'
import { AddressBookEntry, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { ADD_ADDRESS_BOOK } from 'src/logic/addressBook/store/actions/addAddressBook'
import { AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { ADD_ENTRY } from 'src/logic/addressBook/store/actions/addAddressBookEntry'
import { ADD_OR_UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
import { LOAD_ADDRESS_BOOK } from 'src/logic/addressBook/store/actions/loadAddressBook'
import { REMOVE_ENTRY } from 'src/logic/addressBook/store/actions/removeAddressBookEntry'
import { UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
import { getAddressesListFromAdbk } from 'src/logic/addressBook/utils'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
import { checksumAddress } from 'src/utils/checksumAddress'
import { getValidAddressBookName } from 'src/logic/addressBook/utils'
export const ADDRESS_BOOK_REDUCER_ID = 'addressBook'
export type AddressBookCollection = List<AddressBookEntry>
export type AddressBookState = Map<string, Map<string, AddressBookCollection>>
export const buildAddressBook = (storedAdbk) => {
let addressBookBuilt = Map([])
Object.entries(storedAdbk).forEach((adbkProps: any) => {
const safeAddress = checksumAddress(adbkProps[0])
const adbkRecords = adbkProps[1].map(makeAddressBookEntry)
const adbkSafeEntries = List(adbkRecords)
addressBookBuilt = addressBookBuilt.set(safeAddress, adbkSafeEntries)
export const buildAddressBook = (storedAddressBook: AddressBookState): AddressBookState => {
return storedAddressBook.map((addressBookEntry) => {
const { address, name } = addressBookEntry
return makeAddressBookEntry({ address: checksumAddress(address), name })
})
return addressBookBuilt
}
export default handleActions(
{
[LOAD_ADDRESS_BOOK]: (state, action) => {
const { addressBook } = action.payload
return state.set('addressBook', addressBook)
},
[ADD_ADDRESS_BOOK]: (state, action) => {
const { addressBook, safeAddress } = action.payload
// Adds the address book if it does not exists
const found = state.getIn(['addressBook', safeAddress])
if (!found) {
return state.setIn(['addressBook', safeAddress], addressBook)
}
return state
return addressBook
},
[ADD_ENTRY]: (state, action) => {
const { entry } = action.payload
// Adds the entry to all the safes (if it does not already exists)
const newState = state.withMutations((map) => {
const adbkMap = map.get('addressBook')
const entryFound = state.find((oldEntry) => oldEntry.address === entry.address)
if (adbkMap) {
adbkMap.keySeq().forEach((safeAddress) => {
const safeAddressBook = state.getIn(['addressBook', safeAddress])
if (safeAddressBook) {
const adbkAddressList = getAddressesListFromAdbk(safeAddressBook)
const found = adbkAddressList.includes(entry.address)
if (!found) {
const updatedSafeAdbkList = safeAddressBook.push(entry)
map.setIn(['addressBook', safeAddress], updatedSafeAdbkList)
}
}
})
}
})
return newState
// Only adds entries with valid names
const validName = getValidAddressBookName(entry.name)
if (!entryFound && validName) {
state.push(entry)
}
return state
},
[UPDATE_ENTRY]: (state, action) => {
const { entry } = action.payload
// Updates the entry from all the safes
const newState = state.withMutations((map) => {
map
.get('addressBook')
.keySeq()
.forEach((safeAddress) => {
const entriesList = state.getIn(['addressBook', safeAddress])
const entryIndex = entriesList.findIndex((entryItem) => sameAddress(entryItem.address, entry.address))
const updatedEntriesList = entriesList.set(entryIndex, entry)
map.setIn(['addressBook', safeAddress], updatedEntriesList)
})
})
return newState
const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entry.address)
if (entryIndex >= 0) {
state[entryIndex] = entry
}
return state
},
[REMOVE_ENTRY]: (state, action) => {
const { entryAddress } = action.payload
// Removes the entry from all the safes
const newState = state.withMutations((map) => {
map
.get('addressBook')
.keySeq()
.forEach((safeAddress) => {
const entriesList = state.getIn(['addressBook', safeAddress])
const entryIndex = entriesList.findIndex((entry) => sameAddress(entry.address, entryAddress))
const updatedEntriesList = entriesList.remove(entryIndex)
map.setIn(['addressBook', safeAddress], updatedEntriesList)
})
})
return newState
const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entryAddress)
state.splice(entryIndex, 1)
return state
},
[ADD_OR_UPDATE_ENTRY]: (state, action) => {
const { entry, entryAddress } = action.payload
const { entry } = action.payload
// Adds or Updates the entry to all the safes
return state.withMutations((map) => {
const addressBook = map.get('addressBook')
if (addressBook) {
addressBook.keySeq().forEach((safeAddress) => {
const safeAddressBook = state.getIn(['addressBook', safeAddress])
const entryIndex = safeAddressBook.findIndex((entryItem) => sameAddress(entryItem.address, entryAddress))
// Only updates entries with valid names
const validName = getValidAddressBookName(entry.name)
if (!validName) {
return state
}
if (entryIndex !== -1) {
const updatedEntriesList = safeAddressBook.update(entryIndex, (currentEntry) => currentEntry.merge(entry))
map.setIn(['addressBook', safeAddress], updatedEntriesList)
} else {
const updatedSafeAdbkList = safeAddressBook.push(makeAddressBookEntry(entry))
map.setIn(['addressBook', safeAddress], updatedSafeAdbkList)
}
})
}
})
const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entry.address)
if (entryIndex >= 0) {
state[entryIndex] = entry
} else {
state.push(entry)
}
return state
},
},
Map({
addressBook: Map({}),
}),
[],
)

View File

@ -1,24 +0,0 @@
import { AddressBookEntryRecord, AddressBookEntryProps } from 'src/logic/addressBook/model/addressBook'
import { Map, List } from 'immutable'
export interface AddressBookReducerState {
addressBook: AddressBookMap
}
interface AddressBookMapSerialized {
[key: string]: AddressBookEntryProps
}
interface AddressBookReducerStateSerialized extends AddressBookReducerState {
addressBook: Record<string, AddressBookEntryProps[]>
}
export interface AddressBookMap extends Map<string> {
toJS(): AddressBookMapSerialized
get(key: string, notSetValue: unknown): List<AddressBookEntryRecord>
}
export interface AddressBookReducerMap extends Map<string, any> {
toJS(): AddressBookReducerStateSerialized
get<K extends keyof AddressBookReducerState>(key: K): AddressBookReducerState[K]
}

View File

@ -1,35 +1,22 @@
import { AppReduxState } from 'src/store'
import { List } from 'immutable'
import { createSelector } from 'reselect'
import { ADDRESS_BOOK_REDUCER_ID } from 'src/logic/addressBook/store/reducer/addressBook'
import { AddressBookMap } from 'src/logic/addressBook/store/reducer/types/addressBook.d'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
export const addressBookMapSelector = (state: AppReduxState): AddressBookMap =>
state[ADDRESS_BOOK_REDUCER_ID].get('addressBook')
import { AddressBookState } from 'src/logic/addressBook/model/addressBook'
export const getAddressBook = createSelector(
addressBookMapSelector,
safeParamAddressFromStateSelector,
(addressBook, safeAddress) => {
let result = List([])
if (addressBook) {
result = addressBook.get(safeAddress, List())
}
return result
},
)
export const addressBookSelector = (state: AppReduxState): AddressBookState => state[ADDRESS_BOOK_REDUCER_ID]
export const getNameFromAddressBook = createSelector(
getAddressBook,
export const getNameFromAddressBookSelector = createSelector(
addressBookSelector,
(_, address) => address,
(addressBook, address) => {
const adbkEntry = addressBook.find((addressBookItem) => addressBookItem.address === address)
if (adbkEntry) {
return adbkEntry.name
}
return 'UNKNOWN'
},
)

View File

@ -0,0 +1,312 @@
import { List } from 'immutable'
import {
checkIfEntryWasDeletedFromAddressBook,
getAddressBookFromStorage,
getAddressesListFromAddressBook,
getNameFromAddressBook,
getOwnersWithNameFromAddressBook,
isValidAddressBookName,
migrateOldAddressBook,
OldAddressBookEntry,
OldAddressBookType,
saveAddressBook,
} from 'src/logic/addressBook/utils/index'
import { buildAddressBook } from 'src/logic/addressBook/store/reducer/addressBook'
import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
const getMockAddressBookEntry = (address: string, name: string = 'test'): AddressBookEntry =>
makeAddressBookEntry({
address,
name,
})
const getMockOldAddressBookEntry = ({ address = '', name = '', isOwner = false }): OldAddressBookEntry => {
return {
address,
name,
isOwner,
}
}
describe('getAddressesListFromAdbk', () => {
const entry1 = getMockAddressBookEntry('123456', 'test1')
const entry2 = getMockAddressBookEntry('78910', 'test2')
const entry3 = getMockAddressBookEntry('4781321', 'test3')
it('It should returns the list of addresses within the addressBook given a safeAddressBook', () => {
// given
const safeAddressBook = [entry1, entry2, entry3]
const expectedResult = [entry1.address, entry2.address, entry3.address]
// when
const result = getAddressesListFromAddressBook(safeAddressBook)
// then
expect(result).toStrictEqual(expectedResult)
})
})
describe('getNameFromSafeAddressBook', () => {
const entry1 = getMockAddressBookEntry('123456', 'test1')
const entry2 = getMockAddressBookEntry('78910', 'test2')
const entry3 = getMockAddressBookEntry('4781321', 'test3')
it('It should returns the user name given a safeAddressBook and an user account', () => {
// given
const safeAddressBook = [entry1, entry2, entry3]
const expectedResult = entry2.name
// when
const result = getNameFromAddressBook(safeAddressBook, entry2.address)
// then
expect(result).toBe(expectedResult)
})
})
describe('getOwnersWithNameFromAddressBook', () => {
const entry1 = getMockAddressBookEntry('123456', 'test1')
const entry2 = getMockAddressBookEntry('78910', 'test2')
const entry3 = getMockAddressBookEntry('4781321', 'test3')
it('It should returns the list of owners with their names given a safeAddressBook and a list of owners', () => {
// given
const safeAddressBook = [entry1, entry2, entry3]
const ownerList = List([
{ address: entry1.address, name: '' },
{ address: entry2.address, name: '' },
])
const expectedResult = List([
{ address: entry1.address, name: entry1.name },
{ address: entry2.address, name: entry2.name },
])
// when
const result = getOwnersWithNameFromAddressBook(safeAddressBook, ownerList)
// then
expect(result).toStrictEqual(expectedResult)
})
})
jest.mock('src/utils/storage/index')
describe('saveAddressBook', () => {
const mockAdd1 = '0x696fd93D725d84acfFf6c62a1fe8C94E1c9E934A'
const mockAdd2 = '0x2C7aC78b01Be0FC66AD29b684ffAb0C93B381D00'
const mockAdd3 = '0x537BD452c3505FC07bA242E437bD29D4E1DC9126'
const entry1 = getMockAddressBookEntry(mockAdd1, 'test1')
const entry2 = getMockAddressBookEntry(mockAdd2, 'test2')
const entry3 = getMockAddressBookEntry(mockAdd3, 'test3')
afterAll(() => {
jest.unmock('src/utils/storage/index')
})
it('It should save a given addressBook to the localStorage', async () => {
// given
const addressBook: AddressBookState = [entry1, entry2, entry3]
// when
await saveAddressBook(addressBook)
const storageUtils = require('src/utils/storage/index')
const spy = storageUtils.loadFromStorage.mockImplementationOnce(() => JSON.stringify(addressBook))
const storedAddressBook = await getAddressBookFromStorage()
// @ts-ignore
let result = buildAddressBook(storedAddressBook)
// then
expect(result).toStrictEqual(addressBook)
expect(spy).toHaveBeenCalled()
})
})
describe('migrateOldAddressBook', () => {
const safeAddress1 = '0x696fd93D725d84acfFf6c62a1fe8C94E1c9E934A'
const safeAddress2 = '0x2C7aC78b01Be0FC66AD29b684ffAb0C93B381D00'
const mockAdd1 = '0x9163c2F4452E3399CB60AAf737231Af87548DA91'
const mockAdd2 = '0xC4e446Da9C3D37385C86488294C6758c4e25dbD8'
it('It should receive an addressBook in old format and return the same addressBook in new format', () => {
// given
const entry1 = getMockOldAddressBookEntry({ name: 'test1', address: mockAdd1 })
const entry2 = getMockOldAddressBookEntry({ name: 'test2', address: mockAdd2 })
const oldAddressBook: OldAddressBookType = {
[safeAddress1]: [entry1],
[safeAddress2]: [entry2],
}
const expectedEntry1 = getMockAddressBookEntry(mockAdd1, 'test1')
const expectedEntry2 = getMockAddressBookEntry(mockAdd2, 'test2')
const expectedResult = [expectedEntry1, expectedEntry2]
// when
const result = migrateOldAddressBook(oldAddressBook)
// then
expect(result).toStrictEqual(expectedResult)
})
})
describe('getAddressBookFromStorage', () => {
const safeAddress1 = '0x696fd93D725d84acfFf6c62a1fe8C94E1c9E934A'
const safeAddress2 = '0x2C7aC78b01Be0FC66AD29b684ffAb0C93B381D00'
const mockAdd1 = '0x9163c2F4452E3399CB60AAf737231Af87548DA91'
const mockAdd2 = '0xC4e446Da9C3D37385C86488294C6758c4e25dbD8'
beforeAll(() => {
jest.mock('src/utils/storage/index')
})
afterAll(() => {
jest.unmock('src/utils/storage/index')
})
it('It should return null if no addressBook in storage', async () => {
// given
const expectedResult = null
const storageUtils = require('src/utils/storage/index')
const spy = storageUtils.loadFromStorage.mockImplementationOnce(() => null)
// when
const result = await getAddressBookFromStorage()
// then
expect(result).toStrictEqual(expectedResult)
expect(spy).toHaveBeenCalled()
})
it('It should return migrated addressBook if old addressBook in storage', async () => {
// given
const expectedEntry1 = getMockAddressBookEntry(mockAdd1, 'test1')
const expectedEntry2 = getMockAddressBookEntry(mockAdd2, 'test2')
const entry1 = getMockOldAddressBookEntry({ name: 'test1', address: mockAdd1 })
const entry2 = getMockOldAddressBookEntry({ name: 'test2', address: mockAdd2 })
const oldAddressBook: OldAddressBookType = {
[safeAddress1]: [entry1],
[safeAddress2]: [entry2],
}
const expectedResult = [expectedEntry1, expectedEntry2]
const storageUtils = require('src/utils/storage/index')
const spy = storageUtils.loadFromStorage.mockImplementationOnce(() => oldAddressBook)
// when
const result = await getAddressBookFromStorage()
// then
expect(result).toStrictEqual(expectedResult)
expect(spy).toHaveBeenCalled()
})
it('It should return addressBook if addressBook in storage', async () => {
// given
const expectedEntry1 = getMockAddressBookEntry(mockAdd1, 'test1')
const expectedEntry2 = getMockAddressBookEntry(mockAdd2, 'test2')
const expectedResult = [expectedEntry1, expectedEntry2]
const storageUtils = require('src/utils/storage/index')
const spy = storageUtils.loadFromStorage.mockImplementationOnce(() => JSON.stringify(expectedResult))
// when
const result = await getAddressBookFromStorage()
// then
expect(result).toStrictEqual(expectedResult)
expect(spy).toHaveBeenCalled()
})
})
describe('isValidAddressBookName', () => {
it('It should return false if given a blacklisted name like UNKNOWN', () => {
// given
const addressNameInput = 'UNKNOWN'
const expectedResult = false
// when
const result = isValidAddressBookName(addressNameInput)
// then
expect(result).toStrictEqual(expectedResult)
})
it('It should return false if given a blacklisted name like MY WALLET', () => {
// given
const addressNameInput = 'MY WALLET'
const expectedResult = false
// when
const result = isValidAddressBookName(addressNameInput)
// then
expect(result).toStrictEqual(expectedResult)
})
it('It should return false if given a blacklisted name like OWNER #', () => {
// given
const addressNameInput = 'OWNER #'
const expectedResult = false
// when
const result = isValidAddressBookName(addressNameInput)
// then
expect(result).toStrictEqual(expectedResult)
})
it('It should return true if the given address name is valid', () => {
// given
const addressNameInput = 'User'
const expectedResult = true
// when
const result = isValidAddressBookName(addressNameInput)
// then
expect(result).toEqual(expectedResult)
})
})
describe('checkIfEntryWasDeletedFromAddressBook', () => {
const mockAdd1 = '0x696fd93D725d84acfFf6c62a1fe8C94E1c9E934A'
const mockAdd2 = '0x2C7aC78b01Be0FC66AD29b684ffAb0C93B381D00'
const mockAdd3 = '0x537BD452c3505FC07bA242E437bD29D4E1DC9126'
const entry1 = getMockAddressBookEntry(mockAdd1, 'test1')
const entry2 = getMockAddressBookEntry(mockAdd2, 'test2')
const entry3 = getMockAddressBookEntry(mockAdd3, 'test3')
it('It should return true if a given entry was deleted from addressBook', () => {
// given
const addressBookEntry = entry1
const addressBook: AddressBookState = [entry2, entry3]
const safeAlreadyLoaded = true
const expectedResult = true
// when
const result = checkIfEntryWasDeletedFromAddressBook(addressBookEntry, addressBook, safeAlreadyLoaded)
// then
expect(result).toEqual(expectedResult)
})
it('It should return false if a given entry was not deleted from addressBook', () => {
// given
const addressBookEntry = entry1
const addressBook: AddressBookState = [entry1, entry2, entry3]
const safeAlreadyLoaded = true
const expectedResult = false
// when
const result = checkIfEntryWasDeletedFromAddressBook(addressBookEntry, addressBook, safeAlreadyLoaded)
// then
expect(result).toEqual(expectedResult)
})
it('It should return false if the safe was not already loaded', () => {
// given
const addressBookEntry = entry1
const addressBook: AddressBookState = [entry1, entry2, entry3]
const safeAlreadyLoaded = false
const expectedResult = false
// when
const result = checkIfEntryWasDeletedFromAddressBook(addressBookEntry, addressBook, safeAlreadyLoaded)
// then
expect(result).toEqual(expectedResult)
})
})

View File

@ -1,47 +1,140 @@
import { List } from 'immutable'
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
import { AddressBookEntryProps } from './../model/addressBook'
import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { SafeOwner } from 'src/logic/safe/store/models/safe'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
const ADDRESS_BOOK_STORAGE_KEY = 'ADDRESS_BOOK_STORAGE_KEY'
export const getAddressBookFromStorage = async (): Promise<Array<AddressBookEntryProps> | undefined> => {
const data = await loadFromStorage<Array<AddressBookEntryProps>>(ADDRESS_BOOK_STORAGE_KEY)
return data
export type OldAddressBookEntry = {
address: string
name: string
isOwner: boolean
}
export const saveAddressBook = async (addressBook) => {
export type OldAddressBookType = {
[safeAddress: string]: [OldAddressBookEntry]
}
const ADDRESSBOOK_INVALID_NAMES = ['UNKNOWN', 'OWNER #', 'MY WALLET']
export const migrateOldAddressBook = (oldAddressBook: OldAddressBookType): AddressBookState => {
const values: AddressBookState = []
const adbkValues = Object.values(oldAddressBook)
for (const safeIterator of adbkValues) {
for (const safeAddressBook of safeIterator) {
if (!values.find((entry) => sameAddress(entry.address, safeAddressBook.address))) {
values.push(makeAddressBookEntry({ address: safeAddressBook.address, name: safeAddressBook.name }))
}
}
}
return values
}
export const getAddressBookFromStorage = async (): Promise<AddressBookState | null> => {
const result: OldAddressBookType | string | undefined = await loadFromStorage(ADDRESS_BOOK_STORAGE_KEY)
if (!result) {
return null
}
if (typeof result === 'string') {
return JSON.parse(result)
}
return migrateOldAddressBook(result as OldAddressBookType)
}
export const saveAddressBook = async (addressBook: AddressBookState): Promise<void> => {
try {
await saveToStorage(ADDRESS_BOOK_STORAGE_KEY, addressBook.toJSON())
await saveToStorage(ADDRESS_BOOK_STORAGE_KEY, JSON.stringify(addressBook))
} catch (err) {
console.error('Error storing addressBook in localstorage', err)
}
}
export const getAddressesListFromAdbk = (addressBook) => Array.from(addressBook).map((entry: any) => entry.address)
export const getAddressesListFromAddressBook = (addressBook: AddressBookState): string[] =>
addressBook.map((entry) => entry.address)
export const getNameFromAdbk = (addressBook, userAddress) => {
type GetNameFromAddressBookOptions = {
filterOnlyValidName: boolean
}
export const getNameFromAddressBook = (
addressBook: AddressBookState,
userAddress: string,
options?: GetNameFromAddressBookOptions,
): string | null => {
const entry = addressBook.find((addressBookItem) => addressBookItem.address === userAddress)
if (entry) {
return entry.name
return options?.filterOnlyValidName ? getValidAddressBookName(entry.name) : entry.name
}
return null
}
export const isValidAddressBookName = (addressBookName: string): boolean => {
const hasInvalidName = ADDRESSBOOK_INVALID_NAMES.find((invalidName) =>
addressBookName.toUpperCase().includes(invalidName),
)
return !hasInvalidName
}
export const getValidAddressBookName = (addressBookName: string): string | null => {
return isValidAddressBookName(addressBookName) ? addressBookName : null
}
export const getOwnersWithNameFromAddressBook = (
addressBook: AddressBookEntryProps,
addressBook: AddressBookState,
ownerList: List<SafeOwner>,
): List<SafeOwner> | [] => {
): List<SafeOwner> => {
if (!ownerList) {
return []
return List([])
}
const ownersListWithAdbkNames = ownerList.map((owner) => {
const ownerName = getNameFromAdbk(addressBook, owner.address)
return ownerList.map((owner) => {
const ownerName = getNameFromAddressBook(addressBook, owner.address)
return {
address: owner.address,
name: ownerName || owner.name,
}
})
return ownersListWithAdbkNames
}
export const formatAddressListToAddressBookNames = (
addressBook: AddressBookState,
addresses: string[],
): AddressBookEntry[] => {
if (!addresses.length) {
return []
}
return addresses.map((address) => {
const ownerName = getNameFromAddressBook(addressBook, address)
return {
address: address,
name: ownerName || '',
}
})
}
/**
* If the safe is not loaded, the owner wasn't not deleted
* If the safe is already loaded and the owner has a valid name, will return true if the address is not already on the addressBook
* @param name
* @param address
* @param addressBook
* @param safeAlreadyLoaded
*/
export const checkIfEntryWasDeletedFromAddressBook = (
{ name, address }: AddressBookEntry,
addressBook: AddressBookState,
safeAlreadyLoaded: boolean,
): boolean => {
if (!safeAlreadyLoaded) {
return false
}
const addressShouldBeOnTheAddressBook = !!getValidAddressBookName(name)
const isAlreadyInAddressBook = !!addressBook.find((entry) => sameAddress(entry.address, address))
return addressShouldBeOnTheAddressBook && !isAlreadyInAddressBook
}

View File

@ -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
}

View File

@ -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

View File

@ -98,11 +98,9 @@ export const estimateGasForDeployingSafe = async (
return gas * parseInt(gasPrice, 10)
}
export const getGnosisSafeInstanceAt = async (safeAddress: string): Promise<GnosisSafe> => {
export const getGnosisSafeInstanceAt = (safeAddress: string): GnosisSafe => {
const web3 = getWeb3()
const gnosisSafe = await new web3.eth.Contract(GnosisSafeSol.abi as AbiItem[], safeAddress) as unknown as GnosisSafe
return gnosisSafe
return new web3.eth.Contract(GnosisSafeSol.abi as AbiItem[], safeAddress) as unknown as GnosisSafe
}
const cleanByteCodeMetadata = (bytecode: string): string => {

View File

@ -0,0 +1,56 @@
import { aNewStore } from 'src/store'
import fetchTokenCurrenciesBalances from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances'
import axios from 'axios'
import { getTxServiceHost } from 'src/config'
jest.mock('axios')
describe('fetchTokenCurrenciesBalances', () => {
let store
const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
const excludeSpamTokens = true
beforeEach(() => {
store = aNewStore()
})
afterAll(() => {
jest.unmock('axios')
})
it('Given a safe address, calls the API and returns token balances', async () => {
// given
const expectedResult = [
{
balance: '849890000000000000',
balanceUsd: '337.2449',
token: null,
tokenAddress: null,
usdConversion: '396.81',
},
{
balance: '24698677800000000000',
balanceUsd: '29.3432',
token: {
name: 'Dai',
symbol: 'DAI',
decimals: 18,
logoUri: 'https://gnosis-safe-token-logos.s3.amazonaws.com/0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa.png',
},
tokenAddress: '0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa',
usdConversion: '1.188',
},
]
const apiUrl = getTxServiceHost()
// @ts-ignore
axios.get.mockImplementationOnce(() => Promise.resolve(expectedResult))
// when
const result = await fetchTokenCurrenciesBalances(safeAddress, excludeSpamTokens)
// then
expect(result).toStrictEqual(expectedResult)
expect(axios.get).toHaveBeenCalled()
expect(axios.get).toBeCalledWith(`${apiUrl}safes/${safeAddress}/balances/usd/?exclude_spam=${excludeSpamTokens}`, {
params: { limit: 3000 },
})
})
})

View File

@ -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<AxiosResponse<BalanceEndpoint[]>> => {
if (!safeAddress) {
return null
}
const fetchTokenCurrenciesBalances = (
safeAddress: string,
excludeSpamTokens = true,
): Promise<AxiosResponse<BalanceEndpoint[]>> => {
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: {

View File

@ -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<typeof setCurrencyBalances | typeof setSelectedCurrency | typeof setCurrencyRate>,
): Promise<void> => {
try {
const storedCurrencies: Map<string, CurrencyRateValue> | 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()
}

View File

@ -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<typeof setCurrencyBalances | typeof setSelectedCurrency | typeof setCurrencyRate>,
): Promise<void> => {
try {
const storedSelectedCurrency = await loadSelectedCurrency()
dispatch(setSelectedCurrency(safeAddress, storedSelectedCurrency || AVAILABLE_CURRENCIES.USD))
} catch (err) {
console.error('Error fetching currency values', err)
}
return Promise.resolve()
}

View File

@ -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,
}),
)

View File

@ -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<AppReduxState, undefined, AnyAction>,
): void => {
dispatch(setCurrentCurrency(safeAddress, selectedCurrency))
dispatch(fetchCurrencyRate(safeAddress, selectedCurrency))
}

View File

@ -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<string, CurrencyRateValue> = 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
}

View File

@ -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)
},
)

View File

@ -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<string, CurrencyRateValue>): Promise<void> => {
const SELECTED_CURRENCY_STORAGE_KEY = 'SELECTED_CURRENCY'
export const saveSelectedCurrency = async (selectedCurrency: AVAILABLE_CURRENCIES): Promise<void> => {
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<Map<string, CurrencyRateValue> | unknown> => {
return (await loadFromStorage(CURRENCY_VALUES_STORAGE_KEY)) || {}
export const loadSelectedCurrency = async (): Promise<AVAILABLE_CURRENCIES | undefined> => {
return await loadFromStorage(SELECTED_CURRENCY_STORAGE_KEY)
}

View File

@ -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))
}

View File

@ -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<void> => {
const currentSession = await getCurrentSessionFromStorage()
dispatch(loadCurrentSession(makeCurrentSession(currentSession ? currentSession : {})))
dispatch(loadCurrentSession(currentSession))
}
export default loadCurrentSessionFromStorage

View File

@ -1,5 +0,0 @@
import { Record } from 'immutable'
export const makeCurrentSession = Record({
viewedSafes: [],
})

View File

@ -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,
)

View File

@ -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<CurrentSessionState | undefined> =>
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)
}

View File

@ -16,7 +16,7 @@ interface DebounceOptions {
export const useDebouncedCallback = <T extends (...args: unknown[]) => unknown>(
callback: T,
delay = 0,
options: DebounceOptions,
options?: DebounceOptions,
): T & { cancel: () => void } => useCallback(debounce(callback, delay, options), [callback, delay, options])
export const useDebounce = <T extends unknown>(value: T, delay = 0, options?: DebounceOptions): T => {

View File

@ -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 = (

View File

@ -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))
})
}

View File

@ -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<Dispatch>()
useEffect(() => {
@ -22,13 +22,13 @@ export const useLoadSafe = (safeAddress: string): void => {
return dispatch(fetchSafeTokens(safeAddress))
})
.then(() => {
dispatch(loadAddressBookFromStorage())
dispatch(fetchSafeCreationTx(safeAddress))
dispatch(fetchTransactions(safeAddress))
return dispatch(addViewedSafe(safeAddress))
})
}
}
dispatch(loadAddressBookFromStorage())
fetchData()
}, [dispatch, safeAddress])

View File

@ -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<number>(null)
const timer = useRef<number>()
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)
}
}

View File

@ -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<string, Record<TokenProps> & Readonly<TokenProps>>()
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<string, Record<TokenProps> & Readonly<TokenProps>>()
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<string, Record<TokenProps> & Readonly<TokenProps>>()
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<string, Record<TokenProps> & Readonly<TokenProps>>()
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<string, Record<TokenProps> & Readonly<TokenProps>>()
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')
})
})

View File

@ -0,0 +1,9 @@
import { createAction } from 'redux-actions'
import { SafeRecordProps } from '../models/safe'
export const ADD_OR_UPDATE_SAFE = 'ADD_OR_UPDATE_SAFE'
export const addOrUpdateSafe = createAction(ADD_OR_UPDATE_SAFE, (safe: SafeRecordProps) => ({
safe,
}))

View File

@ -18,15 +18,16 @@ export const buildOwnersFrom = (names: string[], addresses: string[]): List<Safe
return List(owners)
}
export const addSafe = createAction(ADD_SAFE, (safe) => ({
export const addSafe = createAction(ADD_SAFE, (safe: SafeRecordProps, loadedFromStorage = false) => ({
safe,
loadedFromStorage,
}))
const saveSafe = (safe: SafeRecordProps) => (dispatch: Dispatch, getState: () => AppReduxState): void => {
const state = getState()
const safeList = safesListSelector(state)
dispatch(addSafe(safe))
dispatch(addSafe(safe, true))
if (safeList.size === 0) {
dispatch(setDefaultSafe(safe.address))

View File

@ -2,7 +2,7 @@ import axios, { AxiosResponse } from 'axios'
import { getAllTransactionsUriFrom, getTxServiceHost } from 'src/config'
import { checksumAddress } from 'src/utils/checksumAddress'
import { Transaction } from '../../models/types/transactions'
import { Transaction } from '../../models/types/transactions.d'
export type ServiceUriParams = {
safeAddress: string
@ -30,8 +30,8 @@ const getAllTransactionsUri = (safeAddress: string): string => {
const fetchAllTransactions = async (
urlParams: ServiceUriParams,
eTag: string | null,
): Promise<{ responseEtag: string; results: Transaction[]; count?: number }> => {
eTag?: string,
): Promise<{ responseEtag?: string; results: Transaction[]; count?: number }> => {
const { safeAddress, limit, offset, orderBy, queued, trusted } = urlParams
try {
const url = getAllTransactionsUri(safeAddress)

View File

@ -1,5 +1,5 @@
import { createAction } from 'redux-actions'
import { Transaction } from '../../models/types/transactions'
import { Transaction } from '../../models/types/transactions.d'
export const LOAD_MORE_TRANSACTIONS = 'LOAD_MORE_TRANSACTIONS'

View File

@ -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<Promise<void>, AppReduxState, undefined, AnyAction>
type ConfirmEventHandler = (safeTxHash: string) => void
type ErrorEventHandler = () => void
const createTransaction = (
{
@ -124,6 +126,7 @@ const createTransaction = (
origin = null,
}: CreateTransactionArgs,
onUserConfirm?: ConfirmEventHandler,
onError?: ErrorEventHandler,
): CreateTransactionAction => async (dispatch: Dispatch, getState: () => AppReduxState): Promise<void> => {
const state = getState()
@ -169,6 +172,7 @@ const createTransaction = (
sender: from,
sigs,
}
const safeTxHash = generateSafeTxHash(safeAddress, txArgs)
try {
// Here we're checking that safe contract version is greater or equal 1.1.1, but
@ -176,20 +180,19 @@ const createTransaction = (
const canTryOffchainSigning =
!isExecution && !smartContractWallet && semverSatisfies(safeVersion, SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES)
if (canTryOffchainSigning) {
const signature = await tryOffchainSigning({ ...txArgs, safeAddress }, hardwareWallet)
const signature = await tryOffchainSigning(safeTxHash, { ...txArgs, safeAddress }, hardwareWallet)
if (signature) {
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
dispatch(enqueueSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded))
dispatch(fetchTransactions(safeAddress))
await saveTxToHistory({ ...txArgs, signature, origin })
dispatch(enqueueSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded))
dispatch(fetchTransactions(safeAddress))
onUserConfirm?.(safeTxHash)
return
}
}
const safeTxHash = generateSafeTxHash(safeAddress, txArgs)
const tx = isExecution
? await getExecutionTransaction(txArgs)
: await getApprovalTransaction(safeInstance, safeTxHash)
@ -205,6 +208,7 @@ const createTransaction = (
confirmations: [], // this is used to determine if a tx is pending or not. See `calculateTransactionStatus` helper
value: txArgs.valueInWei,
safeTxHash,
dataDecoded: decodeMethods(txArgs.data),
submissionDate: new Date().toISOString(),
}
const mockedTx = await mockTransaction(txToMock, safeAddress, state)
@ -240,6 +244,8 @@ const createTransaction = (
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
removeTxFromStore(mockedTx, safeAddress, dispatch, state)
console.error('Tx error: ', error)
onError?.()
})
.then(async (receipt) => {
if (pendingExecutionKey) {

View File

@ -6,7 +6,6 @@ import { getLocalSafe, getSafeName } from 'src/logic/safe/utils'
import { enabledFeatures, safeNeedsUpdate } from 'src/logic/safe/utils/safeVersion'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
import { getBalanceInEtherOf } from 'src/logic/wallets/getWeb3'
import addSafe from 'src/logic/safe/store/actions/addSafe'
import addSafeOwner from 'src/logic/safe/store/actions/addSafeOwner'
import removeSafeOwner from 'src/logic/safe/store/actions/removeSafeOwner'
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
@ -17,8 +16,9 @@ import { ModulePair, SafeOwner, SafeRecordProps } from 'src/logic/safe/store/mod
import { Action, Dispatch } from 'redux'
import { SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts'
import { AppReduxState } from 'src/store'
import { latestMasterContractVersionSelector } from '../selectors'
const buildOwnersFrom = (safeOwners: string[], localSafe: SafeRecordProps): List<SafeOwner> => {
const buildOwnersFrom = (safeOwners: string[], localSafe?: SafeRecordProps): List<SafeOwner> => {
const ownersList = safeOwners.map((ownerAddress) => {
const convertedAdd = checksumAddress(ownerAddress)
@ -85,7 +85,7 @@ export const buildSafe = async (
needsUpdate,
featuresEnabled,
balances: Map(),
latestIncomingTxBlock: null,
latestIncomingTxBlock: 0,
activeAssets: Set(),
activeTokens: Set(),
blacklistedAssets: Set(),
@ -114,11 +114,12 @@ export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: Dispatch
])
// Converts from [ { address, ownerName} ] to address array
const localOwners = localSafe ? localSafe.owners.map((localOwner) => localOwner.address) : undefined
const localOwners = localSafe ? localSafe.owners.map((localOwner) => localOwner.address) : []
dispatch(
updateSafe({
address: safeAddress,
name: localSafe?.name,
modules: buildModulesLinkedList(modules?.array, modules?.next),
nonce: Number(remoteNonce),
threshold: Number(remoteThreshold),
@ -126,30 +127,27 @@ export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: Dispatch
)
// If the remote owners does not contain a local address, we remove that local owner
if (localOwners) {
localOwners.forEach((localAddress) => {
const remoteOwnerIndex = remoteOwners.findIndex((remoteAddress) => sameAddress(remoteAddress, localAddress))
if (remoteOwnerIndex === -1) {
dispatch(removeSafeOwner({ safeAddress, ownerAddress: localAddress }))
}
})
localOwners.forEach((localAddress) => {
const remoteOwnerIndex = remoteOwners.findIndex((remoteAddress) => sameAddress(remoteAddress, localAddress))
if (remoteOwnerIndex === -1) {
dispatch(removeSafeOwner({ safeAddress, ownerAddress: localAddress }))
}
})
// If the remote has an owner that we don't have locally, we add it
remoteOwners.forEach((remoteAddress) => {
const localOwnerIndex = localOwners.findIndex((localAddress) => sameAddress(remoteAddress, localAddress))
if (localOwnerIndex === -1) {
dispatch(
addSafeOwner({
safeAddress,
ownerAddress: remoteAddress,
ownerName: 'UNKNOWN',
}),
)
}
})
}
// If the remote has an owner that we don't have locally, we add it
remoteOwners.forEach((remoteAddress) => {
const localOwnerIndex = localOwners.findIndex((localAddress) => sameAddress(remoteAddress, localAddress))
if (localOwnerIndex === -1) {
dispatch(
addSafeOwner({
safeAddress,
ownerAddress: remoteAddress,
ownerName: 'UNKNOWN',
}),
)
}
})
}
export default (safeAdd: string) => async (
dispatch: Dispatch<any>,
getState: () => AppReduxState,
@ -157,12 +155,15 @@ export default (safeAdd: string) => async (
try {
const safeAddress = checksumAddress(safeAdd)
const safeName = (await getSafeName(safeAddress)) || 'LOADED SAFE'
const latestMasterContractVersion = getState().safes.get('latestMasterContractVersion')
const latestMasterContractVersion = latestMasterContractVersionSelector(getState())
const safeProps = await buildSafe(safeAddress, safeName, latestMasterContractVersion)
dispatch(addSafe(safeProps))
// `updateSafe`, as `loadSafesFromStorage` will populate the store previous to this call
// and `addSafe` will only add a newly non-existent safe
// For the case where the safe does not exist in the localStorage,
// `updateSafe` uses a default `notSetValue` to add the Safe to the store
dispatch(updateSafe(safeProps))
} catch (err) {
// eslint-disable-next-line
console.error('Error while updating Safe information: ', err)
return Promise.resolve()

View File

@ -1,19 +1,19 @@
import { addSafe } from './addSafe'
import { Dispatch } from 'redux'
import { SAFES_KEY } from 'src/logic/safe/utils'
import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
import { buildSafe } from 'src/logic/safe/store/reducer/safe'
import { loadFromStorage } from 'src/utils/storage'
import { Dispatch } from 'redux'
import { addSafe } from './addSafe'
const loadSafesFromStorage = () => async (dispatch: Dispatch): Promise<void> => {
try {
const safes = await loadFromStorage(SAFES_KEY)
const safes = await loadFromStorage<Record<string, SafeRecordProps>>(SAFES_KEY)
if (safes) {
Object.values(safes).forEach((safeProps) => {
dispatch(addSafe(buildSafe(safeProps)))
dispatch(addSafe(buildSafe(safeProps), true))
})
}
} catch (err) {

View File

@ -1,4 +1,3 @@
import { fromJS } from 'immutable'
import semverSatisfies from 'semver/functions/satisfies'
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
@ -20,9 +19,9 @@ import {
import { getLastTx, getNewTxNonce, shouldExecuteTransaction } from 'src/logic/safe/store/actions/utils'
import { getErrorMessage } from 'src/test/utils/ethereumErrors'
import { makeConfirmation } from '../models/confirmation'
import { storeTx } from './createTransaction'
import { TransactionStatus } from '../models/types/transaction'
import { TransactionStatus } from 'src/logic/safe/store/models/types/transaction'
import { makeConfirmation } from 'src/logic/safe/store/models/confirmation'
const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddress, tx, userAddress }) => async (
dispatch,
@ -34,7 +33,7 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
const lastTx = await getLastTx(safeAddress)
const nonce = await getNewTxNonce(null, lastTx, safeInstance)
const nonce = await getNewTxNonce(undefined, lastTx, safeInstance)
const isExecution = approveAndExecute || (await shouldExecuteTransaction(safeInstance, nonce, lastTx))
const safeVersion = await getCurrentSafeVersion(safeInstance)
@ -79,12 +78,14 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres
const canTryOffchainSigning =
!isExecution && !smartContractWallet && semverSatisfies(safeVersion, SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES)
if (canTryOffchainSigning) {
const signature = await tryOffchainSigning({ ...txArgs, safeAddress }, hardwareWallet)
const signature = await tryOffchainSigning(tx.safeTxHash, { ...txArgs, safeAddress }, hardwareWallet)
if (signature) {
dispatch(closeSnackbarAction(beforeExecutionKey))
await saveTxToHistory({ ...txArgs, signature })
// TODO: while we wait for the tx to be stored in the service and later update the tx info
// we should update the tx status in the store to disable owners' action buttons
dispatch(enqueueSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded))
dispatch(fetchTransactions(safeAddress))
@ -105,9 +106,7 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres
const txToMock: TxToMock = {
...txArgs,
confirmations: txArgs.confirmations, // this is used to determine if a tx is pending or not. See `calculateTransactionStatus` helper
value: txArgs.valueInWei,
submissionDate: txArgs.submissionDate,
}
const mockedTx = await mockTransaction(txToMock, safeAddress, state)
@ -123,10 +122,14 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres
await Promise.all([
saveTxToHistory({ ...txArgs, txHash }),
storeTx(
mockedTx.updateIn(
['ownersWithPendingActions', mockedTx.isCancellationTx ? 'reject' : 'confirm'],
(previous) => previous.push(from),
),
mockedTx.withMutations((record) => {
record
.updateIn(
['ownersWithPendingActions', mockedTx.isCancellationTx ? 'reject' : 'confirm'],
(previous) => previous.push(from),
)
.set('status', TransactionStatus.PENDING)
}),
safeAddress,
dispatch,
state,
@ -175,16 +178,20 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres
: TransactionStatus.FAILED,
)
.updateIn(['ownersWithPendingActions', 'reject'], (prev) => prev.clear())
.updateIn(['ownersWithPendingActions', 'confirm'], (prev) => prev.clear())
})
: mockedTx.withMutations((record) => {
record
.updateIn(['ownersWithPendingActions', toStoreTx.isCancellationTx ? 'reject' : 'confirm'], (previous) =>
previous.pop(),
)
.set('status', TransactionStatus.AWAITING_CONFIRMATIONS)
})
: mockedTx.set('status', TransactionStatus.AWAITING_CONFIRMATIONS)
await storeTx(
toStoreTx.withMutations((record) => {
record
.set('confirmations', fromJS([...tx.confirmations, makeConfirmation({ owner: from })]))
.updateIn(['ownersWithPendingActions', toStoreTx.isCancellationTx ? 'reject' : 'confirm'], (previous) =>
previous.pop(from),
)
toStoreTx.update('confirmations', (confirmations) => {
const index = confirmations.findIndex(({ owner }) => owner === from)
return index === -1 ? confirmations.push(makeConfirmation({ owner: from })) : confirmations
}),
safeAddress,
dispatch,

View File

@ -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)

View File

@ -39,8 +39,9 @@ export default (safeAddress: string): ThunkAction<Promise<void>, AppReduxState,
}
const incomingTransactions = await loadIncomingTransactions(safeAddress)
const safeIncomingTxs = incomingTransactions.get(safeAddress)
if (incomingTransactions.get(safeAddress).size) {
if (safeIncomingTxs?.size) {
dispatch(addIncomingTransactions(incomingTransactions))
}
} catch (error) {

View File

@ -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<Map<string, List<any>>> => {
const { eTag, results } = await fetchTransactions(TransactionTypes.INCOMING, safeAddress, previousETag)
previousETag = eTag

View File

@ -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<number, TxServiceModel>; 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<SafeTransactionsType> => {
const defaultResponse = {
cancel: Map(),

View File

@ -1,4 +1,4 @@
import { Transaction, TxType } from 'src/logic/safe/store/models/types/transactions'
import { Transaction, TxType } from 'src/logic/safe/store/models/types/transactions.d'
export const isMultiSigTx = (tx: Transaction): boolean => {
return TxType[tx.txType] === TxType.MULTISIG_TRANSACTION

View File

@ -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<string, Token>,
): Promise<boolean> => {
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<any> => {
): Promise<RefundParams | null> => {
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<Transaction> => {
@ -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))),
),
}),

View File

@ -1,17 +1,9 @@
import updateSafe from './updateSafe'
import { Set } from 'immutable'
import updateAssetsList from './updateAssetsList'
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
// the selector uses ownProps argument/router props to get the address of the safe
// so in order to use it I had to recreate the same structure
// const generateMatchProps = (safeAddress: string) => ({
// match: {
// params: {
// [SAFE_PARAM_ADDRESS]: safeAddress,
// },
// },
// })
const updateActiveAssets = (safeAddress, activeAssets) => async (dispatch) => {
dispatch(updateSafe({ address: safeAddress, activeAssets }))
const updateActiveAssets = (safeAddress: string, activeAssets: Set<string>) => (dispatch: Dispatch): void => {
dispatch(updateAssetsList({ safeAddress, activeAssets }))
}
export default updateActiveAssets

View File

@ -1,4 +1,6 @@
import updateSafe from './updateSafe'
import { Set } from 'immutable'
import updateTokensList from './updateTokensList'
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
// the selector uses ownProps argument/router props to get the address of the safe
// so in order to use it I had to recreate the same structure
@ -10,8 +12,8 @@ import updateSafe from './updateSafe'
// },
// })
const updateActiveTokens = (safeAddress, activeTokens) => async (dispatch) => {
dispatch(updateSafe({ address: safeAddress, activeTokens }))
const updateActiveTokens = (safeAddress: string, activeTokens: Set<string>) => (dispatch: Dispatch): void => {
dispatch(updateTokensList({ safeAddress, activeTokens }))
}
export default updateActiveTokens

View File

@ -0,0 +1,7 @@
import { createAction } from 'redux-actions'
export const UPDATE_ASSETS_LIST = 'UPDATE_ASSETS_LIST'
const updateAssetsList = createAction(UPDATE_ASSETS_LIST)
export default updateAssetsList

View File

@ -1,7 +1,9 @@
import updateSafe from './updateSafe'
import { Set } from 'immutable'
import updateAssetsList from './updateAssetsList'
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
const updateBlacklistedAssets = (safeAddress, blacklistedAssets) => async (dispatch) => {
dispatch(updateSafe({ address: safeAddress, blacklistedAssets }))
const updateBlacklistedAssets = (safeAddress: string, blacklistedAssets: Set<string>) => (dispatch: Dispatch): void => {
dispatch(updateAssetsList({ safeAddress, blacklistedAssets }))
}
export default updateBlacklistedAssets

View File

@ -1,7 +1,9 @@
import updateSafe from './updateSafe'
import { Set } from 'immutable'
import updateTokensList from './updateTokensList'
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
const updateBlacklistedTokens = (safeAddress, blacklistedTokens) => async (dispatch) => {
dispatch(updateSafe({ address: safeAddress, blacklistedTokens }))
const updateBlacklistedTokens = (safeAddress: string, blacklistedTokens: Set<string>) => (dispatch: Dispatch): void => {
dispatch(updateTokensList({ safeAddress, blacklistedTokens }))
}
export default updateBlacklistedTokens

View File

@ -0,0 +1,7 @@
import { createAction } from 'redux-actions'
export const UPDATE_TOKENS_LIST = 'UPDATE_TOKENS_LIST'
const updateTokenList = createAction(UPDATE_TOKENS_LIST)
export default updateTokenList

View File

@ -4,7 +4,7 @@ import axios from 'axios'
import { buildTxServiceUrl } from 'src/logic/safe/transactions/txHistory'
export const getLastTx = async (safeAddress: string): Promise<TxServiceModel> => {
export const getLastTx = async (safeAddress: string): Promise<TxServiceModel | null> => {
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<TxServiceModel> =>
}
export const getNewTxNonce = async (
txNonce: string | null,
lastTx: TxServiceModel,
txNonce: string | undefined,
lastTx: TxServiceModel | null,
safeInstance: GnosisSafe,
): Promise<string> => {
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<boolean> => {
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

View File

@ -85,7 +85,7 @@ const notificationsMiddleware = (store) => (next) => async (action) => {
const safes = safesMapSelector(state)
const currentSafe = safes.get(safeAddress)
if (!isUserAnOwner(currentSafe, userAddress) || awaitingTransactions.size === 0) {
if (!currentSafe || !isUserAnOwner(currentSafe, userAddress) || awaitingTransactions.size === 0) {
break
}
@ -103,8 +103,8 @@ const notificationsMiddleware = (store) => (next) => async (action) => {
}
case ADD_INCOMING_TRANSACTIONS: {
action.payload.forEach((incomingTransactions, safeAddress) => {
const { latestIncomingTxBlock } = state.safes.get('safes').get(safeAddress)
const viewedSafes = state.currentSession ? state.currentSession.get('viewedSafes') : []
const { latestIncomingTxBlock } = state.safes.get('safes').get(safeAddress, {})
const viewedSafes = state.currentSession['viewedSafes']
const recurringUser = viewedSafes?.includes(safeAddress)
const newIncomingTransactions = incomingTransactions.filter((tx) => tx.blockNumber > latestIncomingTxBlock)

View File

@ -1,4 +1,3 @@
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { addAddressBookEntry } from 'src/logic/addressBook/store/actions/addAddressBookEntry'
import { saveDefaultSafe, saveSafes } from 'src/logic/safe/utils'
import { tokensSelector } from 'src/logic/tokens/store/selectors'
@ -12,17 +11,30 @@ import { REMOVE_SAFE_OWNER } from 'src/logic/safe/store/actions/removeSafeOwner'
import { REPLACE_SAFE_OWNER } from 'src/logic/safe/store/actions/replaceSafeOwner'
import { SET_DEFAULT_SAFE } from 'src/logic/safe/store/actions/setDefaultSafe'
import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe'
import { UPDATE_TOKENS_LIST } from 'src/logic/safe/store/actions/updateTokensList'
import { UPDATE_ASSETS_LIST } from 'src/logic/safe/store/actions/updateAssetsList'
import { getActiveTokensAddressesForAllSafes, safesMapSelector } from 'src/logic/safe/store/selectors'
import { checksumAddress } from 'src/utils/checksumAddress'
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
import { checkIfEntryWasDeletedFromAddressBook, isValidAddressBookName } from 'src/logic/addressBook/utils'
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
import { updateAddressBookEntry } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
import { ADD_OR_UPDATE_SAFE } from 'src/logic/safe/store/actions/addOrUpdateSafe'
const watchedActions = [
ADD_SAFE,
UPDATE_SAFE,
REMOVE_SAFE,
ADD_OR_UPDATE_SAFE,
ADD_SAFE_OWNER,
REMOVE_SAFE_OWNER,
REPLACE_SAFE_OWNER,
EDIT_SAFE_OWNER,
ACTIVATE_TOKEN_FOR_ALL_SAFES,
UPDATE_TOKENS_LIST,
UPDATE_ASSETS_LIST,
SET_DEFAULT_SAFE,
]
@ -48,6 +60,7 @@ const safeStorageMware = (store) => (next) => async (action) => {
const state = store.getState()
const { dispatch } = store
const safes = safesMapSelector(state)
const addressBook = addressBookSelector(state)
await saveSafes(safes.toJSON())
switch (action.type) {
@ -56,19 +69,60 @@ const safeStorageMware = (store) => (next) => async (action) => {
break
}
case ADD_SAFE: {
const { safe, loadedFromStorage } = action.payload
const safeAlreadyLoaded =
loadedFromStorage || safes.find((safeIterator) => sameAddress(safeIterator.address, safe.address))
safe.owners.forEach((owner) => {
const checksumEntry = makeAddressBookEntry({ address: checksumAddress(owner.address), name: owner.name })
const ownerWasAlreadyInAddressBook = checkIfEntryWasDeletedFromAddressBook(
checksumEntry,
addressBook,
safeAlreadyLoaded,
)
if (!ownerWasAlreadyInAddressBook) {
dispatch(addAddressBookEntry(checksumEntry, { notifyEntryUpdate: false }))
}
const addressAlreadyExists = addressBook.find((entry) => sameAddress(entry.address, checksumEntry.address))
if (isValidAddressBookName(checksumEntry.name) && addressAlreadyExists) {
dispatch(updateAddressBookEntry(checksumEntry))
}
})
const safeWasAlreadyInAddressBook = checkIfEntryWasDeletedFromAddressBook(
{ address: safe.address, name: safe.name },
addressBook,
safeAlreadyLoaded,
)
if (!safeWasAlreadyInAddressBook) {
dispatch(
addAddressBookEntry(makeAddressBookEntry({ address: safe.address, name: safe.name }), {
notifyEntryUpdate: true,
}),
)
}
break
}
case ADD_OR_UPDATE_SAFE: {
const { safe } = action.payload
const ownersArray = safe.owners.toJS()
// Adds the owners to the address book
ownersArray.forEach((owner) => {
dispatch(addAddressBookEntry(makeAddressBookEntry({ ...owner, isOwner: true })))
safe.owners.forEach((owner) => {
const checksumEntry = makeAddressBookEntry({ address: checksumAddress(owner.address), name: owner.name })
if (isValidAddressBookName(checksumEntry.name)) {
dispatch(addOrUpdateAddressBookEntry(checksumEntry))
}
})
break
}
case UPDATE_SAFE: {
const { activeTokens } = action.payload
const { activeTokens, name, address } = action.payload
if (activeTokens) {
recalculateActiveTokens(state)
}
if (name) {
dispatch(addOrUpdateAddressBookEntry(makeAddressBookEntry({ name, address })))
}
break
}
case SET_DEFAULT_SAFE: {

View File

@ -2,7 +2,7 @@ import { List, Map, RecordOf } from 'immutable'
import { Confirmation } from './confirmation'
import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
import { DataDecoded, Transfer } from './transactions'
import { DecodedParams } from 'src/routes/safe/store/models/types/transactions'
import { DecodedParams } from 'src/routes/safe/store/models/types/transactions.d'
export enum TransactionTypes {
INCOMING = 'incoming',
@ -33,6 +33,7 @@ export enum PendingActionType {
REJECT = 'reject',
}
export type PendingActionValues = PendingActionType[keyof PendingActionType]
export type RefundParams = { fee: string; symbol: string }
export type TransactionProps = {
baseGas: number
@ -43,7 +44,7 @@ export type TransactionProps = {
creator: string
creationTx: boolean
customTx: boolean
data?: string | null
data: string | null
dataDecoded: DataDecoded | null
decimals?: (number | string) | null
decodedParams: DecodedParams | null
@ -51,7 +52,7 @@ export type TransactionProps = {
executionTxHash?: string | null
executor: string
factoryAddress: string
fee?: string // It will be replace with the new TXs types.
fee: string | null // It will be replace with the new TXs types.
gasPrice: string
gasToken: string
isCancellationTx: boolean
@ -63,18 +64,18 @@ export type TransactionProps = {
masterCopy: string
modifySettingsTx: boolean
multiSendTx: boolean
nonce?: number | null
nonce: number
operation: number
origin: string | null
ownersWithPendingActions: Map<PendingActionValues, List<any>>
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<TransactionProps>
export type TxArgs = {
baseGas: number
data?: string | null
data: string
gasPrice: string
gasToken: string
nonce: number

View File

@ -1,7 +1,10 @@
import { handleActions } from 'redux-actions'
import { Transaction } from '../models/types/transactions'
import { LOAD_MORE_TRANSACTIONS, LoadMoreTransactionsAction } from '../actions/allTransactions/pagination'
import { Transaction } from 'src/logic/safe/store/models/types/transactions.d'
import {
LOAD_MORE_TRANSACTIONS,
LoadMoreTransactionsAction,
} from 'src/logic/safe/store/actions/allTransactions/pagination'
export const TRANSACTIONS = 'allTransactions'

View File

@ -1,4 +1,4 @@
import { Map, Set } from 'immutable'
import { Map, Set, List } from 'immutable'
import { handleActions } from 'redux-actions'
import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from 'src/logic/safe/store/actions/activateTokenForAllSafes'
@ -11,10 +11,14 @@ import { REPLACE_SAFE_OWNER } from 'src/logic/safe/store/actions/replaceSafeOwne
import { SET_DEFAULT_SAFE } from 'src/logic/safe/store/actions/setDefaultSafe'
import { SET_LATEST_MASTER_CONTRACT_VERSION } from 'src/logic/safe/store/actions/setLatestMasterContractVersion'
import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe'
import { UPDATE_TOKENS_LIST } from 'src/logic/safe/store/actions/updateTokensList'
import { UPDATE_ASSETS_LIST } from 'src/logic/safe/store/actions/updateAssetsList'
import { makeOwner } from 'src/logic/safe/store/models/owner'
import makeSafe, { SafeRecordProps } from 'src/logic/safe/store/models/safe'
import { checksumAddress } from 'src/utils/checksumAddress'
import { SafeReducerMap } from 'src/routes/safe/store/reducer/types/safe'
import { ADD_OR_UPDATE_SAFE } from 'src/logic/safe/store/actions/addOrUpdateSafe'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
export const SAFE_REDUCER_ID = 'safes'
export const DEFAULT_SAFE_INITIAL_STATE = 'NOT_ASKED'
@ -37,11 +41,37 @@ export const buildSafe = (storedSafe: SafeRecordProps): SafeRecordProps => {
blacklistedTokens,
activeAssets,
blacklistedAssets,
latestIncomingTxBlock: null,
latestIncomingTxBlock: 0,
modules: null,
}
}
const updateSafeProps = (prevSafe, safe) => {
return prevSafe.withMutations((record) => {
// Every property is updated individually to overcome the issue with nested data being overwritten
const safeProperties = Object.keys(safe)
// We check each safe property sent in action.payload
safeProperties.forEach((key) => {
if (safe[key] && typeof safe[key] === 'object') {
if (safe[key].length >= 0) {
// If type is array we update the array
record.update(key, () => safe[key])
} else if (safe[key].size >= 0) {
// If type is Immutable List we replace current List
// If type is Object we do a merge
List.isList(safe[key])
? record.update(key, (current) => current.set(safe[key]))
: record.update(key, (current) => current.merge(safe[key]))
}
} else {
// By default we overwrite the value. This is for strings, numbers and unset values
record.set(key, safe[key])
}
})
})
}
export default handleActions(
{
[UPDATE_SAFE]: (state: SafeReducerMap, action) => {
@ -50,8 +80,8 @@ export default handleActions(
return state.updateIn(
['safes', safeAddress],
makeSafe({ name: 'LOADED SAFE', address: safeAddress }),
(prevSafe) => prevSafe.merge(safe),
makeSafe({ name: safe?.name || 'LOADED SAFE', address: safeAddress }),
(prevSafe) => updateSafeProps(prevSafe, safe),
)
},
[ACTIVATE_TOKEN_FOR_ALL_SAFES]: (state: SafeReducerMap, action) => {
@ -65,7 +95,7 @@ export default handleActions(
const safeActiveTokens = map.getIn(['safes', safeAddress, 'activeTokens'])
const activeTokens = safeActiveTokens.add(tokenAddress)
map.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.merge({ activeTokens }))
map.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.mergeDeep({ activeTokens }))
})
})
},
@ -77,11 +107,24 @@ export default handleActions(
// with initial props and it would overwrite existing ones
if (state.hasIn(['safes', safe.address])) {
return state.updateIn(['safes', safe.address], (prevSafe) => prevSafe.merge(safe))
return state
}
return state.setIn(['safes', safe.address], makeSafe(safe))
},
[ADD_OR_UPDATE_SAFE]: (state: SafeReducerMap, action) => {
const { safe } = action.payload
if (!state.hasIn(['safes', safe.address])) {
return state.setIn(['safes', safe.address], makeSafe(safe))
}
return state.updateIn(
['safes', safe.address],
makeSafe({ name: 'LOADED SAFE', address: safe.address }),
(prevSafe) => updateSafeProps(prevSafe, safe),
)
},
[REMOVE_SAFE]: (state: SafeReducerMap, action) => {
const safeAddress = action.payload
@ -90,6 +133,14 @@ export default handleActions(
[ADD_SAFE_OWNER]: (state: SafeReducerMap, action) => {
const { ownerAddress, ownerName, safeAddress } = action.payload
const addressFound = state
.getIn(['safes', safeAddress])
.owners.find((owner) => sameAddress(owner.address, ownerAddress))
if (addressFound) {
return state
}
return state.updateIn(['safes', safeAddress], (prevSafe) =>
prevSafe.merge({
owners: prevSafe.owners.push(makeOwner({ address: ownerAddress, name: ownerName })),
@ -127,6 +178,24 @@ export default handleActions(
return prevSafe.merge({ owners: updatedOwners })
})
},
[UPDATE_TOKENS_LIST]: (state: SafeReducerMap, action) => {
// Only activeTokens or blackListedTokens is required
const { safeAddress, activeTokens, blacklistedTokens } = action.payload
const key = activeTokens ? 'activeTokens' : 'blacklistedTokens'
const list = activeTokens ?? blacklistedTokens
return state.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.set(key, list))
},
[UPDATE_ASSETS_LIST]: (state: SafeReducerMap, action) => {
// Only activeAssets or blackListedAssets is required
const { safeAddress, activeAssets, blacklistedAssets } = action.payload
const key = activeAssets ? 'activeAssets' : 'blacklistedAssets'
const list = activeAssets ?? blacklistedAssets
return state.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.set(key, list))
},
[SET_DEFAULT_SAFE]: (state: SafeReducerMap, action) => state.set('defaultSafe', action.payload),
[SET_LATEST_MASTER_CONTRACT_VERSION]: (state: SafeReducerMap, action) =>
state.set('latestMasterContractVersion', action.payload),

View File

@ -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),
)

View File

@ -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<string> =>
safes.get(safeAddress).get('activeAssets')
safes.get(safeAddress)?.get('activeAssets') || Set()
export const safeBlacklistedAssetsSelectorBySafe = (safeAddress: string, safes: SafesMap): Set<string> =>
safes.get(safeAddress).get('blacklistedAssets')
safes.get(safeAddress)?.get('blacklistedAssets') || Set()
const baseSafe = makeSafe()
export const safeFieldSelector = <K extends keyof SafeRecordProps>(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'))

View File

@ -0,0 +1,59 @@
import { Set, Map } from 'immutable'
import { aNewStore } from 'src/store'
import updateActiveTokens from 'src/logic/safe/store/actions/updateActiveTokens'
import '@testing-library/jest-dom/extend-expect'
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
import { makeToken } from 'src/logic/tokens/store/model/token'
import { safesMapSelector } from 'src/logic/safe/store/selectors'
describe('Feature > Balances', () => {
let store
const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
beforeEach(async () => {
store = aNewStore()
})
it('It should return an updated balance when updates active tokens', async () => {
// given
const tokensAmount = '100'
const token = makeToken({
address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9',
name: 'OmiseGo',
symbol: 'OMG',
decimals: 18,
logoUri:
'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true',
})
const balances = Map({
[token.address]: tokensAmount,
})
const expectedResult = '100'
// when
store.dispatch(updateSafe({ address: safeAddress, balances }))
store.dispatch(updateActiveTokens(safeAddress, Set([token.address])))
const safe = safesMapSelector(store.getState()).get(safeAddress)
const balanceResult = safe?.get('balances').get(token.address)
const activeTokens = safe?.get('activeTokens')
const tokenIsActive = activeTokens?.has(token.address)
// then
expect(balanceResult).toBe(expectedResult)
expect(tokenIsActive).toBe(true)
})
it('The store should have an updated ether balance after updating the value', async () => {
// given
const etherAmount = '1'
const expectedResult = '1'
// when
store.dispatch(updateSafe({ address: safeAddress, ethBalance: etherAmount }))
const safe = safesMapSelector(store.getState()).get(safeAddress)
const balanceResult = safe?.get('ethBalance')
// then
expect(balanceResult).toBe(expectedResult)
})
})

View File

@ -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<Transaction>,
cancellationTxs,
userAccount: string,
): List<Transaction> => {
return allTransactions.filter((tx) => {
const cancelTx = !!tx.nonce && !isNaN(Number(tx.nonce)) ? cancellationTxs.get(`${tx.nonce}`) : null

View File

@ -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()

View File

@ -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)

View File

@ -1,5 +1,6 @@
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
import { AbstractProvider } from 'web3-core'
import { getWeb3 } from 'src/logic/wallets/getWeb3'
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
const EIP712_NOT_SUPPORTED_ERROR_MSG = "EIP712 is not supported by user's wallet"
@ -59,7 +60,7 @@ const generateTypedDataFrom = async ({
}
export const getEIP712Signer = (version?: string) => async (txArgs) => {
const web3: any = getWeb3()
const web3 = getWeb3()
const typedData = await generateTypedDataFrom(txArgs)
let method = 'eth_signTypedData_v3'
@ -80,13 +81,14 @@ export const getEIP712Signer = (version?: string) => async (txArgs) => {
}
return new Promise((resolve, reject) => {
web3.currentProvider.sendAsync(signedTypedData, (err, signature) => {
const provider = web3.currentProvider as AbstractProvider
provider.sendAsync(signedTypedData, (err, signature) => {
if (err) {
reject(err)
return
}
if (signature.result == null) {
if (signature?.result == null) {
reject(new Error(EIP712_NOT_SUPPORTED_ERROR_MSG))
return
}

View File

@ -4,26 +4,13 @@ import { AbstractProvider } from 'web3-core/types'
const ETH_SIGN_NOT_SUPPORTED_ERROR_MSG = 'ETH_SIGN_NOT_SUPPORTED'
export const ethSigner = async ({
baseGas,
data,
gasPrice,
gasToken,
nonce,
operation,
refundReceiver,
safeInstance,
safeTxGas,
sender,
to,
valueInWei,
}): Promise<string> => {
type EthSignerArgs = {
safeTxHash: string
sender: string
}
export const ethSigner = async ({ safeTxHash, sender }: EthSignerArgs): Promise<string> => {
const web3 = await getWeb3()
const txHash = await safeInstance.methods
.getTransactionHash(to, valueInWei, data, operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, nonce)
.call({
from: sender,
})
return new Promise(function (resolve, reject) {
const provider = web3.currentProvider as AbstractProvider
@ -31,7 +18,7 @@ export const ethSigner = async ({
{
jsonrpc: '2.0',
method: 'eth_sign',
params: [sender, txHash],
params: [sender, safeTxHash],
id: new Date().getTime(),
},
async function (err, signature) {
@ -39,7 +26,7 @@ export const ethSigner = async ({
return reject(err)
}
if (signature.result == null) {
if (signature?.result == null) {
reject(new Error(ETH_SIGN_NOT_SUPPORTED_ERROR_MSG))
return
}

View File

@ -8,7 +8,7 @@ import { ethSigner } from './ethSigner'
const SIGNERS = {
EIP712_V3: getEIP712Signer('v3'),
EIP712_V4: getEIP712Signer('v4'),
EIP712: getEIP712Signer() as any,
EIP712: getEIP712Signer(),
ETH_SIGN: ethSigner,
}
@ -18,13 +18,13 @@ const getSignersByWallet = (isHW) =>
export const SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES = '>=1.1.1'
export const tryOffchainSigning = async (txArgs, isHW) => {
export const tryOffchainSigning = async (safeTxHash: string, txArgs, isHW: boolean): Promise<string> => {
let signature
const signerByWallet = getSignersByWallet(isHW)
for (const signingFunc of signerByWallet) {
try {
signature = await signingFunc(txArgs)
signature = await signingFunc({ ...txArgs, safeTxHash })
break
} catch (err) {

View File

@ -1,6 +1,6 @@
import { NonPayableTransactionObject } from 'src/types/contracts/types.d'
import { TxArgs } from 'src/logic/safe/store/models/types/transaction'
import { GnosisSafe } from 'src/types/contracts/GnosisSafe'
import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
export const CALL = 0
export const DELEGATE_CALL = 1

View File

@ -2,20 +2,20 @@ import { loadFromStorage, saveToStorage } from 'src/utils/storage'
import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
export const SAFES_KEY = 'SAFES'
export const TX_KEY = 'TX'
export const DEFAULT_SAFE_KEY = 'DEFAULT_SAFE'
export const getSafeName = async (safeAddress: string): Promise<string | undefined> => {
const safes = await loadFromStorage(SAFES_KEY)
if (!safes) {
return undefined
}
const safe = safes[safeAddress]
type StoredSafes = Record<string, SafeRecordProps>
return safe ? safe.name : undefined
export const loadStoredSafes = (): Promise<StoredSafes | undefined> => {
return loadFromStorage<StoredSafes>(SAFES_KEY)
}
export const saveSafes = async (safes) => {
export const getSafeName = async (safeAddress: string): Promise<string | undefined> => {
const safes = await loadStoredSafes()
return safes?.[safeAddress]?.name
}
export const saveSafes = async (safes: StoredSafes): Promise<void> => {
try {
await saveToStorage(SAFES_KEY, safes)
} catch (err) {
@ -23,9 +23,9 @@ export const saveSafes = async (safes) => {
}
}
export const getLocalSafe = async (safeAddress: string): Promise<SafeRecordProps | null> => {
const storedSafes = (await loadFromStorage(SAFES_KEY)) || {}
return storedSafes[safeAddress] || null
export const getLocalSafe = async (safeAddress: string): Promise<SafeRecordProps | undefined> => {
const storedSafes = await loadStoredSafes()
return storedSafes?.[safeAddress]
}
export const getDefaultSafe = async (): Promise<string> => {

View File

@ -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<s
gnosisSafeInstance.methods.VERSION().call()
export const enabledFeatures = (version: string): string[] =>
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<SafeVersionInfo> => {
if (!gnosisSafeInstance || !lastSafeVersion) {
return null
throw new Error('checkIfSafeNeedsUpdate: No Safe Instance or version provided')
}
const safeMasterVersion = await getCurrentSafeVersion(gnosisSafeInstance)
const current = semverValid(safeMasterVersion)
const latest = semverValid(lastSafeVersion)
const current = semverValid(safeMasterVersion) as string
const latest = semverValid(lastSafeVersion) as string
const needUpdate = safeNeedsUpdate(safeMasterVersion, lastSafeVersion)
return { current, latest, needUpdate }
@ -69,7 +71,7 @@ export const getCurrentMasterContractLastVersion = async (): Promise<string> =>
export const getSafeVersionInfo = async (safeAddress: string): Promise<SafeVersionInfo> => {
try {
const safeMaster = await getGnosisSafeInstanceAt(safeAddress)
const safeMaster = getGnosisSafeInstanceAt(safeAddress)
const lastSafeVersion = await getCurrentMasterContractLastVersion()
return checkIfSafeNeedsUpdate(safeMaster, lastSafeVersion)
} catch (err) {

View File

@ -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,

View File

@ -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<boolean> => {
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<Token> => {
if (!tokenAddress) {
return null
}
export const getTokenInfos = async (tokenAddress: string): Promise<Token | undefined> => {
const { tokens } = store.getState()
const localToken = tokens.get(tokenAddress)
@ -74,7 +72,7 @@ export const getTokenInfos = async (tokenAddress: string): Promise<Token> => {
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<Token> => {
return token
}
export const fetchTokens = () => async (dispatch, getState) => {
export const fetchTokens = () => async (
dispatch: ThunkDispatch<AppReduxState, undefined, AnyAction>,
getState: () => AppReduxState,
): Promise<void> => {
try {
const currentSavedTokens = tokensSelector(getState())

View File

@ -5,7 +5,7 @@ export type TokenProps = {
name: string
symbol: string
decimals: number | string
logoUri?: string | null
logoUri: string
balance?: number | string
}

View File

@ -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)
})
})

View File

@ -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()
})
})

View File

@ -35,18 +35,18 @@ export const isAddressAToken = async (tokenAddress: string): Promise<boolean> =>
// } 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<string, Token>,
): 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<string, Token>,
): Promise<boolean> => {
let isSendTokenTx = !isSendERC721Transaction(tx, txCode, knownTokens) && isTokenTransfer(tx)
@ -118,8 +118,8 @@ export const isERC721Contract = async (contractAddress: string): Promise<boolean
let isERC721 = false
try {
isERC721 = true
await ERC721Token.at(contractAddress)
isERC721 = true
} catch (error) {
console.warn('Asset not found')
}

View File

@ -2,7 +2,7 @@ import { List } from 'immutable'
import { SafeRecord } from 'src/logic/safe/store/models/safe'
export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
export const sameAddress = (firstAddress: string, secondAddress: string): boolean => {
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
}

View File

@ -96,7 +96,7 @@ const isSmartContractWallet = async (web3Provider: Web3, account: string): Promi
}
export const getProviderInfo = async (web3Instance: Web3, providerName = 'Wallet'): Promise<ProviderProps> => {
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)

Some files were not shown because too many files have changed in this diff Show More