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",
|
||||
"version": "3.6.7",
|
||||
"version": "3.7.0",
|
||||
"description": "Allowing crypto users manage funds in a safer way",
|
||||
"website": "https://github.com/gnosis/safe-react#readme",
|
||||
"bugs": {
|
||||
|
@ -161,7 +161,7 @@
|
|||
"@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-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",
|
||||
"@ledgerhq/hw-transport-node-hid-singleton": "5.51.1",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
|
@ -214,6 +214,7 @@
|
|||
"react-ga": "3.3.0",
|
||||
"react-hot-loader": "4.13.0",
|
||||
"react-intersection-observer": "^8.31.0",
|
||||
"react-papaparse": "^3.14.0",
|
||||
"react-qr-reader": "^2.2.1",
|
||||
"react-redux": "7.2.3",
|
||||
"react-router-dom": "5.2.0",
|
||||
|
@ -221,6 +222,7 @@
|
|||
"react-window": "^1.8.6",
|
||||
"redux": "4.0.5",
|
||||
"redux-actions": "^2.6.5",
|
||||
"redux-localstorage-simple": "^2.4.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"reselect": "^4.0.0",
|
||||
"semver": "^7.3.2",
|
||||
|
|
|
@ -20,15 +20,11 @@ import { getNetworkId } from 'src/config'
|
|||
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
|
||||
import { networkSelector } from 'src/logic/wallets/store/selectors'
|
||||
import { SAFELIST_ADDRESS, WELCOME_ADDRESS } from 'src/routes/routes'
|
||||
import {
|
||||
safeTotalFiatBalanceSelector,
|
||||
safeNameSelector,
|
||||
safeParamAddressFromStateSelector,
|
||||
safeLoadedViaUrlSelector,
|
||||
} from 'src/logic/safe/store/selectors'
|
||||
import { safeTotalFiatBalanceSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors'
|
||||
import Modal from 'src/components/Modal'
|
||||
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 { useSafeScheduledUpdates } from 'src/logic/safe/hooks/useSafeScheduledUpdates'
|
||||
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 history = useHistory()
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const safeName = useSelector(safeNameSelector) ?? ''
|
||||
const safeName = useSafeName(safeAddress)
|
||||
const { safeActionsState, onShow, onHide, showSendFunds, hideSendFunds } = useSafeActions()
|
||||
const currentSafeBalance = useSelector(safeTotalFiatBalanceSelector)
|
||||
const currentCurrency = useSelector(currentCurrencySelector)
|
||||
const granted = useSelector(grantedSelector)
|
||||
const sidebarItems = useSidebarItems()
|
||||
const isSafeLoadedViaUrl = useSelector(safeLoadedViaUrlSelector)
|
||||
const safeLoaded = useLoadSafe(safeAddress, isSafeLoadedViaUrl)
|
||||
const safeLoaded = useLoadSafe(safeAddress)
|
||||
useSafeScheduledUpdates(safeLoaded, safeAddress)
|
||||
|
||||
const sendFunds = safeActionsState.sendFunds
|
||||
|
|
|
@ -56,7 +56,7 @@ const useSidebarItems = (): ListItemType[] => {
|
|||
href: `${matchSafeWithAddress?.url}/transactions`,
|
||||
},
|
||||
{
|
||||
label: 'AddressBook',
|
||||
label: 'ADDRESS BOOK',
|
||||
icon: <ListIcon type="addressBook" />,
|
||||
selected: matchSafeWithAction?.params.safeAction === '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 { cookieBannerOpen } from 'src/logic/cookies/store/selectors'
|
||||
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 { loadGoogleAnalytics, removeCookies } from 'src/utils/googleAnalytics'
|
||||
import { closeIntercom, isIntercomLoaded, loadIntercom } from 'src/utils/intercom'
|
||||
|
@ -98,7 +97,7 @@ interface CookiesBannerFormProps {
|
|||
const CookiesBanner = (): ReactElement => {
|
||||
const classes = useStyles()
|
||||
const dispatch = useRef(useDispatch())
|
||||
const { url: appUrl } = useSafeAppUrl()
|
||||
|
||||
const [showAnalytics, setShowAnalytics] = useState(false)
|
||||
const [showIntercom, setShowIntercom] = useState(false)
|
||||
const [localNecessary, setLocalNecessary] = useState(true)
|
||||
|
@ -107,12 +106,6 @@ const CookiesBanner = (): ReactElement => {
|
|||
|
||||
const showBanner = useSelector(cookieBannerOpen)
|
||||
|
||||
useEffect(() => {
|
||||
if (appUrl) {
|
||||
setTimeout(closeIntercom, 50)
|
||||
}
|
||||
}, [appUrl])
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchCookiesFromStorage() {
|
||||
const cookiesState = await loadFromCookie(COOKIES_KEY)
|
||||
|
@ -178,7 +171,7 @@ const CookiesBanner = (): ReactElement => {
|
|||
dispatch.current(openCookieBanner({ cookieBannerOpen: false }))
|
||||
}
|
||||
|
||||
if (showIntercom && !appUrl) {
|
||||
if (showIntercom) {
|
||||
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 { 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 { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import DefaultBadge from './DefaultBadge'
|
||||
import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||
import { DefaultSafe } from 'src/routes/safe/store/reducer/types/safe'
|
||||
import setDefaultSafe from 'src/logic/safe/store/actions/setDefaultSafe'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { getNetworkInfo } from 'src/config'
|
||||
import { useDispatch } from 'react-redux'
|
||||
|
||||
const StyledButtonLink = styled(ButtonLink)`
|
||||
visibility: hidden;
|
||||
|
@ -51,10 +53,10 @@ type Props = {
|
|||
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
|
||||
export const AddressWrapper = (props: Props): React.ReactElement => {
|
||||
export const AddressWrapper = ({ safe, defaultSafe }: Props): ReactElement => {
|
||||
const classes = useStyles()
|
||||
const { safe, defaultSafe } = props
|
||||
const dispatch = useDispatch()
|
||||
const safeName = useSelector((state) => safeNameSelector(state, safe.address))
|
||||
|
||||
const setDefaultSafeAction = (safeAddress: string) => {
|
||||
dispatch(setDefaultSafe(safeAddress))
|
||||
|
@ -62,7 +64,7 @@ export const AddressWrapper = (props: Props): React.ReactElement => {
|
|||
|
||||
return (
|
||||
<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}>
|
||||
<Text size="xl">{`${formatAmount(safe.ethBalance)} ${nativeCoin.name}`}</Text>
|
||||
|
|
|
@ -39,7 +39,7 @@ type Props = {
|
|||
export const SafeListSidebar = ({ children }: Props): ReactElement => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [filter, setFilter] = useState('')
|
||||
const safes = useSelector(sortedSafeListSelector).filter((safe) => !safe.loadedViaUrl)
|
||||
const safes = useSelector(sortedSafeListSelector)
|
||||
const defaultSafe = useSelector(defaultSafeSelector)
|
||||
const currentSafe = useSelector(safeParamAddressFromStateSelector)
|
||||
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
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,
|
||||
OWNER_ADDRESS_IS_SAFE_ADDRESS_ERROR,
|
||||
mustBeHexData,
|
||||
validAddressBookName,
|
||||
} from 'src/components/forms/validator'
|
||||
|
||||
describe('Forms > Validators', () => {
|
||||
|
@ -249,4 +250,26 @@ describe('Forms > Validators', () => {
|
|||
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 { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { isFeatureEnabled } from 'src/config'
|
||||
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
|
||||
export type GenericValidatorType = (...args: unknown[]) => ValidatorReturnType
|
||||
|
@ -74,9 +75,7 @@ export const mustBeHexData = (data: string): ValidatorReturnType => {
|
|||
export const mustBeAddressHash = memoize(
|
||||
(address: string): ValidatorReturnType => {
|
||||
const errorMessage = 'Must be a valid address'
|
||||
const startsWith0x = address?.startsWith('0x')
|
||||
const isAddress = getWeb3().utils.isAddress(address)
|
||||
return startsWith0x && isAddress ? undefined : errorMessage
|
||||
return isValidAddress(address) ? undefined : errorMessage
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -101,8 +100,12 @@ export const mustBeEthereumContractAddress = memoize(
|
|||
},
|
||||
)
|
||||
|
||||
export const minMaxLength = (minLen: number, maxLen: number) => (value: string): ValidatorReturnType =>
|
||||
value.length >= +minLen && value.length <= +maxLen ? undefined : `Should be ${minLen} to ${maxLen} symbols`
|
||||
export const minMaxLength = (minLen: number, maxLen: number) => (value: string): ValidatorReturnType => {
|
||||
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 => {
|
||||
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 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))
|
||||
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 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 {
|
||||
UNKNOWN = 0,
|
||||
MAINNET = 1,
|
||||
MORDEN = 2,
|
||||
ROPSTEN = 3,
|
||||
|
@ -42,9 +43,8 @@ export enum ETHEREUM_NETWORK {
|
|||
KOVAN = 42,
|
||||
XDAI = 100,
|
||||
ENERGY_WEB_CHAIN = 246,
|
||||
VOLTA = 73799,
|
||||
UNKNOWN = 0,
|
||||
LOCAL = 4447,
|
||||
VOLTA = 73799,
|
||||
}
|
||||
|
||||
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 = {
|
||||
address: string
|
||||
name: string
|
||||
address: string // the contact address
|
||||
name: string // human-readable name
|
||||
chainId: ETHEREUM_NETWORK // see https://chainid.network
|
||||
}
|
||||
|
||||
const networkId = getNetworkId()
|
||||
|
||||
export const makeAddressBookEntry = ({
|
||||
address = '',
|
||||
name = '',
|
||||
address,
|
||||
name,
|
||||
chainId = networkId,
|
||||
}: {
|
||||
address: string
|
||||
name?: string
|
||||
name: string
|
||||
chainId?: number
|
||||
}): AddressBookEntry => ({
|
||||
address,
|
||||
name,
|
||||
chainId,
|
||||
})
|
||||
|
||||
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 { 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[] => {
|
||||
const addressBook = addressBookSelector(state)
|
||||
return addressBook.map((entry) => entry.address)
|
||||
type AddressBookMap = {
|
||||
[chainId: number]: {
|
||||
[address: string]: AddressBookEntry
|
||||
}
|
||||
}
|
||||
|
||||
export const getNameFromAddressBookSelector = createSelector(
|
||||
addressBookSelector,
|
||||
(_, address) => address,
|
||||
(addressBook, address) => {
|
||||
const adbkEntry = addressBook.find((addressBookItem) => addressBookItem.address === address)
|
||||
export const addressBookMapSelector = createSelector(
|
||||
[addressBookSelector],
|
||||
(addressBook): AddressBookMap => {
|
||||
const addressBookMap = {}
|
||||
|
||||
if (adbkEntry) {
|
||||
return adbkEntry.name
|
||||
}
|
||||
return 'UNKNOWN'
|
||||
addressBook.forEach((entry) => {
|
||||
const { address, chainId } = entry
|
||||
if (!addressBookMap[chainId]) {
|
||||
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 {
|
||||
checkIfEntryWasDeletedFromAddressBook,
|
||||
getAddressBookFromStorage,
|
||||
getNameFromAddressBook,
|
||||
getOwnersWithNameFromAddressBook,
|
||||
isValidAddressBookName,
|
||||
migrateOldAddressBook,
|
||||
OldAddressBookEntry,
|
||||
OldAddressBookType,
|
||||
saveAddressBook,
|
||||
} from 'src/logic/addressBook/utils/index'
|
||||
import { buildAddressBook } from 'src/logic/addressBook/store/reducer/addressBook'
|
||||
} from 'src/logic/addressBook/utils'
|
||||
import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
|
||||
const getMockAddressBookEntry = (address: string, name: string = 'test'): AddressBookEntry =>
|
||||
|
@ -19,14 +11,6 @@ const getMockAddressBookEntry = (address: string, name: string = 'test'): Addres
|
|||
name,
|
||||
})
|
||||
|
||||
const getMockOldAddressBookEntry = ({ address = '', name = '', isOwner = false }): OldAddressBookEntry => {
|
||||
return {
|
||||
address,
|
||||
name,
|
||||
isOwner,
|
||||
}
|
||||
}
|
||||
|
||||
describe('getNameFromSafeAddressBook', () => {
|
||||
const entry1 = getMockAddressBookEntry('123456', 'test1')
|
||||
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', () => {
|
||||
it('It should return false if given a blacklisted name like UNKNOWN', () => {
|
||||
// given
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import { List } from 'immutable'
|
||||
import { mustBeEthereumContractAddress } from 'src/components/forms/validator'
|
||||
import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { SafeOwner } from 'src/logic/safe/store/models/safe'
|
||||
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
|
||||
import { AddressBookEntry, AddressBookState } from 'src/logic/addressBook/model/addressBook'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
|
||||
|
||||
const ADDRESS_BOOK_STORAGE_KEY = 'ADDRESS_BOOK_STORAGE_KEY'
|
||||
import { AppReduxState } from 'src/store'
|
||||
import { Overwrite } from 'src/types/helpers'
|
||||
|
||||
export type OldAddressBookEntry = {
|
||||
address: string
|
||||
|
@ -17,44 +15,7 @@ export type OldAddressBookType = {
|
|||
[safeAddress: string]: [OldAddressBookEntry]
|
||||
}
|
||||
|
||||
const ADDRESSBOOK_INVALID_NAMES = ['UNKNOWN', 'OWNER #', 'MY WALLET']
|
||||
|
||||
export const migrateOldAddressBook = (oldAddressBook: OldAddressBookType): AddressBookState => {
|
||||
const values: AddressBookState = []
|
||||
const adbkValues = Object.values(oldAddressBook)
|
||||
|
||||
for (const safeIterator of adbkValues) {
|
||||
for (const safeAddressBook of safeIterator) {
|
||||
if (!values.find((entry) => sameAddress(entry.address, safeAddressBook.address))) {
|
||||
values.push(makeAddressBookEntry({ address: safeAddressBook.address, name: safeAddressBook.name }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
export const getAddressBookFromStorage = async (): Promise<AddressBookState | null> => {
|
||||
const result: OldAddressBookType | string | undefined = await loadFromStorage(ADDRESS_BOOK_STORAGE_KEY)
|
||||
|
||||
if (!result) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (typeof result === 'string') {
|
||||
return JSON.parse(result)
|
||||
}
|
||||
|
||||
return migrateOldAddressBook(result as OldAddressBookType)
|
||||
}
|
||||
|
||||
export const saveAddressBook = async (addressBook: AddressBookState): Promise<void> => {
|
||||
try {
|
||||
await saveToStorage(ADDRESS_BOOK_STORAGE_KEY, JSON.stringify(addressBook))
|
||||
} catch (err) {
|
||||
console.error('Error storing addressBook in localstorage', err)
|
||||
}
|
||||
}
|
||||
export const ADDRESS_BOOK_INVALID_NAMES = ['UNKNOWN', 'OWNER #', 'MY WALLET']
|
||||
|
||||
type GetNameFromAddressBookOptions = {
|
||||
filterOnlyValidName: boolean
|
||||
|
@ -73,32 +34,17 @@ export const getNameFromAddressBook = (
|
|||
}
|
||||
|
||||
export const isValidAddressBookName = (addressBookName: string): boolean => {
|
||||
const hasInvalidName = ADDRESSBOOK_INVALID_NAMES.find((invalidName) =>
|
||||
addressBookName.toUpperCase().includes(invalidName),
|
||||
const hasInvalidName = ADDRESS_BOOK_INVALID_NAMES.find((invalidName) =>
|
||||
addressBookName?.toUpperCase().includes(invalidName),
|
||||
)
|
||||
return !hasInvalidName
|
||||
}
|
||||
|
||||
// TODO: is this really required?
|
||||
export const getValidAddressBookName = (addressBookName: string): string | 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 = (
|
||||
addressBook: AddressBookState,
|
||||
addresses: string[],
|
||||
|
@ -111,12 +57,13 @@ export const formatAddressListToAddressBookNames = (
|
|||
return {
|
||||
address: address,
|
||||
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
|
||||
* @param name
|
||||
* @param address
|
||||
|
@ -172,3 +119,11 @@ export const filterAddressEntries = (
|
|||
|
||||
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 {
|
||||
___0 = '0: No such error code',
|
||||
_100 = '100: Invalid input in the address field',
|
||||
_200 = '200: Failed migrating to the address book v2',
|
||||
_600 = '600: Error fetching token list',
|
||||
_601 = '601: Error fetching balances',
|
||||
_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,
|
||||
}
|
||||
|
||||
const addressBookImportEntries = {
|
||||
beforeExecution: null,
|
||||
afterRejection: null,
|
||||
waitingConfirmation: null,
|
||||
afterExecution: {
|
||||
noMoreConfirmationsNeeded: NOTIFICATIONS.ADDRESS_BOOK_IMPORT_ENTRIES_SUCCESS,
|
||||
moreConfirmationsNeeded: null,
|
||||
},
|
||||
afterExecutionError: null,
|
||||
}
|
||||
|
||||
const addressBookDeleteEntry = {
|
||||
beforeExecution: null,
|
||||
afterRejection: null,
|
||||
|
@ -153,6 +164,17 @@ const addressBookDeleteEntry = {
|
|||
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) => {
|
||||
let notificationsQueue
|
||||
|
||||
|
@ -193,18 +215,26 @@ export const getNotificationsFromTxType: any = (txType, origin) => {
|
|||
notificationsQueue = waitingTransactionNotificationsQueue
|
||||
break
|
||||
}
|
||||
case TX_NOTIFICATION_TYPES.ADDRESSBOOK_NEW_ENTRY: {
|
||||
case TX_NOTIFICATION_TYPES.ADDRESS_BOOK_NEW_ENTRY: {
|
||||
notificationsQueue = addressBookNewEntry
|
||||
break
|
||||
}
|
||||
case TX_NOTIFICATION_TYPES.ADDRESSBOOK_EDIT_ENTRY: {
|
||||
case TX_NOTIFICATION_TYPES.ADDRESS_BOOK_EDIT_ENTRY: {
|
||||
notificationsQueue = addressBookEditEntry
|
||||
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
|
||||
break
|
||||
}
|
||||
case TX_NOTIFICATION_TYPES.ADDRESS_BOOK_EXPORT_ENTRIES: {
|
||||
notificationsQueue = addressBookExportEntries
|
||||
break
|
||||
}
|
||||
default: {
|
||||
notificationsQueue = defaultNotificationsQueue
|
||||
break
|
||||
|
|
|
@ -52,7 +52,10 @@ const NOTIFICATION_IDS = {
|
|||
WRONG_NETWORK_MSG: 'WRONG_NETWORK_MSG',
|
||||
ADDRESS_BOOK_NEW_ENTRY_SUCCESS: 'ADDRESS_BOOK_NEW_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_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',
|
||||
}
|
||||
|
||||
|
@ -207,10 +210,22 @@ export const NOTIFICATIONS: Record<NotificationId, Notification> = {
|
|||
message: 'Entry saved successfully',
|
||||
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: {
|
||||
message: 'Entry deleted successfully',
|
||||
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_NEW_VERSION_AVAILABLE: {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
|
||||
import loadAddressBookFromStorage from 'src/logic/addressBook/store/actions/loadAddressBookFromStorage'
|
||||
import addViewedSafe from 'src/logic/currentSession/store/actions/addViewedSafe'
|
||||
import { fetchSafeTokens } from 'src/logic/tokens/store/actions/fetchSafeTokens'
|
||||
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 { 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 [isSafeLoaded, setIsSafeLoaded] = useState(false)
|
||||
|
||||
|
@ -23,15 +22,11 @@ export const useLoadSafe = (safeAddress?: string, loadedViaUrl = true): boolean
|
|||
await dispatch(fetchSafeTokens(safeAddress))
|
||||
await dispatch(updateAvailableCurrencies())
|
||||
await dispatch(fetchTransactions(safeAddress))
|
||||
if (!loadedViaUrl) {
|
||||
dispatch(addViewedSafe(safeAddress))
|
||||
}
|
||||
dispatch(addViewedSafe(safeAddress))
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(loadAddressBookFromStorage())
|
||||
fetchData()
|
||||
}, [dispatch, safeAddress, loadedViaUrl])
|
||||
}, [dispatch, safeAddress])
|
||||
|
||||
return isSafeLoaded
|
||||
}
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
// --no-ignore
|
||||
import axios from 'axios'
|
||||
import { List, Map } from 'immutable'
|
||||
import { Map } from 'immutable'
|
||||
import configureMockStore from 'redux-mock-store'
|
||||
import thunk from 'redux-thunk'
|
||||
|
||||
import { buildSafe, fetchSafe } from 'src/logic/safe/store/actions/fetchSafe'
|
||||
import * as storageUtils from 'src/utils/storage'
|
||||
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 { DEFAULT_SAFE_INITIAL_STATE } from 'src/logic/safe/store/reducer/safe'
|
||||
import { inMemoryPartialSafeInformation, localSafesInfo, remoteSafeInfoWithoutModules } from '../mocks/safeInformation'
|
||||
|
@ -25,51 +23,46 @@ describe('buildSafe', () => {
|
|||
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 }))
|
||||
storageUtil.loadFromStorage.mockImplementationOnce(async () => localSafesInfo)
|
||||
const finalValues: Overwrite<Partial<SafeRecordProps>, { name: string }> = {
|
||||
name: 'My Safe Name that will last',
|
||||
const finalValues: Partial<SafeRecordProps> = {
|
||||
modules: undefined,
|
||||
spendingLimits: undefined,
|
||||
}
|
||||
|
||||
const builtSafe = await buildSafe(SAFE_ADDRESS, finalValues.name)
|
||||
const builtSafe = await buildSafe(SAFE_ADDRESS)
|
||||
|
||||
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(() => {})
|
||||
mockedAxios.get.mockImplementationOnce(async () => {
|
||||
throw new Error('-- test -- no resource available')
|
||||
})
|
||||
const name = 'My Safe Name that will last'
|
||||
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 }))
|
||||
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({
|
||||
name,
|
||||
address: SAFE_ADDRESS,
|
||||
threshold: 2,
|
||||
owners: List(
|
||||
[
|
||||
{ address: '0xcCdd7e3af1c24c08D8B65A328351e7e23923d875' },
|
||||
{ address: '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F' },
|
||||
{ address: '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47' },
|
||||
{ address: '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2' },
|
||||
{ address: '0x5e47249883F6a1d639b84e8228547fB289e222b6' },
|
||||
].map(makeOwner),
|
||||
),
|
||||
owners: [
|
||||
'0xcCdd7e3af1c24c08D8B65A328351e7e23923d875',
|
||||
'0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F',
|
||||
'0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47',
|
||||
'0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2',
|
||||
'0x5e47249883F6a1d639b84e8228547fB289e222b6',
|
||||
],
|
||||
modules: undefined,
|
||||
spendingLimits: undefined,
|
||||
nonce: 492,
|
||||
|
@ -78,19 +71,18 @@ describe('buildSafe', () => {
|
|||
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(() => {})
|
||||
mockedAxios.get.mockImplementationOnce(async () => {
|
||||
throw new Error('-- test -- no resource available')
|
||||
})
|
||||
const finalValues: Overwrite<Partial<SafeRecordProps>, { name: string }> = {
|
||||
name: 'My Safe Name that will last',
|
||||
const finalValues: Partial<SafeRecordProps> = {
|
||||
address: SAFE_ADDRESS,
|
||||
owners: undefined,
|
||||
}
|
||||
storageUtil.loadFromStorage.mockImplementationOnce(async () => undefined)
|
||||
|
||||
const builtSafe = await buildSafe(SAFE_ADDRESS, finalValues.name)
|
||||
const builtSafe = await buildSafe(SAFE_ADDRESS)
|
||||
|
||||
expect(builtSafe).toStrictEqual(finalValues)
|
||||
})
|
||||
|
@ -99,7 +91,6 @@ describe('buildSafe', () => {
|
|||
describe('fetchSafe', () => {
|
||||
const SAFE_ADDRESS = '0xe414604Ad49602C0b9c0b08D0781ECF96740786a'
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>
|
||||
const storageUtil = require('src/utils/storage/index') as jest.Mocked<typeof storageUtils>
|
||||
const middlewares = [thunk]
|
||||
const mockStore = configureMockStore(middlewares)
|
||||
|
||||
|
@ -115,15 +106,13 @@ describe('fetchSafe', () => {
|
|||
payload: {
|
||||
address: SAFE_ADDRESS,
|
||||
threshold: 2,
|
||||
owners: List(
|
||||
[
|
||||
{ address: '0xcCdd7e3af1c24c08D8B65A328351e7e23923d875' },
|
||||
{ address: '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F' },
|
||||
{ address: '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47' },
|
||||
{ address: '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2' },
|
||||
{ address: '0x5e47249883F6a1d639b84e8228547fB289e222b6' },
|
||||
].map(makeOwner),
|
||||
),
|
||||
owners: [
|
||||
'0xcCdd7e3af1c24c08D8B65A328351e7e23923d875',
|
||||
'0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F',
|
||||
'0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47',
|
||||
'0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2',
|
||||
'0x5e47249883F6a1d639b84e8228547fB289e222b6',
|
||||
],
|
||||
modules: undefined,
|
||||
spendingLimits: undefined,
|
||||
nonce: 492,
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import axios from 'axios'
|
||||
import { List } from 'immutable'
|
||||
|
||||
import { FEATURES } from 'src/config/networks/network.d'
|
||||
import {
|
||||
|
@ -9,8 +8,7 @@ import {
|
|||
getNewTxNonce,
|
||||
shouldExecuteTransaction,
|
||||
} from 'src/logic/safe/store/actions/utils'
|
||||
import { makeOwner } from 'src/logic/safe/store/models/owner'
|
||||
import { SafeOwner, SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||
import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||
import { buildTxServiceUrl } from 'src/logic/safe/transactions'
|
||||
import { getMockedSafeInstance, getMockedTxServiceModel } from 'src/test/utils/safeHelper'
|
||||
import {
|
||||
|
@ -223,96 +221,49 @@ describe('buildSafeOwners', () => {
|
|||
expect(buildSafeOwners()).toBeUndefined()
|
||||
})
|
||||
it('should return `localSafeOwners` if no `remoteSafeOwners` were provided', () => {
|
||||
const expectedOwners = List(
|
||||
[
|
||||
{ address: '0xcCdd7e3af1c24c08D8B65A328351e7e23923d875' },
|
||||
{ address: '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F' },
|
||||
{ address: '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47' },
|
||||
{ address: '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2' },
|
||||
{ address: '0x5e47249883F6a1d639b84e8228547fB289e222b6' },
|
||||
].map(makeOwner),
|
||||
)
|
||||
const expectedOwners = [
|
||||
'0xcCdd7e3af1c24c08D8B65A328351e7e23923d875',
|
||||
'0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F',
|
||||
'0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47',
|
||||
'0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2',
|
||||
'0x5e47249883F6a1d639b84e8228547fB289e222b6',
|
||||
]
|
||||
expect(buildSafeOwners(remoteSafeInfoWithModules.owners)).toStrictEqual(expectedOwners)
|
||||
})
|
||||
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 expectedOwners = List(
|
||||
[
|
||||
{
|
||||
name: 'UNKNOWN',
|
||||
address: '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F',
|
||||
},
|
||||
{
|
||||
name: 'UNKNOWN',
|
||||
address: '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47',
|
||||
},
|
||||
{
|
||||
name: 'Owner B',
|
||||
address: '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2',
|
||||
},
|
||||
{
|
||||
name: 'Owner A',
|
||||
address: '0x5e47249883F6a1d639b84e8228547fB289e222b6',
|
||||
},
|
||||
].map(makeOwner),
|
||||
)
|
||||
const expectedOwners = [
|
||||
'0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F',
|
||||
'0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47',
|
||||
'0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2',
|
||||
'0x5e47249883F6a1d639b84e8228547fB289e222b6',
|
||||
]
|
||||
|
||||
expect(buildSafeOwners(remoteOwners, localOwners)).toStrictEqual(expectedOwners)
|
||||
})
|
||||
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 expectedOwners = List(
|
||||
[
|
||||
{
|
||||
name: 'UNKNOWN',
|
||||
address: '0xcCdd7e3af1c24c08D8B65A328351e7e23923d875',
|
||||
},
|
||||
{
|
||||
name: 'UNKNOWN',
|
||||
address: '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F',
|
||||
},
|
||||
{
|
||||
name: 'UNKNOWN',
|
||||
address: '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47',
|
||||
},
|
||||
{
|
||||
name: 'Owner B',
|
||||
address: '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2',
|
||||
},
|
||||
{
|
||||
name: 'UNKNOWN',
|
||||
address: '0x5e47249883F6a1d639b84e8228547fB289e222b6',
|
||||
},
|
||||
].map(makeOwner),
|
||||
)
|
||||
const expectedOwners = [
|
||||
'0xcCdd7e3af1c24c08D8B65A328351e7e23923d875',
|
||||
'0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F',
|
||||
'0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47',
|
||||
'0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2',
|
||||
'0x5e47249883F6a1d639b84e8228547fB289e222b6',
|
||||
]
|
||||
|
||||
expect(buildSafeOwners(remoteOwners, localOwners)).toStrictEqual(expectedOwners)
|
||||
})
|
||||
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 expectedOwners = List(
|
||||
[
|
||||
{
|
||||
name: 'UNKNOWN',
|
||||
address: '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F',
|
||||
},
|
||||
{
|
||||
name: 'UNKNOWN',
|
||||
address: '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47',
|
||||
},
|
||||
{
|
||||
name: 'Owner B',
|
||||
address: '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2',
|
||||
},
|
||||
{
|
||||
name: 'UNKNOWN',
|
||||
address: '0x5e47249883F6a1d639b84e8228547fB289e222b6',
|
||||
},
|
||||
].map(makeOwner),
|
||||
)
|
||||
const expectedOwners = [
|
||||
'0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F',
|
||||
'0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47',
|
||||
'0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2',
|
||||
'0x5e47249883F6a1d639b84e8228547fB289e222b6',
|
||||
]
|
||||
|
||||
expect(buildSafeOwners(remoteOwners, localOwners)).toStrictEqual(expectedOwners)
|
||||
})
|
||||
|
|
|
@ -1,17 +1,9 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
|
||||
import { SafeOwner, SafeRecordProps } from '../models/safe'
|
||||
import { List } from 'immutable'
|
||||
import { makeOwner } from '../models/owner'
|
||||
import { SafeRecordProps } from 'src/logic/safe/store/models/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) => ({
|
||||
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 { Action } from 'redux-actions'
|
||||
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { updateSafe } from 'src/logic/safe/store/actions/updateSafe'
|
||||
import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||
import { getLocalSafe } from 'src/logic/safe/utils'
|
||||
import { allSettled } from 'src/logic/safe/utils/allSettled'
|
||||
import { getSafeInfo, SafeInfo } from 'src/logic/safe/utils/safeInformation'
|
||||
import { AppReduxState } from 'src/store'
|
||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||
import { buildSafeOwners, extractRemoteSafeInfo } from './utils'
|
||||
import { Action } from 'redux-actions'
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @param {string} safeAddress
|
||||
* @param {string} safeName
|
||||
* @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 safeInfo: Partial<SafeRecordProps> = {
|
||||
address,
|
||||
name: safeName,
|
||||
}
|
||||
// setting `loadedViaUrl` to false, as `buildSafe` is called on safe Load or Open flows
|
||||
const safeInfo: Partial<SafeRecordProps> = { address, loadedViaUrl: false }
|
||||
|
||||
const [remote, localSafeInfo] = await allSettled<[SafeInfo | null, SafeRecordProps | undefined | null]>(
|
||||
getSafeInfo(safeAddress),
|
||||
|
@ -52,7 +46,6 @@ export const buildSafe = async (safeAddress: string, safeName: string): Promise<
|
|||
*/
|
||||
export const fetchSafe = (safeAddress: string) => async (
|
||||
dispatch: Dispatch,
|
||||
getState: () => AppReduxState,
|
||||
): Promise<Action<Partial<SafeRecordProps>>> => {
|
||||
const address = checksumAddress(safeAddress)
|
||||
|
||||
|
@ -61,13 +54,10 @@ export const fetchSafe = (safeAddress: string) => async (
|
|||
// remote (client-gateway)
|
||||
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
|
||||
const owners = remoteSafeInfo
|
||||
? // if we have remote info, we can enrich it with local address book information
|
||||
buildSafeOwners(remoteSafeInfo.owners, List(addressBook))
|
||||
? // if we have remote info, we use it
|
||||
buildSafeOwners(remoteSafeInfo.owners)
|
||||
: // if there's no remote info, we keep what's in memory
|
||||
undefined
|
||||
|
||||
|
|
|
@ -1,24 +1,15 @@
|
|||
import { Dispatch } from 'redux'
|
||||
|
||||
import { SAFES_KEY } from 'src/logic/safe/utils'
|
||||
import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||
import { getLocalSafes } from 'src/logic/safe/utils'
|
||||
import { buildSafe } from 'src/logic/safe/store/reducer/safe'
|
||||
import { loadFromStorage } from 'src/utils/storage'
|
||||
|
||||
import { addOrUpdateSafe } from './addOrUpdateSafe'
|
||||
|
||||
const loadSafesFromStorage = () => async (dispatch: Dispatch): Promise<void> => {
|
||||
try {
|
||||
const safes = await loadFromStorage<Record<string, SafeRecordProps>>(SAFES_KEY)
|
||||
const safes = await getLocalSafes()
|
||||
|
||||
if (safes) {
|
||||
Object.values(safes).forEach((safeProps) => {
|
||||
dispatch(addOrUpdateSafe(buildSafe(safeProps)))
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line
|
||||
console.error('Error while getting Safes from storage:', err)
|
||||
if (safes) {
|
||||
safes.forEach((safeProps) => {
|
||||
dispatch(addOrUpdateSafe(buildSafe(safeProps)))
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
import { List } from 'immutable'
|
||||
import { makeOwner } from '../../models/owner'
|
||||
|
||||
export const remoteSafeInfoWithModules = {
|
||||
address: {
|
||||
value: '0xe414604Ad49602C0b9c0b08D0781ECF96740786a',
|
||||
|
@ -86,30 +83,13 @@ export const localSafesInfo = {
|
|||
name: 'Safe A',
|
||||
address: '0xe414604Ad49602C0b9c0b08D0781ECF96740786a',
|
||||
threshold: 2,
|
||||
owners: List(
|
||||
[
|
||||
{
|
||||
name: 'UNKNOWN',
|
||||
address: '0xcCdd7e3af1c24c08D8B65A328351e7e23923d875',
|
||||
},
|
||||
{
|
||||
name: 'UNKNOWN',
|
||||
address: '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F',
|
||||
},
|
||||
{
|
||||
name: 'UNKNOWN',
|
||||
address: '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47',
|
||||
},
|
||||
{
|
||||
name: 'Owner B',
|
||||
address: '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2',
|
||||
},
|
||||
{
|
||||
name: 'Owner A',
|
||||
address: '0x5e47249883F6a1d639b84e8228547fB289e222b6',
|
||||
},
|
||||
].map(makeOwner),
|
||||
),
|
||||
owners: [
|
||||
'0xcCdd7e3af1c24c08D8B65A328351e7e23923d875',
|
||||
'0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F',
|
||||
'0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47',
|
||||
'0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2',
|
||||
'0x5e47249883F6a1d639b84e8228547fB289e222b6',
|
||||
],
|
||||
modules: [['0x0000000000000000000000000000000000000001', '0xCFbFaC74C26F8647cBDb8c5caf80BB5b32E43134']],
|
||||
spendingLimits: [
|
||||
{
|
||||
|
@ -177,30 +157,13 @@ export const inMemoryPartialSafeInformation = {
|
|||
name: 'Safe A',
|
||||
address: '0xe414604Ad49602C0b9c0b08D0781ECF96740786a',
|
||||
threshold: 2,
|
||||
owners: List(
|
||||
[
|
||||
{
|
||||
name: 'UNKNOWN',
|
||||
address: '0xcCdd7e3af1c24c08D8B65A328351e7e23923d875',
|
||||
},
|
||||
{
|
||||
name: 'UNKNOWN',
|
||||
address: '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F',
|
||||
},
|
||||
{
|
||||
name: 'UNKNOWN',
|
||||
address: '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47',
|
||||
},
|
||||
{
|
||||
name: 'Owner B',
|
||||
address: '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2',
|
||||
},
|
||||
{
|
||||
name: 'Owner A',
|
||||
address: '0x5e47249883F6a1d639b84e8228547fB289e222b6',
|
||||
},
|
||||
].map(makeOwner),
|
||||
),
|
||||
owners: [
|
||||
'0xcCdd7e3af1c24c08D8B65A328351e7e23923d875',
|
||||
'0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F',
|
||||
'0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47',
|
||||
'0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2',
|
||||
'0x5e47249883F6a1d639b84e8228547fB289e222b6',
|
||||
],
|
||||
modules: [['0x0000000000000000000000000000000000000001', '0xCFbFaC74C26F8647cBDb8c5caf80BB5b32E43134']],
|
||||
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 { buildModulesLinkedList } from 'src/logic/safe/utils/modules'
|
||||
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 { List } from 'immutable'
|
||||
|
||||
export const getLastTx = async (safeAddress: string): Promise<TxServiceModel | null> => {
|
||||
try {
|
||||
|
@ -105,13 +102,9 @@ export const buildSafeOwners = (
|
|||
localSafeOwners?: SafeRecordProps['owners'],
|
||||
): SafeRecordProps['owners'] | undefined => {
|
||||
if (remoteSafeOwners) {
|
||||
const remoteOwners = remoteSafeOwners.map(({ value }) => {
|
||||
const localOwner = localSafeOwners?.find(({ address }) => sameAddress(address, value))
|
||||
const name = localOwner?.name
|
||||
return makeOwner({ name, address: checksumAddress(value) })
|
||||
})
|
||||
|
||||
return List(remoteOwners)
|
||||
// ToDo: review if checksums addresses is necessary,
|
||||
// as they must be provided already in the checksum form from the services
|
||||
return remoteSafeOwners.map(({ value }) => checksumAddress(value))
|
||||
}
|
||||
|
||||
// nothing to do without remote owners, so we return the stored list
|
||||
|
|
|
@ -1,30 +1,13 @@
|
|||
import { Store } from 'redux'
|
||||
|
||||
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_OWNER } from 'src/logic/safe/store/actions/removeSafeOwner'
|
||||
import { REPLACE_SAFE_OWNER } from 'src/logic/safe/store/actions/replaceSafeOwner'
|
||||
import { SET_DEFAULT_SAFE } from 'src/logic/safe/store/actions/setDefaultSafe'
|
||||
import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe'
|
||||
import { 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'
|
||||
|
||||
const watchedActions = [
|
||||
UPDATE_SAFE,
|
||||
REMOVE_SAFE,
|
||||
ADD_OR_UPDATE_SAFE,
|
||||
ADD_SAFE_OWNER,
|
||||
REMOVE_SAFE_OWNER,
|
||||
REPLACE_SAFE_OWNER,
|
||||
EDIT_SAFE_OWNER,
|
||||
SET_DEFAULT_SAFE,
|
||||
]
|
||||
const watchedActions = [REMOVE_SAFE, SET_DEFAULT_SAFE, UPDATE_SAFE]
|
||||
|
||||
type SafeProps = {
|
||||
safe: SafeRecord
|
||||
|
@ -40,34 +23,10 @@ export const safeStorageMiddleware = (store: Store) => (
|
|||
|
||||
if (watchedActions.includes(action.type)) {
|
||||
const state = store.getState()
|
||||
const { dispatch } = store
|
||||
const safes = safesMapSelector(state)
|
||||
await saveSafes(safes.filter((safe) => !safe.loadedViaUrl).toJSON())
|
||||
|
||||
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: {
|
||||
if (action.payload) {
|
||||
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 { BalanceRecord } from 'src/logic/tokens/store/actions/fetchSafeTokens'
|
||||
|
||||
export type SafeOwner = {
|
||||
name: string
|
||||
address: string
|
||||
}
|
||||
export type SafeOwner = string
|
||||
|
||||
export type ModulePair = [
|
||||
// previous module
|
||||
|
@ -25,39 +23,37 @@ export type SpendingLimit = {
|
|||
}
|
||||
|
||||
export type SafeRecordProps = {
|
||||
name: string
|
||||
address: string
|
||||
threshold: number
|
||||
ethBalance: string
|
||||
totalFiatBalance: string
|
||||
owners: List<SafeOwner>
|
||||
owners: SafeOwner[]
|
||||
modules?: ModulePair[] | null
|
||||
spendingLimits?: SpendingLimit[] | null
|
||||
balances: BalanceRecord[]
|
||||
nonce: number
|
||||
recurringUser?: boolean
|
||||
loadedViaUrl?: boolean
|
||||
currentVersion: string
|
||||
needsUpdate: boolean
|
||||
featuresEnabled: Array<FEATURES>
|
||||
loadedViaUrl: boolean
|
||||
}
|
||||
|
||||
const makeSafe = Record<SafeRecordProps>({
|
||||
name: '',
|
||||
address: '',
|
||||
threshold: 0,
|
||||
ethBalance: '0',
|
||||
totalFiatBalance: '0',
|
||||
owners: List([]),
|
||||
owners: [],
|
||||
modules: [],
|
||||
spendingLimits: [],
|
||||
balances: [],
|
||||
nonce: 0,
|
||||
loadedViaUrl: false,
|
||||
recurringUser: undefined,
|
||||
currentVersion: '',
|
||||
needsUpdate: false,
|
||||
featuresEnabled: [],
|
||||
loadedViaUrl: true,
|
||||
})
|
||||
|
||||
export type SafeRecord = RecordOf<SafeRecordProps>
|
||||
|
|
|
@ -1,30 +1,22 @@
|
|||
import { Map, List } from 'immutable'
|
||||
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_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_LATEST_MASTER_CONTRACT_VERSION } from 'src/logic/safe/store/actions/setLatestMasterContractVersion'
|
||||
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 { AppReduxState } from 'src/store'
|
||||
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 { shouldSafeStoreBeUpdated } from 'src/logic/safe/utils/shouldSafeStoreBeUpdated'
|
||||
import { LOADED_SAFE_KEY } from 'src/utils/constants'
|
||||
|
||||
export const SAFE_REDUCER_ID = 'safes'
|
||||
export const DEFAULT_SAFE_INITIAL_STATE = 'NOT_ASKED'
|
||||
|
||||
export const buildSafe = (storedSafe: SafeRecordProps): SafeRecordProps => {
|
||||
const names = storedSafe.owners.map((owner) => owner.name)
|
||||
const addresses = storedSafe.owners.map((owner) => checksumAddress(owner.address))
|
||||
const owners = buildOwnersFrom(Array.from(names), Array.from(addresses))
|
||||
const owners = storedSafe.owners.map(checksumAddress)
|
||||
|
||||
return {
|
||||
...storedSafe,
|
||||
|
@ -82,15 +74,8 @@ export default handleActions<AppReduxState['safes'], Payloads>(
|
|||
const shouldUpdate = shouldSafeStoreBeUpdated(safe, state.getIn(['safes', safeAddress]))
|
||||
|
||||
return shouldUpdate
|
||||
? state.updateIn(
|
||||
['safes', safeAddress],
|
||||
// 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.updateIn(['safes', safeAddress], makeSafe({ address: safeAddress }), (prevSafe) =>
|
||||
updateSafeProps(prevSafe, safe),
|
||||
)
|
||||
: state
|
||||
},
|
||||
|
@ -104,15 +89,8 @@ export default handleActions<AppReduxState['safes'], Payloads>(
|
|||
|
||||
const shouldUpdate = shouldSafeStoreBeUpdated(safe, state.getIn(['safes', safeAddress]))
|
||||
return shouldUpdate
|
||||
? state.updateIn(
|
||||
['safes', safeAddress],
|
||||
// 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.updateIn(['safes', safeAddress], makeSafe({ address: safeAddress }), (prevSafe) =>
|
||||
updateSafeProps(prevSafe, safe),
|
||||
)
|
||||
: state
|
||||
},
|
||||
|
@ -128,54 +106,6 @@ export default handleActions<AppReduxState['safes'], Payloads>(
|
|||
|
||||
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_LATEST_MASTER_CONTRACT_VERSION]: (state, action: Action<SafeRecord>) =>
|
||||
state.set('latestMasterContractVersion', action.payload),
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
import { List } from 'immutable'
|
||||
import { matchPath, RouteComponentProps } from 'react-router-dom'
|
||||
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 { AppReduxState } from 'src/store'
|
||||
|
||||
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 { BalanceRecord } from 'src/logic/tokens/store/actions/fetchSafeTokens'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
|
||||
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())
|
||||
|
||||
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 defaultSafeSelector = createSelector(safesStateSelector, (safeState) => safeState.get('defaultSafe'))
|
||||
|
@ -83,8 +114,6 @@ export const safeFieldSelector = <K extends keyof SafeRecordProps>(field: K) =>
|
|||
safe: SafeRecord,
|
||||
): 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 safeNeedsUpdateSelector = createSelector(safeSelector, safeFieldSelector('needsUpdate'))
|
||||
|
@ -103,19 +132,24 @@ export const safeFeaturesEnabledSelector = createSelector(safeSelector, safeFiel
|
|||
|
||||
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) => {
|
||||
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',
|
||||
SAFE_NAME_CHANGE_TX: 'SAFE_NAME_CHANGE_TX',
|
||||
OWNER_NAME_CHANGE_TX: 'OWNER_NAME_CHANGE_TX',
|
||||
ADDRESSBOOK_NEW_ENTRY: 'ADDRESSBOOK_NEW_ENTRY',
|
||||
ADDRESSBOOK_EDIT_ENTRY: 'ADDRESSBOOK_EDIT_ENTRY',
|
||||
ADDRESSBOOK_DELETE_ENTRY: 'ADDRESSBOOK_DELETE_ENTRY',
|
||||
ADDRESS_BOOK_NEW_ENTRY: 'ADDRESS_BOOK_NEW_ENTRY',
|
||||
ADDRESS_BOOK_EDIT_ENTRY: 'ADDRESS_BOOK_EDIT_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,
|
||||
ethBalance,
|
||||
threshold,
|
||||
name,
|
||||
nonce,
|
||||
modules,
|
||||
spendingLimits,
|
||||
}: Partial<SafeRecordProps>): SafeRecordProps => {
|
||||
const owner1 = {
|
||||
name: 'MockedOwner1',
|
||||
address: '0x3bE3c2dE077FBC409ae50AFFA66a94a9aE669A8d',
|
||||
}
|
||||
const owner2 = {
|
||||
name: 'MockedOwner2',
|
||||
address: '0xA2366b0c2607de70777d87aCdD1D22F0708fA6a3',
|
||||
}
|
||||
const owner1 = '0x3bE3c2dE077FBC409ae50AFFA66a94a9aE669A8d'
|
||||
const owner2 = '0xA2366b0c2607de70777d87aCdD1D22F0708fA6a3'
|
||||
const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1'
|
||||
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
|
||||
|
||||
return {
|
||||
name: name || 'MockedSafe',
|
||||
address: address || '0xAE173F30ec9A293d37c44BA68d3fCD35F989Ce9F',
|
||||
threshold: threshold || 2,
|
||||
ethBalance: ethBalance || '10',
|
||||
owners: owners || List([owner1, owner2]),
|
||||
owners: owners || [owner1, owner2],
|
||||
modules: modules || [],
|
||||
spendingLimits: spendingLimits || [],
|
||||
balances: balances || [
|
||||
|
@ -46,6 +38,7 @@ const getMockedOldSafe = ({
|
|||
needsUpdate: needsUpdate || false,
|
||||
featuresEnabled: featuresEnabled || [],
|
||||
totalFiatBalance: '110',
|
||||
loadedViaUrl: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -75,21 +68,6 @@ describe('shouldSafeStoreBeUpdated', () => {
|
|||
// Then
|
||||
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`, () => {
|
||||
// given
|
||||
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`, () => {
|
||||
// given
|
||||
const owner1 = {
|
||||
name: 'MockedOwner1',
|
||||
address: '0x3bE3c2dE077FBC409ae50AFFA66a94a9aE669A8d',
|
||||
}
|
||||
const owner2 = {
|
||||
name: 'MockedOwner2',
|
||||
address: '0xA2366b0c2607de70777d87aCdD1D22F0708fA6a3',
|
||||
}
|
||||
const oldSafe = getMockedOldSafe({ owners: List([owner1, owner2]) })
|
||||
const newSafeProps: Partial<SafeRecordProps> = {
|
||||
owners: List([owner1]),
|
||||
}
|
||||
const owner1 = '0x3bE3c2dE077FBC409ae50AFFA66a94a9aE669A8d'
|
||||
const owner2 = '0xA2366b0c2607de70777d87aCdD1D22F0708fA6a3'
|
||||
const oldSafe = getMockedOldSafe({ owners: [owner1, owner2] })
|
||||
const newSafeProps: Partial<SafeRecordProps> = { owners: [owner1] }
|
||||
|
||||
// When
|
||||
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
||||
|
@ -146,9 +116,7 @@ describe('shouldSafeStoreBeUpdated', () => {
|
|||
const oldModulesList = []
|
||||
const newModulesList = null
|
||||
const oldSafe = getMockedOldSafe({ modules: oldModulesList })
|
||||
const newSafeProps: Partial<SafeRecordProps> = {
|
||||
modules: newModulesList,
|
||||
}
|
||||
const newSafeProps: Partial<SafeRecordProps> = { modules: newModulesList }
|
||||
|
||||
// When
|
||||
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
||||
|
@ -161,9 +129,7 @@ describe('shouldSafeStoreBeUpdated', () => {
|
|||
const oldSpendingLimitsList = []
|
||||
const newSpendingLimitsList = null
|
||||
const oldSafe = getMockedOldSafe({ spendingLimits: oldSpendingLimitsList })
|
||||
const newSafeProps: Partial<SafeRecordProps> = {
|
||||
modules: newSpendingLimitsList,
|
||||
}
|
||||
const newSafeProps: Partial<SafeRecordProps> = { modules: newSpendingLimitsList }
|
||||
|
||||
// When
|
||||
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 DEFAULT_SAFE_KEY = 'DEFAULT_SAFE'
|
||||
|
||||
type StoredSafes = Record<string, SafeRecordProps>
|
||||
export type StoredSafes = Record<string, SafeRecordProps>
|
||||
|
||||
export const loadStoredSafes = (): Promise<StoredSafes | undefined> => {
|
||||
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> => {
|
||||
const storedSafes = await loadStoredSafes()
|
||||
return storedSafes?.[safeAddress]
|
||||
|
|
|
@ -6,7 +6,7 @@ import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
|||
const isStateSubset = (superObj, subObj) => {
|
||||
return Object.keys(subObj).every((key) => {
|
||||
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
|
||||
return isEqual(superObj[key], subObj[key])
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import {
|
|||
shortVersionOf,
|
||||
} from 'src/logic/wallets/ethAddresses'
|
||||
import makeSafe from 'src/logic/safe/store/models/safe'
|
||||
import { makeOwner } from 'src/logic/safe/store/models/owner'
|
||||
|
||||
describe('src/logic/wallets/ethAddresses', () => {
|
||||
describe('Utility function: sameAddress', () => {
|
||||
|
@ -113,7 +112,7 @@ describe('src/logic/wallets/ethAddresses', () => {
|
|||
it("Should return false if there's no `userAccount`", () => {
|
||||
// given
|
||||
const userAddress = null
|
||||
const owners = List([makeOwner({ address: userAddress })])
|
||||
const owners = [userAddress]
|
||||
const safeInstance = makeSafe({ owners })
|
||||
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", () => {
|
||||
// given
|
||||
const userAddress = 'address1'
|
||||
const owners = List([makeOwner({ address: userAddress })])
|
||||
const owners = [userAddress]
|
||||
const safeInstance = makeSafe({ owners })
|
||||
const expectedResult = true
|
||||
|
||||
|
@ -153,7 +152,7 @@ describe('src/logic/wallets/ethAddresses', () => {
|
|||
// given
|
||||
const userAddress = 'address1'
|
||||
const userAddress2 = 'address2'
|
||||
const owners = List([makeOwner({ address: userAddress })])
|
||||
const owners = [userAddress]
|
||||
const safeInstance = makeSafe({ owners })
|
||||
const expectedResult = false
|
||||
|
||||
|
@ -170,8 +169,8 @@ describe('src/logic/wallets/ethAddresses', () => {
|
|||
// given
|
||||
const userAddress = 'address1'
|
||||
const userAddress2 = 'address2'
|
||||
const owners1 = List([makeOwner({ address: userAddress })])
|
||||
const owners2 = List([makeOwner({ address: userAddress2 })])
|
||||
const owners1 = [userAddress]
|
||||
const owners2 = [userAddress2]
|
||||
const safeInstance = makeSafe({ owners: owners1 })
|
||||
const safeInstance2 = makeSafe({ owners: owners2 })
|
||||
const safesList = List([safeInstance, safeInstance2])
|
||||
|
@ -188,8 +187,8 @@ describe('src/logic/wallets/ethAddresses', () => {
|
|||
const userAddress = 'address1'
|
||||
const userAddress2 = 'address2'
|
||||
const userAddress3 = 'address3'
|
||||
const owners1 = List([makeOwner({ address: userAddress3 })])
|
||||
const owners2 = List([makeOwner({ address: userAddress2 })])
|
||||
const owners1 = [userAddress3]
|
||||
const owners2 = [userAddress2]
|
||||
const safeInstance = makeSafe({ owners: owners1 })
|
||||
const safeInstance2 = makeSafe({ owners: owners2 })
|
||||
const safesList = List([safeInstance, safeInstance2])
|
||||
|
|
|
@ -40,7 +40,7 @@ export const isUserAnOwner = (safe: SafeRecord, userAccount: string): boolean =>
|
|||
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 =>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import InputAdornment from '@material-ui/core/InputAdornment'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import CheckCircle from '@material-ui/icons/CheckCircle'
|
||||
import * as React from 'react'
|
||||
import React, { ReactElement, ReactNode } from 'react'
|
||||
import { FormApi } from 'final-form'
|
||||
|
||||
import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
|
||||
|
@ -15,7 +15,7 @@ import {
|
|||
noErrorsOn,
|
||||
required,
|
||||
composeValidators,
|
||||
minMaxLength,
|
||||
validAddressBookName,
|
||||
} from 'src/components/forms/validator'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Col from 'src/components/layout/Col'
|
||||
|
@ -68,7 +68,7 @@ interface DetailsFormProps {
|
|||
form: FormApi
|
||||
}
|
||||
|
||||
const DetailsForm = ({ errors, form }: DetailsFormProps): React.ReactElement => {
|
||||
const DetailsForm = ({ errors, form }: DetailsFormProps): ReactElement => {
|
||||
const classes = useStyles()
|
||||
|
||||
const handleScan = (value: string, closeQrModal: () => void): void => {
|
||||
|
@ -92,10 +92,10 @@ const DetailsForm = ({ errors, form }: DetailsFormProps): React.ReactElement =>
|
|||
<Field
|
||||
component={TextField}
|
||||
name={FIELD_LOAD_NAME}
|
||||
placeholder="Name of the Safe"
|
||||
placeholder="Name of the Safe*"
|
||||
text="Safe name"
|
||||
type="text"
|
||||
validate={composeValidators(required, minMaxLength(1, 50))}
|
||||
validate={composeValidators(required, validAddressBookName)}
|
||||
testId="load-safe-name-field"
|
||||
/>
|
||||
</Col>
|
||||
|
@ -145,13 +145,11 @@ const DetailsForm = ({ errors, form }: DetailsFormProps): React.ReactElement =>
|
|||
}
|
||||
|
||||
const DetailsPage = () =>
|
||||
function LoadSafeDetails(controls: React.ReactNode, { errors, form }: StepperPageFormProps): React.ReactElement {
|
||||
function LoadSafeDetails(controls: ReactNode, { errors, form }: StepperPageFormProps): ReactElement {
|
||||
return (
|
||||
<>
|
||||
<OpenPaper controls={controls}>
|
||||
<DetailsForm errors={errors} form={form} />
|
||||
</OpenPaper>
|
||||
</>
|
||||
<OpenPaper controls={controls}>
|
||||
<DetailsForm errors={errors} form={form} />
|
||||
</OpenPaper>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import IconButton from '@material-ui/core/IconButton'
|
||||
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 Block from 'src/components/layout/Block'
|
||||
|
@ -34,13 +34,12 @@ const formMutators = {
|
|||
}
|
||||
|
||||
interface LayoutProps {
|
||||
network: string
|
||||
provider?: string
|
||||
userAddress: string
|
||||
onLoadSafeSubmit: (values: LoadFormValues) => void
|
||||
}
|
||||
|
||||
const Layout = ({ network, onLoadSafeSubmit, provider, userAddress }: LayoutProps): React.ReactElement => (
|
||||
const Layout = ({ onLoadSafeSubmit, provider, userAddress }: LayoutProps): ReactElement => (
|
||||
<>
|
||||
{provider ? (
|
||||
<Block>
|
||||
|
@ -58,8 +57,8 @@ const Layout = ({ network, onLoadSafeSubmit, provider, userAddress }: LayoutProp
|
|||
testId="load-safe-form"
|
||||
>
|
||||
<StepperPage validate={safeFieldsValidation} component={DetailsForm} />
|
||||
<StepperPage network={network} component={OwnerList} />
|
||||
<StepperPage network={network} userAddress={userAddress} component={ReviewInformation} />
|
||||
<StepperPage component={OwnerList} />
|
||||
<StepperPage userAddress={userAddress} component={ReviewInformation} />
|
||||
</Stepper>
|
||||
</Block>
|
||||
) : (
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
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 { getExplorerInfo } from 'src/config'
|
||||
import Field from 'src/components/forms/Field'
|
||||
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 Col from 'src/components/layout/Col'
|
||||
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 { getOwnerAddressBy, getOwnerNameBy } from 'src/routes/open/components/fields'
|
||||
import { styles } from './styles'
|
||||
import { getExplorerInfo } from 'src/config'
|
||||
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
||||
import { LoadFormValues } from 'src/routes/load/container/Load'
|
||||
|
||||
const calculateSafeValues = (owners, threshold, values) => {
|
||||
const initialValues = { ...values }
|
||||
|
@ -41,10 +41,14 @@ const useAddressBookForOwnersNames = (ownersList: string[]): AddressBookEntry[]
|
|||
|
||||
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 classes = useStyles()
|
||||
const { updateInitialProps, values } = props
|
||||
|
||||
const ownersWithNames = useAddressBookForOwnersNames(owners)
|
||||
|
||||
|
@ -88,19 +92,18 @@ const OwnerListComponent = (props) => {
|
|||
<Hairline />
|
||||
<Block margin="md" padding="md">
|
||||
{ownersWithNames.map(({ address, name }, index) => {
|
||||
const ownerName = name || `Owner #${index + 1}`
|
||||
return (
|
||||
<Row className={classes.owner} key={address} data-testid="owner-row">
|
||||
<Col className={classes.ownerName} xs={4}>
|
||||
<Field
|
||||
className={classes.name}
|
||||
component={TextField}
|
||||
initialValue={ownerName}
|
||||
initialValue={name}
|
||||
name={getOwnerNameBy(index)}
|
||||
placeholder="Owner Name*"
|
||||
placeholder="Owner Name"
|
||||
text="Owner Name"
|
||||
type="text"
|
||||
validate={composeValidators(required, minMaxLength(1, 50))}
|
||||
validate={minMaxLength(0, 50)}
|
||||
testId={`load-safe-owner-name-${index}`}
|
||||
/>
|
||||
</Col>
|
||||
|
@ -118,14 +121,12 @@ const OwnerListComponent = (props) => {
|
|||
)
|
||||
}
|
||||
|
||||
const OwnerList = ({ updateInitialProps }, network) =>
|
||||
function LoadSafeOwnerList(controls, { values }): React.ReactElement {
|
||||
const OwnerList = ({ updateInitialProps }) =>
|
||||
function LoadSafeOwnerList(controls: ReactNode, { values }: { values: LoadFormValues }): ReactElement {
|
||||
return (
|
||||
<>
|
||||
<OpenPaper controls={controls} padding={false}>
|
||||
<OwnerListComponent network={network} updateInitialProps={updateInitialProps} values={values} />
|
||||
</OpenPaper>
|
||||
</>
|
||||
<OpenPaper controls={controls} padding={false}>
|
||||
<OwnerListComponent updateInitialProps={updateInitialProps} values={values} />
|
||||
</OpenPaper>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
||||
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 Col from 'src/components/layout/Col'
|
||||
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 { getAccountsFrom } from 'src/routes/open/utils/safeDataExtractor'
|
||||
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'
|
||||
|
||||
const checkIfUserAddressIsAnOwner = (values: LoadFormValues, userAddress: string): boolean => {
|
||||
|
@ -33,108 +33,104 @@ interface Props {
|
|||
values: LoadFormValues
|
||||
}
|
||||
|
||||
const ReviewComponent = ({ userAddress, values }: Props): React.ReactElement => {
|
||||
const ReviewComponent = ({ userAddress, values }: Props): ReactElement => {
|
||||
const classes = useStyles()
|
||||
const isOwner = checkIfUserAddressIsAnOwner(values, userAddress)
|
||||
const owners = getAccountsFrom(values)
|
||||
const safeAddress = values[FIELD_LOAD_ADDRESS]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row className={classes.root}>
|
||||
<Col className={classes.detailsColumn} layout="column" xs={4}>
|
||||
<Block className={classes.details}>
|
||||
<Block margin="lg">
|
||||
<Paragraph color="primary" noMargin size="lg" data-testid="load-safe-step-three">
|
||||
Review details
|
||||
</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>
|
||||
<Row className={classes.root}>
|
||||
<Col className={classes.detailsColumn} layout="column" xs={4}>
|
||||
<Block className={classes.details}>
|
||||
<Block margin="lg">
|
||||
<Paragraph color="primary" noMargin size="lg" data-testid="load-safe-step-three">
|
||||
Review details
|
||||
</Paragraph>
|
||||
</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>
|
||||
</>
|
||||
<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>
|
||||
</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 }) =>
|
||||
function ReviewPage(controls: React.ReactNode, { values }: { values: LoadFormValues }): React.ReactElement {
|
||||
function ReviewPage(controls: ReactNode, { values }: { values: LoadFormValues }): ReactElement {
|
||||
return (
|
||||
<>
|
||||
<OpenPaper controls={controls} padding={false}>
|
||||
<ReviewComponent userAddress={userAddress} values={values} />
|
||||
</OpenPaper>
|
||||
</>
|
||||
<OpenPaper controls={controls} padding={false}>
|
||||
<ReviewComponent userAddress={userAddress} values={values} />
|
||||
</OpenPaper>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,34 +1,25 @@
|
|||
import { List } from 'immutable'
|
||||
import * as React from 'react'
|
||||
import React, { ReactElement } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
|
||||
|
||||
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 { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||
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 { buildSafe } from 'src/logic/safe/store/actions/fetchSafe'
|
||||
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 { 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'
|
||||
|
||||
export const loadSafe = async (
|
||||
safeName: string,
|
||||
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
|
||||
export const loadSafe = async (safeAddress: string, addSafe: (safe: SafeRecordProps) => void): Promise<void> => {
|
||||
const safeProps = await buildSafe(safeAddress)
|
||||
|
||||
const storedSafes = (await loadStoredSafes()) || {}
|
||||
|
||||
|
@ -56,10 +47,9 @@ interface LoadForm {
|
|||
|
||||
export type LoadFormValues = ReviewSafeCreationValues | LoadForm
|
||||
|
||||
const Load = (): React.ReactElement => {
|
||||
const Load = (): ReactElement => {
|
||||
const dispatch = useDispatch()
|
||||
const provider = useSelector(providerNameSelector)
|
||||
const network = useSelector(networkSelector)
|
||||
const userAddress = useSelector(userAccountSelector)
|
||||
|
||||
const addSafeHandler = async (safe: SafeRecordProps) => {
|
||||
|
@ -67,22 +57,32 @@ const Load = (): React.ReactElement => {
|
|||
}
|
||||
const onLoadSafeSubmit = async (values: LoadFormValues) => {
|
||||
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))
|
||||
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 {
|
||||
const safeName = values[FIELD_LOAD_NAME]
|
||||
safeAddress = checksumAddress(safeAddress)
|
||||
const ownerNames = getNamesFrom(values)
|
||||
|
||||
const gnosisSafe = getGnosisSafeInstanceAt(safeAddress)
|
||||
const ownerAddresses = await gnosisSafe.methods.getOwners().call()
|
||||
const owners = getOwnersFrom(ownerNames, ownerAddresses.slice().sort())
|
||||
|
||||
await loadSafe(safeName, safeAddress, owners, addSafeHandler)
|
||||
await loadSafe(safeAddress, addSafeHandler)
|
||||
|
||||
const url = `${SAFELIST_ADDRESS}/${safeAddress}/balances`
|
||||
history.push(url)
|
||||
|
@ -93,12 +93,7 @@ const Load = (): React.ReactElement => {
|
|||
|
||||
return (
|
||||
<Page>
|
||||
<Layout
|
||||
onLoadSafeSubmit={onLoadSafeSubmit}
|
||||
network={ETHEREUM_NETWORK[network]}
|
||||
userAddress={userAddress}
|
||||
provider={provider}
|
||||
/>
|
||||
<Layout onLoadSafeSubmit={onLoadSafeSubmit} userAddress={userAddress} provider={provider} />
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import styled from 'styled-components'
|
|||
import OpenPaper from 'src/components/Stepper/OpenPaper'
|
||||
import Field from 'src/components/forms/Field'
|
||||
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 Paragraph from 'src/components/layout/Paragraph'
|
||||
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"
|
||||
text="Safe name"
|
||||
type="text"
|
||||
validate={composeValidators(required, minMaxLength(1, 50))}
|
||||
validate={composeValidators(required, validAddressBookName)}
|
||||
testId="create-safe-name-field"
|
||||
/>
|
||||
</Block>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Loader } from '@gnosis.pm/safe-react-components'
|
||||
import { backOff } from 'exponential-backoff'
|
||||
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 { useLocation } from 'react-router-dom'
|
||||
import { PromiEvent, TransactionReceipt } from 'web3-core'
|
||||
|
@ -16,7 +16,6 @@ import {
|
|||
CreateSafeValues,
|
||||
getAccountsFrom,
|
||||
getNamesFrom,
|
||||
getOwnersFrom,
|
||||
getSafeCreationSaltFrom,
|
||||
getSafeNameFrom,
|
||||
getThresholdFrom,
|
||||
|
@ -25,8 +24,9 @@ import { SAFELIST_ADDRESS, WELCOME_ADDRESS } from 'src/routes/routes'
|
|||
import { buildSafe } from 'src/logic/safe/store/actions/fetchSafe'
|
||||
import { history } from 'src/store'
|
||||
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 { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||
import { addOrUpdateSafe } from 'src/logic/safe/store/actions/addOrUpdateSafe'
|
||||
import { useAnalytics } from 'src/utils/googleAnalytics'
|
||||
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> => {
|
||||
const confirmations = getThresholdFrom(values)
|
||||
const ownerAddresses = getAccountsFrom(values)
|
||||
|
@ -118,7 +106,7 @@ export const createSafe = (values: CreateSafeValues, userAccount: string): Promi
|
|||
return promiEvent
|
||||
}
|
||||
|
||||
const Open = (): React.ReactElement => {
|
||||
const Open = (): ReactElement => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [showProgress, setShowProgress] = useState(false)
|
||||
const [creationTxPromise, setCreationTxPromise] = useState<PromiEvent<TransactionReceipt>>()
|
||||
|
@ -176,14 +164,24 @@ const Open = (): React.ReactElement => {
|
|||
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 name = pendingCreation ? getSafeNameFrom(pendingCreation) : ''
|
||||
const ownersNames = getNamesFrom(pendingCreation as CreateSafeValues)
|
||||
const ownerAddresses = pendingCreation ? getAccountsFrom(pendingCreation) : []
|
||||
const safeProps = await getSafeProps(safeAddress, name, ownersNames, ownerAddresses)
|
||||
let name = ''
|
||||
let ownersNames: string[] = []
|
||||
let ownersAddresses: string[] = []
|
||||
|
||||
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))
|
||||
|
||||
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 { getNumOwnersFrom } from 'src/routes/open/components/fields'
|
||||
|
||||
|
@ -15,29 +11,18 @@ export type CreateSafeValues = {
|
|||
owners?: number | string
|
||||
}
|
||||
|
||||
export const getAccountsFrom = (values: CreateSafeValues | LoadFormValues): string[] => {
|
||||
const getByRegexFrom = (regex: RegExp) => (values: CreateSafeValues | LoadFormValues): string[] => {
|
||||
const accounts = Object.keys(values)
|
||||
.sort()
|
||||
.filter((key) => /^owner\d+Address$/.test(key))
|
||||
.filter((key) => regex.test(key))
|
||||
|
||||
const numOwners = getNumOwnersFrom(values)
|
||||
return accounts.map((account) => values[account]).slice(0, numOwners)
|
||||
}
|
||||
|
||||
export const getNamesFrom = (values: CreateSafeValues | LoadFormValues): string[] => {
|
||||
const accounts = Object.keys(values)
|
||||
.sort()
|
||||
.filter((key) => /^owner\d+Name$/.test(key))
|
||||
export const getAccountsFrom = getByRegexFrom(/^owner\d+Address$/)
|
||||
|
||||
const numOwners = getNumOwnersFrom(values)
|
||||
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 getNamesFrom = getByRegexFrom(/^owner\d+Name$/)
|
||||
|
||||
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 { useSelector } from 'react-redux'
|
||||
|
||||
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 AddressInput from 'src/components/forms/AddressInput'
|
||||
import Field from 'src/components/forms/Field'
|
||||
import GnoForm from 'src/components/forms/GnoForm'
|
||||
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 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 { addressBookAddressesListSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
|
@ -66,81 +62,76 @@ export const CreateEditEntryModal = ({
|
|||
description={isNew ? 'Create new addressBook entry' : 'Edit addressBook entry'}
|
||||
handleClose={onClose}
|
||||
open={isOpen}
|
||||
paperClassName="smaller-modal-window"
|
||||
title={isNew ? 'Create new entry' : 'Edit entry'}
|
||||
>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.manage} noMargin weight="bolder">
|
||||
{isNew ? 'Create entry' : 'Edit entry'}
|
||||
</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.close} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<GnoForm formMutators={formMutators} onSubmit={onFormSubmitted} initialValues={initialValues}>
|
||||
{(...args) => {
|
||||
const formState = args[2]
|
||||
const mutators = args[3]
|
||||
const handleScan = (value, closeQrModal) => {
|
||||
let scannedAddress = value
|
||||
<Modal.Header onClose={onClose}>
|
||||
<Modal.Header.Title>{isNew ? 'Create entry' : 'Edit entry'}</Modal.Header.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body withoutPadding>
|
||||
<GnoForm formMutators={formMutators} onSubmit={onFormSubmitted} initialValues={initialValues}>
|
||||
{(...args) => {
|
||||
const formState = args[2]
|
||||
const mutators = args[3]
|
||||
const handleScan = (value, closeQrModal) => {
|
||||
let scannedAddress = value
|
||||
|
||||
if (scannedAddress.startsWith('ethereum:')) {
|
||||
scannedAddress = scannedAddress.replace('ethereum:', '')
|
||||
if (scannedAddress.startsWith('ethereum:')) {
|
||||
scannedAddress = scannedAddress.replace('ethereum:', '')
|
||||
}
|
||||
|
||||
mutators.setOwnerAddress(scannedAddress)
|
||||
closeQrModal()
|
||||
}
|
||||
|
||||
mutators.setOwnerAddress(scannedAddress)
|
||||
closeQrModal()
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Block className={classes.container}>
|
||||
<Row margin="md">
|
||||
<Col xs={11}>
|
||||
<Field
|
||||
component={TextField}
|
||||
name="name"
|
||||
placeholder="Name"
|
||||
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} />
|
||||
return (
|
||||
<>
|
||||
<Block className={classes.container}>
|
||||
<Row margin="md">
|
||||
<Col xs={11}>
|
||||
<Field
|
||||
component={TextField}
|
||||
name="name"
|
||||
placeholder="Name*"
|
||||
testId={CREATE_ENTRY_INPUT_NAME_ID}
|
||||
text="Name*"
|
||||
type="text"
|
||||
validate={composeValidators(required, validAddressBookName)}
|
||||
/>
|
||||
</Col>
|
||||
) : null}
|
||||
</Row>
|
||||
</Block>
|
||||
<GenericModal.Footer>
|
||||
<GenericModal.Footer.Buttons
|
||||
cancelButtonProps={{ onClick: onClose }}
|
||||
confirmButtonProps={{
|
||||
disabled: !formState.valid,
|
||||
testId: SAVE_NEW_ENTRY_BTN_ID,
|
||||
text: isNew ? 'Create' : 'Save',
|
||||
}}
|
||||
/>
|
||||
</GenericModal.Footer>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</GnoForm>
|
||||
</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>
|
||||
) : 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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -7,6 +7,9 @@ import { push } from 'connected-react-router'
|
|||
import React from 'react'
|
||||
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 { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { xs } from 'src/theme/variables'
|
||||
|
@ -35,13 +38,11 @@ const useStyles = makeStyles(
|
|||
|
||||
type EllipsisTransactionDetailsProps = {
|
||||
address: string
|
||||
knownAddress?: boolean
|
||||
sendModalOpenHandler?: () => void
|
||||
}
|
||||
|
||||
export const EllipsisTransactionDetails = ({
|
||||
address,
|
||||
knownAddress,
|
||||
sendModalOpenHandler,
|
||||
}: EllipsisTransactionDetailsProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
|
@ -51,6 +52,10 @@ export const EllipsisTransactionDetails = ({
|
|||
const currentSafeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
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 closeMenuHandler = () => setAnchorEl(null)
|
||||
|
@ -73,7 +78,7 @@ export const EllipsisTransactionDetails = ({
|
|||
<Divider key="divider" />,
|
||||
]
|
||||
: null}
|
||||
{knownAddress ? (
|
||||
{isStoredInAddressBook ? (
|
||||
<MenuItem onClick={addOrEditEntryHandler}>Edit Address book Entry</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 TableContainer from '@material-ui/core/TableContainer'
|
||||
import TableRow from '@material-ui/core/TableRow'
|
||||
|
@ -10,37 +10,32 @@ import { useDispatch, useSelector } from 'react-redux'
|
|||
|
||||
import { styles } from './style'
|
||||
|
||||
import { getExplorerInfo } from 'src/config'
|
||||
import { getExplorerInfo, getNetworkId } from 'src/config'
|
||||
import Table from 'src/components/Table'
|
||||
import { cellWidth } from 'src/components/Table/TableHead'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import ButtonLink from 'src/components/layout/ButtonLink'
|
||||
import Col from 'src/components/layout/Col'
|
||||
import Img from 'src/components/layout/Img'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { AddressBookEntry, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { addAddressBookEntry } from 'src/logic/addressBook/store/actions/addAddressBookEntry'
|
||||
import { removeAddressBookEntry } from 'src/logic/addressBook/store/actions/removeAddressBookEntry'
|
||||
import { updateAddressBookEntry } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
|
||||
import { addressBookAddOrUpdate, addressBookImport, addressBookRemove } from 'src/logic/addressBook/store/actions'
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { isUserAnOwnerOfAnySafe, sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
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 {
|
||||
AB_NAME_ID,
|
||||
AB_ADDRESS_ID,
|
||||
ADDRESS_BOOK_ROW_ID,
|
||||
EDIT_ENTRY_BUTTON,
|
||||
REMOVE_ENTRY_BUTTON,
|
||||
SEND_ENTRY_BUTTON,
|
||||
generateColumns,
|
||||
} from 'src/routes/safe/components/AddressBook/columns'
|
||||
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 { checksumAddress } from 'src/utils/checksumAddress'
|
||||
import { grantedSelector } from 'src/routes/safe/container/selector'
|
||||
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
|
||||
import ImportEntriesModal from './ImportEntriesModal'
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
&&.MuiButton-root {
|
||||
|
@ -48,10 +43,22 @@ const StyledButton = styled(Button)`
|
|||
padding: 0 12px;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
svg {
|
||||
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)
|
||||
|
||||
interface AddressBookSelectedEntry extends AddressBookEntry {
|
||||
|
@ -64,7 +71,8 @@ export type Entry = {
|
|||
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 classes = useStyles()
|
||||
|
@ -77,7 +85,9 @@ const AddressBookTable = (): ReactElement => {
|
|||
const granted = useSelector(grantedSelector)
|
||||
const [selectedEntry, setSelectedEntry] = useState<Entry>(initialEntryState)
|
||||
const [editCreateEntryModalOpen, setEditCreateEntryModalOpen] = useState(false)
|
||||
const [importEntryModalOpen, setImportEntryModalOpen] = useState(false)
|
||||
const [deleteEntryModalOpen, setDeleteEntryModalOpen] = useState(false)
|
||||
const [exportEntriesModalOpen, setExportEntriesModalOpen] = useState(false)
|
||||
const [sendFundsModalOpen, setSendFundsModalOpen] = useState(false)
|
||||
const { trackEvent } = useAnalytics()
|
||||
|
||||
|
@ -105,6 +115,7 @@ const AddressBookTable = (): ReactElement => {
|
|||
entry: {
|
||||
name: '',
|
||||
address,
|
||||
chainId,
|
||||
isNew: true,
|
||||
},
|
||||
})
|
||||
|
@ -113,44 +124,73 @@ const AddressBookTable = (): ReactElement => {
|
|||
}, [addressBook, entryAddressToEditOrCreateNew])
|
||||
|
||||
const newEntryModalHandler = (entry: AddressBookEntry) => {
|
||||
// close the modal
|
||||
setEditCreateEntryModalOpen(false)
|
||||
const checksumEntries = {
|
||||
...entry,
|
||||
address: checksumAddress(entry.address),
|
||||
}
|
||||
dispatch(addAddressBookEntry(makeAddressBookEntry(checksumEntries)))
|
||||
// update the store
|
||||
dispatch(addressBookAddOrUpdate(makeAddressBookEntry({ ...entry, address: checksumAddress(entry.address) })))
|
||||
}
|
||||
|
||||
const editEntryModalHandler = (entry: AddressBookEntry) => {
|
||||
// reset the form
|
||||
setSelectedEntry(initialEntryState)
|
||||
// close the modal
|
||||
setEditCreateEntryModalOpen(false)
|
||||
const checksumEntries = {
|
||||
...entry,
|
||||
address: checksumAddress(entry.address),
|
||||
}
|
||||
dispatch(updateAddressBookEntry(makeAddressBookEntry(checksumEntries)))
|
||||
// update the store
|
||||
dispatch(addressBookAddOrUpdate(makeAddressBookEntry({ ...entry, address: checksumAddress(entry.address) })))
|
||||
}
|
||||
|
||||
const deleteEntryModalHandler = () => {
|
||||
const entryAddress = selectedEntry?.entry ? checksumAddress(selectedEntry.entry.address) : ''
|
||||
// reset the form
|
||||
setSelectedEntry(initialEntryState)
|
||||
// close the modal
|
||||
setDeleteEntryModalOpen(false)
|
||||
dispatch(removeAddressBookEntry(entryAddress))
|
||||
// update the store
|
||||
selectedEntry?.entry && dispatch(addressBookRemove(selectedEntry.entry))
|
||||
}
|
||||
|
||||
const importEntryModalHandler = (addressList: AddressBookEntry[]) => {
|
||||
dispatch(addressBookImport(addressList))
|
||||
setImportEntryModalOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row align="center" className={classes.message}>
|
||||
<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
|
||||
onClick={() => {
|
||||
setSelectedEntry(initialEntryState)
|
||||
setEditCreateEntryModalOpen(true)
|
||||
}}
|
||||
size="lg"
|
||||
testId="manage-tokens-btn"
|
||||
color="primary"
|
||||
iconType="add"
|
||||
iconSize="sm"
|
||||
textSize="xl"
|
||||
>
|
||||
+ Create entry
|
||||
Create entry
|
||||
</ButtonLink>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -160,6 +200,7 @@ const AddressBookTable = (): ReactElement => {
|
|||
columns={columns}
|
||||
data={addressBook}
|
||||
defaultFixed
|
||||
defaultOrderBy={AB_NAME_ID}
|
||||
defaultRowsPerPage={25}
|
||||
disableLoadingOnEmptyTable
|
||||
label="Owners"
|
||||
|
@ -196,9 +237,7 @@ const AddressBookTable = (): ReactElement => {
|
|||
})}
|
||||
<TableCell component="td">
|
||||
<Row align="end" className={classes.actions}>
|
||||
<Img
|
||||
alt="Edit entry"
|
||||
className={granted ? classes.editEntryButton : classes.editEntryButtonNonOwner}
|
||||
<UnStyledButton
|
||||
onClick={() => {
|
||||
setSelectedEntry({
|
||||
entry: row,
|
||||
|
@ -206,19 +245,28 @@ const AddressBookTable = (): ReactElement => {
|
|||
})
|
||||
setEditCreateEntryModalOpen(true)
|
||||
}}
|
||||
src={RenameOwnerIcon}
|
||||
testId={EDIT_ENTRY_BUTTON}
|
||||
/>
|
||||
<Img
|
||||
alt="Remove entry"
|
||||
className={granted ? classes.removeEntryButton : classes.removeEntryButtonNonOwner}
|
||||
>
|
||||
<Icon
|
||||
size="sm"
|
||||
type="edit"
|
||||
tooltip="Edit entry"
|
||||
className={granted ? classes.editEntryButton : classes.editEntryButtonNonOwner}
|
||||
/>
|
||||
</UnStyledButton>
|
||||
<UnStyledButton
|
||||
onClick={() => {
|
||||
setSelectedEntry({ entry: row })
|
||||
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 ? (
|
||||
<StyledButton
|
||||
color="primary"
|
||||
|
@ -258,6 +306,12 @@ const AddressBookTable = (): ReactElement => {
|
|||
isOpen={deleteEntryModalOpen}
|
||||
onClose={() => setDeleteEntryModalOpen(false)}
|
||||
/>
|
||||
<ExportEntriesModal isOpen={exportEntriesModalOpen} onClose={() => setExportEntriesModalOpen(false)} />
|
||||
<ImportEntriesModal
|
||||
importEntryModalHandler={importEntryModalHandler}
|
||||
isOpen={importEntryModalOpen}
|
||||
onClose={() => setImportEntryModalOpen(false)}
|
||||
/>
|
||||
<SendModal
|
||||
activeScreenType="chooseTxType"
|
||||
isOpen={sendFundsModalOpen}
|
||||
|
|
|
@ -6,11 +6,8 @@ import { useHistory } from 'react-router-dom'
|
|||
import { useSelector } from 'react-redux'
|
||||
import { INTERFACE_MESSAGES, Transaction, RequestId, LowercaseNetworks } from '@gnosis.pm/safe-apps-sdk-v1'
|
||||
|
||||
import {
|
||||
safeEthBalanceSelector,
|
||||
safeParamAddressFromStateSelector,
|
||||
safeNameSelector,
|
||||
} from 'src/logic/safe/store/selectors'
|
||||
import { safeEthBalanceSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { useSafeName } from 'src/logic/addressBook/hooks/useSafeName'
|
||||
import { grantedSelector } from 'src/routes/safe/container/selector'
|
||||
import { getNetworkId, getNetworkName, getTxServiceUrl } from 'src/config'
|
||||
import { SAFELIST_ADDRESS } from 'src/routes/routes'
|
||||
|
@ -89,7 +86,7 @@ const AppFrame = ({ appUrl }: Props): ReactElement => {
|
|||
const granted = useSelector(grantedSelector)
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const ethBalance = useSelector(safeEthBalanceSelector)
|
||||
const safeName = useSelector(safeNameSelector)
|
||||
const safeName = useSafeName(safeAddress)
|
||||
const { trackEvent } = useAnalytics()
|
||||
const history = useHistory()
|
||||
const { consentReceived, onConsentReceipt } = useLegalConsent()
|
||||
|
|
|
@ -12,12 +12,10 @@ import {
|
|||
} from '@gnosis.pm/safe-apps-sdk-v1'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { useEffect, useCallback, MutableRefObject } from 'react'
|
||||
|
||||
import { getNetworkName, getTxServiceUrl } from 'src/config/'
|
||||
import {
|
||||
safeEthBalanceSelector,
|
||||
safeNameSelector,
|
||||
safeParamAddressFromStateSelector,
|
||||
} from 'src/logic/safe/store/selectors'
|
||||
import { useSafeName } from 'src/logic/addressBook/hooks/useSafeName'
|
||||
import { safeEthBalanceSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { TransactionParams } from '../components/AppFrame'
|
||||
import { SafeApp } from 'src/routes/safe/components/Apps/types'
|
||||
|
||||
|
@ -39,8 +37,8 @@ const useIframeMessageHandler = (
|
|||
iframeRef: MutableRefObject<HTMLIFrameElement | null>,
|
||||
): ReturnType => {
|
||||
const { enqueueSnackbar, closeSnackbar } = useSnackbar()
|
||||
const safeName = useSelector(safeNameSelector)
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const safeName = useSafeName(safeAddress)
|
||||
const ethBalance = useSelector(safeEthBalanceSelector)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
import React from 'react'
|
||||
import { useSafeAppUrl } from 'src/logic/hooks/useSafeAppUrl'
|
||||
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
import AppFrame from './components/AppFrame'
|
||||
import AppsList from './components/AppsList'
|
||||
|
||||
const Apps = (): React.ReactElement => {
|
||||
const { url } = useSafeAppUrl()
|
||||
const useQuery = () => {
|
||||
return new URLSearchParams(useLocation().search)
|
||||
}
|
||||
|
||||
if (url) {
|
||||
return <AppFrame appUrl={url} />
|
||||
const Apps = (): React.ReactElement => {
|
||||
const query = useQuery()
|
||||
const appUrl = query.get('appUrl')
|
||||
|
||||
if (appUrl) {
|
||||
return <AppFrame appUrl={appUrl} />
|
||||
} else {
|
||||
return <AppsList />
|
||||
}
|
||||
|
|
|
@ -4,11 +4,12 @@ import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
|||
import styled from 'styled-components'
|
||||
|
||||
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 Bold from 'src/components/layout/Bold'
|
||||
import { border, xs } from 'src/theme/variables'
|
||||
import Block from 'src/components/layout/Block'
|
||||
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
|
||||
const StyledBlock = styled(Block)`
|
||||
|
@ -24,7 +25,8 @@ const StyledBlock = styled(Block)`
|
|||
`
|
||||
|
||||
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 (
|
||||
<>
|
||||
|
|
|
@ -5,7 +5,7 @@ import React, { Dispatch, ReactElement, SetStateAction, useEffect, useState } fr
|
|||
import { useSelector } from 'react-redux'
|
||||
|
||||
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 { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
|
@ -18,6 +18,8 @@ import {
|
|||
} from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/style'
|
||||
import { trimSpaces } from 'src/utils/strings'
|
||||
|
||||
const chainId = getNetworkId()
|
||||
|
||||
export interface AddressBookProps {
|
||||
fieldMutator: (address: string) => void
|
||||
label?: string
|
||||
|
@ -65,8 +67,8 @@ const BaseAddressBookInput = ({
|
|||
const onChange: AutocompleteProps<AddressBookEntry, false, false, true>['onChange'] = (_, value, reason) => {
|
||||
switch (reason) {
|
||||
case 'select-option': {
|
||||
const { address, name } = value as AddressBookEntry
|
||||
updateAddressInfo({ address, name })
|
||||
const { address, name, chainId } = value as AddressBookEntry
|
||||
updateAddressInfo({ address, name, chainId })
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -99,7 +101,14 @@ const BaseAddressBookInput = ({
|
|||
break
|
||||
}
|
||||
|
||||
const newEntry = typeof validatedAddress === 'string' ? { address, name: normalizedValue } : validatedAddress
|
||||
const newEntry =
|
||||
typeof validatedAddress === 'string'
|
||||
? {
|
||||
address,
|
||||
name: normalizedValue,
|
||||
chainId,
|
||||
}
|
||||
: validatedAddress
|
||||
|
||||
updateAddressInfo(newEntry)
|
||||
break
|
||||
|
@ -114,7 +123,13 @@ const BaseAddressBookInput = ({
|
|||
}
|
||||
|
||||
const newEntry =
|
||||
typeof validatedAddress === 'string' ? { address: validatedAddress, name: '' } : validatedAddress
|
||||
typeof validatedAddress === 'string'
|
||||
? {
|
||||
address: validatedAddress,
|
||||
name: '',
|
||||
chainId,
|
||||
}
|
||||
: validatedAddress
|
||||
|
||||
updateAddressInfo(newEntry)
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
getValueFromTxInputs,
|
||||
} from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils'
|
||||
import { useEstimateTransactionGas, EstimationStatus } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { getNameFromAddressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { useEstimationStatus } from 'src/logic/hooks/useEstimationStatus'
|
||||
import { ButtonStatus, Modal } from 'src/components/Modal'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
|
@ -60,6 +61,9 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE
|
|||
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
|
||||
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
|
||||
const [manualGasLimit, setManualGasLimit] = useState<string | undefined>()
|
||||
const addressName = useSelector((state) =>
|
||||
getNameFromAddressBookSelector(state, { address: tx.contractAddress as string }),
|
||||
)
|
||||
|
||||
const [txInfo, setTxInfo] = useState<{
|
||||
txRecipient: string
|
||||
|
@ -154,7 +158,13 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE
|
|||
</Paragraph>
|
||||
</Row>
|
||||
<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 margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
|
|
|
@ -35,6 +35,7 @@ const useStyles = makeStyles(styles)
|
|||
|
||||
export type CollectibleTx = {
|
||||
recipientAddress: string
|
||||
recipientName?: string
|
||||
assetAddress: string
|
||||
assetName: string
|
||||
nftTokenId: string
|
||||
|
@ -177,6 +178,7 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement =
|
|||
<Col xs={12}>
|
||||
<EthHashInfo
|
||||
hash={tx.recipientAddress}
|
||||
name={tx.recipientName}
|
||||
showAvatar
|
||||
showCopyBtn
|
||||
explorerUrl={getExplorerInfo(tx.recipientAddress)}
|
||||
|
|
|
@ -57,6 +57,7 @@ export type SendCollectibleTxInfo = {
|
|||
assetName: string
|
||||
nftTokenId: string
|
||||
recipientAddress?: string
|
||||
recipientName?: string
|
||||
}
|
||||
|
||||
const SendCollectible = ({
|
||||
|
@ -106,7 +107,7 @@ const SendCollectible = ({
|
|||
if (!values.recipientAddress) {
|
||||
values.recipientAddress = selectedEntry?.address
|
||||
}
|
||||
|
||||
values.recipientName = selectedEntry?.name
|
||||
values.assetName = nftAssets[values.assetAddress].name
|
||||
|
||||
onNext(values)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 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 SendModal from 'src/routes/safe/components/Balances/SendModal'
|
||||
import { CurrencyDropdown } from 'src/routes/safe/components/CurrencyDropdown'
|
||||
import {
|
||||
safeFeaturesEnabledSelector,
|
||||
safeNameSelector,
|
||||
safeParamAddressFromStateSelector,
|
||||
} from 'src/logic/safe/store/selectors'
|
||||
import { useSafeName } from 'src/logic/addressBook/hooks/useSafeName'
|
||||
import { safeFeaturesEnabledSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
|
||||
import { wrapInSuspense } from 'src/utils/wrapInSuspense'
|
||||
import { useFetchTokens } from 'src/logic/safe/hooks/useFetchTokens'
|
||||
|
@ -45,13 +42,13 @@ export const COLLECTIBLES_LOCATION_REGEX = /\/balances\/collectibles$/
|
|||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const Balances = (): React.ReactElement => {
|
||||
const Balances = (): ReactElement => {
|
||||
const classes = useStyles()
|
||||
const [state, setState] = useState(INITIAL_STATE)
|
||||
|
||||
const address = useSelector(safeParamAddressFromStateSelector)
|
||||
const featuresEnabled = useSelector(safeFeaturesEnabledSelector)
|
||||
const safeName = useSelector(safeNameSelector) ?? ''
|
||||
const safeName = useSafeName(address)
|
||||
|
||||
useFetchTokens(address)
|
||||
|
||||
|
|
|
@ -2,10 +2,9 @@ import React, { useEffect, useState } from 'react'
|
|||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
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 { 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 { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||
|
@ -46,7 +45,7 @@ export const sendAddOwner = async (
|
|||
)
|
||||
|
||||
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 {
|
||||
await sendAddOwner(values, safeAddress, txParameters, dispatch)
|
||||
dispatch(
|
||||
addOrUpdateAddressBookEntry(makeAddressBookEntry({ name: values.ownerName, address: values.ownerAddress })),
|
||||
)
|
||||
dispatch(addressBookAddOrUpdate(makeAddressBookEntry({ name: values.ownerName, address: values.ownerAddress })))
|
||||
} catch (error) {
|
||||
console.error('Error while removing an owner', error)
|
||||
}
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import IconButton from '@material-ui/core/IconButton'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import { Mutator } from 'final-form'
|
||||
import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { OnChange } from 'react-final-form-listeners'
|
||||
|
||||
import { styles } from './style'
|
||||
|
||||
import { getNetworkId } from 'src/config'
|
||||
import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
|
||||
import AddressInput from 'src/components/forms/AddressInput'
|
||||
import Field from 'src/components/forms/Field'
|
||||
|
@ -14,16 +17,18 @@ import TextField from 'src/components/forms/TextField'
|
|||
import {
|
||||
addressIsNotCurrentSafe,
|
||||
composeValidators,
|
||||
minMaxLength,
|
||||
required,
|
||||
uniqueAddress,
|
||||
validAddressBookName,
|
||||
} from 'src/components/forms/validator'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Col from 'src/components/layout/Col'
|
||||
import Hairline from 'src/components/layout/Hairline'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
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 { 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_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) => {
|
||||
utils.changeValue(state, 'ownerAddress', () => args[0])
|
||||
},
|
||||
setOwnerName: (args, state, utils) => {
|
||||
utils.changeValue(state, 'ownerName', () => args[0])
|
||||
},
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const chainId = getNetworkId()
|
||||
|
||||
type OwnerFormProps = {
|
||||
onClose: () => void
|
||||
onSubmit: (values) => void
|
||||
|
@ -51,7 +64,8 @@ export const OwnerForm = ({ onClose, onSubmit, initialValues }: OwnerFormProps):
|
|||
const handleSubmit = (values) => {
|
||||
onSubmit(values)
|
||||
}
|
||||
const owners = useSelector(safeOwnersAddressesListSelector)
|
||||
const addressBookMap = useSelector(addressBookMapSelector)
|
||||
const owners = useSelector(safeOwnersSelector)
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const ownerDoesntExist = uniqueAddress(owners)
|
||||
const ownerAddressIsNotSafeAddress = addressIsNotCurrentSafe(safeAddress)
|
||||
|
@ -104,8 +118,18 @@ export const OwnerForm = ({ onClose, onSubmit, initialValues }: OwnerFormProps):
|
|||
testId={ADD_OWNER_NAME_INPUT_TEST_ID}
|
||||
text="Owner name*"
|
||||
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>
|
||||
</Row>
|
||||
<Row margin="md">
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import IconButton from '@material-ui/core/IconButton'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
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 { 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 Col from 'src/components/layout/Col'
|
||||
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 { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||
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 { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
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 chainId = getNetworkId()
|
||||
|
||||
type ReviewAddOwnerProps = {
|
||||
onClickBack: () => void
|
||||
onClose: () => void
|
||||
|
@ -35,12 +41,12 @@ type ReviewAddOwnerProps = {
|
|||
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 [data, setData] = useState('')
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector) as string
|
||||
const safeName = useSelector(safeNameSelector)
|
||||
const owners = useSelector(safeOwnersSelector)
|
||||
const safeName = useSafeName(safeAddress)
|
||||
const owners = useSelector((state) => safeOwnersWithAddressBookDataSelector(state, chainId))
|
||||
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
|
||||
const [manualGasPrice, setManualGasPrice] = 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:
|
||||
</Paragraph>
|
||||
<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>
|
||||
</Block>
|
||||
</Block>
|
||||
|
@ -156,7 +162,7 @@ export const ReviewAddOwner = ({ onClickBack, onClose, onSubmit, values }: Revie
|
|||
<Col className={classes.owners} layout="column" xs={8}>
|
||||
<Row className={classes.ownersTitle}>
|
||||
<Paragraph color="primary" noMargin size="lg">
|
||||
{`${(owners?.size || 0) + 1} Safe owner(s)`}
|
||||
{`${(owners?.length || 0) + 1} Safe owner(s)`}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Hairline />
|
||||
|
|
|
@ -42,6 +42,7 @@ export const ThresholdForm = ({ onClickBack, onClose, onSubmit, initialValues }:
|
|||
const classes = useStyles()
|
||||
const threshold = useSelector(safeThresholdSelector) as number
|
||||
const owners = useSelector(safeOwnersSelector)
|
||||
const numOptions = owners ? owners.length + 1 : 0
|
||||
|
||||
const handleSubmit = (values: SubmitProps) => {
|
||||
onSubmit(values)
|
||||
|
@ -79,7 +80,7 @@ export const ThresholdForm = ({ onClickBack, onClose, onSubmit, initialValues }:
|
|||
render={(props) => (
|
||||
<>
|
||||
<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}`}>
|
||||
{index + 1}
|
||||
</MenuItem>
|
||||
|
@ -92,17 +93,12 @@ export const ThresholdForm = ({ onClickBack, onClose, onSubmit, initialValues }:
|
|||
)}
|
||||
</>
|
||||
)}
|
||||
validate={composeValidators(
|
||||
required,
|
||||
mustBeInteger,
|
||||
minValue(1),
|
||||
maxValue(owners ? owners.size + 1 : 0),
|
||||
)}
|
||||
validate={composeValidators(required, mustBeInteger, minValue(1), maxValue(numOptions))}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={10}>
|
||||
<Paragraph className={classes.ownersText} color="primary" noMargin size="lg">
|
||||
out of {owners ? owners.size + 1 : 0} owner(s)
|
||||
out of {numOptions} owner(s)
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
import IconButton from '@material-ui/core/IconButton'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import React from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { useDispatch } from 'react-redux'
|
||||
|
||||
import Field from 'src/components/forms/Field'
|
||||
import GnoForm from 'src/components/forms/GnoForm'
|
||||
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 Hairline from 'src/components/layout/Hairline'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import Modal, { Modal as GenericModal } from 'src/components/Modal'
|
||||
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 enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
|
||||
import editSafeOwner from 'src/logic/safe/store/actions/editSafeOwner'
|
||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { OwnerData } from 'src/routes/safe/components/Settings/ManageOwners/dataFetcher'
|
||||
|
||||
import { useStyles } from './style'
|
||||
import { getExplorerInfo } from 'src/config'
|
||||
|
@ -29,20 +28,17 @@ export const SAVE_OWNER_CHANGES_BTN_TEST_ID = 'save-owner-changes-btn'
|
|||
type OwnProps = {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
ownerAddress: string
|
||||
selectedOwnerName: string
|
||||
owner: OwnerData
|
||||
}
|
||||
|
||||
export const EditOwnerModal = ({ isOpen, onClose, ownerAddress, selectedOwnerName }: OwnProps): React.ReactElement => {
|
||||
export const EditOwnerModal = ({ isOpen, onClose, owner }: OwnProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const dispatch = useDispatch()
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
|
||||
const handleSubmit = ({ ownerName }: { ownerName: string }): void => {
|
||||
// Update the value only if the ownerName really changed
|
||||
if (ownerName !== selectedOwnerName) {
|
||||
dispatch(editSafeOwner({ safeAddress, ownerAddress, ownerName }))
|
||||
dispatch(addOrUpdateAddressBookEntry(makeAddressBookEntry({ address: ownerAddress, name: ownerName })))
|
||||
if (ownerName !== owner.name) {
|
||||
dispatch(addressBookAddOrUpdate(makeAddressBookEntry({ address: owner.address, name: ownerName })))
|
||||
dispatch(enqueueSnackbar(NOTIFICATIONS.OWNER_NAME_CHANGE_EXECUTED_MSG))
|
||||
}
|
||||
onClose()
|
||||
|
@ -74,22 +70,22 @@ export const EditOwnerModal = ({ isOpen, onClose, ownerAddress, selectedOwnerNam
|
|||
<Row margin="md">
|
||||
<Field
|
||||
component={TextField}
|
||||
initialValue={selectedOwnerName}
|
||||
initialValue={owner.name}
|
||||
name="ownerName"
|
||||
placeholder="Owner name*"
|
||||
testId={RENAME_OWNER_INPUT_TEST_ID}
|
||||
text="Owner name*"
|
||||
type="text"
|
||||
validate={composeValidators(required, minMaxLength(1, 50))}
|
||||
validate={composeValidators(required, validAddressBookName)}
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<Block justify="center">
|
||||
<EthHashInfo
|
||||
hash={ownerAddress}
|
||||
hash={owner.address}
|
||||
showCopyBtn
|
||||
showAvatar
|
||||
explorerUrl={getExplorerInfo(ownerAddress)}
|
||||
explorerUrl={getExplorerInfo(owner.address)}
|
||||
/>
|
||||
</Block>
|
||||
</Row>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { OwnerData } from 'src/routes/safe/components/Settings/ManageOwners/dataFetcher'
|
||||
|
||||
import { CheckOwner } from './screens/CheckOwner'
|
||||
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 { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
|
||||
import { removeSafeOwner } from 'src/logic/safe/store/actions/removeSafeOwner'
|
||||
import { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors'
|
||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
|
||||
type OwnerValues = {
|
||||
ownerAddress: string
|
||||
ownerName: string
|
||||
type OwnerValues = OwnerData & {
|
||||
threshold: string
|
||||
}
|
||||
|
||||
|
@ -27,7 +25,6 @@ export const sendRemoveOwner = async (
|
|||
ownerNameToRemove: string,
|
||||
dispatch: Dispatch,
|
||||
txParameters: TxParameters,
|
||||
threshold?: number,
|
||||
): Promise<void> => {
|
||||
const gnosisSafe = getGnosisSafeInstanceAt(safeAddress)
|
||||
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 txData = gnosisSafe.methods.removeOwner(prevAddress, ownerAddressToRemove, values.threshold).encodeABI()
|
||||
|
||||
const txHash = await dispatch(
|
||||
dispatch(
|
||||
createTransaction({
|
||||
safeAddress,
|
||||
to: safeAddress,
|
||||
|
@ -49,30 +46,19 @@ export const sendRemoveOwner = async (
|
|||
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
|
||||
}),
|
||||
)
|
||||
|
||||
if (txHash && threshold === 1) {
|
||||
dispatch(removeSafeOwner({ safeAddress, ownerAddress: ownerAddressToRemove }))
|
||||
}
|
||||
}
|
||||
|
||||
type RemoveOwnerProps = {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
ownerAddress: string
|
||||
ownerName: string
|
||||
owner: OwnerData
|
||||
}
|
||||
|
||||
export const RemoveOwnerModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
ownerAddress,
|
||||
ownerName,
|
||||
}: RemoveOwnerProps): React.ReactElement => {
|
||||
export const RemoveOwnerModal = ({ isOpen, onClose, owner }: RemoveOwnerProps): React.ReactElement => {
|
||||
const [activeScreen, setActiveScreen] = useState('checkOwner')
|
||||
const [values, setValues] = useState<OwnerValues>({ ownerAddress, ownerName, threshold: '' })
|
||||
const [values, setValues] = useState<OwnerValues>({ ...owner, threshold: '' })
|
||||
const dispatch = useDispatch()
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const threshold = useSelector(safeThresholdSelector) || 1
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
|
@ -101,7 +87,7 @@ export const RemoveOwnerModal = ({
|
|||
|
||||
const onRemoveOwner = (txParameters: TxParameters) => {
|
||||
onClose()
|
||||
sendRemoveOwner(values, safeAddress, ownerAddress, ownerName, dispatch, txParameters, threshold)
|
||||
sendRemoveOwner(values, safeAddress, owner.address, owner.name, dispatch, txParameters)
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -113,9 +99,7 @@ export const RemoveOwnerModal = ({
|
|||
title="Remove owner from Safe"
|
||||
>
|
||||
<>
|
||||
{activeScreen === 'checkOwner' && (
|
||||
<CheckOwner onClose={onClose} onSubmit={ownerSubmitted} ownerAddress={ownerAddress} ownerName={ownerName} />
|
||||
)}
|
||||
{activeScreen === 'checkOwner' && <CheckOwner onClose={onClose} onSubmit={ownerSubmitted} owner={owner} />}
|
||||
{activeScreen === 'selectThreshold' && (
|
||||
<ThresholdForm
|
||||
onClickBack={onClickBack}
|
||||
|
@ -129,8 +113,7 @@ export const RemoveOwnerModal = ({
|
|||
onClickBack={onClickBack}
|
||||
onClose={onClose}
|
||||
onSubmit={onRemoveOwner}
|
||||
ownerAddress={ownerAddress}
|
||||
ownerName={ownerName}
|
||||
owner={owner}
|
||||
threshold={Number(values.threshold)}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -7,6 +7,7 @@ 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 { OwnerData } from 'src/routes/safe/components/Settings/ManageOwners/dataFetcher'
|
||||
|
||||
import { useStyles } from './style'
|
||||
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 {
|
||||
onClose: () => void
|
||||
onSubmit: () => void
|
||||
ownerAddress: string
|
||||
ownerName: string
|
||||
owner: OwnerData
|
||||
}
|
||||
|
||||
export const CheckOwner = ({ onClose, onSubmit, ownerAddress, ownerName }: CheckOwnerProps): ReactElement => {
|
||||
export const CheckOwner = ({ onClose, onSubmit, owner }: CheckOwnerProps): ReactElement => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
|
@ -44,11 +44,11 @@ export const CheckOwner = ({ onClose, onSubmit, ownerAddress, ownerName }: Check
|
|||
<Row>
|
||||
<Col align="center" xs={12}>
|
||||
<EthHashInfo
|
||||
hash={ownerAddress}
|
||||
name={ownerName}
|
||||
hash={owner.address}
|
||||
name={owner.name}
|
||||
showCopyBtn
|
||||
showAvatar
|
||||
explorerUrl={getExplorerInfo(ownerAddress)}
|
||||
explorerUrl={getExplorerInfo(owner.address)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
@ -3,21 +3,23 @@ import Close from '@material-ui/icons/Close'
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
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 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 { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts'
|
||||
import { safeNameSelector, safeOwnersSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { getOwnersWithNameFromAddressBook } from 'src/logic/addressBook/utils'
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import {
|
||||
safeOwnersWithAddressBookDataSelector,
|
||||
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 { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
import { OwnerData } from 'src/routes/safe/components/Settings/ManageOwners/dataFetcher'
|
||||
|
||||
import { useStyles } from './style'
|
||||
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'
|
||||
|
||||
const chainId = getNetworkId()
|
||||
|
||||
type ReviewRemoveOwnerProps = {
|
||||
onClickBack: () => void
|
||||
onClose: () => void
|
||||
onSubmit: (txParameters: TxParameters) => void
|
||||
ownerAddress: string
|
||||
ownerName: string
|
||||
owner: OwnerData
|
||||
threshold?: number
|
||||
}
|
||||
|
||||
|
@ -41,17 +44,15 @@ export const ReviewRemoveOwnerModal = ({
|
|||
onClickBack,
|
||||
onClose,
|
||||
onSubmit,
|
||||
ownerAddress,
|
||||
ownerName,
|
||||
owner,
|
||||
threshold = 1,
|
||||
}: ReviewRemoveOwnerProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const [data, setData] = useState('')
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const safeName = useSelector(safeNameSelector)
|
||||
const owners = useSelector(safeOwnersSelector)
|
||||
const addressBook = useSelector(addressBookSelector)
|
||||
const ownersWithAddressBookName = owners ? getOwnersWithNameFromAddressBook(addressBook, owners) : List([])
|
||||
const safeName = useSafeName(safeAddress)
|
||||
const owners = useSelector((state) => safeOwnersWithAddressBookDataSelector(state, chainId))
|
||||
const numOptions = owners ? owners.length - 1 : 0
|
||||
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
|
||||
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
|
||||
const [manualGasLimit, setManualGasLimit] = useState<string | undefined>()
|
||||
|
@ -85,11 +86,13 @@ export const ReviewRemoveOwnerModal = ({
|
|||
|
||||
const calculateRemoveOwnerData = async () => {
|
||||
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 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 txData = gnosisSafe.methods.removeOwner(prevAddress, ownerAddress, threshold).encodeABI()
|
||||
const txData = gnosisSafe.methods.removeOwner(prevAddress, owner.address, threshold).encodeABI()
|
||||
|
||||
if (isCurrent) {
|
||||
setData(txData)
|
||||
|
@ -103,7 +106,7 @@ export const ReviewRemoveOwnerModal = ({
|
|||
return () => {
|
||||
isCurrent = false
|
||||
}
|
||||
}, [safeAddress, ownerAddress, threshold])
|
||||
}, [safeAddress, owner.address, threshold])
|
||||
|
||||
const closeEditModalCallback = (txParameters: TxParameters) => {
|
||||
const oldGasPrice = Number(gasPriceFormatted)
|
||||
|
@ -168,7 +171,7 @@ export const ReviewRemoveOwnerModal = ({
|
|||
Any transaction requires the confirmation of:
|
||||
</Paragraph>
|
||||
<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>
|
||||
</Block>
|
||||
</Block>
|
||||
|
@ -177,22 +180,22 @@ export const ReviewRemoveOwnerModal = ({
|
|||
<Col className={classes.owners} layout="column" xs={8}>
|
||||
<Row className={classes.ownersTitle}>
|
||||
<Paragraph color="primary" noMargin size="lg">
|
||||
{`${owners ? owners.size - 1 : 0} Safe owner(s)`}
|
||||
{`${numOptions} Safe owner(s)`}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Hairline />
|
||||
{ownersWithAddressBookName?.map(
|
||||
(owner) =>
|
||||
owner.address !== ownerAddress && (
|
||||
<React.Fragment key={owner.address}>
|
||||
{owners?.map(
|
||||
(safeOwner) =>
|
||||
!sameAddress(safeOwner.address, owner.address) && (
|
||||
<React.Fragment key={safeOwner.address}>
|
||||
<Row className={classes.owner}>
|
||||
<Col align="center" xs={12}>
|
||||
<EthHashInfo
|
||||
hash={owner.address}
|
||||
name={owner.name}
|
||||
hash={safeOwner.address}
|
||||
name={safeOwner.name}
|
||||
showCopyBtn
|
||||
showAvatar
|
||||
explorerUrl={getExplorerInfo(owner.address)}
|
||||
explorerUrl={getExplorerInfo(safeOwner.address)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -209,11 +212,11 @@ export const ReviewRemoveOwnerModal = ({
|
|||
<Row className={classes.selectedOwner}>
|
||||
<Col align="center" xs={12}>
|
||||
<EthHashInfo
|
||||
hash={ownerAddress}
|
||||
name={ownerName}
|
||||
hash={owner.address}
|
||||
name={owner.name}
|
||||
showCopyBtn
|
||||
showAvatar
|
||||
explorerUrl={getExplorerInfo(ownerAddress)}
|
||||
explorerUrl={getExplorerInfo(owner.address)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
@ -35,6 +35,7 @@ type Props = {
|
|||
export const ThresholdForm = ({ onClickBack, onClose, onSubmit, initialValues }: Props): ReactElement => {
|
||||
const classes = useStyles()
|
||||
const owners = useSelector(safeOwnersSelector)
|
||||
const ownersCount = owners?.length ?? 0
|
||||
const threshold = useSelector(safeThresholdSelector) as number
|
||||
const handleSubmit = (values) => {
|
||||
onSubmit(values)
|
||||
|
@ -58,7 +59,7 @@ export const ThresholdForm = ({ onClickBack, onClose, onSubmit, initialValues }:
|
|||
onSubmit={handleSubmit}
|
||||
>
|
||||
{() => {
|
||||
const numOptions = owners && owners.size > 1 ? owners.size - 1 : 1
|
||||
const numOptions = ownersCount > 1 ? ownersCount - 1 : 1
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -97,7 +98,7 @@ export const ThresholdForm = ({ onClickBack, onClose, onSubmit, initialValues }:
|
|||
</Col>
|
||||
<Col xs={10}>
|
||||
<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>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
@ -2,12 +2,11 @@ import React, { useEffect, useState } from 'react'
|
|||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
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 { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
|
||||
import { replaceSafeOwner } from 'src/logic/safe/store/actions/replaceSafeOwner'
|
||||
import { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors'
|
||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
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 { ReviewReplaceOwnerModal } from 'src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review'
|
||||
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 = {
|
||||
newOwnerAddress: string
|
||||
newOwnerName: string
|
||||
address: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export const sendReplaceOwner = async (
|
||||
values: OwnerValues,
|
||||
newOwner: OwnerValues,
|
||||
safeAddress: string,
|
||||
ownerAddressToRemove: string,
|
||||
dispatch: Dispatch,
|
||||
txParameters: TxParameters,
|
||||
threshold?: number,
|
||||
): Promise<void> => {
|
||||
const gnosisSafe = getGnosisSafeInstanceAt(safeAddress)
|
||||
const safeOwners = await gnosisSafe.methods.getOwners().call()
|
||||
const index = safeOwners.findIndex((ownerAddress) => sameAddress(ownerAddress, ownerAddressToRemove))
|
||||
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(
|
||||
createTransaction({
|
||||
|
@ -49,47 +49,28 @@ export const sendReplaceOwner = async (
|
|||
}),
|
||||
)
|
||||
|
||||
if (txHash && threshold === 1) {
|
||||
dispatch(
|
||||
replaceSafeOwner({
|
||||
safeAddress,
|
||||
oldOwnerAddress: ownerAddressToRemove,
|
||||
ownerAddress: values.newOwnerAddress,
|
||||
ownerName: values.newOwnerName,
|
||||
}),
|
||||
)
|
||||
if (txHash) {
|
||||
// update the AB
|
||||
dispatch(addressBookAddOrUpdate(makeAddressBookEntry(newOwner)))
|
||||
}
|
||||
}
|
||||
|
||||
type ReplaceOwnerProps = {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
ownerAddress: string
|
||||
ownerName: string
|
||||
owner: OwnerData
|
||||
}
|
||||
|
||||
export const ReplaceOwnerModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
ownerAddress,
|
||||
ownerName,
|
||||
}: ReplaceOwnerProps): React.ReactElement => {
|
||||
export const ReplaceOwnerModal = ({ isOpen, onClose, owner }: ReplaceOwnerProps): React.ReactElement => {
|
||||
const [activeScreen, setActiveScreen] = useState('checkOwner')
|
||||
const [values, setValues] = useState({
|
||||
newOwnerAddress: '',
|
||||
newOwnerName: '',
|
||||
})
|
||||
const [newOwner, setNewOwner] = useState({ address: '', name: '' })
|
||||
const dispatch = useDispatch()
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const threshold = useSelector(safeThresholdSelector) || 1
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
setActiveScreen('checkOwner')
|
||||
setValues({
|
||||
newOwnerAddress: '',
|
||||
newOwnerName: '',
|
||||
})
|
||||
setNewOwner({ address: '', name: '' })
|
||||
},
|
||||
[isOpen],
|
||||
)
|
||||
|
@ -98,24 +79,19 @@ export const ReplaceOwnerModal = ({
|
|||
|
||||
const ownerSubmitted = (newValues) => {
|
||||
const { ownerAddress, ownerName } = newValues
|
||||
const checksumAddr = checksumAddress(ownerAddress)
|
||||
setValues({
|
||||
newOwnerAddress: checksumAddr,
|
||||
newOwnerName: ownerName,
|
||||
})
|
||||
setActiveScreen('reviewReplaceOwner')
|
||||
|
||||
if (isValidAddress(ownerAddress)) {
|
||||
const checksumAddr = checksumAddress(ownerAddress)
|
||||
setNewOwner({ address: checksumAddr, name: ownerName })
|
||||
setActiveScreen('reviewReplaceOwner')
|
||||
}
|
||||
}
|
||||
|
||||
const onReplaceOwner = async (txParameters: TxParameters) => {
|
||||
onClose()
|
||||
try {
|
||||
await sendReplaceOwner(values, safeAddress, ownerAddress, dispatch, txParameters, threshold)
|
||||
|
||||
dispatch(
|
||||
addOrUpdateAddressBookEntry(
|
||||
makeAddressBookEntry({ address: values.newOwnerAddress, name: values.newOwnerName }),
|
||||
),
|
||||
)
|
||||
await sendReplaceOwner(newOwner, safeAddress, owner.address, dispatch, txParameters)
|
||||
dispatch(addressBookAddOrUpdate(makeAddressBookEntry(newOwner)))
|
||||
} catch (error) {
|
||||
console.error('Error while removing an owner', error)
|
||||
}
|
||||
|
@ -131,22 +107,15 @@ export const ReplaceOwnerModal = ({
|
|||
>
|
||||
<>
|
||||
{activeScreen === 'checkOwner' && (
|
||||
<OwnerForm
|
||||
onClose={onClose}
|
||||
onSubmit={ownerSubmitted}
|
||||
initialValues={values}
|
||||
ownerAddress={ownerAddress}
|
||||
ownerName={ownerName}
|
||||
/>
|
||||
<OwnerForm onClose={onClose} onSubmit={ownerSubmitted} initialValues={newOwner} owner={owner} />
|
||||
)}
|
||||
{activeScreen === 'reviewReplaceOwner' && (
|
||||
<ReviewReplaceOwnerModal
|
||||
onClickBack={onClickBack}
|
||||
onClose={onClose}
|
||||
onSubmit={onReplaceOwner}
|
||||
ownerAddress={ownerAddress}
|
||||
ownerName={ownerName}
|
||||
values={values}
|
||||
owner={owner}
|
||||
newOwner={newOwner}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import IconButton from '@material-ui/core/IconButton'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import { Mutator } from 'final-form'
|
||||
import React, { ReactElement } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { OnChange } from 'react-final-form-listeners'
|
||||
|
||||
import AddressInput from 'src/components/forms/AddressInput'
|
||||
import Field from 'src/components/forms/Field'
|
||||
|
@ -10,9 +12,9 @@ import TextField from 'src/components/forms/TextField'
|
|||
import {
|
||||
addressIsNotCurrentSafe,
|
||||
composeValidators,
|
||||
minMaxLength,
|
||||
required,
|
||||
uniqueAddress,
|
||||
validAddressBookName,
|
||||
} from 'src/components/forms/validator'
|
||||
import Block from 'src/components/layout/Block'
|
||||
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 { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
|
||||
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 { getExplorerInfo } from 'src/config'
|
||||
import { getExplorerInfo, getNetworkId } from 'src/config'
|
||||
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
||||
|
||||
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 '../..'
|
||||
|
||||
const formMutators = {
|
||||
const formMutators: Record<
|
||||
string,
|
||||
Mutator<{ setOwnerAddress: { address: string }; setOwnerName: { name: string } }>
|
||||
> = {
|
||||
setOwnerAddress: (args, state, utils) => {
|
||||
utils.changeValue(state, 'ownerAddress', () => args[0])
|
||||
},
|
||||
setOwnerName: (args, state, utils) => {
|
||||
utils.changeValue(state, 'ownerName', () => args[0])
|
||||
},
|
||||
}
|
||||
|
||||
const chainId = getNetworkId()
|
||||
|
||||
type NewOwnerProps = {
|
||||
ownerAddress: string
|
||||
ownerName: string
|
||||
|
@ -47,26 +60,21 @@ type NewOwnerProps = {
|
|||
type OwnerFormProps = {
|
||||
onClose: () => void
|
||||
onSubmit: (values: NewOwnerProps) => void
|
||||
ownerAddress: string
|
||||
ownerName: string
|
||||
owner: OwnerData
|
||||
initialValues?: OwnerValues
|
||||
}
|
||||
|
||||
export const OwnerForm = ({
|
||||
onClose,
|
||||
onSubmit,
|
||||
ownerAddress,
|
||||
ownerName,
|
||||
initialValues,
|
||||
}: OwnerFormProps): ReactElement => {
|
||||
export const OwnerForm = ({ onClose, onSubmit, owner, initialValues }: OwnerFormProps): ReactElement => {
|
||||
const classes = useStyles()
|
||||
|
||||
const handleSubmit = (values: NewOwnerProps) => {
|
||||
onSubmit(values)
|
||||
}
|
||||
const owners = useSelector(safeOwnersAddressesListSelector)
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const addressBookMap = useSelector(addressBookMapSelector)
|
||||
const owners = useSelector(safeOwnersSelector)
|
||||
const ownerDoesntExist = uniqueAddress(owners)
|
||||
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const ownerAddressIsNotSafeAddress = addressIsNotCurrentSafe(safeAddress)
|
||||
|
||||
return (
|
||||
|
@ -85,8 +93,8 @@ export const OwnerForm = ({
|
|||
formMutators={formMutators}
|
||||
onSubmit={handleSubmit}
|
||||
initialValues={{
|
||||
ownerName: initialValues?.newOwnerName,
|
||||
ownerAddress: initialValues?.newOwnerAddress,
|
||||
ownerName: initialValues?.name,
|
||||
ownerAddress: initialValues?.address,
|
||||
}}
|
||||
>
|
||||
{(...args) => {
|
||||
|
@ -118,11 +126,11 @@ export const OwnerForm = ({
|
|||
<Row className={classes.owner}>
|
||||
<Col align="center" xs={12}>
|
||||
<EthHashInfo
|
||||
hash={ownerAddress}
|
||||
name={ownerName}
|
||||
hash={owner.address}
|
||||
name={owner.name}
|
||||
showCopyBtn
|
||||
showAvatar
|
||||
explorerUrl={getExplorerInfo(ownerAddress)}
|
||||
explorerUrl={getExplorerInfo(owner.address)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -138,8 +146,18 @@ export const OwnerForm = ({
|
|||
testId={REPLACE_OWNER_NAME_INPUT_TEST_ID}
|
||||
text="Owner name*"
|
||||
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>
|
||||
</Row>
|
||||
<Row margin="md">
|
||||
|
|
|
@ -2,10 +2,9 @@ import IconButton from '@material-ui/core/IconButton'
|
|||
import Close from '@material-ui/icons/Close'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { List } from 'immutable'
|
||||
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 Col from 'src/components/layout/Col'
|
||||
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 { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts'
|
||||
import {
|
||||
safeNameSelector,
|
||||
safeOwnersSelector,
|
||||
safeOwnersWithAddressBookDataSelector,
|
||||
safeParamAddressFromStateSelector,
|
||||
safeThresholdSelector,
|
||||
} 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 { useSafeName } from 'src/logic/addressBook/hooks/useSafeName'
|
||||
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
||||
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
import { Modal } from 'src/components/Modal'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
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'
|
||||
|
||||
export const REPLACE_OWNER_SUBMIT_BTN_TEST_ID = 'replace-owner-submit-btn'
|
||||
|
||||
const chainId = getNetworkId()
|
||||
|
||||
type ReplaceOwnerProps = {
|
||||
onClose: () => void
|
||||
onClickBack: () => void
|
||||
onSubmit: (txParameters: TxParameters) => void
|
||||
ownerAddress: string
|
||||
ownerName: string
|
||||
values: {
|
||||
newOwnerAddress: string
|
||||
newOwnerName: string
|
||||
owner: OwnerData
|
||||
newOwner: {
|
||||
address: string
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,18 +48,15 @@ export const ReviewReplaceOwnerModal = ({
|
|||
onClickBack,
|
||||
onClose,
|
||||
onSubmit,
|
||||
ownerAddress,
|
||||
ownerName,
|
||||
values,
|
||||
owner,
|
||||
newOwner,
|
||||
}: ReplaceOwnerProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const [data, setData] = useState('')
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const safeName = useSelector(safeNameSelector)
|
||||
const owners = useSelector(safeOwnersSelector)
|
||||
const safeName = useSafeName(safeAddress)
|
||||
const owners = useSelector((state) => safeOwnersWithAddressBookDataSelector(state, chainId))
|
||||
const threshold = useSelector(safeThresholdSelector) || 1
|
||||
const addressBook = useSelector(addressBookSelector)
|
||||
const ownersWithAddressBookName = owners ? getOwnersWithNameFromAddressBook(addressBook, owners) : List([])
|
||||
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
|
||||
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
|
||||
const [manualGasLimit, setManualGasLimit] = useState<string | undefined>()
|
||||
|
@ -88,9 +85,9 @@ export const ReviewReplaceOwnerModal = ({
|
|||
const calculateReplaceOwnerData = async () => {
|
||||
const gnosisSafe = getGnosisSafeInstanceAt(safeAddress)
|
||||
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 txData = gnosisSafe.methods.swapOwner(prevAddress, ownerAddress, values.newOwnerAddress).encodeABI()
|
||||
const txData = gnosisSafe.methods.swapOwner(prevAddress, owner.address, newOwner.address).encodeABI()
|
||||
if (isCurrent) {
|
||||
setData(txData)
|
||||
}
|
||||
|
@ -100,7 +97,7 @@ export const ReviewReplaceOwnerModal = ({
|
|||
return () => {
|
||||
isCurrent = false
|
||||
}
|
||||
}, [ownerAddress, safeAddress, values.newOwnerAddress])
|
||||
}, [owner.address, safeAddress, newOwner.address])
|
||||
|
||||
const closeEditModalCallback = (txParameters: TxParameters) => {
|
||||
const oldGasPrice = Number(gasPriceFormatted)
|
||||
|
@ -164,7 +161,7 @@ export const ReviewReplaceOwnerModal = ({
|
|||
Any transaction requires the confirmation of:
|
||||
</Paragraph>
|
||||
<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>
|
||||
</Block>
|
||||
</Block>
|
||||
|
@ -172,22 +169,22 @@ export const ReviewReplaceOwnerModal = ({
|
|||
<Col className={classes.owners} layout="column" xs={8}>
|
||||
<Row className={classes.ownersTitle}>
|
||||
<Paragraph color="primary" noMargin size="lg">
|
||||
{`${owners?.size || 0} Safe owner(s)`}
|
||||
{`${owners?.length || 0} Safe owner(s)`}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Hairline />
|
||||
{ownersWithAddressBookName?.map(
|
||||
(owner) =>
|
||||
owner.address !== ownerAddress && (
|
||||
<React.Fragment key={owner.address}>
|
||||
{owners?.map(
|
||||
(safeOwner) =>
|
||||
!sameAddress(safeOwner.address, owner.address) && (
|
||||
<React.Fragment key={safeOwner.address}>
|
||||
<Row className={classes.owner}>
|
||||
<Col align="center" xs={12}>
|
||||
<EthHashInfo
|
||||
hash={owner.address}
|
||||
name={owner.name}
|
||||
hash={safeOwner.address}
|
||||
name={safeOwner.name}
|
||||
showCopyBtn
|
||||
showAvatar
|
||||
explorerUrl={getExplorerInfo(owner.address)}
|
||||
explorerUrl={getExplorerInfo(safeOwner.address)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -204,11 +201,11 @@ export const ReviewReplaceOwnerModal = ({
|
|||
<Row className={classes.selectedOwnerRemoved}>
|
||||
<Col align="center" xs={12}>
|
||||
<EthHashInfo
|
||||
hash={ownerAddress}
|
||||
name={ownerName}
|
||||
hash={owner.address}
|
||||
name={owner.name}
|
||||
showCopyBtn
|
||||
showAvatar
|
||||
explorerUrl={getExplorerInfo(ownerAddress)}
|
||||
explorerUrl={getExplorerInfo(owner.address)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -221,11 +218,11 @@ export const ReviewReplaceOwnerModal = ({
|
|||
<Row className={classes.selectedOwnerAdded}>
|
||||
<Col align="center" xs={12}>
|
||||
<EthHashInfo
|
||||
hash={values.newOwnerAddress}
|
||||
name={values.newOwnerName}
|
||||
hash={newOwner.address}
|
||||
name={newOwner.name}
|
||||
showCopyBtn
|
||||
showAvatar
|
||||
explorerUrl={getExplorerInfo(values.newOwnerAddress)}
|
||||
explorerUrl={getExplorerInfo(newOwner.address)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
import { List } from 'immutable'
|
||||
|
||||
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_ADDRESS_ID = 'address'
|
||||
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) => ({
|
||||
[OWNERS_TABLE_NAME_ID]: owner.name,
|
||||
[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 TableCell from '@material-ui/core/TableCell'
|
||||
import TableContainer from '@material-ui/core/TableContainer'
|
||||
import TableRow from '@material-ui/core/TableRow'
|
||||
import cn from 'classnames'
|
||||
import { List } from 'immutable'
|
||||
|
||||
import RemoveOwnerIcon from '../assets/icons/bin.svg'
|
||||
|
||||
|
@ -14,7 +13,7 @@ import { RemoveOwnerModal } from './RemoveOwnerModal'
|
|||
import { ReplaceOwnerModal } from './ReplaceOwnerModal'
|
||||
import RenameOwnerIcon from './assets/icons/rename-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 { getExplorerInfo } from 'src/config'
|
||||
|
@ -28,10 +27,8 @@ import Heading from 'src/components/layout/Heading'
|
|||
import Img from 'src/components/layout/Img'
|
||||
import Paragraph from 'src/components/layout/Paragraph/index'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { getOwnersWithNameFromAddressBook } from 'src/logic/addressBook/utils'
|
||||
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
|
||||
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 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'
|
||||
|
||||
type Props = {
|
||||
addressBook: AddressBookState
|
||||
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 classes = useStyles()
|
||||
|
||||
const [selectedOwnerAddress, setSelectedOwnerAddress] = useState('')
|
||||
const [selectedOwnerName, setSelectedOwnerName] = useState('')
|
||||
const [selectedOwner, setSelectedOwner] = useState<OwnerData | undefined>()
|
||||
const [modalsStatus, setModalStatus] = useState({
|
||||
showAddOwner: false,
|
||||
showRemoveOwner: false,
|
||||
|
@ -58,13 +53,14 @@ const ManageOwners = ({ addressBook, granted, owners }: Props): React.ReactEleme
|
|||
showEditOwner: false,
|
||||
})
|
||||
|
||||
const onShow = (action, row?: any) => () => {
|
||||
const onShow = (action, row?: OwnerData) => () => {
|
||||
setModalStatus((prevState) => ({
|
||||
...prevState,
|
||||
[`show${action}`]: !prevState[`show${action}`],
|
||||
}))
|
||||
setSelectedOwnerAddress(row && row.address)
|
||||
setSelectedOwnerName(row && row.name)
|
||||
if (row) {
|
||||
setSelectedOwner(row)
|
||||
}
|
||||
}
|
||||
|
||||
const onHide = (action) => () => {
|
||||
|
@ -72,8 +68,7 @@ const ManageOwners = ({ addressBook, granted, owners }: Props): React.ReactEleme
|
|||
...prevState,
|
||||
[`show${action}`]: !Boolean(prevState[`show${action}`]),
|
||||
}))
|
||||
setSelectedOwnerAddress('')
|
||||
setSelectedOwnerName('')
|
||||
setSelectedOwner(undefined)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -82,8 +77,7 @@ const ManageOwners = ({ addressBook, granted, owners }: Props): React.ReactEleme
|
|||
|
||||
const columns = generateColumns()
|
||||
const autoColumns = columns.filter((c) => !c.custom)
|
||||
const ownersWithAddressBookName = getOwnersWithNameFromAddressBook(addressBook, owners)
|
||||
const ownerData = getOwnerData(ownersWithAddressBookName)
|
||||
const ownerData = getOwnerData(owners)
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -100,11 +94,11 @@ const ManageOwners = ({ addressBook, granted, owners }: Props): React.ReactEleme
|
|||
columns={columns}
|
||||
data={ownerData}
|
||||
defaultFixed
|
||||
defaultOrderBy={OWNERS_TABLE_NAME_ID}
|
||||
defaultOrderBy={OWNERS_TABLE_ADDRESS_ID}
|
||||
disablePagination
|
||||
label="Owners"
|
||||
noBorder
|
||||
size={ownerData.size}
|
||||
size={ownerData.length}
|
||||
>
|
||||
{(sortedData) =>
|
||||
sortedData.map((row, index) => (
|
||||
|
@ -147,7 +141,7 @@ const ManageOwners = ({ addressBook, granted, owners }: Props): React.ReactEleme
|
|||
src={ReplaceOwnerIcon}
|
||||
testId={REPLACE_OWNER_BTN_TEST_ID}
|
||||
/>
|
||||
{ownerData.size > 1 && (
|
||||
{ownerData.length > 1 && (
|
||||
<Img
|
||||
alt="Remove owner"
|
||||
className={classes.removeOwnerIcon}
|
||||
|
@ -185,24 +179,21 @@ const ManageOwners = ({ addressBook, granted, owners }: Props): React.ReactEleme
|
|||
</>
|
||||
)}
|
||||
<AddOwnerModal isOpen={modalsStatus.showAddOwner} onClose={onHide('AddOwner')} />
|
||||
<RemoveOwnerModal
|
||||
isOpen={modalsStatus.showRemoveOwner}
|
||||
onClose={onHide('RemoveOwner')}
|
||||
ownerAddress={selectedOwnerAddress}
|
||||
ownerName={selectedOwnerName}
|
||||
/>
|
||||
<ReplaceOwnerModal
|
||||
isOpen={modalsStatus.showReplaceOwner}
|
||||
onClose={onHide('ReplaceOwner')}
|
||||
ownerAddress={selectedOwnerAddress}
|
||||
ownerName={selectedOwnerName}
|
||||
/>
|
||||
<EditOwnerModal
|
||||
isOpen={modalsStatus.showEditOwner}
|
||||
onClose={onHide('EditOwner')}
|
||||
ownerAddress={selectedOwnerAddress}
|
||||
selectedOwnerName={selectedOwnerName}
|
||||
/>
|
||||
{selectedOwner && (
|
||||
<>
|
||||
<RemoveOwnerModal
|
||||
isOpen={modalsStatus.showRemoveOwner}
|
||||
onClose={onHide('RemoveOwner')}
|
||||
owner={selectedOwner}
|
||||
/>
|
||||
<ReplaceOwnerModal
|
||||
isOpen={modalsStatus.showReplaceOwner}
|
||||
onClose={onHide('ReplaceOwner')}
|
||||
owner={selectedOwner}
|
||||
/>
|
||||
<EditOwnerModal isOpen={modalsStatus.showEditOwner} onClose={onHide('EditOwner')} owner={selectedOwner} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import React from 'react'
|
||||
|
@ -5,25 +6,23 @@ import { useDispatch, useSelector } from 'react-redux'
|
|||
|
||||
import { useStyles } from './style'
|
||||
|
||||
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
||||
import Modal, { Modal as GenericModal } from 'src/components/Modal'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Hairline from 'src/components/layout/Hairline'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import {
|
||||
defaultSafeSelector,
|
||||
safeNameSelector,
|
||||
safeParamAddressFromStateSelector,
|
||||
} from 'src/logic/safe/store/selectors'
|
||||
import { addressBookMapSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { defaultSafeSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { WELCOME_ADDRESS } from 'src/routes/routes'
|
||||
import { removeLocalSafe } from 'src/logic/safe/store/actions/removeLocalSafe'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
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'
|
||||
|
||||
const chainId = getNetworkId()
|
||||
|
||||
type RemoveSafeModalProps = {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
|
@ -32,12 +31,16 @@ type RemoveSafeModalProps = {
|
|||
export const RemoveSafeModal = ({ isOpen, onClose }: RemoveSafeModalProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
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 dispatch = useDispatch()
|
||||
|
||||
const onRemoveSafeHandler = async () => {
|
||||
// ToDo: review if this is necessary or we should directly use the `removeSafe` action.
|
||||
await dispatch(removeLocalSafe(safeAddress))
|
||||
|
||||
if (sameAddress(safeAddress, defaultSafe)) {
|
||||
await saveDefaultSafe('')
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Icon, Link, Text } from '@gnosis.pm/safe-react-components'
|
||||
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 styled from 'styled-components'
|
||||
|
||||
|
@ -10,13 +10,15 @@ import Modal from 'src/components/Modal'
|
|||
import Field from 'src/components/forms/Field'
|
||||
import GnoForm from 'src/components/forms/GnoForm'
|
||||
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 Button from 'src/components/layout/Button'
|
||||
import Col from 'src/components/layout/Col'
|
||||
import Heading from 'src/components/layout/Heading'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
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 { getNotificationsFromTxType, enhanceSnackbarForAction } from 'src/logic/notifications'
|
||||
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 { updateSafe } from 'src/logic/safe/store/actions/updateSafe'
|
||||
|
||||
import { useSafeName } from 'src/logic/addressBook/hooks/useSafeName'
|
||||
import {
|
||||
latestMasterContractVersionSelector,
|
||||
safeCurrentVersionSelector,
|
||||
safeNameSelector,
|
||||
safeNeedsUpdateSelector,
|
||||
safeParamAddressFromStateSelector,
|
||||
} from 'src/logic/safe/store/selectors'
|
||||
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
|
||||
import { fetchMasterCopies, MasterCopy, MasterCopyDeployer } from 'src/logic/contracts/api/masterCopies'
|
||||
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_SUBMIT_BTN_TEST_ID = 'change-safe-name-btn'
|
||||
|
@ -52,18 +53,18 @@ const StyledIcon = styled(Icon)`
|
|||
left: 6px;
|
||||
`
|
||||
|
||||
const SafeDetails = (): React.ReactElement => {
|
||||
const SafeDetails = (): ReactElement => {
|
||||
const classes = useStyles()
|
||||
const isUserOwner = useSelector(grantedSelector)
|
||||
const latestMasterContractVersion = useSelector(latestMasterContractVersionSelector)
|
||||
const dispatch = useDispatch()
|
||||
const safeName = useSelector(safeNameSelector)
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const safeName = useSafeName(safeAddress)
|
||||
const safeNeedsUpdate = useSelector(safeNeedsUpdateSelector)
|
||||
const safeCurrentVersion = useSelector(safeCurrentVersionSelector)
|
||||
const { trackEvent } = useAnalytics()
|
||||
|
||||
const [isModalOpen, setModalOpen] = React.useState(false)
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const [isModalOpen, setModalOpen] = useState(false)
|
||||
const [safeInfo, setSafeInfo] = useState<MasterCopy | undefined>()
|
||||
|
||||
const toggleModal = () => {
|
||||
|
@ -71,10 +72,9 @@ const SafeDetails = (): React.ReactElement => {
|
|||
}
|
||||
|
||||
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(
|
||||
updateSafe({ address: safeAddress, name: values.safeName, loadedViaUrl: values.safeName === LOADED_SAFE_KEY }),
|
||||
)
|
||||
dispatch(addressBookAddOrUpdate(makeAddressBookEntry({ address: safeAddress, name: values.safeName })))
|
||||
// setting `loadedViaUrl` to `false` as setting a safe's name is considered to intentionally add the safe
|
||||
dispatch(updateSafe({ address: safeAddress, loadedViaUrl: false }))
|
||||
|
||||
const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.SAFE_NAME_CHANGE_TX)
|
||||
dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded)))
|
||||
|
@ -166,7 +166,7 @@ const SafeDetails = (): React.ReactElement => {
|
|||
testId={SAFE_NAME_INPUT_TEST_ID}
|
||||
text="Safe name*"
|
||||
type="text"
|
||||
validate={composeValidators(required, minMaxLength(1, 50))}
|
||||
validate={composeValidators(required, validAddressBookName)}
|
||||
/>
|
||||
</Block>
|
||||
</Block>
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useSelector } from 'react-redux'
|
|||
|
||||
import { getExplorerInfo } from 'src/config'
|
||||
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 DataDisplay from './DataDisplay'
|
||||
|
@ -14,14 +15,14 @@ interface AddressInfoProps {
|
|||
}
|
||||
|
||||
const AddressInfo = ({ address, title }: AddressInfoProps): ReactElement => {
|
||||
const name = useSelector((state) => getNameFromAddressBookSelector(state, address))
|
||||
const name = useSelector((state) => getNameFromAddressBookSelector(state, { address }))
|
||||
const explorerUrl = getExplorerInfo(address)
|
||||
|
||||
return (
|
||||
<DataDisplay title={title}>
|
||||
<EthHashInfo
|
||||
hash={address}
|
||||
name={sameString(name, 'UNKNOWN') ? undefined : name}
|
||||
name={sameString(name, ADDRESS_BOOK_DEFAULT_NAME) ? undefined : name}
|
||||
showCopyBtn
|
||||
showAvatar
|
||||
textSize="lg"
|
||||
|
|
|
@ -3,7 +3,6 @@ import MenuItem from '@material-ui/core/MenuItem'
|
|||
import Close from '@material-ui/icons/Close'
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { List } from 'immutable'
|
||||
|
||||
import Field from 'src/components/forms/Field'
|
||||
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 { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||
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 { ButtonStatus, Modal } from 'src/components/Modal'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
|
@ -32,14 +30,14 @@ const THRESHOLD_FIELD_NAME = 'threshold'
|
|||
|
||||
type ChangeThresholdModalProps = {
|
||||
onClose: () => void
|
||||
owners?: List<SafeOwner>
|
||||
ownersCount?: number
|
||||
safeAddress: string
|
||||
threshold?: number
|
||||
}
|
||||
|
||||
export const ChangeThresholdModal = ({
|
||||
onClose,
|
||||
owners,
|
||||
ownersCount = 0,
|
||||
safeAddress,
|
||||
threshold = 1,
|
||||
}: ChangeThresholdModalProps): ReactElement => {
|
||||
|
@ -164,7 +162,7 @@ export const ChangeThresholdModal = ({
|
|||
render={(props) => (
|
||||
<>
|
||||
<SelectField {...props} disableError>
|
||||
{[...Array(Number(owners?.size))].map((x, index) => (
|
||||
{[...Array(Number(ownersCount))].map((x, index) => (
|
||||
<MenuItem key={index} value={`${index + 1}`}>
|
||||
{index + 1}
|
||||
</MenuItem>
|
||||
|
@ -177,7 +175,7 @@ export const ChangeThresholdModal = ({
|
|||
</Col>
|
||||
<Col xs={10}>
|
||||
<Paragraph className={classes.ownersText} color="primary" noMargin size="lg">
|
||||
{`out of ${owners?.size} owner(s)`}
|
||||
{`out of ${ownersCount} owner(s)`}
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
@ -46,9 +46,9 @@ const ThresholdSettings = (): React.ReactElement => {
|
|||
<Heading tag="h2">Required confirmations</Heading>
|
||||
<Paragraph>Any transaction requires the confirmation of:</Paragraph>
|
||||
<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>
|
||||
{owners && owners.size > 1 && granted && (
|
||||
{owners && owners.length > 1 && granted && (
|
||||
<Row className={classes.buttonRow}>
|
||||
<Button
|
||||
className={classes.modifyBtn}
|
||||
|
@ -68,7 +68,12 @@ const ThresholdSettings = (): React.ReactElement => {
|
|||
open={isModalOpen}
|
||||
title="Change Required Confirmations"
|
||||
>
|
||||
<ChangeThresholdModal onClose={toggleModal} owners={owners} safeAddress={safeAddress} threshold={threshold} />
|
||||
<ChangeThresholdModal
|
||||
onClose={toggleModal}
|
||||
ownersCount={owners?.length}
|
||||
safeAddress={safeAddress}
|
||||
threshold={threshold}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -16,6 +16,7 @@ import ThresholdSettings from './ThresholdSettings'
|
|||
import RemoveSafeIcon from './assets/icons/bin.svg'
|
||||
import { styles } from './style'
|
||||
|
||||
import { getNetworkId } from 'src/config'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import ButtonLink from 'src/components/layout/ButtonLink'
|
||||
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 Row from 'src/components/layout/Row'
|
||||
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 { safeLoadedViaUrlSelector, safeNeedsUpdateSelector, safeOwnersSelector } from 'src/logic/safe/store/selectors'
|
||||
|
||||
export const OWNERS_SETTINGS_TAB_TEST_ID = 'owner-settings-tab'
|
||||
|
||||
const chainId = getNetworkId()
|
||||
|
||||
const INITIAL_STATE = {
|
||||
showRemoveSafe: false,
|
||||
menuOptionIndex: 1,
|
||||
|
@ -40,11 +46,10 @@ const useStyles = makeStyles(styles)
|
|||
const Settings: React.FC = () => {
|
||||
const classes = useStyles()
|
||||
const [state, setState] = useState(INITIAL_STATE)
|
||||
const owners = useSelector(safeOwnersSelector)
|
||||
const isSafeLoadedViaUrl = useSelector(safeLoadedViaUrlSelector)
|
||||
const owners = useSelector((state) => safeOwnersWithAddressBookDataSelector(state, chainId))
|
||||
const needsUpdate = useSelector(safeNeedsUpdateSelector)
|
||||
const granted = useSelector(grantedSelector)
|
||||
const addressBook = useSelector(addressBookSelector)
|
||||
const safe = useSelector(safeSelector)
|
||||
|
||||
const handleChange = (menuOptionIndex) => () => {
|
||||
setState((prevState) => ({ ...prevState, menuOptionIndex }))
|
||||
|
@ -67,13 +72,15 @@ const Settings: React.FC = () => {
|
|||
) : (
|
||||
<>
|
||||
<Row className={classes.message}>
|
||||
{!isSafeLoadedViaUrl && (
|
||||
<ButtonLink className={classes.removeSafeBtn} color="error" onClick={onShow('RemoveSafe')} size="lg">
|
||||
<Span className={classes.links}>Remove Safe</Span>
|
||||
<Img alt="Trash Icon" className={classes.removeSafeIcon} src={RemoveSafeIcon} />
|
||||
</ButtonLink>
|
||||
{!safe?.loadedViaUrl && (
|
||||
<>
|
||||
<ButtonLink className={classes.removeSafeBtn} color="error" onClick={onShow('RemoveSafe')} size="lg">
|
||||
<Span className={classes.links}>Remove Safe</Span>
|
||||
<Img alt="Trash Icon" className={classes.removeSafeIcon} src={RemoveSafeIcon} />
|
||||
</ButtonLink>
|
||||
<RemoveSafeModal isOpen={showRemoveSafe} onClose={onHide('RemoveSafe')} />
|
||||
</>
|
||||
)}
|
||||
<RemoveSafeModal isOpen={showRemoveSafe} onClose={onHide('RemoveSafe')} />
|
||||
</Row>
|
||||
<Block className={classes.root}>
|
||||
<Col className={classes.menuWrapper} layout="column">
|
||||
|
@ -109,7 +116,7 @@ const Settings: React.FC = () => {
|
|||
color={menuOptionIndex === 2 ? 'primary' : 'secondary'}
|
||||
/>
|
||||
<Paragraph className={classes.counter} size="xs">
|
||||
{owners.size}
|
||||
{owners.length}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Hairline className={classes.hairline} />
|
||||
|
@ -148,7 +155,7 @@ const Settings: React.FC = () => {
|
|||
<Col className={classes.contents} layout="column">
|
||||
<Block className={classes.container}>
|
||||
{menuOptionIndex === 1 && <SafeDetails />}
|
||||
{menuOptionIndex === 2 && <ManageOwners addressBook={addressBook} granted={granted} owners={owners} />}
|
||||
{menuOptionIndex === 2 && <ManageOwners granted={granted} owners={owners} />}
|
||||
{menuOptionIndex === 3 && <ThresholdSettings />}
|
||||
{menuOptionIndex === 4 && <SpendingLimitSettings />}
|
||||
{menuOptionIndex === 5 && <Advanced />}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue