Merge branch 'development' of github.com:gnosis/safe-react into feature/#512-network-switching
This commit is contained in:
commit
31180d974a
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "safe-react",
|
"name": "safe-react",
|
||||||
"version": "3.6.7",
|
"version": "3.7.0",
|
||||||
"description": "Allowing crypto users manage funds in a safer way",
|
"description": "Allowing crypto users manage funds in a safer way",
|
||||||
"website": "https://github.com/gnosis/safe-react#readme",
|
"website": "https://github.com/gnosis/safe-react#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
@ -161,7 +161,7 @@
|
||||||
"@gnosis.pm/safe-apps-sdk": "3.1.0-alpha.0",
|
"@gnosis.pm/safe-apps-sdk": "3.1.0-alpha.0",
|
||||||
"@gnosis.pm/safe-apps-sdk-v1": "npm:@gnosis.pm/safe-apps-sdk@0.4.2",
|
"@gnosis.pm/safe-apps-sdk-v1": "npm:@gnosis.pm/safe-apps-sdk@0.4.2",
|
||||||
"@gnosis.pm/safe-contracts": "1.1.1-dev.2",
|
"@gnosis.pm/safe-contracts": "1.1.1-dev.2",
|
||||||
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#4864ebb",
|
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#0e4fcd6",
|
||||||
"@gnosis.pm/util-contracts": "2.0.6",
|
"@gnosis.pm/util-contracts": "2.0.6",
|
||||||
"@ledgerhq/hw-transport-node-hid-singleton": "5.51.1",
|
"@ledgerhq/hw-transport-node-hid-singleton": "5.51.1",
|
||||||
"@material-ui/core": "^4.11.0",
|
"@material-ui/core": "^4.11.0",
|
||||||
|
@ -214,6 +214,7 @@
|
||||||
"react-ga": "3.3.0",
|
"react-ga": "3.3.0",
|
||||||
"react-hot-loader": "4.13.0",
|
"react-hot-loader": "4.13.0",
|
||||||
"react-intersection-observer": "^8.31.0",
|
"react-intersection-observer": "^8.31.0",
|
||||||
|
"react-papaparse": "^3.14.0",
|
||||||
"react-qr-reader": "^2.2.1",
|
"react-qr-reader": "^2.2.1",
|
||||||
"react-redux": "7.2.3",
|
"react-redux": "7.2.3",
|
||||||
"react-router-dom": "5.2.0",
|
"react-router-dom": "5.2.0",
|
||||||
|
@ -221,6 +222,7 @@
|
||||||
"react-window": "^1.8.6",
|
"react-window": "^1.8.6",
|
||||||
"redux": "4.0.5",
|
"redux": "4.0.5",
|
||||||
"redux-actions": "^2.6.5",
|
"redux-actions": "^2.6.5",
|
||||||
|
"redux-localstorage-simple": "^2.4.0",
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"reselect": "^4.0.0",
|
"reselect": "^4.0.0",
|
||||||
"semver": "^7.3.2",
|
"semver": "^7.3.2",
|
||||||
|
|
|
@ -20,15 +20,11 @@ import { getNetworkId } from 'src/config'
|
||||||
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
|
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
|
||||||
import { networkSelector } from 'src/logic/wallets/store/selectors'
|
import { networkSelector } from 'src/logic/wallets/store/selectors'
|
||||||
import { SAFELIST_ADDRESS, WELCOME_ADDRESS } from 'src/routes/routes'
|
import { SAFELIST_ADDRESS, WELCOME_ADDRESS } from 'src/routes/routes'
|
||||||
import {
|
import { safeTotalFiatBalanceSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||||
safeTotalFiatBalanceSelector,
|
|
||||||
safeNameSelector,
|
|
||||||
safeParamAddressFromStateSelector,
|
|
||||||
safeLoadedViaUrlSelector,
|
|
||||||
} from 'src/logic/safe/store/selectors'
|
|
||||||
import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors'
|
import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors'
|
||||||
import Modal from 'src/components/Modal'
|
import Modal from 'src/components/Modal'
|
||||||
import SendModal from 'src/routes/safe/components/Balances/SendModal'
|
import SendModal from 'src/routes/safe/components/Balances/SendModal'
|
||||||
|
import { useSafeName } from 'src/logic/addressBook/hooks/useSafeName'
|
||||||
import { useLoadSafe } from 'src/logic/safe/hooks/useLoadSafe'
|
import { useLoadSafe } from 'src/logic/safe/hooks/useLoadSafe'
|
||||||
import { useSafeScheduledUpdates } from 'src/logic/safe/hooks/useSafeScheduledUpdates'
|
import { useSafeScheduledUpdates } from 'src/logic/safe/hooks/useSafeScheduledUpdates'
|
||||||
import useSafeActions from 'src/logic/safe/hooks/useSafeActions'
|
import useSafeActions from 'src/logic/safe/hooks/useSafeActions'
|
||||||
|
@ -72,14 +68,13 @@ const App: React.FC = ({ children }) => {
|
||||||
const matchSafe = useRouteMatch({ path: `${SAFELIST_ADDRESS}`, strict: false })
|
const matchSafe = useRouteMatch({ path: `${SAFELIST_ADDRESS}`, strict: false })
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||||
const safeName = useSelector(safeNameSelector) ?? ''
|
const safeName = useSafeName(safeAddress)
|
||||||
const { safeActionsState, onShow, onHide, showSendFunds, hideSendFunds } = useSafeActions()
|
const { safeActionsState, onShow, onHide, showSendFunds, hideSendFunds } = useSafeActions()
|
||||||
const currentSafeBalance = useSelector(safeTotalFiatBalanceSelector)
|
const currentSafeBalance = useSelector(safeTotalFiatBalanceSelector)
|
||||||
const currentCurrency = useSelector(currentCurrencySelector)
|
const currentCurrency = useSelector(currentCurrencySelector)
|
||||||
const granted = useSelector(grantedSelector)
|
const granted = useSelector(grantedSelector)
|
||||||
const sidebarItems = useSidebarItems()
|
const sidebarItems = useSidebarItems()
|
||||||
const isSafeLoadedViaUrl = useSelector(safeLoadedViaUrlSelector)
|
const safeLoaded = useLoadSafe(safeAddress)
|
||||||
const safeLoaded = useLoadSafe(safeAddress, isSafeLoadedViaUrl)
|
|
||||||
useSafeScheduledUpdates(safeLoaded, safeAddress)
|
useSafeScheduledUpdates(safeLoaded, safeAddress)
|
||||||
|
|
||||||
const sendFunds = safeActionsState.sendFunds
|
const sendFunds = safeActionsState.sendFunds
|
||||||
|
|
|
@ -56,7 +56,7 @@ const useSidebarItems = (): ListItemType[] => {
|
||||||
href: `${matchSafeWithAddress?.url}/transactions`,
|
href: `${matchSafeWithAddress?.url}/transactions`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'AddressBook',
|
label: 'ADDRESS BOOK',
|
||||||
icon: <ListIcon type="addressBook" />,
|
icon: <ListIcon type="addressBook" />,
|
||||||
selected: matchSafeWithAction?.params.safeAction === 'address-book',
|
selected: matchSafeWithAction?.params.safeAction === 'address-book',
|
||||||
href: `${matchSafeWithAddress?.url}/address-book`,
|
href: `${matchSafeWithAddress?.url}/address-book`,
|
||||||
|
|
|
@ -9,7 +9,6 @@ import { COOKIES_KEY } from 'src/logic/cookies/model/cookie'
|
||||||
import { openCookieBanner } from 'src/logic/cookies/store/actions/openCookieBanner'
|
import { openCookieBanner } from 'src/logic/cookies/store/actions/openCookieBanner'
|
||||||
import { cookieBannerOpen } from 'src/logic/cookies/store/selectors'
|
import { cookieBannerOpen } from 'src/logic/cookies/store/selectors'
|
||||||
import { loadFromCookie, saveCookie } from 'src/logic/cookies/utils'
|
import { loadFromCookie, saveCookie } from 'src/logic/cookies/utils'
|
||||||
import { useSafeAppUrl } from 'src/logic/hooks/useSafeAppUrl'
|
|
||||||
import { mainFontFamily, md, primary, screenSm } from 'src/theme/variables'
|
import { mainFontFamily, md, primary, screenSm } from 'src/theme/variables'
|
||||||
import { loadGoogleAnalytics, removeCookies } from 'src/utils/googleAnalytics'
|
import { loadGoogleAnalytics, removeCookies } from 'src/utils/googleAnalytics'
|
||||||
import { closeIntercom, isIntercomLoaded, loadIntercom } from 'src/utils/intercom'
|
import { closeIntercom, isIntercomLoaded, loadIntercom } from 'src/utils/intercom'
|
||||||
|
@ -98,7 +97,7 @@ interface CookiesBannerFormProps {
|
||||||
const CookiesBanner = (): ReactElement => {
|
const CookiesBanner = (): ReactElement => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const dispatch = useRef(useDispatch())
|
const dispatch = useRef(useDispatch())
|
||||||
const { url: appUrl } = useSafeAppUrl()
|
|
||||||
const [showAnalytics, setShowAnalytics] = useState(false)
|
const [showAnalytics, setShowAnalytics] = useState(false)
|
||||||
const [showIntercom, setShowIntercom] = useState(false)
|
const [showIntercom, setShowIntercom] = useState(false)
|
||||||
const [localNecessary, setLocalNecessary] = useState(true)
|
const [localNecessary, setLocalNecessary] = useState(true)
|
||||||
|
@ -107,12 +106,6 @@ const CookiesBanner = (): ReactElement => {
|
||||||
|
|
||||||
const showBanner = useSelector(cookieBannerOpen)
|
const showBanner = useSelector(cookieBannerOpen)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (appUrl) {
|
|
||||||
setTimeout(closeIntercom, 50)
|
|
||||||
}
|
|
||||||
}, [appUrl])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchCookiesFromStorage() {
|
async function fetchCookiesFromStorage() {
|
||||||
const cookiesState = await loadFromCookie(COOKIES_KEY)
|
const cookiesState = await loadFromCookie(COOKIES_KEY)
|
||||||
|
@ -178,7 +171,7 @@ const CookiesBanner = (): ReactElement => {
|
||||||
dispatch.current(openCookieBanner({ cookieBannerOpen: false }))
|
dispatch.current(openCookieBanner({ cookieBannerOpen: false }))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showIntercom && !appUrl) {
|
if (showIntercom) {
|
||||||
loadIntercom()
|
loadIntercom()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
import React from 'react'
|
|
||||||
import styled from 'styled-components'
|
|
||||||
import { ButtonLink, EthHashInfo, Text } from '@gnosis.pm/safe-react-components'
|
import { ButtonLink, EthHashInfo, Text } from '@gnosis.pm/safe-react-components'
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import React, { ReactElement } from 'react'
|
||||||
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import { safeNameSelector } from 'src/logic/safe/store/selectors'
|
||||||
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
|
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
|
||||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||||
import DefaultBadge from './DefaultBadge'
|
import DefaultBadge from './DefaultBadge'
|
||||||
import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||||
import { DefaultSafe } from 'src/routes/safe/store/reducer/types/safe'
|
import { DefaultSafe } from 'src/routes/safe/store/reducer/types/safe'
|
||||||
import setDefaultSafe from 'src/logic/safe/store/actions/setDefaultSafe'
|
import setDefaultSafe from 'src/logic/safe/store/actions/setDefaultSafe'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
|
||||||
import { getNetworkInfo } from 'src/config'
|
import { getNetworkInfo } from 'src/config'
|
||||||
import { useDispatch } from 'react-redux'
|
|
||||||
|
|
||||||
const StyledButtonLink = styled(ButtonLink)`
|
const StyledButtonLink = styled(ButtonLink)`
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
|
@ -51,10 +53,10 @@ type Props = {
|
||||||
|
|
||||||
const { nativeCoin } = getNetworkInfo()
|
const { nativeCoin } = getNetworkInfo()
|
||||||
|
|
||||||
export const AddressWrapper = (props: Props): React.ReactElement => {
|
export const AddressWrapper = ({ safe, defaultSafe }: Props): ReactElement => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const { safe, defaultSafe } = props
|
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
|
const safeName = useSelector((state) => safeNameSelector(state, safe.address))
|
||||||
|
|
||||||
const setDefaultSafeAction = (safeAddress: string) => {
|
const setDefaultSafeAction = (safeAddress: string) => {
|
||||||
dispatch(setDefaultSafe(safeAddress))
|
dispatch(setDefaultSafe(safeAddress))
|
||||||
|
@ -62,7 +64,7 @@ export const AddressWrapper = (props: Props): React.ReactElement => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.wrapper}>
|
<div className={classes.wrapper}>
|
||||||
<EthHashInfo hash={safe.address} name={safe.name} showAvatar shortenHash={4} />
|
<EthHashInfo hash={safe.address} name={safeName} showAvatar shortenHash={4} />
|
||||||
|
|
||||||
<div className={classes.addressDetails}>
|
<div className={classes.addressDetails}>
|
||||||
<Text size="xl">{`${formatAmount(safe.ethBalance)} ${nativeCoin.name}`}</Text>
|
<Text size="xl">{`${formatAmount(safe.ethBalance)} ${nativeCoin.name}`}</Text>
|
||||||
|
|
|
@ -39,7 +39,7 @@ type Props = {
|
||||||
export const SafeListSidebar = ({ children }: Props): ReactElement => {
|
export const SafeListSidebar = ({ children }: Props): ReactElement => {
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const [filter, setFilter] = useState('')
|
const [filter, setFilter] = useState('')
|
||||||
const safes = useSelector(sortedSafeListSelector).filter((safe) => !safe.loadedViaUrl)
|
const safes = useSelector(sortedSafeListSelector)
|
||||||
const defaultSafe = useSelector(defaultSafeSelector)
|
const defaultSafe = useSelector(defaultSafeSelector)
|
||||||
const currentSafe = useSelector(safeParamAddressFromStateSelector)
|
const currentSafe = useSelector(safeParamAddressFromStateSelector)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import { createSelector } from 'reselect'
|
import { createSelector } from 'reselect'
|
||||||
|
|
||||||
import { safesListSelector } from 'src/logic/safe/store/selectors'
|
import { safesListWithAddressBookNameSelector } from 'src/logic/safe/store/selectors'
|
||||||
|
|
||||||
export const sortedSafeListSelector = createSelector(safesListSelector, (safes) =>
|
/**
|
||||||
safes.sort((a, b) => (a.name > b.name ? 1 : -1)),
|
* Sort safe list by the name in the address book
|
||||||
|
*/
|
||||||
|
export const sortedSafeListSelector = createSelector([safesListWithAddressBookNameSelector], (safes) =>
|
||||||
|
safes.sort((a, b) => (a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1)),
|
||||||
)
|
)
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
addressIsNotCurrentSafe,
|
addressIsNotCurrentSafe,
|
||||||
OWNER_ADDRESS_IS_SAFE_ADDRESS_ERROR,
|
OWNER_ADDRESS_IS_SAFE_ADDRESS_ERROR,
|
||||||
mustBeHexData,
|
mustBeHexData,
|
||||||
|
validAddressBookName,
|
||||||
} from 'src/components/forms/validator'
|
} from 'src/components/forms/validator'
|
||||||
|
|
||||||
describe('Forms > Validators', () => {
|
describe('Forms > Validators', () => {
|
||||||
|
@ -249,4 +250,26 @@ describe('Forms > Validators', () => {
|
||||||
expect(differentFrom('a')('a')).toEqual(getDifferentFromErrMsg('a'))
|
expect(differentFrom('a')('a')).toEqual(getDifferentFromErrMsg('a'))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('validAddressBookName validator', () => {
|
||||||
|
it('Returns error for an empty string', () => {
|
||||||
|
expect(validAddressBookName('')).toBe('Should be 1 to 50 symbols')
|
||||||
|
})
|
||||||
|
it('Returns error for a name longer than 50 chars', () => {
|
||||||
|
expect(validAddressBookName('abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabc')).toBe(
|
||||||
|
'Should be 1 to 50 symbols',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
it('Returns error for a blacklisted name', () => {
|
||||||
|
const blacklistedErrorMessage = 'Name should not include: UNKNOWN, OWNER #, MY WALLET'
|
||||||
|
|
||||||
|
expect(validAddressBookName('unknown')).toBe(blacklistedErrorMessage)
|
||||||
|
expect(validAddressBookName('unknown a')).toBe(blacklistedErrorMessage)
|
||||||
|
expect(validAddressBookName('owner #1')).toBe(blacklistedErrorMessage)
|
||||||
|
expect(validAddressBookName('My Wallet')).toBe(blacklistedErrorMessage)
|
||||||
|
})
|
||||||
|
it('Returns undefined for a non-blacklisted name', () => {
|
||||||
|
expect(validAddressBookName('A valid name')).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { List } from 'immutable'
|
|
||||||
import memoize from 'lodash.memoize'
|
import memoize from 'lodash.memoize'
|
||||||
|
|
||||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||||
import { isFeatureEnabled } from 'src/config'
|
import { isFeatureEnabled } from 'src/config'
|
||||||
import { FEATURES } from 'src/config/networks/network.d'
|
import { FEATURES } from 'src/config/networks/network.d'
|
||||||
|
import { isValidAddress } from 'src/utils/isValidAddress'
|
||||||
|
import { ADDRESS_BOOK_INVALID_NAMES, isValidAddressBookName } from 'src/logic/addressBook/utils'
|
||||||
|
|
||||||
type ValidatorReturnType = string | undefined
|
type ValidatorReturnType = string | undefined
|
||||||
export type GenericValidatorType = (...args: unknown[]) => ValidatorReturnType
|
export type GenericValidatorType = (...args: unknown[]) => ValidatorReturnType
|
||||||
|
@ -74,9 +75,7 @@ export const mustBeHexData = (data: string): ValidatorReturnType => {
|
||||||
export const mustBeAddressHash = memoize(
|
export const mustBeAddressHash = memoize(
|
||||||
(address: string): ValidatorReturnType => {
|
(address: string): ValidatorReturnType => {
|
||||||
const errorMessage = 'Must be a valid address'
|
const errorMessage = 'Must be a valid address'
|
||||||
const startsWith0x = address?.startsWith('0x')
|
return isValidAddress(address) ? undefined : errorMessage
|
||||||
const isAddress = getWeb3().utils.isAddress(address)
|
|
||||||
return startsWith0x && isAddress ? undefined : errorMessage
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -101,8 +100,12 @@ export const mustBeEthereumContractAddress = memoize(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
export const minMaxLength = (minLen: number, maxLen: number) => (value: string): ValidatorReturnType =>
|
export const minMaxLength = (minLen: number, maxLen: number) => (value: string): ValidatorReturnType => {
|
||||||
value.length >= +minLen && value.length <= +maxLen ? undefined : `Should be ${minLen} to ${maxLen} symbols`
|
const testValue = value || ''
|
||||||
|
return testValue.length >= +minLen && testValue.length <= +maxLen
|
||||||
|
? undefined
|
||||||
|
: `Should be ${minLen} to ${maxLen} symbols`
|
||||||
|
}
|
||||||
|
|
||||||
export const minMaxDecimalsLength = (minLen: number, maxLen: number) => (value: string): ValidatorReturnType => {
|
export const minMaxDecimalsLength = (minLen: number, maxLen: number) => (value: string): ValidatorReturnType => {
|
||||||
const decimals = value.split('.')[1] || '0'
|
const decimals = value.split('.')[1] || '0'
|
||||||
|
@ -113,7 +116,7 @@ export const minMaxDecimalsLength = (minLen: number, maxLen: number) => (value:
|
||||||
export const ADDRESS_REPEATED_ERROR = 'Address already introduced'
|
export const ADDRESS_REPEATED_ERROR = 'Address already introduced'
|
||||||
export const OWNER_ADDRESS_IS_SAFE_ADDRESS_ERROR = 'Cannot use Safe itself as owner.'
|
export const OWNER_ADDRESS_IS_SAFE_ADDRESS_ERROR = 'Cannot use Safe itself as owner.'
|
||||||
|
|
||||||
export const uniqueAddress = (addresses: string[] | List<string> = []) => (address?: string): string | undefined => {
|
export const uniqueAddress = (addresses: string[] = []) => (address?: string): string | undefined => {
|
||||||
const addressExists = addresses.some((addressFromList) => sameAddress(addressFromList, address))
|
const addressExists = addresses.some((addressFromList) => sameAddress(addressFromList, address))
|
||||||
return addressExists ? ADDRESS_REPEATED_ERROR : undefined
|
return addressExists ? ADDRESS_REPEATED_ERROR : undefined
|
||||||
}
|
}
|
||||||
|
@ -136,3 +139,15 @@ export const differentFrom = (diffValue: number | string) => (value: string): Va
|
||||||
}
|
}
|
||||||
|
|
||||||
export const noErrorsOn = (name: string, errors: Record<string, unknown>): boolean => errors[name] === undefined
|
export const noErrorsOn = (name: string, errors: Record<string, unknown>): boolean => errors[name] === undefined
|
||||||
|
|
||||||
|
export const validAddressBookName = (name: string): string | undefined => {
|
||||||
|
const lengthError = minMaxLength(1, 50)(name)
|
||||||
|
|
||||||
|
if (lengthError === undefined) {
|
||||||
|
return isValidAddressBookName(name)
|
||||||
|
? undefined
|
||||||
|
: `Name should not include: ${ADDRESS_BOOK_INVALID_NAMES.join(', ')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return lengthError
|
||||||
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ type Token = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ETHEREUM_NETWORK {
|
export enum ETHEREUM_NETWORK {
|
||||||
|
UNKNOWN = 0,
|
||||||
MAINNET = 1,
|
MAINNET = 1,
|
||||||
MORDEN = 2,
|
MORDEN = 2,
|
||||||
ROPSTEN = 3,
|
ROPSTEN = 3,
|
||||||
|
@ -42,9 +43,8 @@ export enum ETHEREUM_NETWORK {
|
||||||
KOVAN = 42,
|
KOVAN = 42,
|
||||||
XDAI = 100,
|
XDAI = 100,
|
||||||
ENERGY_WEB_CHAIN = 246,
|
ENERGY_WEB_CHAIN = 246,
|
||||||
VOLTA = 73799,
|
|
||||||
UNKNOWN = 0,
|
|
||||||
LOCAL = 4447,
|
LOCAL = 4447,
|
||||||
|
VOLTA = 73799,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NetworkSettings = {
|
export type NetworkSettings = {
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
|
import { getNameFromAddressBook } from 'src/logic/addressBook/utils'
|
||||||
|
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||||
|
|
||||||
|
export const useSafeName = (safeAddress: string): string => {
|
||||||
|
const addressBook = useSelector(addressBookSelector)
|
||||||
|
|
||||||
|
return getNameFromAddressBook(addressBook, safeAddress) || ''
|
||||||
|
}
|
|
@ -1,17 +1,28 @@
|
||||||
|
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
|
||||||
|
import { getNetworkId } from 'src/config'
|
||||||
|
|
||||||
|
export const ADDRESS_BOOK_DEFAULT_NAME = 'UNKNOWN'
|
||||||
|
|
||||||
export type AddressBookEntry = {
|
export type AddressBookEntry = {
|
||||||
address: string
|
address: string // the contact address
|
||||||
name: string
|
name: string // human-readable name
|
||||||
|
chainId: ETHEREUM_NETWORK // see https://chainid.network
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const networkId = getNetworkId()
|
||||||
|
|
||||||
export const makeAddressBookEntry = ({
|
export const makeAddressBookEntry = ({
|
||||||
address = '',
|
address,
|
||||||
name = '',
|
name,
|
||||||
|
chainId = networkId,
|
||||||
}: {
|
}: {
|
||||||
address: string
|
address: string
|
||||||
name?: string
|
name: string
|
||||||
|
chainId?: number
|
||||||
}): AddressBookEntry => ({
|
}): AddressBookEntry => ({
|
||||||
address,
|
address,
|
||||||
name,
|
name,
|
||||||
|
chainId,
|
||||||
})
|
})
|
||||||
|
|
||||||
export type AddressBookState = AddressBookEntry[]
|
export type AddressBookState = AddressBookEntry[]
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
import { createAction } from 'redux-actions'
|
|
||||||
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
|
||||||
|
|
||||||
export const ADD_ENTRY = 'ADD_ENTRY'
|
|
||||||
|
|
||||||
type addAddressBookEntryOptions = {
|
|
||||||
notifyEntryUpdate: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const addAddressBookEntry = createAction(
|
|
||||||
ADD_ENTRY,
|
|
||||||
(entry: AddressBookEntry, options?: addAddressBookEntryOptions) => {
|
|
||||||
let notifyEntryUpdate = true
|
|
||||||
if (options) {
|
|
||||||
notifyEntryUpdate = options.notifyEntryUpdate
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
entry,
|
|
||||||
shouldAvoidUpdatesNotifications: !notifyEntryUpdate,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { createAction } from 'redux-actions'
|
|
||||||
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
|
||||||
|
|
||||||
export const ADD_OR_UPDATE_ENTRY = 'ADD_OR_UPDATE_ENTRY'
|
|
||||||
|
|
||||||
export const addOrUpdateAddressBookEntry = createAction(ADD_OR_UPDATE_ENTRY, (entry: AddressBookEntry) => ({
|
|
||||||
entry,
|
|
||||||
}))
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { createAction } from 'redux-actions'
|
||||||
|
|
||||||
|
import { AddressBookEntry, AddressBookState } from 'src/logic/addressBook/model/addressBook'
|
||||||
|
|
||||||
|
// following the suggested naming convention at
|
||||||
|
// https://redux.js.org/style-guide/style-guide#write-action-types-as-domaineventname
|
||||||
|
export enum ADDRESS_BOOK_ACTIONS {
|
||||||
|
ADD_OR_UPDATE = 'addressBook/addOrUpdate',
|
||||||
|
REMOVE = 'addressBook/remove',
|
||||||
|
IMPORT = 'addressBook/import',
|
||||||
|
SAFE_LOAD = 'addressBook/safeLoad',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addressBookAddOrUpdate = createAction<AddressBookEntry>(ADDRESS_BOOK_ACTIONS.ADD_OR_UPDATE)
|
||||||
|
export const addressBookRemove = createAction<AddressBookEntry>(ADDRESS_BOOK_ACTIONS.REMOVE)
|
||||||
|
export const addressBookSafeLoad = createAction<AddressBookState>(ADDRESS_BOOK_ACTIONS.SAFE_LOAD)
|
||||||
|
export const addressBookImport = createAction<AddressBookState>(ADDRESS_BOOK_ACTIONS.IMPORT)
|
|
@ -1,8 +0,0 @@
|
||||||
import { createAction } from 'redux-actions'
|
|
||||||
import { AddressBookState } from 'src/logic/addressBook/model/addressBook'
|
|
||||||
|
|
||||||
export const LOAD_ADDRESS_BOOK = 'LOAD_ADDRESS_BOOK'
|
|
||||||
|
|
||||||
export const loadAddressBook = createAction(LOAD_ADDRESS_BOOK, (addressBook: AddressBookState) => ({
|
|
||||||
addressBook,
|
|
||||||
}))
|
|
|
@ -1,22 +0,0 @@
|
||||||
import { loadAddressBook } from 'src/logic/addressBook/store/actions/loadAddressBook'
|
|
||||||
import { buildAddressBook } from 'src/logic/addressBook/store/reducer/addressBook'
|
|
||||||
import { getAddressBookFromStorage } from 'src/logic/addressBook/utils'
|
|
||||||
import { Dispatch } from 'redux'
|
|
||||||
|
|
||||||
const loadAddressBookFromStorage = () => async (dispatch: Dispatch): Promise<void> => {
|
|
||||||
try {
|
|
||||||
let storedAdBk = await getAddressBookFromStorage()
|
|
||||||
if (!storedAdBk) {
|
|
||||||
storedAdBk = []
|
|
||||||
}
|
|
||||||
|
|
||||||
const addressBook = buildAddressBook(storedAdBk)
|
|
||||||
|
|
||||||
dispatch(loadAddressBook(addressBook))
|
|
||||||
} catch (err) {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
console.error('Error while loading active tokens from storage:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default loadAddressBookFromStorage
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { createAction } from 'redux-actions'
|
|
||||||
|
|
||||||
export const REMOVE_ENTRY = 'REMOVE_ENTRY'
|
|
||||||
|
|
||||||
export const removeAddressBookEntry = createAction(REMOVE_ENTRY, (entryAddress: string) => ({
|
|
||||||
entryAddress,
|
|
||||||
}))
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { createAction } from 'redux-actions'
|
|
||||||
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
|
||||||
|
|
||||||
export const UPDATE_ENTRY = 'UPDATE_ENTRY'
|
|
||||||
|
|
||||||
export const updateAddressBookEntry = createAction(UPDATE_ENTRY, (entry: AddressBookEntry) => ({
|
|
||||||
entry,
|
|
||||||
}))
|
|
|
@ -1,61 +0,0 @@
|
||||||
import { ADD_ENTRY } from 'src/logic/addressBook/store/actions/addAddressBookEntry'
|
|
||||||
import { ADD_OR_UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
|
|
||||||
import { REMOVE_ENTRY } from 'src/logic/addressBook/store/actions/removeAddressBookEntry'
|
|
||||||
import { UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
|
|
||||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
|
||||||
import { saveAddressBook } from 'src/logic/addressBook/utils'
|
|
||||||
import { enhanceSnackbarForAction, getNotificationsFromTxType } from 'src/logic/notifications'
|
|
||||||
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
|
|
||||||
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
|
||||||
import { safesListSelector } from 'src/logic/safe/store/selectors'
|
|
||||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
|
||||||
import { updateSafe } from 'src/logic/safe/store/actions/updateSafe'
|
|
||||||
|
|
||||||
const watchedActions = [ADD_ENTRY, REMOVE_ENTRY, UPDATE_ENTRY, ADD_OR_UPDATE_ENTRY]
|
|
||||||
|
|
||||||
const addressBookMiddleware = (store) => (next) => async (action) => {
|
|
||||||
const handledAction = next(action)
|
|
||||||
|
|
||||||
if (watchedActions.includes(action.type)) {
|
|
||||||
const state = store.getState()
|
|
||||||
const { dispatch } = store
|
|
||||||
const addressBook = addressBookSelector(state)
|
|
||||||
const safes = safesListSelector(state)
|
|
||||||
|
|
||||||
if (addressBook.length) {
|
|
||||||
await saveAddressBook(addressBook)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (action.type) {
|
|
||||||
case ADD_ENTRY: {
|
|
||||||
const { shouldAvoidUpdatesNotifications } = action.payload
|
|
||||||
if (!shouldAvoidUpdatesNotifications) {
|
|
||||||
const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.ADDRESSBOOK_NEW_ENTRY)
|
|
||||||
dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded)))
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case REMOVE_ENTRY: {
|
|
||||||
const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.ADDRESSBOOK_DELETE_ENTRY)
|
|
||||||
dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded)))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case UPDATE_ENTRY: {
|
|
||||||
const { entry } = action.payload
|
|
||||||
const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.ADDRESSBOOK_EDIT_ENTRY)
|
|
||||||
dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded)))
|
|
||||||
const safeFound = safes.find((safe) => sameAddress(safe.address, entry.address))
|
|
||||||
if (safeFound) {
|
|
||||||
dispatch(updateSafe({ address: safeFound.address, name: entry.name }))
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return handledAction
|
|
||||||
}
|
|
||||||
|
|
||||||
export default addressBookMiddleware
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { ADDRESS_BOOK_ACTIONS } from 'src/logic/addressBook/store/actions'
|
||||||
|
import { enhanceSnackbarForAction, getNotificationsFromTxType } from 'src/logic/notifications'
|
||||||
|
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
|
||||||
|
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||||
|
|
||||||
|
const watchedActions = Object.values(ADDRESS_BOOK_ACTIONS)
|
||||||
|
|
||||||
|
export const addressBookMiddleware = (store) => (next) => async (action) => {
|
||||||
|
const handledAction = next(action)
|
||||||
|
if (watchedActions.includes(action.type)) {
|
||||||
|
const { dispatch } = store
|
||||||
|
switch (action.type) {
|
||||||
|
case ADDRESS_BOOK_ACTIONS.ADD_OR_UPDATE: {
|
||||||
|
const { shouldAvoidUpdatesNotifications } = action.payload
|
||||||
|
if (!shouldAvoidUpdatesNotifications) {
|
||||||
|
const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.ADDRESS_BOOK_NEW_ENTRY)
|
||||||
|
dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded)))
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case ADDRESS_BOOK_ACTIONS.REMOVE: {
|
||||||
|
const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.ADDRESS_BOOK_DELETE_ENTRY)
|
||||||
|
dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded)))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case ADDRESS_BOOK_ACTIONS.IMPORT: {
|
||||||
|
const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.ADDRESS_BOOK_IMPORT_ENTRIES)
|
||||||
|
dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded)))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return handledAction
|
||||||
|
}
|
|
@ -1,78 +0,0 @@
|
||||||
import { Action, handleActions } from 'redux-actions'
|
|
||||||
|
|
||||||
import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
|
||||||
import { ADD_ENTRY } from 'src/logic/addressBook/store/actions/addAddressBookEntry'
|
|
||||||
import { ADD_OR_UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
|
|
||||||
import { LOAD_ADDRESS_BOOK } from 'src/logic/addressBook/store/actions/loadAddressBook'
|
|
||||||
import { REMOVE_ENTRY } from 'src/logic/addressBook/store/actions/removeAddressBookEntry'
|
|
||||||
import { UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
|
|
||||||
import { AppReduxState } from 'src/store'
|
|
||||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
|
||||||
import { getValidAddressBookName } from 'src/logic/addressBook/utils'
|
|
||||||
|
|
||||||
export const ADDRESS_BOOK_REDUCER_ID = 'addressBook'
|
|
||||||
|
|
||||||
export const buildAddressBook = (storedAddressBook: AddressBookState): AddressBookState => {
|
|
||||||
return storedAddressBook.map((addressBookEntry) => {
|
|
||||||
const { address, name } = addressBookEntry
|
|
||||||
return makeAddressBookEntry({ address: checksumAddress(address), name })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type AddressBookPayload = { addressBook: AddressBookState }
|
|
||||||
type EntryPayload = { entry: AddressBookEntry }
|
|
||||||
type RemoveEntryPayload = { entryAddress: string }
|
|
||||||
|
|
||||||
type Payloads = AddressBookPayload | EntryPayload | RemoveEntryPayload
|
|
||||||
|
|
||||||
export default handleActions<AppReduxState['addressBook'], Payloads>(
|
|
||||||
{
|
|
||||||
[LOAD_ADDRESS_BOOK]: (state, action: Action<AddressBookPayload>) => {
|
|
||||||
const { addressBook } = action.payload
|
|
||||||
return addressBook
|
|
||||||
},
|
|
||||||
[ADD_ENTRY]: (state, action: Action<EntryPayload>) => {
|
|
||||||
const { entry } = action.payload
|
|
||||||
|
|
||||||
const entryFound = state.find((oldEntry) => oldEntry.address === entry.address)
|
|
||||||
|
|
||||||
if (!entryFound) {
|
|
||||||
state.push(entry)
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
},
|
|
||||||
[UPDATE_ENTRY]: (state, action: Action<EntryPayload>) => {
|
|
||||||
const { entry } = action.payload
|
|
||||||
const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entry.address)
|
|
||||||
if (entryIndex >= 0) {
|
|
||||||
state[entryIndex] = entry
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
},
|
|
||||||
[REMOVE_ENTRY]: (state, action: Action<RemoveEntryPayload>) => {
|
|
||||||
const { entryAddress } = action.payload
|
|
||||||
const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entryAddress)
|
|
||||||
state.splice(entryIndex, 1)
|
|
||||||
return state
|
|
||||||
},
|
|
||||||
[ADD_OR_UPDATE_ENTRY]: (state, action: Action<EntryPayload>) => {
|
|
||||||
const { entry } = action.payload
|
|
||||||
|
|
||||||
// Only updates entries with valid names
|
|
||||||
const validName = getValidAddressBookName(entry.name)
|
|
||||||
if (!validName) {
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entry.address)
|
|
||||||
|
|
||||||
if (entryIndex >= 0) {
|
|
||||||
state[entryIndex] = entry
|
|
||||||
} else {
|
|
||||||
state.push(entry)
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
},
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
)
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { Action, handleActions } from 'redux-actions'
|
||||||
|
|
||||||
|
import { AddressBookEntry, AddressBookState } from 'src/logic/addressBook/model/addressBook'
|
||||||
|
import { ADDRESS_BOOK_ACTIONS } from 'src/logic/addressBook/store/actions'
|
||||||
|
import { getEntryIndex, isValidAddressBookName } from 'src/logic/addressBook/utils'
|
||||||
|
import { AppReduxState } from 'src/store'
|
||||||
|
|
||||||
|
export const ADDRESS_BOOK_REDUCER_ID = 'addressBook'
|
||||||
|
|
||||||
|
type Payloads = AddressBookEntry | AddressBookState
|
||||||
|
|
||||||
|
const batchLoadEntries = (state, action: Action<AddressBookState>): AddressBookState => {
|
||||||
|
const newState = [...state]
|
||||||
|
const addressBookEntries = action.payload
|
||||||
|
addressBookEntries
|
||||||
|
// exclude those entries with invalid name
|
||||||
|
.filter(({ name }) => isValidAddressBookName(name))
|
||||||
|
.forEach((addressBookEntry) => {
|
||||||
|
const entryIndex = getEntryIndex(newState, addressBookEntry)
|
||||||
|
|
||||||
|
if (entryIndex >= 0) {
|
||||||
|
// update
|
||||||
|
newState[entryIndex] = addressBookEntry
|
||||||
|
} else {
|
||||||
|
// add
|
||||||
|
newState.push(addressBookEntry)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return newState
|
||||||
|
}
|
||||||
|
export default handleActions<AppReduxState['addressBook'], Payloads>(
|
||||||
|
{
|
||||||
|
[ADDRESS_BOOK_ACTIONS.ADD_OR_UPDATE]: (state, action: Action<AddressBookEntry>) => {
|
||||||
|
const newState = [...state]
|
||||||
|
const addressBookEntry = action.payload
|
||||||
|
|
||||||
|
const entryIndex = getEntryIndex(newState, addressBookEntry)
|
||||||
|
|
||||||
|
// update
|
||||||
|
if (entryIndex >= 0) {
|
||||||
|
newState[entryIndex] = addressBookEntry
|
||||||
|
return newState
|
||||||
|
}
|
||||||
|
|
||||||
|
// add
|
||||||
|
return [...newState, addressBookEntry]
|
||||||
|
},
|
||||||
|
[ADDRESS_BOOK_ACTIONS.REMOVE]: (state, action: Action<AddressBookEntry>) => {
|
||||||
|
const newState = [...state]
|
||||||
|
const addressBookEntry = action.payload
|
||||||
|
|
||||||
|
const entryIndex = getEntryIndex(newState, addressBookEntry)
|
||||||
|
|
||||||
|
if (entryIndex >= 0) {
|
||||||
|
newState.splice(entryIndex, 1)
|
||||||
|
return newState
|
||||||
|
}
|
||||||
|
|
||||||
|
return newState
|
||||||
|
},
|
||||||
|
[ADDRESS_BOOK_ACTIONS.SAFE_LOAD]: batchLoadEntries,
|
||||||
|
[ADDRESS_BOOK_ACTIONS.IMPORT]: batchLoadEntries,
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
)
|
|
@ -1,27 +1,56 @@
|
||||||
import { AppReduxState } from 'src/store'
|
|
||||||
|
|
||||||
import { createSelector } from 'reselect'
|
import { createSelector } from 'reselect'
|
||||||
|
|
||||||
import { ADDRESS_BOOK_REDUCER_ID } from 'src/logic/addressBook/store/reducer/addressBook'
|
import { getNetworkId } from 'src/config'
|
||||||
|
import { ADDRESS_BOOK_DEFAULT_NAME, AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||||
|
import { AppReduxState } from 'src/store'
|
||||||
|
import { Overwrite } from 'src/types/helpers'
|
||||||
|
|
||||||
import { AddressBookState } from 'src/logic/addressBook/model/addressBook'
|
const networkId = getNetworkId()
|
||||||
|
|
||||||
export const addressBookSelector = (state: AppReduxState): AddressBookState => state[ADDRESS_BOOK_REDUCER_ID]
|
export const addressBookSelector = (state: AppReduxState): AppReduxState['addressBook'] => state['addressBook']
|
||||||
|
|
||||||
export const addressBookAddressesListSelector = (state: AppReduxState): string[] => {
|
type AddressBookMap = {
|
||||||
const addressBook = addressBookSelector(state)
|
[chainId: number]: {
|
||||||
return addressBook.map((entry) => entry.address)
|
[address: string]: AddressBookEntry
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getNameFromAddressBookSelector = createSelector(
|
export const addressBookMapSelector = createSelector(
|
||||||
addressBookSelector,
|
[addressBookSelector],
|
||||||
(_, address) => address,
|
(addressBook): AddressBookMap => {
|
||||||
(addressBook, address) => {
|
const addressBookMap = {}
|
||||||
const adbkEntry = addressBook.find((addressBookItem) => addressBookItem.address === address)
|
|
||||||
|
|
||||||
if (adbkEntry) {
|
addressBook.forEach((entry) => {
|
||||||
return adbkEntry.name
|
const { address, chainId } = entry
|
||||||
}
|
if (!addressBookMap[chainId]) {
|
||||||
return 'UNKNOWN'
|
addressBookMap[chainId] = { [address]: entry }
|
||||||
|
} else {
|
||||||
|
addressBookMap[chainId][address] = entry
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return addressBookMap
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const addressBookAddressesListSelector = createSelector([addressBookSelector], (addressBook): string[] =>
|
||||||
|
addressBook.map(({ address }) => address),
|
||||||
|
)
|
||||||
|
|
||||||
|
type GetNameParams = Overwrite<
|
||||||
|
AddressBookEntry,
|
||||||
|
{ chainId?: AddressBookEntry['chainId']; name?: AddressBookEntry['name'] }
|
||||||
|
>
|
||||||
|
|
||||||
|
type GetNameReturnObject = Overwrite<GetNameParams, { chainId: AddressBookEntry['chainId'] }>
|
||||||
|
|
||||||
|
export const getNameFromAddressBookSelector = createSelector(
|
||||||
|
[
|
||||||
|
addressBookMapSelector,
|
||||||
|
(_, { address, chainId = networkId }: GetNameParams): GetNameReturnObject => ({
|
||||||
|
address,
|
||||||
|
chainId,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
(addressBook, entry) => addressBook?.[entry.chainId]?.[entry.address]?.name ?? ADDRESS_BOOK_DEFAULT_NAME,
|
||||||
|
)
|
||||||
|
|
|
@ -1,16 +1,8 @@
|
||||||
import { List } from 'immutable'
|
|
||||||
import {
|
import {
|
||||||
checkIfEntryWasDeletedFromAddressBook,
|
checkIfEntryWasDeletedFromAddressBook,
|
||||||
getAddressBookFromStorage,
|
|
||||||
getNameFromAddressBook,
|
getNameFromAddressBook,
|
||||||
getOwnersWithNameFromAddressBook,
|
|
||||||
isValidAddressBookName,
|
isValidAddressBookName,
|
||||||
migrateOldAddressBook,
|
} from 'src/logic/addressBook/utils'
|
||||||
OldAddressBookEntry,
|
|
||||||
OldAddressBookType,
|
|
||||||
saveAddressBook,
|
|
||||||
} from 'src/logic/addressBook/utils/index'
|
|
||||||
import { buildAddressBook } from 'src/logic/addressBook/store/reducer/addressBook'
|
|
||||||
import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||||
|
|
||||||
const getMockAddressBookEntry = (address: string, name: string = 'test'): AddressBookEntry =>
|
const getMockAddressBookEntry = (address: string, name: string = 'test'): AddressBookEntry =>
|
||||||
|
@ -19,14 +11,6 @@ const getMockAddressBookEntry = (address: string, name: string = 'test'): Addres
|
||||||
name,
|
name,
|
||||||
})
|
})
|
||||||
|
|
||||||
const getMockOldAddressBookEntry = ({ address = '', name = '', isOwner = false }): OldAddressBookEntry => {
|
|
||||||
return {
|
|
||||||
address,
|
|
||||||
name,
|
|
||||||
isOwner,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('getNameFromSafeAddressBook', () => {
|
describe('getNameFromSafeAddressBook', () => {
|
||||||
const entry1 = getMockAddressBookEntry('123456', 'test1')
|
const entry1 = getMockAddressBookEntry('123456', 'test1')
|
||||||
const entry2 = getMockAddressBookEntry('78910', 'test2')
|
const entry2 = getMockAddressBookEntry('78910', 'test2')
|
||||||
|
@ -44,155 +28,6 @@ describe('getNameFromSafeAddressBook', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('getOwnersWithNameFromAddressBook', () => {
|
|
||||||
const entry1 = getMockAddressBookEntry('123456', 'test1')
|
|
||||||
const entry2 = getMockAddressBookEntry('78910', 'test2')
|
|
||||||
const entry3 = getMockAddressBookEntry('4781321', 'test3')
|
|
||||||
it('It should returns the list of owners with their names given a safeAddressBook and a list of owners', () => {
|
|
||||||
// given
|
|
||||||
const safeAddressBook = [entry1, entry2, entry3]
|
|
||||||
const ownerList = List([
|
|
||||||
{ address: entry1.address, name: '' },
|
|
||||||
{ address: entry2.address, name: '' },
|
|
||||||
])
|
|
||||||
const expectedResult = List([
|
|
||||||
{ address: entry1.address, name: entry1.name },
|
|
||||||
{ address: entry2.address, name: entry2.name },
|
|
||||||
])
|
|
||||||
|
|
||||||
// when
|
|
||||||
const result = getOwnersWithNameFromAddressBook(safeAddressBook, ownerList)
|
|
||||||
|
|
||||||
// then
|
|
||||||
expect(result).toStrictEqual(expectedResult)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
jest.mock('src/utils/storage/index')
|
|
||||||
describe('saveAddressBook', () => {
|
|
||||||
const mockAdd1 = '0x696fd93D725d84acfFf6c62a1fe8C94E1c9E934A'
|
|
||||||
const mockAdd2 = '0x2C7aC78b01Be0FC66AD29b684ffAb0C93B381D00'
|
|
||||||
const mockAdd3 = '0x537BD452c3505FC07bA242E437bD29D4E1DC9126'
|
|
||||||
const entry1 = getMockAddressBookEntry(mockAdd1, 'test1')
|
|
||||||
const entry2 = getMockAddressBookEntry(mockAdd2, 'test2')
|
|
||||||
const entry3 = getMockAddressBookEntry(mockAdd3, 'test3')
|
|
||||||
afterAll(() => {
|
|
||||||
jest.unmock('src/utils/storage/index')
|
|
||||||
})
|
|
||||||
it('It should save a given addressBook to the localStorage', async () => {
|
|
||||||
// given
|
|
||||||
const addressBook: AddressBookState = [entry1, entry2, entry3]
|
|
||||||
|
|
||||||
// when
|
|
||||||
await saveAddressBook(addressBook)
|
|
||||||
|
|
||||||
const storageUtils = require('src/utils/storage/index')
|
|
||||||
const spy = storageUtils.loadFromStorage.mockImplementationOnce(() => JSON.stringify(addressBook))
|
|
||||||
|
|
||||||
const storedAddressBook = await getAddressBookFromStorage()
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
let result = buildAddressBook(storedAddressBook)
|
|
||||||
|
|
||||||
// then
|
|
||||||
expect(result).toStrictEqual(addressBook)
|
|
||||||
expect(spy).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('migrateOldAddressBook', () => {
|
|
||||||
const safeAddress1 = '0x696fd93D725d84acfFf6c62a1fe8C94E1c9E934A'
|
|
||||||
const safeAddress2 = '0x2C7aC78b01Be0FC66AD29b684ffAb0C93B381D00'
|
|
||||||
const mockAdd1 = '0x9163c2F4452E3399CB60AAf737231Af87548DA91'
|
|
||||||
const mockAdd2 = '0xC4e446Da9C3D37385C86488294C6758c4e25dbD8'
|
|
||||||
|
|
||||||
it('It should receive an addressBook in old format and return the same addressBook in new format', () => {
|
|
||||||
// given
|
|
||||||
const entry1 = getMockOldAddressBookEntry({ name: 'test1', address: mockAdd1 })
|
|
||||||
const entry2 = getMockOldAddressBookEntry({ name: 'test2', address: mockAdd2 })
|
|
||||||
|
|
||||||
const oldAddressBook: OldAddressBookType = {
|
|
||||||
[safeAddress1]: [entry1],
|
|
||||||
[safeAddress2]: [entry2],
|
|
||||||
}
|
|
||||||
|
|
||||||
const expectedEntry1 = getMockAddressBookEntry(mockAdd1, 'test1')
|
|
||||||
const expectedEntry2 = getMockAddressBookEntry(mockAdd2, 'test2')
|
|
||||||
const expectedResult = [expectedEntry1, expectedEntry2]
|
|
||||||
|
|
||||||
// when
|
|
||||||
const result = migrateOldAddressBook(oldAddressBook)
|
|
||||||
|
|
||||||
// then
|
|
||||||
expect(result).toStrictEqual(expectedResult)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('getAddressBookFromStorage', () => {
|
|
||||||
const safeAddress1 = '0x696fd93D725d84acfFf6c62a1fe8C94E1c9E934A'
|
|
||||||
const safeAddress2 = '0x2C7aC78b01Be0FC66AD29b684ffAb0C93B381D00'
|
|
||||||
const mockAdd1 = '0x9163c2F4452E3399CB60AAf737231Af87548DA91'
|
|
||||||
const mockAdd2 = '0xC4e446Da9C3D37385C86488294C6758c4e25dbD8'
|
|
||||||
beforeAll(() => {
|
|
||||||
jest.mock('src/utils/storage/index')
|
|
||||||
})
|
|
||||||
afterAll(() => {
|
|
||||||
jest.unmock('src/utils/storage/index')
|
|
||||||
})
|
|
||||||
it('It should return null if no addressBook in storage', async () => {
|
|
||||||
// given
|
|
||||||
const expectedResult = null
|
|
||||||
const storageUtils = require('src/utils/storage/index')
|
|
||||||
const spy = storageUtils.loadFromStorage.mockImplementationOnce(() => null)
|
|
||||||
|
|
||||||
// when
|
|
||||||
const result = await getAddressBookFromStorage()
|
|
||||||
|
|
||||||
// then
|
|
||||||
expect(result).toStrictEqual(expectedResult)
|
|
||||||
expect(spy).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
it('It should return migrated addressBook if old addressBook in storage', async () => {
|
|
||||||
// given
|
|
||||||
const expectedEntry1 = getMockAddressBookEntry(mockAdd1, 'test1')
|
|
||||||
const expectedEntry2 = getMockAddressBookEntry(mockAdd2, 'test2')
|
|
||||||
const entry1 = getMockOldAddressBookEntry({ name: 'test1', address: mockAdd1 })
|
|
||||||
const entry2 = getMockOldAddressBookEntry({ name: 'test2', address: mockAdd2 })
|
|
||||||
const oldAddressBook: OldAddressBookType = {
|
|
||||||
[safeAddress1]: [entry1],
|
|
||||||
[safeAddress2]: [entry2],
|
|
||||||
}
|
|
||||||
const expectedResult = [expectedEntry1, expectedEntry2]
|
|
||||||
|
|
||||||
const storageUtils = require('src/utils/storage/index')
|
|
||||||
const spy = storageUtils.loadFromStorage.mockImplementationOnce(() => oldAddressBook)
|
|
||||||
|
|
||||||
// when
|
|
||||||
const result = await getAddressBookFromStorage()
|
|
||||||
|
|
||||||
// then
|
|
||||||
expect(result).toStrictEqual(expectedResult)
|
|
||||||
expect(spy).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
it('It should return addressBook if addressBook in storage', async () => {
|
|
||||||
// given
|
|
||||||
const expectedEntry1 = getMockAddressBookEntry(mockAdd1, 'test1')
|
|
||||||
const expectedEntry2 = getMockAddressBookEntry(mockAdd2, 'test2')
|
|
||||||
|
|
||||||
const expectedResult = [expectedEntry1, expectedEntry2]
|
|
||||||
|
|
||||||
const storageUtils = require('src/utils/storage/index')
|
|
||||||
const spy = storageUtils.loadFromStorage.mockImplementationOnce(() => JSON.stringify(expectedResult))
|
|
||||||
|
|
||||||
// when
|
|
||||||
const result = await getAddressBookFromStorage()
|
|
||||||
|
|
||||||
// then
|
|
||||||
expect(result).toStrictEqual(expectedResult)
|
|
||||||
expect(spy).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('isValidAddressBookName', () => {
|
describe('isValidAddressBookName', () => {
|
||||||
it('It should return false if given a blacklisted name like UNKNOWN', () => {
|
it('It should return false if given a blacklisted name like UNKNOWN', () => {
|
||||||
// given
|
// given
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import { List } from 'immutable'
|
|
||||||
import { mustBeEthereumContractAddress } from 'src/components/forms/validator'
|
import { mustBeEthereumContractAddress } from 'src/components/forms/validator'
|
||||||
import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
|
||||||
import { SafeOwner } from 'src/logic/safe/store/models/safe'
|
import { AddressBookEntry, AddressBookState } from 'src/logic/addressBook/model/addressBook'
|
||||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||||
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
|
import { AppReduxState } from 'src/store'
|
||||||
|
import { Overwrite } from 'src/types/helpers'
|
||||||
const ADDRESS_BOOK_STORAGE_KEY = 'ADDRESS_BOOK_STORAGE_KEY'
|
|
||||||
|
|
||||||
export type OldAddressBookEntry = {
|
export type OldAddressBookEntry = {
|
||||||
address: string
|
address: string
|
||||||
|
@ -17,44 +15,7 @@ export type OldAddressBookType = {
|
||||||
[safeAddress: string]: [OldAddressBookEntry]
|
[safeAddress: string]: [OldAddressBookEntry]
|
||||||
}
|
}
|
||||||
|
|
||||||
const ADDRESSBOOK_INVALID_NAMES = ['UNKNOWN', 'OWNER #', 'MY WALLET']
|
export const ADDRESS_BOOK_INVALID_NAMES = ['UNKNOWN', 'OWNER #', 'MY WALLET']
|
||||||
|
|
||||||
export const migrateOldAddressBook = (oldAddressBook: OldAddressBookType): AddressBookState => {
|
|
||||||
const values: AddressBookState = []
|
|
||||||
const adbkValues = Object.values(oldAddressBook)
|
|
||||||
|
|
||||||
for (const safeIterator of adbkValues) {
|
|
||||||
for (const safeAddressBook of safeIterator) {
|
|
||||||
if (!values.find((entry) => sameAddress(entry.address, safeAddressBook.address))) {
|
|
||||||
values.push(makeAddressBookEntry({ address: safeAddressBook.address, name: safeAddressBook.name }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getAddressBookFromStorage = async (): Promise<AddressBookState | null> => {
|
|
||||||
const result: OldAddressBookType | string | undefined = await loadFromStorage(ADDRESS_BOOK_STORAGE_KEY)
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof result === 'string') {
|
|
||||||
return JSON.parse(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
return migrateOldAddressBook(result as OldAddressBookType)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const saveAddressBook = async (addressBook: AddressBookState): Promise<void> => {
|
|
||||||
try {
|
|
||||||
await saveToStorage(ADDRESS_BOOK_STORAGE_KEY, JSON.stringify(addressBook))
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error storing addressBook in localstorage', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetNameFromAddressBookOptions = {
|
type GetNameFromAddressBookOptions = {
|
||||||
filterOnlyValidName: boolean
|
filterOnlyValidName: boolean
|
||||||
|
@ -73,32 +34,17 @@ export const getNameFromAddressBook = (
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isValidAddressBookName = (addressBookName: string): boolean => {
|
export const isValidAddressBookName = (addressBookName: string): boolean => {
|
||||||
const hasInvalidName = ADDRESSBOOK_INVALID_NAMES.find((invalidName) =>
|
const hasInvalidName = ADDRESS_BOOK_INVALID_NAMES.find((invalidName) =>
|
||||||
addressBookName.toUpperCase().includes(invalidName),
|
addressBookName?.toUpperCase().includes(invalidName),
|
||||||
)
|
)
|
||||||
return !hasInvalidName
|
return !hasInvalidName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: is this really required?
|
||||||
export const getValidAddressBookName = (addressBookName: string): string | null => {
|
export const getValidAddressBookName = (addressBookName: string): string | null => {
|
||||||
return isValidAddressBookName(addressBookName) ? addressBookName : null
|
return isValidAddressBookName(addressBookName) ? addressBookName : null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getOwnersWithNameFromAddressBook = (
|
|
||||||
addressBook: AddressBookState,
|
|
||||||
ownerList: List<SafeOwner>,
|
|
||||||
): List<SafeOwner> => {
|
|
||||||
if (!ownerList) {
|
|
||||||
return List([])
|
|
||||||
}
|
|
||||||
return ownerList.map((owner) => {
|
|
||||||
const ownerName = getNameFromAddressBook(addressBook, owner.address)
|
|
||||||
return {
|
|
||||||
address: owner.address,
|
|
||||||
name: ownerName || owner.name,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const formatAddressListToAddressBookNames = (
|
export const formatAddressListToAddressBookNames = (
|
||||||
addressBook: AddressBookState,
|
addressBook: AddressBookState,
|
||||||
addresses: string[],
|
addresses: string[],
|
||||||
|
@ -111,12 +57,13 @@ export const formatAddressListToAddressBookNames = (
|
||||||
return {
|
return {
|
||||||
address: address,
|
address: address,
|
||||||
name: ownerName || '',
|
name: ownerName || '',
|
||||||
|
chainId: ETHEREUM_NETWORK.UNKNOWN,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the safe is not loaded, the owner wasn't not deleted
|
* If the safe is not loaded, the owner wasn't deleted
|
||||||
* If the safe is already loaded and the owner has a valid name, will return true if the address is not already on the addressBook
|
* If the safe is already loaded and the owner has a valid name, will return true if the address is not already on the addressBook
|
||||||
* @param name
|
* @param name
|
||||||
* @param address
|
* @param address
|
||||||
|
@ -172,3 +119,11 @@ export const filterAddressEntries = (
|
||||||
|
|
||||||
return foundName || foundAddress
|
return foundName || foundAddress
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const getEntryIndex = (
|
||||||
|
state: AppReduxState['addressBook'],
|
||||||
|
addressBookEntry: Overwrite<AddressBookEntry, { name?: string }>,
|
||||||
|
): number =>
|
||||||
|
state.findIndex(
|
||||||
|
({ address, chainId }) => chainId === addressBookEntry.chainId && sameAddress(address, addressBookEntry.address),
|
||||||
|
)
|
||||||
|
|
|
@ -0,0 +1,133 @@
|
||||||
|
import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||||
|
import { saveSafes, StoredSafes } from 'src/logic/safe/utils'
|
||||||
|
import { removeFromStorage } from 'src/utils/storage'
|
||||||
|
import { getNetworkName } from 'src/config'
|
||||||
|
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||||
|
import { Errors, logError } from 'src/logic/exceptions/CodedException'
|
||||||
|
import { getEntryIndex, isValidAddressBookName } from '.'
|
||||||
|
|
||||||
|
interface StorageConfig {
|
||||||
|
states: string[]
|
||||||
|
namespace: string
|
||||||
|
namespaceSeparator: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrates the safes names from the Safe Object to the Address Book
|
||||||
|
*
|
||||||
|
* Works on the persistence layer, reads from the localStorage and stores back the mutated safe info through immortalDB.
|
||||||
|
*
|
||||||
|
* @note If the Safe name is an invalid AB name, it's renamed to "Migrated from: {safe.name}"
|
||||||
|
*/
|
||||||
|
const migrateSafeNames = ({ states, namespace, namespaceSeparator }: StorageConfig): void => {
|
||||||
|
const prefix = `v2_${getNetworkName()}`
|
||||||
|
const safesKey = `_immortal|${prefix}__SAFES`
|
||||||
|
const storedSafes = localStorage.getItem(safesKey)
|
||||||
|
|
||||||
|
if (!storedSafes) {
|
||||||
|
// nothing left to migrate
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedStoredSafes = JSON.parse(storedSafes) as Record<string, any>
|
||||||
|
|
||||||
|
if (Object.entries(parsedStoredSafes).every(([, { name }]) => name === undefined)) {
|
||||||
|
// no name key, safes already migrated
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// make address book entries from the safe names & addresses
|
||||||
|
const safeAbEntries: AddressBookState = Object.values(parsedStoredSafes)
|
||||||
|
.filter(({ name }) => name && isValidAddressBookName(name))
|
||||||
|
.map(({ address, name }) => makeAddressBookEntry({ address, name }))
|
||||||
|
|
||||||
|
// remove names from the safes in place
|
||||||
|
Object.values(parsedStoredSafes).forEach((item) => {
|
||||||
|
item.owners = item.owners.map((owner: any) => owner.address)
|
||||||
|
delete item.name
|
||||||
|
})
|
||||||
|
const migratedSafes = parsedStoredSafes as StoredSafes
|
||||||
|
|
||||||
|
const [state] = states
|
||||||
|
const addressBookKey = `${namespace}${namespaceSeparator}${state}`
|
||||||
|
const storedAddressBook = localStorage.getItem(addressBookKey)
|
||||||
|
const addressBookToStore: AddressBookState = storedAddressBook ? JSON.parse(storedAddressBook) : []
|
||||||
|
|
||||||
|
// mutate `addressBookToStore` by adding safes' information
|
||||||
|
safeAbEntries.forEach((entry) => {
|
||||||
|
const safeIndex = getEntryIndex(addressBookToStore, entry)
|
||||||
|
|
||||||
|
if (safeIndex >= 0) {
|
||||||
|
// update AB entry with what was stored in the safe object
|
||||||
|
addressBookToStore[safeIndex] = entry
|
||||||
|
} else {
|
||||||
|
// add the safe entry to the AB
|
||||||
|
addressBookToStore.push(entry)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// store the mutated address book
|
||||||
|
localStorage.setItem(addressBookKey, JSON.stringify(addressBookToStore))
|
||||||
|
|
||||||
|
// update stored safe
|
||||||
|
localStorage.setItem(safesKey, JSON.stringify(migratedSafes))
|
||||||
|
saveSafes(migratedSafes).then(() => console.info('Safe objects migrated'))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrates the AddressBook from a per-network to a global storage under the key `ADDRESS_BOOK` in `localStorage`
|
||||||
|
*
|
||||||
|
* The migrated structure will be `{ address, name, chainId }`
|
||||||
|
*
|
||||||
|
* @note Also, adds `chainId` to every entry in the AddressBook list.
|
||||||
|
*/
|
||||||
|
const migrateAddressBook = ({ states, namespace, namespaceSeparator }: StorageConfig): void => {
|
||||||
|
const [state] = states
|
||||||
|
const prefix = `v2_${getNetworkName()}`
|
||||||
|
const newKey = `${namespace}${namespaceSeparator}${state}`
|
||||||
|
const oldKey = 'ADDRESS_BOOK_STORAGE_KEY'
|
||||||
|
const storageKey = `_immortal|${prefix}__${oldKey}`
|
||||||
|
|
||||||
|
if (localStorage.getItem(newKey)) {
|
||||||
|
// already migrated
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const storedAddressBook = localStorage.getItem(storageKey)
|
||||||
|
|
||||||
|
if (!storedAddressBook) {
|
||||||
|
// nothing to migrate
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedAddressBook = JSON.parse(JSON.parse(storedAddressBook as string))
|
||||||
|
|
||||||
|
const migratedAddressBook = (parsedAddressBook as Omit<AddressBookEntry, 'chainId'>[])
|
||||||
|
// exclude those addresses with invalid names and addresses
|
||||||
|
.filter((item) => {
|
||||||
|
return isValidAddressBookName(item.name) && getWeb3().utils.isAddress(item.address)
|
||||||
|
})
|
||||||
|
.map(({ address, ...entry }) =>
|
||||||
|
makeAddressBookEntry({
|
||||||
|
address,
|
||||||
|
...entry,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
localStorage.setItem(newKey, JSON.stringify(migratedAddressBook))
|
||||||
|
|
||||||
|
// Remove the old Address Book storage
|
||||||
|
localStorage.removeItem(storageKey)
|
||||||
|
removeFromStorage(oldKey).then(() => console.info('Legacy Address Book removed'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const migrate = (storageConfig: StorageConfig): void => {
|
||||||
|
try {
|
||||||
|
migrateAddressBook(storageConfig)
|
||||||
|
migrateSafeNames(storageConfig)
|
||||||
|
} catch (e) {
|
||||||
|
logError(Errors._200, e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default migrate
|
|
@ -7,6 +7,7 @@
|
||||||
enum ErrorCodes {
|
enum ErrorCodes {
|
||||||
___0 = '0: No such error code',
|
___0 = '0: No such error code',
|
||||||
_100 = '100: Invalid input in the address field',
|
_100 = '100: Invalid input in the address field',
|
||||||
|
_200 = '200: Failed migrating to the address book v2',
|
||||||
_600 = '600: Error fetching token list',
|
_600 = '600: Error fetching token list',
|
||||||
_601 = '601: Error fetching balances',
|
_601 = '601: Error fetching balances',
|
||||||
_900 = '900: Error loading Safe App',
|
_900 = '900: Error loading Safe App',
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
import { useLocation } from 'react-router-dom'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
type AppUrlReturnType = {
|
|
||||||
url: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useSafeAppUrl = (): AppUrlReturnType => {
|
|
||||||
const [url, setUrl] = useState<string | null>(null)
|
|
||||||
const { search } = useLocation()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (search !== url) {
|
|
||||||
const query = new URLSearchParams(search)
|
|
||||||
setUrl(query.get('appUrl'))
|
|
||||||
}
|
|
||||||
}, [search, url])
|
|
||||||
|
|
||||||
return {
|
|
||||||
url,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -142,6 +142,17 @@ const addressBookEditEntry = {
|
||||||
afterExecutionError: null,
|
afterExecutionError: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addressBookImportEntries = {
|
||||||
|
beforeExecution: null,
|
||||||
|
afterRejection: null,
|
||||||
|
waitingConfirmation: null,
|
||||||
|
afterExecution: {
|
||||||
|
noMoreConfirmationsNeeded: NOTIFICATIONS.ADDRESS_BOOK_IMPORT_ENTRIES_SUCCESS,
|
||||||
|
moreConfirmationsNeeded: null,
|
||||||
|
},
|
||||||
|
afterExecutionError: null,
|
||||||
|
}
|
||||||
|
|
||||||
const addressBookDeleteEntry = {
|
const addressBookDeleteEntry = {
|
||||||
beforeExecution: null,
|
beforeExecution: null,
|
||||||
afterRejection: null,
|
afterRejection: null,
|
||||||
|
@ -153,6 +164,17 @@ const addressBookDeleteEntry = {
|
||||||
afterExecutionError: null,
|
afterExecutionError: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addressBookExportEntries = {
|
||||||
|
beforeExecution: null,
|
||||||
|
afterRejection: null,
|
||||||
|
waitingConfirmation: null,
|
||||||
|
afterExecution: {
|
||||||
|
noMoreConfirmationsNeeded: NOTIFICATIONS.ADDRESS_BOOK_EXPORT_ENTRIES_SUCCESS,
|
||||||
|
moreConfirmationsNeeded: null,
|
||||||
|
},
|
||||||
|
afterExecutionError: NOTIFICATIONS.ADDRESS_BOOK_EXPORT_ENTRIES_ERROR,
|
||||||
|
}
|
||||||
|
|
||||||
export const getNotificationsFromTxType: any = (txType, origin) => {
|
export const getNotificationsFromTxType: any = (txType, origin) => {
|
||||||
let notificationsQueue
|
let notificationsQueue
|
||||||
|
|
||||||
|
@ -193,18 +215,26 @@ export const getNotificationsFromTxType: any = (txType, origin) => {
|
||||||
notificationsQueue = waitingTransactionNotificationsQueue
|
notificationsQueue = waitingTransactionNotificationsQueue
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case TX_NOTIFICATION_TYPES.ADDRESSBOOK_NEW_ENTRY: {
|
case TX_NOTIFICATION_TYPES.ADDRESS_BOOK_NEW_ENTRY: {
|
||||||
notificationsQueue = addressBookNewEntry
|
notificationsQueue = addressBookNewEntry
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case TX_NOTIFICATION_TYPES.ADDRESSBOOK_EDIT_ENTRY: {
|
case TX_NOTIFICATION_TYPES.ADDRESS_BOOK_EDIT_ENTRY: {
|
||||||
notificationsQueue = addressBookEditEntry
|
notificationsQueue = addressBookEditEntry
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case TX_NOTIFICATION_TYPES.ADDRESSBOOK_DELETE_ENTRY: {
|
case TX_NOTIFICATION_TYPES.ADDRESS_BOOK_IMPORT_ENTRIES: {
|
||||||
|
notificationsQueue = addressBookImportEntries
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case TX_NOTIFICATION_TYPES.ADDRESS_BOOK_DELETE_ENTRY: {
|
||||||
notificationsQueue = addressBookDeleteEntry
|
notificationsQueue = addressBookDeleteEntry
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case TX_NOTIFICATION_TYPES.ADDRESS_BOOK_EXPORT_ENTRIES: {
|
||||||
|
notificationsQueue = addressBookExportEntries
|
||||||
|
break
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
notificationsQueue = defaultNotificationsQueue
|
notificationsQueue = defaultNotificationsQueue
|
||||||
break
|
break
|
||||||
|
|
|
@ -52,7 +52,10 @@ const NOTIFICATION_IDS = {
|
||||||
WRONG_NETWORK_MSG: 'WRONG_NETWORK_MSG',
|
WRONG_NETWORK_MSG: 'WRONG_NETWORK_MSG',
|
||||||
ADDRESS_BOOK_NEW_ENTRY_SUCCESS: 'ADDRESS_BOOK_NEW_ENTRY_SUCCESS',
|
ADDRESS_BOOK_NEW_ENTRY_SUCCESS: 'ADDRESS_BOOK_NEW_ENTRY_SUCCESS',
|
||||||
ADDRESS_BOOK_EDIT_ENTRY_SUCCESS: 'ADDRESS_BOOK_EDIT_ENTRY_SUCCESS',
|
ADDRESS_BOOK_EDIT_ENTRY_SUCCESS: 'ADDRESS_BOOK_EDIT_ENTRY_SUCCESS',
|
||||||
|
ADDRESS_BOOK_IMPORT_ENTRIES_SUCCESS: 'ADDRESS_BOOK_IMPORT_ENTRIES_SUCCESS',
|
||||||
ADDRESS_BOOK_DELETE_ENTRY_SUCCESS: 'ADDRESS_BOOK_DELETE_ENTRY_SUCCESS',
|
ADDRESS_BOOK_DELETE_ENTRY_SUCCESS: 'ADDRESS_BOOK_DELETE_ENTRY_SUCCESS',
|
||||||
|
ADDRESS_BOOK_EXPORT_ENTRIES_SUCCESS: 'ADDRESS_BOOK_EXPORT_ENTRIES_SUCCESS',
|
||||||
|
ADDRESS_BOOK_EXPORT_ENTRIES_ERROR: 'ADDRESS_BOOK_EXPORT_ENTRIES_ERROR',
|
||||||
SAFE_NEW_VERSION_AVAILABLE: 'SAFE_NEW_VERSION_AVAILABLE',
|
SAFE_NEW_VERSION_AVAILABLE: 'SAFE_NEW_VERSION_AVAILABLE',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,10 +210,22 @@ export const NOTIFICATIONS: Record<NotificationId, Notification> = {
|
||||||
message: 'Entry saved successfully',
|
message: 'Entry saved successfully',
|
||||||
options: { variant: SUCCESS, persist: false, preventDuplicate: false },
|
options: { variant: SUCCESS, persist: false, preventDuplicate: false },
|
||||||
},
|
},
|
||||||
|
ADDRESS_BOOK_IMPORT_ENTRIES_SUCCESS: {
|
||||||
|
message: 'Entries imported successfully',
|
||||||
|
options: { variant: SUCCESS, persist: false, preventDuplicate: false },
|
||||||
|
},
|
||||||
ADDRESS_BOOK_DELETE_ENTRY_SUCCESS: {
|
ADDRESS_BOOK_DELETE_ENTRY_SUCCESS: {
|
||||||
message: 'Entry deleted successfully',
|
message: 'Entry deleted successfully',
|
||||||
options: { variant: SUCCESS, persist: false, preventDuplicate: false },
|
options: { variant: SUCCESS, persist: false, preventDuplicate: false },
|
||||||
},
|
},
|
||||||
|
ADDRESS_BOOK_EXPORT_ENTRIES_SUCCESS: {
|
||||||
|
message: 'Address book exported',
|
||||||
|
options: { variant: SUCCESS, persist: false, preventDuplicate: false },
|
||||||
|
},
|
||||||
|
ADDRESS_BOOK_EXPORT_ENTRIES_ERROR: {
|
||||||
|
message: 'An error occurred while generating the address book CSV.',
|
||||||
|
options: { variant: ERROR, persist: false, preventDuplicate: false },
|
||||||
|
},
|
||||||
|
|
||||||
// Safe Version
|
// Safe Version
|
||||||
SAFE_NEW_VERSION_AVAILABLE: {
|
SAFE_NEW_VERSION_AVAILABLE: {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useDispatch } from 'react-redux'
|
import { useDispatch } from 'react-redux'
|
||||||
|
|
||||||
import loadAddressBookFromStorage from 'src/logic/addressBook/store/actions/loadAddressBookFromStorage'
|
|
||||||
import addViewedSafe from 'src/logic/currentSession/store/actions/addViewedSafe'
|
import addViewedSafe from 'src/logic/currentSession/store/actions/addViewedSafe'
|
||||||
import { fetchSafeTokens } from 'src/logic/tokens/store/actions/fetchSafeTokens'
|
import { fetchSafeTokens } from 'src/logic/tokens/store/actions/fetchSafeTokens'
|
||||||
import fetchLatestMasterContractVersion from 'src/logic/safe/store/actions/fetchLatestMasterContractVersion'
|
import fetchLatestMasterContractVersion from 'src/logic/safe/store/actions/fetchLatestMasterContractVersion'
|
||||||
|
@ -10,7 +9,7 @@ import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTr
|
||||||
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
||||||
import { updateAvailableCurrencies } from 'src/logic/currencyValues/store/actions/updateAvailableCurrencies'
|
import { updateAvailableCurrencies } from 'src/logic/currencyValues/store/actions/updateAvailableCurrencies'
|
||||||
|
|
||||||
export const useLoadSafe = (safeAddress?: string, loadedViaUrl = true): boolean => {
|
export const useLoadSafe = (safeAddress?: string): boolean => {
|
||||||
const dispatch = useDispatch<Dispatch>()
|
const dispatch = useDispatch<Dispatch>()
|
||||||
const [isSafeLoaded, setIsSafeLoaded] = useState(false)
|
const [isSafeLoaded, setIsSafeLoaded] = useState(false)
|
||||||
|
|
||||||
|
@ -23,15 +22,11 @@ export const useLoadSafe = (safeAddress?: string, loadedViaUrl = true): boolean
|
||||||
await dispatch(fetchSafeTokens(safeAddress))
|
await dispatch(fetchSafeTokens(safeAddress))
|
||||||
await dispatch(updateAvailableCurrencies())
|
await dispatch(updateAvailableCurrencies())
|
||||||
await dispatch(fetchTransactions(safeAddress))
|
await dispatch(fetchTransactions(safeAddress))
|
||||||
if (!loadedViaUrl) {
|
dispatch(addViewedSafe(safeAddress))
|
||||||
dispatch(addViewedSafe(safeAddress))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(loadAddressBookFromStorage())
|
|
||||||
fetchData()
|
fetchData()
|
||||||
}, [dispatch, safeAddress, loadedViaUrl])
|
}, [dispatch, safeAddress])
|
||||||
|
|
||||||
return isSafeLoaded
|
return isSafeLoaded
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
// --no-ignore
|
// --no-ignore
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { List, Map } from 'immutable'
|
import { Map } from 'immutable'
|
||||||
import configureMockStore from 'redux-mock-store'
|
import configureMockStore from 'redux-mock-store'
|
||||||
import thunk from 'redux-thunk'
|
import thunk from 'redux-thunk'
|
||||||
|
|
||||||
import { buildSafe, fetchSafe } from 'src/logic/safe/store/actions/fetchSafe'
|
import { buildSafe, fetchSafe } from 'src/logic/safe/store/actions/fetchSafe'
|
||||||
import * as storageUtils from 'src/utils/storage'
|
import * as storageUtils from 'src/utils/storage'
|
||||||
import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||||
import { makeOwner } from 'src/logic/safe/store/models/owner'
|
|
||||||
import { Overwrite } from 'src/types/helpers'
|
|
||||||
import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe'
|
import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe'
|
||||||
import { DEFAULT_SAFE_INITIAL_STATE } from 'src/logic/safe/store/reducer/safe'
|
import { DEFAULT_SAFE_INITIAL_STATE } from 'src/logic/safe/store/reducer/safe'
|
||||||
import { inMemoryPartialSafeInformation, localSafesInfo, remoteSafeInfoWithoutModules } from '../mocks/safeInformation'
|
import { inMemoryPartialSafeInformation, localSafesInfo, remoteSafeInfoWithoutModules } from '../mocks/safeInformation'
|
||||||
|
@ -25,51 +23,46 @@ describe('buildSafe', () => {
|
||||||
jest.unmock('src/utils/storage/index')
|
jest.unmock('src/utils/storage/index')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return a Partial SafeRecord with a mix of remote and local safe info', async () => {
|
// ToDo: use a property other than `name`
|
||||||
|
it.skip('should return a Partial SafeRecord with a mix of remote and local safe info', async () => {
|
||||||
mockedAxios.get.mockImplementationOnce(async () => ({ data: remoteSafeInfoWithoutModules }))
|
mockedAxios.get.mockImplementationOnce(async () => ({ data: remoteSafeInfoWithoutModules }))
|
||||||
storageUtil.loadFromStorage.mockImplementationOnce(async () => localSafesInfo)
|
storageUtil.loadFromStorage.mockImplementationOnce(async () => localSafesInfo)
|
||||||
const finalValues: Overwrite<Partial<SafeRecordProps>, { name: string }> = {
|
const finalValues: Partial<SafeRecordProps> = {
|
||||||
name: 'My Safe Name that will last',
|
|
||||||
modules: undefined,
|
modules: undefined,
|
||||||
spendingLimits: undefined,
|
spendingLimits: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
const builtSafe = await buildSafe(SAFE_ADDRESS, finalValues.name)
|
const builtSafe = await buildSafe(SAFE_ADDRESS)
|
||||||
|
|
||||||
expect(builtSafe).toStrictEqual({ ...inMemoryPartialSafeInformation, ...finalValues })
|
expect(builtSafe).toStrictEqual({ ...inMemoryPartialSafeInformation, ...finalValues })
|
||||||
})
|
})
|
||||||
it('should return a Partial SafeRecord when `remoteSafeInfo` is not present', async () => {
|
it.skip('should return a Partial SafeRecord when `remoteSafeInfo` is not present', async () => {
|
||||||
jest.spyOn(global.console, 'error').mockImplementationOnce(() => {})
|
jest.spyOn(global.console, 'error').mockImplementationOnce(() => {})
|
||||||
mockedAxios.get.mockImplementationOnce(async () => {
|
mockedAxios.get.mockImplementationOnce(async () => {
|
||||||
throw new Error('-- test -- no resource available')
|
throw new Error('-- test -- no resource available')
|
||||||
})
|
})
|
||||||
const name = 'My Safe Name that will last'
|
|
||||||
storageUtil.loadFromStorage.mockImplementationOnce(async () => localSafesInfo)
|
storageUtil.loadFromStorage.mockImplementationOnce(async () => localSafesInfo)
|
||||||
|
|
||||||
const builtSafe = await buildSafe(SAFE_ADDRESS, name)
|
const builtSafe = await buildSafe(SAFE_ADDRESS)
|
||||||
|
|
||||||
expect(builtSafe).toStrictEqual({ ...inMemoryPartialSafeInformation, name })
|
expect(builtSafe).toStrictEqual({ ...inMemoryPartialSafeInformation })
|
||||||
})
|
})
|
||||||
it('should return a Partial SafeRecord when `localSafeInfo` is not present', async () => {
|
it.skip('should return a Partial SafeRecord when `localSafeInfo` is not present', async () => {
|
||||||
mockedAxios.get.mockImplementationOnce(async () => ({ data: remoteSafeInfoWithoutModules }))
|
mockedAxios.get.mockImplementationOnce(async () => ({ data: remoteSafeInfoWithoutModules }))
|
||||||
storageUtil.loadFromStorage.mockImplementationOnce(async () => undefined)
|
storageUtil.loadFromStorage.mockImplementationOnce(async () => undefined)
|
||||||
const name = 'My Safe Name that WILL last'
|
|
||||||
|
|
||||||
const builtSafe = await buildSafe(SAFE_ADDRESS, name)
|
const builtSafe = await buildSafe(SAFE_ADDRESS)
|
||||||
|
|
||||||
expect(builtSafe).toStrictEqual({
|
expect(builtSafe).toStrictEqual({
|
||||||
name,
|
|
||||||
address: SAFE_ADDRESS,
|
address: SAFE_ADDRESS,
|
||||||
threshold: 2,
|
threshold: 2,
|
||||||
owners: List(
|
owners: [
|
||||||
[
|
'0xcCdd7e3af1c24c08D8B65A328351e7e23923d875',
|
||||||
{ address: '0xcCdd7e3af1c24c08D8B65A328351e7e23923d875' },
|
'0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F',
|
||||||
{ address: '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F' },
|
'0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47',
|
||||||
{ address: '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47' },
|
'0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2',
|
||||||
{ address: '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2' },
|
'0x5e47249883F6a1d639b84e8228547fB289e222b6',
|
||||||
{ address: '0x5e47249883F6a1d639b84e8228547fB289e222b6' },
|
],
|
||||||
].map(makeOwner),
|
|
||||||
),
|
|
||||||
modules: undefined,
|
modules: undefined,
|
||||||
spendingLimits: undefined,
|
spendingLimits: undefined,
|
||||||
nonce: 492,
|
nonce: 492,
|
||||||
|
@ -78,19 +71,18 @@ describe('buildSafe', () => {
|
||||||
featuresEnabled: ['ERC721', 'ERC1155', 'SAFE_APPS', 'CONTRACT_INTERACTION'],
|
featuresEnabled: ['ERC721', 'ERC1155', 'SAFE_APPS', 'CONTRACT_INTERACTION'],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('should return a Partial SafeRecord with only `address` and `name` keys if it fails to recover info', async () => {
|
it.skip('should return a Partial SafeRecord with only `address` and `name` keys if it fails to recover info', async () => {
|
||||||
jest.spyOn(global.console, 'error').mockImplementationOnce(() => {})
|
jest.spyOn(global.console, 'error').mockImplementationOnce(() => {})
|
||||||
mockedAxios.get.mockImplementationOnce(async () => {
|
mockedAxios.get.mockImplementationOnce(async () => {
|
||||||
throw new Error('-- test -- no resource available')
|
throw new Error('-- test -- no resource available')
|
||||||
})
|
})
|
||||||
const finalValues: Overwrite<Partial<SafeRecordProps>, { name: string }> = {
|
const finalValues: Partial<SafeRecordProps> = {
|
||||||
name: 'My Safe Name that will last',
|
|
||||||
address: SAFE_ADDRESS,
|
address: SAFE_ADDRESS,
|
||||||
owners: undefined,
|
owners: undefined,
|
||||||
}
|
}
|
||||||
storageUtil.loadFromStorage.mockImplementationOnce(async () => undefined)
|
storageUtil.loadFromStorage.mockImplementationOnce(async () => undefined)
|
||||||
|
|
||||||
const builtSafe = await buildSafe(SAFE_ADDRESS, finalValues.name)
|
const builtSafe = await buildSafe(SAFE_ADDRESS)
|
||||||
|
|
||||||
expect(builtSafe).toStrictEqual(finalValues)
|
expect(builtSafe).toStrictEqual(finalValues)
|
||||||
})
|
})
|
||||||
|
@ -99,7 +91,6 @@ describe('buildSafe', () => {
|
||||||
describe('fetchSafe', () => {
|
describe('fetchSafe', () => {
|
||||||
const SAFE_ADDRESS = '0xe414604Ad49602C0b9c0b08D0781ECF96740786a'
|
const SAFE_ADDRESS = '0xe414604Ad49602C0b9c0b08D0781ECF96740786a'
|
||||||
const mockedAxios = axios as jest.Mocked<typeof axios>
|
const mockedAxios = axios as jest.Mocked<typeof axios>
|
||||||
const storageUtil = require('src/utils/storage/index') as jest.Mocked<typeof storageUtils>
|
|
||||||
const middlewares = [thunk]
|
const middlewares = [thunk]
|
||||||
const mockStore = configureMockStore(middlewares)
|
const mockStore = configureMockStore(middlewares)
|
||||||
|
|
||||||
|
@ -115,15 +106,13 @@ describe('fetchSafe', () => {
|
||||||
payload: {
|
payload: {
|
||||||
address: SAFE_ADDRESS,
|
address: SAFE_ADDRESS,
|
||||||
threshold: 2,
|
threshold: 2,
|
||||||
owners: List(
|
owners: [
|
||||||
[
|
'0xcCdd7e3af1c24c08D8B65A328351e7e23923d875',
|
||||||
{ address: '0xcCdd7e3af1c24c08D8B65A328351e7e23923d875' },
|
'0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F',
|
||||||
{ address: '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F' },
|
'0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47',
|
||||||
{ address: '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47' },
|
'0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2',
|
||||||
{ address: '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2' },
|
'0x5e47249883F6a1d639b84e8228547fB289e222b6',
|
||||||
{ address: '0x5e47249883F6a1d639b84e8228547fB289e222b6' },
|
],
|
||||||
].map(makeOwner),
|
|
||||||
),
|
|
||||||
modules: undefined,
|
modules: undefined,
|
||||||
spendingLimits: undefined,
|
spendingLimits: undefined,
|
||||||
nonce: 492,
|
nonce: 492,
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { List } from 'immutable'
|
|
||||||
|
|
||||||
import { FEATURES } from 'src/config/networks/network.d'
|
import { FEATURES } from 'src/config/networks/network.d'
|
||||||
import {
|
import {
|
||||||
|
@ -9,8 +8,7 @@ import {
|
||||||
getNewTxNonce,
|
getNewTxNonce,
|
||||||
shouldExecuteTransaction,
|
shouldExecuteTransaction,
|
||||||
} from 'src/logic/safe/store/actions/utils'
|
} from 'src/logic/safe/store/actions/utils'
|
||||||
import { makeOwner } from 'src/logic/safe/store/models/owner'
|
import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||||
import { SafeOwner, SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
|
||||||
import { buildTxServiceUrl } from 'src/logic/safe/transactions'
|
import { buildTxServiceUrl } from 'src/logic/safe/transactions'
|
||||||
import { getMockedSafeInstance, getMockedTxServiceModel } from 'src/test/utils/safeHelper'
|
import { getMockedSafeInstance, getMockedTxServiceModel } from 'src/test/utils/safeHelper'
|
||||||
import {
|
import {
|
||||||
|
@ -223,96 +221,49 @@ describe('buildSafeOwners', () => {
|
||||||
expect(buildSafeOwners()).toBeUndefined()
|
expect(buildSafeOwners()).toBeUndefined()
|
||||||
})
|
})
|
||||||
it('should return `localSafeOwners` if no `remoteSafeOwners` were provided', () => {
|
it('should return `localSafeOwners` if no `remoteSafeOwners` were provided', () => {
|
||||||
const expectedOwners = List(
|
const expectedOwners = [
|
||||||
[
|
'0xcCdd7e3af1c24c08D8B65A328351e7e23923d875',
|
||||||
{ address: '0xcCdd7e3af1c24c08D8B65A328351e7e23923d875' },
|
'0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F',
|
||||||
{ address: '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F' },
|
'0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47',
|
||||||
{ address: '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47' },
|
'0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2',
|
||||||
{ address: '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2' },
|
'0x5e47249883F6a1d639b84e8228547fB289e222b6',
|
||||||
{ address: '0x5e47249883F6a1d639b84e8228547fB289e222b6' },
|
]
|
||||||
].map(makeOwner),
|
|
||||||
)
|
|
||||||
expect(buildSafeOwners(remoteSafeInfoWithModules.owners)).toStrictEqual(expectedOwners)
|
expect(buildSafeOwners(remoteSafeInfoWithModules.owners)).toStrictEqual(expectedOwners)
|
||||||
})
|
})
|
||||||
it('should discard those owners that are not present in `remoteSafeOwners`', () => {
|
it('should discard those owners that are not present in `remoteSafeOwners`', () => {
|
||||||
const localOwners: List<SafeOwner> = List(localSafesInfo[SAFE_ADDRESS].owners.map(makeOwner))
|
const localOwners: SafeRecordProps['owners'] = localSafesInfo[SAFE_ADDRESS].owners
|
||||||
const [, ...remoteOwners] = remoteSafeInfoWithModules.owners
|
const [, ...remoteOwners] = remoteSafeInfoWithModules.owners
|
||||||
const expectedOwners = List(
|
const expectedOwners = [
|
||||||
[
|
'0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F',
|
||||||
{
|
'0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47',
|
||||||
name: 'UNKNOWN',
|
'0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2',
|
||||||
address: '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F',
|
'0x5e47249883F6a1d639b84e8228547fB289e222b6',
|
||||||
},
|
]
|
||||||
{
|
|
||||||
name: 'UNKNOWN',
|
|
||||||
address: '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Owner B',
|
|
||||||
address: '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Owner A',
|
|
||||||
address: '0x5e47249883F6a1d639b84e8228547fB289e222b6',
|
|
||||||
},
|
|
||||||
].map(makeOwner),
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(buildSafeOwners(remoteOwners, localOwners)).toStrictEqual(expectedOwners)
|
expect(buildSafeOwners(remoteOwners, localOwners)).toStrictEqual(expectedOwners)
|
||||||
})
|
})
|
||||||
it('should add those owners that are not present in `localSafeOwners`', () => {
|
it('should add those owners that are not present in `localSafeOwners`', () => {
|
||||||
const localOwners: List<SafeOwner> = List(localSafesInfo[SAFE_ADDRESS].owners.slice(0, 4).map(makeOwner))
|
const localOwners: SafeRecordProps['owners'] = localSafesInfo[SAFE_ADDRESS].owners.slice(0, 4)
|
||||||
const remoteOwners = remoteSafeInfoWithModules.owners
|
const remoteOwners = remoteSafeInfoWithModules.owners
|
||||||
const expectedOwners = List(
|
const expectedOwners = [
|
||||||
[
|
'0xcCdd7e3af1c24c08D8B65A328351e7e23923d875',
|
||||||
{
|
'0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F',
|
||||||
name: 'UNKNOWN',
|
'0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47',
|
||||||
address: '0xcCdd7e3af1c24c08D8B65A328351e7e23923d875',
|
'0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2',
|
||||||
},
|
'0x5e47249883F6a1d639b84e8228547fB289e222b6',
|
||||||
{
|
]
|
||||||
name: 'UNKNOWN',
|
|
||||||
address: '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'UNKNOWN',
|
|
||||||
address: '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Owner B',
|
|
||||||
address: '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'UNKNOWN',
|
|
||||||
address: '0x5e47249883F6a1d639b84e8228547fB289e222b6',
|
|
||||||
},
|
|
||||||
].map(makeOwner),
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(buildSafeOwners(remoteOwners, localOwners)).toStrictEqual(expectedOwners)
|
expect(buildSafeOwners(remoteOwners, localOwners)).toStrictEqual(expectedOwners)
|
||||||
})
|
})
|
||||||
it('should preserve those owners that are present in `remoteSafeOwners` with data present in `localSafeOwners`', () => {
|
it('should preserve those owners that are present in `remoteSafeOwners` with data present in `localSafeOwners`', () => {
|
||||||
const localOwners: List<SafeOwner> = List(localSafesInfo[SAFE_ADDRESS].owners.slice(0, 4).map(makeOwner))
|
const localOwners: SafeRecordProps['owners'] = localSafesInfo[SAFE_ADDRESS].owners.slice(0, 4)
|
||||||
const [, ...remoteOwners] = remoteSafeInfoWithModules.owners
|
const [, ...remoteOwners] = remoteSafeInfoWithModules.owners
|
||||||
const expectedOwners = List(
|
const expectedOwners = [
|
||||||
[
|
'0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F',
|
||||||
{
|
'0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47',
|
||||||
name: 'UNKNOWN',
|
'0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2',
|
||||||
address: '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F',
|
'0x5e47249883F6a1d639b84e8228547fB289e222b6',
|
||||||
},
|
]
|
||||||
{
|
|
||||||
name: 'UNKNOWN',
|
|
||||||
address: '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Owner B',
|
|
||||||
address: '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'UNKNOWN',
|
|
||||||
address: '0x5e47249883F6a1d639b84e8228547fB289e222b6',
|
|
||||||
},
|
|
||||||
].map(makeOwner),
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(buildSafeOwners(remoteOwners, localOwners)).toStrictEqual(expectedOwners)
|
expect(buildSafeOwners(remoteOwners, localOwners)).toStrictEqual(expectedOwners)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,17 +1,9 @@
|
||||||
import { createAction } from 'redux-actions'
|
import { createAction } from 'redux-actions'
|
||||||
|
|
||||||
import { SafeOwner, SafeRecordProps } from '../models/safe'
|
import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||||
import { List } from 'immutable'
|
|
||||||
import { makeOwner } from '../models/owner'
|
|
||||||
|
|
||||||
export const ADD_OR_UPDATE_SAFE = 'ADD_OR_UPDATE_SAFE'
|
export const ADD_OR_UPDATE_SAFE = 'ADD_OR_UPDATE_SAFE'
|
||||||
|
|
||||||
export const buildOwnersFrom = (names: string[], addresses: string[]): List<SafeOwner> => {
|
|
||||||
const owners = names.map((name, index) => makeOwner({ name, address: addresses[index] }))
|
|
||||||
|
|
||||||
return List(owners)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const addOrUpdateSafe = createAction(ADD_OR_UPDATE_SAFE, (safe: SafeRecordProps) => ({
|
export const addOrUpdateSafe = createAction(ADD_OR_UPDATE_SAFE, (safe: SafeRecordProps) => ({
|
||||||
safe,
|
safe,
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
import { createAction } from 'redux-actions'
|
|
||||||
|
|
||||||
export const ADD_SAFE_OWNER = 'ADD_SAFE_OWNER'
|
|
||||||
|
|
||||||
export const addSafeOwner = createAction(ADD_SAFE_OWNER)
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { createAction } from 'redux-actions'
|
|
||||||
|
|
||||||
export const EDIT_SAFE_OWNER = 'EDIT_SAFE_OWNER'
|
|
||||||
|
|
||||||
const editSafeOwner = createAction(EDIT_SAFE_OWNER)
|
|
||||||
|
|
||||||
export default editSafeOwner
|
|
|
@ -1,16 +1,13 @@
|
||||||
import { List } from 'immutable'
|
|
||||||
import { Dispatch } from 'redux'
|
import { Dispatch } from 'redux'
|
||||||
|
import { Action } from 'redux-actions'
|
||||||
|
|
||||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
|
||||||
import { updateSafe } from 'src/logic/safe/store/actions/updateSafe'
|
import { updateSafe } from 'src/logic/safe/store/actions/updateSafe'
|
||||||
import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||||
import { getLocalSafe } from 'src/logic/safe/utils'
|
import { getLocalSafe } from 'src/logic/safe/utils'
|
||||||
import { allSettled } from 'src/logic/safe/utils/allSettled'
|
import { allSettled } from 'src/logic/safe/utils/allSettled'
|
||||||
import { getSafeInfo, SafeInfo } from 'src/logic/safe/utils/safeInformation'
|
import { getSafeInfo, SafeInfo } from 'src/logic/safe/utils/safeInformation'
|
||||||
import { AppReduxState } from 'src/store'
|
|
||||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||||
import { buildSafeOwners, extractRemoteSafeInfo } from './utils'
|
import { buildSafeOwners, extractRemoteSafeInfo } from './utils'
|
||||||
import { Action } from 'redux-actions'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a Safe Record that will be added to the app's store
|
* Builds a Safe Record that will be added to the app's store
|
||||||
|
@ -19,15 +16,12 @@ import { Action } from 'redux-actions'
|
||||||
* @note It's being used by "Load Existing Safe" and "Create New Safe" flows
|
* @note It's being used by "Load Existing Safe" and "Create New Safe" flows
|
||||||
*
|
*
|
||||||
* @param {string} safeAddress
|
* @param {string} safeAddress
|
||||||
* @param {string} safeName
|
|
||||||
* @returns Promise<SafeRecordProps>
|
* @returns Promise<SafeRecordProps>
|
||||||
*/
|
*/
|
||||||
export const buildSafe = async (safeAddress: string, safeName: string): Promise<SafeRecordProps> => {
|
export const buildSafe = async (safeAddress: string): Promise<SafeRecordProps> => {
|
||||||
const address = checksumAddress(safeAddress)
|
const address = checksumAddress(safeAddress)
|
||||||
const safeInfo: Partial<SafeRecordProps> = {
|
// setting `loadedViaUrl` to false, as `buildSafe` is called on safe Load or Open flows
|
||||||
address,
|
const safeInfo: Partial<SafeRecordProps> = { address, loadedViaUrl: false }
|
||||||
name: safeName,
|
|
||||||
}
|
|
||||||
|
|
||||||
const [remote, localSafeInfo] = await allSettled<[SafeInfo | null, SafeRecordProps | undefined | null]>(
|
const [remote, localSafeInfo] = await allSettled<[SafeInfo | null, SafeRecordProps | undefined | null]>(
|
||||||
getSafeInfo(safeAddress),
|
getSafeInfo(safeAddress),
|
||||||
|
@ -52,7 +46,6 @@ export const buildSafe = async (safeAddress: string, safeName: string): Promise<
|
||||||
*/
|
*/
|
||||||
export const fetchSafe = (safeAddress: string) => async (
|
export const fetchSafe = (safeAddress: string) => async (
|
||||||
dispatch: Dispatch,
|
dispatch: Dispatch,
|
||||||
getState: () => AppReduxState,
|
|
||||||
): Promise<Action<Partial<SafeRecordProps>>> => {
|
): Promise<Action<Partial<SafeRecordProps>>> => {
|
||||||
const address = checksumAddress(safeAddress)
|
const address = checksumAddress(safeAddress)
|
||||||
|
|
||||||
|
@ -61,13 +54,10 @@ export const fetchSafe = (safeAddress: string) => async (
|
||||||
// remote (client-gateway)
|
// remote (client-gateway)
|
||||||
const safeInfo = remoteSafeInfo ? await extractRemoteSafeInfo(remoteSafeInfo) : {}
|
const safeInfo = remoteSafeInfo ? await extractRemoteSafeInfo(remoteSafeInfo) : {}
|
||||||
|
|
||||||
// TODO: REVIEW: having the owner's names duplicated with what's in the address book seems a bit odd
|
|
||||||
const state = getState()
|
|
||||||
const addressBook = addressBookSelector(state)
|
|
||||||
// update owner's information
|
// update owner's information
|
||||||
const owners = remoteSafeInfo
|
const owners = remoteSafeInfo
|
||||||
? // if we have remote info, we can enrich it with local address book information
|
? // if we have remote info, we use it
|
||||||
buildSafeOwners(remoteSafeInfo.owners, List(addressBook))
|
buildSafeOwners(remoteSafeInfo.owners)
|
||||||
: // if there's no remote info, we keep what's in memory
|
: // if there's no remote info, we keep what's in memory
|
||||||
undefined
|
undefined
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,15 @@
|
||||||
import { Dispatch } from 'redux'
|
import { Dispatch } from 'redux'
|
||||||
|
import { getLocalSafes } 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 { addOrUpdateSafe } from './addOrUpdateSafe'
|
import { addOrUpdateSafe } from './addOrUpdateSafe'
|
||||||
|
|
||||||
const loadSafesFromStorage = () => async (dispatch: Dispatch): Promise<void> => {
|
const loadSafesFromStorage = () => async (dispatch: Dispatch): Promise<void> => {
|
||||||
try {
|
const safes = await getLocalSafes()
|
||||||
const safes = await loadFromStorage<Record<string, SafeRecordProps>>(SAFES_KEY)
|
|
||||||
|
|
||||||
if (safes) {
|
if (safes) {
|
||||||
Object.values(safes).forEach((safeProps) => {
|
safes.forEach((safeProps) => {
|
||||||
dispatch(addOrUpdateSafe(buildSafe(safeProps)))
|
dispatch(addOrUpdateSafe(buildSafe(safeProps)))
|
||||||
})
|
})
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
console.error('Error while getting Safes from storage:', err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
import { List } from 'immutable'
|
|
||||||
import { makeOwner } from '../../models/owner'
|
|
||||||
|
|
||||||
export const remoteSafeInfoWithModules = {
|
export const remoteSafeInfoWithModules = {
|
||||||
address: {
|
address: {
|
||||||
value: '0xe414604Ad49602C0b9c0b08D0781ECF96740786a',
|
value: '0xe414604Ad49602C0b9c0b08D0781ECF96740786a',
|
||||||
|
@ -86,30 +83,13 @@ export const localSafesInfo = {
|
||||||
name: 'Safe A',
|
name: 'Safe A',
|
||||||
address: '0xe414604Ad49602C0b9c0b08D0781ECF96740786a',
|
address: '0xe414604Ad49602C0b9c0b08D0781ECF96740786a',
|
||||||
threshold: 2,
|
threshold: 2,
|
||||||
owners: List(
|
owners: [
|
||||||
[
|
'0xcCdd7e3af1c24c08D8B65A328351e7e23923d875',
|
||||||
{
|
'0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F',
|
||||||
name: 'UNKNOWN',
|
'0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47',
|
||||||
address: '0xcCdd7e3af1c24c08D8B65A328351e7e23923d875',
|
'0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2',
|
||||||
},
|
'0x5e47249883F6a1d639b84e8228547fB289e222b6',
|
||||||
{
|
],
|
||||||
name: 'UNKNOWN',
|
|
||||||
address: '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'UNKNOWN',
|
|
||||||
address: '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Owner B',
|
|
||||||
address: '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Owner A',
|
|
||||||
address: '0x5e47249883F6a1d639b84e8228547fB289e222b6',
|
|
||||||
},
|
|
||||||
].map(makeOwner),
|
|
||||||
),
|
|
||||||
modules: [['0x0000000000000000000000000000000000000001', '0xCFbFaC74C26F8647cBDb8c5caf80BB5b32E43134']],
|
modules: [['0x0000000000000000000000000000000000000001', '0xCFbFaC74C26F8647cBDb8c5caf80BB5b32E43134']],
|
||||||
spendingLimits: [
|
spendingLimits: [
|
||||||
{
|
{
|
||||||
|
@ -177,30 +157,13 @@ export const inMemoryPartialSafeInformation = {
|
||||||
name: 'Safe A',
|
name: 'Safe A',
|
||||||
address: '0xe414604Ad49602C0b9c0b08D0781ECF96740786a',
|
address: '0xe414604Ad49602C0b9c0b08D0781ECF96740786a',
|
||||||
threshold: 2,
|
threshold: 2,
|
||||||
owners: List(
|
owners: [
|
||||||
[
|
'0xcCdd7e3af1c24c08D8B65A328351e7e23923d875',
|
||||||
{
|
'0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F',
|
||||||
name: 'UNKNOWN',
|
'0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47',
|
||||||
address: '0xcCdd7e3af1c24c08D8B65A328351e7e23923d875',
|
'0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2',
|
||||||
},
|
'0x5e47249883F6a1d639b84e8228547fB289e222b6',
|
||||||
{
|
],
|
||||||
name: 'UNKNOWN',
|
|
||||||
address: '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'UNKNOWN',
|
|
||||||
address: '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Owner B',
|
|
||||||
address: '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Owner A',
|
|
||||||
address: '0x5e47249883F6a1d639b84e8228547fB289e222b6',
|
|
||||||
},
|
|
||||||
].map(makeOwner),
|
|
||||||
),
|
|
||||||
modules: [['0x0000000000000000000000000000000000000001', '0xCFbFaC74C26F8647cBDb8c5caf80BB5b32E43134']],
|
modules: [['0x0000000000000000000000000000000000000001', '0xCFbFaC74C26F8647cBDb8c5caf80BB5b32E43134']],
|
||||||
spendingLimits: [
|
spendingLimits: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
import { createAction } from 'redux-actions'
|
|
||||||
|
|
||||||
export const REMOVE_SAFE_OWNER = 'REMOVE_SAFE_OWNER'
|
|
||||||
|
|
||||||
export const removeSafeOwner = createAction(REMOVE_SAFE_OWNER)
|
|
|
@ -1,5 +0,0 @@
|
||||||
import { createAction } from 'redux-actions'
|
|
||||||
|
|
||||||
export const REPLACE_SAFE_OWNER = 'REPLACE_SAFE_OWNER'
|
|
||||||
|
|
||||||
export const replaceSafeOwner = createAction(REPLACE_SAFE_OWNER)
|
|
|
@ -8,10 +8,7 @@ import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||||
import { getSpendingLimits } from 'src/logic/safe/utils/spendingLimits'
|
import { getSpendingLimits } from 'src/logic/safe/utils/spendingLimits'
|
||||||
import { buildModulesLinkedList } from 'src/logic/safe/utils/modules'
|
import { buildModulesLinkedList } from 'src/logic/safe/utils/modules'
|
||||||
import { enabledFeatures, safeNeedsUpdate } from 'src/logic/safe/utils/safeVersion'
|
import { enabledFeatures, safeNeedsUpdate } from 'src/logic/safe/utils/safeVersion'
|
||||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
|
||||||
import { makeOwner } from 'src/logic/safe/store/models/owner'
|
|
||||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||||
import { List } from 'immutable'
|
|
||||||
|
|
||||||
export const getLastTx = async (safeAddress: string): Promise<TxServiceModel | null> => {
|
export const getLastTx = async (safeAddress: string): Promise<TxServiceModel | null> => {
|
||||||
try {
|
try {
|
||||||
|
@ -105,13 +102,9 @@ export const buildSafeOwners = (
|
||||||
localSafeOwners?: SafeRecordProps['owners'],
|
localSafeOwners?: SafeRecordProps['owners'],
|
||||||
): SafeRecordProps['owners'] | undefined => {
|
): SafeRecordProps['owners'] | undefined => {
|
||||||
if (remoteSafeOwners) {
|
if (remoteSafeOwners) {
|
||||||
const remoteOwners = remoteSafeOwners.map(({ value }) => {
|
// ToDo: review if checksums addresses is necessary,
|
||||||
const localOwner = localSafeOwners?.find(({ address }) => sameAddress(address, value))
|
// as they must be provided already in the checksum form from the services
|
||||||
const name = localOwner?.name
|
return remoteSafeOwners.map(({ value }) => checksumAddress(value))
|
||||||
return makeOwner({ name, address: checksumAddress(value) })
|
|
||||||
})
|
|
||||||
|
|
||||||
return List(remoteOwners)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// nothing to do without remote owners, so we return the stored list
|
// nothing to do without remote owners, so we return the stored list
|
||||||
|
|
|
@ -1,30 +1,13 @@
|
||||||
import { Store } from 'redux'
|
import { Store } from 'redux'
|
||||||
|
|
||||||
import { saveDefaultSafe, saveSafes } from 'src/logic/safe/utils'
|
import { saveDefaultSafe, saveSafes } from 'src/logic/safe/utils'
|
||||||
import { ADD_SAFE_OWNER } from 'src/logic/safe/store/actions/addSafeOwner'
|
|
||||||
import { EDIT_SAFE_OWNER } from 'src/logic/safe/store/actions/editSafeOwner'
|
|
||||||
import { REMOVE_SAFE } from 'src/logic/safe/store/actions/removeSafe'
|
import { REMOVE_SAFE } from 'src/logic/safe/store/actions/removeSafe'
|
||||||
import { REMOVE_SAFE_OWNER } from 'src/logic/safe/store/actions/removeSafeOwner'
|
|
||||||
import { REPLACE_SAFE_OWNER } from 'src/logic/safe/store/actions/replaceSafeOwner'
|
|
||||||
import { SET_DEFAULT_SAFE } from 'src/logic/safe/store/actions/setDefaultSafe'
|
import { SET_DEFAULT_SAFE } from 'src/logic/safe/store/actions/setDefaultSafe'
|
||||||
import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe'
|
import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe'
|
||||||
import { safesMapSelector } from 'src/logic/safe/store/selectors'
|
import { safesMapSelector } from 'src/logic/safe/store/selectors'
|
||||||
import { ADD_OR_UPDATE_SAFE } from 'src/logic/safe/store/actions/addOrUpdateSafe'
|
|
||||||
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
|
||||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
|
||||||
import { isValidAddressBookName } from 'src/logic/addressBook/utils'
|
|
||||||
import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
|
|
||||||
import { SafeRecord } from '../models/safe'
|
import { SafeRecord } from '../models/safe'
|
||||||
|
|
||||||
const watchedActions = [
|
const watchedActions = [REMOVE_SAFE, SET_DEFAULT_SAFE, UPDATE_SAFE]
|
||||||
UPDATE_SAFE,
|
|
||||||
REMOVE_SAFE,
|
|
||||||
ADD_OR_UPDATE_SAFE,
|
|
||||||
ADD_SAFE_OWNER,
|
|
||||||
REMOVE_SAFE_OWNER,
|
|
||||||
REPLACE_SAFE_OWNER,
|
|
||||||
EDIT_SAFE_OWNER,
|
|
||||||
SET_DEFAULT_SAFE,
|
|
||||||
]
|
|
||||||
|
|
||||||
type SafeProps = {
|
type SafeProps = {
|
||||||
safe: SafeRecord
|
safe: SafeRecord
|
||||||
|
@ -40,34 +23,10 @@ export const safeStorageMiddleware = (store: Store) => (
|
||||||
|
|
||||||
if (watchedActions.includes(action.type)) {
|
if (watchedActions.includes(action.type)) {
|
||||||
const state = store.getState()
|
const state = store.getState()
|
||||||
const { dispatch } = store
|
|
||||||
const safes = safesMapSelector(state)
|
const safes = safesMapSelector(state)
|
||||||
await saveSafes(safes.filter((safe) => !safe.loadedViaUrl).toJSON())
|
await saveSafes(safes.filter((safe) => !safe.loadedViaUrl).toJSON())
|
||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ADD_OR_UPDATE_SAFE: {
|
|
||||||
const { safe } = action.payload as SafeProps
|
|
||||||
safe.owners.forEach((owner: { address: string; name: any }) => {
|
|
||||||
const checksumEntry = makeAddressBookEntry({ address: checksumAddress(owner.address), name: owner.name })
|
|
||||||
if (isValidAddressBookName(checksumEntry.name)) {
|
|
||||||
dispatch(addOrUpdateAddressBookEntry(checksumEntry))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// add the recently loaded safe as an entry in the address book
|
|
||||||
// if it exists already, it will be replaced with the recently added name
|
|
||||||
if (safe.name) {
|
|
||||||
dispatch(addOrUpdateAddressBookEntry(makeAddressBookEntry({ name: safe.name, address: safe.address })))
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case UPDATE_SAFE: {
|
|
||||||
const { name, address } = action.payload as { name: string; address: string }
|
|
||||||
if (name) {
|
|
||||||
dispatch(addOrUpdateAddressBookEntry(makeAddressBookEntry({ name, address })))
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case SET_DEFAULT_SAFE: {
|
case SET_DEFAULT_SAFE: {
|
||||||
if (action.payload) {
|
if (action.payload) {
|
||||||
saveDefaultSafe(action.payload as string)
|
saveDefaultSafe(action.payload as string)
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { Record } from 'immutable'
|
|
||||||
|
|
||||||
export const makeOwner = Record({
|
|
||||||
name: 'UNKNOWN',
|
|
||||||
address: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
// Usage const someRecord: Owner = makeOwner({ name: ... })
|
|
|
@ -1,11 +1,9 @@
|
||||||
import { List, Record, RecordOf } from 'immutable'
|
import { Record, RecordOf } from 'immutable'
|
||||||
|
|
||||||
import { FEATURES } from 'src/config/networks/network.d'
|
import { FEATURES } from 'src/config/networks/network.d'
|
||||||
import { BalanceRecord } from 'src/logic/tokens/store/actions/fetchSafeTokens'
|
import { BalanceRecord } from 'src/logic/tokens/store/actions/fetchSafeTokens'
|
||||||
|
|
||||||
export type SafeOwner = {
|
export type SafeOwner = string
|
||||||
name: string
|
|
||||||
address: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ModulePair = [
|
export type ModulePair = [
|
||||||
// previous module
|
// previous module
|
||||||
|
@ -25,39 +23,37 @@ export type SpendingLimit = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SafeRecordProps = {
|
export type SafeRecordProps = {
|
||||||
name: string
|
|
||||||
address: string
|
address: string
|
||||||
threshold: number
|
threshold: number
|
||||||
ethBalance: string
|
ethBalance: string
|
||||||
totalFiatBalance: string
|
totalFiatBalance: string
|
||||||
owners: List<SafeOwner>
|
owners: SafeOwner[]
|
||||||
modules?: ModulePair[] | null
|
modules?: ModulePair[] | null
|
||||||
spendingLimits?: SpendingLimit[] | null
|
spendingLimits?: SpendingLimit[] | null
|
||||||
balances: BalanceRecord[]
|
balances: BalanceRecord[]
|
||||||
nonce: number
|
nonce: number
|
||||||
recurringUser?: boolean
|
recurringUser?: boolean
|
||||||
loadedViaUrl?: boolean
|
|
||||||
currentVersion: string
|
currentVersion: string
|
||||||
needsUpdate: boolean
|
needsUpdate: boolean
|
||||||
featuresEnabled: Array<FEATURES>
|
featuresEnabled: Array<FEATURES>
|
||||||
|
loadedViaUrl: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const makeSafe = Record<SafeRecordProps>({
|
const makeSafe = Record<SafeRecordProps>({
|
||||||
name: '',
|
|
||||||
address: '',
|
address: '',
|
||||||
threshold: 0,
|
threshold: 0,
|
||||||
ethBalance: '0',
|
ethBalance: '0',
|
||||||
totalFiatBalance: '0',
|
totalFiatBalance: '0',
|
||||||
owners: List([]),
|
owners: [],
|
||||||
modules: [],
|
modules: [],
|
||||||
spendingLimits: [],
|
spendingLimits: [],
|
||||||
balances: [],
|
balances: [],
|
||||||
nonce: 0,
|
nonce: 0,
|
||||||
loadedViaUrl: false,
|
|
||||||
recurringUser: undefined,
|
recurringUser: undefined,
|
||||||
currentVersion: '',
|
currentVersion: '',
|
||||||
needsUpdate: false,
|
needsUpdate: false,
|
||||||
featuresEnabled: [],
|
featuresEnabled: [],
|
||||||
|
loadedViaUrl: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
export type SafeRecord = RecordOf<SafeRecordProps>
|
export type SafeRecord = RecordOf<SafeRecordProps>
|
||||||
|
|
|
@ -1,30 +1,22 @@
|
||||||
import { Map, List } from 'immutable'
|
import { Map, List } from 'immutable'
|
||||||
import { Action, handleActions } from 'redux-actions'
|
import { Action, handleActions } from 'redux-actions'
|
||||||
|
|
||||||
import { ADD_SAFE_OWNER } from 'src/logic/safe/store/actions/addSafeOwner'
|
|
||||||
import { EDIT_SAFE_OWNER } from 'src/logic/safe/store/actions/editSafeOwner'
|
|
||||||
import { REMOVE_SAFE } from 'src/logic/safe/store/actions/removeSafe'
|
import { REMOVE_SAFE } from 'src/logic/safe/store/actions/removeSafe'
|
||||||
import { REMOVE_SAFE_OWNER } from 'src/logic/safe/store/actions/removeSafeOwner'
|
|
||||||
import { REPLACE_SAFE_OWNER } from 'src/logic/safe/store/actions/replaceSafeOwner'
|
|
||||||
import { SET_DEFAULT_SAFE } from 'src/logic/safe/store/actions/setDefaultSafe'
|
import { SET_DEFAULT_SAFE } from 'src/logic/safe/store/actions/setDefaultSafe'
|
||||||
import { SET_LATEST_MASTER_CONTRACT_VERSION } from 'src/logic/safe/store/actions/setLatestMasterContractVersion'
|
import { SET_LATEST_MASTER_CONTRACT_VERSION } from 'src/logic/safe/store/actions/setLatestMasterContractVersion'
|
||||||
import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe'
|
import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe'
|
||||||
import { makeOwner } from 'src/logic/safe/store/models/owner'
|
|
||||||
import makeSafe, { SafeRecord, SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
import makeSafe, { SafeRecord, SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||||
import { AppReduxState } from 'src/store'
|
import { AppReduxState } from 'src/store'
|
||||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||||
import { ADD_OR_UPDATE_SAFE, buildOwnersFrom } from 'src/logic/safe/store/actions/addOrUpdateSafe'
|
import { ADD_OR_UPDATE_SAFE } from 'src/logic/safe/store/actions/addOrUpdateSafe'
|
||||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||||
import { shouldSafeStoreBeUpdated } from 'src/logic/safe/utils/shouldSafeStoreBeUpdated'
|
import { shouldSafeStoreBeUpdated } from 'src/logic/safe/utils/shouldSafeStoreBeUpdated'
|
||||||
import { LOADED_SAFE_KEY } from 'src/utils/constants'
|
|
||||||
|
|
||||||
export const SAFE_REDUCER_ID = 'safes'
|
export const SAFE_REDUCER_ID = 'safes'
|
||||||
export const DEFAULT_SAFE_INITIAL_STATE = 'NOT_ASKED'
|
export const DEFAULT_SAFE_INITIAL_STATE = 'NOT_ASKED'
|
||||||
|
|
||||||
export const buildSafe = (storedSafe: SafeRecordProps): SafeRecordProps => {
|
export const buildSafe = (storedSafe: SafeRecordProps): SafeRecordProps => {
|
||||||
const names = storedSafe.owners.map((owner) => owner.name)
|
const owners = storedSafe.owners.map(checksumAddress)
|
||||||
const addresses = storedSafe.owners.map((owner) => checksumAddress(owner.address))
|
|
||||||
const owners = buildOwnersFrom(Array.from(names), Array.from(addresses))
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...storedSafe,
|
...storedSafe,
|
||||||
|
@ -82,15 +74,8 @@ export default handleActions<AppReduxState['safes'], Payloads>(
|
||||||
const shouldUpdate = shouldSafeStoreBeUpdated(safe, state.getIn(['safes', safeAddress]))
|
const shouldUpdate = shouldSafeStoreBeUpdated(safe, state.getIn(['safes', safeAddress]))
|
||||||
|
|
||||||
return shouldUpdate
|
return shouldUpdate
|
||||||
? state.updateIn(
|
? state.updateIn(['safes', safeAddress], makeSafe({ address: safeAddress }), (prevSafe) =>
|
||||||
['safes', safeAddress],
|
updateSafeProps(prevSafe, safe),
|
||||||
// This intermediate value is used as prevSafe if no previous state. Else is not used
|
|
||||||
makeSafe({
|
|
||||||
name: safe?.name || LOADED_SAFE_KEY,
|
|
||||||
address: safeAddress,
|
|
||||||
loadedViaUrl: !safe?.name || safe?.name === LOADED_SAFE_KEY,
|
|
||||||
}),
|
|
||||||
(prevSafe) => updateSafeProps(prevSafe, safe),
|
|
||||||
)
|
)
|
||||||
: state
|
: state
|
||||||
},
|
},
|
||||||
|
@ -104,15 +89,8 @@ export default handleActions<AppReduxState['safes'], Payloads>(
|
||||||
|
|
||||||
const shouldUpdate = shouldSafeStoreBeUpdated(safe, state.getIn(['safes', safeAddress]))
|
const shouldUpdate = shouldSafeStoreBeUpdated(safe, state.getIn(['safes', safeAddress]))
|
||||||
return shouldUpdate
|
return shouldUpdate
|
||||||
? state.updateIn(
|
? state.updateIn(['safes', safeAddress], makeSafe({ address: safeAddress }), (prevSafe) =>
|
||||||
['safes', safeAddress],
|
updateSafeProps(prevSafe, safe),
|
||||||
// This intermediate value is used as prevSafe if no previous state. Else is not used
|
|
||||||
makeSafe({
|
|
||||||
name: safe?.name || LOADED_SAFE_KEY,
|
|
||||||
address: safeAddress,
|
|
||||||
loadedViaUrl: !safe?.name || safe?.name === LOADED_SAFE_KEY,
|
|
||||||
}),
|
|
||||||
(prevSafe) => updateSafeProps(prevSafe, safe),
|
|
||||||
)
|
)
|
||||||
: state
|
: state
|
||||||
},
|
},
|
||||||
|
@ -128,54 +106,6 @@ export default handleActions<AppReduxState['safes'], Payloads>(
|
||||||
|
|
||||||
return newState
|
return newState
|
||||||
},
|
},
|
||||||
[ADD_SAFE_OWNER]: (state, action: Action<FullOwnerPayload>) => {
|
|
||||||
const { ownerAddress, ownerName, safeAddress } = action.payload
|
|
||||||
|
|
||||||
const addressFound = state
|
|
||||||
.getIn(['safes', safeAddress])
|
|
||||||
.owners.find((owner) => sameAddress(owner.address, ownerAddress))
|
|
||||||
|
|
||||||
if (addressFound) {
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
return state.updateIn(['safes', safeAddress], (prevSafe) =>
|
|
||||||
prevSafe.merge({
|
|
||||||
owners: prevSafe.owners.push(makeOwner({ address: ownerAddress, name: ownerName })),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
[REMOVE_SAFE_OWNER]: (state, action: Action<BaseOwnerPayload>) => {
|
|
||||||
const { ownerAddress, safeAddress } = action.payload
|
|
||||||
|
|
||||||
return state.updateIn(['safes', safeAddress], (prevSafe) =>
|
|
||||||
prevSafe.merge({
|
|
||||||
owners: prevSafe.owners.filter((o) => o.address.toLowerCase() !== ownerAddress.toLowerCase()),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
[REPLACE_SAFE_OWNER]: (state, action: Action<ReplaceOwnerPayload>) => {
|
|
||||||
const { oldOwnerAddress, ownerAddress, ownerName, safeAddress } = action.payload
|
|
||||||
|
|
||||||
return state.updateIn(['safes', safeAddress], (prevSafe) =>
|
|
||||||
prevSafe.merge({
|
|
||||||
owners: prevSafe.owners
|
|
||||||
.filter((o) => o.address.toLowerCase() !== oldOwnerAddress.toLowerCase())
|
|
||||||
.push(makeOwner({ address: ownerAddress, name: ownerName })),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
[EDIT_SAFE_OWNER]: (state, action: Action<FullOwnerPayload>) => {
|
|
||||||
const { ownerAddress, ownerName, safeAddress } = action.payload
|
|
||||||
|
|
||||||
return state.updateIn(['safes', safeAddress], (prevSafe) => {
|
|
||||||
const ownerToUpdateIndex = prevSafe.owners.findIndex(
|
|
||||||
(o) => o.address.toLowerCase() === ownerAddress.toLowerCase(),
|
|
||||||
)
|
|
||||||
const updatedOwners = prevSafe.owners.update(ownerToUpdateIndex, (owner) => owner.set('name', ownerName))
|
|
||||||
return prevSafe.merge({ owners: updatedOwners })
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[SET_DEFAULT_SAFE]: (state, action: Action<SafeRecord>) => state.set('defaultSafe', action.payload),
|
[SET_DEFAULT_SAFE]: (state, action: Action<SafeRecord>) => state.set('defaultSafe', action.payload),
|
||||||
[SET_LATEST_MASTER_CONTRACT_VERSION]: (state, action: Action<SafeRecord>) =>
|
[SET_LATEST_MASTER_CONTRACT_VERSION]: (state, action: Action<SafeRecord>) =>
|
||||||
state.set('latestMasterContractVersion', action.payload),
|
state.set('latestMasterContractVersion', action.payload),
|
||||||
|
|
|
@ -1,15 +1,20 @@
|
||||||
import { List } from 'immutable'
|
import { List } from 'immutable'
|
||||||
import { matchPath, RouteComponentProps } from 'react-router-dom'
|
import { matchPath, RouteComponentProps } from 'react-router-dom'
|
||||||
import { createSelector } from 'reselect'
|
import { createSelector } from 'reselect'
|
||||||
import { SAFELIST_ADDRESS, SAFE_PARAM_ADDRESS } from 'src/routes/routes'
|
|
||||||
|
|
||||||
|
import { getNetworkId } from 'src/config'
|
||||||
import { SAFE_REDUCER_ID } from 'src/logic/safe/store/reducer/safe'
|
import { SAFE_REDUCER_ID } from 'src/logic/safe/store/reducer/safe'
|
||||||
import { AppReduxState } from 'src/store'
|
import { AppReduxState } from 'src/store'
|
||||||
|
|
||||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||||
import makeSafe, { SafeRecord, SafeRecordProps } from '../models/safe'
|
import { ETHEREUM_NETWORK } from 'src/config/networks/network'
|
||||||
|
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||||
|
import { addressBookMapSelector, addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||||
|
import makeSafe, { SafeRecord, SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||||
|
import { SAFELIST_ADDRESS, SAFE_PARAM_ADDRESS } from 'src/routes/routes'
|
||||||
import { SafesMap } from 'src/routes/safe/store/reducer/types/safe'
|
import { SafesMap } from 'src/routes/safe/store/reducer/types/safe'
|
||||||
import { BalanceRecord } from 'src/logic/tokens/store/actions/fetchSafeTokens'
|
import { BalanceRecord } from 'src/logic/tokens/store/actions/fetchSafeTokens'
|
||||||
|
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||||
|
|
||||||
const safesStateSelector = (state: AppReduxState) => state[SAFE_REDUCER_ID]
|
const safesStateSelector = (state: AppReduxState) => state[SAFE_REDUCER_ID]
|
||||||
|
|
||||||
|
@ -17,6 +22,32 @@ export const safesMapSelector = (state: AppReduxState): SafesMap => safesStateSe
|
||||||
|
|
||||||
export const safesListSelector = createSelector(safesMapSelector, (safes): List<SafeRecord> => safes.toList())
|
export const safesListSelector = createSelector(safesMapSelector, (safes): List<SafeRecord> => safes.toList())
|
||||||
|
|
||||||
|
const chainId = getNetworkId()
|
||||||
|
|
||||||
|
type SafeRecordWithName = SafeRecordProps & { name: string }
|
||||||
|
|
||||||
|
export const safesListWithAddressBookNameSelector = createSelector(
|
||||||
|
[safesListSelector, addressBookMapSelector],
|
||||||
|
(safesList, addressBookMap): List<SafeRecordWithName> => {
|
||||||
|
const addressBook = addressBookMap?.[chainId]
|
||||||
|
|
||||||
|
return safesList
|
||||||
|
.filter((safeRecord) => !safeRecord.loadedViaUrl)
|
||||||
|
.map((safeRecord) => {
|
||||||
|
const safe = safeRecord.toObject()
|
||||||
|
const name = addressBook?.[safe.address]?.name ?? ''
|
||||||
|
return { ...safe, name }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export const safeNameSelector = createSelector(
|
||||||
|
[safesListWithAddressBookNameSelector, (_, safeName: string) => safeName],
|
||||||
|
(safes, safeAddress): string => {
|
||||||
|
return safes.find((safe) => sameAddress(safe.address, safeAddress))?.name ?? ''
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
export const safesCountSelector = createSelector(safesMapSelector, (safes) => safes.size)
|
export const safesCountSelector = createSelector(safesMapSelector, (safes) => safes.size)
|
||||||
|
|
||||||
export const defaultSafeSelector = createSelector(safesStateSelector, (safeState) => safeState.get('defaultSafe'))
|
export const defaultSafeSelector = createSelector(safesStateSelector, (safeState) => safeState.get('defaultSafe'))
|
||||||
|
@ -83,8 +114,6 @@ export const safeFieldSelector = <K extends keyof SafeRecordProps>(field: K) =>
|
||||||
safe: SafeRecord,
|
safe: SafeRecord,
|
||||||
): SafeRecordProps[K] | undefined => (safe ? safe.get(field, baseSafe.get(field)) : undefined)
|
): SafeRecordProps[K] | undefined => (safe ? safe.get(field, baseSafe.get(field)) : undefined)
|
||||||
|
|
||||||
export const safeNameSelector = createSelector(safeSelector, safeFieldSelector('name'))
|
|
||||||
|
|
||||||
export const safeEthBalanceSelector = createSelector(safeSelector, safeFieldSelector('ethBalance'))
|
export const safeEthBalanceSelector = createSelector(safeSelector, safeFieldSelector('ethBalance'))
|
||||||
|
|
||||||
export const safeNeedsUpdateSelector = createSelector(safeSelector, safeFieldSelector('needsUpdate'))
|
export const safeNeedsUpdateSelector = createSelector(safeSelector, safeFieldSelector('needsUpdate'))
|
||||||
|
@ -103,19 +132,24 @@ export const safeFeaturesEnabledSelector = createSelector(safeSelector, safeFiel
|
||||||
|
|
||||||
export const safeSpendingLimitsSelector = createSelector(safeSelector, safeFieldSelector('spendingLimits'))
|
export const safeSpendingLimitsSelector = createSelector(safeSelector, safeFieldSelector('spendingLimits'))
|
||||||
|
|
||||||
export const safeLoadedViaUrlSelector = createSelector(safeSelector, safeFieldSelector('loadedViaUrl'))
|
|
||||||
|
|
||||||
export const safeOwnersAddressesListSelector = createSelector(
|
|
||||||
safeOwnersSelector,
|
|
||||||
(owners): List<string> => {
|
|
||||||
if (!owners) {
|
|
||||||
return List([])
|
|
||||||
}
|
|
||||||
|
|
||||||
return owners?.map(({ address }) => address)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
export const safeTotalFiatBalanceSelector = createSelector(safeSelector, (currentSafe) => {
|
export const safeTotalFiatBalanceSelector = createSelector(safeSelector, (currentSafe) => {
|
||||||
return currentSafe?.totalFiatBalance
|
return currentSafe?.totalFiatBalance
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const safeOwnersWithAddressBookDataSelector = createSelector(
|
||||||
|
[safeOwnersSelector, addressBookSelector, (_, chainId: ETHEREUM_NETWORK) => chainId],
|
||||||
|
(owners, addressBook, chainId): AppReduxState['addressBook'] | undefined =>
|
||||||
|
owners?.map((ownerAddress) => {
|
||||||
|
const ownerInAddressBook = addressBook.find(
|
||||||
|
(addressBookEntry) =>
|
||||||
|
sameAddress(ownerAddress, addressBookEntry.address) && chainId === addressBookEntry.chainId,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (ownerInAddressBook) {
|
||||||
|
return ownerInAddressBook
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there's no owner's data in the AB, we create an in-memory AB-like structure
|
||||||
|
return makeAddressBookEntry({ address: ownerAddress, name: '' })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
|
@ -8,7 +8,9 @@ export const TX_NOTIFICATION_TYPES = {
|
||||||
REMOVE_SPENDING_LIMIT_TX: 'REMOVE_SPENDING_LIMIT_TX',
|
REMOVE_SPENDING_LIMIT_TX: 'REMOVE_SPENDING_LIMIT_TX',
|
||||||
SAFE_NAME_CHANGE_TX: 'SAFE_NAME_CHANGE_TX',
|
SAFE_NAME_CHANGE_TX: 'SAFE_NAME_CHANGE_TX',
|
||||||
OWNER_NAME_CHANGE_TX: 'OWNER_NAME_CHANGE_TX',
|
OWNER_NAME_CHANGE_TX: 'OWNER_NAME_CHANGE_TX',
|
||||||
ADDRESSBOOK_NEW_ENTRY: 'ADDRESSBOOK_NEW_ENTRY',
|
ADDRESS_BOOK_NEW_ENTRY: 'ADDRESS_BOOK_NEW_ENTRY',
|
||||||
ADDRESSBOOK_EDIT_ENTRY: 'ADDRESSBOOK_EDIT_ENTRY',
|
ADDRESS_BOOK_EDIT_ENTRY: 'ADDRESS_BOOK_EDIT_ENTRY',
|
||||||
ADDRESSBOOK_DELETE_ENTRY: 'ADDRESSBOOK_DELETE_ENTRY',
|
ADDRESS_BOOK_DELETE_ENTRY: 'ADDRESS_BOOK_DELETE_ENTRY',
|
||||||
|
ADDRESS_BOOK_EXPORT_ENTRIES: 'ADDRESS_BOOK_EXPORT_ENTRIES',
|
||||||
|
ADDRESS_BOOK_IMPORT_ENTRIES: 'ADDRESS_BOOK_IMPORT_ENTRIES',
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,28 +12,20 @@ const getMockedOldSafe = ({
|
||||||
currentVersion,
|
currentVersion,
|
||||||
ethBalance,
|
ethBalance,
|
||||||
threshold,
|
threshold,
|
||||||
name,
|
|
||||||
nonce,
|
nonce,
|
||||||
modules,
|
modules,
|
||||||
spendingLimits,
|
spendingLimits,
|
||||||
}: Partial<SafeRecordProps>): SafeRecordProps => {
|
}: Partial<SafeRecordProps>): SafeRecordProps => {
|
||||||
const owner1 = {
|
const owner1 = '0x3bE3c2dE077FBC409ae50AFFA66a94a9aE669A8d'
|
||||||
name: 'MockedOwner1',
|
const owner2 = '0xA2366b0c2607de70777d87aCdD1D22F0708fA6a3'
|
||||||
address: '0x3bE3c2dE077FBC409ae50AFFA66a94a9aE669A8d',
|
|
||||||
}
|
|
||||||
const owner2 = {
|
|
||||||
name: 'MockedOwner2',
|
|
||||||
address: '0xA2366b0c2607de70777d87aCdD1D22F0708fA6a3',
|
|
||||||
}
|
|
||||||
const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1'
|
const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1'
|
||||||
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
|
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: name || 'MockedSafe',
|
|
||||||
address: address || '0xAE173F30ec9A293d37c44BA68d3fCD35F989Ce9F',
|
address: address || '0xAE173F30ec9A293d37c44BA68d3fCD35F989Ce9F',
|
||||||
threshold: threshold || 2,
|
threshold: threshold || 2,
|
||||||
ethBalance: ethBalance || '10',
|
ethBalance: ethBalance || '10',
|
||||||
owners: owners || List([owner1, owner2]),
|
owners: owners || [owner1, owner2],
|
||||||
modules: modules || [],
|
modules: modules || [],
|
||||||
spendingLimits: spendingLimits || [],
|
spendingLimits: spendingLimits || [],
|
||||||
balances: balances || [
|
balances: balances || [
|
||||||
|
@ -46,6 +38,7 @@ const getMockedOldSafe = ({
|
||||||
needsUpdate: needsUpdate || false,
|
needsUpdate: needsUpdate || false,
|
||||||
featuresEnabled: featuresEnabled || [],
|
featuresEnabled: featuresEnabled || [],
|
||||||
totalFiatBalance: '110',
|
totalFiatBalance: '110',
|
||||||
|
loadedViaUrl: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,21 +68,6 @@ describe('shouldSafeStoreBeUpdated', () => {
|
||||||
// Then
|
// Then
|
||||||
expect(expectedResult).toEqual(true)
|
expect(expectedResult).toEqual(true)
|
||||||
})
|
})
|
||||||
it(`Given an old safe and a new name for the safe, should return true`, () => {
|
|
||||||
// given
|
|
||||||
const oldName = 'oldName'
|
|
||||||
const newName = 'newName'
|
|
||||||
const oldSafe = getMockedOldSafe({ name: oldName })
|
|
||||||
const newSafeProps: Partial<SafeRecordProps> = {
|
|
||||||
name: newName,
|
|
||||||
}
|
|
||||||
|
|
||||||
// When
|
|
||||||
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
|
||||||
|
|
||||||
// Then
|
|
||||||
expect(expectedResult).toEqual(true)
|
|
||||||
})
|
|
||||||
it(`Given an old safe and a new threshold for the safe, should return true`, () => {
|
it(`Given an old safe and a new threshold for the safe, should return true`, () => {
|
||||||
// given
|
// given
|
||||||
const oldThreshold = 1
|
const oldThreshold = 1
|
||||||
|
@ -122,18 +100,10 @@ describe('shouldSafeStoreBeUpdated', () => {
|
||||||
})
|
})
|
||||||
it(`Given an old owners list and a new owners list for the safe, should return true`, () => {
|
it(`Given an old owners list and a new owners list for the safe, should return true`, () => {
|
||||||
// given
|
// given
|
||||||
const owner1 = {
|
const owner1 = '0x3bE3c2dE077FBC409ae50AFFA66a94a9aE669A8d'
|
||||||
name: 'MockedOwner1',
|
const owner2 = '0xA2366b0c2607de70777d87aCdD1D22F0708fA6a3'
|
||||||
address: '0x3bE3c2dE077FBC409ae50AFFA66a94a9aE669A8d',
|
const oldSafe = getMockedOldSafe({ owners: [owner1, owner2] })
|
||||||
}
|
const newSafeProps: Partial<SafeRecordProps> = { owners: [owner1] }
|
||||||
const owner2 = {
|
|
||||||
name: 'MockedOwner2',
|
|
||||||
address: '0xA2366b0c2607de70777d87aCdD1D22F0708fA6a3',
|
|
||||||
}
|
|
||||||
const oldSafe = getMockedOldSafe({ owners: List([owner1, owner2]) })
|
|
||||||
const newSafeProps: Partial<SafeRecordProps> = {
|
|
||||||
owners: List([owner1]),
|
|
||||||
}
|
|
||||||
|
|
||||||
// When
|
// When
|
||||||
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
||||||
|
@ -146,9 +116,7 @@ describe('shouldSafeStoreBeUpdated', () => {
|
||||||
const oldModulesList = []
|
const oldModulesList = []
|
||||||
const newModulesList = null
|
const newModulesList = null
|
||||||
const oldSafe = getMockedOldSafe({ modules: oldModulesList })
|
const oldSafe = getMockedOldSafe({ modules: oldModulesList })
|
||||||
const newSafeProps: Partial<SafeRecordProps> = {
|
const newSafeProps: Partial<SafeRecordProps> = { modules: newModulesList }
|
||||||
modules: newModulesList,
|
|
||||||
}
|
|
||||||
|
|
||||||
// When
|
// When
|
||||||
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
||||||
|
@ -161,9 +129,7 @@ describe('shouldSafeStoreBeUpdated', () => {
|
||||||
const oldSpendingLimitsList = []
|
const oldSpendingLimitsList = []
|
||||||
const newSpendingLimitsList = null
|
const newSpendingLimitsList = null
|
||||||
const oldSafe = getMockedOldSafe({ spendingLimits: oldSpendingLimitsList })
|
const oldSafe = getMockedOldSafe({ spendingLimits: oldSpendingLimitsList })
|
||||||
const newSafeProps: Partial<SafeRecordProps> = {
|
const newSafeProps: Partial<SafeRecordProps> = { modules: newSpendingLimitsList }
|
||||||
modules: newSpendingLimitsList,
|
|
||||||
}
|
|
||||||
|
|
||||||
// When
|
// When
|
||||||
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||||
export const SAFES_KEY = 'SAFES'
|
export const SAFES_KEY = 'SAFES'
|
||||||
export const DEFAULT_SAFE_KEY = 'DEFAULT_SAFE'
|
export const DEFAULT_SAFE_KEY = 'DEFAULT_SAFE'
|
||||||
|
|
||||||
type StoredSafes = Record<string, SafeRecordProps>
|
export type StoredSafes = Record<string, SafeRecordProps>
|
||||||
|
|
||||||
export const loadStoredSafes = (): Promise<StoredSafes | undefined> => {
|
export const loadStoredSafes = (): Promise<StoredSafes | undefined> => {
|
||||||
return loadFromStorage<StoredSafes>(SAFES_KEY)
|
return loadFromStorage<StoredSafes>(SAFES_KEY)
|
||||||
|
@ -18,6 +18,11 @@ export const saveSafes = async (safes: StoredSafes): Promise<void> => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getLocalSafes = async (): Promise<SafeRecordProps[] | undefined> => {
|
||||||
|
const storedSafes = await loadStoredSafes()
|
||||||
|
return storedSafes ? Object.values(storedSafes) : undefined
|
||||||
|
}
|
||||||
|
|
||||||
export const getLocalSafe = async (safeAddress: string): Promise<SafeRecordProps | undefined> => {
|
export const getLocalSafe = async (safeAddress: string): Promise<SafeRecordProps | undefined> => {
|
||||||
const storedSafes = await loadStoredSafes()
|
const storedSafes = await loadStoredSafes()
|
||||||
return storedSafes?.[safeAddress]
|
return storedSafes?.[safeAddress]
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||||
const isStateSubset = (superObj, subObj) => {
|
const isStateSubset = (superObj, subObj) => {
|
||||||
return Object.keys(subObj).every((key) => {
|
return Object.keys(subObj).every((key) => {
|
||||||
if (subObj[key] && typeof subObj[key] == 'object') {
|
if (subObj[key] && typeof subObj[key] == 'object') {
|
||||||
if (typeof subObj[key] === 'object' || subObj[key].length >= 0) {
|
if (typeof subObj[key] === 'object' || subObj[key].size >= 0) {
|
||||||
// If type is Immutable Map, List or Object we use Immutable equals
|
// If type is Immutable Map, List or Object we use Immutable equals
|
||||||
return isEqual(superObj[key], subObj[key])
|
return isEqual(superObj[key], subObj[key])
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@ import {
|
||||||
shortVersionOf,
|
shortVersionOf,
|
||||||
} from 'src/logic/wallets/ethAddresses'
|
} from 'src/logic/wallets/ethAddresses'
|
||||||
import makeSafe from 'src/logic/safe/store/models/safe'
|
import makeSafe from 'src/logic/safe/store/models/safe'
|
||||||
import { makeOwner } from 'src/logic/safe/store/models/owner'
|
|
||||||
|
|
||||||
describe('src/logic/wallets/ethAddresses', () => {
|
describe('src/logic/wallets/ethAddresses', () => {
|
||||||
describe('Utility function: sameAddress', () => {
|
describe('Utility function: sameAddress', () => {
|
||||||
|
@ -113,7 +112,7 @@ describe('src/logic/wallets/ethAddresses', () => {
|
||||||
it("Should return false if there's no `userAccount`", () => {
|
it("Should return false if there's no `userAccount`", () => {
|
||||||
// given
|
// given
|
||||||
const userAddress = null
|
const userAddress = null
|
||||||
const owners = List([makeOwner({ address: userAddress })])
|
const owners = [userAddress]
|
||||||
const safeInstance = makeSafe({ owners })
|
const safeInstance = makeSafe({ owners })
|
||||||
const expectedResult = false
|
const expectedResult = false
|
||||||
|
|
||||||
|
@ -139,7 +138,7 @@ describe('src/logic/wallets/ethAddresses', () => {
|
||||||
it("Should return true if `userAccount` is not in the list of Safe's owners", () => {
|
it("Should return true if `userAccount` is not in the list of Safe's owners", () => {
|
||||||
// given
|
// given
|
||||||
const userAddress = 'address1'
|
const userAddress = 'address1'
|
||||||
const owners = List([makeOwner({ address: userAddress })])
|
const owners = [userAddress]
|
||||||
const safeInstance = makeSafe({ owners })
|
const safeInstance = makeSafe({ owners })
|
||||||
const expectedResult = true
|
const expectedResult = true
|
||||||
|
|
||||||
|
@ -153,7 +152,7 @@ describe('src/logic/wallets/ethAddresses', () => {
|
||||||
// given
|
// given
|
||||||
const userAddress = 'address1'
|
const userAddress = 'address1'
|
||||||
const userAddress2 = 'address2'
|
const userAddress2 = 'address2'
|
||||||
const owners = List([makeOwner({ address: userAddress })])
|
const owners = [userAddress]
|
||||||
const safeInstance = makeSafe({ owners })
|
const safeInstance = makeSafe({ owners })
|
||||||
const expectedResult = false
|
const expectedResult = false
|
||||||
|
|
||||||
|
@ -170,8 +169,8 @@ describe('src/logic/wallets/ethAddresses', () => {
|
||||||
// given
|
// given
|
||||||
const userAddress = 'address1'
|
const userAddress = 'address1'
|
||||||
const userAddress2 = 'address2'
|
const userAddress2 = 'address2'
|
||||||
const owners1 = List([makeOwner({ address: userAddress })])
|
const owners1 = [userAddress]
|
||||||
const owners2 = List([makeOwner({ address: userAddress2 })])
|
const owners2 = [userAddress2]
|
||||||
const safeInstance = makeSafe({ owners: owners1 })
|
const safeInstance = makeSafe({ owners: owners1 })
|
||||||
const safeInstance2 = makeSafe({ owners: owners2 })
|
const safeInstance2 = makeSafe({ owners: owners2 })
|
||||||
const safesList = List([safeInstance, safeInstance2])
|
const safesList = List([safeInstance, safeInstance2])
|
||||||
|
@ -188,8 +187,8 @@ describe('src/logic/wallets/ethAddresses', () => {
|
||||||
const userAddress = 'address1'
|
const userAddress = 'address1'
|
||||||
const userAddress2 = 'address2'
|
const userAddress2 = 'address2'
|
||||||
const userAddress3 = 'address3'
|
const userAddress3 = 'address3'
|
||||||
const owners1 = List([makeOwner({ address: userAddress3 })])
|
const owners1 = [userAddress3]
|
||||||
const owners2 = List([makeOwner({ address: userAddress2 })])
|
const owners2 = [userAddress2]
|
||||||
const safeInstance = makeSafe({ owners: owners1 })
|
const safeInstance = makeSafe({ owners: owners1 })
|
||||||
const safeInstance2 = makeSafe({ owners: owners2 })
|
const safeInstance2 = makeSafe({ owners: owners2 })
|
||||||
const safesList = List([safeInstance, safeInstance2])
|
const safesList = List([safeInstance, safeInstance2])
|
||||||
|
|
|
@ -40,7 +40,7 @@ export const isUserAnOwner = (safe: SafeRecord, userAccount: string): boolean =>
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return owners.find((owner) => sameAddress(owner.address, userAccount)) !== undefined
|
return owners.find((address) => sameAddress(address, userAccount)) !== undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isUserAnOwnerOfAnySafe = (safes: List<SafeRecord> | SafeRecord[], userAccount: string): boolean =>
|
export const isUserAnOwnerOfAnySafe = (safes: List<SafeRecord> | SafeRecord[], userAccount: string): boolean =>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import InputAdornment from '@material-ui/core/InputAdornment'
|
import InputAdornment from '@material-ui/core/InputAdornment'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import CheckCircle from '@material-ui/icons/CheckCircle'
|
import CheckCircle from '@material-ui/icons/CheckCircle'
|
||||||
import * as React from 'react'
|
import React, { ReactElement, ReactNode } from 'react'
|
||||||
import { FormApi } from 'final-form'
|
import { FormApi } from 'final-form'
|
||||||
|
|
||||||
import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
|
import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
|
||||||
|
@ -15,7 +15,7 @@ import {
|
||||||
noErrorsOn,
|
noErrorsOn,
|
||||||
required,
|
required,
|
||||||
composeValidators,
|
composeValidators,
|
||||||
minMaxLength,
|
validAddressBookName,
|
||||||
} from 'src/components/forms/validator'
|
} from 'src/components/forms/validator'
|
||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
import Col from 'src/components/layout/Col'
|
import Col from 'src/components/layout/Col'
|
||||||
|
@ -68,7 +68,7 @@ interface DetailsFormProps {
|
||||||
form: FormApi
|
form: FormApi
|
||||||
}
|
}
|
||||||
|
|
||||||
const DetailsForm = ({ errors, form }: DetailsFormProps): React.ReactElement => {
|
const DetailsForm = ({ errors, form }: DetailsFormProps): ReactElement => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
const handleScan = (value: string, closeQrModal: () => void): void => {
|
const handleScan = (value: string, closeQrModal: () => void): void => {
|
||||||
|
@ -92,10 +92,10 @@ const DetailsForm = ({ errors, form }: DetailsFormProps): React.ReactElement =>
|
||||||
<Field
|
<Field
|
||||||
component={TextField}
|
component={TextField}
|
||||||
name={FIELD_LOAD_NAME}
|
name={FIELD_LOAD_NAME}
|
||||||
placeholder="Name of the Safe"
|
placeholder="Name of the Safe*"
|
||||||
text="Safe name"
|
text="Safe name"
|
||||||
type="text"
|
type="text"
|
||||||
validate={composeValidators(required, minMaxLength(1, 50))}
|
validate={composeValidators(required, validAddressBookName)}
|
||||||
testId="load-safe-name-field"
|
testId="load-safe-name-field"
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -145,13 +145,11 @@ const DetailsForm = ({ errors, form }: DetailsFormProps): React.ReactElement =>
|
||||||
}
|
}
|
||||||
|
|
||||||
const DetailsPage = () =>
|
const DetailsPage = () =>
|
||||||
function LoadSafeDetails(controls: React.ReactNode, { errors, form }: StepperPageFormProps): React.ReactElement {
|
function LoadSafeDetails(controls: ReactNode, { errors, form }: StepperPageFormProps): ReactElement {
|
||||||
return (
|
return (
|
||||||
<>
|
<OpenPaper controls={controls}>
|
||||||
<OpenPaper controls={controls}>
|
<DetailsForm errors={errors} form={form} />
|
||||||
<DetailsForm errors={errors} form={form} />
|
</OpenPaper>
|
||||||
</OpenPaper>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import IconButton from '@material-ui/core/IconButton'
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
import ChevronLeft from '@material-ui/icons/ChevronLeft'
|
import ChevronLeft from '@material-ui/icons/ChevronLeft'
|
||||||
import * as React from 'react'
|
import React, { ReactElement } from 'react'
|
||||||
|
|
||||||
import Stepper, { StepperPage } from 'src/components/Stepper'
|
import Stepper, { StepperPage } from 'src/components/Stepper'
|
||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
|
@ -34,13 +34,12 @@ const formMutators = {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
network: string
|
|
||||||
provider?: string
|
provider?: string
|
||||||
userAddress: string
|
userAddress: string
|
||||||
onLoadSafeSubmit: (values: LoadFormValues) => void
|
onLoadSafeSubmit: (values: LoadFormValues) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const Layout = ({ network, onLoadSafeSubmit, provider, userAddress }: LayoutProps): React.ReactElement => (
|
const Layout = ({ onLoadSafeSubmit, provider, userAddress }: LayoutProps): ReactElement => (
|
||||||
<>
|
<>
|
||||||
{provider ? (
|
{provider ? (
|
||||||
<Block>
|
<Block>
|
||||||
|
@ -58,8 +57,8 @@ const Layout = ({ network, onLoadSafeSubmit, provider, userAddress }: LayoutProp
|
||||||
testId="load-safe-form"
|
testId="load-safe-form"
|
||||||
>
|
>
|
||||||
<StepperPage validate={safeFieldsValidation} component={DetailsForm} />
|
<StepperPage validate={safeFieldsValidation} component={DetailsForm} />
|
||||||
<StepperPage network={network} component={OwnerList} />
|
<StepperPage component={OwnerList} />
|
||||||
<StepperPage network={network} userAddress={userAddress} component={ReviewInformation} />
|
<StepperPage userAddress={userAddress} component={ReviewInformation} />
|
||||||
</Stepper>
|
</Stepper>
|
||||||
</Block>
|
</Block>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
|
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import TableContainer from '@material-ui/core/TableContainer'
|
import TableContainer from '@material-ui/core/TableContainer'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { ReactElement, ReactNode, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
|
import { getExplorerInfo } from 'src/config'
|
||||||
import Field from 'src/components/forms/Field'
|
import Field from 'src/components/forms/Field'
|
||||||
import TextField from 'src/components/forms/TextField'
|
import TextField from 'src/components/forms/TextField'
|
||||||
import { composeValidators, minMaxLength, required } from 'src/components/forms/validator'
|
import { minMaxLength } from 'src/components/forms/validator'
|
||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
import Col from 'src/components/layout/Col'
|
import Col from 'src/components/layout/Col'
|
||||||
import Hairline from 'src/components/layout/Hairline'
|
import Hairline from 'src/components/layout/Hairline'
|
||||||
|
@ -21,8 +22,7 @@ import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||||
import { FIELD_LOAD_ADDRESS, THRESHOLD } from 'src/routes/load/components/fields'
|
import { FIELD_LOAD_ADDRESS, THRESHOLD } from 'src/routes/load/components/fields'
|
||||||
import { getOwnerAddressBy, getOwnerNameBy } from 'src/routes/open/components/fields'
|
import { getOwnerAddressBy, getOwnerNameBy } from 'src/routes/open/components/fields'
|
||||||
import { styles } from './styles'
|
import { styles } from './styles'
|
||||||
import { getExplorerInfo } from 'src/config'
|
import { LoadFormValues } from 'src/routes/load/container/Load'
|
||||||
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
|
||||||
|
|
||||||
const calculateSafeValues = (owners, threshold, values) => {
|
const calculateSafeValues = (owners, threshold, values) => {
|
||||||
const initialValues = { ...values }
|
const initialValues = { ...values }
|
||||||
|
@ -41,10 +41,14 @@ const useAddressBookForOwnersNames = (ownersList: string[]): AddressBookEntry[]
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const OwnerListComponent = (props) => {
|
interface OwnerListComponentProps {
|
||||||
|
values: LoadFormValues
|
||||||
|
updateInitialProps: (initialValues) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const OwnerListComponent = ({ values, updateInitialProps }: OwnerListComponentProps): ReactElement => {
|
||||||
const [owners, setOwners] = useState<string[]>([])
|
const [owners, setOwners] = useState<string[]>([])
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const { updateInitialProps, values } = props
|
|
||||||
|
|
||||||
const ownersWithNames = useAddressBookForOwnersNames(owners)
|
const ownersWithNames = useAddressBookForOwnersNames(owners)
|
||||||
|
|
||||||
|
@ -88,19 +92,18 @@ const OwnerListComponent = (props) => {
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Block margin="md" padding="md">
|
<Block margin="md" padding="md">
|
||||||
{ownersWithNames.map(({ address, name }, index) => {
|
{ownersWithNames.map(({ address, name }, index) => {
|
||||||
const ownerName = name || `Owner #${index + 1}`
|
|
||||||
return (
|
return (
|
||||||
<Row className={classes.owner} key={address} data-testid="owner-row">
|
<Row className={classes.owner} key={address} data-testid="owner-row">
|
||||||
<Col className={classes.ownerName} xs={4}>
|
<Col className={classes.ownerName} xs={4}>
|
||||||
<Field
|
<Field
|
||||||
className={classes.name}
|
className={classes.name}
|
||||||
component={TextField}
|
component={TextField}
|
||||||
initialValue={ownerName}
|
initialValue={name}
|
||||||
name={getOwnerNameBy(index)}
|
name={getOwnerNameBy(index)}
|
||||||
placeholder="Owner Name*"
|
placeholder="Owner Name"
|
||||||
text="Owner Name"
|
text="Owner Name"
|
||||||
type="text"
|
type="text"
|
||||||
validate={composeValidators(required, minMaxLength(1, 50))}
|
validate={minMaxLength(0, 50)}
|
||||||
testId={`load-safe-owner-name-${index}`}
|
testId={`load-safe-owner-name-${index}`}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -118,14 +121,12 @@ const OwnerListComponent = (props) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const OwnerList = ({ updateInitialProps }, network) =>
|
const OwnerList = ({ updateInitialProps }) =>
|
||||||
function LoadSafeOwnerList(controls, { values }): React.ReactElement {
|
function LoadSafeOwnerList(controls: ReactNode, { values }: { values: LoadFormValues }): ReactElement {
|
||||||
return (
|
return (
|
||||||
<>
|
<OpenPaper controls={controls} padding={false}>
|
||||||
<OpenPaper controls={controls} padding={false}>
|
<OwnerListComponent updateInitialProps={updateInitialProps} values={values} />
|
||||||
<OwnerListComponent network={network} updateInitialProps={updateInitialProps} values={values} />
|
</OpenPaper>
|
||||||
</OpenPaper>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
||||||
import TableContainer from '@material-ui/core/TableContainer'
|
import TableContainer from '@material-ui/core/TableContainer'
|
||||||
import React from 'react'
|
import React, { ReactElement, ReactNode } from 'react'
|
||||||
|
|
||||||
|
import { getExplorerInfo } from 'src/config'
|
||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
import Col from 'src/components/layout/Col'
|
import Col from 'src/components/layout/Col'
|
||||||
import Hairline from 'src/components/layout/Hairline'
|
import Hairline from 'src/components/layout/Hairline'
|
||||||
|
@ -11,8 +13,6 @@ import { FIELD_LOAD_ADDRESS, FIELD_LOAD_NAME, THRESHOLD } from 'src/routes/load/
|
||||||
import { getNumOwnersFrom, getOwnerAddressBy, getOwnerNameBy } from 'src/routes/open/components/fields'
|
import { getNumOwnersFrom, getOwnerAddressBy, getOwnerNameBy } from 'src/routes/open/components/fields'
|
||||||
import { getAccountsFrom } from 'src/routes/open/utils/safeDataExtractor'
|
import { getAccountsFrom } from 'src/routes/open/utils/safeDataExtractor'
|
||||||
import { useStyles } from './styles'
|
import { useStyles } from './styles'
|
||||||
import { getExplorerInfo } from 'src/config'
|
|
||||||
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
|
||||||
import { LoadFormValues } from 'src/routes/load/container/Load'
|
import { LoadFormValues } from 'src/routes/load/container/Load'
|
||||||
|
|
||||||
const checkIfUserAddressIsAnOwner = (values: LoadFormValues, userAddress: string): boolean => {
|
const checkIfUserAddressIsAnOwner = (values: LoadFormValues, userAddress: string): boolean => {
|
||||||
|
@ -33,108 +33,104 @@ interface Props {
|
||||||
values: LoadFormValues
|
values: LoadFormValues
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReviewComponent = ({ userAddress, values }: Props): React.ReactElement => {
|
const ReviewComponent = ({ userAddress, values }: Props): ReactElement => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const isOwner = checkIfUserAddressIsAnOwner(values, userAddress)
|
const isOwner = checkIfUserAddressIsAnOwner(values, userAddress)
|
||||||
const owners = getAccountsFrom(values)
|
const owners = getAccountsFrom(values)
|
||||||
const safeAddress = values[FIELD_LOAD_ADDRESS]
|
const safeAddress = values[FIELD_LOAD_ADDRESS]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Row className={classes.root}>
|
||||||
<Row className={classes.root}>
|
<Col className={classes.detailsColumn} layout="column" xs={4}>
|
||||||
<Col className={classes.detailsColumn} layout="column" xs={4}>
|
<Block className={classes.details}>
|
||||||
<Block className={classes.details}>
|
<Block margin="lg">
|
||||||
<Block margin="lg">
|
<Paragraph color="primary" noMargin size="lg" data-testid="load-safe-step-three">
|
||||||
<Paragraph color="primary" noMargin size="lg" data-testid="load-safe-step-three">
|
Review details
|
||||||
Review details
|
</Paragraph>
|
||||||
</Paragraph>
|
|
||||||
</Block>
|
|
||||||
<Block margin="lg">
|
|
||||||
<Paragraph color="disabled" noMargin size="sm">
|
|
||||||
Name of the Safe
|
|
||||||
</Paragraph>
|
|
||||||
<Paragraph
|
|
||||||
className={classes.name}
|
|
||||||
color="primary"
|
|
||||||
noMargin
|
|
||||||
size="lg"
|
|
||||||
weight="bolder"
|
|
||||||
data-testid="load-form-review-safe-name"
|
|
||||||
>
|
|
||||||
{values[FIELD_LOAD_NAME]}
|
|
||||||
</Paragraph>
|
|
||||||
</Block>
|
|
||||||
<Block margin="lg">
|
|
||||||
<Paragraph color="disabled" noMargin size="sm">
|
|
||||||
Safe address
|
|
||||||
</Paragraph>
|
|
||||||
<Row className={classes.container}>
|
|
||||||
<EthHashInfo
|
|
||||||
hash={safeAddress}
|
|
||||||
shortenHash={4}
|
|
||||||
showAvatar
|
|
||||||
showCopyBtn
|
|
||||||
explorerUrl={getExplorerInfo(safeAddress)}
|
|
||||||
/>
|
|
||||||
</Row>
|
|
||||||
</Block>
|
|
||||||
<Block margin="lg">
|
|
||||||
<Paragraph color="disabled" noMargin size="sm">
|
|
||||||
Connected wallet client is owner?
|
|
||||||
</Paragraph>
|
|
||||||
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
|
|
||||||
{isOwner ? 'Yes' : 'No (read-only)'}
|
|
||||||
</Paragraph>
|
|
||||||
</Block>
|
|
||||||
<Block margin="lg">
|
|
||||||
<Paragraph color="disabled" noMargin size="sm">
|
|
||||||
Any transaction requires the confirmation of:
|
|
||||||
</Paragraph>
|
|
||||||
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
|
|
||||||
{`${values[THRESHOLD]} out of ${getNumOwnersFrom(values)} owners`}
|
|
||||||
</Paragraph>
|
|
||||||
</Block>
|
|
||||||
</Block>
|
</Block>
|
||||||
</Col>
|
<Block margin="lg">
|
||||||
<Col className={classes.ownersColumn} layout="column" xs={8}>
|
<Paragraph color="disabled" noMargin size="sm">
|
||||||
<TableContainer>
|
Name of the Safe
|
||||||
<Block className={classes.owners}>
|
</Paragraph>
|
||||||
<Paragraph color="primary" noMargin size="lg">
|
<Paragraph
|
||||||
{`${getNumOwnersFrom(values)} Safe owners`}
|
className={classes.name}
|
||||||
</Paragraph>
|
color="primary"
|
||||||
</Block>
|
noMargin
|
||||||
<Hairline />
|
size="lg"
|
||||||
{owners.map((address, index) => (
|
weight="bolder"
|
||||||
<>
|
data-testid="load-form-review-safe-name"
|
||||||
<Row className={classes.owner} testId={'load-safe-review-owner-name-' + index}>
|
>
|
||||||
<Col align="center" xs={12}>
|
{values[FIELD_LOAD_NAME]}
|
||||||
<EthHashInfo
|
</Paragraph>
|
||||||
hash={address}
|
</Block>
|
||||||
name={values[getOwnerNameBy(index)]}
|
<Block margin="lg">
|
||||||
showAvatar
|
<Paragraph color="disabled" noMargin size="sm">
|
||||||
showCopyBtn
|
Safe address
|
||||||
explorerUrl={getExplorerInfo(address)}
|
</Paragraph>
|
||||||
/>
|
<Row className={classes.container}>
|
||||||
</Col>
|
<EthHashInfo
|
||||||
</Row>
|
hash={safeAddress}
|
||||||
{index !== owners.length - 1 && <Hairline />}
|
shortenHash={4}
|
||||||
</>
|
showAvatar
|
||||||
))}
|
showCopyBtn
|
||||||
</TableContainer>
|
explorerUrl={getExplorerInfo(safeAddress)}
|
||||||
</Col>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
</>
|
</Block>
|
||||||
|
<Block margin="lg">
|
||||||
|
<Paragraph color="disabled" noMargin size="sm">
|
||||||
|
Connected wallet client is owner?
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
|
||||||
|
{isOwner ? 'Yes' : 'No (read-only)'}
|
||||||
|
</Paragraph>
|
||||||
|
</Block>
|
||||||
|
<Block margin="lg">
|
||||||
|
<Paragraph color="disabled" noMargin size="sm">
|
||||||
|
Any transaction requires the confirmation of:
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
|
||||||
|
{`${values[THRESHOLD]} out of ${getNumOwnersFrom(values)} owners`}
|
||||||
|
</Paragraph>
|
||||||
|
</Block>
|
||||||
|
</Block>
|
||||||
|
</Col>
|
||||||
|
<Col className={classes.ownersColumn} layout="column" xs={8}>
|
||||||
|
<TableContainer>
|
||||||
|
<Block className={classes.owners}>
|
||||||
|
<Paragraph color="primary" noMargin size="lg">
|
||||||
|
{`${getNumOwnersFrom(values)} Safe owners`}
|
||||||
|
</Paragraph>
|
||||||
|
</Block>
|
||||||
|
<Hairline />
|
||||||
|
{owners.map((address, index) => (
|
||||||
|
<>
|
||||||
|
<Row className={classes.owner} testId={'load-safe-review-owner-name-' + index}>
|
||||||
|
<Col align="center" xs={12}>
|
||||||
|
<EthHashInfo
|
||||||
|
hash={address}
|
||||||
|
name={values[getOwnerNameBy(index)]}
|
||||||
|
showAvatar
|
||||||
|
showCopyBtn
|
||||||
|
explorerUrl={getExplorerInfo(address)}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
{index !== owners.length - 1 && <Hairline />}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</TableContainer>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Review = ({ userAddress }: { userAddress: string }) =>
|
const Review = ({ userAddress }: { userAddress: string }) =>
|
||||||
function ReviewPage(controls: React.ReactNode, { values }: { values: LoadFormValues }): React.ReactElement {
|
function ReviewPage(controls: ReactNode, { values }: { values: LoadFormValues }): ReactElement {
|
||||||
return (
|
return (
|
||||||
<>
|
<OpenPaper controls={controls} padding={false}>
|
||||||
<OpenPaper controls={controls} padding={false}>
|
<ReviewComponent userAddress={userAddress} values={values} />
|
||||||
<ReviewComponent userAddress={userAddress} values={values} />
|
</OpenPaper>
|
||||||
</OpenPaper>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,34 +1,25 @@
|
||||||
import { List } from 'immutable'
|
import React, { ReactElement } from 'react'
|
||||||
import * as React from 'react'
|
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
|
|
||||||
|
|
||||||
import Layout from 'src/routes/load/components/Layout'
|
import Layout from 'src/routes/load/components/Layout'
|
||||||
import { FIELD_LOAD_ADDRESS, FIELD_LOAD_NAME } from '../components/fields'
|
import { AddressBookEntry, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||||
|
import { addressBookSafeLoad } from 'src/logic/addressBook/store/actions'
|
||||||
|
import { FIELD_LOAD_ADDRESS } from 'src/routes/load/components/fields'
|
||||||
|
|
||||||
import Page from 'src/components/layout/Page'
|
import Page from 'src/components/layout/Page'
|
||||||
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
|
||||||
import { saveSafes, loadStoredSafes } from 'src/logic/safe/utils'
|
import { saveSafes, loadStoredSafes } from 'src/logic/safe/utils'
|
||||||
import { getNamesFrom, getOwnersFrom } from 'src/routes/open/utils/safeDataExtractor'
|
import { getAccountsFrom, getNamesFrom } 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 { SafeOwner, SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||||
import { networkSelector, providerNameSelector, userAccountSelector } from 'src/logic/wallets/store/selectors'
|
import { isValidAddress } from 'src/utils/isValidAddress'
|
||||||
|
import { providerNameSelector, userAccountSelector } from 'src/logic/wallets/store/selectors'
|
||||||
import { addOrUpdateSafe } from 'src/logic/safe/store/actions/addOrUpdateSafe'
|
import { addOrUpdateSafe } from 'src/logic/safe/store/actions/addOrUpdateSafe'
|
||||||
|
|
||||||
export const loadSafe = async (
|
export const loadSafe = async (safeAddress: string, addSafe: (safe: SafeRecordProps) => void): Promise<void> => {
|
||||||
safeName: string,
|
const safeProps = await buildSafe(safeAddress)
|
||||||
safeAddress: string,
|
|
||||||
owners: List<SafeOwner>,
|
|
||||||
addSafe: (safe: SafeRecordProps) => void,
|
|
||||||
): Promise<void> => {
|
|
||||||
const safeProps = await buildSafe(safeAddress, safeName)
|
|
||||||
safeProps.owners = owners
|
|
||||||
// We are manually adding the safe. We enforce this state in case the safe was previously
|
|
||||||
// accessed by URL
|
|
||||||
safeProps.loadedViaUrl = false
|
|
||||||
|
|
||||||
const storedSafes = (await loadStoredSafes()) || {}
|
const storedSafes = (await loadStoredSafes()) || {}
|
||||||
|
|
||||||
|
@ -56,10 +47,9 @@ interface LoadForm {
|
||||||
|
|
||||||
export type LoadFormValues = ReviewSafeCreationValues | LoadForm
|
export type LoadFormValues = ReviewSafeCreationValues | LoadForm
|
||||||
|
|
||||||
const Load = (): React.ReactElement => {
|
const Load = (): ReactElement => {
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const provider = useSelector(providerNameSelector)
|
const provider = useSelector(providerNameSelector)
|
||||||
const network = useSelector(networkSelector)
|
|
||||||
const userAddress = useSelector(userAccountSelector)
|
const userAddress = useSelector(userAccountSelector)
|
||||||
|
|
||||||
const addSafeHandler = async (safe: SafeRecordProps) => {
|
const addSafeHandler = async (safe: SafeRecordProps) => {
|
||||||
|
@ -67,22 +57,32 @@ const Load = (): React.ReactElement => {
|
||||||
}
|
}
|
||||||
const onLoadSafeSubmit = async (values: LoadFormValues) => {
|
const onLoadSafeSubmit = async (values: LoadFormValues) => {
|
||||||
let safeAddress = values[FIELD_LOAD_ADDRESS]
|
let safeAddress = values[FIELD_LOAD_ADDRESS]
|
||||||
// TODO: review this check. It doesn't seems to be necessary at this point
|
|
||||||
if (!safeAddress) {
|
if (!isValidAddress(safeAddress)) {
|
||||||
console.error('failed to add Safe address', JSON.stringify(values))
|
console.error('failed to add Safe address', JSON.stringify(values))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ownersNames = getNamesFrom(values)
|
||||||
|
const ownersAddresses = getAccountsFrom(values)
|
||||||
|
|
||||||
|
const owners = ownersAddresses.reduce((acc, address, index) => {
|
||||||
|
if (ownersNames[index]) {
|
||||||
|
// Do not add owners to addressbook if names are empty
|
||||||
|
const newAddressBookEntry = makeAddressBookEntry({
|
||||||
|
address,
|
||||||
|
name: ownersNames[index],
|
||||||
|
})
|
||||||
|
acc.push(newAddressBookEntry)
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, [] as AddressBookEntry[])
|
||||||
|
const safe = makeAddressBookEntry({ address: safeAddress, name: values.name })
|
||||||
|
await dispatch(addressBookSafeLoad([...owners, safe]))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const safeName = values[FIELD_LOAD_NAME]
|
|
||||||
safeAddress = checksumAddress(safeAddress)
|
safeAddress = checksumAddress(safeAddress)
|
||||||
const ownerNames = getNamesFrom(values)
|
await loadSafe(safeAddress, addSafeHandler)
|
||||||
|
|
||||||
const gnosisSafe = getGnosisSafeInstanceAt(safeAddress)
|
|
||||||
const ownerAddresses = await gnosisSafe.methods.getOwners().call()
|
|
||||||
const owners = getOwnersFrom(ownerNames, ownerAddresses.slice().sort())
|
|
||||||
|
|
||||||
await loadSafe(safeName, safeAddress, owners, addSafeHandler)
|
|
||||||
|
|
||||||
const url = `${SAFELIST_ADDRESS}/${safeAddress}/balances`
|
const url = `${SAFELIST_ADDRESS}/${safeAddress}/balances`
|
||||||
history.push(url)
|
history.push(url)
|
||||||
|
@ -93,12 +93,7 @@ const Load = (): React.ReactElement => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<Layout
|
<Layout onLoadSafeSubmit={onLoadSafeSubmit} userAddress={userAddress} provider={provider} />
|
||||||
onLoadSafeSubmit={onLoadSafeSubmit}
|
|
||||||
network={ETHEREUM_NETWORK[network]}
|
|
||||||
userAddress={userAddress}
|
|
||||||
provider={provider}
|
|
||||||
/>
|
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import styled from 'styled-components'
|
||||||
import OpenPaper from 'src/components/Stepper/OpenPaper'
|
import OpenPaper from 'src/components/Stepper/OpenPaper'
|
||||||
import Field from 'src/components/forms/Field'
|
import Field from 'src/components/forms/Field'
|
||||||
import TextField from 'src/components/forms/TextField'
|
import TextField from 'src/components/forms/TextField'
|
||||||
import { composeValidators, minMaxLength, required } from 'src/components/forms/validator'
|
import { composeValidators, required, validAddressBookName } from 'src/components/forms/validator'
|
||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
import Paragraph from 'src/components/layout/Paragraph'
|
import Paragraph from 'src/components/layout/Paragraph'
|
||||||
import { FIELD_NAME } from 'src/routes/open/components/fields'
|
import { FIELD_NAME } from 'src/routes/open/components/fields'
|
||||||
|
@ -56,7 +56,7 @@ const SafeNameForm = ({ safeName }: { safeName: string }): React.ReactElement =>
|
||||||
placeholder="Name of the new Safe"
|
placeholder="Name of the new Safe"
|
||||||
text="Safe name"
|
text="Safe name"
|
||||||
type="text"
|
type="text"
|
||||||
validate={composeValidators(required, minMaxLength(1, 50))}
|
validate={composeValidators(required, validAddressBookName)}
|
||||||
testId="create-safe-name-field"
|
testId="create-safe-name-field"
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Loader } from '@gnosis.pm/safe-react-components'
|
import { Loader } from '@gnosis.pm/safe-react-components'
|
||||||
import { backOff } from 'exponential-backoff'
|
import { backOff } from 'exponential-backoff'
|
||||||
import queryString from 'query-string'
|
import queryString from 'query-string'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState, ReactElement } from 'react'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import { PromiEvent, TransactionReceipt } from 'web3-core'
|
import { PromiEvent, TransactionReceipt } from 'web3-core'
|
||||||
|
@ -16,7 +16,6 @@ import {
|
||||||
CreateSafeValues,
|
CreateSafeValues,
|
||||||
getAccountsFrom,
|
getAccountsFrom,
|
||||||
getNamesFrom,
|
getNamesFrom,
|
||||||
getOwnersFrom,
|
|
||||||
getSafeCreationSaltFrom,
|
getSafeCreationSaltFrom,
|
||||||
getSafeNameFrom,
|
getSafeNameFrom,
|
||||||
getThresholdFrom,
|
getThresholdFrom,
|
||||||
|
@ -25,8 +24,9 @@ import { SAFELIST_ADDRESS, WELCOME_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, removeFromStorage, saveToStorage } from 'src/utils/storage'
|
import { loadFromStorage, removeFromStorage, saveToStorage } from 'src/utils/storage'
|
||||||
|
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||||
|
import { addressBookSafeLoad } from 'src/logic/addressBook/store/actions'
|
||||||
import { userAccountSelector } from 'src/logic/wallets/store/selectors'
|
import { userAccountSelector } from 'src/logic/wallets/store/selectors'
|
||||||
import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
|
||||||
import { addOrUpdateSafe } from 'src/logic/safe/store/actions/addOrUpdateSafe'
|
import { addOrUpdateSafe } from 'src/logic/safe/store/actions/addOrUpdateSafe'
|
||||||
import { useAnalytics } from 'src/utils/googleAnalytics'
|
import { useAnalytics } from 'src/utils/googleAnalytics'
|
||||||
import { sleep } from 'src/utils/timer'
|
import { sleep } from 'src/utils/timer'
|
||||||
|
@ -78,18 +78,6 @@ const getSafePropsValuesFromQueryParams = (queryParams: SafeCreationQueryParams)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getSafeProps = async (
|
|
||||||
safeAddress: string,
|
|
||||||
safeName: string,
|
|
||||||
ownersNames: string[],
|
|
||||||
ownerAddresses: string[],
|
|
||||||
): Promise<SafeRecordProps> => {
|
|
||||||
const safeProps = await buildSafe(safeAddress, safeName)
|
|
||||||
safeProps.owners = getOwnersFrom(ownersNames, ownerAddresses)
|
|
||||||
|
|
||||||
return safeProps
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createSafe = (values: CreateSafeValues, userAccount: string): PromiEvent<TransactionReceipt> => {
|
export const createSafe = (values: CreateSafeValues, userAccount: string): PromiEvent<TransactionReceipt> => {
|
||||||
const confirmations = getThresholdFrom(values)
|
const confirmations = getThresholdFrom(values)
|
||||||
const ownerAddresses = getAccountsFrom(values)
|
const ownerAddresses = getAccountsFrom(values)
|
||||||
|
@ -118,7 +106,7 @@ export const createSafe = (values: CreateSafeValues, userAccount: string): Promi
|
||||||
return promiEvent
|
return promiEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
const Open = (): React.ReactElement => {
|
const Open = (): ReactElement => {
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [showProgress, setShowProgress] = useState(false)
|
const [showProgress, setShowProgress] = useState(false)
|
||||||
const [creationTxPromise, setCreationTxPromise] = useState<PromiEvent<TransactionReceipt>>()
|
const [creationTxPromise, setCreationTxPromise] = useState<PromiEvent<TransactionReceipt>>()
|
||||||
|
@ -176,14 +164,24 @@ const Open = (): React.ReactElement => {
|
||||||
setShowProgress(true)
|
setShowProgress(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSafeCreated = async (safeAddress): Promise<void> => {
|
const onSafeCreated = async (safeAddress: string): Promise<void> => {
|
||||||
const pendingCreation = await loadFromStorage<LoadedSafeType>(SAFE_PENDING_CREATION_STORAGE_KEY)
|
const pendingCreation = await loadFromStorage<LoadedSafeType>(SAFE_PENDING_CREATION_STORAGE_KEY)
|
||||||
|
|
||||||
const name = pendingCreation ? getSafeNameFrom(pendingCreation) : ''
|
let name = ''
|
||||||
const ownersNames = getNamesFrom(pendingCreation as CreateSafeValues)
|
let ownersNames: string[] = []
|
||||||
const ownerAddresses = pendingCreation ? getAccountsFrom(pendingCreation) : []
|
let ownersAddresses: string[] = []
|
||||||
const safeProps = await getSafeProps(safeAddress, name, ownersNames, ownerAddresses)
|
|
||||||
|
|
||||||
|
if (pendingCreation) {
|
||||||
|
name = getSafeNameFrom(pendingCreation)
|
||||||
|
ownersNames = getNamesFrom(pendingCreation as CreateSafeValues)
|
||||||
|
ownersAddresses = getAccountsFrom(pendingCreation)
|
||||||
|
}
|
||||||
|
|
||||||
|
const owners = ownersAddresses.map((address, index) => makeAddressBookEntry({ address, name: ownersNames[index] }))
|
||||||
|
const safe = makeAddressBookEntry({ address: safeAddress, name })
|
||||||
|
await dispatch(addressBookSafeLoad([...owners, safe]))
|
||||||
|
|
||||||
|
const safeProps = await buildSafe(safeAddress)
|
||||||
await dispatch(addOrUpdateSafe(safeProps))
|
await dispatch(addOrUpdateSafe(safeProps))
|
||||||
|
|
||||||
trackEvent({
|
trackEvent({
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
import { List } from 'immutable'
|
|
||||||
|
|
||||||
import { makeOwner } from 'src/logic/safe/store/models/owner'
|
|
||||||
import { SafeOwner } from 'src/logic/safe/store/models/safe'
|
|
||||||
import { LoadFormValues } from 'src/routes/load/container/Load'
|
import { LoadFormValues } from 'src/routes/load/container/Load'
|
||||||
import { getNumOwnersFrom } from 'src/routes/open/components/fields'
|
import { getNumOwnersFrom } from 'src/routes/open/components/fields'
|
||||||
|
|
||||||
|
@ -15,29 +11,18 @@ export type CreateSafeValues = {
|
||||||
owners?: number | string
|
owners?: number | string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAccountsFrom = (values: CreateSafeValues | LoadFormValues): string[] => {
|
const getByRegexFrom = (regex: RegExp) => (values: CreateSafeValues | LoadFormValues): string[] => {
|
||||||
const accounts = Object.keys(values)
|
const accounts = Object.keys(values)
|
||||||
.sort()
|
.sort()
|
||||||
.filter((key) => /^owner\d+Address$/.test(key))
|
.filter((key) => regex.test(key))
|
||||||
|
|
||||||
const numOwners = getNumOwnersFrom(values)
|
const numOwners = getNumOwnersFrom(values)
|
||||||
return accounts.map((account) => values[account]).slice(0, numOwners)
|
return accounts.map((account) => values[account]).slice(0, numOwners)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getNamesFrom = (values: CreateSafeValues | LoadFormValues): string[] => {
|
export const getAccountsFrom = getByRegexFrom(/^owner\d+Address$/)
|
||||||
const accounts = Object.keys(values)
|
|
||||||
.sort()
|
|
||||||
.filter((key) => /^owner\d+Name$/.test(key))
|
|
||||||
|
|
||||||
const numOwners = getNumOwnersFrom(values)
|
export const getNamesFrom = getByRegexFrom(/^owner\d+Name$/)
|
||||||
return accounts.map((account) => values[account]).slice(0, numOwners)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getOwnersFrom = (names: string[], addresses: string[]): List<SafeOwner> => {
|
|
||||||
const owners = names.map((name, index) => makeOwner({ name, address: addresses[index] }))
|
|
||||||
|
|
||||||
return List(owners)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getThresholdFrom = (values: CreateSafeValues): number => Number(values.confirmations)
|
export const getThresholdFrom = (values: CreateSafeValues): number => Number(values.confirmations)
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,17 @@
|
||||||
import IconButton from '@material-ui/core/IconButton'
|
|
||||||
import Close from '@material-ui/icons/Close'
|
|
||||||
import React, { ReactElement } from 'react'
|
import React, { ReactElement } from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
import { useStyles } from './style'
|
import { useStyles } from './style'
|
||||||
|
|
||||||
import Modal, { Modal as GenericModal } from 'src/components/Modal'
|
import { Modal } from 'src/components/Modal'
|
||||||
import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
|
import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
|
||||||
import AddressInput from 'src/components/forms/AddressInput'
|
import AddressInput from 'src/components/forms/AddressInput'
|
||||||
import Field from 'src/components/forms/Field'
|
import Field from 'src/components/forms/Field'
|
||||||
import GnoForm from 'src/components/forms/GnoForm'
|
import GnoForm from 'src/components/forms/GnoForm'
|
||||||
import TextField from 'src/components/forms/TextField'
|
import TextField from 'src/components/forms/TextField'
|
||||||
import { composeValidators, minMaxLength, required, uniqueAddress } from 'src/components/forms/validator'
|
import { composeValidators, required, uniqueAddress, validAddressBookName } from 'src/components/forms/validator'
|
||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
import Col from 'src/components/layout/Col'
|
import Col from 'src/components/layout/Col'
|
||||||
import Hairline from 'src/components/layout/Hairline'
|
|
||||||
import Paragraph from 'src/components/layout/Paragraph'
|
|
||||||
import Row from 'src/components/layout/Row'
|
import Row from 'src/components/layout/Row'
|
||||||
import { addressBookAddressesListSelector } from 'src/logic/addressBook/store/selectors'
|
import { addressBookAddressesListSelector } from 'src/logic/addressBook/store/selectors'
|
||||||
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||||
|
@ -66,81 +62,76 @@ export const CreateEditEntryModal = ({
|
||||||
description={isNew ? 'Create new addressBook entry' : 'Edit addressBook entry'}
|
description={isNew ? 'Create new addressBook entry' : 'Edit addressBook entry'}
|
||||||
handleClose={onClose}
|
handleClose={onClose}
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
paperClassName="smaller-modal-window"
|
|
||||||
title={isNew ? 'Create new entry' : 'Edit entry'}
|
title={isNew ? 'Create new entry' : 'Edit entry'}
|
||||||
>
|
>
|
||||||
<Row align="center" className={classes.heading} grow>
|
<Modal.Header onClose={onClose}>
|
||||||
<Paragraph className={classes.manage} noMargin weight="bolder">
|
<Modal.Header.Title>{isNew ? 'Create entry' : 'Edit entry'}</Modal.Header.Title>
|
||||||
{isNew ? 'Create entry' : 'Edit entry'}
|
</Modal.Header>
|
||||||
</Paragraph>
|
<Modal.Body withoutPadding>
|
||||||
<IconButton disableRipple onClick={onClose}>
|
<GnoForm formMutators={formMutators} onSubmit={onFormSubmitted} initialValues={initialValues}>
|
||||||
<Close className={classes.close} />
|
{(...args) => {
|
||||||
</IconButton>
|
const formState = args[2]
|
||||||
</Row>
|
const mutators = args[3]
|
||||||
<Hairline />
|
const handleScan = (value, closeQrModal) => {
|
||||||
<GnoForm formMutators={formMutators} onSubmit={onFormSubmitted} initialValues={initialValues}>
|
let scannedAddress = value
|
||||||
{(...args) => {
|
|
||||||
const formState = args[2]
|
|
||||||
const mutators = args[3]
|
|
||||||
const handleScan = (value, closeQrModal) => {
|
|
||||||
let scannedAddress = value
|
|
||||||
|
|
||||||
if (scannedAddress.startsWith('ethereum:')) {
|
if (scannedAddress.startsWith('ethereum:')) {
|
||||||
scannedAddress = scannedAddress.replace('ethereum:', '')
|
scannedAddress = scannedAddress.replace('ethereum:', '')
|
||||||
|
}
|
||||||
|
|
||||||
|
mutators.setOwnerAddress(scannedAddress)
|
||||||
|
closeQrModal()
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
mutators.setOwnerAddress(scannedAddress)
|
<>
|
||||||
closeQrModal()
|
<Block className={classes.container}>
|
||||||
}
|
<Row margin="md">
|
||||||
return (
|
<Col xs={11}>
|
||||||
<>
|
<Field
|
||||||
<Block className={classes.container}>
|
component={TextField}
|
||||||
<Row margin="md">
|
name="name"
|
||||||
<Col xs={11}>
|
placeholder="Name*"
|
||||||
<Field
|
testId={CREATE_ENTRY_INPUT_NAME_ID}
|
||||||
component={TextField}
|
text="Name*"
|
||||||
name="name"
|
type="text"
|
||||||
placeholder="Name"
|
validate={composeValidators(required, validAddressBookName)}
|
||||||
testId={CREATE_ENTRY_INPUT_NAME_ID}
|
/>
|
||||||
text="Name"
|
|
||||||
type="text"
|
|
||||||
validate={composeValidators(required, minMaxLength(1, 50))}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Row margin="md">
|
|
||||||
<Col xs={11}>
|
|
||||||
<AddressInput
|
|
||||||
disabled={!isNew}
|
|
||||||
fieldMutator={mutators.setOwnerAddress}
|
|
||||||
name="address"
|
|
||||||
placeholder="Address*"
|
|
||||||
testId={CREATE_ENTRY_INPUT_ADDRESS_ID}
|
|
||||||
text="Address*"
|
|
||||||
validators={[(value?: string) => (isNew ? isUniqueAddress(value) : undefined)]}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
{isNew ? (
|
|
||||||
<Col center="xs" className={classes} middle="xs" xs={1}>
|
|
||||||
<ScanQRWrapper handleScan={handleScan} />
|
|
||||||
</Col>
|
</Col>
|
||||||
) : null}
|
</Row>
|
||||||
</Row>
|
<Row margin="md">
|
||||||
</Block>
|
<Col xs={11}>
|
||||||
<GenericModal.Footer>
|
<AddressInput
|
||||||
<GenericModal.Footer.Buttons
|
disabled={!isNew}
|
||||||
cancelButtonProps={{ onClick: onClose }}
|
fieldMutator={mutators.setOwnerAddress}
|
||||||
confirmButtonProps={{
|
name="address"
|
||||||
disabled: !formState.valid,
|
placeholder="Address*"
|
||||||
testId: SAVE_NEW_ENTRY_BTN_ID,
|
testId={CREATE_ENTRY_INPUT_ADDRESS_ID}
|
||||||
text: isNew ? 'Create' : 'Save',
|
text="Address*"
|
||||||
}}
|
validators={[(value?: string) => (isNew ? isUniqueAddress(value) : undefined)]}
|
||||||
/>
|
/>
|
||||||
</GenericModal.Footer>
|
</Col>
|
||||||
</>
|
{isNew ? (
|
||||||
)
|
<Col center="xs" className={classes} middle="xs" xs={1}>
|
||||||
}}
|
<ScanQRWrapper handleScan={handleScan} />
|
||||||
</GnoForm>
|
</Col>
|
||||||
|
) : null}
|
||||||
|
</Row>
|
||||||
|
</Block>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Modal.Footer.Buttons
|
||||||
|
cancelButtonProps={{ onClick: onClose }}
|
||||||
|
confirmButtonProps={{
|
||||||
|
disabled: !formState.valid,
|
||||||
|
testId: SAVE_NEW_ENTRY_BTN_ID,
|
||||||
|
text: isNew ? 'Create' : 'Save',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal.Footer>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</GnoForm>
|
||||||
|
</Modal.Body>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,9 @@ import { push } from 'connected-react-router'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
|
||||||
|
import { sameString } from 'src/utils/strings'
|
||||||
|
import { ADDRESS_BOOK_DEFAULT_NAME } from 'src/logic/addressBook/model/addressBook'
|
||||||
|
import { getNameFromAddressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||||
import { SAFELIST_ADDRESS } from 'src/routes/routes'
|
import { SAFELIST_ADDRESS } from 'src/routes/routes'
|
||||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||||
import { xs } from 'src/theme/variables'
|
import { xs } from 'src/theme/variables'
|
||||||
|
@ -35,13 +38,11 @@ const useStyles = makeStyles(
|
||||||
|
|
||||||
type EllipsisTransactionDetailsProps = {
|
type EllipsisTransactionDetailsProps = {
|
||||||
address: string
|
address: string
|
||||||
knownAddress?: boolean
|
|
||||||
sendModalOpenHandler?: () => void
|
sendModalOpenHandler?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EllipsisTransactionDetails = ({
|
export const EllipsisTransactionDetails = ({
|
||||||
address,
|
address,
|
||||||
knownAddress,
|
|
||||||
sendModalOpenHandler,
|
sendModalOpenHandler,
|
||||||
}: EllipsisTransactionDetailsProps): React.ReactElement => {
|
}: EllipsisTransactionDetailsProps): React.ReactElement => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
@ -51,6 +52,10 @@ export const EllipsisTransactionDetails = ({
|
||||||
const currentSafeAddress = useSelector(safeParamAddressFromStateSelector)
|
const currentSafeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||||
const isOwnerConnected = useSelector(grantedSelector)
|
const isOwnerConnected = useSelector(grantedSelector)
|
||||||
|
|
||||||
|
const recipientName = useSelector((state) => getNameFromAddressBookSelector(state, { address }))
|
||||||
|
// We have to check that the name returned is not UNKNOWN
|
||||||
|
const isStoredInAddressBook = !sameString(recipientName, ADDRESS_BOOK_DEFAULT_NAME)
|
||||||
|
|
||||||
const handleClick = (event) => setAnchorEl(event.currentTarget)
|
const handleClick = (event) => setAnchorEl(event.currentTarget)
|
||||||
|
|
||||||
const closeMenuHandler = () => setAnchorEl(null)
|
const closeMenuHandler = () => setAnchorEl(null)
|
||||||
|
@ -73,7 +78,7 @@ export const EllipsisTransactionDetails = ({
|
||||||
<Divider key="divider" />,
|
<Divider key="divider" />,
|
||||||
]
|
]
|
||||||
: null}
|
: null}
|
||||||
{knownAddress ? (
|
{isStoredInAddressBook ? (
|
||||||
<MenuItem onClick={addOrEditEntryHandler}>Edit Address book Entry</MenuItem>
|
<MenuItem onClick={addOrEditEntryHandler}>Edit Address book Entry</MenuItem>
|
||||||
) : (
|
) : (
|
||||||
<MenuItem onClick={addOrEditEntryHandler}>Add to address book</MenuItem>
|
<MenuItem onClick={addOrEditEntryHandler}>Add to address book</MenuItem>
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
<svg width="108" height="96" viewBox="0 0 108 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M54 4C79.405 4 100 24.595 100 50C100 75.405 79.405 96 54 96C28.595 96 8 75.405 8 50C8 24.595 28.595 4 54 4Z" fill="#F7F5F5"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M37.3327 60.8333L37.3327 66.6663H70.6663V60.8333C70.6663 60.6037 70.6663 58.3333 72.7496 58.3333C74.833 58.3333 74.833 60.6037 74.833 60.8333V68.7503C74.8325 68.7503 74.8321 68.7503 74.8317 68.7503C74.8313 69.8959 73.8961 70.833 72.7484 70.833H35.2484C34.1025 70.833 33.165 69.8955 33.165 68.7496C33.165 68.7284 33.1654 68.7073 33.166 68.6862L33.166 60.8333C33.166 60.6037 33.166 58.3333 35.2493 58.3333C37.3327 58.3333 37.3327 60.6037 37.3327 60.8333Z" fill="#B2B5B2"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M51.9156 36.2792V60.4105C51.9156 61.5626 52.849 62.4938 53.999 62.4938C55.151 62.4938 56.0823 61.5626 56.0823 60.4105V36.2792L64.6698 44.8647C65.4844 45.6793 66.8031 45.6793 67.6156 44.8647C68.4302 44.0501 68.4302 42.7334 67.6156 41.9188L55.8302 30.1334C55.7698 30.073 55.7052 30.0167 55.6406 29.9667C55.2573 29.4772 54.6656 29.1667 53.999 29.1667C53.3344 29.1667 52.7406 29.4772 52.3594 29.9667C52.2948 30.0167 52.2281 30.073 52.1698 30.1334L40.3844 41.9188C39.5698 42.7334 39.5698 44.0501 40.3844 44.8647C41.1969 45.6793 42.5156 45.6793 43.3302 44.8647L51.9156 36.2792Z" fill="#B2B5B2"/>
|
||||||
|
<circle cx="80" cy="74" r="17" fill="#F7F5F5"/>
|
||||||
|
<rect x="78.25" y="63.5" width="3.5" height="14" rx="1" fill="#F02525"/>
|
||||||
|
<path d="M80 80.5625C81.2081 80.5625 82.1875 81.5419 82.1875 82.75C82.1875 83.9581 81.2081 84.9375 80 84.9375C78.7919 84.9375 77.8125 83.9581 77.8125 82.75C77.8125 81.5419 78.7919 80.5625 80 80.5625Z" fill="#F02525"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M97.5 74C97.5 83.665 89.665 91.5 80 91.5C70.335 91.5 62.5 83.665 62.5 74C62.5 64.335 70.335 56.5 80 56.5C89.665 56.5 97.5 64.335 97.5 74ZM66 74C66 81.732 72.268 88 80 88C87.732 88 94 81.732 94 74C94 66.268 87.732 60 80 60C72.268 60 66 66.268 66 74Z" fill="#F02525"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
|
@ -0,0 +1,8 @@
|
||||||
|
<svg width="108" height="96" viewBox="0 0 108 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M54 4C79.405 4 100 24.595 100 50C100 75.405 79.405 96 54 96C28.595 96 8 75.405 8 50C8 24.595 28.595 4 54 4Z" fill="#F7F5F5"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M37.3329 60.8333L37.3329 66.6663H70.6665V60.8333C70.6665 60.6037 70.6665 58.3333 72.7499 58.3333C74.8332 58.3333 74.8332 60.6037 74.8332 60.8333V68.7503C74.8328 68.7503 74.8324 68.7503 74.8319 68.7503C74.8316 69.8959 73.8963 70.833 72.7486 70.833H35.2486C34.1028 70.833 33.1653 69.8955 33.1653 68.7496C33.1653 68.7284 33.1656 68.7073 33.1662 68.6862L33.1662 60.8333C33.1662 60.6037 33.1662 58.3333 35.2496 58.3333C37.3329 58.3333 37.3329 60.6037 37.3329 60.8333Z" fill="#B2B5B2"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M51.9161 36.2791V60.4104C51.9161 61.5625 52.8494 62.4937 53.9994 62.4937C55.1515 62.4937 56.0828 61.5625 56.0828 60.4104V36.2791L64.6703 44.8645C65.4849 45.6791 66.8036 45.6791 67.6161 44.8645C68.4307 44.05 68.4307 42.7333 67.6161 41.9187L55.8307 30.1333C55.7703 30.0729 55.7057 30.0166 55.6411 29.9666C55.2578 29.477 54.6661 29.1666 53.9994 29.1666C53.3349 29.1666 52.7411 29.477 52.3599 29.9666C52.2953 30.0166 52.2286 30.0729 52.1703 30.1333L40.3849 41.9187C39.5703 42.7333 39.5703 44.05 40.3849 44.8645C41.1974 45.6791 42.5161 45.6791 43.3307 44.8645L51.9161 36.2791Z" fill="#B2B5B2"/>
|
||||||
|
<circle cx="80" cy="74" r="17" fill="#F7F5F5"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M97.5 74C97.5 83.665 89.665 91.5 80 91.5C70.335 91.5 62.5 83.665 62.5 74C62.5 64.335 70.335 56.5 80 56.5C89.665 56.5 97.5 64.335 97.5 74ZM66 74C66 81.732 72.268 88 80 88C87.732 88 94 81.732 94 74C94 66.268 87.732 60 80 60C72.268 60 66 66.268 66 74Z" fill="#008C73"/>
|
||||||
|
<path d="M72.443 70.9826C71.7373 70.3222 70.6299 70.3588 69.9694 71.0644C69.309 71.77 69.3456 72.8775 70.0512 73.5379L77.2979 80.3209C77.9934 80.9719 79.0818 80.947 79.7468 80.265L89.1041 70.668C89.7788 69.976 89.7648 68.868 89.0728 68.1933C88.3808 67.5186 87.2728 67.5326 86.5981 68.2246L78.4379 76.5939L72.443 70.9826Z" fill="#008C73"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
|
@ -0,0 +1,8 @@
|
||||||
|
<svg width="108" height="96" viewBox="0 0 108 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M54 4C79.405 4 100 24.595 100 50C100 75.405 79.405 96 54 96C28.595 96 8 75.405 8 50C8 24.595 28.595 4 54 4Z" fill="#F7F5F5"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M37.3327 60.8333L37.3327 66.6663H70.6663V60.8333C70.6663 60.6037 70.6663 58.3333 72.7496 58.3333C74.833 58.3333 74.833 60.6037 74.833 60.8333V68.7503C74.8325 68.7503 74.8321 68.7503 74.8317 68.7503C74.8313 69.8959 73.8961 70.833 72.7484 70.833H35.2484C34.1025 70.833 33.165 69.8955 33.165 68.7496C33.165 68.7284 33.1654 68.7073 33.166 68.6862L33.166 60.8333C33.166 60.6037 33.166 58.3333 35.2493 58.3333C37.3327 58.3333 37.3327 60.6037 37.3327 60.8333Z" fill="#B2B5B2"/>
|
||||||
|
<circle cx="80" cy="74" r="17" fill="#F7F5F5"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M73.871 80C73.243 80 72.63 79.7078 72.2767 79.1771C71.7366 78.3629 72.0114 77.2989 72.8898 76.7984L78.2621 73.7429V66.7287C78.2621 65.7736 79.0994 65 80.131 65C81.1636 65 82 65.7736 82 66.7287V74.7093C82 75.3091 81.6636 75.8675 81.1104 76.1821L74.8484 79.7442C74.5429 79.917 74.2046 80 73.871 80Z" fill="#5D6D74"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M97.5 74C97.5 83.665 89.665 91.5 80 91.5C70.335 91.5 62.5 83.665 62.5 74C62.5 64.335 70.335 56.5 80 56.5C89.665 56.5 97.5 64.335 97.5 74ZM66 74C66 81.732 72.268 88 80 88C87.732 88 94 81.732 94 74C94 66.268 87.732 60 80 60C72.268 60 66 66.268 66 74Z" fill="#5D6D74"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M51.9156 36.2791V60.4104C51.9156 61.5625 52.849 62.4937 53.999 62.4937C55.151 62.4937 56.0823 61.5625 56.0823 60.4104V36.2791L64.6698 44.8645C65.4844 45.6791 66.8031 45.6791 67.6156 44.8645C68.4302 44.05 68.4302 42.7333 67.6156 41.9187L55.8302 30.1333C55.7698 30.0729 55.7052 30.0166 55.6406 29.9666C55.2573 29.477 54.6656 29.1666 53.999 29.1666C53.3344 29.1666 52.7406 29.477 52.3594 29.9666C52.2948 30.0166 52.2281 30.0729 52.1698 30.1333L40.3844 41.9187C39.5698 42.7333 39.5698 44.05 40.3844 44.8645C41.1969 45.6791 42.5156 45.6791 43.3302 44.8645L51.9156 36.2791Z" fill="#B2B5B2"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
|
@ -0,0 +1,164 @@
|
||||||
|
import React, { ReactElement, useEffect, useState } from 'react'
|
||||||
|
import { format } from 'date-fns'
|
||||||
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
|
import { CSVDownloader, jsonToCSV } from 'react-papaparse'
|
||||||
|
import { Button, Loader, Text } from '@gnosis.pm/safe-react-components'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import { enhanceSnackbarForAction, getNotificationsFromTxType } from 'src/logic/notifications'
|
||||||
|
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
|
||||||
|
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||||
|
|
||||||
|
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||||
|
import { AddressBookState } from 'src/logic/addressBook/model/addressBook'
|
||||||
|
|
||||||
|
import { lg, md, background } from 'src/theme/variables'
|
||||||
|
|
||||||
|
import { Modal } from 'src/components/Modal'
|
||||||
|
import Img from 'src/components/layout/Img'
|
||||||
|
import Row from 'src/components/layout/Row'
|
||||||
|
import HelpInfo from 'src/routes/safe/components/AddressBook/HelpInfo'
|
||||||
|
|
||||||
|
import SuccessSvg from './assets/success.svg'
|
||||||
|
import ErrorSvg from './assets/error.svg'
|
||||||
|
import LoadingSvg from './assets/wait.svg'
|
||||||
|
|
||||||
|
type ExportEntriesModalProps = {
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImageContainer = styled(Row)`
|
||||||
|
padding: ${md} ${lg};
|
||||||
|
justify-content: center;
|
||||||
|
`
|
||||||
|
const StyledButton = styled(Button)`
|
||||||
|
&.MuiButtonBase-root.MuiButton-root {
|
||||||
|
padding: 0;
|
||||||
|
.MuiButton-label {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const InfoContainer = styled(Row)`
|
||||||
|
background-color: ${background};
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
padding: ${lg};
|
||||||
|
text-align: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
const BodyImage = styled.div`
|
||||||
|
grid-row: 1;
|
||||||
|
`
|
||||||
|
const StyledLoader = styled(Loader)`
|
||||||
|
margin-right: 5px;
|
||||||
|
`
|
||||||
|
const StyledCSVLink = styled(CSVDownloader)`
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const ExportEntriesModal = ({ isOpen, onClose }: ExportEntriesModalProps): ReactElement => {
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const addressBook: AddressBookState = useSelector(addressBookSelector)
|
||||||
|
const [loading, setLoading] = useState<boolean>(true)
|
||||||
|
const [error, setError] = useState<string | undefined>('')
|
||||||
|
const [csvData, setCsvData] = useState<string>('')
|
||||||
|
const [doRetry, setDoRetry] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const date = format(new Date(), 'yyyy-MM-dd')
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
//This timeout prevents modal to be closed abruptly
|
||||||
|
setLoading(true)
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!loading) {
|
||||||
|
const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.ADDRESS_BOOK_EXPORT_ENTRIES)
|
||||||
|
const action = error
|
||||||
|
? notification.afterExecution.afterExecutionError
|
||||||
|
: notification.afterExecution.noMoreConfirmationsNeeded
|
||||||
|
dispatch(enqueueSnackbar(enhanceSnackbarForAction(action)))
|
||||||
|
}
|
||||||
|
onClose()
|
||||||
|
}, 600)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleCsvData = () => {
|
||||||
|
if (!isOpen && !doRetry) return
|
||||||
|
setLoading(true)
|
||||||
|
setError('')
|
||||||
|
try {
|
||||||
|
setCsvData(jsonToCSV(addressBook))
|
||||||
|
} catch (e) {
|
||||||
|
setLoading(false)
|
||||||
|
setError(e.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
setDoRetry(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCsvData()
|
||||||
|
}, [addressBook, isOpen, doRetry, csvData])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal description="Export address book" handleClose={onClose} open={isOpen} title="Export address book">
|
||||||
|
<Modal.Header onClose={onClose}>
|
||||||
|
<Modal.Header.Title withoutMargin>Export address book</Modal.Header.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body withoutPadding>
|
||||||
|
<ImageContainer>
|
||||||
|
<BodyImage>
|
||||||
|
<Img alt="Export" height={92} src={error ? ErrorSvg : loading ? LoadingSvg : SuccessSvg} />
|
||||||
|
</BodyImage>
|
||||||
|
</ImageContainer>
|
||||||
|
<InfoContainer>
|
||||||
|
<Text color="primary" as="p" size="xl">
|
||||||
|
{!error ? (
|
||||||
|
<Text size="xl" as="span">
|
||||||
|
You're about to export a CSV file with{' '}
|
||||||
|
<Text size="xl" strong as="span">
|
||||||
|
{addressBook.length} address book entries. <br />
|
||||||
|
<HelpInfo />
|
||||||
|
</Text>
|
||||||
|
.
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text size="xl" as="span">
|
||||||
|
An error occurred while generating the address book CSV.
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</InfoContainer>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer withoutBorder>
|
||||||
|
<Row>
|
||||||
|
<Button size="md" variant="outlined" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<StyledButton
|
||||||
|
color="primary"
|
||||||
|
size="md"
|
||||||
|
disabled={loading}
|
||||||
|
onClick={error ? () => setDoRetry(true) : handleClose}
|
||||||
|
>
|
||||||
|
{!error ? (
|
||||||
|
<StyledCSVLink data={csvData} bom={true} filename={`gnosis-safe-address-book-${date}`} type="link">
|
||||||
|
{loading && <StyledLoader color="secondaryLight" size="xs" />}
|
||||||
|
Download
|
||||||
|
</StyledCSVLink>
|
||||||
|
) : (
|
||||||
|
'Retry'
|
||||||
|
)}
|
||||||
|
</StyledButton>
|
||||||
|
</Row>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import React, { ReactElement } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { Text, Link, Icon } from '@gnosis.pm/safe-react-components'
|
||||||
|
|
||||||
|
const StyledIcon = styled(Icon)`
|
||||||
|
svg {
|
||||||
|
position: relative;
|
||||||
|
top: 4px;
|
||||||
|
left: 4px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const HelpInfo = (): ReactElement => (
|
||||||
|
<Link
|
||||||
|
href="https://help.gnosis-safe.io/en/articles/5299068-address-book-export-and-import"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
title="Export & import info"
|
||||||
|
>
|
||||||
|
<Text size="xl" as="span" color="primary">
|
||||||
|
Learn about the address book import and export
|
||||||
|
</Text>
|
||||||
|
<StyledIcon size="sm" type="externalLink" color="primary" />
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default HelpInfo
|
|
@ -0,0 +1,219 @@
|
||||||
|
import React, { ReactElement, useState } from 'react'
|
||||||
|
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { Text } from '@gnosis.pm/safe-react-components'
|
||||||
|
import { Modal } from 'src/components/Modal'
|
||||||
|
import { CSVReader } from 'react-papaparse'
|
||||||
|
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||||
|
import { isValidAddress } from 'src/utils/isValidAddress'
|
||||||
|
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||||
|
import HelpInfo from 'src/routes/safe/components/AddressBook/HelpInfo'
|
||||||
|
|
||||||
|
const ImportContainer = styled.div`
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 24px;
|
||||||
|
align-items: center;
|
||||||
|
/* width: 200px;*/
|
||||||
|
min-height: 100px;
|
||||||
|
display: flex;
|
||||||
|
`
|
||||||
|
|
||||||
|
const InfoContainer = styled.div`
|
||||||
|
background-color: ${({ theme }) => theme.colors.background};
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 16px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const WRONG_FILE_EXTENSION_ERROR = 'Only CSV files are allowed'
|
||||||
|
const FILE_SIZE_TOO_BIG = 'The size of the file is over 1 MB'
|
||||||
|
const FILE_BYTES_LIMIT = 1000000
|
||||||
|
const IMPORT_SUPPORTED_FORMATS = [
|
||||||
|
'application/vnd.ms-excel',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
'text/csv',
|
||||||
|
]
|
||||||
|
|
||||||
|
type ImportEntriesModalProps = {
|
||||||
|
importEntryModalHandler: (addressList: AddressBookEntry[]) => void
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImportEntriesModal = ({ importEntryModalHandler, isOpen, onClose }: ImportEntriesModalProps): ReactElement => {
|
||||||
|
const [csvLoaded, setCsvLoaded] = useState(false)
|
||||||
|
const [importError, setImportError] = useState('')
|
||||||
|
const [entryList, setEntryList] = useState<AddressBookEntry[]>([])
|
||||||
|
|
||||||
|
const handleImportEntrySubmit = () => {
|
||||||
|
setCsvLoaded(false)
|
||||||
|
importEntryModalHandler(entryList)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOnDrop = (data, file) => {
|
||||||
|
const slicedData = data.slice(1)
|
||||||
|
|
||||||
|
const fileError = validateFile(file)
|
||||||
|
if (fileError) {
|
||||||
|
setImportError(fileError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataError = validateCsvData(slicedData)
|
||||||
|
if (dataError) {
|
||||||
|
setImportError(dataError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatedList = slicedData.map((entry) => {
|
||||||
|
return { address: checksumAddress(entry.data[0]), name: entry.data[1], chainId: parseInt(entry.data[2]) }
|
||||||
|
})
|
||||||
|
setEntryList(formatedList)
|
||||||
|
setImportError('')
|
||||||
|
setCsvLoaded(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateFile = (file) => {
|
||||||
|
if (!IMPORT_SUPPORTED_FORMATS.includes(file.type)) {
|
||||||
|
return WRONG_FILE_EXTENSION_ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size >= FILE_BYTES_LIMIT) {
|
||||||
|
return FILE_SIZE_TOO_BIG
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateCsvData = (data) => {
|
||||||
|
for (let index = 0; index < data.length; index++) {
|
||||||
|
const entry = data[index]
|
||||||
|
if (!entry.data[0] || !entry.data[1] || !entry.data[2]) {
|
||||||
|
return `Invalid amount of columns on row ${index + 1}`
|
||||||
|
}
|
||||||
|
// Verify address properties
|
||||||
|
const address = entry.data[0].toLowerCase()
|
||||||
|
if (!isValidAddress(address)) {
|
||||||
|
return `Invalid address on row ${index + 1}`
|
||||||
|
}
|
||||||
|
if (isNaN(entry.data[2])) {
|
||||||
|
return `Invalid chain id on row ${index + 1}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOnError = (error) => {
|
||||||
|
setImportError(error.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOnRemoveFile = () => {
|
||||||
|
setCsvLoaded(false)
|
||||||
|
setImportError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setCsvLoaded(false)
|
||||||
|
setEntryList([])
|
||||||
|
setImportError('')
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal description="Import address book" handleClose={handleClose} open={isOpen} title="Import address book">
|
||||||
|
<Modal.Header onClose={handleClose}>
|
||||||
|
<Modal.Header.Title>Import address book</Modal.Header.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body withoutPadding>
|
||||||
|
<ImportContainer>
|
||||||
|
<CSVReader
|
||||||
|
onDrop={handleOnDrop}
|
||||||
|
onError={handleOnError}
|
||||||
|
addRemoveButton
|
||||||
|
onRemoveFile={handleOnRemoveFile}
|
||||||
|
style={{
|
||||||
|
dropArea: {
|
||||||
|
borderColor: '#B2B5B2',
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
dropAreaActive: {
|
||||||
|
borderColor: '#008C73',
|
||||||
|
/* borderColor: '${({ theme }) => theme.colors.primary}', */
|
||||||
|
},
|
||||||
|
dropFile: {
|
||||||
|
width: 200,
|
||||||
|
height: 100,
|
||||||
|
background: '#fff',
|
||||||
|
boxShadow: 'rgb(40 54 61 / 18%) 1px 2px 10px 0px',
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
fileSizeInfo: {
|
||||||
|
color: '#001428',
|
||||||
|
lineHeight: 1,
|
||||||
|
position: 'absolute',
|
||||||
|
left: '10px',
|
||||||
|
top: '12px',
|
||||||
|
},
|
||||||
|
fileNameInfo: {
|
||||||
|
color: importError === '' ? '#008C73' : '#DB3A3D',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: 1.4,
|
||||||
|
padding: '0 0.4em',
|
||||||
|
margin: '1.2em 0 0.5em 0',
|
||||||
|
maxHeight: '59px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
progressBar: {
|
||||||
|
backgroundColor: '#008C73',
|
||||||
|
},
|
||||||
|
removeButton: {
|
||||||
|
color: '#DB3A3D',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text size="xl">
|
||||||
|
Drop your CSV file here <br />
|
||||||
|
or click to upload.
|
||||||
|
</Text>
|
||||||
|
</CSVReader>
|
||||||
|
</ImportContainer>
|
||||||
|
<InfoContainer>
|
||||||
|
{importError !== '' && (
|
||||||
|
<Text size="xl" color="error">
|
||||||
|
{importError}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{!csvLoaded && importError === '' && (
|
||||||
|
<Text color="text" as="p" size="xl">
|
||||||
|
Only CSV files exported from Gnosis Safe are allowed. <br />
|
||||||
|
<HelpInfo />
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{csvLoaded && importError === '' && (
|
||||||
|
<>
|
||||||
|
<Text size="xl" as="span">{`You're about to import`}</Text>
|
||||||
|
<Text size="xl" strong as="span">{` ${entryList.length} entries to your address book`}</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</InfoContainer>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Modal.Footer.Buttons
|
||||||
|
cancelButtonProps={{ onClick: () => handleClose() }}
|
||||||
|
confirmButtonProps={{
|
||||||
|
color: 'primary',
|
||||||
|
disabled: !csvLoaded || importError !== '',
|
||||||
|
onClick: handleImportEntrySubmit,
|
||||||
|
text: 'Import',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImportEntriesModal
|
|
@ -1,4 +1,4 @@
|
||||||
import { Button, EthHashInfo, FixedIcon, Text } from '@gnosis.pm/safe-react-components'
|
import { Button, EthHashInfo, FixedIcon, Text, ButtonLink, Icon } from '@gnosis.pm/safe-react-components'
|
||||||
import TableCell from '@material-ui/core/TableCell'
|
import TableCell from '@material-ui/core/TableCell'
|
||||||
import TableContainer from '@material-ui/core/TableContainer'
|
import TableContainer from '@material-ui/core/TableContainer'
|
||||||
import TableRow from '@material-ui/core/TableRow'
|
import TableRow from '@material-ui/core/TableRow'
|
||||||
|
@ -10,37 +10,32 @@ import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
|
||||||
import { styles } from './style'
|
import { styles } from './style'
|
||||||
|
|
||||||
import { getExplorerInfo } from 'src/config'
|
import { getExplorerInfo, getNetworkId } from 'src/config'
|
||||||
import Table from 'src/components/Table'
|
import Table from 'src/components/Table'
|
||||||
import { cellWidth } from 'src/components/Table/TableHead'
|
import { cellWidth } from 'src/components/Table/TableHead'
|
||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
import ButtonLink from 'src/components/layout/ButtonLink'
|
|
||||||
import Col from 'src/components/layout/Col'
|
import Col from 'src/components/layout/Col'
|
||||||
import Img from 'src/components/layout/Img'
|
|
||||||
import Row from 'src/components/layout/Row'
|
import Row from 'src/components/layout/Row'
|
||||||
import { AddressBookEntry, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
import { AddressBookEntry, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||||
import { addAddressBookEntry } from 'src/logic/addressBook/store/actions/addAddressBookEntry'
|
import { addressBookAddOrUpdate, addressBookImport, addressBookRemove } from 'src/logic/addressBook/store/actions'
|
||||||
import { removeAddressBookEntry } from 'src/logic/addressBook/store/actions/removeAddressBookEntry'
|
|
||||||
import { updateAddressBookEntry } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
|
|
||||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||||
import { isUserAnOwnerOfAnySafe, sameAddress } from 'src/logic/wallets/ethAddresses'
|
import { isUserAnOwnerOfAnySafe, sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||||
import { CreateEditEntryModal } from 'src/routes/safe/components/AddressBook/CreateEditEntryModal'
|
import { CreateEditEntryModal } from 'src/routes/safe/components/AddressBook/CreateEditEntryModal'
|
||||||
|
import { ExportEntriesModal } from 'src/routes/safe/components/AddressBook/ExportEntriesModal'
|
||||||
import { DeleteEntryModal } from 'src/routes/safe/components/AddressBook/DeleteEntryModal'
|
import { DeleteEntryModal } from 'src/routes/safe/components/AddressBook/DeleteEntryModal'
|
||||||
import {
|
import {
|
||||||
|
AB_NAME_ID,
|
||||||
AB_ADDRESS_ID,
|
AB_ADDRESS_ID,
|
||||||
ADDRESS_BOOK_ROW_ID,
|
ADDRESS_BOOK_ROW_ID,
|
||||||
EDIT_ENTRY_BUTTON,
|
|
||||||
REMOVE_ENTRY_BUTTON,
|
|
||||||
SEND_ENTRY_BUTTON,
|
SEND_ENTRY_BUTTON,
|
||||||
generateColumns,
|
generateColumns,
|
||||||
} from 'src/routes/safe/components/AddressBook/columns'
|
} from 'src/routes/safe/components/AddressBook/columns'
|
||||||
import SendModal from 'src/routes/safe/components/Balances/SendModal'
|
import SendModal from 'src/routes/safe/components/Balances/SendModal'
|
||||||
import RenameOwnerIcon from 'src/routes/safe/components/Settings/ManageOwners/assets/icons/rename-owner.svg'
|
|
||||||
import RemoveOwnerIcon from 'src/routes/safe/components/Settings/assets/icons/bin.svg'
|
|
||||||
import { addressBookQueryParamsSelector, safesListSelector } from 'src/logic/safe/store/selectors'
|
import { addressBookQueryParamsSelector, safesListSelector } from 'src/logic/safe/store/selectors'
|
||||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||||
import { grantedSelector } from 'src/routes/safe/container/selector'
|
import { grantedSelector } from 'src/routes/safe/container/selector'
|
||||||
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
|
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
|
||||||
|
import ImportEntriesModal from './ImportEntriesModal'
|
||||||
|
|
||||||
const StyledButton = styled(Button)`
|
const StyledButton = styled(Button)`
|
||||||
&&.MuiButton-root {
|
&&.MuiButton-root {
|
||||||
|
@ -48,10 +43,22 @@ const StyledButton = styled(Button)`
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
min-width: auto;
|
min-width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
margin: 0 6px 0 0;
|
margin: 0 6px 0 0;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
const UnStyledButton = styled.button`
|
||||||
|
background: none;
|
||||||
|
color: inherit;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
outline-color: ${({ theme }) => theme.colors.icon};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
`
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
interface AddressBookSelectedEntry extends AddressBookEntry {
|
interface AddressBookSelectedEntry extends AddressBookEntry {
|
||||||
|
@ -64,7 +71,8 @@ export type Entry = {
|
||||||
isOwnerAddress?: boolean
|
isOwnerAddress?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialEntryState: Entry = { entry: { address: '', name: '', isNew: true } }
|
const chainId = getNetworkId()
|
||||||
|
const initialEntryState: Entry = { entry: { address: '', name: '', chainId, isNew: true } }
|
||||||
|
|
||||||
const AddressBookTable = (): ReactElement => {
|
const AddressBookTable = (): ReactElement => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
@ -77,7 +85,9 @@ const AddressBookTable = (): ReactElement => {
|
||||||
const granted = useSelector(grantedSelector)
|
const granted = useSelector(grantedSelector)
|
||||||
const [selectedEntry, setSelectedEntry] = useState<Entry>(initialEntryState)
|
const [selectedEntry, setSelectedEntry] = useState<Entry>(initialEntryState)
|
||||||
const [editCreateEntryModalOpen, setEditCreateEntryModalOpen] = useState(false)
|
const [editCreateEntryModalOpen, setEditCreateEntryModalOpen] = useState(false)
|
||||||
|
const [importEntryModalOpen, setImportEntryModalOpen] = useState(false)
|
||||||
const [deleteEntryModalOpen, setDeleteEntryModalOpen] = useState(false)
|
const [deleteEntryModalOpen, setDeleteEntryModalOpen] = useState(false)
|
||||||
|
const [exportEntriesModalOpen, setExportEntriesModalOpen] = useState(false)
|
||||||
const [sendFundsModalOpen, setSendFundsModalOpen] = useState(false)
|
const [sendFundsModalOpen, setSendFundsModalOpen] = useState(false)
|
||||||
const { trackEvent } = useAnalytics()
|
const { trackEvent } = useAnalytics()
|
||||||
|
|
||||||
|
@ -105,6 +115,7 @@ const AddressBookTable = (): ReactElement => {
|
||||||
entry: {
|
entry: {
|
||||||
name: '',
|
name: '',
|
||||||
address,
|
address,
|
||||||
|
chainId,
|
||||||
isNew: true,
|
isNew: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -113,44 +124,73 @@ const AddressBookTable = (): ReactElement => {
|
||||||
}, [addressBook, entryAddressToEditOrCreateNew])
|
}, [addressBook, entryAddressToEditOrCreateNew])
|
||||||
|
|
||||||
const newEntryModalHandler = (entry: AddressBookEntry) => {
|
const newEntryModalHandler = (entry: AddressBookEntry) => {
|
||||||
|
// close the modal
|
||||||
setEditCreateEntryModalOpen(false)
|
setEditCreateEntryModalOpen(false)
|
||||||
const checksumEntries = {
|
// update the store
|
||||||
...entry,
|
dispatch(addressBookAddOrUpdate(makeAddressBookEntry({ ...entry, address: checksumAddress(entry.address) })))
|
||||||
address: checksumAddress(entry.address),
|
|
||||||
}
|
|
||||||
dispatch(addAddressBookEntry(makeAddressBookEntry(checksumEntries)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const editEntryModalHandler = (entry: AddressBookEntry) => {
|
const editEntryModalHandler = (entry: AddressBookEntry) => {
|
||||||
|
// reset the form
|
||||||
setSelectedEntry(initialEntryState)
|
setSelectedEntry(initialEntryState)
|
||||||
|
// close the modal
|
||||||
setEditCreateEntryModalOpen(false)
|
setEditCreateEntryModalOpen(false)
|
||||||
const checksumEntries = {
|
// update the store
|
||||||
...entry,
|
dispatch(addressBookAddOrUpdate(makeAddressBookEntry({ ...entry, address: checksumAddress(entry.address) })))
|
||||||
address: checksumAddress(entry.address),
|
|
||||||
}
|
|
||||||
dispatch(updateAddressBookEntry(makeAddressBookEntry(checksumEntries)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteEntryModalHandler = () => {
|
const deleteEntryModalHandler = () => {
|
||||||
const entryAddress = selectedEntry?.entry ? checksumAddress(selectedEntry.entry.address) : ''
|
// reset the form
|
||||||
setSelectedEntry(initialEntryState)
|
setSelectedEntry(initialEntryState)
|
||||||
|
// close the modal
|
||||||
setDeleteEntryModalOpen(false)
|
setDeleteEntryModalOpen(false)
|
||||||
dispatch(removeAddressBookEntry(entryAddress))
|
// update the store
|
||||||
|
selectedEntry?.entry && dispatch(addressBookRemove(selectedEntry.entry))
|
||||||
|
}
|
||||||
|
|
||||||
|
const importEntryModalHandler = (addressList: AddressBookEntry[]) => {
|
||||||
|
dispatch(addressBookImport(addressList))
|
||||||
|
setImportEntryModalOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Row align="center" className={classes.message}>
|
<Row align="center" className={classes.message}>
|
||||||
<Col end="sm" xs={12}>
|
<Col end="sm" xs={12}>
|
||||||
|
<ButtonLink
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedEntry(initialEntryState)
|
||||||
|
setExportEntriesModalOpen(true)
|
||||||
|
}}
|
||||||
|
color="primary"
|
||||||
|
iconType="exportImg"
|
||||||
|
iconSize="sm"
|
||||||
|
textSize="xl"
|
||||||
|
>
|
||||||
|
Export
|
||||||
|
</ButtonLink>
|
||||||
|
<ButtonLink
|
||||||
|
onClick={() => {
|
||||||
|
setImportEntryModalOpen(true)
|
||||||
|
}}
|
||||||
|
color="primary"
|
||||||
|
iconType="importImg"
|
||||||
|
iconSize="sm"
|
||||||
|
textSize="xl"
|
||||||
|
>
|
||||||
|
Import
|
||||||
|
</ButtonLink>
|
||||||
<ButtonLink
|
<ButtonLink
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedEntry(initialEntryState)
|
setSelectedEntry(initialEntryState)
|
||||||
setEditCreateEntryModalOpen(true)
|
setEditCreateEntryModalOpen(true)
|
||||||
}}
|
}}
|
||||||
size="lg"
|
color="primary"
|
||||||
testId="manage-tokens-btn"
|
iconType="add"
|
||||||
|
iconSize="sm"
|
||||||
|
textSize="xl"
|
||||||
>
|
>
|
||||||
+ Create entry
|
Create entry
|
||||||
</ButtonLink>
|
</ButtonLink>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -160,6 +200,7 @@ const AddressBookTable = (): ReactElement => {
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={addressBook}
|
data={addressBook}
|
||||||
defaultFixed
|
defaultFixed
|
||||||
|
defaultOrderBy={AB_NAME_ID}
|
||||||
defaultRowsPerPage={25}
|
defaultRowsPerPage={25}
|
||||||
disableLoadingOnEmptyTable
|
disableLoadingOnEmptyTable
|
||||||
label="Owners"
|
label="Owners"
|
||||||
|
@ -196,9 +237,7 @@ const AddressBookTable = (): ReactElement => {
|
||||||
})}
|
})}
|
||||||
<TableCell component="td">
|
<TableCell component="td">
|
||||||
<Row align="end" className={classes.actions}>
|
<Row align="end" className={classes.actions}>
|
||||||
<Img
|
<UnStyledButton
|
||||||
alt="Edit entry"
|
|
||||||
className={granted ? classes.editEntryButton : classes.editEntryButtonNonOwner}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedEntry({
|
setSelectedEntry({
|
||||||
entry: row,
|
entry: row,
|
||||||
|
@ -206,19 +245,28 @@ const AddressBookTable = (): ReactElement => {
|
||||||
})
|
})
|
||||||
setEditCreateEntryModalOpen(true)
|
setEditCreateEntryModalOpen(true)
|
||||||
}}
|
}}
|
||||||
src={RenameOwnerIcon}
|
>
|
||||||
testId={EDIT_ENTRY_BUTTON}
|
<Icon
|
||||||
/>
|
size="sm"
|
||||||
<Img
|
type="edit"
|
||||||
alt="Remove entry"
|
tooltip="Edit entry"
|
||||||
className={granted ? classes.removeEntryButton : classes.removeEntryButtonNonOwner}
|
className={granted ? classes.editEntryButton : classes.editEntryButtonNonOwner}
|
||||||
|
/>
|
||||||
|
</UnStyledButton>
|
||||||
|
<UnStyledButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedEntry({ entry: row })
|
setSelectedEntry({ entry: row })
|
||||||
setDeleteEntryModalOpen(true)
|
setDeleteEntryModalOpen(true)
|
||||||
}}
|
}}
|
||||||
src={RemoveOwnerIcon}
|
>
|
||||||
testId={REMOVE_ENTRY_BUTTON}
|
<Icon
|
||||||
/>
|
size="sm"
|
||||||
|
type="delete"
|
||||||
|
color="error"
|
||||||
|
tooltip="Delete entry"
|
||||||
|
className={granted ? classes.removeEntryButton : classes.removeEntryButtonNonOwner}
|
||||||
|
/>
|
||||||
|
</UnStyledButton>
|
||||||
{granted ? (
|
{granted ? (
|
||||||
<StyledButton
|
<StyledButton
|
||||||
color="primary"
|
color="primary"
|
||||||
|
@ -258,6 +306,12 @@ const AddressBookTable = (): ReactElement => {
|
||||||
isOpen={deleteEntryModalOpen}
|
isOpen={deleteEntryModalOpen}
|
||||||
onClose={() => setDeleteEntryModalOpen(false)}
|
onClose={() => setDeleteEntryModalOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
<ExportEntriesModal isOpen={exportEntriesModalOpen} onClose={() => setExportEntriesModalOpen(false)} />
|
||||||
|
<ImportEntriesModal
|
||||||
|
importEntryModalHandler={importEntryModalHandler}
|
||||||
|
isOpen={importEntryModalOpen}
|
||||||
|
onClose={() => setImportEntryModalOpen(false)}
|
||||||
|
/>
|
||||||
<SendModal
|
<SendModal
|
||||||
activeScreenType="chooseTxType"
|
activeScreenType="chooseTxType"
|
||||||
isOpen={sendFundsModalOpen}
|
isOpen={sendFundsModalOpen}
|
||||||
|
|
|
@ -6,11 +6,8 @@ import { useHistory } from 'react-router-dom'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { INTERFACE_MESSAGES, Transaction, RequestId, LowercaseNetworks } from '@gnosis.pm/safe-apps-sdk-v1'
|
import { INTERFACE_MESSAGES, Transaction, RequestId, LowercaseNetworks } from '@gnosis.pm/safe-apps-sdk-v1'
|
||||||
|
|
||||||
import {
|
import { safeEthBalanceSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||||
safeEthBalanceSelector,
|
import { useSafeName } from 'src/logic/addressBook/hooks/useSafeName'
|
||||||
safeParamAddressFromStateSelector,
|
|
||||||
safeNameSelector,
|
|
||||||
} from 'src/logic/safe/store/selectors'
|
|
||||||
import { grantedSelector } from 'src/routes/safe/container/selector'
|
import { grantedSelector } from 'src/routes/safe/container/selector'
|
||||||
import { getNetworkId, getNetworkName, getTxServiceUrl } from 'src/config'
|
import { getNetworkId, getNetworkName, getTxServiceUrl } from 'src/config'
|
||||||
import { SAFELIST_ADDRESS } from 'src/routes/routes'
|
import { SAFELIST_ADDRESS } from 'src/routes/routes'
|
||||||
|
@ -89,7 +86,7 @@ const AppFrame = ({ appUrl }: Props): ReactElement => {
|
||||||
const granted = useSelector(grantedSelector)
|
const granted = useSelector(grantedSelector)
|
||||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||||
const ethBalance = useSelector(safeEthBalanceSelector)
|
const ethBalance = useSelector(safeEthBalanceSelector)
|
||||||
const safeName = useSelector(safeNameSelector)
|
const safeName = useSafeName(safeAddress)
|
||||||
const { trackEvent } = useAnalytics()
|
const { trackEvent } = useAnalytics()
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const { consentReceived, onConsentReceipt } = useLegalConsent()
|
const { consentReceived, onConsentReceipt } = useLegalConsent()
|
||||||
|
|
|
@ -12,12 +12,10 @@ import {
|
||||||
} from '@gnosis.pm/safe-apps-sdk-v1'
|
} from '@gnosis.pm/safe-apps-sdk-v1'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
import { useEffect, useCallback, MutableRefObject } from 'react'
|
import { useEffect, useCallback, MutableRefObject } from 'react'
|
||||||
|
|
||||||
import { getNetworkName, getTxServiceUrl } from 'src/config/'
|
import { getNetworkName, getTxServiceUrl } from 'src/config/'
|
||||||
import {
|
import { useSafeName } from 'src/logic/addressBook/hooks/useSafeName'
|
||||||
safeEthBalanceSelector,
|
import { safeEthBalanceSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||||
safeNameSelector,
|
|
||||||
safeParamAddressFromStateSelector,
|
|
||||||
} from 'src/logic/safe/store/selectors'
|
|
||||||
import { TransactionParams } from '../components/AppFrame'
|
import { TransactionParams } from '../components/AppFrame'
|
||||||
import { SafeApp } from 'src/routes/safe/components/Apps/types'
|
import { SafeApp } from 'src/routes/safe/components/Apps/types'
|
||||||
|
|
||||||
|
@ -39,8 +37,8 @@ const useIframeMessageHandler = (
|
||||||
iframeRef: MutableRefObject<HTMLIFrameElement | null>,
|
iframeRef: MutableRefObject<HTMLIFrameElement | null>,
|
||||||
): ReturnType => {
|
): ReturnType => {
|
||||||
const { enqueueSnackbar, closeSnackbar } = useSnackbar()
|
const { enqueueSnackbar, closeSnackbar } = useSnackbar()
|
||||||
const safeName = useSelector(safeNameSelector)
|
|
||||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||||
|
const safeName = useSafeName(safeAddress)
|
||||||
const ethBalance = useSelector(safeEthBalanceSelector)
|
const ethBalance = useSelector(safeEthBalanceSelector)
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useSafeAppUrl } from 'src/logic/hooks/useSafeAppUrl'
|
|
||||||
|
import { useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
import AppFrame from './components/AppFrame'
|
import AppFrame from './components/AppFrame'
|
||||||
import AppsList from './components/AppsList'
|
import AppsList from './components/AppsList'
|
||||||
|
|
||||||
const Apps = (): React.ReactElement => {
|
const useQuery = () => {
|
||||||
const { url } = useSafeAppUrl()
|
return new URLSearchParams(useLocation().search)
|
||||||
|
}
|
||||||
|
|
||||||
if (url) {
|
const Apps = (): React.ReactElement => {
|
||||||
return <AppFrame appUrl={url} />
|
const query = useQuery()
|
||||||
|
const appUrl = query.get('appUrl')
|
||||||
|
|
||||||
|
if (appUrl) {
|
||||||
|
return <AppFrame appUrl={appUrl} />
|
||||||
} else {
|
} else {
|
||||||
return <AppsList />
|
return <AppsList />
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,12 @@ import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import { getExplorerInfo, getNetworkInfo } from 'src/config'
|
import { getExplorerInfo, getNetworkInfo } from 'src/config'
|
||||||
import { safeSelector } from 'src/logic/safe/store/selectors'
|
import { safeNameSelector, safeSelector } from 'src/logic/safe/store/selectors'
|
||||||
import Paragraph from 'src/components/layout/Paragraph'
|
import Paragraph from 'src/components/layout/Paragraph'
|
||||||
import Bold from 'src/components/layout/Bold'
|
import Bold from 'src/components/layout/Bold'
|
||||||
import { border, xs } from 'src/theme/variables'
|
import { border, xs } from 'src/theme/variables'
|
||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
|
|
||||||
const { nativeCoin } = getNetworkInfo()
|
const { nativeCoin } = getNetworkInfo()
|
||||||
|
|
||||||
const StyledBlock = styled(Block)`
|
const StyledBlock = styled(Block)`
|
||||||
|
@ -24,7 +25,8 @@ const StyledBlock = styled(Block)`
|
||||||
`
|
`
|
||||||
|
|
||||||
const SafeInfo = (): React.ReactElement => {
|
const SafeInfo = (): React.ReactElement => {
|
||||||
const { address: safeAddress = '', ethBalance, name: safeName } = useSelector(safeSelector) || {}
|
const { address: safeAddress = '', ethBalance } = useSelector(safeSelector) || {}
|
||||||
|
const safeName = useSelector((state) => safeNameSelector(state, safeAddress))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -5,7 +5,7 @@ import React, { Dispatch, ReactElement, SetStateAction, useEffect, useState } fr
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
import { mustBeEthereumAddress, mustBeEthereumContractAddress } from 'src/components/forms/validator'
|
import { mustBeEthereumAddress, mustBeEthereumContractAddress } from 'src/components/forms/validator'
|
||||||
import { isFeatureEnabled } from 'src/config'
|
import { getNetworkId, isFeatureEnabled } from 'src/config'
|
||||||
import { FEATURES } from 'src/config/networks/network.d'
|
import { FEATURES } from 'src/config/networks/network.d'
|
||||||
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||||
|
@ -18,6 +18,8 @@ import {
|
||||||
} from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/style'
|
} from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/style'
|
||||||
import { trimSpaces } from 'src/utils/strings'
|
import { trimSpaces } from 'src/utils/strings'
|
||||||
|
|
||||||
|
const chainId = getNetworkId()
|
||||||
|
|
||||||
export interface AddressBookProps {
|
export interface AddressBookProps {
|
||||||
fieldMutator: (address: string) => void
|
fieldMutator: (address: string) => void
|
||||||
label?: string
|
label?: string
|
||||||
|
@ -65,8 +67,8 @@ const BaseAddressBookInput = ({
|
||||||
const onChange: AutocompleteProps<AddressBookEntry, false, false, true>['onChange'] = (_, value, reason) => {
|
const onChange: AutocompleteProps<AddressBookEntry, false, false, true>['onChange'] = (_, value, reason) => {
|
||||||
switch (reason) {
|
switch (reason) {
|
||||||
case 'select-option': {
|
case 'select-option': {
|
||||||
const { address, name } = value as AddressBookEntry
|
const { address, name, chainId } = value as AddressBookEntry
|
||||||
updateAddressInfo({ address, name })
|
updateAddressInfo({ address, name, chainId })
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,7 +101,14 @@ const BaseAddressBookInput = ({
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
const newEntry = typeof validatedAddress === 'string' ? { address, name: normalizedValue } : validatedAddress
|
const newEntry =
|
||||||
|
typeof validatedAddress === 'string'
|
||||||
|
? {
|
||||||
|
address,
|
||||||
|
name: normalizedValue,
|
||||||
|
chainId,
|
||||||
|
}
|
||||||
|
: validatedAddress
|
||||||
|
|
||||||
updateAddressInfo(newEntry)
|
updateAddressInfo(newEntry)
|
||||||
break
|
break
|
||||||
|
@ -114,7 +123,13 @@ const BaseAddressBookInput = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const newEntry =
|
const newEntry =
|
||||||
typeof validatedAddress === 'string' ? { address: validatedAddress, name: '' } : validatedAddress
|
typeof validatedAddress === 'string'
|
||||||
|
? {
|
||||||
|
address: validatedAddress,
|
||||||
|
name: '',
|
||||||
|
chainId,
|
||||||
|
}
|
||||||
|
: validatedAddress
|
||||||
|
|
||||||
updateAddressInfo(newEntry)
|
updateAddressInfo(newEntry)
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ import {
|
||||||
getValueFromTxInputs,
|
getValueFromTxInputs,
|
||||||
} from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils'
|
} from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils'
|
||||||
import { useEstimateTransactionGas, EstimationStatus } from 'src/logic/hooks/useEstimateTransactionGas'
|
import { useEstimateTransactionGas, EstimationStatus } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||||
|
import { getNameFromAddressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||||
import { useEstimationStatus } from 'src/logic/hooks/useEstimationStatus'
|
import { useEstimationStatus } from 'src/logic/hooks/useEstimationStatus'
|
||||||
import { ButtonStatus, Modal } from 'src/components/Modal'
|
import { ButtonStatus, Modal } from 'src/components/Modal'
|
||||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||||
|
@ -60,6 +61,9 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE
|
||||||
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
|
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
|
||||||
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
|
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
|
||||||
const [manualGasLimit, setManualGasLimit] = useState<string | undefined>()
|
const [manualGasLimit, setManualGasLimit] = useState<string | undefined>()
|
||||||
|
const addressName = useSelector((state) =>
|
||||||
|
getNameFromAddressBookSelector(state, { address: tx.contractAddress as string }),
|
||||||
|
)
|
||||||
|
|
||||||
const [txInfo, setTxInfo] = useState<{
|
const [txInfo, setTxInfo] = useState<{
|
||||||
txRecipient: string
|
txRecipient: string
|
||||||
|
@ -154,7 +158,13 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Row>
|
</Row>
|
||||||
<Row align="center" margin="md">
|
<Row align="center" margin="md">
|
||||||
<EthHashInfo hash={tx.contractAddress as string} showAvatar showCopyBtn explorerUrl={explorerUrl} />
|
<EthHashInfo
|
||||||
|
hash={tx.contractAddress as string}
|
||||||
|
name={addressName}
|
||||||
|
showAvatar
|
||||||
|
showCopyBtn
|
||||||
|
explorerUrl={explorerUrl}
|
||||||
|
/>
|
||||||
</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' }}>
|
||||||
|
|
|
@ -35,6 +35,7 @@ const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
export type CollectibleTx = {
|
export type CollectibleTx = {
|
||||||
recipientAddress: string
|
recipientAddress: string
|
||||||
|
recipientName?: string
|
||||||
assetAddress: string
|
assetAddress: string
|
||||||
assetName: string
|
assetName: string
|
||||||
nftTokenId: string
|
nftTokenId: string
|
||||||
|
@ -177,6 +178,7 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement =
|
||||||
<Col xs={12}>
|
<Col xs={12}>
|
||||||
<EthHashInfo
|
<EthHashInfo
|
||||||
hash={tx.recipientAddress}
|
hash={tx.recipientAddress}
|
||||||
|
name={tx.recipientName}
|
||||||
showAvatar
|
showAvatar
|
||||||
showCopyBtn
|
showCopyBtn
|
||||||
explorerUrl={getExplorerInfo(tx.recipientAddress)}
|
explorerUrl={getExplorerInfo(tx.recipientAddress)}
|
||||||
|
|
|
@ -57,6 +57,7 @@ export type SendCollectibleTxInfo = {
|
||||||
assetName: string
|
assetName: string
|
||||||
nftTokenId: string
|
nftTokenId: string
|
||||||
recipientAddress?: string
|
recipientAddress?: string
|
||||||
|
recipientName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const SendCollectible = ({
|
const SendCollectible = ({
|
||||||
|
@ -106,7 +107,7 @@ const SendCollectible = ({
|
||||||
if (!values.recipientAddress) {
|
if (!values.recipientAddress) {
|
||||||
values.recipientAddress = selectedEntry?.address
|
values.recipientAddress = selectedEntry?.address
|
||||||
}
|
}
|
||||||
|
values.recipientName = selectedEntry?.name
|
||||||
values.assetName = nftAssets[values.assetAddress].name
|
values.assetName = nftAssets[values.assetAddress].name
|
||||||
|
|
||||||
onNext(values)
|
onNext(values)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { ReactElement, useEffect, useState } from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
import ReceiveModal from 'src/components/App/ReceiveModal'
|
import ReceiveModal from 'src/components/App/ReceiveModal'
|
||||||
|
@ -13,11 +13,8 @@ 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 {
|
import { useSafeName } from 'src/logic/addressBook/hooks/useSafeName'
|
||||||
safeFeaturesEnabledSelector,
|
import { safeFeaturesEnabledSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||||
safeNameSelector,
|
|
||||||
safeParamAddressFromStateSelector,
|
|
||||||
} 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'
|
||||||
|
@ -45,13 +42,13 @@ export const COLLECTIBLES_LOCATION_REGEX = /\/balances\/collectibles$/
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const Balances = (): React.ReactElement => {
|
const Balances = (): ReactElement => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const [state, setState] = useState(INITIAL_STATE)
|
const [state, setState] = useState(INITIAL_STATE)
|
||||||
|
|
||||||
const address = useSelector(safeParamAddressFromStateSelector)
|
const address = useSelector(safeParamAddressFromStateSelector)
|
||||||
const featuresEnabled = useSelector(safeFeaturesEnabledSelector)
|
const featuresEnabled = useSelector(safeFeaturesEnabledSelector)
|
||||||
const safeName = useSelector(safeNameSelector) ?? ''
|
const safeName = useSafeName(address)
|
||||||
|
|
||||||
useFetchTokens(address)
|
useFetchTokens(address)
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,9 @@ import React, { useEffect, useState } from 'react'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
|
||||||
import Modal from 'src/components/Modal'
|
import Modal from 'src/components/Modal'
|
||||||
import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
|
import { addressBookAddOrUpdate } from 'src/logic/addressBook/store/actions'
|
||||||
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||||
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||||
import { addSafeOwner } from 'src/logic/safe/store/actions/addSafeOwner'
|
|
||||||
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
|
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
|
||||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||||
|
@ -46,7 +45,7 @@ export const sendAddOwner = async (
|
||||||
)
|
)
|
||||||
|
|
||||||
if (txHash) {
|
if (txHash) {
|
||||||
dispatch(addSafeOwner({ safeAddress, ownerName: values.ownerName, ownerAddress: values.ownerAddress }))
|
dispatch(addressBookAddOrUpdate(makeAddressBookEntry({ address: values.ownerAddress, name: values.ownerName })))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,9 +98,7 @@ export const AddOwnerModal = ({ isOpen, onClose }: Props): React.ReactElement =>
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sendAddOwner(values, safeAddress, txParameters, dispatch)
|
await sendAddOwner(values, safeAddress, txParameters, dispatch)
|
||||||
dispatch(
|
dispatch(addressBookAddOrUpdate(makeAddressBookEntry({ name: values.ownerName, address: values.ownerAddress })))
|
||||||
addOrUpdateAddressBookEntry(makeAddressBookEntry({ name: values.ownerName, address: values.ownerAddress })),
|
|
||||||
)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error while removing an owner', error)
|
console.error('Error while removing an owner', error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
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 { Mutator } from 'final-form'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
|
import { OnChange } from 'react-final-form-listeners'
|
||||||
|
|
||||||
import { styles } from './style'
|
import { styles } from './style'
|
||||||
|
|
||||||
|
import { getNetworkId } from 'src/config'
|
||||||
import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
|
import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
|
||||||
import AddressInput from 'src/components/forms/AddressInput'
|
import AddressInput from 'src/components/forms/AddressInput'
|
||||||
import Field from 'src/components/forms/Field'
|
import Field from 'src/components/forms/Field'
|
||||||
|
@ -14,16 +17,18 @@ import TextField from 'src/components/forms/TextField'
|
||||||
import {
|
import {
|
||||||
addressIsNotCurrentSafe,
|
addressIsNotCurrentSafe,
|
||||||
composeValidators,
|
composeValidators,
|
||||||
minMaxLength,
|
|
||||||
required,
|
required,
|
||||||
uniqueAddress,
|
uniqueAddress,
|
||||||
|
validAddressBookName,
|
||||||
} from 'src/components/forms/validator'
|
} from 'src/components/forms/validator'
|
||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
import Col from 'src/components/layout/Col'
|
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 { safeOwnersAddressesListSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
import { addressBookMapSelector } from 'src/logic/addressBook/store/selectors'
|
||||||
|
import { safeOwnersSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||||
|
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
|
||||||
|
|
||||||
import { OwnerValues } from '../..'
|
import { OwnerValues } from '../..'
|
||||||
import { Modal } from 'src/components/Modal'
|
import { Modal } from 'src/components/Modal'
|
||||||
|
@ -32,14 +37,22 @@ export const ADD_OWNER_NAME_INPUT_TEST_ID = 'add-owner-name-input'
|
||||||
export const ADD_OWNER_ADDRESS_INPUT_TEST_ID = 'add-owner-address-testid'
|
export const ADD_OWNER_ADDRESS_INPUT_TEST_ID = 'add-owner-address-testid'
|
||||||
export const ADD_OWNER_NEXT_BTN_TEST_ID = 'add-owner-next-btn'
|
export const ADD_OWNER_NEXT_BTN_TEST_ID = 'add-owner-next-btn'
|
||||||
|
|
||||||
const formMutators = {
|
const formMutators: Record<
|
||||||
|
string,
|
||||||
|
Mutator<{ setOwnerAddress: { address: string }; setOwnerName: { name: string } }>
|
||||||
|
> = {
|
||||||
setOwnerAddress: (args, state, utils) => {
|
setOwnerAddress: (args, state, utils) => {
|
||||||
utils.changeValue(state, 'ownerAddress', () => args[0])
|
utils.changeValue(state, 'ownerAddress', () => args[0])
|
||||||
},
|
},
|
||||||
|
setOwnerName: (args, state, utils) => {
|
||||||
|
utils.changeValue(state, 'ownerName', () => args[0])
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const chainId = getNetworkId()
|
||||||
|
|
||||||
type OwnerFormProps = {
|
type OwnerFormProps = {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onSubmit: (values) => void
|
onSubmit: (values) => void
|
||||||
|
@ -51,7 +64,8 @@ export const OwnerForm = ({ onClose, onSubmit, initialValues }: OwnerFormProps):
|
||||||
const handleSubmit = (values) => {
|
const handleSubmit = (values) => {
|
||||||
onSubmit(values)
|
onSubmit(values)
|
||||||
}
|
}
|
||||||
const owners = useSelector(safeOwnersAddressesListSelector)
|
const addressBookMap = useSelector(addressBookMapSelector)
|
||||||
|
const owners = useSelector(safeOwnersSelector)
|
||||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||||
const ownerDoesntExist = uniqueAddress(owners)
|
const ownerDoesntExist = uniqueAddress(owners)
|
||||||
const ownerAddressIsNotSafeAddress = addressIsNotCurrentSafe(safeAddress)
|
const ownerAddressIsNotSafeAddress = addressIsNotCurrentSafe(safeAddress)
|
||||||
|
@ -104,8 +118,18 @@ export const OwnerForm = ({ onClose, onSubmit, initialValues }: OwnerFormProps):
|
||||||
testId={ADD_OWNER_NAME_INPUT_TEST_ID}
|
testId={ADD_OWNER_NAME_INPUT_TEST_ID}
|
||||||
text="Owner name*"
|
text="Owner name*"
|
||||||
type="text"
|
type="text"
|
||||||
validate={composeValidators(required, minMaxLength(1, 50))}
|
validate={composeValidators(required, validAddressBookName)}
|
||||||
/>
|
/>
|
||||||
|
<OnChange name="ownerAddress">
|
||||||
|
{async (address: string) => {
|
||||||
|
if (web3ReadOnly.utils.isAddress(address)) {
|
||||||
|
const { name: ownerName } = addressBookMap[chainId][address]
|
||||||
|
if (ownerName) {
|
||||||
|
mutators.setOwnerName(ownerName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
</OnChange>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row margin="md">
|
<Row margin="md">
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
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 React, { useEffect, useState } from 'react'
|
import React, { ReactElement, useEffect, useState } from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
||||||
|
|
||||||
import { getExplorerInfo } from 'src/config'
|
import { getExplorerInfo, getNetworkId } from 'src/config'
|
||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
import Col from 'src/components/layout/Col'
|
import Col from 'src/components/layout/Col'
|
||||||
import Hairline from 'src/components/layout/Hairline'
|
import Hairline from 'src/components/layout/Hairline'
|
||||||
|
@ -13,7 +13,11 @@ import Paragraph from 'src/components/layout/Paragraph'
|
||||||
import Row from 'src/components/layout/Row'
|
import Row from 'src/components/layout/Row'
|
||||||
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||||
import { useEstimationStatus } from 'src/logic/hooks/useEstimationStatus'
|
import { useEstimationStatus } from 'src/logic/hooks/useEstimationStatus'
|
||||||
import { safeNameSelector, safeOwnersSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
import { useSafeName } from 'src/logic/addressBook/hooks/useSafeName'
|
||||||
|
import {
|
||||||
|
safeOwnersWithAddressBookDataSelector,
|
||||||
|
safeParamAddressFromStateSelector,
|
||||||
|
} from 'src/logic/safe/store/selectors'
|
||||||
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
||||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||||
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||||
|
@ -28,6 +32,8 @@ export const ADD_OWNER_SUBMIT_BTN_TEST_ID = 'add-owner-submit-btn'
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const chainId = getNetworkId()
|
||||||
|
|
||||||
type ReviewAddOwnerProps = {
|
type ReviewAddOwnerProps = {
|
||||||
onClickBack: () => void
|
onClickBack: () => void
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
|
@ -35,12 +41,12 @@ type ReviewAddOwnerProps = {
|
||||||
values: OwnerValues
|
values: OwnerValues
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ReviewAddOwner = ({ onClickBack, onClose, onSubmit, values }: ReviewAddOwnerProps): React.ReactElement => {
|
export const ReviewAddOwner = ({ onClickBack, onClose, onSubmit, values }: ReviewAddOwnerProps): ReactElement => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const [data, setData] = useState('')
|
const [data, setData] = useState('')
|
||||||
const safeAddress = useSelector(safeParamAddressFromStateSelector) as string
|
const safeAddress = useSelector(safeParamAddressFromStateSelector) as string
|
||||||
const safeName = useSelector(safeNameSelector)
|
const safeName = useSafeName(safeAddress)
|
||||||
const owners = useSelector(safeOwnersSelector)
|
const owners = useSelector((state) => safeOwnersWithAddressBookDataSelector(state, chainId))
|
||||||
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
|
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
|
||||||
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
|
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
|
||||||
const [manualGasLimit, setManualGasLimit] = useState<string | undefined>()
|
const [manualGasLimit, setManualGasLimit] = useState<string | undefined>()
|
||||||
|
@ -148,7 +154,7 @@ export const ReviewAddOwner = ({ onClickBack, onClose, onSubmit, values }: Revie
|
||||||
Any transaction requires the confirmation of:
|
Any transaction requires the confirmation of:
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
|
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
|
||||||
{`${values.threshold} out of ${(owners?.size || 0) + 1} owner(s)`}
|
{`${values.threshold} out of ${(owners?.length || 0) + 1} owner(s)`}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Block>
|
</Block>
|
||||||
</Block>
|
</Block>
|
||||||
|
@ -156,7 +162,7 @@ export const ReviewAddOwner = ({ onClickBack, onClose, onSubmit, values }: Revie
|
||||||
<Col className={classes.owners} layout="column" xs={8}>
|
<Col className={classes.owners} layout="column" xs={8}>
|
||||||
<Row className={classes.ownersTitle}>
|
<Row className={classes.ownersTitle}>
|
||||||
<Paragraph color="primary" noMargin size="lg">
|
<Paragraph color="primary" noMargin size="lg">
|
||||||
{`${(owners?.size || 0) + 1} Safe owner(s)`}
|
{`${(owners?.length || 0) + 1} Safe owner(s)`}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Row>
|
</Row>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
|
|
|
@ -42,6 +42,7 @@ export const ThresholdForm = ({ onClickBack, onClose, onSubmit, initialValues }:
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const threshold = useSelector(safeThresholdSelector) as number
|
const threshold = useSelector(safeThresholdSelector) as number
|
||||||
const owners = useSelector(safeOwnersSelector)
|
const owners = useSelector(safeOwnersSelector)
|
||||||
|
const numOptions = owners ? owners.length + 1 : 0
|
||||||
|
|
||||||
const handleSubmit = (values: SubmitProps) => {
|
const handleSubmit = (values: SubmitProps) => {
|
||||||
onSubmit(values)
|
onSubmit(values)
|
||||||
|
@ -79,7 +80,7 @@ export const ThresholdForm = ({ onClickBack, onClose, onSubmit, initialValues }:
|
||||||
render={(props) => (
|
render={(props) => (
|
||||||
<>
|
<>
|
||||||
<SelectField {...props} disableError>
|
<SelectField {...props} disableError>
|
||||||
{[...Array(Number(owners ? owners.size + 1 : 0))].map((x, index) => (
|
{[...Array(Number(numOptions))].map((x, index) => (
|
||||||
<MenuItem key={index} value={`${index + 1}`}>
|
<MenuItem key={index} value={`${index + 1}`}>
|
||||||
{index + 1}
|
{index + 1}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -92,17 +93,12 @@ export const ThresholdForm = ({ onClickBack, onClose, onSubmit, initialValues }:
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
validate={composeValidators(
|
validate={composeValidators(required, mustBeInteger, minValue(1), maxValue(numOptions))}
|
||||||
required,
|
|
||||||
mustBeInteger,
|
|
||||||
minValue(1),
|
|
||||||
maxValue(owners ? owners.size + 1 : 0),
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={10}>
|
<Col xs={10}>
|
||||||
<Paragraph className={classes.ownersText} color="primary" noMargin size="lg">
|
<Paragraph className={classes.ownersText} color="primary" noMargin size="lg">
|
||||||
out of {owners ? owners.size + 1 : 0} owner(s)
|
out of {numOptions} owner(s)
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -1,23 +1,22 @@
|
||||||
import IconButton from '@material-ui/core/IconButton'
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
import Close from '@material-ui/icons/Close'
|
import Close from '@material-ui/icons/Close'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch } from 'react-redux'
|
||||||
|
|
||||||
import Field from 'src/components/forms/Field'
|
import Field from 'src/components/forms/Field'
|
||||||
import GnoForm from 'src/components/forms/GnoForm'
|
import GnoForm from 'src/components/forms/GnoForm'
|
||||||
import TextField from 'src/components/forms/TextField'
|
import TextField from 'src/components/forms/TextField'
|
||||||
import { composeValidators, minMaxLength, required } from 'src/components/forms/validator'
|
import { composeValidators, required, validAddressBookName } from 'src/components/forms/validator'
|
||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
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 Modal, { Modal as GenericModal } from 'src/components/Modal'
|
import Modal, { Modal as GenericModal } from 'src/components/Modal'
|
||||||
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||||
import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
|
import { addressBookAddOrUpdate } from 'src/logic/addressBook/store/actions'
|
||||||
import { NOTIFICATIONS } from 'src/logic/notifications'
|
import { NOTIFICATIONS } from 'src/logic/notifications'
|
||||||
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
|
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
|
||||||
import editSafeOwner from 'src/logic/safe/store/actions/editSafeOwner'
|
import { OwnerData } from 'src/routes/safe/components/Settings/ManageOwners/dataFetcher'
|
||||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
|
||||||
|
|
||||||
import { useStyles } from './style'
|
import { useStyles } from './style'
|
||||||
import { getExplorerInfo } from 'src/config'
|
import { getExplorerInfo } from 'src/config'
|
||||||
|
@ -29,20 +28,17 @@ export const SAVE_OWNER_CHANGES_BTN_TEST_ID = 'save-owner-changes-btn'
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
ownerAddress: string
|
owner: OwnerData
|
||||||
selectedOwnerName: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EditOwnerModal = ({ isOpen, onClose, ownerAddress, selectedOwnerName }: OwnProps): React.ReactElement => {
|
export const EditOwnerModal = ({ isOpen, onClose, owner }: OwnProps): React.ReactElement => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
|
||||||
|
|
||||||
const handleSubmit = ({ ownerName }: { ownerName: string }): void => {
|
const handleSubmit = ({ ownerName }: { ownerName: string }): void => {
|
||||||
// Update the value only if the ownerName really changed
|
// Update the value only if the ownerName really changed
|
||||||
if (ownerName !== selectedOwnerName) {
|
if (ownerName !== owner.name) {
|
||||||
dispatch(editSafeOwner({ safeAddress, ownerAddress, ownerName }))
|
dispatch(addressBookAddOrUpdate(makeAddressBookEntry({ address: owner.address, name: ownerName })))
|
||||||
dispatch(addOrUpdateAddressBookEntry(makeAddressBookEntry({ address: ownerAddress, name: ownerName })))
|
|
||||||
dispatch(enqueueSnackbar(NOTIFICATIONS.OWNER_NAME_CHANGE_EXECUTED_MSG))
|
dispatch(enqueueSnackbar(NOTIFICATIONS.OWNER_NAME_CHANGE_EXECUTED_MSG))
|
||||||
}
|
}
|
||||||
onClose()
|
onClose()
|
||||||
|
@ -74,22 +70,22 @@ export const EditOwnerModal = ({ isOpen, onClose, ownerAddress, selectedOwnerNam
|
||||||
<Row margin="md">
|
<Row margin="md">
|
||||||
<Field
|
<Field
|
||||||
component={TextField}
|
component={TextField}
|
||||||
initialValue={selectedOwnerName}
|
initialValue={owner.name}
|
||||||
name="ownerName"
|
name="ownerName"
|
||||||
placeholder="Owner name*"
|
placeholder="Owner name*"
|
||||||
testId={RENAME_OWNER_INPUT_TEST_ID}
|
testId={RENAME_OWNER_INPUT_TEST_ID}
|
||||||
text="Owner name*"
|
text="Owner name*"
|
||||||
type="text"
|
type="text"
|
||||||
validate={composeValidators(required, minMaxLength(1, 50))}
|
validate={composeValidators(required, validAddressBookName)}
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Block justify="center">
|
<Block justify="center">
|
||||||
<EthHashInfo
|
<EthHashInfo
|
||||||
hash={ownerAddress}
|
hash={owner.address}
|
||||||
showCopyBtn
|
showCopyBtn
|
||||||
showAvatar
|
showAvatar
|
||||||
explorerUrl={getExplorerInfo(ownerAddress)}
|
explorerUrl={getExplorerInfo(owner.address)}
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
import { OwnerData } from 'src/routes/safe/components/Settings/ManageOwners/dataFetcher'
|
||||||
|
|
||||||
import { CheckOwner } from './screens/CheckOwner'
|
import { CheckOwner } from './screens/CheckOwner'
|
||||||
import { ReviewRemoveOwnerModal } from './screens/Review'
|
import { ReviewRemoveOwnerModal } from './screens/Review'
|
||||||
|
@ -9,14 +10,11 @@ import Modal from 'src/components/Modal'
|
||||||
import { SENTINEL_ADDRESS, getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
import { SENTINEL_ADDRESS, getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||||
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||||
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
|
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
|
||||||
import { removeSafeOwner } from 'src/logic/safe/store/actions/removeSafeOwner'
|
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||||
import { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors'
|
|
||||||
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
||||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||||
|
|
||||||
type OwnerValues = {
|
type OwnerValues = OwnerData & {
|
||||||
ownerAddress: string
|
|
||||||
ownerName: string
|
|
||||||
threshold: string
|
threshold: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +25,6 @@ export const sendRemoveOwner = async (
|
||||||
ownerNameToRemove: string,
|
ownerNameToRemove: string,
|
||||||
dispatch: Dispatch,
|
dispatch: Dispatch,
|
||||||
txParameters: TxParameters,
|
txParameters: TxParameters,
|
||||||
threshold?: number,
|
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const gnosisSafe = getGnosisSafeInstanceAt(safeAddress)
|
const gnosisSafe = getGnosisSafeInstanceAt(safeAddress)
|
||||||
const safeOwners = await gnosisSafe.methods.getOwners().call()
|
const safeOwners = await gnosisSafe.methods.getOwners().call()
|
||||||
|
@ -37,7 +34,7 @@ export const sendRemoveOwner = async (
|
||||||
const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1]
|
const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1]
|
||||||
const txData = gnosisSafe.methods.removeOwner(prevAddress, ownerAddressToRemove, values.threshold).encodeABI()
|
const txData = gnosisSafe.methods.removeOwner(prevAddress, ownerAddressToRemove, values.threshold).encodeABI()
|
||||||
|
|
||||||
const txHash = await dispatch(
|
dispatch(
|
||||||
createTransaction({
|
createTransaction({
|
||||||
safeAddress,
|
safeAddress,
|
||||||
to: safeAddress,
|
to: safeAddress,
|
||||||
|
@ -49,30 +46,19 @@ export const sendRemoveOwner = async (
|
||||||
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
|
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (txHash && threshold === 1) {
|
|
||||||
dispatch(removeSafeOwner({ safeAddress, ownerAddress: ownerAddressToRemove }))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RemoveOwnerProps = {
|
type RemoveOwnerProps = {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
ownerAddress: string
|
owner: OwnerData
|
||||||
ownerName: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RemoveOwnerModal = ({
|
export const RemoveOwnerModal = ({ isOpen, onClose, owner }: RemoveOwnerProps): React.ReactElement => {
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
ownerAddress,
|
|
||||||
ownerName,
|
|
||||||
}: RemoveOwnerProps): React.ReactElement => {
|
|
||||||
const [activeScreen, setActiveScreen] = useState('checkOwner')
|
const [activeScreen, setActiveScreen] = useState('checkOwner')
|
||||||
const [values, setValues] = useState<OwnerValues>({ ownerAddress, ownerName, threshold: '' })
|
const [values, setValues] = useState<OwnerValues>({ ...owner, threshold: '' })
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||||
const threshold = useSelector(safeThresholdSelector) || 1
|
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => () => {
|
() => () => {
|
||||||
|
@ -101,7 +87,7 @@ export const RemoveOwnerModal = ({
|
||||||
|
|
||||||
const onRemoveOwner = (txParameters: TxParameters) => {
|
const onRemoveOwner = (txParameters: TxParameters) => {
|
||||||
onClose()
|
onClose()
|
||||||
sendRemoveOwner(values, safeAddress, ownerAddress, ownerName, dispatch, txParameters, threshold)
|
sendRemoveOwner(values, safeAddress, owner.address, owner.name, dispatch, txParameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -113,9 +99,7 @@ export const RemoveOwnerModal = ({
|
||||||
title="Remove owner from Safe"
|
title="Remove owner from Safe"
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
{activeScreen === 'checkOwner' && (
|
{activeScreen === 'checkOwner' && <CheckOwner onClose={onClose} onSubmit={ownerSubmitted} owner={owner} />}
|
||||||
<CheckOwner onClose={onClose} onSubmit={ownerSubmitted} ownerAddress={ownerAddress} ownerName={ownerName} />
|
|
||||||
)}
|
|
||||||
{activeScreen === 'selectThreshold' && (
|
{activeScreen === 'selectThreshold' && (
|
||||||
<ThresholdForm
|
<ThresholdForm
|
||||||
onClickBack={onClickBack}
|
onClickBack={onClickBack}
|
||||||
|
@ -129,8 +113,7 @@ export const RemoveOwnerModal = ({
|
||||||
onClickBack={onClickBack}
|
onClickBack={onClickBack}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onSubmit={onRemoveOwner}
|
onSubmit={onRemoveOwner}
|
||||||
ownerAddress={ownerAddress}
|
owner={owner}
|
||||||
ownerName={ownerName}
|
|
||||||
threshold={Number(values.threshold)}
|
threshold={Number(values.threshold)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -7,6 +7,7 @@ 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 { OwnerData } from 'src/routes/safe/components/Settings/ManageOwners/dataFetcher'
|
||||||
|
|
||||||
import { useStyles } from './style'
|
import { useStyles } from './style'
|
||||||
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
||||||
|
@ -18,11 +19,10 @@ export const REMOVE_OWNER_MODAL_NEXT_BTN_TEST_ID = 'remove-owner-next-btn'
|
||||||
interface CheckOwnerProps {
|
interface CheckOwnerProps {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onSubmit: () => void
|
onSubmit: () => void
|
||||||
ownerAddress: string
|
owner: OwnerData
|
||||||
ownerName: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CheckOwner = ({ onClose, onSubmit, ownerAddress, ownerName }: CheckOwnerProps): ReactElement => {
|
export const CheckOwner = ({ onClose, onSubmit, owner }: CheckOwnerProps): ReactElement => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -44,11 +44,11 @@ export const CheckOwner = ({ onClose, onSubmit, ownerAddress, ownerName }: Check
|
||||||
<Row>
|
<Row>
|
||||||
<Col align="center" xs={12}>
|
<Col align="center" xs={12}>
|
||||||
<EthHashInfo
|
<EthHashInfo
|
||||||
hash={ownerAddress}
|
hash={owner.address}
|
||||||
name={ownerName}
|
name={owner.name}
|
||||||
showCopyBtn
|
showCopyBtn
|
||||||
showAvatar
|
showAvatar
|
||||||
explorerUrl={getExplorerInfo(ownerAddress)}
|
explorerUrl={getExplorerInfo(owner.address)}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -3,21 +3,23 @@ import Close from '@material-ui/icons/Close'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
||||||
import { List } from 'immutable'
|
|
||||||
|
|
||||||
import { getExplorerInfo } from 'src/config'
|
import { getExplorerInfo, getNetworkId } from 'src/config'
|
||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
import Col from 'src/components/layout/Col'
|
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 { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts'
|
import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts'
|
||||||
import { safeNameSelector, safeOwnersSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
import {
|
||||||
import { getOwnersWithNameFromAddressBook } from 'src/logic/addressBook/utils'
|
safeOwnersWithAddressBookDataSelector,
|
||||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
safeParamAddressFromStateSelector,
|
||||||
|
} from 'src/logic/safe/store/selectors'
|
||||||
|
import { useSafeName } from 'src/logic/addressBook/hooks/useSafeName'
|
||||||
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
||||||
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||||
|
import { OwnerData } from 'src/routes/safe/components/Settings/ManageOwners/dataFetcher'
|
||||||
|
|
||||||
import { useStyles } from './style'
|
import { useStyles } from './style'
|
||||||
import { Modal } from 'src/components/Modal'
|
import { Modal } from 'src/components/Modal'
|
||||||
|
@ -28,12 +30,13 @@ import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||||
|
|
||||||
export const REMOVE_OWNER_REVIEW_BTN_TEST_ID = 'remove-owner-review-btn'
|
export const REMOVE_OWNER_REVIEW_BTN_TEST_ID = 'remove-owner-review-btn'
|
||||||
|
|
||||||
|
const chainId = getNetworkId()
|
||||||
|
|
||||||
type ReviewRemoveOwnerProps = {
|
type ReviewRemoveOwnerProps = {
|
||||||
onClickBack: () => void
|
onClickBack: () => void
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onSubmit: (txParameters: TxParameters) => void
|
onSubmit: (txParameters: TxParameters) => void
|
||||||
ownerAddress: string
|
owner: OwnerData
|
||||||
ownerName: string
|
|
||||||
threshold?: number
|
threshold?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,17 +44,15 @@ export const ReviewRemoveOwnerModal = ({
|
||||||
onClickBack,
|
onClickBack,
|
||||||
onClose,
|
onClose,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
ownerAddress,
|
owner,
|
||||||
ownerName,
|
|
||||||
threshold = 1,
|
threshold = 1,
|
||||||
}: ReviewRemoveOwnerProps): React.ReactElement => {
|
}: ReviewRemoveOwnerProps): React.ReactElement => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const [data, setData] = useState('')
|
const [data, setData] = useState('')
|
||||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||||
const safeName = useSelector(safeNameSelector)
|
const safeName = useSafeName(safeAddress)
|
||||||
const owners = useSelector(safeOwnersSelector)
|
const owners = useSelector((state) => safeOwnersWithAddressBookDataSelector(state, chainId))
|
||||||
const addressBook = useSelector(addressBookSelector)
|
const numOptions = owners ? owners.length - 1 : 0
|
||||||
const ownersWithAddressBookName = owners ? getOwnersWithNameFromAddressBook(addressBook, owners) : List([])
|
|
||||||
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
|
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
|
||||||
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
|
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
|
||||||
const [manualGasLimit, setManualGasLimit] = useState<string | undefined>()
|
const [manualGasLimit, setManualGasLimit] = useState<string | undefined>()
|
||||||
|
@ -85,11 +86,13 @@ export const ReviewRemoveOwnerModal = ({
|
||||||
|
|
||||||
const calculateRemoveOwnerData = async () => {
|
const calculateRemoveOwnerData = async () => {
|
||||||
try {
|
try {
|
||||||
|
// FixMe: if the order returned by the service is the same as in the contracts
|
||||||
|
// the data lookup can be removed from here
|
||||||
const gnosisSafe = getGnosisSafeInstanceAt(safeAddress)
|
const gnosisSafe = getGnosisSafeInstanceAt(safeAddress)
|
||||||
const safeOwners = await gnosisSafe.methods.getOwners().call()
|
const safeOwners = await gnosisSafe.methods.getOwners().call()
|
||||||
const index = safeOwners.findIndex((owner) => sameAddress(owner, ownerAddress))
|
const index = safeOwners.findIndex((ownerAddress) => sameAddress(ownerAddress, owner.address))
|
||||||
const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1]
|
const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1]
|
||||||
const txData = gnosisSafe.methods.removeOwner(prevAddress, ownerAddress, threshold).encodeABI()
|
const txData = gnosisSafe.methods.removeOwner(prevAddress, owner.address, threshold).encodeABI()
|
||||||
|
|
||||||
if (isCurrent) {
|
if (isCurrent) {
|
||||||
setData(txData)
|
setData(txData)
|
||||||
|
@ -103,7 +106,7 @@ export const ReviewRemoveOwnerModal = ({
|
||||||
return () => {
|
return () => {
|
||||||
isCurrent = false
|
isCurrent = false
|
||||||
}
|
}
|
||||||
}, [safeAddress, ownerAddress, threshold])
|
}, [safeAddress, owner.address, threshold])
|
||||||
|
|
||||||
const closeEditModalCallback = (txParameters: TxParameters) => {
|
const closeEditModalCallback = (txParameters: TxParameters) => {
|
||||||
const oldGasPrice = Number(gasPriceFormatted)
|
const oldGasPrice = Number(gasPriceFormatted)
|
||||||
|
@ -168,7 +171,7 @@ export const ReviewRemoveOwnerModal = ({
|
||||||
Any transaction requires the confirmation of:
|
Any transaction requires the confirmation of:
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
|
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
|
||||||
{`${threshold} out of ${owners ? owners.size - 1 : 0} owner(s)`}
|
{`${threshold} out of ${numOptions} owner(s)`}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Block>
|
</Block>
|
||||||
</Block>
|
</Block>
|
||||||
|
@ -177,22 +180,22 @@ export const ReviewRemoveOwnerModal = ({
|
||||||
<Col className={classes.owners} layout="column" xs={8}>
|
<Col className={classes.owners} layout="column" xs={8}>
|
||||||
<Row className={classes.ownersTitle}>
|
<Row className={classes.ownersTitle}>
|
||||||
<Paragraph color="primary" noMargin size="lg">
|
<Paragraph color="primary" noMargin size="lg">
|
||||||
{`${owners ? owners.size - 1 : 0} Safe owner(s)`}
|
{`${numOptions} Safe owner(s)`}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Row>
|
</Row>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
{ownersWithAddressBookName?.map(
|
{owners?.map(
|
||||||
(owner) =>
|
(safeOwner) =>
|
||||||
owner.address !== ownerAddress && (
|
!sameAddress(safeOwner.address, owner.address) && (
|
||||||
<React.Fragment key={owner.address}>
|
<React.Fragment key={safeOwner.address}>
|
||||||
<Row className={classes.owner}>
|
<Row className={classes.owner}>
|
||||||
<Col align="center" xs={12}>
|
<Col align="center" xs={12}>
|
||||||
<EthHashInfo
|
<EthHashInfo
|
||||||
hash={owner.address}
|
hash={safeOwner.address}
|
||||||
name={owner.name}
|
name={safeOwner.name}
|
||||||
showCopyBtn
|
showCopyBtn
|
||||||
showAvatar
|
showAvatar
|
||||||
explorerUrl={getExplorerInfo(owner.address)}
|
explorerUrl={getExplorerInfo(safeOwner.address)}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -209,11 +212,11 @@ export const ReviewRemoveOwnerModal = ({
|
||||||
<Row className={classes.selectedOwner}>
|
<Row className={classes.selectedOwner}>
|
||||||
<Col align="center" xs={12}>
|
<Col align="center" xs={12}>
|
||||||
<EthHashInfo
|
<EthHashInfo
|
||||||
hash={ownerAddress}
|
hash={owner.address}
|
||||||
name={ownerName}
|
name={owner.name}
|
||||||
showCopyBtn
|
showCopyBtn
|
||||||
showAvatar
|
showAvatar
|
||||||
explorerUrl={getExplorerInfo(ownerAddress)}
|
explorerUrl={getExplorerInfo(owner.address)}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -35,6 +35,7 @@ type Props = {
|
||||||
export const ThresholdForm = ({ onClickBack, onClose, onSubmit, initialValues }: Props): ReactElement => {
|
export const ThresholdForm = ({ onClickBack, onClose, onSubmit, initialValues }: Props): ReactElement => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const owners = useSelector(safeOwnersSelector)
|
const owners = useSelector(safeOwnersSelector)
|
||||||
|
const ownersCount = owners?.length ?? 0
|
||||||
const threshold = useSelector(safeThresholdSelector) as number
|
const threshold = useSelector(safeThresholdSelector) as number
|
||||||
const handleSubmit = (values) => {
|
const handleSubmit = (values) => {
|
||||||
onSubmit(values)
|
onSubmit(values)
|
||||||
|
@ -58,7 +59,7 @@ export const ThresholdForm = ({ onClickBack, onClose, onSubmit, initialValues }:
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
{() => {
|
{() => {
|
||||||
const numOptions = owners && owners.size > 1 ? owners.size - 1 : 1
|
const numOptions = ownersCount > 1 ? ownersCount - 1 : 1
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -97,7 +98,7 @@ export const ThresholdForm = ({ onClickBack, onClose, onSubmit, initialValues }:
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={10}>
|
<Col xs={10}>
|
||||||
<Paragraph className={classes.ownersText} color="primary" noMargin size="lg">
|
<Paragraph className={classes.ownersText} color="primary" noMargin size="lg">
|
||||||
out of {owners ? owners.size - 1 : 0} owner(s)
|
out of {ownersCount ? ownersCount - 1 : 0} owner(s)
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -2,12 +2,11 @@ import React, { useEffect, useState } from 'react'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
|
||||||
import Modal from 'src/components/Modal'
|
import Modal from 'src/components/Modal'
|
||||||
import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
|
import { addressBookAddOrUpdate } from 'src/logic/addressBook/store/actions'
|
||||||
import { SENTINEL_ADDRESS, getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
import { SENTINEL_ADDRESS, getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||||
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||||
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
|
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
|
||||||
import { replaceSafeOwner } from 'src/logic/safe/store/actions/replaceSafeOwner'
|
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||||
import { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors'
|
|
||||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||||
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||||
|
@ -16,25 +15,26 @@ import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
||||||
import { OwnerForm } from 'src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm'
|
import { OwnerForm } from 'src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm'
|
||||||
import { ReviewReplaceOwnerModal } from 'src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review'
|
import { ReviewReplaceOwnerModal } from 'src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review'
|
||||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||||
|
import { isValidAddress } from 'src/utils/isValidAddress'
|
||||||
|
import { OwnerData } from 'src/routes/safe/components/Settings/ManageOwners/dataFetcher'
|
||||||
|
|
||||||
export type OwnerValues = {
|
export type OwnerValues = {
|
||||||
newOwnerAddress: string
|
address: string
|
||||||
newOwnerName: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sendReplaceOwner = async (
|
export const sendReplaceOwner = async (
|
||||||
values: OwnerValues,
|
newOwner: OwnerValues,
|
||||||
safeAddress: string,
|
safeAddress: string,
|
||||||
ownerAddressToRemove: string,
|
ownerAddressToRemove: string,
|
||||||
dispatch: Dispatch,
|
dispatch: Dispatch,
|
||||||
txParameters: TxParameters,
|
txParameters: TxParameters,
|
||||||
threshold?: number,
|
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const gnosisSafe = getGnosisSafeInstanceAt(safeAddress)
|
const gnosisSafe = getGnosisSafeInstanceAt(safeAddress)
|
||||||
const safeOwners = await gnosisSafe.methods.getOwners().call()
|
const safeOwners = await gnosisSafe.methods.getOwners().call()
|
||||||
const index = safeOwners.findIndex((ownerAddress) => sameAddress(ownerAddress, ownerAddressToRemove))
|
const index = safeOwners.findIndex((ownerAddress) => sameAddress(ownerAddress, ownerAddressToRemove))
|
||||||
const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1]
|
const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1]
|
||||||
const txData = gnosisSafe.methods.swapOwner(prevAddress, ownerAddressToRemove, values.newOwnerAddress).encodeABI()
|
const txData = gnosisSafe.methods.swapOwner(prevAddress, ownerAddressToRemove, newOwner.address).encodeABI()
|
||||||
|
|
||||||
const txHash = await dispatch(
|
const txHash = await dispatch(
|
||||||
createTransaction({
|
createTransaction({
|
||||||
|
@ -49,47 +49,28 @@ export const sendReplaceOwner = async (
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (txHash && threshold === 1) {
|
if (txHash) {
|
||||||
dispatch(
|
// update the AB
|
||||||
replaceSafeOwner({
|
dispatch(addressBookAddOrUpdate(makeAddressBookEntry(newOwner)))
|
||||||
safeAddress,
|
|
||||||
oldOwnerAddress: ownerAddressToRemove,
|
|
||||||
ownerAddress: values.newOwnerAddress,
|
|
||||||
ownerName: values.newOwnerName,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReplaceOwnerProps = {
|
type ReplaceOwnerProps = {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
ownerAddress: string
|
owner: OwnerData
|
||||||
ownerName: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ReplaceOwnerModal = ({
|
export const ReplaceOwnerModal = ({ isOpen, onClose, owner }: ReplaceOwnerProps): React.ReactElement => {
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
ownerAddress,
|
|
||||||
ownerName,
|
|
||||||
}: ReplaceOwnerProps): React.ReactElement => {
|
|
||||||
const [activeScreen, setActiveScreen] = useState('checkOwner')
|
const [activeScreen, setActiveScreen] = useState('checkOwner')
|
||||||
const [values, setValues] = useState({
|
const [newOwner, setNewOwner] = useState({ address: '', name: '' })
|
||||||
newOwnerAddress: '',
|
|
||||||
newOwnerName: '',
|
|
||||||
})
|
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||||
const threshold = useSelector(safeThresholdSelector) || 1
|
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => () => {
|
() => () => {
|
||||||
setActiveScreen('checkOwner')
|
setActiveScreen('checkOwner')
|
||||||
setValues({
|
setNewOwner({ address: '', name: '' })
|
||||||
newOwnerAddress: '',
|
|
||||||
newOwnerName: '',
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
[isOpen],
|
[isOpen],
|
||||||
)
|
)
|
||||||
|
@ -98,24 +79,19 @@ export const ReplaceOwnerModal = ({
|
||||||
|
|
||||||
const ownerSubmitted = (newValues) => {
|
const ownerSubmitted = (newValues) => {
|
||||||
const { ownerAddress, ownerName } = newValues
|
const { ownerAddress, ownerName } = newValues
|
||||||
const checksumAddr = checksumAddress(ownerAddress)
|
|
||||||
setValues({
|
if (isValidAddress(ownerAddress)) {
|
||||||
newOwnerAddress: checksumAddr,
|
const checksumAddr = checksumAddress(ownerAddress)
|
||||||
newOwnerName: ownerName,
|
setNewOwner({ address: checksumAddr, name: ownerName })
|
||||||
})
|
setActiveScreen('reviewReplaceOwner')
|
||||||
setActiveScreen('reviewReplaceOwner')
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onReplaceOwner = async (txParameters: TxParameters) => {
|
const onReplaceOwner = async (txParameters: TxParameters) => {
|
||||||
onClose()
|
onClose()
|
||||||
try {
|
try {
|
||||||
await sendReplaceOwner(values, safeAddress, ownerAddress, dispatch, txParameters, threshold)
|
await sendReplaceOwner(newOwner, safeAddress, owner.address, dispatch, txParameters)
|
||||||
|
dispatch(addressBookAddOrUpdate(makeAddressBookEntry(newOwner)))
|
||||||
dispatch(
|
|
||||||
addOrUpdateAddressBookEntry(
|
|
||||||
makeAddressBookEntry({ address: values.newOwnerAddress, name: values.newOwnerName }),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error while removing an owner', error)
|
console.error('Error while removing an owner', error)
|
||||||
}
|
}
|
||||||
|
@ -131,22 +107,15 @@ export const ReplaceOwnerModal = ({
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
{activeScreen === 'checkOwner' && (
|
{activeScreen === 'checkOwner' && (
|
||||||
<OwnerForm
|
<OwnerForm onClose={onClose} onSubmit={ownerSubmitted} initialValues={newOwner} owner={owner} />
|
||||||
onClose={onClose}
|
|
||||||
onSubmit={ownerSubmitted}
|
|
||||||
initialValues={values}
|
|
||||||
ownerAddress={ownerAddress}
|
|
||||||
ownerName={ownerName}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{activeScreen === 'reviewReplaceOwner' && (
|
{activeScreen === 'reviewReplaceOwner' && (
|
||||||
<ReviewReplaceOwnerModal
|
<ReviewReplaceOwnerModal
|
||||||
onClickBack={onClickBack}
|
onClickBack={onClickBack}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onSubmit={onReplaceOwner}
|
onSubmit={onReplaceOwner}
|
||||||
ownerAddress={ownerAddress}
|
owner={owner}
|
||||||
ownerName={ownerName}
|
newOwner={newOwner}
|
||||||
values={values}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import IconButton from '@material-ui/core/IconButton'
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
import Close from '@material-ui/icons/Close'
|
import Close from '@material-ui/icons/Close'
|
||||||
|
import { Mutator } from 'final-form'
|
||||||
import React, { ReactElement } from 'react'
|
import React, { ReactElement } from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
|
import { OnChange } from 'react-final-form-listeners'
|
||||||
|
|
||||||
import AddressInput from 'src/components/forms/AddressInput'
|
import AddressInput from 'src/components/forms/AddressInput'
|
||||||
import Field from 'src/components/forms/Field'
|
import Field from 'src/components/forms/Field'
|
||||||
|
@ -10,9 +12,9 @@ import TextField from 'src/components/forms/TextField'
|
||||||
import {
|
import {
|
||||||
addressIsNotCurrentSafe,
|
addressIsNotCurrentSafe,
|
||||||
composeValidators,
|
composeValidators,
|
||||||
minMaxLength,
|
|
||||||
required,
|
required,
|
||||||
uniqueAddress,
|
uniqueAddress,
|
||||||
|
validAddressBookName,
|
||||||
} from 'src/components/forms/validator'
|
} from 'src/components/forms/validator'
|
||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
import Col from 'src/components/layout/Col'
|
import Col from 'src/components/layout/Col'
|
||||||
|
@ -21,10 +23,13 @@ import Paragraph from 'src/components/layout/Paragraph'
|
||||||
import Row from 'src/components/layout/Row'
|
import Row from 'src/components/layout/Row'
|
||||||
import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
|
import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
|
||||||
import { Modal } from 'src/components/Modal'
|
import { Modal } from 'src/components/Modal'
|
||||||
import { safeOwnersAddressesListSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
import { safeOwnersSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||||
|
import { addressBookMapSelector } from 'src/logic/addressBook/store/selectors'
|
||||||
|
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
|
||||||
|
import { OwnerData } from 'src/routes/safe/components/Settings/ManageOwners/dataFetcher'
|
||||||
|
|
||||||
import { useStyles } from './style'
|
import { useStyles } from './style'
|
||||||
import { getExplorerInfo } from 'src/config'
|
import { getExplorerInfo, getNetworkId } from 'src/config'
|
||||||
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
||||||
|
|
||||||
export const REPLACE_OWNER_NAME_INPUT_TEST_ID = 'replace-owner-name-input'
|
export const REPLACE_OWNER_NAME_INPUT_TEST_ID = 'replace-owner-name-input'
|
||||||
|
@ -33,12 +38,20 @@ export const REPLACE_OWNER_NEXT_BTN_TEST_ID = 'replace-owner-next-btn'
|
||||||
|
|
||||||
import { OwnerValues } from '../..'
|
import { OwnerValues } from '../..'
|
||||||
|
|
||||||
const formMutators = {
|
const formMutators: Record<
|
||||||
|
string,
|
||||||
|
Mutator<{ setOwnerAddress: { address: string }; setOwnerName: { name: string } }>
|
||||||
|
> = {
|
||||||
setOwnerAddress: (args, state, utils) => {
|
setOwnerAddress: (args, state, utils) => {
|
||||||
utils.changeValue(state, 'ownerAddress', () => args[0])
|
utils.changeValue(state, 'ownerAddress', () => args[0])
|
||||||
},
|
},
|
||||||
|
setOwnerName: (args, state, utils) => {
|
||||||
|
utils.changeValue(state, 'ownerName', () => args[0])
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const chainId = getNetworkId()
|
||||||
|
|
||||||
type NewOwnerProps = {
|
type NewOwnerProps = {
|
||||||
ownerAddress: string
|
ownerAddress: string
|
||||||
ownerName: string
|
ownerName: string
|
||||||
|
@ -47,26 +60,21 @@ type NewOwnerProps = {
|
||||||
type OwnerFormProps = {
|
type OwnerFormProps = {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onSubmit: (values: NewOwnerProps) => void
|
onSubmit: (values: NewOwnerProps) => void
|
||||||
ownerAddress: string
|
owner: OwnerData
|
||||||
ownerName: string
|
|
||||||
initialValues?: OwnerValues
|
initialValues?: OwnerValues
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OwnerForm = ({
|
export const OwnerForm = ({ onClose, onSubmit, owner, initialValues }: OwnerFormProps): ReactElement => {
|
||||||
onClose,
|
|
||||||
onSubmit,
|
|
||||||
ownerAddress,
|
|
||||||
ownerName,
|
|
||||||
initialValues,
|
|
||||||
}: OwnerFormProps): ReactElement => {
|
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
const handleSubmit = (values: NewOwnerProps) => {
|
const handleSubmit = (values: NewOwnerProps) => {
|
||||||
onSubmit(values)
|
onSubmit(values)
|
||||||
}
|
}
|
||||||
const owners = useSelector(safeOwnersAddressesListSelector)
|
const addressBookMap = useSelector(addressBookMapSelector)
|
||||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
const owners = useSelector(safeOwnersSelector)
|
||||||
const ownerDoesntExist = uniqueAddress(owners)
|
const ownerDoesntExist = uniqueAddress(owners)
|
||||||
|
|
||||||
|
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||||
const ownerAddressIsNotSafeAddress = addressIsNotCurrentSafe(safeAddress)
|
const ownerAddressIsNotSafeAddress = addressIsNotCurrentSafe(safeAddress)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -85,8 +93,8 @@ export const OwnerForm = ({
|
||||||
formMutators={formMutators}
|
formMutators={formMutators}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
initialValues={{
|
initialValues={{
|
||||||
ownerName: initialValues?.newOwnerName,
|
ownerName: initialValues?.name,
|
||||||
ownerAddress: initialValues?.newOwnerAddress,
|
ownerAddress: initialValues?.address,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(...args) => {
|
{(...args) => {
|
||||||
|
@ -118,11 +126,11 @@ export const OwnerForm = ({
|
||||||
<Row className={classes.owner}>
|
<Row className={classes.owner}>
|
||||||
<Col align="center" xs={12}>
|
<Col align="center" xs={12}>
|
||||||
<EthHashInfo
|
<EthHashInfo
|
||||||
hash={ownerAddress}
|
hash={owner.address}
|
||||||
name={ownerName}
|
name={owner.name}
|
||||||
showCopyBtn
|
showCopyBtn
|
||||||
showAvatar
|
showAvatar
|
||||||
explorerUrl={getExplorerInfo(ownerAddress)}
|
explorerUrl={getExplorerInfo(owner.address)}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -138,8 +146,18 @@ export const OwnerForm = ({
|
||||||
testId={REPLACE_OWNER_NAME_INPUT_TEST_ID}
|
testId={REPLACE_OWNER_NAME_INPUT_TEST_ID}
|
||||||
text="Owner name*"
|
text="Owner name*"
|
||||||
type="text"
|
type="text"
|
||||||
validate={composeValidators(required, minMaxLength(1, 50))}
|
validate={composeValidators(required, validAddressBookName)}
|
||||||
/>
|
/>
|
||||||
|
<OnChange name="ownerAddress">
|
||||||
|
{async (address: string) => {
|
||||||
|
if (web3ReadOnly.utils.isAddress(address)) {
|
||||||
|
const ownerName = addressBookMap?.[chainId]?.[address]?.name
|
||||||
|
if (ownerName) {
|
||||||
|
mutators.setOwnerName(ownerName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
</OnChange>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row margin="md">
|
<Row margin="md">
|
||||||
|
|
|
@ -2,10 +2,9 @@ import IconButton from '@material-ui/core/IconButton'
|
||||||
import Close from '@material-ui/icons/Close'
|
import Close from '@material-ui/icons/Close'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { List } from 'immutable'
|
|
||||||
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
||||||
|
|
||||||
import { getExplorerInfo } from 'src/config'
|
import { getExplorerInfo, getNetworkId } from 'src/config'
|
||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
import Col from 'src/components/layout/Col'
|
import Col from 'src/components/layout/Col'
|
||||||
import Hairline from 'src/components/layout/Hairline'
|
import Hairline from 'src/components/layout/Hairline'
|
||||||
|
@ -13,34 +12,35 @@ import Paragraph from 'src/components/layout/Paragraph'
|
||||||
import Row from 'src/components/layout/Row'
|
import Row from 'src/components/layout/Row'
|
||||||
import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts'
|
import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts'
|
||||||
import {
|
import {
|
||||||
safeNameSelector,
|
safeOwnersWithAddressBookDataSelector,
|
||||||
safeOwnersSelector,
|
|
||||||
safeParamAddressFromStateSelector,
|
safeParamAddressFromStateSelector,
|
||||||
safeThresholdSelector,
|
safeThresholdSelector,
|
||||||
} from 'src/logic/safe/store/selectors'
|
} from 'src/logic/safe/store/selectors'
|
||||||
import { getOwnersWithNameFromAddressBook } from 'src/logic/addressBook/utils'
|
|
||||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
|
||||||
import { useEstimationStatus } from 'src/logic/hooks/useEstimationStatus'
|
import { useEstimationStatus } from 'src/logic/hooks/useEstimationStatus'
|
||||||
|
import { useSafeName } from 'src/logic/addressBook/hooks/useSafeName'
|
||||||
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
||||||
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||||
import { Modal } from 'src/components/Modal'
|
import { Modal } from 'src/components/Modal'
|
||||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||||
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
|
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
|
||||||
|
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||||
|
import { OwnerData } from 'src/routes/safe/components/Settings/ManageOwners/dataFetcher'
|
||||||
|
|
||||||
import { useStyles } from './style'
|
import { useStyles } from './style'
|
||||||
|
|
||||||
export const REPLACE_OWNER_SUBMIT_BTN_TEST_ID = 'replace-owner-submit-btn'
|
export const REPLACE_OWNER_SUBMIT_BTN_TEST_ID = 'replace-owner-submit-btn'
|
||||||
|
|
||||||
|
const chainId = getNetworkId()
|
||||||
|
|
||||||
type ReplaceOwnerProps = {
|
type ReplaceOwnerProps = {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onClickBack: () => void
|
onClickBack: () => void
|
||||||
onSubmit: (txParameters: TxParameters) => void
|
onSubmit: (txParameters: TxParameters) => void
|
||||||
ownerAddress: string
|
owner: OwnerData
|
||||||
ownerName: string
|
newOwner: {
|
||||||
values: {
|
address: string
|
||||||
newOwnerAddress: string
|
name: string
|
||||||
newOwnerName: string
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,18 +48,15 @@ export const ReviewReplaceOwnerModal = ({
|
||||||
onClickBack,
|
onClickBack,
|
||||||
onClose,
|
onClose,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
ownerAddress,
|
owner,
|
||||||
ownerName,
|
newOwner,
|
||||||
values,
|
|
||||||
}: ReplaceOwnerProps): React.ReactElement => {
|
}: ReplaceOwnerProps): React.ReactElement => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const [data, setData] = useState('')
|
const [data, setData] = useState('')
|
||||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||||
const safeName = useSelector(safeNameSelector)
|
const safeName = useSafeName(safeAddress)
|
||||||
const owners = useSelector(safeOwnersSelector)
|
const owners = useSelector((state) => safeOwnersWithAddressBookDataSelector(state, chainId))
|
||||||
const threshold = useSelector(safeThresholdSelector) || 1
|
const threshold = useSelector(safeThresholdSelector) || 1
|
||||||
const addressBook = useSelector(addressBookSelector)
|
|
||||||
const ownersWithAddressBookName = owners ? getOwnersWithNameFromAddressBook(addressBook, owners) : List([])
|
|
||||||
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
|
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
|
||||||
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
|
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
|
||||||
const [manualGasLimit, setManualGasLimit] = useState<string | undefined>()
|
const [manualGasLimit, setManualGasLimit] = useState<string | undefined>()
|
||||||
|
@ -88,9 +85,9 @@ export const ReviewReplaceOwnerModal = ({
|
||||||
const calculateReplaceOwnerData = async () => {
|
const calculateReplaceOwnerData = async () => {
|
||||||
const gnosisSafe = getGnosisSafeInstanceAt(safeAddress)
|
const gnosisSafe = getGnosisSafeInstanceAt(safeAddress)
|
||||||
const safeOwners = await gnosisSafe.methods.getOwners().call()
|
const safeOwners = await gnosisSafe.methods.getOwners().call()
|
||||||
const index = safeOwners.findIndex((owner) => owner.toLowerCase() === ownerAddress.toLowerCase())
|
const index = safeOwners.findIndex((ownerAddress) => sameAddress(ownerAddress, owner.address))
|
||||||
const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1]
|
const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1]
|
||||||
const txData = gnosisSafe.methods.swapOwner(prevAddress, ownerAddress, values.newOwnerAddress).encodeABI()
|
const txData = gnosisSafe.methods.swapOwner(prevAddress, owner.address, newOwner.address).encodeABI()
|
||||||
if (isCurrent) {
|
if (isCurrent) {
|
||||||
setData(txData)
|
setData(txData)
|
||||||
}
|
}
|
||||||
|
@ -100,7 +97,7 @@ export const ReviewReplaceOwnerModal = ({
|
||||||
return () => {
|
return () => {
|
||||||
isCurrent = false
|
isCurrent = false
|
||||||
}
|
}
|
||||||
}, [ownerAddress, safeAddress, values.newOwnerAddress])
|
}, [owner.address, safeAddress, newOwner.address])
|
||||||
|
|
||||||
const closeEditModalCallback = (txParameters: TxParameters) => {
|
const closeEditModalCallback = (txParameters: TxParameters) => {
|
||||||
const oldGasPrice = Number(gasPriceFormatted)
|
const oldGasPrice = Number(gasPriceFormatted)
|
||||||
|
@ -164,7 +161,7 @@ export const ReviewReplaceOwnerModal = ({
|
||||||
Any transaction requires the confirmation of:
|
Any transaction requires the confirmation of:
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
|
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
|
||||||
{`${threshold} out of ${owners?.size || 0} owner(s)`}
|
{`${threshold} out of ${owners?.length || 0} owner(s)`}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Block>
|
</Block>
|
||||||
</Block>
|
</Block>
|
||||||
|
@ -172,22 +169,22 @@ export const ReviewReplaceOwnerModal = ({
|
||||||
<Col className={classes.owners} layout="column" xs={8}>
|
<Col className={classes.owners} layout="column" xs={8}>
|
||||||
<Row className={classes.ownersTitle}>
|
<Row className={classes.ownersTitle}>
|
||||||
<Paragraph color="primary" noMargin size="lg">
|
<Paragraph color="primary" noMargin size="lg">
|
||||||
{`${owners?.size || 0} Safe owner(s)`}
|
{`${owners?.length || 0} Safe owner(s)`}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Row>
|
</Row>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
{ownersWithAddressBookName?.map(
|
{owners?.map(
|
||||||
(owner) =>
|
(safeOwner) =>
|
||||||
owner.address !== ownerAddress && (
|
!sameAddress(safeOwner.address, owner.address) && (
|
||||||
<React.Fragment key={owner.address}>
|
<React.Fragment key={safeOwner.address}>
|
||||||
<Row className={classes.owner}>
|
<Row className={classes.owner}>
|
||||||
<Col align="center" xs={12}>
|
<Col align="center" xs={12}>
|
||||||
<EthHashInfo
|
<EthHashInfo
|
||||||
hash={owner.address}
|
hash={safeOwner.address}
|
||||||
name={owner.name}
|
name={safeOwner.name}
|
||||||
showCopyBtn
|
showCopyBtn
|
||||||
showAvatar
|
showAvatar
|
||||||
explorerUrl={getExplorerInfo(owner.address)}
|
explorerUrl={getExplorerInfo(safeOwner.address)}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -204,11 +201,11 @@ export const ReviewReplaceOwnerModal = ({
|
||||||
<Row className={classes.selectedOwnerRemoved}>
|
<Row className={classes.selectedOwnerRemoved}>
|
||||||
<Col align="center" xs={12}>
|
<Col align="center" xs={12}>
|
||||||
<EthHashInfo
|
<EthHashInfo
|
||||||
hash={ownerAddress}
|
hash={owner.address}
|
||||||
name={ownerName}
|
name={owner.name}
|
||||||
showCopyBtn
|
showCopyBtn
|
||||||
showAvatar
|
showAvatar
|
||||||
explorerUrl={getExplorerInfo(ownerAddress)}
|
explorerUrl={getExplorerInfo(owner.address)}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -221,11 +218,11 @@ export const ReviewReplaceOwnerModal = ({
|
||||||
<Row className={classes.selectedOwnerAdded}>
|
<Row className={classes.selectedOwnerAdded}>
|
||||||
<Col align="center" xs={12}>
|
<Col align="center" xs={12}>
|
||||||
<EthHashInfo
|
<EthHashInfo
|
||||||
hash={values.newOwnerAddress}
|
hash={newOwner.address}
|
||||||
name={values.newOwnerName}
|
name={newOwner.name}
|
||||||
showCopyBtn
|
showCopyBtn
|
||||||
showAvatar
|
showAvatar
|
||||||
explorerUrl={getExplorerInfo(values.newOwnerAddress)}
|
explorerUrl={getExplorerInfo(newOwner.address)}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
import { List } from 'immutable'
|
import { List } from 'immutable'
|
||||||
|
|
||||||
import { TableColumn } from 'src/components/Table/types.d'
|
import { TableColumn } from 'src/components/Table/types.d'
|
||||||
import { SafeOwner } from 'src/logic/safe/store/models/safe'
|
import { AddressBookState } from 'src/logic/addressBook/model/addressBook'
|
||||||
|
|
||||||
export const OWNERS_TABLE_NAME_ID = 'name'
|
export const OWNERS_TABLE_NAME_ID = 'name'
|
||||||
export const OWNERS_TABLE_ADDRESS_ID = 'address'
|
export const OWNERS_TABLE_ADDRESS_ID = 'address'
|
||||||
export const OWNERS_TABLE_ACTIONS_ID = 'actions'
|
export const OWNERS_TABLE_ACTIONS_ID = 'actions'
|
||||||
|
|
||||||
export const getOwnerData = (owners: List<SafeOwner>): List<{ address: string; name: string }> => {
|
export type OwnerData = { address: string; name: string }
|
||||||
|
|
||||||
|
export const getOwnerData = (owners: AddressBookState): OwnerData[] => {
|
||||||
return owners.map((owner) => ({
|
return owners.map((owner) => ({
|
||||||
[OWNERS_TABLE_NAME_ID]: owner.name,
|
[OWNERS_TABLE_NAME_ID]: owner.name,
|
||||||
[OWNERS_TABLE_ADDRESS_ID]: owner.address,
|
[OWNERS_TABLE_ADDRESS_ID]: owner.address,
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect, ReactElement } from 'react'
|
||||||
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
||||||
import TableCell from '@material-ui/core/TableCell'
|
import TableCell from '@material-ui/core/TableCell'
|
||||||
import TableContainer from '@material-ui/core/TableContainer'
|
import TableContainer from '@material-ui/core/TableContainer'
|
||||||
import TableRow from '@material-ui/core/TableRow'
|
import TableRow from '@material-ui/core/TableRow'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import { List } from 'immutable'
|
|
||||||
|
|
||||||
import RemoveOwnerIcon from '../assets/icons/bin.svg'
|
import RemoveOwnerIcon from '../assets/icons/bin.svg'
|
||||||
|
|
||||||
|
@ -14,7 +13,7 @@ import { RemoveOwnerModal } from './RemoveOwnerModal'
|
||||||
import { ReplaceOwnerModal } from './ReplaceOwnerModal'
|
import { ReplaceOwnerModal } from './ReplaceOwnerModal'
|
||||||
import RenameOwnerIcon from './assets/icons/rename-owner.svg'
|
import RenameOwnerIcon from './assets/icons/rename-owner.svg'
|
||||||
import ReplaceOwnerIcon from './assets/icons/replace-owner.svg'
|
import ReplaceOwnerIcon from './assets/icons/replace-owner.svg'
|
||||||
import { OWNERS_TABLE_ADDRESS_ID, OWNERS_TABLE_NAME_ID, generateColumns, getOwnerData } from './dataFetcher'
|
import { OWNERS_TABLE_ADDRESS_ID, generateColumns, getOwnerData, OwnerData } from './dataFetcher'
|
||||||
import { useStyles } from './style'
|
import { useStyles } from './style'
|
||||||
|
|
||||||
import { getExplorerInfo } from 'src/config'
|
import { getExplorerInfo } from 'src/config'
|
||||||
|
@ -28,10 +27,8 @@ import Heading from 'src/components/layout/Heading'
|
||||||
import Img from 'src/components/layout/Img'
|
import Img from 'src/components/layout/Img'
|
||||||
import Paragraph from 'src/components/layout/Paragraph/index'
|
import Paragraph from 'src/components/layout/Paragraph/index'
|
||||||
import Row from 'src/components/layout/Row'
|
import Row from 'src/components/layout/Row'
|
||||||
import { getOwnersWithNameFromAddressBook } from 'src/logic/addressBook/utils'
|
|
||||||
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
|
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
|
||||||
import { AddressBookState } from 'src/logic/addressBook/model/addressBook'
|
import { AddressBookState } from 'src/logic/addressBook/model/addressBook'
|
||||||
import { SafeOwner } from 'src/logic/safe/store/models/safe'
|
|
||||||
|
|
||||||
export const RENAME_OWNER_BTN_TEST_ID = 'rename-owner-btn'
|
export const RENAME_OWNER_BTN_TEST_ID = 'rename-owner-btn'
|
||||||
export const REMOVE_OWNER_BTN_TEST_ID = 'remove-owner-btn'
|
export const REMOVE_OWNER_BTN_TEST_ID = 'remove-owner-btn'
|
||||||
|
@ -40,17 +37,15 @@ export const REPLACE_OWNER_BTN_TEST_ID = 'replace-owner-btn'
|
||||||
export const OWNERS_ROW_TEST_ID = 'owners-row'
|
export const OWNERS_ROW_TEST_ID = 'owners-row'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
addressBook: AddressBookState
|
|
||||||
granted: boolean
|
granted: boolean
|
||||||
owners: List<SafeOwner>
|
owners: AddressBookState
|
||||||
}
|
}
|
||||||
|
|
||||||
const ManageOwners = ({ addressBook, granted, owners }: Props): React.ReactElement => {
|
const ManageOwners = ({ granted, owners }: Props): ReactElement => {
|
||||||
const { trackEvent } = useAnalytics()
|
const { trackEvent } = useAnalytics()
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
const [selectedOwnerAddress, setSelectedOwnerAddress] = useState('')
|
const [selectedOwner, setSelectedOwner] = useState<OwnerData | undefined>()
|
||||||
const [selectedOwnerName, setSelectedOwnerName] = useState('')
|
|
||||||
const [modalsStatus, setModalStatus] = useState({
|
const [modalsStatus, setModalStatus] = useState({
|
||||||
showAddOwner: false,
|
showAddOwner: false,
|
||||||
showRemoveOwner: false,
|
showRemoveOwner: false,
|
||||||
|
@ -58,13 +53,14 @@ const ManageOwners = ({ addressBook, granted, owners }: Props): React.ReactEleme
|
||||||
showEditOwner: false,
|
showEditOwner: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const onShow = (action, row?: any) => () => {
|
const onShow = (action, row?: OwnerData) => () => {
|
||||||
setModalStatus((prevState) => ({
|
setModalStatus((prevState) => ({
|
||||||
...prevState,
|
...prevState,
|
||||||
[`show${action}`]: !prevState[`show${action}`],
|
[`show${action}`]: !prevState[`show${action}`],
|
||||||
}))
|
}))
|
||||||
setSelectedOwnerAddress(row && row.address)
|
if (row) {
|
||||||
setSelectedOwnerName(row && row.name)
|
setSelectedOwner(row)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onHide = (action) => () => {
|
const onHide = (action) => () => {
|
||||||
|
@ -72,8 +68,7 @@ const ManageOwners = ({ addressBook, granted, owners }: Props): React.ReactEleme
|
||||||
...prevState,
|
...prevState,
|
||||||
[`show${action}`]: !Boolean(prevState[`show${action}`]),
|
[`show${action}`]: !Boolean(prevState[`show${action}`]),
|
||||||
}))
|
}))
|
||||||
setSelectedOwnerAddress('')
|
setSelectedOwner(undefined)
|
||||||
setSelectedOwnerName('')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -82,8 +77,7 @@ const ManageOwners = ({ addressBook, granted, owners }: Props): React.ReactEleme
|
||||||
|
|
||||||
const columns = generateColumns()
|
const columns = generateColumns()
|
||||||
const autoColumns = columns.filter((c) => !c.custom)
|
const autoColumns = columns.filter((c) => !c.custom)
|
||||||
const ownersWithAddressBookName = getOwnersWithNameFromAddressBook(addressBook, owners)
|
const ownerData = getOwnerData(owners)
|
||||||
const ownerData = getOwnerData(ownersWithAddressBookName)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -100,11 +94,11 @@ const ManageOwners = ({ addressBook, granted, owners }: Props): React.ReactEleme
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={ownerData}
|
data={ownerData}
|
||||||
defaultFixed
|
defaultFixed
|
||||||
defaultOrderBy={OWNERS_TABLE_NAME_ID}
|
defaultOrderBy={OWNERS_TABLE_ADDRESS_ID}
|
||||||
disablePagination
|
disablePagination
|
||||||
label="Owners"
|
label="Owners"
|
||||||
noBorder
|
noBorder
|
||||||
size={ownerData.size}
|
size={ownerData.length}
|
||||||
>
|
>
|
||||||
{(sortedData) =>
|
{(sortedData) =>
|
||||||
sortedData.map((row, index) => (
|
sortedData.map((row, index) => (
|
||||||
|
@ -147,7 +141,7 @@ const ManageOwners = ({ addressBook, granted, owners }: Props): React.ReactEleme
|
||||||
src={ReplaceOwnerIcon}
|
src={ReplaceOwnerIcon}
|
||||||
testId={REPLACE_OWNER_BTN_TEST_ID}
|
testId={REPLACE_OWNER_BTN_TEST_ID}
|
||||||
/>
|
/>
|
||||||
{ownerData.size > 1 && (
|
{ownerData.length > 1 && (
|
||||||
<Img
|
<Img
|
||||||
alt="Remove owner"
|
alt="Remove owner"
|
||||||
className={classes.removeOwnerIcon}
|
className={classes.removeOwnerIcon}
|
||||||
|
@ -185,24 +179,21 @@ const ManageOwners = ({ addressBook, granted, owners }: Props): React.ReactEleme
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<AddOwnerModal isOpen={modalsStatus.showAddOwner} onClose={onHide('AddOwner')} />
|
<AddOwnerModal isOpen={modalsStatus.showAddOwner} onClose={onHide('AddOwner')} />
|
||||||
<RemoveOwnerModal
|
{selectedOwner && (
|
||||||
isOpen={modalsStatus.showRemoveOwner}
|
<>
|
||||||
onClose={onHide('RemoveOwner')}
|
<RemoveOwnerModal
|
||||||
ownerAddress={selectedOwnerAddress}
|
isOpen={modalsStatus.showRemoveOwner}
|
||||||
ownerName={selectedOwnerName}
|
onClose={onHide('RemoveOwner')}
|
||||||
/>
|
owner={selectedOwner}
|
||||||
<ReplaceOwnerModal
|
/>
|
||||||
isOpen={modalsStatus.showReplaceOwner}
|
<ReplaceOwnerModal
|
||||||
onClose={onHide('ReplaceOwner')}
|
isOpen={modalsStatus.showReplaceOwner}
|
||||||
ownerAddress={selectedOwnerAddress}
|
onClose={onHide('ReplaceOwner')}
|
||||||
ownerName={selectedOwnerName}
|
owner={selectedOwner}
|
||||||
/>
|
/>
|
||||||
<EditOwnerModal
|
<EditOwnerModal isOpen={modalsStatus.showEditOwner} onClose={onHide('EditOwner')} owner={selectedOwner} />
|
||||||
isOpen={modalsStatus.showEditOwner}
|
</>
|
||||||
onClose={onHide('EditOwner')}
|
)}
|
||||||
ownerAddress={selectedOwnerAddress}
|
|
||||||
selectedOwnerName={selectedOwnerName}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
||||||
import IconButton from '@material-ui/core/IconButton'
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
import Close from '@material-ui/icons/Close'
|
import Close from '@material-ui/icons/Close'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
@ -5,25 +6,23 @@ import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
|
||||||
import { useStyles } from './style'
|
import { useStyles } from './style'
|
||||||
|
|
||||||
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
|
||||||
import Modal, { Modal as GenericModal } from 'src/components/Modal'
|
import Modal, { Modal as GenericModal } from 'src/components/Modal'
|
||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
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 {
|
import { addressBookMapSelector } from 'src/logic/addressBook/store/selectors'
|
||||||
defaultSafeSelector,
|
import { defaultSafeSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||||
safeNameSelector,
|
|
||||||
safeParamAddressFromStateSelector,
|
|
||||||
} from 'src/logic/safe/store/selectors'
|
|
||||||
import { WELCOME_ADDRESS } from 'src/routes/routes'
|
import { WELCOME_ADDRESS } from 'src/routes/routes'
|
||||||
import { removeLocalSafe } from 'src/logic/safe/store/actions/removeLocalSafe'
|
import { removeLocalSafe } from 'src/logic/safe/store/actions/removeLocalSafe'
|
||||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||||
import { saveDefaultSafe } from 'src/logic/safe/utils'
|
import { saveDefaultSafe } from 'src/logic/safe/utils'
|
||||||
|
|
||||||
import { getExplorerInfo } from 'src/config'
|
import { getExplorerInfo, getNetworkId } from 'src/config'
|
||||||
import Col from 'src/components/layout/Col'
|
import Col from 'src/components/layout/Col'
|
||||||
|
|
||||||
|
const chainId = getNetworkId()
|
||||||
|
|
||||||
type RemoveSafeModalProps = {
|
type RemoveSafeModalProps = {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
|
@ -32,12 +31,16 @@ type RemoveSafeModalProps = {
|
||||||
export const RemoveSafeModal = ({ isOpen, onClose }: RemoveSafeModalProps): React.ReactElement => {
|
export const RemoveSafeModal = ({ isOpen, onClose }: RemoveSafeModalProps): React.ReactElement => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||||
const safeName = useSelector(safeNameSelector)
|
const addressBookMap = useSelector(addressBookMapSelector)
|
||||||
|
const safeAddressBookEntry = addressBookMap[chainId]?.[safeAddress]
|
||||||
|
const safeName = safeAddressBookEntry?.name
|
||||||
const defaultSafe = useSelector(defaultSafeSelector)
|
const defaultSafe = useSelector(defaultSafeSelector)
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
const onRemoveSafeHandler = async () => {
|
const onRemoveSafeHandler = async () => {
|
||||||
|
// ToDo: review if this is necessary or we should directly use the `removeSafe` action.
|
||||||
await dispatch(removeLocalSafe(safeAddress))
|
await dispatch(removeLocalSafe(safeAddress))
|
||||||
|
|
||||||
if (sameAddress(safeAddress, defaultSafe)) {
|
if (sameAddress(safeAddress, defaultSafe)) {
|
||||||
await saveDefaultSafe('')
|
await saveDefaultSafe('')
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Icon, Link, Text } from '@gnosis.pm/safe-react-components'
|
import { Icon, Link, Text } from '@gnosis.pm/safe-react-components'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { ReactElement, useEffect, useState } from 'react'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
@ -10,13 +10,15 @@ import Modal from 'src/components/Modal'
|
||||||
import Field from 'src/components/forms/Field'
|
import Field from 'src/components/forms/Field'
|
||||||
import GnoForm from 'src/components/forms/GnoForm'
|
import GnoForm from 'src/components/forms/GnoForm'
|
||||||
import TextField from 'src/components/forms/TextField'
|
import TextField from 'src/components/forms/TextField'
|
||||||
import { composeValidators, minMaxLength, required } from 'src/components/forms/validator'
|
import { composeValidators, required, validAddressBookName } from 'src/components/forms/validator'
|
||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
import Button from 'src/components/layout/Button'
|
import Button from 'src/components/layout/Button'
|
||||||
import Col from 'src/components/layout/Col'
|
import Col from 'src/components/layout/Col'
|
||||||
import Heading from 'src/components/layout/Heading'
|
import Heading from 'src/components/layout/Heading'
|
||||||
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 { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||||
|
import { addressBookAddOrUpdate } from 'src/logic/addressBook/store/actions'
|
||||||
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
|
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
|
||||||
import { getNotificationsFromTxType, enhanceSnackbarForAction } from 'src/logic/notifications'
|
import { getNotificationsFromTxType, enhanceSnackbarForAction } from 'src/logic/notifications'
|
||||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||||
|
@ -25,17 +27,16 @@ import { UpdateSafeModal } from 'src/routes/safe/components/Settings/UpdateSafeM
|
||||||
import { grantedSelector } from 'src/routes/safe/container/selector'
|
import { grantedSelector } from 'src/routes/safe/container/selector'
|
||||||
import { updateSafe } from 'src/logic/safe/store/actions/updateSafe'
|
import { updateSafe } from 'src/logic/safe/store/actions/updateSafe'
|
||||||
|
|
||||||
|
import { useSafeName } from 'src/logic/addressBook/hooks/useSafeName'
|
||||||
import {
|
import {
|
||||||
latestMasterContractVersionSelector,
|
latestMasterContractVersionSelector,
|
||||||
safeCurrentVersionSelector,
|
safeCurrentVersionSelector,
|
||||||
safeNameSelector,
|
|
||||||
safeNeedsUpdateSelector,
|
safeNeedsUpdateSelector,
|
||||||
safeParamAddressFromStateSelector,
|
safeParamAddressFromStateSelector,
|
||||||
} from 'src/logic/safe/store/selectors'
|
} from 'src/logic/safe/store/selectors'
|
||||||
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
|
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
|
||||||
import { fetchMasterCopies, MasterCopy, MasterCopyDeployer } from 'src/logic/contracts/api/masterCopies'
|
import { fetchMasterCopies, MasterCopy, MasterCopyDeployer } from 'src/logic/contracts/api/masterCopies'
|
||||||
import { getMasterCopyAddressFromProxyAddress } from 'src/logic/contracts/safeContracts'
|
import { getMasterCopyAddressFromProxyAddress } from 'src/logic/contracts/safeContracts'
|
||||||
import { LOADED_SAFE_KEY } from 'src/utils/constants'
|
|
||||||
|
|
||||||
export const SAFE_NAME_INPUT_TEST_ID = 'safe-name-input'
|
export const SAFE_NAME_INPUT_TEST_ID = 'safe-name-input'
|
||||||
export const SAFE_NAME_SUBMIT_BTN_TEST_ID = 'change-safe-name-btn'
|
export const SAFE_NAME_SUBMIT_BTN_TEST_ID = 'change-safe-name-btn'
|
||||||
|
@ -52,18 +53,18 @@ const StyledIcon = styled(Icon)`
|
||||||
left: 6px;
|
left: 6px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const SafeDetails = (): React.ReactElement => {
|
const SafeDetails = (): ReactElement => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const isUserOwner = useSelector(grantedSelector)
|
const isUserOwner = useSelector(grantedSelector)
|
||||||
const latestMasterContractVersion = useSelector(latestMasterContractVersionSelector)
|
const latestMasterContractVersion = useSelector(latestMasterContractVersionSelector)
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const safeName = useSelector(safeNameSelector)
|
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||||
|
const safeName = useSafeName(safeAddress)
|
||||||
const safeNeedsUpdate = useSelector(safeNeedsUpdateSelector)
|
const safeNeedsUpdate = useSelector(safeNeedsUpdateSelector)
|
||||||
const safeCurrentVersion = useSelector(safeCurrentVersionSelector)
|
const safeCurrentVersion = useSelector(safeCurrentVersionSelector)
|
||||||
const { trackEvent } = useAnalytics()
|
const { trackEvent } = useAnalytics()
|
||||||
|
|
||||||
const [isModalOpen, setModalOpen] = React.useState(false)
|
const [isModalOpen, setModalOpen] = useState(false)
|
||||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
|
||||||
const [safeInfo, setSafeInfo] = useState<MasterCopy | undefined>()
|
const [safeInfo, setSafeInfo] = useState<MasterCopy | undefined>()
|
||||||
|
|
||||||
const toggleModal = () => {
|
const toggleModal = () => {
|
||||||
|
@ -71,10 +72,9 @@ const SafeDetails = (): React.ReactElement => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = (values) => {
|
const handleSubmit = (values) => {
|
||||||
// In case they set a name we assume the safe want to be stored even if it was opened via URL
|
dispatch(addressBookAddOrUpdate(makeAddressBookEntry({ address: safeAddress, name: values.safeName })))
|
||||||
dispatch(
|
// setting `loadedViaUrl` to `false` as setting a safe's name is considered to intentionally add the safe
|
||||||
updateSafe({ address: safeAddress, name: values.safeName, loadedViaUrl: values.safeName === LOADED_SAFE_KEY }),
|
dispatch(updateSafe({ address: safeAddress, loadedViaUrl: false }))
|
||||||
)
|
|
||||||
|
|
||||||
const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.SAFE_NAME_CHANGE_TX)
|
const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.SAFE_NAME_CHANGE_TX)
|
||||||
dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded)))
|
dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded)))
|
||||||
|
@ -166,7 +166,7 @@ const SafeDetails = (): React.ReactElement => {
|
||||||
testId={SAFE_NAME_INPUT_TEST_ID}
|
testId={SAFE_NAME_INPUT_TEST_ID}
|
||||||
text="Safe name*"
|
text="Safe name*"
|
||||||
type="text"
|
type="text"
|
||||||
validate={composeValidators(required, minMaxLength(1, 50))}
|
validate={composeValidators(required, validAddressBookName)}
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
import { getExplorerInfo } from 'src/config'
|
import { getExplorerInfo } from 'src/config'
|
||||||
import { getNameFromAddressBookSelector } from 'src/logic/addressBook/store/selectors'
|
import { getNameFromAddressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||||
|
import { ADDRESS_BOOK_DEFAULT_NAME } from 'src/logic/addressBook/model/addressBook'
|
||||||
import { sameString } from 'src/utils/strings'
|
import { sameString } from 'src/utils/strings'
|
||||||
|
|
||||||
import DataDisplay from './DataDisplay'
|
import DataDisplay from './DataDisplay'
|
||||||
|
@ -14,14 +15,14 @@ interface AddressInfoProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddressInfo = ({ address, title }: AddressInfoProps): ReactElement => {
|
const AddressInfo = ({ address, title }: AddressInfoProps): ReactElement => {
|
||||||
const name = useSelector((state) => getNameFromAddressBookSelector(state, address))
|
const name = useSelector((state) => getNameFromAddressBookSelector(state, { address }))
|
||||||
const explorerUrl = getExplorerInfo(address)
|
const explorerUrl = getExplorerInfo(address)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataDisplay title={title}>
|
<DataDisplay title={title}>
|
||||||
<EthHashInfo
|
<EthHashInfo
|
||||||
hash={address}
|
hash={address}
|
||||||
name={sameString(name, 'UNKNOWN') ? undefined : name}
|
name={sameString(name, ADDRESS_BOOK_DEFAULT_NAME) ? undefined : name}
|
||||||
showCopyBtn
|
showCopyBtn
|
||||||
showAvatar
|
showAvatar
|
||||||
textSize="lg"
|
textSize="lg"
|
||||||
|
|
|
@ -3,7 +3,6 @@ import MenuItem from '@material-ui/core/MenuItem'
|
||||||
import Close from '@material-ui/icons/Close'
|
import Close from '@material-ui/icons/Close'
|
||||||
import React, { ReactElement, useEffect, useState } from 'react'
|
import React, { ReactElement, useEffect, useState } from 'react'
|
||||||
import { useDispatch } from 'react-redux'
|
import { useDispatch } from 'react-redux'
|
||||||
import { List } from 'immutable'
|
|
||||||
|
|
||||||
import Field from 'src/components/forms/Field'
|
import Field from 'src/components/forms/Field'
|
||||||
import GnoForm from 'src/components/forms/GnoForm'
|
import GnoForm from 'src/components/forms/GnoForm'
|
||||||
|
@ -16,7 +15,6 @@ import Paragraph from 'src/components/layout/Paragraph'
|
||||||
import Row from 'src/components/layout/Row'
|
import Row from 'src/components/layout/Row'
|
||||||
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||||
import { useEstimationStatus } from 'src/logic/hooks/useEstimationStatus'
|
import { useEstimationStatus } from 'src/logic/hooks/useEstimationStatus'
|
||||||
import { SafeOwner } from 'src/logic/safe/store/models/safe'
|
|
||||||
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||||
import { ButtonStatus, Modal } from 'src/components/Modal'
|
import { ButtonStatus, Modal } from 'src/components/Modal'
|
||||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||||
|
@ -32,14 +30,14 @@ const THRESHOLD_FIELD_NAME = 'threshold'
|
||||||
|
|
||||||
type ChangeThresholdModalProps = {
|
type ChangeThresholdModalProps = {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
owners?: List<SafeOwner>
|
ownersCount?: number
|
||||||
safeAddress: string
|
safeAddress: string
|
||||||
threshold?: number
|
threshold?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChangeThresholdModal = ({
|
export const ChangeThresholdModal = ({
|
||||||
onClose,
|
onClose,
|
||||||
owners,
|
ownersCount = 0,
|
||||||
safeAddress,
|
safeAddress,
|
||||||
threshold = 1,
|
threshold = 1,
|
||||||
}: ChangeThresholdModalProps): ReactElement => {
|
}: ChangeThresholdModalProps): ReactElement => {
|
||||||
|
@ -164,7 +162,7 @@ export const ChangeThresholdModal = ({
|
||||||
render={(props) => (
|
render={(props) => (
|
||||||
<>
|
<>
|
||||||
<SelectField {...props} disableError>
|
<SelectField {...props} disableError>
|
||||||
{[...Array(Number(owners?.size))].map((x, index) => (
|
{[...Array(Number(ownersCount))].map((x, index) => (
|
||||||
<MenuItem key={index} value={`${index + 1}`}>
|
<MenuItem key={index} value={`${index + 1}`}>
|
||||||
{index + 1}
|
{index + 1}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -177,7 +175,7 @@ export const ChangeThresholdModal = ({
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={10}>
|
<Col xs={10}>
|
||||||
<Paragraph className={classes.ownersText} color="primary" noMargin size="lg">
|
<Paragraph className={classes.ownersText} color="primary" noMargin size="lg">
|
||||||
{`out of ${owners?.size} owner(s)`}
|
{`out of ${ownersCount} owner(s)`}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -46,9 +46,9 @@ const ThresholdSettings = (): React.ReactElement => {
|
||||||
<Heading tag="h2">Required confirmations</Heading>
|
<Heading tag="h2">Required confirmations</Heading>
|
||||||
<Paragraph>Any transaction requires the confirmation of:</Paragraph>
|
<Paragraph>Any transaction requires the confirmation of:</Paragraph>
|
||||||
<Paragraph className={classes.ownersText} size="lg">
|
<Paragraph className={classes.ownersText} size="lg">
|
||||||
<Bold>{threshold}</Bold> out of <Bold>{owners?.size || 0}</Bold> owners
|
<Bold>{threshold}</Bold> out of <Bold>{owners?.length || 0}</Bold> owners
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
{owners && owners.size > 1 && granted && (
|
{owners && owners.length > 1 && granted && (
|
||||||
<Row className={classes.buttonRow}>
|
<Row className={classes.buttonRow}>
|
||||||
<Button
|
<Button
|
||||||
className={classes.modifyBtn}
|
className={classes.modifyBtn}
|
||||||
|
@ -68,7 +68,12 @@ const ThresholdSettings = (): React.ReactElement => {
|
||||||
open={isModalOpen}
|
open={isModalOpen}
|
||||||
title="Change Required Confirmations"
|
title="Change Required Confirmations"
|
||||||
>
|
>
|
||||||
<ChangeThresholdModal onClose={toggleModal} owners={owners} safeAddress={safeAddress} threshold={threshold} />
|
<ChangeThresholdModal
|
||||||
|
onClose={toggleModal}
|
||||||
|
ownersCount={owners?.length}
|
||||||
|
safeAddress={safeAddress}
|
||||||
|
threshold={threshold}
|
||||||
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,6 +16,7 @@ import ThresholdSettings from './ThresholdSettings'
|
||||||
import RemoveSafeIcon from './assets/icons/bin.svg'
|
import RemoveSafeIcon from './assets/icons/bin.svg'
|
||||||
import { styles } from './style'
|
import { styles } from './style'
|
||||||
|
|
||||||
|
import { getNetworkId } from 'src/config'
|
||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
import ButtonLink from 'src/components/layout/ButtonLink'
|
import ButtonLink from 'src/components/layout/ButtonLink'
|
||||||
import Col from 'src/components/layout/Col'
|
import Col from 'src/components/layout/Col'
|
||||||
|
@ -24,12 +25,17 @@ import Img from 'src/components/layout/Img'
|
||||||
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 Span from 'src/components/layout/Span'
|
import Span from 'src/components/layout/Span'
|
||||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
import {
|
||||||
|
safeNeedsUpdateSelector,
|
||||||
|
safeOwnersWithAddressBookDataSelector,
|
||||||
|
safeSelector,
|
||||||
|
} from 'src/logic/safe/store/selectors'
|
||||||
import { grantedSelector } from 'src/routes/safe/container/selector'
|
import { grantedSelector } from 'src/routes/safe/container/selector'
|
||||||
import { safeLoadedViaUrlSelector, safeNeedsUpdateSelector, safeOwnersSelector } from 'src/logic/safe/store/selectors'
|
|
||||||
|
|
||||||
export const OWNERS_SETTINGS_TAB_TEST_ID = 'owner-settings-tab'
|
export const OWNERS_SETTINGS_TAB_TEST_ID = 'owner-settings-tab'
|
||||||
|
|
||||||
|
const chainId = getNetworkId()
|
||||||
|
|
||||||
const INITIAL_STATE = {
|
const INITIAL_STATE = {
|
||||||
showRemoveSafe: false,
|
showRemoveSafe: false,
|
||||||
menuOptionIndex: 1,
|
menuOptionIndex: 1,
|
||||||
|
@ -40,11 +46,10 @@ const useStyles = makeStyles(styles)
|
||||||
const Settings: React.FC = () => {
|
const Settings: React.FC = () => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const [state, setState] = useState(INITIAL_STATE)
|
const [state, setState] = useState(INITIAL_STATE)
|
||||||
const owners = useSelector(safeOwnersSelector)
|
const owners = useSelector((state) => safeOwnersWithAddressBookDataSelector(state, chainId))
|
||||||
const isSafeLoadedViaUrl = useSelector(safeLoadedViaUrlSelector)
|
|
||||||
const needsUpdate = useSelector(safeNeedsUpdateSelector)
|
const needsUpdate = useSelector(safeNeedsUpdateSelector)
|
||||||
const granted = useSelector(grantedSelector)
|
const granted = useSelector(grantedSelector)
|
||||||
const addressBook = useSelector(addressBookSelector)
|
const safe = useSelector(safeSelector)
|
||||||
|
|
||||||
const handleChange = (menuOptionIndex) => () => {
|
const handleChange = (menuOptionIndex) => () => {
|
||||||
setState((prevState) => ({ ...prevState, menuOptionIndex }))
|
setState((prevState) => ({ ...prevState, menuOptionIndex }))
|
||||||
|
@ -67,13 +72,15 @@ const Settings: React.FC = () => {
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Row className={classes.message}>
|
<Row className={classes.message}>
|
||||||
{!isSafeLoadedViaUrl && (
|
{!safe?.loadedViaUrl && (
|
||||||
<ButtonLink className={classes.removeSafeBtn} color="error" onClick={onShow('RemoveSafe')} size="lg">
|
<>
|
||||||
<Span className={classes.links}>Remove Safe</Span>
|
<ButtonLink className={classes.removeSafeBtn} color="error" onClick={onShow('RemoveSafe')} size="lg">
|
||||||
<Img alt="Trash Icon" className={classes.removeSafeIcon} src={RemoveSafeIcon} />
|
<Span className={classes.links}>Remove Safe</Span>
|
||||||
</ButtonLink>
|
<Img alt="Trash Icon" className={classes.removeSafeIcon} src={RemoveSafeIcon} />
|
||||||
|
</ButtonLink>
|
||||||
|
<RemoveSafeModal isOpen={showRemoveSafe} onClose={onHide('RemoveSafe')} />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<RemoveSafeModal isOpen={showRemoveSafe} onClose={onHide('RemoveSafe')} />
|
|
||||||
</Row>
|
</Row>
|
||||||
<Block className={classes.root}>
|
<Block className={classes.root}>
|
||||||
<Col className={classes.menuWrapper} layout="column">
|
<Col className={classes.menuWrapper} layout="column">
|
||||||
|
@ -109,7 +116,7 @@ const Settings: React.FC = () => {
|
||||||
color={menuOptionIndex === 2 ? 'primary' : 'secondary'}
|
color={menuOptionIndex === 2 ? 'primary' : 'secondary'}
|
||||||
/>
|
/>
|
||||||
<Paragraph className={classes.counter} size="xs">
|
<Paragraph className={classes.counter} size="xs">
|
||||||
{owners.size}
|
{owners.length}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Row>
|
</Row>
|
||||||
<Hairline className={classes.hairline} />
|
<Hairline className={classes.hairline} />
|
||||||
|
@ -148,7 +155,7 @@ const Settings: React.FC = () => {
|
||||||
<Col className={classes.contents} layout="column">
|
<Col className={classes.contents} layout="column">
|
||||||
<Block className={classes.container}>
|
<Block className={classes.container}>
|
||||||
{menuOptionIndex === 1 && <SafeDetails />}
|
{menuOptionIndex === 1 && <SafeDetails />}
|
||||||
{menuOptionIndex === 2 && <ManageOwners addressBook={addressBook} granted={granted} owners={owners} />}
|
{menuOptionIndex === 2 && <ManageOwners granted={granted} owners={owners} />}
|
||||||
{menuOptionIndex === 3 && <ThresholdSettings />}
|
{menuOptionIndex === 3 && <ThresholdSettings />}
|
||||||
{menuOptionIndex === 4 && <SpendingLimitSettings />}
|
{menuOptionIndex === 4 && <SpendingLimitSettings />}
|
||||||
{menuOptionIndex === 5 && <Advanced />}
|
{menuOptionIndex === 5 && <Advanced />}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue