Tech debt 1265: Enable strictNullChecks TS compiler option (#1301)

* dep bump, enable strictNullChecks ts compiler option

* working on errors wip

* remove unused imports

* nullchecks errors wip

* fixing errors

* fixing errors

* fixing errors

* fix clipboard func type errors

* error fixes

* error fixes

* error fixes

* error fixes

* error fixes

* error fixes

* error fixing

* error fixing

* error fixing

* error fixing

* error fixing

* error fixing

* error fixing

* error fixing

* error fixing

* error fixing

* error fixing

* error fixing

* error fixing

* error fixing

* error fixing

* Finally made it

* eslint fixes

* eslint fixes 2

* fix send funds validation

* Update style load in ThresholdSettings

* Fix isValidAddress default state for SendCollectible

* fix OwnersColumn return type

* fix extractUsefulMethods typing

Co-authored-by: Daniel Sanchez <daniel.sanchez@gnosis.pm>
This commit is contained in:
Mikhail Mikheev 2020-09-04 16:03:09 +04:00 committed by GitHub
parent c5bafa6569
commit bfed9679f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
144 changed files with 933 additions and 868 deletions

View File

@ -178,7 +178,7 @@
"bignumber.js": "9.0.0", "bignumber.js": "9.0.0",
"bnc-onboard": "1.12.0", "bnc-onboard": "1.12.0",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"concurrently": "^5.2.0", "concurrently": "^5.3.0",
"connected-react-router": "6.8.0", "connected-react-router": "6.8.0",
"coveralls": "^3.1.0", "coveralls": "^3.1.0",
"currency-flags": "2.1.2", "currency-flags": "2.1.2",
@ -190,19 +190,19 @@
"eth-sig-util": "^2.5.3", "eth-sig-util": "^2.5.3",
"ethereum-blockies-base64": "^1.0.2", "ethereum-blockies-base64": "^1.0.2",
"ethereumjs-abi": "0.6.8", "ethereumjs-abi": "0.6.8",
"exponential-backoff": "^3.0.1", "exponential-backoff": "^3.1.0",
"express": "^4.17.1", "express": "^4.17.1",
"final-form": "^4.20.1", "final-form": "^4.20.1",
"final-form-calculate": "^1.3.1", "final-form-calculate": "^1.3.1",
"history": "4.10.1", "history": "4.10.1",
"immortal-db": "^1.0.3", "immortal-db": "^1.1.0",
"immutable": "^4.0.0-rc.12", "immutable": "^4.0.0-rc.12",
"js-cookie": "^2.2.1", "js-cookie": "^2.2.1",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lodash.memoize": "^4.1.2", "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", "notistack": "https://github.com/gnosis/notistack.git#v0.9.4",
"open": "^7.1.0", "open": "^7.2.0",
"polished": "3.6.6", "polished": "3.6.6",
"qrcode.react": "1.0.0", "qrcode.react": "1.0.0",
"query-string": "6.13.1", "query-string": "6.13.1",
@ -215,7 +215,7 @@
"react-qr-reader": "^2.2.1", "react-qr-reader": "^2.2.1",
"react-redux": "7.2.1", "react-redux": "7.2.1",
"react-router-dom": "5.2.0", "react-router-dom": "5.2.0",
"react-scripts": "^3.4.1", "react-scripts": "^3.4.3",
"react-window": "^1.8.5", "react-window": "^1.8.5",
"recompose": "^0.30.0", "recompose": "^0.30.0",
"redux": "4.0.5", "redux": "4.0.5",
@ -263,7 +263,7 @@
"eslint-plugin-import": "2.22.0", "eslint-plugin-import": "2.22.0",
"eslint-plugin-jsx-a11y": "^6.3.1", "eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-prettier": "^3.1.4", "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", "eslint-plugin-sort-destructure-keys": "1.3.5",
"ethereumjs-abi": "0.6.8", "ethereumjs-abi": "0.6.8",
"husky": "^4.2.5", "husky": "^4.2.5",
@ -272,9 +272,9 @@
"prettier": "2.1.1", "prettier": "2.1.1",
"react-app-rewired": "^2.1.6", "react-app-rewired": "^2.1.6",
"react-docgen-typescript-loader": "^3.7.2", "react-docgen-typescript-loader": "^3.7.2",
"truffle": "5.1.36", "truffle": "5.1.41",
"typechain": "^2.0.0", "typechain": "^2.0.0",
"typescript": "3.9.7", "typescript": "3.9.7",
"wait-on": "5.1.0" "wait-on": "5.2.0"
} }
} }

View File

@ -1,9 +1,8 @@
import IconButton from '@material-ui/core/IconButton' 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 Close from '@material-ui/icons/Close'
import QRCode from 'qrcode.react' import QRCode from 'qrcode.react'
import * as React from 'react' import * as React from 'react'
import { useSelector } from 'react-redux'
import CopyBtn from 'src/components/CopyBtn' import CopyBtn from 'src/components/CopyBtn'
import EtherscanBtn from 'src/components/EtherscanBtn' 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 Hairline from 'src/components/layout/Hairline'
import Paragraph from 'src/components/layout/Paragraph' import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row' 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 { lg, md, screenSm, secondaryText, sm } from 'src/theme/variables'
import { copyToClipboard } from 'src/utils/clipboard' import { copyToClipboard } from 'src/utils/clipboard'
const styles = () => ({ const useStyles = makeStyles(
heading: { createStyles({
padding: `${md} ${lg}`, heading: {
justifyContent: 'space-between', padding: `${md} ${lg}`,
maxHeight: '75px', justifyContent: 'space-between',
boxSizing: 'border-box', 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)',
}, },
}, close: {
addressContainer: { height: lg,
flexDirection: 'column', width: lg,
justifyContent: 'center', fill: secondaryText,
margin: `${lg} 0`,
[`@media (min-width: ${screenSm}px)`]: {
flexDirection: 'row',
}, },
}, qrContainer: {
address: { backgroundColor: '#fff',
marginLeft: sm, padding: md,
marginRight: sm, borderRadius: '6px',
maxWidth: '70%', border: `1px solid ${secondaryText}`,
overflowWrap: 'break-word',
[`@media (min-width: ${screenSm}px)`]: {
maxWidth: 'none',
}, },
}, 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 ( return (
<> <>
<Row align="center" className={classes.heading} grow> <Row align="center" className={classes.heading} grow>
<Paragraph className={classes.manage} noMargin size="xl" weight="bolder"> <Paragraph noMargin size="xl" weight="bolder">
Receive funds Receive funds
</Paragraph> </Paragraph>
<IconButton disableRipple onClick={onClose}> <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 { formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount'
import { grantedSelector } from 'src/routes/safe/container/selector' import { grantedSelector } from 'src/routes/safe/container/selector'
import Receive from './ModalReceive' import Receive from './ReceiveModal'
import { useSidebarItems } from 'src/components/AppLayout/Sidebar/useSidebarItems' import { useSidebarItems } from 'src/components/AppLayout/Sidebar/useSidebarItems'
const notificationStyles = { const notificationStyles = {
@ -79,7 +79,8 @@ const App: React.FC = ({ children }) => {
const sendFunds = safeActionsState.sendFunds as { isOpen: boolean; selectedToken: string } const sendFunds = safeActionsState.sendFunds as { isOpen: boolean; selectedToken: string }
const formattedTotalBalance = currentSafeBalance ? formatAmountInUsFormat(currentSafeBalance) : '' const formattedTotalBalance = currentSafeBalance ? formatAmountInUsFormat(currentSafeBalance) : ''
const balance = !!formattedTotalBalance && !!currentCurrency ? `${formattedTotalBalance} ${currentCurrency}` : null const balance =
!!formattedTotalBalance && !!currentCurrency ? `${formattedTotalBalance} ${currentCurrency}` : undefined
useEffect(() => { useEffect(() => {
if (matchSafe?.isExact) { if (matchSafe?.isExact) {
@ -133,14 +134,16 @@ const App: React.FC = ({ children }) => {
selectedToken={sendFunds.selectedToken} selectedToken={sendFunds.selectedToken}
/> />
<Modal {safeAddress && safeName && (
description="Receive Tokens Form" <Modal
handleClose={onReceiveHide} description="Receive Tokens Form"
open={safeActionsState.showReceive as boolean} handleClose={onReceiveHide}
title="Receive Tokens" open={safeActionsState.showReceive as boolean}
> title="Receive Tokens"
<Receive onClose={onReceiveHide} /> >
</Modal> <Receive onClose={onReceiveHide} safeAddress={safeAddress} safeName={safeName} />
</Modal>
)}
</> </>
</SnackbarProvider> </SnackbarProvider>
<CookiesBanner /> <CookiesBanner />

View File

@ -50,7 +50,7 @@ export const Base = (): React.ReactElement => {
safeAddress="0xEE63624cC4Dd2355B16b35eFaadF3F7450A9438B" safeAddress="0xEE63624cC4Dd2355B16b35eFaadF3F7450A9438B"
safeName="someName" safeName="someName"
granted={true} granted={true}
balance={null} balance={undefined}
onToggleSafeList={() => console.log} onToggleSafeList={() => console.log}
onReceiveClick={() => console.log} onReceiveClick={() => console.log}
onNewTransactionClick={() => console.log} onNewTransactionClick={() => console.log}

View File

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

View File

@ -79,10 +79,10 @@ const UnStyledButton = styled.button`
` `
type Props = { type Props = {
address: string | null address: string | undefined
safeName: string safeName: string | undefined
granted: boolean granted: boolean
balance: string | null balance: string | undefined
onToggleSafeList: () => void onToggleSafeList: () => void
onReceiveClick: () => void onReceiveClick: () => void
onNewTransactionClick: () => void onNewTransactionClick: () => void

View File

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

View File

@ -60,9 +60,9 @@ export const FooterWrapper = styled.footer`
type Props = { type Props = {
sidebarItems: ListItemType[] sidebarItems: ListItemType[]
safeAddress: string | null safeAddress: string | undefined
safeName: string | null safeName: string | undefined
balance: string | null balance: string | undefined
granted: boolean granted: boolean
onToggleSafeList: () => void onToggleSafeList: () => void
onReceiveClick: () => void onReceiveClick: () => void

View File

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

View File

@ -8,7 +8,7 @@ interface CellWidth {
maxWidth: string maxWidth: string
} }
export const cellWidth = (width: string | number): CellWidth | undefined => { export const cellWidth = (width?: string | number): CellWidth | undefined => {
if (!width) { if (!width) {
return undefined return undefined
} }

View File

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

View File

@ -19,7 +19,7 @@ export const saveAddressBook = async (addressBook) => {
} }
} }
export const getAddressesListFromAdbk = (addressBook) => Array.from(addressBook).map((entry: any) => entry.address) export const getAddressesListFromAdbk = (addressBook: List<any>) => addressBook.map((entry: any) => entry.address)
export const getNameFromAdbk = (addressBook, userAddress) => { export const getNameFromAdbk = (addressBook, userAddress) => {
const entry = addressBook.find((addressBookItem) => addressBookItem.address === userAddress) const entry = addressBook.find((addressBookItem) => addressBookItem.address === userAddress)

View File

@ -2,14 +2,19 @@ import { AbiItem } from 'web3-utils'
import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3' 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 action: string
methodSignature: string methodSignature: string
signatureHash: string signatureHash: string
} }
export const getMethodSignature = ({ inputs, name }: AbiItem): 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})` return `${name}(${params})`
} }
@ -35,12 +40,17 @@ export const isAllowedMethod = ({ name, type }: AbiItem): boolean => {
} }
export const getMethodAction = ({ stateMutability }: AbiItem): 'read' | 'write' => { export const getMethodAction = ({ stateMutability }: AbiItem): 'read' | 'write' => {
if (!stateMutability) {
return 'write'
}
return ['view', 'pure'].includes(stateMutability) ? 'read' : 'write' return ['view', 'pure'].includes(stateMutability) ? 'read' : 'write'
} }
export const extractUsefulMethods = (abi: AbiItem[]): AbiItemExtended[] => { export const extractUsefulMethods = (abi: AbiItem[]): AbiItemExtended[] => {
return abi const allowedAbiItems = abi.filter(isAllowedMethod) as AllowedAbiItem[]
.filter(isAllowedMethod)
return allowedAbiItems
.map( .map(
(method): AbiItemExtended => ({ (method): AbiItemExtended => ({
action: getMethodAction(method), action: getMethodAction(method),
@ -48,9 +58,11 @@ export const extractUsefulMethods = (abi: AbiItem[]): AbiItemExtended[] => {
...method, ...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 => { export const isPayable = (method: AbiItem | AbiItemExtended): boolean => {
return method.payable return !!method?.payable
} }

View File

@ -10,9 +10,8 @@ import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3'
* @param {array<{ args: [any], method: string, type: 'eth'|undefined } | string>} args.methods - methods to be called * @param {array<{ args: [any], method: string, type: 'eth'|undefined } | string>} args.methods - methods to be called
* @returns {Promise<[*]>} * @returns {Promise<[*]>}
*/ */
const generateBatchRequests = ({ abi, address, batch, context, methods }: any): any => { const generateBatchRequests = ({ abi, address, batch = new web3.BatchRequest() , context, methods }: any): any => {
const contractInstance: any = new web3.eth.Contract(abi, address) const contractInstance: any = new web3.eth.Contract(abi, address)
const localBatch = batch ? null : new web3.BatchRequest()
const values = methods.map((methodObject) => { const values = methods.map((methodObject) => {
let method, type, args = [] let method, type, args = []
@ -39,14 +38,15 @@ const generateBatchRequests = ({ abi, address, batch, context, methods }: any):
} else { } else {
request = contractInstance.methods[method](...args).call.request(resolver) request = contractInstance.methods[method](...args).call.request(resolver)
} }
batch ? batch.add(request) : localBatch.add(request)
batch.add(request)
} catch (e) { } catch (e) {
resolve(null) resolve(null)
} }
}) })
}) })
localBatch && localBatch.execute() batch.execute()
const returnValues = context ? [context, ...values] : values const returnValues = context ? [context, ...values] : values

View File

@ -6,15 +6,12 @@ import { TokenProps } from 'src/logic/tokens/store/model/token'
export type BalanceEndpoint = { export type BalanceEndpoint = {
balance: string balance: string
balanceUsd: string balanceUsd: string
tokenAddress?: string tokenAddress: string
token?: TokenProps token?: TokenProps
usdConversion: string usdConversion: string
} }
const fetchTokenCurrenciesBalances = (safeAddress?: string): Promise<AxiosResponse<BalanceEndpoint[]>> => { const fetchTokenCurrenciesBalances = (safeAddress: string): Promise<AxiosResponse<BalanceEndpoint[]>> => {
if (!safeAddress) {
return null
}
const apiUrl = getTxServiceHost() const apiUrl = getTxServiceHost()
const url = `${apiUrl}safes/${safeAddress}/balances/usd/` const url = `${apiUrl}safes/${safeAddress}/balances/usd/`

View File

@ -12,7 +12,7 @@ export const fetchCurrencyValues = (safeAddress: string) => async (
dispatch: Dispatch<typeof setCurrencyBalances | typeof setSelectedCurrency | typeof setCurrencyRate>, dispatch: Dispatch<typeof setCurrencyBalances | typeof setSelectedCurrency | typeof setCurrencyRate>,
): Promise<void> => { ): Promise<void> => {
try { try {
const storedCurrencies: Map<string, CurrencyRateValue> | unknown = await loadCurrencyValues() const storedCurrencies = await loadCurrencyValues()
const storedCurrency = storedCurrencies[safeAddress] const storedCurrency = storedCurrencies[safeAddress]
if (!storedCurrency) { if (!storedCurrency) {
return batch(() => { return batch(() => {

View File

@ -1,18 +1,11 @@
import fetchCurrencyRate from 'src/logic/currencyValues/store/actions/fetchCurrencyRate' 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 { 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'
const watchedActions = [SET_CURRENT_CURRENCY, SET_CURRENCY_RATE, SET_CURRENCY_BALANCES] const watchedActions = [SET_CURRENT_CURRENCY]
const currencyValuesStorageMiddleware = (store) => (next) => async (action) => { const currencyValuesStorageMiddleware = (store) => (next) => async (action) => {
const handledAction = next(action) const handledAction = next(action)
if (watchedActions.includes(action.type)) { if (watchedActions.includes(action.type)) {
const state = store.getState()
const { dispatch } = store const { dispatch } = store
switch (action.type) { switch (action.type) {
case SET_CURRENT_CURRENCY: { case SET_CURRENT_CURRENCY: {
@ -20,22 +13,6 @@ const currencyValuesStorageMiddleware = (store) => (next) => async (action) => {
dispatch(fetchCurrencyRate(safeAddress, selectedCurrency)) dispatch(fetchCurrencyRate(safeAddress, selectedCurrency))
break break
} }
case SET_CURRENCY_RATE:
case SET_CURRENCY_BALANCES: {
const currencyValues = currencyValuesSelector(state)
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)
break
}
default: default:
break break

View File

@ -16,7 +16,7 @@ export const safeFiatBalancesSelector = createSelector(
currencyValuesSelector, currencyValuesSelector,
safeParamAddressFromStateSelector, safeParamAddressFromStateSelector,
(currencyValues, safeAddress): CurrencyReducerMap | undefined => { (currencyValues, safeAddress): CurrencyReducerMap | undefined => {
if (!currencyValues) return if (!currencyValues || !safeAddress) return
return currencyValues.get(safeAddress) return currencyValues.get(safeAddress)
}, },
) )

View File

@ -11,6 +11,6 @@ export const saveCurrencyValues = async (currencyValues: Map<string, CurrencyRat
} }
} }
export const loadCurrencyValues = async (): Promise<Map<string, CurrencyRateValue> | unknown> => { export const loadCurrencyValues = async (): Promise<Record<string, CurrencyRateValue>> => {
return (await loadFromStorage(CURRENCY_VALUES_STORAGE_KEY)) || {} return (await loadFromStorage(CURRENCY_VALUES_STORAGE_KEY)) || {}
} }

View File

@ -5,7 +5,9 @@ import { getCurrentSessionFromStorage } from 'src/logic/currentSession/utils'
const loadCurrentSessionFromStorage = () => async (dispatch) => { const loadCurrentSessionFromStorage = () => async (dispatch) => {
const currentSession = await getCurrentSessionFromStorage() const currentSession = await getCurrentSessionFromStorage()
dispatch(loadCurrentSession(makeCurrentSession(currentSession ? currentSession : {}))) if (currentSession) {
dispatch(loadCurrentSession(makeCurrentSession(currentSession)))
}
} }
export default loadCurrentSessionFromStorage export default loadCurrentSessionFromStorage

View File

@ -1,5 +1,9 @@
import { Record } from 'immutable' import { Record } from 'immutable'
export const makeCurrentSession = Record({ type SessionProps = {
viewedSafes: string[]
}
export const makeCurrentSession = Record<SessionProps>({
viewedSafes: [], viewedSafes: [],
}) })

View File

@ -7,6 +7,10 @@ import { saveCurrentSessionToStorage } from 'src/logic/currentSession/utils'
export const CURRENT_SESSION_REDUCER_ID = 'currentSession' export const CURRENT_SESSION_REDUCER_ID = 'currentSession'
export type SerializedSessionState = {
viewedSafes: string[]
}
export default handleActions( export default handleActions(
{ {
[LOAD_CURRENT_SESSION]: (state, action) => state.merge(Map(action.payload)), [LOAD_CURRENT_SESSION]: (state, action) => state.merge(Map(action.payload)),

View File

@ -1,8 +1,10 @@
import { loadFromStorage, saveToStorage } from 'src/utils/storage' import { loadFromStorage, saveToStorage } from 'src/utils/storage'
import { SerializedSessionState } from 'src/logic/currentSession/store/reducer/currentSession'
const CURRENT_SESSION_STORAGE_KEY = 'CURRENT_SESSION' const CURRENT_SESSION_STORAGE_KEY = 'CURRENT_SESSION'
export const getCurrentSessionFromStorage = async () => loadFromStorage(CURRENT_SESSION_STORAGE_KEY) export const getCurrentSessionFromStorage = async (): Promise<SerializedSessionState | undefined> =>
loadFromStorage(CURRENT_SESSION_STORAGE_KEY)
export const saveCurrentSessionToStorage = async (currentSession) => { export const saveCurrentSessionToStorage = async (currentSession) => {
try { try {

View File

@ -16,7 +16,7 @@ interface DebounceOptions {
export const useDebouncedCallback = <T extends (...args: unknown[]) => unknown>( export const useDebouncedCallback = <T extends (...args: unknown[]) => unknown>(
callback: T, callback: T,
delay = 0, delay = 0,
options: DebounceOptions, options?: DebounceOptions,
): T & { cancel: () => void } => useCallback(debounce(callback, delay, options), [callback, delay, options]) ): T & { cancel: () => void } => useCallback(debounce(callback, delay, options), [callback, delay, options])
export const useDebounce = <T extends unknown>(value: T, delay = 0, options?: DebounceOptions): T => { 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) const appInfo = getAppInfoFromOrigin(origin)
return { ...notification, message: `${appInfo.name}: ${notification.message}` } return { ...notification, message: `${appInfo ? appInfo.name : 'Unknown origin'}: ${notification.message}` }
} }
const getStandardTxNotificationsQueue = ( const getStandardTxNotificationsQueue = (

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 fetchSafeCreationTx from 'src/logic/safe/store/actions/fetchSafeCreationTx'
import { Dispatch } from 'src/logic/safe/store/actions/types.d' 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>() const dispatch = useDispatch<Dispatch>()
useEffect(() => { 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 fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions'
import { TIMEOUT } from 'src/utils/constants' import { TIMEOUT } from 'src/utils/constants'
export const useSafeScheduledUpdates = (safeAddress: string): void => { export const useSafeScheduledUpdates = (safeAddress?: string): void => {
const dispatch = useDispatch() const dispatch = useDispatch()
const timer = useRef<number>(null) const timer = useRef<number>()
useEffect(() => { useEffect(() => {
// using this variable to prevent setting a timeout when the component is already unmounted or the effect // 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) { if (mounted) {
timer.current = setTimeout(() => { timer.current = setTimeout(() => {
fetchSafeData(safeAddress) fetchSafeData(address)
}, TIMEOUT * 3) }, TIMEOUT * 3)
} }
} }

View File

@ -30,8 +30,8 @@ const getAllTransactionsUri = (safeAddress: string): string => {
const fetchAllTransactions = async ( const fetchAllTransactions = async (
urlParams: ServiceUriParams, urlParams: ServiceUriParams,
eTag: string | null, eTag?: string,
): Promise<{ responseEtag: string; results: Transaction[]; count?: number }> => { ): Promise<{ responseEtag?: string; results: Transaction[]; count?: number }> => {
const { safeAddress, limit, offset, orderBy, queued, trusted } = urlParams const { safeAddress, limit, offset, orderBy, queued, trusted } = urlParams
try { try {
const url = getAllTransactionsUri(safeAddress) const url = getAllTransactionsUri(safeAddress)

View File

@ -100,7 +100,7 @@ interface CreateTransactionArgs {
navigateToTransactionsTab?: boolean navigateToTransactionsTab?: boolean
notifiedTransaction: string notifiedTransaction: string
operation?: number operation?: number
origin?: string origin?: string | null
safeAddress: string safeAddress: string
to: string to: string
txData?: string txData?: string

View File

@ -18,7 +18,7 @@ import { Action, Dispatch } from 'redux'
import { SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts' import { SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts'
import { AppReduxState } from 'src/store' 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 ownersList = safeOwners.map((ownerAddress) => {
const convertedAdd = checksumAddress(ownerAddress) const convertedAdd = checksumAddress(ownerAddress)
@ -85,7 +85,7 @@ export const buildSafe = async (
needsUpdate, needsUpdate,
featuresEnabled, featuresEnabled,
balances: Map(), balances: Map(),
latestIncomingTxBlock: null, latestIncomingTxBlock: 0,
activeAssets: Set(), activeAssets: Set(),
activeTokens: Set(), activeTokens: Set(),
blacklistedAssets: 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 { 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 { buildSafe } from 'src/logic/safe/store/reducer/safe'
import { loadFromStorage } from 'src/utils/storage' import { loadFromStorage } from 'src/utils/storage'
import { Dispatch } from 'redux'
import { addSafe } from './addSafe'
const loadSafesFromStorage = () => async (dispatch: Dispatch): Promise<void> => { const loadSafesFromStorage = () => async (dispatch: Dispatch): Promise<void> => {
try { try {
const safes = await loadFromStorage(SAFES_KEY) const safes = await loadFromStorage<Record<string, SafeRecordProps>>(SAFES_KEY)
if (safes) { if (safes) {
Object.values(safes).forEach((safeProps) => { Object.values(safes).forEach((safeProps) => {

View File

@ -34,7 +34,7 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres
const safeInstance = await getGnosisSafeInstanceAt(safeAddress) const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
const lastTx = await getLastTx(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 isExecution = approveAndExecute || (await shouldExecuteTransaction(safeInstance, nonce, lastTx))
const safeVersion = await getCurrentSafeVersion(safeInstance) const safeVersion = await getCurrentSafeVersion(safeInstance)

View File

@ -27,7 +27,7 @@ async function fetchTransactions(
txType: TransactionTypes.INCOMING | TransactionTypes.OUTGOING, txType: TransactionTypes.INCOMING | TransactionTypes.OUTGOING,
safeAddress: string, safeAddress: string,
eTag: string | null, eTag: string | null,
): Promise<{ eTag: string; results: TxServiceModel[] | IncomingTxServiceModel[] }> { ): Promise<{ eTag: string | null; results: TxServiceModel[] | IncomingTxServiceModel[] }> {
try { try {
const url = getServiceUrl(txType, safeAddress) const url = getServiceUrl(txType, safeAddress)
const response = await axios.get(url, eTag ? { headers: { 'If-None-Match': eTag } } : undefined) 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 incomingTransactions = await loadIncomingTransactions(safeAddress)
const safeIncomingTxs = incomingTransactions.get(safeAddress)
if (incomingTransactions.get(safeAddress).size) { if (safeIncomingTxs?.size) {
dispatch(addIncomingTransactions(incomingTransactions)) dispatch(addIncomingTransactions(incomingTransactions))
} }
} catch (error) { } catch (error) {

View File

@ -68,8 +68,8 @@ const batchIncomingTxsTokenDataRequest = (txs: IncomingTxServiceModel[]) => {
) )
} }
let previousETag = null let previousETag: string | null = null
export const loadIncomingTransactions = async (safeAddress: string) => { export const loadIncomingTransactions = async (safeAddress: string): Promise<Map<string, List<any>>> => {
const { eTag, results } = await fetchTransactions(TransactionTypes.INCOMING, safeAddress, previousETag) const { eTag, results } = await fetchTransactions(TransactionTypes.INCOMING, safeAddress, previousETag)
previousETag = eTag previousETag = eTag

View File

@ -27,8 +27,7 @@ export type TxServiceModel = {
blockNumber?: number | null blockNumber?: number | null
confirmations: ConfirmationServiceModel[] confirmations: ConfirmationServiceModel[]
confirmationsRequired: number confirmationsRequired: number
creationTx?: boolean | null data: string | null
data?: string | null
dataDecoded?: DataDecoded dataDecoded?: DataDecoded
ethGasPrice: string ethGasPrice: string
executionDate?: string | null executionDate?: string | null
@ -40,15 +39,15 @@ export type TxServiceModel = {
isExecuted: boolean isExecuted: boolean
isSuccessful: boolean isSuccessful: boolean
modified: string modified: string
nonce?: number | null nonce: number
operation: number operation: number
origin?: string | null origin: string | null
refundReceiver: string refundReceiver: string
safe: string safe: string
safeTxGas: number safeTxGas: number
safeTxHash: string safeTxHash: string
signatures: string signatures: string
submissionDate?: string | null submissionDate: string | null
to: string to: string
transactionHash?: string | null transactionHash?: string | null
value: string value: string
@ -78,7 +77,7 @@ export type BatchProcessTxsProps = OutgoingTxs & {
*/ */
const extractCancelAndOutgoingTxs = (safeAddress: string, outgoingTxs: TxServiceModel[]): OutgoingTxs => { const extractCancelAndOutgoingTxs = (safeAddress: string, outgoingTxs: TxServiceModel[]): OutgoingTxs => {
return outgoingTxs.reduce( return outgoingTxs.reduce(
(acc, transaction) => { (acc: { cancellationTxs: Record<number, TxServiceModel>; outgoingTxs: TxServiceModel[] }, transaction) => {
if ( if (
isCancelTransaction(transaction, safeAddress) && isCancelTransaction(transaction, safeAddress) &&
outgoingTxs.find((tx) => tx.nonce === transaction.nonce && !isCancelTransaction(tx, safeAddress)) outgoingTxs.find((tx) => tx.nonce === transaction.nonce && !isCancelTransaction(tx, safeAddress))
@ -164,7 +163,7 @@ const batchProcessOutgoingTransactions = async ({
// outgoing transactions // outgoing transactions
const outgoingTxsWithData = outgoingTxs.length ? await batchRequestContractCode(outgoingTxs) : [] const outgoingTxsWithData = outgoingTxs.length ? await batchRequestContractCode(outgoingTxs) : []
const outgoing = [] const outgoing: Transaction[] = []
for (const [tx, txCode] of outgoingTxsWithData) { for (const [tx, txCode] of outgoingTxsWithData) {
outgoing.push( outgoing.push(
await buildTx({ await buildTx({
@ -182,7 +181,7 @@ const batchProcessOutgoingTransactions = async ({
return { cancel, outgoing } return { cancel, outgoing }
} }
let previousETag = null let previousETag: string | null = null
export const loadOutgoingTransactions = async (safeAddress: string): Promise<SafeTransactionsType> => { export const loadOutgoingTransactions = async (safeAddress: string): Promise<SafeTransactionsType> => {
const defaultResponse = { const defaultResponse = {
cancel: Map(), cancel: Map(),

View File

@ -19,6 +19,7 @@ import {
TransactionTypes, TransactionTypes,
TransactionTypeValues, TransactionTypeValues,
TxArgs, TxArgs,
RefundParams,
} from 'src/logic/safe/store/models/types/transaction' } from 'src/logic/safe/store/models/types/transaction'
import { CANCELLATION_TRANSACTIONS_REDUCER_ID } from 'src/logic/safe/store/reducer/cancellationTransactions' import { CANCELLATION_TRANSACTIONS_REDUCER_ID } from 'src/logic/safe/store/reducer/cancellationTransactions'
import { SAFE_REDUCER_ID } from 'src/logic/safe/store/reducer/safe' import { SAFE_REDUCER_ID } from 'src/logic/safe/store/reducer/safe'
@ -65,15 +66,15 @@ export const isModifySettingsTransaction = (tx: TxServiceModel, safeAddress: str
} }
export const isMultiSendTransaction = (tx: TxServiceModel): boolean => { 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 => { export const isUpgradeTransaction = (tx: TxServiceModel): boolean => {
return ( return (
!isEmptyData(tx.data) && !isEmptyData(tx.data) &&
isMultiSendTransaction(tx) && isMultiSendTransaction(tx) &&
tx.data.substr(308, 8) === '7de7edef' && // 7de7edef - changeMasterCopy (308, 8) tx.data?.substr(308, 8) === '7de7edef' && // 7de7edef - changeMasterCopy (308, 8)
tx.data.substr(550, 8) === 'f08a0323' // f08a0323 - setFallbackHandler (550, 8) tx.data?.substr(550, 8) === 'f08a0323' // f08a0323 - setFallbackHandler (550, 8)
) )
} }
@ -83,7 +84,7 @@ export const isOutgoingTransaction = (tx: TxServiceModel, safeAddress: string):
export const isCustomTransaction = async ( export const isCustomTransaction = async (
tx: TxServiceModel, tx: TxServiceModel,
txCode: string, txCode: string | null,
safeAddress: string, safeAddress: string,
knownTokens: Map<string, Token>, knownTokens: Map<string, Token>,
): Promise<boolean> => { ): Promise<boolean> => {
@ -98,9 +99,9 @@ export const isCustomTransaction = async (
export const getRefundParams = async ( export const getRefundParams = async (
tx: TxServiceModel, tx: TxServiceModel,
tokenInfo: (string) => Promise<{ decimals: number; symbol: string } | null>, tokenInfo: (string) => Promise<{ decimals: number; symbol: string } | null>,
): Promise<any> => { ): Promise<RefundParams | null> => {
const txGasPrice = Number(tx.gasPrice) const txGasPrice = Number(tx.gasPrice)
let refundParams = null let refundParams: RefundParams | null = null
if (txGasPrice > 0) { if (txGasPrice > 0) {
let refundSymbol = 'ETH' let refundSymbol = 'ETH'
@ -273,7 +274,6 @@ export const buildTx = async ({
blockNumber: tx.blockNumber, blockNumber: tx.blockNumber,
cancelled: isTxCancelled, cancelled: isTxCancelled,
confirmations, confirmations,
creationTx: tx.creationTx,
customTx: isCustomTx, customTx: isCustomTx,
data: tx.data ? tx.data : EMPTY_DATA, data: tx.data ? tx.data : EMPTY_DATA,
dataDecoded: tx.dataDecoded, dataDecoded: tx.dataDecoded,
@ -325,7 +325,7 @@ export const mockTransaction = (tx: TxToMock, safeAddress: string, state: AppRed
return buildTx({ return buildTx({
cancellationTxs, cancellationTxs,
currentUser: null, currentUser: undefined,
knownTokens, knownTokens,
outgoingTxs, outgoingTxs,
safe, safe,
@ -344,7 +344,7 @@ export const updateStoredTransactionsStatus = (dispatch: (any) => void, walletRe
dispatch( dispatch(
addOrUpdateTransactions({ addOrUpdateTransactions({
safeAddress, safeAddress,
transactions: transactions.withMutations((list) => transactions: transactions.withMutations((list: any[]) =>
list.map((tx) => tx.set('status', calculateTransactionStatus(tx, safe, walletRecord.account))), list.map((tx) => tx.set('status', calculateTransactionStatus(tx, safe, walletRecord.account))),
), ),
}), }),

View File

@ -4,7 +4,7 @@ import axios from 'axios'
import { buildTxServiceUrl } from 'src/logic/safe/transactions/txHistory' 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 { try {
const url = buildTxServiceUrl(safeAddress) const url = buildTxServiceUrl(safeAddress)
const response = await axios.get(url, { params: { limit: 1 } }) const response = await axios.get(url, { params: { limit: 1 } })
@ -17,23 +17,24 @@ export const getLastTx = async (safeAddress: string): Promise<TxServiceModel> =>
} }
export const getNewTxNonce = async ( export const getNewTxNonce = async (
txNonce: string | null, txNonce: string | undefined,
lastTx: TxServiceModel, lastTx: TxServiceModel | null,
safeInstance: GnosisSafe, safeInstance: GnosisSafe,
): Promise<string> => { ): Promise<string> => {
if (!Number.isInteger(Number.parseInt(txNonce, 10))) { if (typeof txNonce === 'string' && !Number.isInteger(Number.parseInt(txNonce, 10))) {
return lastTx === null return lastTx === null
? // use current's safe nonce as fallback ? // use current's safe nonce as fallback
(await safeInstance.methods.nonce().call()).toString() (await safeInstance.methods.nonce().call()).toString()
: `${lastTx.nonce + 1}` : `${lastTx.nonce + 1}`
} }
return txNonce
return txNonce as string
} }
export const shouldExecuteTransaction = async ( export const shouldExecuteTransaction = async (
safeInstance: GnosisSafe, safeInstance: GnosisSafe,
nonce: string, nonce: string,
lastTx: TxServiceModel, lastTx: TxServiceModel | null,
): Promise<boolean> => { ): Promise<boolean> => {
const threshold = await safeInstance.methods.getThreshold().call() const threshold = await safeInstance.methods.getThreshold().call()
@ -45,7 +46,7 @@ export const shouldExecuteTransaction = async (
// by the user using the exec button. // by the user using the exec button.
const canExecuteCurrentTransaction = lastTx && lastTx.isExecuted const canExecuteCurrentTransaction = lastTx && lastTx.isExecuted
return isFirstTransaction || canExecuteCurrentTransaction return isFirstTransaction || !!canExecuteCurrentTransaction
} }
return false return false

View File

@ -85,7 +85,7 @@ const notificationsMiddleware = (store) => (next) => async (action) => {
const safes = safesMapSelector(state) const safes = safesMapSelector(state)
const currentSafe = safes.get(safeAddress) const currentSafe = safes.get(safeAddress)
if (!isUserAnOwner(currentSafe, userAddress) || awaitingTransactions.size === 0) { if (!currentSafe || !isUserAnOwner(currentSafe, userAddress) || awaitingTransactions.size === 0) {
break break
} }

View File

@ -33,6 +33,7 @@ export enum PendingActionType {
REJECT = 'reject', REJECT = 'reject',
} }
export type PendingActionValues = PendingActionType[keyof PendingActionType] export type PendingActionValues = PendingActionType[keyof PendingActionType]
export type RefundParams = { fee: string; symbol: string }
export type TransactionProps = { export type TransactionProps = {
baseGas: number baseGas: number
@ -43,7 +44,7 @@ export type TransactionProps = {
creator: string creator: string
creationTx: boolean creationTx: boolean
customTx: boolean customTx: boolean
data?: string | null data: string | null
dataDecoded: DataDecoded | null dataDecoded: DataDecoded | null
decimals?: (number | string) | null decimals?: (number | string) | null
decodedParams: DecodedParams | null decodedParams: DecodedParams | null
@ -51,7 +52,7 @@ export type TransactionProps = {
executionTxHash?: string | null executionTxHash?: string | null
executor: string executor: string
factoryAddress: 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 gasPrice: string
gasToken: string gasToken: string
isCancellationTx: boolean isCancellationTx: boolean
@ -63,18 +64,18 @@ export type TransactionProps = {
masterCopy: string masterCopy: string
modifySettingsTx: boolean modifySettingsTx: boolean
multiSendTx: boolean multiSendTx: boolean
nonce?: number | null nonce: number
operation: number operation: number
origin: string | null origin: string | null
ownersWithPendingActions: Map<PendingActionValues, List<any>> ownersWithPendingActions: Map<PendingActionValues, List<any>>
recipient: string recipient: string
refundParams: any refundParams: RefundParams | null
refundReceiver: string refundReceiver: string
safeTxGas: number safeTxGas: number
safeTxHash: string safeTxHash: string
setupData: string setupData: string
status?: TransactionStatus status: TransactionStatus
submissionDate?: string | null submissionDate: string | null
symbol?: string | null symbol?: string | null
transactionHash: string | null transactionHash: string | null
transfers?: Transfer[] transfers?: Transfer[]
@ -87,7 +88,7 @@ export type Transaction = RecordOf<TransactionProps>
export type TxArgs = { export type TxArgs = {
baseGas: number baseGas: number
data?: string | null data: string
gasPrice: string gasPrice: string
gasToken: string gasToken: string
nonce: number nonce: number

View File

@ -37,7 +37,7 @@ export const buildSafe = (storedSafe: SafeRecordProps): SafeRecordProps => {
blacklistedTokens, blacklistedTokens,
activeAssets, activeAssets,
blacklistedAssets, blacklistedAssets,
latestIncomingTxBlock: null, latestIncomingTxBlock: 0,
modules: null, modules: null,
} }
} }

View File

@ -12,11 +12,11 @@ export const allTransactionsSelector = createSelector(getTransactionsStateSelect
export const safeAllTransactionsSelector = createSelector( export const safeAllTransactionsSelector = createSelector(
safeParamAddressFromStateSelector, safeParamAddressFromStateSelector,
allTransactionsSelector, allTransactionsSelector,
(safeAddress, transactions) => transactions[safeAddress]?.transactions || [], (safeAddress, transactions) => (safeAddress ? transactions[safeAddress]?.transactions : []),
) )
export const safeTotalTransactionsAmountSelector = createSelector( export const safeTotalTransactionsAmountSelector = createSelector(
safeParamAddressFromStateSelector, safeParamAddressFromStateSelector,
allTransactionsSelector, 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] const incomingTransactionsSelector = (state: AppReduxState) => state[INCOMING_TRANSACTIONS_REDUCER_ID]
export const safeParamAddressFromStateSelector = (state: AppReduxState): string | null => { export const safeParamAddressFromStateSelector = (state: AppReduxState): string | undefined => {
const match = matchPath<{ safeAddress: string }>(state.router.location.pathname, { const match = matchPath<{ safeAddress: string }>(state.router.location.pathname, {
path: `${SAFELIST_ADDRESS}/:safeAddress`, path: `${SAFELIST_ADDRESS}/:safeAddress`,
}) })
@ -45,7 +45,7 @@ export const safeParamAddressFromStateSelector = (state: AppReduxState): string
return checksumAddress(match.params.safeAddress) return checksumAddress(match.params.safeAddress)
} }
return null return undefined
} }
export const safeParamAddressSelector = ( export const safeParamAddressSelector = (
@ -177,16 +177,16 @@ export const safeBlacklistedAssetsSelector = createSelector(
) )
export const safeActiveAssetsSelectorBySafe = (safeAddress: string, safes: SafesMap): Set<string> => 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> => export const safeBlacklistedAssetsSelectorBySafe = (safeAddress: string, safes: SafesMap): Set<string> =>
safes.get(safeAddress).get('blacklistedAssets') safes.get(safeAddress)?.get('blacklistedAssets') || Set()
const baseSafe = makeSafe() const baseSafe = makeSafe()
export const safeFieldSelector = <K extends keyof SafeRecordProps>(field: K) => ( export const safeFieldSelector = <K extends keyof SafeRecordProps>(field: K) => (
safe: SafeRecord, 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')) export const safeNameSelector = createSelector(safeSelector, safeFieldSelector('name'))

View File

@ -1,8 +1,13 @@
import { List } from 'immutable' import { List } from 'immutable'
import { isPendingTransaction } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers' 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) => { return allTransactions.filter((tx) => {
const cancelTx = !!tx.nonce && !isNaN(Number(tx.nonce)) ? cancellationTxs.get(`${tx.nonce}`) : null 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 accumulator + 16
} }
return data.match(/.{2}/g).reduce(reducer, 0) return data.match(/.{2}/g)?.reduce(reducer, 0)
} }
export const estimateTxGasCosts = async ( export const estimateTxGasCosts = async (
@ -38,6 +38,11 @@ export const estimateTxGasCosts = async (
try { try {
const web3 = getWeb3() const web3 = getWeb3()
const from = await getAccountFrom(web3) 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 safeInstance = (new web3.eth.Contract(GnosisSafeSol.abi as AbiItem[], safeAddress) as unknown) as GnosisSafe
const nonce = await safeInstance.methods.nonce().call() const nonce = await safeInstance.methods.nonce().call()
const threshold = await safeInstance.methods.getThreshold().call() const threshold = await safeInstance.methods.getThreshold().call()

View File

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

View File

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

View File

@ -11,13 +11,15 @@ export const FEATURES = [
{ name: 'ERC1155', validVersion: '>=1.1.1' }, { 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) { if (!currentVersion || !latestVersion) {
return false return false
} }
const current = semverValid(currentVersion) const current = semverValid(currentVersion) as string
const latest = semverValid(latestVersion) const latest = semverValid(latestVersion) as string
return latest ? semverLessThan(current, latest) : false return latest ? semverLessThan(current, latest) : false
} }
@ -26,7 +28,7 @@ export const getCurrentSafeVersion = (gnosisSafeInstance: GnosisSafe): Promise<s
gnosisSafeInstance.methods.VERSION().call() gnosisSafeInstance.methods.VERSION().call()
export const enabledFeatures = (version: string): string[] => export const enabledFeatures = (version: string): string[] =>
FEATURES.reduce((acc, feature) => { FEATURES.reduce((acc: string[], feature: Feature) => {
if (semverSatisfies(version, feature.validVersion)) { if (semverSatisfies(version, feature.validVersion)) {
acc.push(feature.name) acc.push(feature.name)
} }
@ -44,11 +46,11 @@ export const checkIfSafeNeedsUpdate = async (
lastSafeVersion: string, lastSafeVersion: string,
): Promise<SafeVersionInfo> => { ): Promise<SafeVersionInfo> => {
if (!gnosisSafeInstance || !lastSafeVersion) { if (!gnosisSafeInstance || !lastSafeVersion) {
return null throw new Error('checkIfSafeNeedsUpdate: No Safe Instance or version provided')
} }
const safeMasterVersion = await getCurrentSafeVersion(gnosisSafeInstance) const safeMasterVersion = await getCurrentSafeVersion(gnosisSafeInstance)
const current = semverValid(safeMasterVersion) const current = semverValid(safeMasterVersion) as string
const latest = semverValid(lastSafeVersion) const latest = semverValid(lastSafeVersion) as string
const needUpdate = safeNeedsUpdate(safeMasterVersion, lastSafeVersion) const needUpdate = safeNeedsUpdate(safeMasterVersion, lastSafeVersion)
return { current, latest, needUpdate } return { current, latest, needUpdate }

View File

@ -48,7 +48,7 @@ const extractDataFromResult = (currentTokens: TokenState) => (
if (tokenAddress === null) { if (tokenAddress === null) {
acc.ethBalance = humanReadableValue(balance, 18) acc.ethBalance = humanReadableValue(balance, 18)
} else { } 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)) { if (currentTokens && !currentTokens.get(tokenAddress)) {
acc.tokens = acc.tokens.push(makeToken({ address: tokenAddress, ...token })) acc.tokens = acc.tokens.push(makeToken({ address: tokenAddress, ...token }))
@ -57,7 +57,7 @@ const extractDataFromResult = (currentTokens: TokenState) => (
acc.currencyList = acc.currencyList.push( acc.currencyList = acc.currencyList.push(
makeBalanceCurrency({ makeBalanceCurrency({
currencyName: balanceUsd ? AVAILABLE_CURRENCIES.USD : null, currencyName: balanceUsd ? AVAILABLE_CURRENCIES.USD : undefined,
tokenAddress, tokenAddress,
balanceInBaseCurrency: balanceUsd, balanceInBaseCurrency: balanceUsd,
balanceInSelectedCurrency: balanceUsd, balanceInSelectedCurrency: balanceUsd,

View File

@ -57,11 +57,7 @@ const getTokenValues = (tokenAddress) =>
methods: ['decimals', 'name', 'symbol'], methods: ['decimals', 'name', 'symbol'],
}) })
export const getTokenInfos = async (tokenAddress: string): Promise<Token> => { export const getTokenInfos = async (tokenAddress: string): Promise<Token | undefined> => {
if (!tokenAddress) {
return null
}
const { tokens } = store.getState() const { tokens } = store.getState()
const localToken = tokens.get(tokenAddress) const localToken = tokens.get(tokenAddress)
@ -74,7 +70,7 @@ export const getTokenInfos = async (tokenAddress: string): Promise<Token> => {
const [tokenDecimals, tokenName, tokenSymbol] = await getTokenValues(tokenAddress) const [tokenDecimals, tokenName, tokenSymbol] = await getTokenValues(tokenAddress)
if (tokenDecimals === null) { if (tokenDecimals === null) {
return null return undefined
} }
const token = makeToken({ const token = makeToken({

View File

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

View File

@ -35,18 +35,18 @@ export const isAddressAToken = async (tokenAddress: string): Promise<boolean> =>
// } catch { // } catch {
// return 'Not a token address' // 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' return call !== '0x'
} }
export const isTokenTransfer = (tx: TxServiceModel): boolean => { 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 = ( export const isSendERC721Transaction = (
tx: TxServiceModel, tx: TxServiceModel,
txCode: string, txCode: string | null,
knownTokens: Map<string, Token>, knownTokens: Map<string, Token>,
): boolean => { ): boolean => {
// "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85" - ens token contract, includes safeTransferFrom // "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85" - ens token contract, includes safeTransferFrom
@ -78,7 +78,7 @@ export const getERC20DecimalsAndSymbol = async (
try { try {
const storedTokenInfo = await getTokenInfos(tokenAddress) const storedTokenInfo = await getTokenInfos(tokenAddress)
if (storedTokenInfo === null) { if (!storedTokenInfo) {
const [tokenDecimals, tokenSymbol] = await generateBatchRequests({ const [tokenDecimals, tokenSymbol] = await generateBatchRequests({
abi: ALTERNATIVE_TOKEN_ABI, abi: ALTERNATIVE_TOKEN_ABI,
address: tokenAddress, address: tokenAddress,
@ -96,7 +96,7 @@ export const getERC20DecimalsAndSymbol = async (
export const isSendERC20Transaction = async ( export const isSendERC20Transaction = async (
tx: TxServiceModel, tx: TxServiceModel,
txCode: string, txCode: string | null,
knownTokens: Map<string, Token>, knownTokens: Map<string, Token>,
): Promise<boolean> => { ): Promise<boolean> => {
let isSendTokenTx = !isSendERC721Transaction(tx, txCode, knownTokens) && isTokenTransfer(tx) let isSendTokenTx = !isSendERC721Transaction(tx, txCode, knownTokens) && isTokenTransfer(tx)

View File

@ -2,7 +2,7 @@ import { List } from 'immutable'
import { SafeRecord } from 'src/logic/safe/store/models/safe' import { SafeRecord } from 'src/logic/safe/store/models/safe'
export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' 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) { if (!firstAddress) {
return false return false
} }

View File

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

View File

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

View File

@ -120,6 +120,8 @@ const DetailsForm = ({ errors, form }: DetailsFormProps): React.ReactElement =>
fieldMutator={(val) => { fieldMutator={(val) => {
form.mutators.setValue(FIELD_LOAD_ADDRESS, val) form.mutators.setValue(FIELD_LOAD_ADDRESS, val)
}} }}
// eslint-disable-next-line
// @ts-ignore
inputAdornment={ inputAdornment={
noErrorsOn(FIELD_LOAD_ADDRESS, errors) && { noErrorsOn(FIELD_LOAD_ADDRESS, errors) && {
endAdornment: ( endAdornment: (
@ -156,12 +158,15 @@ const DetailsForm = ({ errors, form }: DetailsFormProps): React.ReactElement =>
) )
} }
const DetailsPage = () => (controls: React.ReactNode, { errors, form }: StepperPageFormProps): React.ReactElement => ( const DetailsPage = () =>
<> function LoadSafeDetails(controls: React.ReactNode, { errors, form }: StepperPageFormProps): React.ReactElement {
<OpenPaper controls={controls}> return (
<DetailsForm errors={errors} form={form} /> <>
</OpenPaper> <OpenPaper controls={controls}>
</> <DetailsForm errors={errors} form={form} />
) </OpenPaper>
</>
)
}
export default DetailsPage export default DetailsPage

View File

@ -79,7 +79,7 @@ const calculateSafeValues = (owners, threshold, values) => {
} }
const OwnerListComponent = (props) => { const OwnerListComponent = (props) => {
const [owners, setOwners] = useState([]) const [owners, setOwners] = useState<string[]>([])
const { classes, updateInitialProps, values } = props const { classes, updateInitialProps, values } = props
useEffect(() => { useEffect(() => {
@ -156,12 +156,15 @@ const OwnerListComponent = (props) => {
const OwnerListPage = withStyles(styles as any)(OwnerListComponent) const OwnerListPage = withStyles(styles as any)(OwnerListComponent)
const OwnerList = ({ updateInitialProps }, network) => (controls, { values }) => ( const OwnerList = ({ updateInitialProps }, network) =>
<> function LoadSafeOwnerList(controls, { values }): React.ReactElement {
<OpenPaper controls={controls} padding={false}> return (
<OwnerListPage network={network} updateInitialProps={updateInitialProps} values={values} /> <>
</OpenPaper> <OpenPaper controls={controls} padding={false}>
</> <OwnerListPage network={network} updateInitialProps={updateInitialProps} values={values} />
) </OpenPaper>
</>
)
}
export default OwnerList 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 Page from 'src/components/layout/Page'
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' 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 { getNamesFrom, getOwnersFrom } from 'src/routes/open/utils/safeDataExtractor'
import { SAFELIST_ADDRESS } from 'src/routes/routes' import { SAFELIST_ADDRESS } from 'src/routes/routes'
import { buildSafe } from 'src/logic/safe/store/actions/fetchSafe' import { buildSafe } from 'src/logic/safe/store/actions/fetchSafe'
import { history } from 'src/store' import { history } from 'src/store'
import { loadFromStorage } from 'src/utils/storage'
import { SafeOwner, SafeRecordProps } from 'src/logic/safe/store/models/safe' import { SafeOwner, SafeRecordProps } from 'src/logic/safe/store/models/safe'
import { List } from 'immutable' import { List } from 'immutable'
import { checksumAddress } from 'src/utils/checksumAddress' import { checksumAddress } from 'src/utils/checksumAddress'
@ -27,7 +26,7 @@ export const loadSafe = async (
const safeProps = await buildSafe(safeAddress, safeName) const safeProps = await buildSafe(safeAddress, safeName)
safeProps.owners = owners safeProps.owners = owners
const storedSafes = (await loadFromStorage(SAFES_KEY)) || {} const storedSafes = (await loadStoredSafes()) || {}
storedSafes[safeAddress] = safeProps storedSafes[safeAddress] = safeProps

View File

@ -138,6 +138,8 @@ const SafeOwners = (props) => {
fieldMutator={(val) => { fieldMutator={(val) => {
form.mutators.setValue(addressName, val) form.mutators.setValue(addressName, val)
}} }}
// eslint-disable-next-line
// @ts-ignore
inputAdornment={ inputAdornment={
noErrorsOn(addressName, errors) && { noErrorsOn(addressName, errors) && {
endAdornment: ( endAdornment: (
@ -217,18 +219,21 @@ const SafeOwners = (props) => {
const SafeOwnersForm = withStyles(styles as any)(withRouter(SafeOwners)) const SafeOwnersForm = withStyles(styles as any)(withRouter(SafeOwners))
const SafeOwnersPage = ({ updateInitialProps }) => (controls, { errors, form, values }) => ( const SafeOwnersPage = ({ updateInitialProps }) =>
<> function OpenSafeOwnersPage(controls, { errors, form, values }) {
<OpenPaper controls={controls} padding={false}> return (
<SafeOwnersForm <>
errors={errors} <OpenPaper controls={controls} padding={false}>
form={form} <SafeOwnersForm
otherAccounts={getAccountsFrom(values)} errors={errors}
updateInitialProps={updateInitialProps} form={form}
values={values} otherAccounts={getAccountsFrom(values)}
/> updateInitialProps={updateInitialProps}
</OpenPaper> values={values}
</> />
) </OpenPaper>
</>
)
}
export default SafeOwnersPage export default SafeOwnersPage

View File

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

View File

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

View File

@ -50,7 +50,7 @@ const AddressBookTable = ({ classes }) => {
const safesList = useSelector(safesListSelector) const safesList = useSelector(safesListSelector)
const entryAddressToEditOrCreateNew = useSelector(addressBookQueryParamsSelector) const entryAddressToEditOrCreateNew = useSelector(addressBookQueryParamsSelector)
const addressBook = useSelector(getAddressBook) const addressBook = useSelector(getAddressBook)
const [selectedEntry, setSelectedEntry] = useState(null) const [selectedEntry, setSelectedEntry] = useState<any>(null)
const [editCreateEntryModalOpen, setEditCreateEntryModalOpen] = useState(false) const [editCreateEntryModalOpen, setEditCreateEntryModalOpen] = useState(false)
const [deleteEntryModalOpen, setDeleteEntryModalOpen] = useState(false) const [deleteEntryModalOpen, setDeleteEntryModalOpen] = useState(false)
const [sendFundsModalOpen, setSendFundsModalOpen] = useState(false) const [sendFundsModalOpen, setSendFundsModalOpen] = useState(false)
@ -70,7 +70,7 @@ const AddressBookTable = ({ classes }) => {
if (entryAddressToEditOrCreateNew) { if (entryAddressToEditOrCreateNew) {
const checksumEntryAdd = checksumAddress(entryAddressToEditOrCreateNew) const checksumEntryAdd = checksumAddress(entryAddressToEditOrCreateNew)
const key = addressBook.findKey((entry) => entry.address === checksumEntryAdd) const key = addressBook.findKey((entry) => entry.address === checksumEntryAdd)
if (key >= 0) { if (key && key >= 0) {
// Edit old entry // Edit old entry
const value = addressBook.get(key) const value = addressBook.get(key)
setSelectedEntry({ entry: value, index: key }) setSelectedEntry({ entry: value, index: key })

View File

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

View File

@ -14,7 +14,7 @@ const AppAgreement = (): React.ReactElement => {
const { visited } = useFormState({ subscription: { visited: true } }) const { visited } = useFormState({ subscription: { visited: true } })
// trick to prevent having the field validated by default. Not sure why this happens in this form // 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 ( return (
<Field <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 { const {
input: { value: appUrl }, input: { value: appUrl },
} = useField('appUrl', { subscription: { value: true } }) } = useField('appUrl', { subscription: { value: true } })
@ -52,7 +52,7 @@ const AppUrl = ({ appList }: { appList: SafeApp[] }): React.ReactElement => {
const { visited } = useFormState({ subscription: { visited: true } }) const { visited } = useFormState({ subscription: { visited: true } })
// trick to prevent having the field validated by default. Not sure why this happens in this form // 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 ( return (
<Field label="App URL" name="appUrl" placeholder="App URL" type="text" component={TextField} validate={validate} /> <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 onSubmitButtonStatusChange: (disabled: boolean) => void
} }
const SubmitButtonStatus = ({ appInfo, onSubmitButtonStatusChange }: SubmitButtonStatusProps): React.ReactElement => { const SubmitButtonStatus = ({ appInfo, onSubmitButtonStatusChange }: SubmitButtonStatusProps): null => {
const { valid, validating, visited } = useFormState({ const { valid, validating, visited } = useFormState({
subscription: { valid: true, validating: true, visited: true }, subscription: { valid: true, validating: true, visited: true },
}) })
React.useEffect(() => { React.useEffect(() => {
// if non visited, fields were not evaluated yet. Then, the default value is considered invalid // 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)) onSubmitButtonStatusChange(validating || !valid || !fieldsVisited || !isAppManifestValid(appInfo))
}, [validating, valid, visited, onSubmitButtonStatusChange, appInfo]) }, [validating, valid, visited, onSubmitButtonStatusChange, appInfo])

View File

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

View File

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

View File

@ -31,7 +31,7 @@ const isTxValid = (t: Transaction): boolean => {
} }
const isAddressValid = mustBeEthereumAddress(t.to) === undefined 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` const Wrapper = styled.div`
@ -84,7 +84,7 @@ const ConfirmTransactionModal = ({
onCancel, onCancel,
onUserConfirm, onUserConfirm,
onClose, onClose,
}: OwnProps): React.ReactElement => { }: OwnProps): React.ReactElement | null => {
const dispatch = useDispatch() const dispatch = useDispatch()
if (!isOpen) { if (!isOpen) {
return null return null

View File

@ -14,6 +14,8 @@ type Props = {
onAppRemoved: (appId: string) => void onAppRemoved: (appId: string) => void
} }
type AppListItem = SafeApp & { checked: boolean }
const ManageApps = ({ appList, onAppAdded, onAppToggle, onAppRemoved }: Props): React.ReactElement => { const ManageApps = ({ appList, onAppAdded, onAppToggle, onAppRemoved }: Props): React.ReactElement => {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [isSubmitDisabled, setIsSubmitDisabled] = useState(true) const [isSubmitDisabled, setIsSubmitDisabled] = useState(true)
@ -28,7 +30,7 @@ const ManageApps = ({ appList, onAppAdded, onAppToggle, onAppRemoved }: Props):
const closeModal = () => setIsOpen(false) const closeModal = () => setIsOpen(false)
const getItemList = () => const getItemList = (): AppListItem[] =>
appList.map((a) => { appList.map((a) => {
return { ...a, checked: !a.disabled } 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 // using the appURL to recover app info
for (let index = 0; index < list.length; index++) { for (let index = 0; index < list.length; index++) {
try { try {

View File

@ -44,7 +44,7 @@ const useIframeMessageHandler = (
selectedApp: SafeApp | undefined, selectedApp: SafeApp | undefined,
openConfirmationModal: (txs: Transaction[], requestId: RequestId) => void, openConfirmationModal: (txs: Transaction[], requestId: RequestId) => void,
closeModal: () => void, closeModal: () => void,
iframeRef: MutableRefObject<HTMLIFrameElement>, iframeRef: MutableRefObject<HTMLIFrameElement | null>,
): ReturnType => { ): ReturnType => {
const { enqueueSnackbar, closeSnackbar } = useSnackbar() const { enqueueSnackbar, closeSnackbar } = useSnackbar()
const safeName = useSelector(safeNameSelector) const safeName = useSelector(safeNameSelector)
@ -60,8 +60,8 @@ const useIframeMessageHandler = (
requestId: requestId || Math.trunc(window.performance.now()), requestId: requestId || Math.trunc(window.performance.now()),
} }
if (iframeRef?.current && selectedApp) { if (iframeRef && selectedApp) {
iframeRef.current.contentWindow.postMessage(requestWithMessage, selectedApp.url) iframeRef.current?.contentWindow?.postMessage(requestWithMessage, selectedApp.url)
} }
}, },
[iframeRef, selectedApp], [iframeRef, selectedApp],
@ -77,7 +77,9 @@ const useIframeMessageHandler = (
switch (msg.data.messageId) { switch (msg.data.messageId) {
case SDK_MESSAGES.SEND_TRANSACTIONS: { case SDK_MESSAGES.SEND_TRANSACTIONS: {
openConfirmationModal(msg.data.data, requestId) if (msg.data.data) {
openConfirmationModal(msg.data.data, requestId)
}
break break
} }
@ -85,9 +87,9 @@ const useIframeMessageHandler = (
const message = { const message = {
messageId: INTERFACE_MESSAGES.ON_SAFE_INFO, messageId: INTERFACE_MESSAGES.ON_SAFE_INFO,
data: { data: {
safeAddress, safeAddress: safeAddress as string,
network: network, network,
ethBalance, ethBalance: ethBalance as string,
}, },
} }
@ -104,7 +106,7 @@ const useIframeMessageHandler = (
if (message.origin === window.origin) { if (message.origin === window.origin) {
return 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}`) console.error(`ThirdPartyApp: A message was received from an unknown origin ${message.origin}`)
return return
} }

View File

@ -7,6 +7,7 @@ import styled, { css } from 'styled-components'
import ManageApps from './components/ManageApps' import ManageApps from './components/ManageApps'
import AppFrame from './components/AppFrame' import AppFrame from './components/AppFrame'
import { useAppList } from './hooks/useAppList' import { useAppList } from './hooks/useAppList'
import { SafeApp } from './types.d'
import LCL from 'src/components/ListContentLayout' import LCL from 'src/components/ListContentLayout'
import { networkSelector } from 'src/logic/wallets/store/selectors' import { networkSelector } from 'src/logic/wallets/store/selectors'
@ -63,7 +64,7 @@ const Apps = (): React.ReactElement => {
const [confirmTransactionModal, setConfirmTransactionModal] = useState<ConfirmTransactionModalState>( const [confirmTransactionModal, setConfirmTransactionModal] = useState<ConfirmTransactionModalState>(
INITIAL_CONFIRM_TX_MODAL_STATE, INITIAL_CONFIRM_TX_MODAL_STATE,
) )
const iframeRef = useRef<HTMLIFrameElement>() const iframeRef = useRef<HTMLIFrameElement>(null)
const { trackEvent } = useAnalytics() const { trackEvent } = useAnalytics()
const granted = useSelector(grantedSelector) const granted = useSelector(grantedSelector)
@ -146,14 +147,14 @@ const Apps = (): React.ReactElement => {
sendMessageToIframe({ sendMessageToIframe({
messageId: INTERFACE_MESSAGES.ON_SAFE_INFO, messageId: INTERFACE_MESSAGES.ON_SAFE_INFO,
data: { data: {
safeAddress, safeAddress: safeAddress as string,
network, network,
ethBalance, ethBalance: ethBalance as string,
}, },
}) })
}, [ethBalance, network, safeAddress, selectedApp, sendMessageToIframe]) }, [ethBalance, network, safeAddress, selectedApp, sendMessageToIframe])
if (loadingAppList || !appList.length) { if (loadingAppList || !appList.length || !safeAddress) {
return ( return (
<LoadingContainer> <LoadingContainer>
<Loader size="md" /> <Loader size="md" />
@ -199,10 +200,10 @@ const Apps = (): React.ReactElement => {
</CenteredMT> </CenteredMT>
<ConfirmTransactionModal <ConfirmTransactionModal
isOpen={confirmTransactionModal.isOpen} isOpen={confirmTransactionModal.isOpen}
app={selectedApp} app={selectedApp as SafeApp}
safeAddress={safeAddress} safeAddress={safeAddress}
ethBalance={ethBalance} ethBalance={ethBalance as string}
safeName={safeName} safeName={safeName as string}
txs={confirmTransactionModal.txs} txs={confirmTransactionModal.txs}
onCancel={closeConfirmationModal} onCancel={closeConfirmationModal}
onClose={closeConfirmationModal} onClose={closeConfirmationModal}

View File

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

View File

@ -1,7 +1,7 @@
import axios from 'axios' import axios from 'axios'
import memoize from 'lodash.memoize' import memoize from 'lodash.memoize'
import { SafeApp } from './types' import { SafeApp } from './types.d'
import { getGnosisSafeAppsUrl } from 'src/config/index' import { getGnosisSafeAppsUrl } from 'src/config/index'
import { getContentFromENS } from 'src/logic/wallets/getWeb3' import { getContentFromENS } from 'src/logic/wallets/getWeb3'
@ -62,8 +62,8 @@ export const isAppManifestValid = (appInfo: SafeApp): boolean =>
!appInfo.error !appInfo.error
export const getAppInfoFromUrl = memoize( export const getAppInfoFromUrl = memoize(
async (appUrl?: string): Promise<SafeApp> => { async (appUrl: string): Promise<SafeApp> => {
let res = { id: undefined, url: appUrl, name: 'unknown', iconUrl: appsIconSvg, error: true, description: '' } let res = { id: '', url: appUrl, name: 'unknown', iconUrl: appsIconSvg, error: true, description: '' }
if (!appUrl?.length) { if (!appUrl?.length) {
return res return res

View File

@ -79,7 +79,7 @@ const Collectibles = (): React.ReactElement => {
const classes = useStyles() const classes = useStyles()
const [selectedToken, setSelectedToken] = React.useState({}) const [selectedToken, setSelectedToken] = React.useState({})
const [sendNFTsModalOpen, setSendNFTsModalOpen] = React.useState(false) const [sendNFTsModalOpen, setSendNFTsModalOpen] = React.useState(false)
const { address, ethBalance, name } = useSelector(safeSelector) const { address, ethBalance, name } = useSelector(safeSelector) || {}
const nftTokens = useSelector(nftTokensSelector) const nftTokens = useSelector(nftTokensSelector)
const activeAssetsList = useSelector(activeNftAssetsListSelector) const activeAssetsList = useSelector(activeNftAssetsListSelector)
const { trackEvent } = useAnalytics() const { trackEvent } = useAnalytics()

View File

@ -5,7 +5,7 @@ import AddressInfo from 'src/components/AddressInfo'
import { safeSelector } from 'src/logic/safe/store/selectors' import { safeSelector } from 'src/logic/safe/store/selectors'
const SafeInfo = () => { const SafeInfo = () => {
const { address: safeAddress = '', ethBalance, name: safeName } = useSelector(safeSelector) const { address: safeAddress = '', ethBalance, name: safeName } = useSelector(safeSelector) || {}
return <AddressInfo ethBalance={ethBalance} safeAddress={safeAddress} safeName={safeName} /> return <AddressInfo ethBalance={ethBalance} safeAddress={safeAddress} safeName={safeName} />
} }

View File

@ -22,9 +22,9 @@ export interface AddressBookProps {
pristine: boolean pristine: boolean
recipientAddress?: string recipientAddress?: string
setSelectedEntry: ( setSelectedEntry: (
entry: { address?: string; name?: string } | React.SetStateAction<{ address: string; name: string }>, entry: { address?: string; name?: string } | React.SetStateAction<{ address: string; name: string }> | null,
) => void ) => void
setIsValidAddress: (valid?: boolean) => void setIsValidAddress: (valid: boolean) => void
} }
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
@ -157,7 +157,7 @@ const AddressBookInput = ({
optionsArray.filter((item) => { optionsArray.filter((item) => {
const inputLowerCase = inputValue.toLowerCase() const inputLowerCase = inputValue.toLowerCase()
const foundName = item.name.toLowerCase().includes(inputLowerCase) const foundName = item.name.toLowerCase().includes(inputLowerCase)
const foundAddress = item.address.toLowerCase().includes(inputLowerCase) const foundAddress = item.address?.toLowerCase().includes(inputLowerCase)
return foundName || foundAddress return foundName || foundAddress
}) })
} }
@ -212,6 +212,11 @@ const AddressBookInput = ({
)} )}
renderOption={(adbkEntry) => { renderOption={(adbkEntry) => {
const { address, name } = adbkEntry const { address, name } = adbkEntry
if (!address) {
return
}
return ( return (
<div className={classes.itemOptionList}> <div className={classes.itemOptionList}>
<div className={classes.identicon}> <div className={classes.identicon}>

View File

@ -62,8 +62,8 @@ const useStyles = makeStyles({
const ChooseTxType = ({ onClose, recipientAddress, setActiveScreen }) => { const ChooseTxType = ({ onClose, recipientAddress, setActiveScreen }) => {
const classes = useStyles() const classes = useStyles()
const { featuresEnabled } = useSelector(safeSelector) const { featuresEnabled } = useSelector(safeSelector) || {}
const erc721Enabled = featuresEnabled.includes('ERC721') const erc721Enabled = featuresEnabled?.includes('ERC721')
const [disableContractInteraction, setDisableContractInteraction] = React.useState(!!recipientAddress) const [disableContractInteraction, setDisableContractInteraction] = React.useState(!!recipientAddress)
React.useEffect(() => { React.useEffect(() => {

View File

@ -11,6 +11,7 @@ import {
mustBeEthereumAddress, mustBeEthereumAddress,
mustBeEthereumContractAddress, mustBeEthereumContractAddress,
required, required,
Validator,
} from 'src/components/forms/validator' } from 'src/components/forms/validator'
import Col from 'src/components/layout/Col' import Col from 'src/components/layout/Col'
import Row from 'src/components/layout/Row' import Row from 'src/components/layout/Row'
@ -34,8 +35,12 @@ const EthAddressInput = ({
text, text,
}: EthAddressInputProps): React.ReactElement => { }: EthAddressInputProps): React.ReactElement => {
const classes = useStyles() const classes = useStyles()
const validatorsList = [isRequired && required, mustBeEthereumAddress, isContract && mustBeEthereumContractAddress] const validatorsList = [
const validate = composeValidators(...validatorsList.filter((_) => _)) isRequired && required,
mustBeEthereumAddress,
isContract && mustBeEthereumContractAddress,
] as Validator[]
const validate = composeValidators(...validatorsList.filter((validator) => validator))
const { pristine } = useFormState({ subscription: { pristine: true } }) const { pristine } = useFormState({ subscription: { pristine: true } })
const { const {
input: { value }, input: { value },

View File

@ -20,14 +20,18 @@ const useStyles = makeStyles(styles)
interface EthValueProps { interface EthValueProps {
onSetMax: (ethBalance: string) => void onSetMax: (ethBalance: string) => void
} }
const EthValue = ({ onSetMax }: EthValueProps) => { const EthValue = ({ onSetMax }: EthValueProps): React.ReactElement | null => {
const classes = useStyles() const classes = useStyles()
const { ethBalance } = useSelector(safeSelector) const { ethBalance } = useSelector(safeSelector) || {}
const { const {
input: { value: method }, input: { value: method },
} = useField('selectedMethod', { subscription: { value: true } }) } = useField('selectedMethod', { subscription: { value: true } })
const disabled = !isPayable(method) const disabled = !isPayable(method)
if (!ethBalance) {
return null
}
return disabled ? null : ( return disabled ? null : (
<> <>
<Row className={classes.fullWidth} margin="xs"> <Row className={classes.fullWidth} margin="xs">

View File

@ -16,7 +16,7 @@ import { NO_CONTRACT } from 'src/routes/safe/components/Balances/SendModal/scree
import CheckIcon from 'src/routes/safe/components/CurrencyDropdown/img/check.svg' import CheckIcon from 'src/routes/safe/components/CurrencyDropdown/img/check.svg'
import { useDropdownStyles } from 'src/routes/safe/components/CurrencyDropdown/style' import { useDropdownStyles } from 'src/routes/safe/components/CurrencyDropdown/style'
import { DropdownListTheme } from 'src/theme/mui' import { DropdownListTheme } from 'src/theme/mui'
import { extractUsefulMethods } from 'src/logic/contractInteraction/sources/ABIService' import { extractUsefulMethods, AbiItemExtended } from 'src/logic/contractInteraction/sources/ABIService'
const MENU_WIDTH = '452px' const MENU_WIDTH = '452px'
@ -24,7 +24,7 @@ interface MethodsDropdownProps {
onChange: (method: AbiItem) => void onChange: (method: AbiItem) => void
} }
const MethodsDropdown = ({ onChange }: MethodsDropdownProps) => { const MethodsDropdown = ({ onChange }: MethodsDropdownProps): React.ReactElement | null => {
const classes = useDropdownStyles({ buttonWidth: MENU_WIDTH }) const classes = useDropdownStyles({ buttonWidth: MENU_WIDTH })
const { const {
input: { value: abi }, input: { value: abi },
@ -34,8 +34,8 @@ const MethodsDropdown = ({ onChange }: MethodsDropdownProps) => {
initialValues: { selectedMethod: selectedMethodByDefault }, initialValues: { selectedMethod: selectedMethodByDefault },
} = useFormState({ subscription: { initialValues: true } }) } = useFormState({ subscription: { initialValues: true } })
const [selectedMethod, setSelectedMethod] = React.useState(selectedMethodByDefault ? selectedMethodByDefault : {}) const [selectedMethod, setSelectedMethod] = React.useState(selectedMethodByDefault ? selectedMethodByDefault : {})
const [methodsList, setMethodsList] = React.useState([]) const [methodsList, setMethodsList] = React.useState<AbiItemExtended[]>([])
const [methodsListFiltered, setMethodsListFiltered] = React.useState([]) const [methodsListFiltered, setMethodsListFiltered] = React.useState<AbiItemExtended[]>([])
const [anchorEl, setAnchorEl] = React.useState(null) const [anchorEl, setAnchorEl] = React.useState(null)
const [searchParams, setSearchParams] = React.useState('') const [searchParams, setSearchParams] = React.useState('')
@ -50,7 +50,7 @@ const MethodsDropdown = ({ onChange }: MethodsDropdownProps) => {
}, [abi]) }, [abi])
React.useEffect(() => { React.useEffect(() => {
setMethodsListFiltered(methodsList.filter(({ name }) => name.toLowerCase().includes(searchParams.toLowerCase()))) setMethodsListFiltered(methodsList.filter(({ name }) => name?.toLowerCase().includes(searchParams.toLowerCase())))
}, [methodsList, searchParams]) }, [methodsList, searchParams])
const handleClick = (event) => { const handleClick = (event) => {

View File

@ -15,7 +15,7 @@ type Props = {
placeholder: string placeholder: string
} }
const InputComponent = ({ type, keyValue, placeholder }: Props): React.ReactElement => { const InputComponent = ({ type, keyValue, placeholder }: Props): React.ReactElement | null => {
if (!type) { if (!type) {
return null return null
} }

View File

@ -7,18 +7,18 @@ import InputComponent from './InputComponent'
import { generateFormFieldKey } from '../utils' import { generateFormFieldKey } from '../utils'
import { AbiItemExtended } from 'src/logic/contractInteraction/sources/ABIService' import { AbiItemExtended } from 'src/logic/contractInteraction/sources/ABIService'
const RenderInputParams = (): React.ReactElement => { const RenderInputParams = (): React.ReactElement | null => {
const { const {
meta: { valid: validABI }, meta: { valid: validABI },
} = useField('abi', { subscription: { valid: true, value: true } }) } = useField('abi', { subscription: { valid: true, value: true } })
const { const {
input: { value: method }, input: { value: method },
}: { input: { value: AbiItemExtended } } = useField('selectedMethod', { subscription: { value: true } }) }: { input: { value: AbiItemExtended } } = useField('selectedMethod', { subscription: { value: true } })
const renderInputs = validABI && !!method && method.inputs.length const renderInputs = validABI && !!method && method.inputs?.length
return !renderInputs ? null : ( return !renderInputs ? null : (
<> <>
{method.inputs.map(({ name, type }, index) => { {method.inputs?.map(({ name, type }, index) => {
const placeholder = name ? `${name} (${type})` : type const placeholder = name ? `${name} (${type})` : type
const key = generateFormFieldKey(type, method.signatureHash, index) const key = generateFormFieldKey(type, method.signatureHash, index)

View File

@ -40,11 +40,11 @@ type Props = {
tx: TransactionReviewType tx: TransactionReviewType
} }
const ContractInteractionReview = ({ onClose, onPrev, tx }: Props) => { const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactElement => {
const { enqueueSnackbar, closeSnackbar } = useSnackbar() const { enqueueSnackbar, closeSnackbar } = useSnackbar()
const classes = useStyles() const classes = useStyles()
const dispatch = useDispatch() const dispatch = useDispatch()
const { address: safeAddress } = useSelector(safeSelector) const { address: safeAddress } = useSelector(safeSelector) || {}
const [gasCosts, setGasCosts] = useState('< 0.001') const [gasCosts, setGasCosts] = useState('< 0.001')
useEffect(() => { useEffect(() => {
@ -54,7 +54,7 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props) => {
const { fromWei, toBN } = getWeb3().utils const { fromWei, toBN } = getWeb3().utils
const txData = tx.data ? tx.data.trim() : '' const txData = tx.data ? tx.data.trim() : ''
const estimatedGasCosts = await estimateTxGasCosts(safeAddress, tx.contractAddress, txData) const estimatedGasCosts = await estimateTxGasCosts(safeAddress as string, tx.contractAddress as string, txData)
const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether') const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether')
const formattedGasCosts = formatAmount(gasCostsAsEth) const formattedGasCosts = formatAmount(gasCostsAsEth)
@ -102,7 +102,7 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props) => {
</Paragraph> </Paragraph>
</Row> </Row>
<Row align="center" margin="md"> <Row align="center" margin="md">
<AddressInfo safeAddress={tx.contractAddress} /> <AddressInfo safeAddress={tx.contractAddress as string} />
</Row> </Row>
<Row margin="xs"> <Row margin="xs">
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}> <Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
@ -129,11 +129,11 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props) => {
</Row> </Row>
<Row align="center" margin="md"> <Row align="center" margin="md">
<Paragraph className={classes.value} size="md" style={{ margin: 0 }}> <Paragraph className={classes.value} size="md" style={{ margin: 0 }}>
{tx.selectedMethod.name} {tx.selectedMethod?.name}
</Paragraph> </Paragraph>
</Row> </Row>
{tx.selectedMethod.inputs.map(({ name, type }, index) => { {tx.selectedMethod?.inputs?.map(({ name, type }, index) => {
const key = generateFormFieldKey(type, tx.selectedMethod.signatureHash, index) const key = generateFormFieldKey(type, tx.selectedMethod?.signatureHash || '', index)
const value: string = getValueFromTxInputs(key, type, tx) const value: string = getValueFromTxInputs(key, type, tx)
return ( return (

View File

@ -1,7 +1,6 @@
import IconButton from '@material-ui/core/IconButton' import IconButton from '@material-ui/core/IconButton'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close' import Close from '@material-ui/icons/Close'
import { useSnackbar } from 'notistack'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
@ -39,10 +38,9 @@ type Props = {
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => { const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => {
const { enqueueSnackbar, closeSnackbar } = useSnackbar()
const classes = useStyles() const classes = useStyles()
const dispatch = useDispatch() const dispatch = useDispatch()
const { address: safeAddress } = useSelector(safeSelector) const { address: safeAddress } = useSelector(safeSelector) || {}
const [gasCosts, setGasCosts] = useState<string>('< 0.001') const [gasCosts, setGasCosts] = useState<string>('< 0.001')
useEffect(() => { useEffect(() => {
@ -52,7 +50,7 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => {
const { fromWei, toBN } = getWeb3().utils const { fromWei, toBN } = getWeb3().utils
const txData = tx.data ? tx.data.trim() : '' const txData = tx.data ? tx.data.trim() : ''
const estimatedGasCosts = await estimateTxGasCosts(safeAddress, tx.contractAddress, txData) const estimatedGasCosts = await estimateTxGasCosts(safeAddress as string, tx.contractAddress as string, txData)
const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether') const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether')
const formattedGasCosts = formatAmount(gasCostsAsEth) const formattedGasCosts = formatAmount(gasCostsAsEth)
@ -76,14 +74,12 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => {
dispatch( dispatch(
createTransaction({ createTransaction({
safeAddress, safeAddress: safeAddress as string,
to: txRecipient, to: txRecipient as string,
valueInWei: txValue, valueInWei: txValue,
txData, txData,
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
enqueueSnackbar, }),
closeSnackbar,
} as any),
) )
onClose() onClose()
@ -118,15 +114,15 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => {
</Row> </Row>
<Row align="center" margin="md"> <Row align="center" margin="md">
<Col xs={1}> <Col xs={1}>
<Identicon address={tx.contractAddress} diameter={32} /> <Identicon address={tx.contractAddress as string} diameter={32} />
</Col> </Col>
<Col layout="column" xs={11}> <Col layout="column" xs={11}>
<Block justify="left"> <Block justify="left">
<Paragraph noMargin weight="bolder"> <Paragraph noMargin weight="bolder">
{tx.contractAddress} {tx.contractAddress}
</Paragraph> </Paragraph>
<CopyBtn content={tx.contractAddress} /> <CopyBtn content={tx.contractAddress as string} />
<EtherscanBtn type="address" value={tx.contractAddress} /> <EtherscanBtn type="address" value={tx.contractAddress as string} />
</Block> </Block>
</Col> </Col>
</Row> </Row>

View File

@ -52,7 +52,7 @@ const useStyles = makeStyles(styles)
const SendCustomTx: React.FC<Props> = ({ initialValues, onClose, onNext, contractAddress, switchMethod, isABI }) => { const SendCustomTx: React.FC<Props> = ({ initialValues, onClose, onNext, contractAddress, switchMethod, isABI }) => {
const classes = useStyles() const classes = useStyles()
const { ethBalance } = useSelector(safeSelector) const { ethBalance } = useSelector(safeSelector) || {}
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false) const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string } | null>({ const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string } | null>({
address: contractAddress || initialValues.contractAddress, address: contractAddress || initialValues.contractAddress,
@ -230,7 +230,7 @@ const SendCustomTx: React.FC<Props> = ({ initialValues, onClose, onNext, contrac
placeholder="Value*" placeholder="Value*"
text="Value*" text="Value*"
type="text" type="text"
validate={composeValidators(mustBeFloat, maxValue(ethBalance), minValue(0))} validate={composeValidators(mustBeFloat, maxValue(ethBalance || '0'), minValue(0))}
/> />
</Col> </Col>
</Row> </Row>

View File

@ -49,7 +49,7 @@ const ContractInteraction: React.FC<ContractInteractionProps> = ({
isABI, isABI,
}) => { }) => {
const classes = useStyles() const classes = useStyles()
const { address: safeAddress = '' } = useSelector(safeSelector) const { address: safeAddress = '' } = useSelector(safeSelector) || {}
let setCallResults let setCallResults
React.useMemo(() => { React.useMemo(() => {

View File

@ -59,7 +59,7 @@ export const formMutators: Record<string, Mutator<{ selectedMethod: { name: stri
}, },
setSelectedMethod: (args, state, utils) => { setSelectedMethod: (args, state, utils) => {
const modified = const modified =
state.lastFormState.values.selectedMethod && state.lastFormState.values.selectedMethod.name !== args[0].name state.lastFormState?.values.selectedMethod && state.lastFormState.values.selectedMethod.name !== args[0].name
if (modified) { if (modified) {
utils.changeValue(state, 'callResults', () => '') utils.changeValue(state, 'callResults', () => '')
@ -115,8 +115,8 @@ export const createTxObject = (
): ContractSendMethod => { ): ContractSendMethod => {
const web3 = getWeb3() const web3 = getWeb3()
const contract: any = new web3.eth.Contract([method], contractAddress) const contract: any = new web3.eth.Contract([method], contractAddress)
const { inputs, name, signatureHash } = method const { inputs, name = '', signatureHash } = method
const args = inputs.map(extractMethodArgs(signatureHash, values)) const args = inputs?.map(extractMethodArgs(signatureHash, values)) || []
return contract.methods[name](...args) return contract.methods[name](...args)
} }

View File

@ -43,7 +43,7 @@ const ReviewCollectible = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx
const classes = useStyles() const classes = useStyles()
const shortener = textShortener() const shortener = textShortener()
const dispatch = useDispatch() const dispatch = useDispatch()
const { address: safeAddress } = useSelector(safeSelector) const { address: safeAddress } = useSelector(safeSelector) || {}
const nftTokens = useSelector(nftTokensSelector) const nftTokens = useSelector(nftTokensSelector)
const [gasCosts, setGasCosts] = useState('< 0.001') const [gasCosts, setGasCosts] = useState('< 0.001')
const txToken = nftTokens.find( const txToken = nftTokens.find(
@ -66,7 +66,7 @@ const ReviewCollectible = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx
const tokenInstance = await ERC721Token.at(tx.assetAddress) const tokenInstance = await ERC721Token.at(tx.assetAddress)
const txData = tokenInstance.contract.methods[methodToCall](...params).encodeABI() const txData = tokenInstance.contract.methods[methodToCall](...params).encodeABI()
const estimatedGasCosts = await estimateTxGasCosts(safeAddress, tx.recipientAddress, txData) const estimatedGasCosts = await estimateTxGasCosts(safeAddress as string, tx.recipientAddress, txData)
const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether') const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether')
const formattedGasCosts = formatAmount(gasCostsAsEth) const formattedGasCosts = formatAmount(gasCostsAsEth)
@ -148,7 +148,7 @@ const ReviewCollectible = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx
<Row align="center" margin="md"> <Row align="center" margin="md">
<Img alt={txToken.name} height={28} onError={setImageToPlaceholder} src={txToken.image} /> <Img alt={txToken.name} height={28} onError={setImageToPlaceholder} src={txToken.image} />
<Paragraph className={classes.amount} noMargin size="md"> <Paragraph className={classes.amount} noMargin size="md">
{shortener(txToken.name)} (Token ID: {shortener(txToken.tokenId)}) {shortener(txToken.name)} (Token ID: {shortener(txToken.tokenId as string)})
</Paragraph> </Paragraph>
</Row> </Row>
)} )}

View File

@ -3,7 +3,7 @@ import { makeStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close' import Close from '@material-ui/icons/Close'
import { BigNumber } from 'bignumber.js' import { BigNumber } from 'bignumber.js'
import { withSnackbar } from 'notistack' import { withSnackbar } from 'notistack'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState, useMemo } from 'react'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import ArrowDown from '../assets/arrow-down.svg' import ArrowDown from '../assets/arrow-down.svg'
@ -39,14 +39,14 @@ const useStyles = makeStyles(styles as any)
const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }) => { const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }) => {
const classes = useStyles() const classes = useStyles()
const dispatch = useDispatch() const dispatch = useDispatch()
const { address: safeAddress } = useSelector(safeSelector) const { address: safeAddress } = useSelector(safeSelector) || {}
const tokens = useSelector(extendedSafeTokensSelector) const tokens = useSelector(extendedSafeTokensSelector)
const [gasCosts, setGasCosts] = useState('< 0.001') const [gasCosts, setGasCosts] = useState('< 0.001')
const [data, setData] = useState('') const [data, setData] = useState('')
const txToken = tokens.find((token) => token.address === tx.token) const txToken = useMemo(() => tokens.find((token) => token.address === tx.token), [tokens, tx.token])
const isSendingETH = txToken.address === ETH_ADDRESS const isSendingETH = txToken?.address === ETH_ADDRESS
const txRecipient = isSendingETH ? tx.recipientAddress : txToken.address const txRecipient = isSendingETH ? tx.recipientAddress : txToken?.address
useEffect(() => { useEffect(() => {
let isCurrent = true let isCurrent = true
@ -54,18 +54,22 @@ const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }) => {
const estimateGas = async () => { const estimateGas = async () => {
const { fromWei, toBN } = getWeb3().utils const { fromWei, toBN } = getWeb3().utils
if (!txToken) {
return
}
let txData = EMPTY_DATA let txData = EMPTY_DATA
if (!isSendingETH) { if (!isSendingETH) {
const StandardToken = await getHumanFriendlyToken() const StandardToken = await getHumanFriendlyToken()
const tokenInstance = await StandardToken.at(txToken.address) const tokenInstance = await StandardToken.at(txToken.address as string)
const decimals = await tokenInstance.decimals() const decimals = await tokenInstance.decimals()
const txAmount = new BigNumber(tx.amount).times(10 ** decimals.toNumber()).toString() const txAmount = new BigNumber(tx.amount).times(10 ** decimals.toNumber()).toString()
txData = tokenInstance.contract.methods.transfer(tx.recipientAddress, txAmount).encodeABI() txData = tokenInstance.contract.methods.transfer(tx.recipientAddress, txAmount).encodeABI()
} }
const estimatedGasCosts = await estimateTxGasCosts(safeAddress, txRecipient, txData) const estimatedGasCosts = await estimateTxGasCosts(safeAddress as string, txRecipient, txData)
const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether') const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether')
const formattedGasCosts = formatAmount(gasCostsAsEth) const formattedGasCosts = formatAmount(gasCostsAsEth)
@ -80,7 +84,7 @@ const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }) => {
return () => { return () => {
isCurrent = false isCurrent = false
} }
}, [isSendingETH, safeAddress, tx.amount, tx.recipientAddress, txRecipient, txToken.address]) }, [isSendingETH, safeAddress, tx.amount, tx.recipientAddress, txRecipient, txToken])
const submitTx = async () => { const submitTx = async () => {
const web3 = getWeb3() const web3 = getWeb3()
@ -155,9 +159,14 @@ const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }) => {
</Paragraph> </Paragraph>
</Row> </Row>
<Row align="center" margin="md"> <Row align="center" margin="md">
<Img alt={txToken.name} height={28} onError={setImageToPlaceholder} src={txToken.logoUri} /> <Img alt={txToken?.name as string} height={28} onError={setImageToPlaceholder} src={txToken?.logoUri} />
<Paragraph className={classes.amount} noMargin size="md" data-testid={`amount-${txToken.symbol}-review-step`}> <Paragraph
{tx.amount} {txToken.symbol} className={classes.amount}
noMargin
size="md"
data-testid={`amount-${txToken?.symbol as string}-review-step`}
>
{tx.amount} {txToken?.symbol}
</Paragraph> </Paragraph>
</Row> </Row>
<Row> <Row>

View File

@ -53,7 +53,7 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
name: '', name: '',
}) })
const [pristine, setPristine] = useState(true) const [pristine, setPristine] = useState(true)
const [isValidAddress, setIsValidAddress] = useState(true) const [isValidAddress, setIsValidAddress] = useState(false)
React.useMemo(() => { React.useMemo(() => {
if (selectedEntry === null && pristine) { if (selectedEntry === null && pristine) {
@ -129,7 +129,7 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
<div <div
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.keyCode !== 9) { if (e.keyCode !== 9) {
setSelectedEntry(null) setSelectedEntry({ address: '', name: 'string' })
} }
}} }}
role="listbox" role="listbox"
@ -150,7 +150,7 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
<Paragraph <Paragraph
className={classes.selectAddress} className={classes.selectAddress}
noMargin noMargin
onClick={() => setSelectedEntry(null)} onClick={() => setSelectedEntry({ address: '', name: 'string' })}
weight="bolder" weight="bolder"
> >
{selectedEntry.name} {selectedEntry.name}
@ -158,7 +158,7 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
<Paragraph <Paragraph
className={classes.selectAddress} className={classes.selectAddress}
noMargin noMargin
onClick={() => setSelectedEntry(null)} onClick={() => setSelectedEntry({ address: '', name: 'string' })}
weight="bolder" weight="bolder"
> >
{selectedEntry.address} {selectedEntry.address}

View File

@ -58,7 +58,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
}) })
const [pristine, setPristine] = useState(true) const [pristine, setPristine] = useState(true)
const [isValidAddress, setIsValidAddress] = useState(true) const [isValidAddress, setIsValidAddress] = useState(false)
React.useMemo(() => { React.useMemo(() => {
if (selectedEntry === null && pristine) { if (selectedEntry === null && pristine) {
@ -130,7 +130,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
<div <div
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.keyCode !== 9) { if (e.keyCode !== 9) {
setSelectedEntry(null) setSelectedEntry({ address: '', name: 'string' })
} }
}} }}
role="listbox" role="listbox"
@ -151,7 +151,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
<Paragraph <Paragraph
className={classes.selectAddress} className={classes.selectAddress}
noMargin noMargin
onClick={() => setSelectedEntry(null)} onClick={() => setSelectedEntry({ address: '', name: 'string' })}
weight="bolder" weight="bolder"
> >
{selectedEntry.name} {selectedEntry.name}
@ -159,7 +159,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
<Paragraph <Paragraph
className={classes.selectAddress} className={classes.selectAddress}
noMargin noMargin
onClick={() => setSelectedEntry(null)} onClick={() => setSelectedEntry({ address: '', name: 'string' })}
weight="bolder" weight="bolder"
> >
{selectedEntry.address} {selectedEntry.address}
@ -204,7 +204,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
Amount Amount
</Paragraph> </Paragraph>
<ButtonLink <ButtonLink
onClick={() => mutators.setMax(selectedTokenRecord.balance)} onClick={() => mutators.setMax(selectedTokenRecord?.balance)}
weight="bold" weight="bold"
testId="send-max-btn" testId="send-max-btn"
> >
@ -230,7 +230,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
required, required,
mustBeFloat, mustBeFloat,
minValue(0, false), minValue(0, false),
maxValue(selectedTokenRecord?.balance), maxValue(selectedTokenRecord?.balance || 0),
)} )}
/> />
<OnChange name="token"> <OnChange name="token">

View File

@ -61,7 +61,7 @@ export const getBalanceData = (
symbol: token.symbol, symbol: token.symbol,
}, },
assetOrder: token.name, assetOrder: token.name,
[BALANCE_TABLE_BALANCE_ID]: `${formatAmountInUsFormat(token.balance.toString())} ${token.symbol}`, [BALANCE_TABLE_BALANCE_ID]: `${formatAmountInUsFormat(token.balance?.toString() || '0')} ${token.symbol}`,
balanceOrder: Number(token.balance), balanceOrder: Number(token.balance),
[FIXED]: token.symbol === 'ETH', [FIXED]: token.symbol === 'ETH',
[BALANCE_TABLE_VALUE_ID]: getTokenPriceInCurrency(token, currencySelected, currencyValues, currencyRate), [BALANCE_TABLE_VALUE_ID]: getTokenPriceInCurrency(token, currencySelected, currencyValues, currencyRate),

View File

@ -2,7 +2,7 @@ import { makeStyles } from '@material-ui/core/styles'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import Receive from 'src/components/App/ModalReceive' import Receive from 'src/components/App/ReceiveModal'
import Tokens from './Tokens' import Tokens from './Tokens'
import { styles } from './style' import { styles } from './style'
@ -15,7 +15,11 @@ import Row from 'src/components/layout/Row'
import { SAFELIST_ADDRESS } from 'src/routes/routes' import { SAFELIST_ADDRESS } from 'src/routes/routes'
import SendModal from 'src/routes/safe/components/Balances/SendModal' import SendModal from 'src/routes/safe/components/Balances/SendModal'
import CurrencyDropdown from 'src/routes/safe/components/CurrencyDropdown' import CurrencyDropdown from 'src/routes/safe/components/CurrencyDropdown'
import { safeFeaturesEnabledSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import {
safeFeaturesEnabledSelector,
safeParamAddressFromStateSelector,
safeNameSelector,
} from 'src/logic/safe/store/selectors'
import { wrapInSuspense } from 'src/utils/wrapInSuspense' import { wrapInSuspense } from 'src/utils/wrapInSuspense'
import { useFetchTokens } from 'src/logic/safe/hooks/useFetchTokens' import { useFetchTokens } from 'src/logic/safe/hooks/useFetchTokens'
@ -33,7 +37,7 @@ const INITIAL_STATE = {
showManageCollectibleModal: false, showManageCollectibleModal: false,
sendFunds: { sendFunds: {
isOpen: false, isOpen: false,
selectedToken: undefined, selectedToken: '',
}, },
showReceive: false, showReceive: false,
} }
@ -49,11 +53,12 @@ const Balances = (): React.ReactElement => {
const address = useSelector(safeParamAddressFromStateSelector) const address = useSelector(safeParamAddressFromStateSelector)
const featuresEnabled = useSelector(safeFeaturesEnabledSelector) const featuresEnabled = useSelector(safeFeaturesEnabledSelector)
const safeName = useSelector(safeNameSelector)
useFetchTokens(address) useFetchTokens(address as string)
useEffect(() => { useEffect(() => {
const erc721Enabled = featuresEnabled && featuresEnabled.includes('ERC721') const erc721Enabled = Boolean(featuresEnabled?.includes('ERC721'))
setState((prevState) => ({ setState((prevState) => ({
...prevState, ...prevState,
@ -84,7 +89,7 @@ const Balances = (): React.ReactElement => {
...prevState, ...prevState,
sendFunds: { sendFunds: {
isOpen: false, isOpen: false,
selectedToken: undefined, selectedToken: '',
}, },
})) }))
} }
@ -224,7 +229,7 @@ const Balances = (): React.ReactElement => {
paperClassName={receiveModal} paperClassName={receiveModal}
title="Receive Tokens" title="Receive Tokens"
> >
<Receive onClose={() => onHide('Receive')} /> <Receive safeAddress={address as string} safeName={safeName as string} onClose={() => onHide('Receive')} />
</Modal> </Modal>
</> </>
) )

View File

@ -22,7 +22,7 @@ import { setImageToPlaceholder } from '../Balances/utils'
import Img from 'src/components/layout/Img/index' import Img from 'src/components/layout/Img/index'
import etherIcon from 'src/assets/icons/icon_etherTokens.svg' import etherIcon from 'src/assets/icons/icon_etherTokens.svg'
const CurrencyDropdown = (): React.ReactElement => { const CurrencyDropdown = (): React.ReactElement | null => {
const currenciesList = Object.values(AVAILABLE_CURRENCIES) const currenciesList = Object.values(AVAILABLE_CURRENCIES)
const safeAddress = useSelector(safeParamAddressFromStateSelector) const safeAddress = useSelector(safeParamAddressFromStateSelector)
const dispatch = useDispatch() const dispatch = useDispatch()
@ -48,7 +48,11 @@ const CurrencyDropdown = (): React.ReactElement => {
handleClose() handleClose()
} }
return !selectedCurrency ? null : ( if (!selectedCurrency) {
return null
}
return (
<MuiThemeProvider theme={DropdownListTheme}> <MuiThemeProvider theme={DropdownListTheme}>
<> <>
<button className={classes.button} onClick={handleClick} type="button"> <button className={classes.button} onClick={handleClick} type="button">

View File

@ -50,7 +50,7 @@ const ModulesTable = ({ moduleData }: ModulesTableProps): React.ReactElement =>
const [viewRemoveModuleModal, setViewRemoveModuleModal] = React.useState(false) const [viewRemoveModuleModal, setViewRemoveModuleModal] = React.useState(false)
const hideRemoveModuleModal = () => setViewRemoveModuleModal(false) const hideRemoveModuleModal = () => setViewRemoveModuleModal(false)
const [selectedModule, setSelectedModule] = React.useState(null) const [selectedModule, setSelectedModule] = React.useState<ModulePair>()
const triggerRemoveSelectedModule = (module: ModulePair): void => { const triggerRemoveSelectedModule = (module: ModulePair): void => {
setSelectedModule(module) setSelectedModule(module)
setViewRemoveModuleModal(true) setViewRemoveModuleModal(true)
@ -67,7 +67,7 @@ const ModulesTable = ({ moduleData }: ModulesTableProps): React.ReactElement =>
disablePagination disablePagination
label="Modules" label="Modules"
noBorder noBorder
size={moduleData.length} size={moduleData?.length}
> >
{(sortedData) => {(sortedData) =>
sortedData.map((row, index) => ( sortedData.map((row, index) => (
@ -117,7 +117,9 @@ const ModulesTable = ({ moduleData }: ModulesTableProps): React.ReactElement =>
} }
</Table> </Table>
</TableContainer> </TableContainer>
{viewRemoveModuleModal && <RemoveModuleModal onClose={hideRemoveModuleModal} selectedModule={selectedModule} />} {viewRemoveModuleModal && selectedModule && (
<RemoveModuleModal onClose={hideRemoveModuleModal} selectedModule={selectedModule} />
)}
</> </>
) )
} }

View File

@ -46,7 +46,7 @@ interface RemoveModuleModal {
const RemoveModuleModal = ({ onClose, selectedModule }: RemoveModuleModal): React.ReactElement => { const RemoveModuleModal = ({ onClose, selectedModule }: RemoveModuleModal): React.ReactElement => {
const classes = useStyles() const classes = useStyles()
const safeAddress = useSelector(safeParamAddressFromStateSelector) const safeAddress = useSelector(safeParamAddressFromStateSelector) as string
const dispatch = useDispatch() const dispatch = useDispatch()
const removeSelectedModule = async (): Promise<void> => { const removeSelectedModule = async (): Promise<void> => {

View File

@ -42,7 +42,7 @@ const Advanced = (): React.ReactElement => {
const classes = useStyles() const classes = useStyles()
const nonce = useSelector(safeNonceSelector) const nonce = useSelector(safeNonceSelector)
const modules = useSelector(safeModulesSelector) const modules = useSelector(safeModulesSelector)
const moduleData = getModuleData(modules) ?? null const moduleData = modules ? getModuleData(modules) ?? null : null
const { trackEvent } = useAnalytics() const { trackEvent } = useAnalytics()
useEffect(() => { useEffect(() => {

View File

@ -35,7 +35,7 @@ const OwnerForm = ({ classes, onClose, onSubmit }) => {
onSubmit(values) onSubmit(values)
} }
const owners = useSelector(safeOwnersSelector) const owners = useSelector(safeOwnersSelector)
const ownerDoesntExist = uniqueAddress(owners.map((o) => o.address)) const ownerDoesntExist = uniqueAddress(owners?.map((o) => o.address) || [])
return ( return (
<> <>

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