Merge branch 'development' into release/v2.11.1

This commit is contained in:
Daniel Sanchez 2020-09-16 18:59:49 +02:00 committed by GitHub
commit e5468704d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
186 changed files with 4721 additions and 1739 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

@ -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.12.0",
"classnames": "^2.2.6",
"concurrently": "^5.2.0",
"concurrently": "^5.3.0",
"connected-react-router": "6.8.0",
"coveralls": "^3.1.0",
"currency-flags": "2.1.2",
@ -190,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.6",
"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",
@ -236,15 +236,15 @@
"@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",
"@testing-library/user-event": "12.1.3",
"@typechain/web3-v1": "^1.0.0",
"@types/history": "4.6.2",
"@types/jest": "^26.0.9",
"@types/jest": "^26.0.12",
"@types/lodash.memoize": "^4.1.6",
"@types/node": "14.6.0",
"@types/react": "^16.9.47",
"@types/node": "14.6.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",
@ -255,26 +255,26 @@
"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.2.13",
"node-sass": "^4.14.1",
"prettier": "2.0.5",
"prettier": "2.1.1",
"react-app-rewired": "^2.1.6",
"react-docgen-typescript-loader": "^3.7.2",
"truffle": "5.1.36",
"truffle": "5.1.41",
"typechain": "^2.0.0",
"typescript": "3.9.7",
"wait-on": "5.1.0"
"wait-on": "5.2.0"
}
}

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

@ -6,12 +6,10 @@ export interface AddressBookEntryProps {
isOwner: boolean
}
export type AddressBookEntryRecord = RecordOf<AddressBookEntryProps>
export const makeAddressBookEntry = Record<AddressBookEntryProps>({
address: '',
name: '',
isOwner: false,
})
export type AddressBookEntry = RecordOf<AddressBookEntryProps>
export type AddressBookEntryRecord = RecordOf<AddressBookEntryProps>

View File

@ -1,27 +1,31 @@
import { List, Map } from 'immutable'
import { handleActions } from 'redux-actions'
import { AddressBookEntry, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import {
AddressBookEntryRecord,
AddressBookEntryProps,
makeAddressBookEntry,
} from 'src/logic/addressBook/model/addressBook'
import { ADD_ADDRESS_BOOK } from 'src/logic/addressBook/store/actions/addAddressBook'
import { ADD_ENTRY } from 'src/logic/addressBook/store/actions/addAddressBookEntry'
import { ADD_OR_UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
import { LOAD_ADDRESS_BOOK } from 'src/logic/addressBook/store/actions/loadAddressBook'
import { REMOVE_ENTRY } from 'src/logic/addressBook/store/actions/removeAddressBookEntry'
import { UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
import { getAddressesListFromAdbk } from 'src/logic/addressBook/utils'
import { getAddressesListFromSafeAddressBook } from 'src/logic/addressBook/utils'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
import { checksumAddress } from 'src/utils/checksumAddress'
export const ADDRESS_BOOK_REDUCER_ID = 'addressBook'
export type AddressBookCollection = List<AddressBookEntry>
export type AddressBookCollection = List<AddressBookEntryRecord>
export type AddressBookState = Map<string, Map<string, AddressBookCollection>>
export const buildAddressBook = (storedAdbk) => {
let addressBookBuilt = Map([])
export const buildAddressBook = (storedAdbk: AddressBookEntryProps[]): Map<string, AddressBookCollection> => {
let addressBookBuilt: Map<string, AddressBookCollection> = Map([])
Object.entries(storedAdbk).forEach((adbkProps: any) => {
const safeAddress = checksumAddress(adbkProps[0])
const adbkRecords = adbkProps[1].map(makeAddressBookEntry)
const adbkRecords: AddressBookEntryRecord[] = adbkProps[1].map(makeAddressBookEntry)
const adbkSafeEntries = List(adbkRecords)
addressBookBuilt = addressBookBuilt.set(safeAddress, adbkSafeEntries)
})
@ -55,7 +59,7 @@ export default handleActions(
const safeAddressBook = state.getIn(['addressBook', safeAddress])
if (safeAddressBook) {
const adbkAddressList = getAddressesListFromAdbk(safeAddressBook)
const adbkAddressList = getAddressesListFromSafeAddressBook(safeAddressBook)
const found = adbkAddressList.includes(entry.address)
if (!found) {
const updatedSafeAdbkList = safeAddressBook.push(entry)

View File

@ -4,6 +4,7 @@ import { createSelector } from 'reselect'
import { ADDRESS_BOOK_REDUCER_ID } from 'src/logic/addressBook/store/reducer/addressBook'
import { AddressBookMap } from 'src/logic/addressBook/store/reducer/types/addressBook.d'
import { AddressBookEntryRecord } from 'src/logic/addressBook/model/addressBook'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
export const addressBookMapSelector = (state: AppReduxState): AddressBookMap =>
@ -13,8 +14,8 @@ export const getAddressBook = createSelector(
addressBookMapSelector,
safeParamAddressFromStateSelector,
(addressBook, safeAddress) => {
let result = List([])
if (addressBook) {
let result: List<AddressBookEntryRecord> = List([])
if (addressBook && safeAddress) {
result = addressBook.get(safeAddress, List())
}
return result

View File

@ -0,0 +1,102 @@
import { Map, List } from 'immutable'
import {
getAddressBookFromStorage,
getAddressesListFromSafeAddressBook,
getNameFromSafeAddressBook,
getOwnersWithNameFromAddressBook,
saveAddressBook,
} from 'src/logic/addressBook/utils/index'
import { buildAddressBook } from 'src/logic/addressBook/store/reducer/addressBook'
import { AddressBookEntryRecord, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
const getMockAddressBookEntry = (
address: string,
name: string = 'test',
isOwner: boolean = false,
): AddressBookEntryRecord =>
makeAddressBookEntry({
address,
name,
isOwner,
})
describe('getAddressesListFromAdbk', () => {
const entry1 = getMockAddressBookEntry('123456', 'test1')
const entry2 = getMockAddressBookEntry('78910', 'test2')
const entry3 = getMockAddressBookEntry('4781321', 'test3')
it('It should returns the list of addresses within the addressBook given a safeAddressBook', () => {
// given
const safeAddressBook = List([entry1, entry2, entry3])
const expectedResult = [entry1.address, entry2.address, entry3.address]
// when
const result = getAddressesListFromSafeAddressBook(safeAddressBook)
// then
expect(result).toStrictEqual(expectedResult)
})
})
describe('getNameFromSafeAddressBook', () => {
const entry1 = getMockAddressBookEntry('123456', 'test1')
const entry2 = getMockAddressBookEntry('78910', 'test2')
const entry3 = getMockAddressBookEntry('4781321', 'test3')
it('It should returns the user name given a safeAddressBook and an user account', () => {
// given
const safeAddressBook = List([entry1, entry2, entry3])
const expectedResult = entry2.name
// when
const result = getNameFromSafeAddressBook(safeAddressBook, entry2.address)
// then
expect(result).toBe(expectedResult)
})
})
describe('getOwnersWithNameFromAddressBook', () => {
const entry1 = getMockAddressBookEntry('123456', 'test1')
const entry2 = getMockAddressBookEntry('78910', 'test2')
const entry3 = getMockAddressBookEntry('4781321', 'test3')
it('It should returns the list of owners with their names given a safeAddressBook and a list of owners', () => {
// given
const safeAddressBook = List([entry1, entry2, entry3])
const ownerList = List([
{ address: entry1.address, name: '' },
{ address: entry2.address, name: '' },
])
const expectedResult = List([
{ address: entry1.address, name: entry1.name },
{ address: entry2.address, name: entry2.name },
])
// when
const result = getOwnersWithNameFromAddressBook(safeAddressBook, ownerList)
// then
expect(result).toStrictEqual(expectedResult)
})
})
describe('saveAddressBook', () => {
const safeAddress1 = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
const safeAddress2 = '0x344B941b1aAE2e4Be73987212FC4741687Bf0503'
const entry1 = getMockAddressBookEntry('123456', 'test1')
const entry2 = getMockAddressBookEntry('78910', 'test2')
const entry3 = getMockAddressBookEntry('4781321', 'test3')
it('It should save a given addressBook to the localStorage', async () => {
// given
const addressBook = Map({ [safeAddress1]: List([entry1, entry2]), [safeAddress2]: List([entry3]) })
// when
// @ts-ignore
await saveAddressBook(addressBook)
const storedAdBk = await getAddressBookFromStorage()
// @ts-ignore
let result = buildAddressBook(storedAdBk)
// then
expect(result).toStrictEqual(addressBook)
})
})

View File

@ -1,27 +1,28 @@
import { List } from 'immutable'
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
import { AddressBookEntryProps } from './../model/addressBook'
import { AddressBookEntryRecord, AddressBookEntryProps } from '../model/addressBook'
import { SafeOwner } from 'src/logic/safe/store/models/safe'
import { AddressBookCollection } from '../store/reducer/addressBook'
import { AddressBookMap } from '../store/reducer/types/addressBook'
const ADDRESS_BOOK_STORAGE_KEY = 'ADDRESS_BOOK_STORAGE_KEY'
export const getAddressBookFromStorage = async (): Promise<Array<AddressBookEntryProps> | undefined> => {
const data = await loadFromStorage<Array<AddressBookEntryProps>>(ADDRESS_BOOK_STORAGE_KEY)
return data
return await loadFromStorage<Array<AddressBookEntryProps>>(ADDRESS_BOOK_STORAGE_KEY)
}
export const saveAddressBook = async (addressBook) => {
export const saveAddressBook = async (addressBook: AddressBookMap): Promise<void> => {
try {
await saveToStorage(ADDRESS_BOOK_STORAGE_KEY, addressBook.toJSON())
await saveToStorage(ADDRESS_BOOK_STORAGE_KEY, addressBook.toJS())
} catch (err) {
console.error('Error storing addressBook in localstorage', err)
}
}
export const getAddressesListFromAdbk = (addressBook) => Array.from(addressBook).map((entry: any) => entry.address)
export const getAddressesListFromSafeAddressBook = (addressBook: AddressBookCollection): string[] =>
Array.from(addressBook).map((entry: AddressBookEntryRecord) => entry.address)
export const getNameFromAdbk = (addressBook, userAddress) => {
export const getNameFromSafeAddressBook = (addressBook: AddressBookCollection, userAddress: string): string | null => {
const entry = addressBook.find((addressBookItem) => addressBookItem.address === userAddress)
if (entry) {
return entry.name
@ -30,18 +31,17 @@ export const getNameFromAdbk = (addressBook, userAddress) => {
}
export const getOwnersWithNameFromAddressBook = (
addressBook: AddressBookEntryProps,
addressBook: AddressBookCollection,
ownerList: List<SafeOwner>,
): List<SafeOwner> | [] => {
if (!ownerList) {
return []
}
const ownersListWithAdbkNames = ownerList.map((owner) => {
const ownerName = getNameFromAdbk(addressBook, owner.address)
return ownerList.map((owner) => {
const ownerName = getNameFromSafeAddressBook(addressBook, owner.address)
return {
address: owner.address,
name: ownerName || owner.name,
}
})
return ownersListWithAdbkNames
}

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

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

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(() => {

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

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

@ -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 RejectEventHandler = () => void
const createTransaction = (
{
@ -124,6 +126,7 @@ const createTransaction = (
origin = null,
}: CreateTransactionArgs,
onUserConfirm?: ConfirmEventHandler,
onUserReject?: RejectEventHandler,
): CreateTransactionAction => async (dispatch: Dispatch, getState: () => AppReduxState): Promise<void> => {
const state = getState()
@ -205,6 +208,7 @@ const createTransaction = (
confirmations: [], // this is used to determine if a tx is pending or not. See `calculateTransactionStatus` helper
value: txArgs.valueInWei,
safeTxHash,
dataDecoded: decodeMethods(txArgs.data),
submissionDate: new Date().toISOString(),
}
const mockedTx = await mockTransaction(txToMock, safeAddress, state)
@ -240,6 +244,21 @@ const createTransaction = (
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
removeTxFromStore(mockedTx, safeAddress, dispatch, state)
console.error('Tx error: ', error)
// Different wallets return different error messages in this case. This is an assumption that if
// error message includes "user" word, the tx was rejected by user
let errorIncludesUserWord = false
if (typeof error === 'string') {
errorIncludesUserWord = (error as string).includes('User') || (error as string).includes('user')
}
if (error.message) {
errorIncludesUserWord = error.message.includes('User') || error.message.includes('user')
}
if (errorIncludesUserWord) {
onUserReject?.()
}
})
.then(async (receipt) => {
if (pendingExecutionKey) {

View File

@ -18,7 +18,7 @@ import { Action, Dispatch } from 'redux'
import { SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts'
import { AppReduxState } from 'src/store'
const buildOwnersFrom = (safeOwners: string[], localSafe: SafeRecordProps): List<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(),

View File

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

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)
@ -85,6 +84,8 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres
dispatch(closeSnackbarAction(beforeExecutionKey))
await saveTxToHistory({ ...txArgs, signature })
// TODO: while we wait for the tx to be stored in the service and later update the tx info
// we should update the tx status in the store to disable owners' action buttons
dispatch(enqueueSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded))
dispatch(fetchTransactions(safeAddress))
@ -105,9 +106,7 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres
const txToMock: TxToMock = {
...txArgs,
confirmations: txArgs.confirmations, // this is used to determine if a tx is pending or not. See `calculateTransactionStatus` helper
value: txArgs.valueInWei,
submissionDate: txArgs.submissionDate,
}
const mockedTx = await mockTransaction(txToMock, safeAddress, state)
@ -123,10 +122,14 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres
await Promise.all([
saveTxToHistory({ ...txArgs, txHash }),
storeTx(
mockedTx.updateIn(
['ownersWithPendingActions', mockedTx.isCancellationTx ? 'reject' : 'confirm'],
(previous) => previous.push(from),
),
mockedTx.withMutations((record) => {
record
.updateIn(
['ownersWithPendingActions', mockedTx.isCancellationTx ? 'reject' : 'confirm'],
(previous) => previous.push(from),
)
.set('status', TransactionStatus.PENDING)
}),
safeAddress,
dispatch,
state,
@ -175,16 +178,20 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres
: TransactionStatus.FAILED,
)
.updateIn(['ownersWithPendingActions', 'reject'], (prev) => prev.clear())
.updateIn(['ownersWithPendingActions', 'confirm'], (prev) => prev.clear())
})
: mockedTx.withMutations((record) => {
record
.updateIn(['ownersWithPendingActions', toStoreTx.isCancellationTx ? 'reject' : 'confirm'], (previous) =>
previous.pop(),
)
.set('status', TransactionStatus.AWAITING_CONFIRMATIONS)
})
: mockedTx.set('status', TransactionStatus.AWAITING_CONFIRMATIONS)
await storeTx(
toStoreTx.withMutations((record) => {
record
.set('confirmations', fromJS([...tx.confirmations, makeConfirmation({ owner: from })]))
.updateIn(['ownersWithPendingActions', toStoreTx.isCancellationTx ? 'reject' : 'confirm'], (previous) =>
previous.pop(from),
)
toStoreTx.update('confirmations', (confirmations) => {
const index = confirmations.findIndex(({ owner }) => owner === from)
return index === -1 ? confirmations.push(makeConfirmation({ owner: from })) : confirmations
}),
safeAddress,
dispatch,

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

@ -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,4 +1,6 @@
import updateSafe from './updateSafe'
import { Set } from 'immutable'
import { Dispatch } from 'redux'
// the selector uses ownProps argument/router props to get the address of the safe
// so in order to use it I had to recreate the same structure
@ -10,7 +12,7 @@ import updateSafe from './updateSafe'
// },
// })
const updateActiveTokens = (safeAddress, activeTokens) => async (dispatch) => {
const updateActiveTokens = (safeAddress: string, activeTokens: Set<string>) => (dispatch: Dispatch): void => {
dispatch(updateSafe({ address: safeAddress, activeTokens }))
}

View File

@ -1,6 +1,8 @@
import updateSafe from './updateSafe'
import { Dispatch } from 'redux'
import { Set } from 'immutable'
const updateBlacklistedTokens = (safeAddress, blacklistedTokens) => async (dispatch) => {
const updateBlacklistedTokens = (safeAddress: string, blacklistedTokens: Set<string>) => (dispatch: Dispatch): void => {
dispatch(updateSafe({ address: safeAddress, blacklistedTokens }))
}

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
}
@ -104,7 +104,7 @@ const notificationsMiddleware = (store) => (next) => async (action) => {
case ADD_INCOMING_TRANSACTIONS: {
action.payload.forEach((incomingTransactions, safeAddress) => {
const { latestIncomingTxBlock } = state.safes.get('safes').get(safeAddress)
const viewedSafes = state.currentSession ? state.currentSession.get('viewedSafes') : []
const viewedSafes = state.currentSession['viewedSafes']
const recurringUser = viewedSafes?.includes(safeAddress)
const newIncomingTransactions = incomingTransactions.filter((tx) => tx.blockNumber > latestIncomingTxBlock)

View File

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

@ -37,7 +37,7 @@ export const buildSafe = (storedSafe: SafeRecordProps): SafeRecordProps => {
blacklistedTokens,
activeAssets,
blacklistedAssets,
latestIncomingTxBlock: null,
latestIncomingTxBlock: 0,
modules: null,
}
}
@ -65,7 +65,7 @@ export default handleActions(
const safeActiveTokens = map.getIn(['safes', safeAddress, 'activeTokens'])
const activeTokens = safeActiveTokens.add(tokenAddress)
map.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.merge({ activeTokens }))
map.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.mergeDeep({ activeTokens }))
})
})
},
@ -77,7 +77,7 @@ export default handleActions(
// with initial props and it would overwrite existing ones
if (state.hasIn(['safes', safe.address])) {
return state.updateIn(['safes', safe.address], (prevSafe) => prevSafe.merge(safe))
return state
}
return state.setIn(['safes', safe.address], makeSafe(safe))

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

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

@ -39,7 +39,7 @@ export const ethSigner = async ({
return reject(err)
}
if (signature.result == null) {
if (signature?.result == null) {
reject(new Error(ETH_SIGN_NOT_SUPPORTED_ERROR_MSG))
return
}

View File

@ -2,11 +2,18 @@ import { loadFromStorage, saveToStorage } from 'src/utils/storage'
import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
export const SAFES_KEY = 'SAFES'
export const TX_KEY = 'TX'
export const DEFAULT_SAFE_KEY = 'DEFAULT_SAFE'
type StoredSafes = Record<string, SafeRecordProps>
export const loadStoredSafes = async (): Promise<StoredSafes | undefined> => {
const safes = await loadFromStorage<StoredSafes>(SAFES_KEY)
return safes
}
export const getSafeName = async (safeAddress: string): Promise<string | undefined> => {
const safes = await loadFromStorage(SAFES_KEY)
const safes = await loadStoredSafes()
if (!safes) {
return undefined
}
@ -23,9 +30,9 @@ export const saveSafes = async (safes) => {
}
}
export const getLocalSafe = async (safeAddress: string): Promise<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 }

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)

View File

@ -16,8 +16,7 @@ export const loadLastUsedProvider = async (): Promise<string | undefined> => {
return lastUsedProvider
}
let watcherInterval = null
let watcherInterval
const providerWatcherMware = (store) => (next) => async (action) => {
const handledAction = next(action)

View File

@ -0,0 +1,282 @@
//@ts-nocheck
import {
isUserAnOwner,
isUserAnOwnerOfAnySafe,
isValidEnsName,
sameAddress,
shortVersionOf,
} from 'src/logic/wallets/ethAddresses'
import makeSafe from 'src/logic/safe/store/models/safe'
import { makeOwner } from 'src/logic/safe/store/models/owner'
import { List } from 'immutable'
describe('Utility function: sameAddress', () => {
it('It should return false if no address given', () => {
// given
const safeAddress = null
const safeAddress2 = '0x344B941b1aAE2e4Be73987212FC4741687Bf0503'
// when
const result = sameAddress(safeAddress, safeAddress2)
// then
expect(result).toBe(false)
})
it('It should return false if not second address given', () => {
// given
const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
const safeAddress2 = null
// when
const result = sameAddress(safeAddress, safeAddress2)
// then
expect(result).toBe(false)
})
it('It should return true if two equal addresses given', () => {
// given
const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
// when
const result = sameAddress(safeAddress, safeAddress)
// then
expect(result).toBe(true)
})
it('If should return false if two different addresses given', () => {
// given
const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
const safeAddress2 = '0x344B941b1aAE2e4Be73987212FC4741687Bf0503'
// when
const result = sameAddress(safeAddress, safeAddress2)
// then
expect(result).toBe(false)
})
})
describe('Utility function: shortVersionOf', () => {
it('It should return Unknown if no address given', () => {
// given
const safeAddress = null
const cut = 5
const expectedResult = 'Unknown'
// when
const result = shortVersionOf(safeAddress, cut)
// then
expect(result).toBe(expectedResult)
})
it('It should return 0x344...f0503 if given 0x344B941b1aAE2e4Be73987212FC4741687Bf0503 and a cut = 5', () => {
// given
const safeAddress = '0x344B941b1aAE2e4Be73987212FC4741687Bf0503'
const cut = 5
const expectedResult = `0x344...f0503`
// when
const result = shortVersionOf(safeAddress, cut)
// then
expect(result).toBe(expectedResult)
})
it('If should return the same address if a cut value bigger than the address length given', () => {
// given
const safeAddress = '0x344B941b1aAE2e4Be73987212FC4741687Bf0503'
const cut = safeAddress.length
const expectedResult = safeAddress
// when
const result = shortVersionOf(safeAddress, cut)
// then
expect(result).toBe(expectedResult)
})
})
describe('Utility function: isUserAnOwner', () => {
it("Should return false if there's no Safe", () => {
// given
const userAddress = 'address1'
const safeInstance = null
const expectedResult = false
// when
const result = isUserAnOwner(safeInstance, userAddress)
// then
expect(result).toBe(expectedResult)
})
it("Should return false if there's no `userAccount`", () => {
// given
const userAddress = null
const owners = List([makeOwner({ address: userAddress })])
const safeInstance = makeSafe({ owners })
const expectedResult = false
// when
const result = isUserAnOwner(safeInstance, userAddress)
// then
expect(result).toBe(expectedResult)
})
it('Should return false if there are no owners for the Safe', () => {
// given
const userAddress = 'address1'
const owners = null
const safeInstance = makeSafe({ owners })
const expectedResult = false
// when
const result = isUserAnOwner(safeInstance, userAddress)
// then
expect(result).toBe(expectedResult)
})
it("Should return true if `userAccount` is not in the list of Safe's owners", () => {
// given
const userAddress = 'address1'
const owners = List([makeOwner({ address: userAddress })])
const safeInstance = makeSafe({ owners })
const expectedResult = true
// when
const result = isUserAnOwner(safeInstance, userAddress)
// then
expect(result).toBe(expectedResult)
})
it("Should return false if `userAccount` is not in the list of Safe's owners", () => {
// given
const userAddress = 'address1'
const userAddress2 = 'address2'
const owners = List([makeOwner({ address: userAddress })])
const safeInstance = makeSafe({ owners })
const expectedResult = false
// when
const result = isUserAnOwner(safeInstance, userAddress2)
// then
expect(result).toBe(expectedResult)
})
})
describe('Utility function: isUserAnOwnerOfAnySafe', () => {
it('Should return true if given a list of safes, one of them has an owner equal to the userAccount', () => {
// given
const userAddress = 'address1'
const userAddress2 = 'address2'
const owners1 = List([makeOwner({ address: userAddress })])
const owners2 = List([makeOwner({ address: userAddress2 })])
const safeInstance = makeSafe({ owners: owners1 })
const safeInstance2 = makeSafe({ owners: owners2 })
const safesList = List([safeInstance, safeInstance2])
const expectedResult = true
// when
const result = isUserAnOwnerOfAnySafe(safesList, userAddress)
// then
expect(result).toBe(expectedResult)
})
it('It should return false if given a list of safes, none of them has an owner equal to the userAccount', () => {
// given
const userAddress = 'address1'
const userAddress2 = 'address2'
const userAddress3 = 'address3'
const owners1 = List([makeOwner({ address: userAddress3 })])
const owners2 = List([makeOwner({ address: userAddress2 })])
const safeInstance = makeSafe({ owners: owners1 })
const safeInstance2 = makeSafe({ owners: owners2 })
const safesList = List([safeInstance, safeInstance2])
const expectedResult = false
// when
const result = isUserAnOwnerOfAnySafe(safesList, userAddress)
// then
expect(result).toBe(expectedResult)
})
})
describe('Utility function: isValidEnsName', () => {
it('If should return false if given no ens name', () => {
// given
const ensName = null
const expectedResult = false
// when
const result = isValidEnsName(ensName)
// then
expect(result).toBe(expectedResult)
})
it('It should return false for an ens without extension in format [value].[eth|test|xyz|luxe]', () => {
// given
const ensName = 'test'
const expectedResult = false
// when
const result = isValidEnsName(ensName)
// then
expect(result).toBe(expectedResult)
})
it('It should return false for an ens without the format [value].[eth|test|xyz|luxe]', () => {
// given
const ensName = 'test.et12312'
const expectedResult = false
// when
const result = isValidEnsName(ensName)
// then
expect(result).toBe(expectedResult)
})
it('It should return true for an ens in format [value].eth', () => {
// given
const ensName = 'test.eth'
const expectedResult = true
// when
const result = isValidEnsName(ensName)
// then
expect(result).toBe(expectedResult)
})
it('It should return true for ens in format [value].test', () => {
// given
const ensName = 'test.test'
const expectedResult = true
// when
const result = isValidEnsName(ensName)
// then
expect(result).toBe(expectedResult)
})
it('It should return true for an ens in the format [value].xyz', () => {
// given
const ensName = 'test.xyz'
const expectedResult = true
// when
const result = isValidEnsName(ensName)
// then
expect(result).toBe(expectedResult)
})
it('It should return true for an ens in format [value].luxe', () => {
// given
const ensName = 'test.luxe'
const expectedResult = true
// when
const result = isValidEnsName(ensName)
// then
expect(result).toBe(expectedResult)
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 986 KiB

View File

@ -1,15 +1,16 @@
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
import { WALLET_PROVIDER } from 'src/logic/wallets/getWeb3'
import contractDataImage from './images/contractData.png'
const USER_ENABLED_LEDGER_TX_DATA = 'USER_ENABLED_LEDGER_TX_DATA'
function transactionDataCheck(): any {
return async (stateAndHelpers) => {
const { wallet } = stateAndHelpers
const isTransactionDataEnabled = await loadFromStorage<boolean>(USER_ENABLED_LEDGER_TX_DATA)
if (wallet && wallet.name === WALLET_PROVIDER.LEDGER && !isTransactionDataEnabled) {
if (wallet && wallet.name.toUpperCase() === WALLET_PROVIDER.LEDGER && !isTransactionDataEnabled) {
return {
heading: 'Allow Transaction Data', // edit modal heading here
description: 'Please allow transaction data on your Ledger device.', // edit modal description that is displayed here. You can include html strings here and they will be rendered as html elements.
description: `<div><p><strong>Important</strong>: In order to sign transactions with your Ledger device, you will have to activate the "Contract Data" setting in the Ethereum app on your Ledger.</p><img style="width:100%" src=${contractDataImage} /></div>`, // edit modal description that is displayed here. You can include html strings here and they will be rendered as html elements.
eventCode: 'allowTransactionData',
button: {
text: 'Done',

View File

@ -10,7 +10,13 @@ import { StepperPageFormProps } from 'src/components/Stepper'
import AddressInput from 'src/components/forms/AddressInput'
import Field from 'src/components/forms/Field'
import TextField from 'src/components/forms/TextField'
import { mustBeEthereumAddress, noErrorsOn, required } from 'src/components/forms/validator'
import {
mustBeEthereumAddress,
noErrorsOn,
required,
composeValidators,
minMaxLength,
} from 'src/components/forms/validator'
import Block from 'src/components/layout/Block'
import Col from 'src/components/layout/Col'
import Paragraph from 'src/components/layout/Paragraph'
@ -109,7 +115,7 @@ const DetailsForm = ({ errors, form }: DetailsFormProps): React.ReactElement =>
placeholder="Name of the Safe"
text="Safe name"
type="text"
validate={required}
validate={composeValidators(required, minMaxLength(1, 50))}
testId="load-safe-name-field"
/>
</Col>
@ -120,6 +126,8 @@ const DetailsForm = ({ errors, form }: DetailsFormProps): React.ReactElement =>
fieldMutator={(val) => {
form.mutators.setValue(FIELD_LOAD_ADDRESS, val)
}}
// eslint-disable-next-line
// @ts-ignore
inputAdornment={
noErrorsOn(FIELD_LOAD_ADDRESS, errors) && {
endAdornment: (
@ -156,12 +164,15 @@ const DetailsForm = ({ errors, form }: DetailsFormProps): React.ReactElement =>
)
}
const DetailsPage = () => (controls: React.ReactNode, { errors, form }: StepperPageFormProps): React.ReactElement => (
<>
<OpenPaper controls={controls}>
<DetailsForm errors={errors} form={form} />
</OpenPaper>
</>
)
const DetailsPage = () =>
function LoadSafeDetails(controls: React.ReactNode, { errors, form }: StepperPageFormProps): React.ReactElement {
return (
<>
<OpenPaper controls={controls}>
<DetailsForm errors={errors} form={form} />
</OpenPaper>
</>
)
}
export default DetailsPage

View File

@ -8,7 +8,7 @@ import Identicon from 'src/components/Identicon'
import OpenPaper from 'src/components/Stepper/OpenPaper'
import Field from 'src/components/forms/Field'
import TextField from 'src/components/forms/TextField'
import { required } from 'src/components/forms/validator'
import { composeValidators, minMaxLength, required } from 'src/components/forms/validator'
import Block from 'src/components/layout/Block'
import Col from 'src/components/layout/Col'
import Hairline from 'src/components/layout/Hairline'
@ -79,7 +79,7 @@ const calculateSafeValues = (owners, threshold, values) => {
}
const OwnerListComponent = (props) => {
const [owners, setOwners] = useState([])
const [owners, setOwners] = useState<string[]>([])
const { classes, updateInitialProps, values } = props
useEffect(() => {
@ -132,7 +132,7 @@ const OwnerListComponent = (props) => {
placeholder="Owner Name*"
text="Owner Name"
type="text"
validate={required}
validate={composeValidators(required, minMaxLength(1, 50))}
testId={`load-safe-owner-name-${index}`}
/>
</Col>
@ -156,12 +156,15 @@ const OwnerListComponent = (props) => {
const OwnerListPage = withStyles(styles as any)(OwnerListComponent)
const OwnerList = ({ updateInitialProps }, network) => (controls, { values }) => (
<>
<OpenPaper controls={controls} padding={false}>
<OwnerListPage network={network} updateInitialProps={updateInitialProps} values={values} />
</OpenPaper>
</>
)
const OwnerList = ({ updateInitialProps }, network) =>
function LoadSafeOwnerList(controls, { values }): React.ReactElement {
return (
<>
<OpenPaper controls={controls} padding={false}>
<OwnerListPage network={network} updateInitialProps={updateInitialProps} values={values} />
</OpenPaper>
</>
)
}
export default OwnerList

View File

@ -6,12 +6,11 @@ import { FIELD_LOAD_ADDRESS, FIELD_LOAD_NAME } from '../components/fields'
import Page from 'src/components/layout/Page'
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import { SAFES_KEY, saveSafes } from 'src/logic/safe/utils'
import { saveSafes, loadStoredSafes } from 'src/logic/safe/utils'
import { getNamesFrom, getOwnersFrom } from 'src/routes/open/utils/safeDataExtractor'
import { SAFELIST_ADDRESS } from 'src/routes/routes'
import { buildSafe } from 'src/logic/safe/store/actions/fetchSafe'
import { history } from 'src/store'
import { loadFromStorage } from 'src/utils/storage'
import { SafeOwner, SafeRecordProps } from 'src/logic/safe/store/models/safe'
import { List } from 'immutable'
import { checksumAddress } from 'src/utils/checksumAddress'
@ -27,7 +26,7 @@ export const loadSafe = async (
const safeProps = await buildSafe(safeAddress, safeName)
safeProps.owners = owners
const storedSafes = (await loadFromStorage(SAFES_KEY)) || {}
const storedSafes = (await loadStoredSafes()) || {}
storedSafes[safeAddress] = safeProps

View File

@ -1,16 +1,16 @@
import { withStyles } from '@material-ui/core/styles'
import { createStyles, makeStyles } from '@material-ui/core/styles'
import * as React from 'react'
import OpenPaper from 'src/components/Stepper/OpenPaper'
import Field from 'src/components/forms/Field'
import TextField from 'src/components/forms/TextField'
import { required } from 'src/components/forms/validator'
import { composeValidators, minMaxLength, required } from 'src/components/forms/validator'
import Block from 'src/components/layout/Block'
import Paragraph from 'src/components/layout/Paragraph'
import { FIELD_NAME } from 'src/routes/open/components/fields'
import { secondary, sm } from 'src/theme/variables'
const styles = () => ({
const styles = createStyles({
root: {
display: 'flex',
maxWidth: '440px',
@ -28,51 +28,56 @@ const styles = () => ({
},
})
const SafeName = ({ classes, safeName }) => (
<>
<Block margin="lg">
<Paragraph color="primary" noMargin size="md">
You are about to create a new Gnosis Safe wallet with one or more owners. First, let&apos;s give your new wallet
a name. This name is only stored locally and will never be shared with Gnosis or any third parties.
</Paragraph>
</Block>
<Block className={classes.root} margin="lg">
<Field
component={TextField}
defaultValue={safeName}
name={FIELD_NAME}
placeholder="Name of the new Safe"
text="Safe name"
type="text"
validate={required}
testId="create-safe-name-field"
/>
</Block>
<Block margin="lg">
<Paragraph className={classes.links} color="primary" noMargin size="md">
By continuing you consent to the{' '}
<a href="https://gnosis-safe.io/terms" rel="noopener noreferrer" target="_blank">
terms of use
</a>{' '}
and{' '}
<a href="https://gnosis-safe.io/privacy" rel="noopener noreferrer" target="_blank">
privacy policy
</a>
.
</Paragraph>
</Block>
</>
)
const useSafeNameStyles = makeStyles(styles)
const SafeNameForm = withStyles(styles as any)(SafeName)
const SafeNameForm = ({ safeName }: { safeName: string }): React.ReactElement => {
const classes = useSafeNameStyles()
const SafeNamePage = () => (controls, { values }) => {
const { safeName } = values
return (
<OpenPaper controls={controls}>
<SafeNameForm safeName={safeName} />
</OpenPaper>
<>
<Block margin="lg">
<Paragraph color="primary" noMargin size="md">
You are about to create a new Gnosis Safe wallet with one or more owners. First, let&apos;s give your new
wallet a name. This name is only stored locally and will never be shared with Gnosis or any third parties.
</Paragraph>
</Block>
<Block className={classes.root} margin="lg">
<Field
component={TextField}
defaultValue={safeName}
name={FIELD_NAME}
placeholder="Name of the new Safe"
text="Safe name"
type="text"
validate={composeValidators(required, minMaxLength(1, 50))}
testId="create-safe-name-field"
/>
</Block>
<Block margin="lg">
<Paragraph className={classes.links} color="primary" noMargin size="md">
By continuing you consent to the{' '}
<a href="https://gnosis-safe.io/terms" rel="noopener noreferrer" target="_blank">
terms of use
</a>{' '}
and{' '}
<a href="https://gnosis-safe.io/privacy" rel="noopener noreferrer" target="_blank">
privacy policy
</a>
.
</Paragraph>
</Block>
</>
)
}
export default SafeNamePage
const SafeNamePageComponent = () =>
function SafeNamePage(controls, { values }): React.ReactElement {
const { safeName } = values
return (
<OpenPaper controls={controls}>
<SafeNameForm safeName={safeName} />
</OpenPaper>
)
}
export default SafeNamePageComponent

View File

@ -16,7 +16,14 @@ import AddressInput from 'src/components/forms/AddressInput'
import Field from 'src/components/forms/Field'
import SelectField from 'src/components/forms/SelectField'
import TextField from 'src/components/forms/TextField'
import { composeValidators, minValue, mustBeInteger, noErrorsOn, required } from 'src/components/forms/validator'
import {
composeValidators,
minValue,
mustBeInteger,
noErrorsOn,
required,
minMaxLength,
} from 'src/components/forms/validator'
import Block from 'src/components/layout/Block'
import Button from 'src/components/layout/Button'
import Col from 'src/components/layout/Col'
@ -129,7 +136,7 @@ const SafeOwners = (props) => {
placeholder="Owner Name*"
text="Owner Name"
type="text"
validate={required}
validate={composeValidators(required, minMaxLength(1, 50))}
testId={`create-safe-owner-name-field-${index}`}
/>
</Col>
@ -138,6 +145,8 @@ const SafeOwners = (props) => {
fieldMutator={(val) => {
form.mutators.setValue(addressName, val)
}}
// eslint-disable-next-line
// @ts-ignore
inputAdornment={
noErrorsOn(addressName, errors) && {
endAdornment: (
@ -217,18 +226,21 @@ const SafeOwners = (props) => {
const SafeOwnersForm = withStyles(styles as any)(withRouter(SafeOwners))
const SafeOwnersPage = ({ updateInitialProps }) => (controls, { errors, form, values }) => (
<>
<OpenPaper controls={controls} padding={false}>
<SafeOwnersForm
errors={errors}
form={form}
otherAccounts={getAccountsFrom(values)}
updateInitialProps={updateInitialProps}
values={values}
/>
</OpenPaper>
</>
)
const SafeOwnersPage = ({ updateInitialProps }) =>
function OpenSafeOwnersPage(controls, { errors, form, values }) {
return (
<>
<OpenPaper controls={controls} padding={false}>
<SafeOwnersForm
errors={errors}
form={form}
otherAccounts={getAccountsFrom(values)}
updateInitialProps={updateInitialProps}
values={values}
/>
</OpenPaper>
</>
)
}
export default SafeOwnersPage

View File

@ -161,7 +161,7 @@ const Open = ({ addSafe, network, provider, userAccount }: OwnProps): React.Reac
pathname: `${SAFELIST_ADDRESS}/${safeProps.address}/balances`,
state: {
name,
tx: pendingCreation.txHash,
tx: pendingCreation?.txHash,
},
}
@ -177,7 +177,7 @@ const Open = ({ addSafe, network, provider, userAccount }: OwnProps): React.Reac
const onRetry = async () => {
const values = await loadFromStorage<{ txHash: string }>(SAFE_PENDING_CREATION_STORAGE_KEY)
delete values.txHash
delete values?.txHash
await saveToStorage(SAFE_PENDING_CREATION_STORAGE_KEY, values)
setSafeCreationPendingInfo(values)
createSafeProxy()

View File

@ -105,7 +105,7 @@ const BackButton = styled(Button)`
const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, provider, submittedPromise }: any) => {
const [loading, setLoading] = useState(true)
const [stepIndex, setStepIndex] = useState(0)
const [safeCreationTxHash, setSafeCreationTxHash] = useState()
const [safeCreationTxHash, setSafeCreationTxHash] = useState('')
const [createdSafeAddress, setCreatedSafeAddress] = useState()
const [error, setError] = useState(false)
@ -242,7 +242,7 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, provider
useEffect(() => {
let interval
const awaitUntilSafeIsDeployed = async () => {
const awaitUntilSafeIsDeployed = async (safeCreationTxHash: string) => {
try {
const web3 = getWeb3()
const receipt = await web3.eth.getTransactionReceipt(safeCreationTxHash)
@ -283,7 +283,9 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, provider
return
}
awaitUntilSafeIsDeployed()
if (typeof safeCreationTxHash === 'string') {
awaitUntilSafeIsDeployed(safeCreationTxHash)
}
return () => {
clearInterval(interval)
@ -294,7 +296,7 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, provider
return <Loader size="sm" />
}
let FooterComponent = null
let FooterComponent
if (error) {
FooterComponent = ErrorFooter
} else if (steps[stepIndex].footerComponent) {

View File

@ -20,7 +20,7 @@ import Hairline from 'src/components/layout/Hairline'
import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import { getAddressBook } from 'src/logic/addressBook/store/selectors'
import { getAddressesListFromAdbk } from 'src/logic/addressBook/utils'
import { getAddressesListFromSafeAddressBook } from 'src/logic/addressBook/utils'
export const CREATE_ENTRY_INPUT_NAME_ID = 'create-entry-input-name'
export const CREATE_ENTRY_INPUT_ADDRESS_ID = 'create-entry-input-address'
@ -43,7 +43,7 @@ const CreateEditEntryModalComponent = ({
}
const addressBook = useSelector(getAddressBook)
const addressBookAddressesList = getAddressesListFromAdbk(addressBook)
const addressBookAddressesList = getAddressesListFromSafeAddressBook(addressBook)
const entryDoesntExist = uniqueAddress(addressBookAddressesList)
const formMutators = {

View File

@ -41,6 +41,7 @@ import RemoveOwnerIcon from 'src/routes/safe/components/Settings/assets/icons/bi
import RemoveOwnerIconDisabled from 'src/routes/safe/components/Settings/assets/icons/disabled-bin.svg'
import { addressBookQueryParamsSelector, safesListSelector } from 'src/logic/safe/store/selectors'
import { checksumAddress } from 'src/utils/checksumAddress'
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
const AddressBookTable = ({ classes }) => {
const columns = generateColumns()
@ -49,10 +50,15 @@ const AddressBookTable = ({ classes }) => {
const safesList = useSelector(safesListSelector)
const entryAddressToEditOrCreateNew = useSelector(addressBookQueryParamsSelector)
const addressBook = useSelector(getAddressBook)
const [selectedEntry, setSelectedEntry] = useState(null)
const [selectedEntry, setSelectedEntry] = useState<any>(null)
const [editCreateEntryModalOpen, setEditCreateEntryModalOpen] = useState(false)
const [deleteEntryModalOpen, setDeleteEntryModalOpen] = useState(false)
const [sendFundsModalOpen, setSendFundsModalOpen] = useState(false)
const { trackEvent } = useAnalytics()
useEffect(() => {
trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'AddressBook' })
}, [trackEvent])
useEffect(() => {
if (entryAddressToEditOrCreateNew) {
@ -64,7 +70,7 @@ const AddressBookTable = ({ classes }) => {
if (entryAddressToEditOrCreateNew) {
const checksumEntryAdd = checksumAddress(entryAddressToEditOrCreateNew)
const key = addressBook.findKey((entry) => entry.address === checksumEntryAdd)
if (key >= 0) {
if (key && key >= 0) {
// Edit old entry
const value = addressBook.get(key)
setSelectedEntry({ entry: value, index: key })

View File

@ -38,7 +38,7 @@ const Transactions = (): React.ReactElement => {
{transactionsByPage.map((tx: Transaction, index) => {
let txHash = ''
if ('transactionHash' in tx) {
txHash = tx.transactionHash
txHash = tx.transactionHash as string
}
if ('txHash' in tx) {
txHash = tx.txHash

View File

@ -14,7 +14,7 @@ const AppAgreement = (): React.ReactElement => {
const { visited } = useFormState({ subscription: { visited: true } })
// trick to prevent having the field validated by default. Not sure why this happens in this form
const validate = !visited.agreementAccepted ? undefined : required
const validate = !visited?.agreementAccepted ? undefined : required
return (
<Field

View File

@ -28,7 +28,7 @@ export const appUrlResolver = createDecorator({
},
})
export const AppInfoUpdater = ({ onAppInfo }: { onAppInfo: (appInfo: SafeApp) => void }): React.ReactElement => {
export const AppInfoUpdater = ({ onAppInfo }: { onAppInfo: (appInfo: SafeApp) => void }): null => {
const {
input: { value: appUrl },
} = useField('appUrl', { subscription: { value: true } })
@ -52,7 +52,7 @@ const AppUrl = ({ appList }: { appList: SafeApp[] }): React.ReactElement => {
const { visited } = useFormState({ subscription: { visited: true } })
// trick to prevent having the field validated by default. Not sure why this happens in this form
const validate = !visited.appUrl ? undefined : composeValidators(required, validateUrl, uniqueApp(appList))
const validate = !visited?.appUrl ? undefined : composeValidators(required, validateUrl, uniqueApp(appList))
return (
<Field label="App URL" name="appUrl" placeholder="App URL" type="text" component={TextField} validate={validate} />

View File

@ -9,14 +9,14 @@ interface SubmitButtonStatusProps {
onSubmitButtonStatusChange: (disabled: boolean) => void
}
const SubmitButtonStatus = ({ appInfo, onSubmitButtonStatusChange }: SubmitButtonStatusProps): React.ReactElement => {
const SubmitButtonStatus = ({ appInfo, onSubmitButtonStatusChange }: SubmitButtonStatusProps): null => {
const { valid, validating, visited } = useFormState({
subscription: { valid: true, validating: true, visited: true },
})
React.useEffect(() => {
// if non visited, fields were not evaluated yet. Then, the default value is considered invalid
const fieldsVisited = visited.agreementAccepted && visited.appUrl
const fieldsVisited = visited?.agreementAccepted && visited.appUrl
onSubmitButtonStatusChange(validating || !valid || !fieldsVisited || !isAppManifestValid(appInfo))
}, [validating, valid, visited, onSubmitButtonStatusChange, appInfo])

View File

@ -40,7 +40,7 @@ const INITIAL_VALUES: AddAppFormValues = {
}
const APP_INFO: SafeApp = {
id: undefined,
id: '',
url: '',
name: '',
iconUrl: appsIconSvg,

View File

@ -54,7 +54,7 @@ const AppFrame = forwardRef<HTMLIFrameElement, AppFrameProps>(function AppFrameC
const redirectToBalance = () => history.push(`${SAFELIST_ADDRESS}/${safeAddress}/balances`)
if (!selectedApp) {
return null
return <div />
}
if (!consentReceived) {

View File

@ -31,7 +31,7 @@ const isTxValid = (t: Transaction): boolean => {
}
const isAddressValid = mustBeEthereumAddress(t.to) === undefined
return isAddressValid && t.data && typeof t.data === 'string'
return isAddressValid && !!t.data && typeof t.data === 'string'
}
const Wrapper = styled.div`
@ -71,6 +71,7 @@ type OwnProps = {
ethBalance: string
onCancel: () => void
onUserConfirm: (safeTxHash: string) => void
onUserTxReject: () => void
onClose: () => void
}
@ -84,7 +85,8 @@ const ConfirmTransactionModal = ({
onCancel,
onUserConfirm,
onClose,
}: OwnProps): React.ReactElement => {
onUserTxReject,
}: OwnProps): React.ReactElement | null => {
const dispatch = useDispatch()
if (!isOpen) {
return null
@ -111,6 +113,7 @@ const ConfirmTransactionModal = ({
navigateToTransactionsTab: false,
},
handleUserConfirmation,
onUserTxReject,
),
)
onClose()
@ -133,27 +136,25 @@ const ConfirmTransactionModal = ({
<>
<AddressInfo ethBalance={ethBalance} safeAddress={safeAddress} safeName={safeName} />
<DividerLine withArrow />
{txs.map((tx, index) => {
return (
<Wrapper key={index}>
<Collapse description={<AddressInfo safeAddress={tx.to} />} title={`Transaction ${index + 1}`}>
<CollapseContent>
<div className="section">
<Heading tag="h3">Value</Heading>
<div className="value-section">
<Img alt="Ether" height={40} src={getEthAsToken('0').logoUri} />
<Bold>{humanReadableValue(tx.value, 18)} ETH</Bold>
</div>
{txs.map((tx, index) => (
<Wrapper key={index}>
<Collapse description={<AddressInfo safeAddress={tx.to} />} title={`Transaction ${index + 1}`}>
<CollapseContent>
<div className="section">
<Heading tag="h3">Value</Heading>
<div className="value-section">
<Img alt="Ether" height={40} src={getEthAsToken('0').logoUri} />
<Bold>{humanReadableValue(tx.value, 18)} ETH</Bold>
</div>
<div className="section">
<Heading tag="h3">Data (hex encoded)*</Heading>
<StyledTextBox>{tx.data}</StyledTextBox>
</div>
</CollapseContent>
</Collapse>
</Wrapper>
)
})}
</div>
<div className="section">
<Heading tag="h3">Data (hex encoded)*</Heading>
<StyledTextBox>{tx.data}</StyledTextBox>
</div>
</CollapseContent>
</Collapse>
</Wrapper>
))}
</>
)

View File

@ -14,6 +14,8 @@ type Props = {
onAppRemoved: (appId: string) => void
}
type AppListItem = SafeApp & { checked: boolean }
const ManageApps = ({ appList, onAppAdded, onAppToggle, onAppRemoved }: Props): React.ReactElement => {
const [isOpen, setIsOpen] = useState(false)
const [isSubmitDisabled, setIsSubmitDisabled] = useState(true)
@ -28,7 +30,7 @@ const ManageApps = ({ appList, onAppAdded, onAppToggle, onAppRemoved }: Props):
const closeModal = () => setIsOpen(false)
const getItemList = () =>
const getItemList = (): AppListItem[] =>
appList.map((a) => {
return { ...a, checked: !a.disabled }
})

View File

@ -42,7 +42,7 @@ const useAppList = (): UseAppListReturnType => {
}
})
let apps = []
let apps: SafeApp[] = []
// using the appURL to recover app info
for (let index = 0; index < list.length; index++) {
try {

View File

@ -8,6 +8,7 @@ import {
INTERFACE_MESSAGES,
RequestId,
Transaction,
LowercaseNetworks,
} from '@gnosis.pm/safe-apps-sdk'
import { useDispatch, useSelector } from 'react-redux'
import { useEffect, useCallback, MutableRefObject } from 'react'
@ -44,7 +45,7 @@ const useIframeMessageHandler = (
selectedApp: SafeApp | undefined,
openConfirmationModal: (txs: Transaction[], requestId: RequestId) => void,
closeModal: () => void,
iframeRef: MutableRefObject<HTMLIFrameElement>,
iframeRef: MutableRefObject<HTMLIFrameElement | null>,
): ReturnType => {
const { enqueueSnackbar, closeSnackbar } = useSnackbar()
const safeName = useSelector(safeNameSelector)
@ -60,8 +61,8 @@ const useIframeMessageHandler = (
requestId: requestId || Math.trunc(window.performance.now()),
}
if (iframeRef?.current && selectedApp) {
iframeRef.current.contentWindow.postMessage(requestWithMessage, selectedApp.url)
if (iframeRef && selectedApp) {
iframeRef.current?.contentWindow?.postMessage(requestWithMessage, selectedApp.url)
}
},
[iframeRef, selectedApp],
@ -77,7 +78,9 @@ const useIframeMessageHandler = (
switch (msg.data.messageId) {
case SDK_MESSAGES.SEND_TRANSACTIONS: {
openConfirmationModal(msg.data.data, requestId)
if (msg.data.data) {
openConfirmationModal(msg.data.data, requestId)
}
break
}
@ -85,9 +88,9 @@ const useIframeMessageHandler = (
const message = {
messageId: INTERFACE_MESSAGES.ON_SAFE_INFO,
data: {
safeAddress,
network: network,
ethBalance,
safeAddress: safeAddress as string,
network: network.toLowerCase() as LowercaseNetworks,
ethBalance: ethBalance as string,
},
}
@ -104,7 +107,7 @@ const useIframeMessageHandler = (
if (message.origin === window.origin) {
return
}
if (!selectedApp.url.includes(message.origin)) {
if (!selectedApp?.url.includes(message.origin)) {
console.error(`ThirdPartyApp: A message was received from an unknown origin ${message.origin}`)
return
}

View File

@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useRef, useState, useMemo } from 'react'
import { INTERFACE_MESSAGES, Transaction, RequestId } from '@gnosis.pm/safe-apps-sdk'
import { INTERFACE_MESSAGES, Transaction, RequestId, LowercaseNetworks } from '@gnosis.pm/safe-apps-sdk'
import { Card, IconText, Loader, Menu, Title } from '@gnosis.pm/safe-react-components'
import { useSelector } from 'react-redux'
import styled, { css } from 'styled-components'
@ -7,6 +7,7 @@ import styled, { css } from 'styled-components'
import ManageApps from './components/ManageApps'
import AppFrame from './components/AppFrame'
import { useAppList } from './hooks/useAppList'
import { SafeApp } from './types.d'
import LCL from 'src/components/ListContentLayout'
import { networkSelector } from 'src/logic/wallets/store/selectors'
@ -19,6 +20,7 @@ import {
import { isSameURL } from 'src/utils/url'
import { useIframeMessageHandler } from './hooks/useIframeMessageHandler'
import ConfirmTransactionModal from './components/ConfirmTransactionModal'
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
const centerCSS = css`
display: flex;
@ -62,8 +64,9 @@ const Apps = (): React.ReactElement => {
const [confirmTransactionModal, setConfirmTransactionModal] = useState<ConfirmTransactionModalState>(
INITIAL_CONFIRM_TX_MODAL_STATE,
)
const iframeRef = useRef<HTMLIFrameElement>()
const iframeRef = useRef<HTMLIFrameElement>(null)
const { trackEvent } = useAnalytics()
const granted = useSelector(grantedSelector)
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const safeName = useSelector(safeNameSelector)
@ -99,6 +102,13 @@ const Apps = (): React.ReactElement => {
)
}
const onUserTxReject = () => {
sendMessageToIframe(
{ messageId: INTERFACE_MESSAGES.TRANSACTION_REJECTED, data: {} },
confirmTransactionModal.requestId,
)
}
const onSelectApp = useCallback(
(appId) => {
if (selectedAppId === appId) {
@ -111,6 +121,7 @@ const Apps = (): React.ReactElement => {
[selectedAppId],
)
// Auto Select app first App
useEffect(() => {
const selectFirstEnabledApp = () => {
const firstEnabledApp = appList.find((a) => !a.disabled)
@ -124,7 +135,14 @@ const Apps = (): React.ReactElement => {
if (initialSelect || currentAppWasDisabled) {
selectFirstEnabledApp()
}
}, [appList, selectedApp, selectedAppId])
}, [appList, selectedApp, selectedAppId, trackEvent])
// track GA
useEffect(() => {
if (selectedApp) {
trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Apps', label: selectedApp.name })
}
}, [selectedApp, trackEvent])
const handleIframeLoad = useCallback(() => {
const iframe = iframeRef.current
@ -136,14 +154,14 @@ const Apps = (): React.ReactElement => {
sendMessageToIframe({
messageId: INTERFACE_MESSAGES.ON_SAFE_INFO,
data: {
safeAddress,
network,
ethBalance,
safeAddress: safeAddress as string,
network: network.toLowerCase() as LowercaseNetworks,
ethBalance: ethBalance as string,
},
})
}, [ethBalance, network, safeAddress, selectedApp, sendMessageToIframe])
if (loadingAppList || !appList.length) {
if (loadingAppList || !appList.length || !safeAddress) {
return (
<LoadingContainer>
<Loader size="md" />
@ -189,14 +207,15 @@ const Apps = (): React.ReactElement => {
</CenteredMT>
<ConfirmTransactionModal
isOpen={confirmTransactionModal.isOpen}
app={selectedApp}
app={selectedApp as SafeApp}
safeAddress={safeAddress}
ethBalance={ethBalance}
safeName={safeName}
ethBalance={ethBalance as string}
safeName={safeName as string}
txs={confirmTransactionModal.txs}
onCancel={closeConfirmationModal}
onClose={closeConfirmationModal}
onUserConfirm={onUserTxConfirm}
onUserTxReject={onUserTxReject}
/>
</>
)

View File

@ -1,5 +1,5 @@
export type SafeApp = {
id: string | undefined
id: string
url: string
name: string
iconUrl: string

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