Merge branch 'development' of github.com:gnosis/safe-react into development
This commit is contained in:
commit
f99a18cabc
|
@ -176,7 +176,7 @@
|
|||
"async-sema": "^3.1.0",
|
||||
"axios": "0.20.0",
|
||||
"bignumber.js": "9.0.0",
|
||||
"bnc-onboard": "1.12.0",
|
||||
"bnc-onboard": "1.13.1",
|
||||
"classnames": "^2.2.6",
|
||||
"concurrently": "^5.3.0",
|
||||
"connected-react-router": "6.8.0",
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import { Record, RecordOf } from 'immutable'
|
||||
|
||||
export interface AddressBookEntryProps {
|
||||
export type AddressBookEntry = {
|
||||
address: string
|
||||
name: string
|
||||
isOwner: boolean
|
||||
}
|
||||
|
||||
export const makeAddressBookEntry = Record<AddressBookEntryProps>({
|
||||
address: '',
|
||||
name: '',
|
||||
isOwner: false,
|
||||
export const makeAddressBookEntry = ({
|
||||
address = '',
|
||||
name = '',
|
||||
}: {
|
||||
address: string
|
||||
name?: string
|
||||
}): AddressBookEntry => ({
|
||||
address,
|
||||
name,
|
||||
})
|
||||
|
||||
export type AddressBookEntryRecord = RecordOf<AddressBookEntryProps>
|
||||
export type AddressBookState = AddressBookEntry[]
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
|
||||
export const ADD_ADDRESS_BOOK = 'ADD_ADDRESS_BOOK'
|
||||
|
||||
export const addAddressBook = createAction(ADD_ADDRESS_BOOK, (addressBook, safeAddress) => ({
|
||||
addressBook,
|
||||
safeAddress,
|
||||
}))
|
|
@ -1,7 +1,22 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
|
||||
export const ADD_ENTRY = 'ADD_ENTRY'
|
||||
|
||||
export const addAddressBookEntry = createAction(ADD_ENTRY, (entry) => ({
|
||||
entry,
|
||||
}))
|
||||
type addAddressBookEntryOptions = {
|
||||
notifyEntryUpdate: boolean
|
||||
}
|
||||
|
||||
export const addAddressBookEntry = createAction(
|
||||
ADD_ENTRY,
|
||||
(entry: AddressBookEntry, options: addAddressBookEntryOptions) => {
|
||||
let notifyEntryUpdate = true
|
||||
if (options) {
|
||||
notifyEntryUpdate = options.notifyEntryUpdate
|
||||
}
|
||||
return {
|
||||
entry,
|
||||
shouldAvoidUpdatesNotifications: !notifyEntryUpdate,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
|
||||
export const ADD_OR_UPDATE_ENTRY = 'ADD_OR_UPDATE_ENTRY'
|
||||
|
||||
export const addOrUpdateAddressBookEntry = createAction(ADD_OR_UPDATE_ENTRY, (entryAddress, entry) => ({
|
||||
entryAddress,
|
||||
export const addOrUpdateAddressBookEntry = createAction(ADD_OR_UPDATE_ENTRY, (entry: AddressBookEntry) => ({
|
||||
entry,
|
||||
}))
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
import { AddressBookState } from 'src/logic/addressBook/model/addressBook'
|
||||
|
||||
export const LOAD_ADDRESS_BOOK = 'LOAD_ADDRESS_BOOK'
|
||||
|
||||
export const loadAddressBook = createAction(LOAD_ADDRESS_BOOK, (addressBook) => ({
|
||||
export const loadAddressBook = createAction(LOAD_ADDRESS_BOOK, (addressBook: AddressBookState) => ({
|
||||
addressBook,
|
||||
}))
|
||||
|
|
|
@ -1,29 +1,17 @@
|
|||
import { List } from 'immutable'
|
||||
|
||||
import { loadAddressBook } from 'src/logic/addressBook/store/actions/loadAddressBook'
|
||||
import { buildAddressBook } from 'src/logic/addressBook/store/reducer/addressBook'
|
||||
import { getAddressBookFromStorage } from 'src/logic/addressBook/utils'
|
||||
import { safesListSelector } from 'src/logic/safe/store/selectors'
|
||||
import { Dispatch } from 'redux'
|
||||
|
||||
const loadAddressBookFromStorage = () => async (dispatch, getState) => {
|
||||
const loadAddressBookFromStorage = () => async (dispatch: Dispatch): Promise<void> => {
|
||||
try {
|
||||
const state = getState()
|
||||
let storedAdBk = await getAddressBookFromStorage()
|
||||
if (!storedAdBk) {
|
||||
storedAdBk = []
|
||||
}
|
||||
|
||||
let addressBook = buildAddressBook(storedAdBk)
|
||||
// Fetch all the current safes, in case that we don't have a safe on the adbk, we add it
|
||||
const safes = safesListSelector(state)
|
||||
const adbkEntries = addressBook.keySeq().toArray()
|
||||
safes.forEach((safe) => {
|
||||
const { address } = safe
|
||||
const found = adbkEntries.includes(address)
|
||||
if (!found) {
|
||||
addressBook = addressBook.set(address, List([]))
|
||||
}
|
||||
})
|
||||
const addressBook = buildAddressBook(storedAdBk)
|
||||
|
||||
dispatch(loadAddressBook(addressBook))
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line
|
||||
|
|
|
@ -2,6 +2,6 @@ import { createAction } from 'redux-actions'
|
|||
|
||||
export const REMOVE_ENTRY = 'REMOVE_ENTRY'
|
||||
|
||||
export const removeAddressBookEntry = createAction(REMOVE_ENTRY, (entryAddress) => ({
|
||||
export const removeAddressBookEntry = createAction(REMOVE_ENTRY, (entryAddress: string) => ({
|
||||
entryAddress,
|
||||
}))
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { updateAddressBookEntry } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
|
||||
import { saveAddressBook } from 'src/logic/addressBook/utils'
|
||||
|
||||
const saveAndUpdateAddressBook = (addressBook) => async (dispatch) => {
|
||||
try {
|
||||
dispatch(updateAddressBookEntry(makeAddressBookEntry(addressBook)))
|
||||
await saveAddressBook(addressBook)
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line
|
||||
console.error('Error while loading active tokens from storage:', err)
|
||||
}
|
||||
}
|
||||
|
||||
export default saveAndUpdateAddressBook
|
|
@ -1,7 +1,8 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
|
||||
export const UPDATE_ENTRY = 'UPDATE_ENTRY'
|
||||
|
||||
export const updateAddressBookEntry = createAction(UPDATE_ENTRY, (entry) => ({
|
||||
export const updateAddressBookEntry = createAction(UPDATE_ENTRY, (entry: AddressBookEntry) => ({
|
||||
entry,
|
||||
}))
|
||||
|
|
|
@ -2,7 +2,7 @@ import { ADD_ENTRY } from 'src/logic/addressBook/store/actions/addAddressBookEnt
|
|||
import { ADD_OR_UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
|
||||
import { REMOVE_ENTRY } from 'src/logic/addressBook/store/actions/removeAddressBookEntry'
|
||||
import { UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
|
||||
import { addressBookMapSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { saveAddressBook } from 'src/logic/addressBook/utils'
|
||||
import { enhanceSnackbarForAction, getNotificationsFromTxType } from 'src/logic/notifications'
|
||||
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
|
||||
|
@ -16,15 +16,15 @@ const addressBookMiddleware = (store) => (next) => async (action) => {
|
|||
if (watchedActions.includes(action.type)) {
|
||||
const state = store.getState()
|
||||
const { dispatch } = store
|
||||
const addressBook = addressBookMapSelector(state)
|
||||
if (addressBook) {
|
||||
const addressBook = addressBookSelector(state)
|
||||
if (addressBook.length) {
|
||||
await saveAddressBook(addressBook)
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case ADD_ENTRY: {
|
||||
const { isOwner } = action.payload.entry
|
||||
if (!isOwner) {
|
||||
const { shouldAvoidUpdatesNotifications } = action.payload
|
||||
if (!shouldAvoidUpdatesNotifications) {
|
||||
const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.ADDRESSBOOK_NEW_ENTRY)
|
||||
dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded)))
|
||||
}
|
||||
|
|
|
@ -1,134 +1,73 @@
|
|||
import { List, Map } from 'immutable'
|
||||
import { handleActions } from 'redux-actions'
|
||||
|
||||
import {
|
||||
AddressBookEntryRecord,
|
||||
AddressBookEntryProps,
|
||||
makeAddressBookEntry,
|
||||
} from 'src/logic/addressBook/model/addressBook'
|
||||
import { ADD_ADDRESS_BOOK } from 'src/logic/addressBook/store/actions/addAddressBook'
|
||||
import { AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { ADD_ENTRY } from 'src/logic/addressBook/store/actions/addAddressBookEntry'
|
||||
import { ADD_OR_UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
|
||||
import { LOAD_ADDRESS_BOOK } from 'src/logic/addressBook/store/actions/loadAddressBook'
|
||||
import { REMOVE_ENTRY } from 'src/logic/addressBook/store/actions/removeAddressBookEntry'
|
||||
import { UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
|
||||
import { getAddressesListFromSafeAddressBook } from 'src/logic/addressBook/utils'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||
import { getValidAddressBookName } from 'src/logic/addressBook/utils'
|
||||
|
||||
export const ADDRESS_BOOK_REDUCER_ID = 'addressBook'
|
||||
|
||||
export type AddressBookCollection = List<AddressBookEntryRecord>
|
||||
export type AddressBookState = Map<string, Map<string, AddressBookCollection>>
|
||||
|
||||
export const buildAddressBook = (storedAdbk: AddressBookEntryProps[]): Map<string, AddressBookCollection> => {
|
||||
let addressBookBuilt: Map<string, AddressBookCollection> = Map([])
|
||||
Object.entries(storedAdbk).forEach((adbkProps: any) => {
|
||||
const safeAddress = checksumAddress(adbkProps[0])
|
||||
const adbkRecords: AddressBookEntryRecord[] = adbkProps[1].map(makeAddressBookEntry)
|
||||
const adbkSafeEntries = List(adbkRecords)
|
||||
addressBookBuilt = addressBookBuilt.set(safeAddress, adbkSafeEntries)
|
||||
export const buildAddressBook = (storedAddressBook: AddressBookState): AddressBookState => {
|
||||
return storedAddressBook.map((addressBookEntry) => {
|
||||
const { address, name } = addressBookEntry
|
||||
return makeAddressBookEntry({ address: checksumAddress(address), name })
|
||||
})
|
||||
return addressBookBuilt
|
||||
}
|
||||
|
||||
export default handleActions(
|
||||
{
|
||||
[LOAD_ADDRESS_BOOK]: (state, action) => {
|
||||
const { addressBook } = action.payload
|
||||
return state.set('addressBook', addressBook)
|
||||
},
|
||||
[ADD_ADDRESS_BOOK]: (state, action) => {
|
||||
const { addressBook, safeAddress } = action.payload
|
||||
// Adds the address book if it does not exists
|
||||
const found = state.getIn(['addressBook', safeAddress])
|
||||
if (!found) {
|
||||
return state.setIn(['addressBook', safeAddress], addressBook)
|
||||
}
|
||||
return state
|
||||
return addressBook
|
||||
},
|
||||
[ADD_ENTRY]: (state, action) => {
|
||||
const { entry } = action.payload
|
||||
|
||||
// Adds the entry to all the safes (if it does not already exists)
|
||||
const newState = state.withMutations((map) => {
|
||||
const adbkMap = map.get('addressBook')
|
||||
const entryFound = state.find((oldEntry) => oldEntry.address === entry.address)
|
||||
|
||||
if (adbkMap) {
|
||||
adbkMap.keySeq().forEach((safeAddress) => {
|
||||
const safeAddressBook = state.getIn(['addressBook', safeAddress])
|
||||
|
||||
if (safeAddressBook) {
|
||||
const adbkAddressList = getAddressesListFromSafeAddressBook(safeAddressBook)
|
||||
const found = adbkAddressList.includes(entry.address)
|
||||
if (!found) {
|
||||
const updatedSafeAdbkList = safeAddressBook.push(entry)
|
||||
map.setIn(['addressBook', safeAddress], updatedSafeAdbkList)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
return newState
|
||||
// Only adds entries with valid names
|
||||
const validName = getValidAddressBookName(entry.name)
|
||||
if (!entryFound && validName) {
|
||||
state.push(entry)
|
||||
}
|
||||
return state
|
||||
},
|
||||
[UPDATE_ENTRY]: (state, action) => {
|
||||
const { entry } = action.payload
|
||||
|
||||
// Updates the entry from all the safes
|
||||
const newState = state.withMutations((map) => {
|
||||
map
|
||||
.get('addressBook')
|
||||
.keySeq()
|
||||
.forEach((safeAddress) => {
|
||||
const entriesList = state.getIn(['addressBook', safeAddress])
|
||||
const entryIndex = entriesList.findIndex((entryItem) => sameAddress(entryItem.address, entry.address))
|
||||
const updatedEntriesList = entriesList.set(entryIndex, entry)
|
||||
map.setIn(['addressBook', safeAddress], updatedEntriesList)
|
||||
})
|
||||
})
|
||||
|
||||
return newState
|
||||
const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entry.address)
|
||||
if (entryIndex >= 0) {
|
||||
state[entryIndex] = entry
|
||||
}
|
||||
return state
|
||||
},
|
||||
[REMOVE_ENTRY]: (state, action) => {
|
||||
const { entryAddress } = action.payload
|
||||
// Removes the entry from all the safes
|
||||
const newState = state.withMutations((map) => {
|
||||
map
|
||||
.get('addressBook')
|
||||
.keySeq()
|
||||
.forEach((safeAddress) => {
|
||||
const entriesList = state.getIn(['addressBook', safeAddress])
|
||||
const entryIndex = entriesList.findIndex((entry) => sameAddress(entry.address, entryAddress))
|
||||
const updatedEntriesList = entriesList.remove(entryIndex)
|
||||
map.setIn(['addressBook', safeAddress], updatedEntriesList)
|
||||
})
|
||||
})
|
||||
return newState
|
||||
const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entryAddress)
|
||||
state.splice(entryIndex, 1)
|
||||
return state
|
||||
},
|
||||
[ADD_OR_UPDATE_ENTRY]: (state, action) => {
|
||||
const { entry, entryAddress } = action.payload
|
||||
const { entry } = action.payload
|
||||
|
||||
// Adds or Updates the entry to all the safes
|
||||
return state.withMutations((map) => {
|
||||
const addressBook = map.get('addressBook')
|
||||
if (addressBook) {
|
||||
addressBook.keySeq().forEach((safeAddress) => {
|
||||
const safeAddressBook = state.getIn(['addressBook', safeAddress])
|
||||
const entryIndex = safeAddressBook.findIndex((entryItem) => sameAddress(entryItem.address, entryAddress))
|
||||
// Only updates entries with valid names
|
||||
const validName = getValidAddressBookName(entry.name)
|
||||
if (!validName) {
|
||||
return state
|
||||
}
|
||||
|
||||
if (entryIndex !== -1) {
|
||||
const updatedEntriesList = safeAddressBook.update(entryIndex, (currentEntry) => currentEntry.merge(entry))
|
||||
map.setIn(['addressBook', safeAddress], updatedEntriesList)
|
||||
} else {
|
||||
const updatedSafeAdbkList = safeAddressBook.push(makeAddressBookEntry(entry))
|
||||
map.setIn(['addressBook', safeAddress], updatedSafeAdbkList)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entry.address)
|
||||
|
||||
if (entryIndex >= 0) {
|
||||
state[entryIndex] = entry
|
||||
} else {
|
||||
state.push(entry)
|
||||
}
|
||||
return state
|
||||
},
|
||||
},
|
||||
Map({
|
||||
addressBook: Map({}),
|
||||
}),
|
||||
[],
|
||||
)
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
import { AddressBookEntryRecord, AddressBookEntryProps } from 'src/logic/addressBook/model/addressBook'
|
||||
import { Map, List } from 'immutable'
|
||||
|
||||
export interface AddressBookReducerState {
|
||||
addressBook: AddressBookMap
|
||||
}
|
||||
|
||||
interface AddressBookMapSerialized {
|
||||
[key: string]: AddressBookEntryProps
|
||||
}
|
||||
|
||||
interface AddressBookReducerStateSerialized extends AddressBookReducerState {
|
||||
addressBook: Record<string, AddressBookEntryProps[]>
|
||||
}
|
||||
|
||||
export interface AddressBookMap extends Map<string> {
|
||||
toJS(): AddressBookMapSerialized
|
||||
get(key: string, notSetValue: unknown): List<AddressBookEntryRecord>
|
||||
}
|
||||
|
||||
export interface AddressBookReducerMap extends Map<string, any> {
|
||||
toJS(): AddressBookReducerStateSerialized
|
||||
get<K extends keyof AddressBookReducerState>(key: K): AddressBookReducerState[K]
|
||||
}
|
|
@ -1,36 +1,22 @@
|
|||
import { AppReduxState } from 'src/store'
|
||||
import { List } from 'immutable'
|
||||
|
||||
import { createSelector } from 'reselect'
|
||||
|
||||
import { ADDRESS_BOOK_REDUCER_ID } from 'src/logic/addressBook/store/reducer/addressBook'
|
||||
import { AddressBookMap } from 'src/logic/addressBook/store/reducer/types/addressBook.d'
|
||||
import { AddressBookEntryRecord } from 'src/logic/addressBook/model/addressBook'
|
||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
|
||||
export const addressBookMapSelector = (state: AppReduxState): AddressBookMap =>
|
||||
state[ADDRESS_BOOK_REDUCER_ID].get('addressBook')
|
||||
import { AddressBookState } from 'src/logic/addressBook/model/addressBook'
|
||||
|
||||
export const getAddressBook = createSelector(
|
||||
addressBookMapSelector,
|
||||
safeParamAddressFromStateSelector,
|
||||
(addressBook, safeAddress) => {
|
||||
let result: List<AddressBookEntryRecord> = List([])
|
||||
if (addressBook && safeAddress) {
|
||||
result = addressBook.get(safeAddress, List())
|
||||
}
|
||||
return result
|
||||
},
|
||||
)
|
||||
export const addressBookSelector = (state: AppReduxState): AddressBookState => state[ADDRESS_BOOK_REDUCER_ID]
|
||||
|
||||
export const getNameFromAddressBook = createSelector(
|
||||
getAddressBook,
|
||||
export const getNameFromAddressBookSelector = createSelector(
|
||||
addressBookSelector,
|
||||
(_, address) => address,
|
||||
(addressBook, address) => {
|
||||
const adbkEntry = addressBook.find((addressBookItem) => addressBookItem.address === address)
|
||||
|
||||
if (adbkEntry) {
|
||||
return adbkEntry.name
|
||||
}
|
||||
|
||||
return 'UNKNOWN'
|
||||
},
|
||||
)
|
||||
|
|
|
@ -1,25 +1,33 @@
|
|||
import { Map, List } from 'immutable'
|
||||
import { List } from 'immutable'
|
||||
import {
|
||||
checkIfEntryWasDeletedFromAddressBook,
|
||||
getAddressBookFromStorage,
|
||||
getAddressesListFromSafeAddressBook,
|
||||
getNameFromSafeAddressBook,
|
||||
getAddressesListFromAddressBook,
|
||||
getNameFromAddressBook,
|
||||
getOwnersWithNameFromAddressBook,
|
||||
isValidAddressBookName,
|
||||
migrateOldAddressBook,
|
||||
OldAddressBookEntry,
|
||||
OldAddressBookType,
|
||||
saveAddressBook,
|
||||
} from 'src/logic/addressBook/utils/index'
|
||||
import { buildAddressBook } from 'src/logic/addressBook/store/reducer/addressBook'
|
||||
import { AddressBookEntryRecord, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
|
||||
const getMockAddressBookEntry = (
|
||||
address: string,
|
||||
name: string = 'test',
|
||||
isOwner: boolean = false,
|
||||
): AddressBookEntryRecord =>
|
||||
const getMockAddressBookEntry = (address: string, name: string = 'test'): AddressBookEntry =>
|
||||
makeAddressBookEntry({
|
||||
address,
|
||||
name,
|
||||
isOwner,
|
||||
})
|
||||
|
||||
const getMockOldAddressBookEntry = ({ address = '', name = '', isOwner = false }): OldAddressBookEntry => {
|
||||
return {
|
||||
address,
|
||||
name,
|
||||
isOwner,
|
||||
}
|
||||
}
|
||||
|
||||
describe('getAddressesListFromAdbk', () => {
|
||||
const entry1 = getMockAddressBookEntry('123456', 'test1')
|
||||
const entry2 = getMockAddressBookEntry('78910', 'test2')
|
||||
|
@ -27,11 +35,11 @@ describe('getAddressesListFromAdbk', () => {
|
|||
|
||||
it('It should returns the list of addresses within the addressBook given a safeAddressBook', () => {
|
||||
// given
|
||||
const safeAddressBook = List([entry1, entry2, entry3])
|
||||
const safeAddressBook = [entry1, entry2, entry3]
|
||||
const expectedResult = [entry1.address, entry2.address, entry3.address]
|
||||
|
||||
// when
|
||||
const result = getAddressesListFromSafeAddressBook(safeAddressBook)
|
||||
const result = getAddressesListFromAddressBook(safeAddressBook)
|
||||
|
||||
// then
|
||||
expect(result).toStrictEqual(expectedResult)
|
||||
|
@ -44,11 +52,11 @@ describe('getNameFromSafeAddressBook', () => {
|
|||
const entry3 = getMockAddressBookEntry('4781321', 'test3')
|
||||
it('It should returns the user name given a safeAddressBook and an user account', () => {
|
||||
// given
|
||||
const safeAddressBook = List([entry1, entry2, entry3])
|
||||
const safeAddressBook = [entry1, entry2, entry3]
|
||||
const expectedResult = entry2.name
|
||||
|
||||
// when
|
||||
const result = getNameFromSafeAddressBook(safeAddressBook, entry2.address)
|
||||
const result = getNameFromAddressBook(safeAddressBook, entry2.address)
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
|
@ -61,7 +69,7 @@ describe('getOwnersWithNameFromAddressBook', () => {
|
|||
const entry3 = getMockAddressBookEntry('4781321', 'test3')
|
||||
it('It should returns the list of owners with their names given a safeAddressBook and a list of owners', () => {
|
||||
// given
|
||||
const safeAddressBook = List([entry1, entry2, entry3])
|
||||
const safeAddressBook = [entry1, entry2, entry3]
|
||||
const ownerList = List([
|
||||
{ address: entry1.address, name: '' },
|
||||
{ address: entry2.address, name: '' },
|
||||
|
@ -79,24 +87,226 @@ describe('getOwnersWithNameFromAddressBook', () => {
|
|||
})
|
||||
})
|
||||
|
||||
jest.mock('src/utils/storage/index')
|
||||
describe('saveAddressBook', () => {
|
||||
const safeAddress1 = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
|
||||
const safeAddress2 = '0x344B941b1aAE2e4Be73987212FC4741687Bf0503'
|
||||
const entry1 = getMockAddressBookEntry('123456', 'test1')
|
||||
const entry2 = getMockAddressBookEntry('78910', 'test2')
|
||||
const entry3 = getMockAddressBookEntry('4781321', 'test3')
|
||||
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 = Map({ [safeAddress1]: List([entry1, entry2]), [safeAddress2]: List([entry3]) })
|
||||
const addressBook: AddressBookState = [entry1, entry2, entry3]
|
||||
|
||||
// when
|
||||
// @ts-ignore
|
||||
await saveAddressBook(addressBook)
|
||||
const storedAdBk = await getAddressBookFromStorage()
|
||||
|
||||
const storageUtils = require('src/utils/storage/index')
|
||||
const spy = storageUtils.loadFromStorage.mockImplementationOnce(() => JSON.stringify(addressBook))
|
||||
|
||||
const storedAddressBook = await getAddressBookFromStorage()
|
||||
|
||||
// @ts-ignore
|
||||
let result = buildAddressBook(storedAdBk)
|
||||
let result = buildAddressBook(storedAddressBook)
|
||||
|
||||
// then
|
||||
expect(result).toStrictEqual(addressBook)
|
||||
expect(spy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('migrateOldAddressBook', () => {
|
||||
const safeAddress1 = '0x696fd93D725d84acfFf6c62a1fe8C94E1c9E934A'
|
||||
const safeAddress2 = '0x2C7aC78b01Be0FC66AD29b684ffAb0C93B381D00'
|
||||
const mockAdd1 = '0x9163c2F4452E3399CB60AAf737231Af87548DA91'
|
||||
const mockAdd2 = '0xC4e446Da9C3D37385C86488294C6758c4e25dbD8'
|
||||
|
||||
it('It should receive an addressBook in old format and return the same addressBook in new format', () => {
|
||||
// given
|
||||
const entry1 = getMockOldAddressBookEntry({ name: 'test1', address: mockAdd1 })
|
||||
const entry2 = getMockOldAddressBookEntry({ name: 'test2', address: mockAdd2 })
|
||||
|
||||
const oldAddressBook: OldAddressBookType = {
|
||||
[safeAddress1]: [entry1],
|
||||
[safeAddress2]: [entry2],
|
||||
}
|
||||
|
||||
const expectedEntry1 = getMockAddressBookEntry(mockAdd1, 'test1')
|
||||
const expectedEntry2 = getMockAddressBookEntry(mockAdd2, 'test2')
|
||||
const expectedResult = [expectedEntry1, expectedEntry2]
|
||||
|
||||
// when
|
||||
const result = migrateOldAddressBook(oldAddressBook)
|
||||
|
||||
// then
|
||||
expect(result).toStrictEqual(expectedResult)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getAddressBookFromStorage', () => {
|
||||
const safeAddress1 = '0x696fd93D725d84acfFf6c62a1fe8C94E1c9E934A'
|
||||
const safeAddress2 = '0x2C7aC78b01Be0FC66AD29b684ffAb0C93B381D00'
|
||||
const mockAdd1 = '0x9163c2F4452E3399CB60AAf737231Af87548DA91'
|
||||
const mockAdd2 = '0xC4e446Da9C3D37385C86488294C6758c4e25dbD8'
|
||||
beforeAll(() => {
|
||||
jest.mock('src/utils/storage/index')
|
||||
})
|
||||
afterAll(() => {
|
||||
jest.unmock('src/utils/storage/index')
|
||||
})
|
||||
it('It should return null if no addressBook in storage', async () => {
|
||||
// given
|
||||
const expectedResult = null
|
||||
const storageUtils = require('src/utils/storage/index')
|
||||
const spy = storageUtils.loadFromStorage.mockImplementationOnce(() => null)
|
||||
|
||||
// when
|
||||
const result = await getAddressBookFromStorage()
|
||||
|
||||
// then
|
||||
expect(result).toStrictEqual(expectedResult)
|
||||
expect(spy).toHaveBeenCalled()
|
||||
})
|
||||
it('It should return migrated addressBook if old addressBook in storage', async () => {
|
||||
// given
|
||||
const expectedEntry1 = getMockAddressBookEntry(mockAdd1, 'test1')
|
||||
const expectedEntry2 = getMockAddressBookEntry(mockAdd2, 'test2')
|
||||
const entry1 = getMockOldAddressBookEntry({ name: 'test1', address: mockAdd1 })
|
||||
const entry2 = getMockOldAddressBookEntry({ name: 'test2', address: mockAdd2 })
|
||||
const oldAddressBook: OldAddressBookType = {
|
||||
[safeAddress1]: [entry1],
|
||||
[safeAddress2]: [entry2],
|
||||
}
|
||||
const expectedResult = [expectedEntry1, expectedEntry2]
|
||||
|
||||
const storageUtils = require('src/utils/storage/index')
|
||||
const spy = storageUtils.loadFromStorage.mockImplementationOnce(() => oldAddressBook)
|
||||
|
||||
// when
|
||||
const result = await getAddressBookFromStorage()
|
||||
|
||||
// then
|
||||
expect(result).toStrictEqual(expectedResult)
|
||||
expect(spy).toHaveBeenCalled()
|
||||
})
|
||||
it('It should return addressBook if addressBook in storage', async () => {
|
||||
// given
|
||||
const expectedEntry1 = getMockAddressBookEntry(mockAdd1, 'test1')
|
||||
const expectedEntry2 = getMockAddressBookEntry(mockAdd2, 'test2')
|
||||
|
||||
const expectedResult = [expectedEntry1, expectedEntry2]
|
||||
|
||||
const storageUtils = require('src/utils/storage/index')
|
||||
const spy = storageUtils.loadFromStorage.mockImplementationOnce(() => JSON.stringify(expectedResult))
|
||||
|
||||
// when
|
||||
const result = await getAddressBookFromStorage()
|
||||
|
||||
// then
|
||||
expect(result).toStrictEqual(expectedResult)
|
||||
expect(spy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('isValidAddressBookName', () => {
|
||||
it('It should return false if given a blacklisted name like UNKNOWN', () => {
|
||||
// given
|
||||
const addressNameInput = 'UNKNOWN'
|
||||
|
||||
const expectedResult = false
|
||||
|
||||
// when
|
||||
const result = isValidAddressBookName(addressNameInput)
|
||||
|
||||
// then
|
||||
expect(result).toStrictEqual(expectedResult)
|
||||
})
|
||||
it('It should return false if given a blacklisted name like MY WALLET', () => {
|
||||
// given
|
||||
const addressNameInput = 'MY WALLET'
|
||||
|
||||
const expectedResult = false
|
||||
|
||||
// when
|
||||
const result = isValidAddressBookName(addressNameInput)
|
||||
|
||||
// then
|
||||
expect(result).toStrictEqual(expectedResult)
|
||||
})
|
||||
it('It should return false if given a blacklisted name like OWNER #', () => {
|
||||
// given
|
||||
const addressNameInput = 'OWNER #'
|
||||
|
||||
const expectedResult = false
|
||||
|
||||
// when
|
||||
const result = isValidAddressBookName(addressNameInput)
|
||||
|
||||
// then
|
||||
expect(result).toStrictEqual(expectedResult)
|
||||
})
|
||||
it('It should return true if the given address name is valid', () => {
|
||||
// given
|
||||
const addressNameInput = 'User'
|
||||
|
||||
const expectedResult = true
|
||||
|
||||
// when
|
||||
const result = isValidAddressBookName(addressNameInput)
|
||||
|
||||
// then
|
||||
expect(result).toEqual(expectedResult)
|
||||
})
|
||||
})
|
||||
|
||||
describe('checkIfEntryWasDeletedFromAddressBook', () => {
|
||||
const mockAdd1 = '0x696fd93D725d84acfFf6c62a1fe8C94E1c9E934A'
|
||||
const mockAdd2 = '0x2C7aC78b01Be0FC66AD29b684ffAb0C93B381D00'
|
||||
const mockAdd3 = '0x537BD452c3505FC07bA242E437bD29D4E1DC9126'
|
||||
const entry1 = getMockAddressBookEntry(mockAdd1, 'test1')
|
||||
const entry2 = getMockAddressBookEntry(mockAdd2, 'test2')
|
||||
const entry3 = getMockAddressBookEntry(mockAdd3, 'test3')
|
||||
it('It should return true if a given entry was deleted from addressBook', () => {
|
||||
// given
|
||||
const addressBookEntry = entry1
|
||||
const addressBook: AddressBookState = [entry2, entry3]
|
||||
const safeAlreadyLoaded = true
|
||||
const expectedResult = true
|
||||
|
||||
// when
|
||||
const result = checkIfEntryWasDeletedFromAddressBook(addressBookEntry, addressBook, safeAlreadyLoaded)
|
||||
|
||||
// then
|
||||
expect(result).toEqual(expectedResult)
|
||||
})
|
||||
it('It should return false if a given entry was not deleted from addressBook', () => {
|
||||
// given
|
||||
const addressBookEntry = entry1
|
||||
const addressBook: AddressBookState = [entry1, entry2, entry3]
|
||||
const safeAlreadyLoaded = true
|
||||
const expectedResult = false
|
||||
|
||||
// when
|
||||
const result = checkIfEntryWasDeletedFromAddressBook(addressBookEntry, addressBook, safeAlreadyLoaded)
|
||||
|
||||
// then
|
||||
expect(result).toEqual(expectedResult)
|
||||
})
|
||||
it('It should return false if the safe was not already loaded', () => {
|
||||
// given
|
||||
const addressBookEntry = entry1
|
||||
const addressBook: AddressBookState = [entry1, entry2, entry3]
|
||||
const safeAlreadyLoaded = false
|
||||
const expectedResult = false
|
||||
|
||||
// when
|
||||
const result = checkIfEntryWasDeletedFromAddressBook(addressBookEntry, addressBook, safeAlreadyLoaded)
|
||||
|
||||
// then
|
||||
expect(result).toEqual(expectedResult)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,47 +1,140 @@
|
|||
import { List } from 'immutable'
|
||||
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
|
||||
import { AddressBookEntryRecord, AddressBookEntryProps } from '../model/addressBook'
|
||||
import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { SafeOwner } from 'src/logic/safe/store/models/safe'
|
||||
import { AddressBookCollection } from '../store/reducer/addressBook'
|
||||
import { AddressBookMap } from '../store/reducer/types/addressBook'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
|
||||
const ADDRESS_BOOK_STORAGE_KEY = 'ADDRESS_BOOK_STORAGE_KEY'
|
||||
|
||||
export const getAddressBookFromStorage = async (): Promise<Array<AddressBookEntryProps> | undefined> => {
|
||||
return await loadFromStorage<Array<AddressBookEntryProps>>(ADDRESS_BOOK_STORAGE_KEY)
|
||||
export type OldAddressBookEntry = {
|
||||
address: string
|
||||
name: string
|
||||
isOwner: boolean
|
||||
}
|
||||
|
||||
export const saveAddressBook = async (addressBook: AddressBookMap): Promise<void> => {
|
||||
export type OldAddressBookType = {
|
||||
[safeAddress: string]: [OldAddressBookEntry]
|
||||
}
|
||||
|
||||
const ADDRESSBOOK_INVALID_NAMES = ['UNKNOWN', 'OWNER #', 'MY WALLET']
|
||||
|
||||
export const migrateOldAddressBook = (oldAddressBook: OldAddressBookType): AddressBookState => {
|
||||
const values: AddressBookState = []
|
||||
const adbkValues = Object.values(oldAddressBook)
|
||||
|
||||
for (const safeIterator of adbkValues) {
|
||||
for (const safeAddressBook of safeIterator) {
|
||||
if (!values.find((entry) => sameAddress(entry.address, safeAddressBook.address))) {
|
||||
values.push(makeAddressBookEntry({ address: safeAddressBook.address, name: safeAddressBook.name }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
export const getAddressBookFromStorage = async (): Promise<AddressBookState | null> => {
|
||||
const result: OldAddressBookType | string | undefined = await loadFromStorage(ADDRESS_BOOK_STORAGE_KEY)
|
||||
|
||||
if (!result) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (typeof result === 'string') {
|
||||
return JSON.parse(result)
|
||||
}
|
||||
|
||||
return migrateOldAddressBook(result as OldAddressBookType)
|
||||
}
|
||||
|
||||
export const saveAddressBook = async (addressBook: AddressBookState): Promise<void> => {
|
||||
try {
|
||||
await saveToStorage(ADDRESS_BOOK_STORAGE_KEY, addressBook.toJS())
|
||||
await saveToStorage(ADDRESS_BOOK_STORAGE_KEY, JSON.stringify(addressBook))
|
||||
} catch (err) {
|
||||
console.error('Error storing addressBook in localstorage', err)
|
||||
}
|
||||
}
|
||||
|
||||
export const getAddressesListFromSafeAddressBook = (addressBook: AddressBookCollection): string[] =>
|
||||
Array.from(addressBook).map((entry: AddressBookEntryRecord) => entry.address)
|
||||
export const getAddressesListFromAddressBook = (addressBook: AddressBookState): string[] =>
|
||||
addressBook.map((entry) => entry.address)
|
||||
|
||||
export const getNameFromSafeAddressBook = (addressBook: AddressBookCollection, userAddress: string): string | null => {
|
||||
type GetNameFromAddressBookOptions = {
|
||||
filterOnlyValidName: boolean
|
||||
}
|
||||
|
||||
export const getNameFromAddressBook = (
|
||||
addressBook: AddressBookState,
|
||||
userAddress: string,
|
||||
options?: GetNameFromAddressBookOptions,
|
||||
): string | null => {
|
||||
const entry = addressBook.find((addressBookItem) => addressBookItem.address === userAddress)
|
||||
if (entry) {
|
||||
return entry.name
|
||||
return options?.filterOnlyValidName ? getValidAddressBookName(entry.name) : entry.name
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export const isValidAddressBookName = (addressBookName: string): boolean => {
|
||||
const hasInvalidName = ADDRESSBOOK_INVALID_NAMES.find((invalidName) =>
|
||||
addressBookName.toUpperCase().includes(invalidName),
|
||||
)
|
||||
return !hasInvalidName
|
||||
}
|
||||
|
||||
export const getValidAddressBookName = (addressBookName: string): string | null => {
|
||||
return isValidAddressBookName(addressBookName) ? addressBookName : null
|
||||
}
|
||||
|
||||
export const getOwnersWithNameFromAddressBook = (
|
||||
addressBook: AddressBookCollection,
|
||||
addressBook: AddressBookState,
|
||||
ownerList: List<SafeOwner>,
|
||||
): List<SafeOwner> | [] => {
|
||||
): List<SafeOwner> => {
|
||||
if (!ownerList) {
|
||||
return []
|
||||
return List([])
|
||||
}
|
||||
return ownerList.map((owner) => {
|
||||
const ownerName = getNameFromSafeAddressBook(addressBook, owner.address)
|
||||
const ownerName = getNameFromAddressBook(addressBook, owner.address)
|
||||
return {
|
||||
address: owner.address,
|
||||
name: ownerName || owner.name,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const formatAddressListToAddressBookNames = (
|
||||
addressBook: AddressBookState,
|
||||
addresses: string[],
|
||||
): AddressBookEntry[] => {
|
||||
if (!addresses.length) {
|
||||
return []
|
||||
}
|
||||
return addresses.map((address) => {
|
||||
const ownerName = getNameFromAddressBook(addressBook, address)
|
||||
return {
|
||||
address: address,
|
||||
name: ownerName || '',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* If the safe is not loaded, the owner wasn't not deleted
|
||||
* If the safe is already loaded and the owner has a valid name, will return true if the address is not already on the addressBook
|
||||
* @param name
|
||||
* @param address
|
||||
* @param addressBook
|
||||
* @param safeAlreadyLoaded
|
||||
*/
|
||||
export const checkIfEntryWasDeletedFromAddressBook = (
|
||||
{ name, address }: AddressBookEntry,
|
||||
addressBook: AddressBookState,
|
||||
safeAlreadyLoaded: boolean,
|
||||
): boolean => {
|
||||
if (!safeAlreadyLoaded) {
|
||||
return false
|
||||
}
|
||||
|
||||
const addressShouldBeOnTheAddressBook = !!getValidAddressBookName(name)
|
||||
const isAlreadyInAddressBook = !!addressBook.find((entry) => sameAddress(entry.address, address))
|
||||
return addressShouldBeOnTheAddressBook && !isAlreadyInAddressBook
|
||||
}
|
||||
|
|
|
@ -98,11 +98,9 @@ export const estimateGasForDeployingSafe = async (
|
|||
return gas * parseInt(gasPrice, 10)
|
||||
}
|
||||
|
||||
export const getGnosisSafeInstanceAt = async (safeAddress: string): Promise<GnosisSafe> => {
|
||||
export const getGnosisSafeInstanceAt = (safeAddress: string): GnosisSafe => {
|
||||
const web3 = getWeb3()
|
||||
const gnosisSafe = await new web3.eth.Contract(GnosisSafeSol.abi as AbiItem[], safeAddress) as unknown as GnosisSafe
|
||||
|
||||
return gnosisSafe
|
||||
return new web3.eth.Contract(GnosisSafeSol.abi as AbiItem[], safeAddress) as unknown as GnosisSafe
|
||||
}
|
||||
|
||||
const cleanByteCodeMetadata = (bytecode: string): string => {
|
||||
|
|
|
@ -48,6 +48,8 @@ describe('fetchTokenCurrenciesBalances', () => {
|
|||
// then
|
||||
expect(result).toStrictEqual(expectedResult)
|
||||
expect(axios.get).toHaveBeenCalled()
|
||||
expect(axios.get).toBeCalledWith(`${apiUrl}safes/${safeAddress}/balances/usd/`, { params: { limit: 3000 } })
|
||||
expect(axios.get).toBeCalledWith(`${apiUrl}safes/${safeAddress}/balances/usd/?exclude_spam=true`, {
|
||||
params: { limit: 3000 },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -22,13 +22,13 @@ export const useLoadSafe = (safeAddress?: string): void => {
|
|||
return dispatch(fetchSafeTokens(safeAddress))
|
||||
})
|
||||
.then(() => {
|
||||
dispatch(loadAddressBookFromStorage())
|
||||
dispatch(fetchSafeCreationTx(safeAddress))
|
||||
dispatch(fetchTransactions(safeAddress))
|
||||
return dispatch(addViewedSafe(safeAddress))
|
||||
})
|
||||
}
|
||||
}
|
||||
dispatch(loadAddressBookFromStorage())
|
||||
|
||||
fetchData()
|
||||
}, [dispatch, safeAddress])
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
|
||||
import { SafeRecordProps } from '../models/safe'
|
||||
|
||||
export const ADD_OR_UPDATE_SAFE = 'ADD_OR_UPDATE_SAFE'
|
||||
|
||||
export const addOrUpdateSafe = createAction(ADD_OR_UPDATE_SAFE, (safe: SafeRecordProps) => ({
|
||||
safe,
|
||||
}))
|
|
@ -18,15 +18,16 @@ export const buildOwnersFrom = (names: string[], addresses: string[]): List<Safe
|
|||
return List(owners)
|
||||
}
|
||||
|
||||
export const addSafe = createAction(ADD_SAFE, (safe) => ({
|
||||
export const addSafe = createAction(ADD_SAFE, (safe: SafeRecordProps, loadedFromStorage = false) => ({
|
||||
safe,
|
||||
loadedFromStorage,
|
||||
}))
|
||||
|
||||
const saveSafe = (safe: SafeRecordProps) => (dispatch: Dispatch, getState: () => AppReduxState): void => {
|
||||
const state = getState()
|
||||
const safeList = safesListSelector(state)
|
||||
|
||||
dispatch(addSafe(safe))
|
||||
dispatch(addSafe(safe, true))
|
||||
|
||||
if (safeList.size === 0) {
|
||||
dispatch(setDefaultSafe(safe.address))
|
||||
|
|
|
@ -111,7 +111,7 @@ interface CreateTransactionArgs {
|
|||
|
||||
type CreateTransactionAction = ThunkAction<Promise<void>, AppReduxState, undefined, AnyAction>
|
||||
type ConfirmEventHandler = (safeTxHash: string) => void
|
||||
type RejectEventHandler = () => void
|
||||
type ErrorEventHandler = () => void
|
||||
|
||||
const createTransaction = (
|
||||
{
|
||||
|
@ -126,7 +126,7 @@ const createTransaction = (
|
|||
origin = null,
|
||||
}: CreateTransactionArgs,
|
||||
onUserConfirm?: ConfirmEventHandler,
|
||||
onUserReject?: RejectEventHandler,
|
||||
onError?: ErrorEventHandler,
|
||||
): CreateTransactionAction => async (dispatch: Dispatch, getState: () => AppReduxState): Promise<void> => {
|
||||
const state = getState()
|
||||
|
||||
|
@ -172,6 +172,7 @@ const createTransaction = (
|
|||
sender: from,
|
||||
sigs,
|
||||
}
|
||||
const safeTxHash = generateSafeTxHash(safeAddress, txArgs)
|
||||
|
||||
try {
|
||||
// Here we're checking that safe contract version is greater or equal 1.1.1, but
|
||||
|
@ -179,20 +180,19 @@ const createTransaction = (
|
|||
const canTryOffchainSigning =
|
||||
!isExecution && !smartContractWallet && semverSatisfies(safeVersion, SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES)
|
||||
if (canTryOffchainSigning) {
|
||||
const signature = await tryOffchainSigning({ ...txArgs, safeAddress }, hardwareWallet)
|
||||
const signature = await tryOffchainSigning(safeTxHash, { ...txArgs, safeAddress }, hardwareWallet)
|
||||
|
||||
if (signature) {
|
||||
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
|
||||
dispatch(enqueueSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded))
|
||||
dispatch(fetchTransactions(safeAddress))
|
||||
|
||||
await saveTxToHistory({ ...txArgs, signature, origin })
|
||||
dispatch(enqueueSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded))
|
||||
|
||||
dispatch(fetchTransactions(safeAddress))
|
||||
onUserConfirm?.(safeTxHash)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const safeTxHash = generateSafeTxHash(safeAddress, txArgs)
|
||||
const tx = isExecution
|
||||
? await getExecutionTransaction(txArgs)
|
||||
: await getApprovalTransaction(safeInstance, safeTxHash)
|
||||
|
@ -245,20 +245,7 @@ const createTransaction = (
|
|||
removeTxFromStore(mockedTx, safeAddress, dispatch, state)
|
||||
console.error('Tx error: ', error)
|
||||
|
||||
// Different wallets return different error messages in this case. This is an assumption that if
|
||||
// error message includes "user" word, the tx was rejected by user
|
||||
|
||||
let errorIncludesUserWord = false
|
||||
if (typeof error === 'string') {
|
||||
errorIncludesUserWord = (error as string).includes('User') || (error as string).includes('user')
|
||||
}
|
||||
if (error.message) {
|
||||
errorIncludesUserWord = error.message.includes('User') || error.message.includes('user')
|
||||
}
|
||||
|
||||
if (errorIncludesUserWord) {
|
||||
onUserReject?.()
|
||||
}
|
||||
onError?.()
|
||||
})
|
||||
.then(async (receipt) => {
|
||||
if (pendingExecutionKey) {
|
||||
|
|
|
@ -6,7 +6,6 @@ import { getLocalSafe, getSafeName } from 'src/logic/safe/utils'
|
|||
import { enabledFeatures, safeNeedsUpdate } from 'src/logic/safe/utils/safeVersion'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { getBalanceInEtherOf } from 'src/logic/wallets/getWeb3'
|
||||
import addSafe from 'src/logic/safe/store/actions/addSafe'
|
||||
import addSafeOwner from 'src/logic/safe/store/actions/addSafeOwner'
|
||||
import removeSafeOwner from 'src/logic/safe/store/actions/removeSafeOwner'
|
||||
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
|
||||
|
@ -17,6 +16,7 @@ import { ModulePair, SafeOwner, SafeRecordProps } from 'src/logic/safe/store/mod
|
|||
import { Action, Dispatch } from 'redux'
|
||||
import { SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts'
|
||||
import { AppReduxState } from 'src/store'
|
||||
import { latestMasterContractVersionSelector } from '../selectors'
|
||||
|
||||
const buildOwnersFrom = (safeOwners: string[], localSafe?: SafeRecordProps): List<SafeOwner> => {
|
||||
const ownersList = safeOwners.map((ownerAddress) => {
|
||||
|
@ -114,11 +114,12 @@ export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: Dispatch
|
|||
])
|
||||
|
||||
// Converts from [ { address, ownerName} ] to address array
|
||||
const localOwners = localSafe ? localSafe.owners.map((localOwner) => localOwner.address) : undefined
|
||||
const localOwners = localSafe ? localSafe.owners.map((localOwner) => localOwner.address) : []
|
||||
|
||||
dispatch(
|
||||
updateSafe({
|
||||
address: safeAddress,
|
||||
name: localSafe?.name,
|
||||
modules: buildModulesLinkedList(modules?.array, modules?.next),
|
||||
nonce: Number(remoteNonce),
|
||||
threshold: Number(remoteThreshold),
|
||||
|
@ -126,30 +127,27 @@ export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: Dispatch
|
|||
)
|
||||
|
||||
// If the remote owners does not contain a local address, we remove that local owner
|
||||
if (localOwners) {
|
||||
localOwners.forEach((localAddress) => {
|
||||
const remoteOwnerIndex = remoteOwners.findIndex((remoteAddress) => sameAddress(remoteAddress, localAddress))
|
||||
if (remoteOwnerIndex === -1) {
|
||||
dispatch(removeSafeOwner({ safeAddress, ownerAddress: localAddress }))
|
||||
}
|
||||
})
|
||||
localOwners.forEach((localAddress) => {
|
||||
const remoteOwnerIndex = remoteOwners.findIndex((remoteAddress) => sameAddress(remoteAddress, localAddress))
|
||||
if (remoteOwnerIndex === -1) {
|
||||
dispatch(removeSafeOwner({ safeAddress, ownerAddress: localAddress }))
|
||||
}
|
||||
})
|
||||
|
||||
// If the remote has an owner that we don't have locally, we add it
|
||||
remoteOwners.forEach((remoteAddress) => {
|
||||
const localOwnerIndex = localOwners.findIndex((localAddress) => sameAddress(remoteAddress, localAddress))
|
||||
if (localOwnerIndex === -1) {
|
||||
dispatch(
|
||||
addSafeOwner({
|
||||
safeAddress,
|
||||
ownerAddress: remoteAddress,
|
||||
ownerName: 'UNKNOWN',
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
// If the remote has an owner that we don't have locally, we add it
|
||||
remoteOwners.forEach((remoteAddress) => {
|
||||
const localOwnerIndex = localOwners.findIndex((localAddress) => sameAddress(remoteAddress, localAddress))
|
||||
if (localOwnerIndex === -1) {
|
||||
dispatch(
|
||||
addSafeOwner({
|
||||
safeAddress,
|
||||
ownerAddress: remoteAddress,
|
||||
ownerName: 'UNKNOWN',
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default (safeAdd: string) => async (
|
||||
dispatch: Dispatch<any>,
|
||||
getState: () => AppReduxState,
|
||||
|
@ -157,12 +155,15 @@ export default (safeAdd: string) => async (
|
|||
try {
|
||||
const safeAddress = checksumAddress(safeAdd)
|
||||
const safeName = (await getSafeName(safeAddress)) || 'LOADED SAFE'
|
||||
const latestMasterContractVersion = getState().safes.get('latestMasterContractVersion')
|
||||
const latestMasterContractVersion = latestMasterContractVersionSelector(getState())
|
||||
const safeProps = await buildSafe(safeAddress, safeName, latestMasterContractVersion)
|
||||
|
||||
dispatch(addSafe(safeProps))
|
||||
// `updateSafe`, as `loadSafesFromStorage` will populate the store previous to this call
|
||||
// and `addSafe` will only add a newly non-existent safe
|
||||
// For the case where the safe does not exist in the localStorage,
|
||||
// `updateSafe` uses a default `notSetValue` to add the Safe to the store
|
||||
dispatch(updateSafe(safeProps))
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line
|
||||
console.error('Error while updating Safe information: ', err)
|
||||
|
||||
return Promise.resolve()
|
||||
|
|
|
@ -13,7 +13,7 @@ const loadSafesFromStorage = () => async (dispatch: Dispatch): Promise<void> =>
|
|||
|
||||
if (safes) {
|
||||
Object.values(safes).forEach((safeProps) => {
|
||||
dispatch(addSafe(buildSafe(safeProps)))
|
||||
dispatch(addSafe(buildSafe(safeProps), true))
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
|
@ -78,7 +78,7 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres
|
|||
const canTryOffchainSigning =
|
||||
!isExecution && !smartContractWallet && semverSatisfies(safeVersion, SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES)
|
||||
if (canTryOffchainSigning) {
|
||||
const signature = await tryOffchainSigning({ ...txArgs, safeAddress }, hardwareWallet)
|
||||
const signature = await tryOffchainSigning(tx.safeTxHash, { ...txArgs, safeAddress }, hardwareWallet)
|
||||
|
||||
if (signature) {
|
||||
dispatch(closeSnackbarAction(beforeExecutionKey))
|
||||
|
|
|
@ -103,7 +103,7 @@ const notificationsMiddleware = (store) => (next) => async (action) => {
|
|||
}
|
||||
case ADD_INCOMING_TRANSACTIONS: {
|
||||
action.payload.forEach((incomingTransactions, safeAddress) => {
|
||||
const { latestIncomingTxBlock } = state.safes.get('safes').get(safeAddress)
|
||||
const { latestIncomingTxBlock } = state.safes.get('safes').get(safeAddress, {})
|
||||
const viewedSafes = state.currentSession['viewedSafes']
|
||||
const recurringUser = viewedSafes?.includes(safeAddress)
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { addAddressBookEntry } from 'src/logic/addressBook/store/actions/addAddressBookEntry'
|
||||
import { saveDefaultSafe, saveSafes } from 'src/logic/safe/utils'
|
||||
import { tokensSelector } from 'src/logic/tokens/store/selectors'
|
||||
|
@ -13,11 +12,20 @@ import { REPLACE_SAFE_OWNER } from 'src/logic/safe/store/actions/replaceSafeOwne
|
|||
import { SET_DEFAULT_SAFE } from 'src/logic/safe/store/actions/setDefaultSafe'
|
||||
import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe'
|
||||
import { getActiveTokensAddressesForAllSafes, safesMapSelector } from 'src/logic/safe/store/selectors'
|
||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
|
||||
import { checkIfEntryWasDeletedFromAddressBook, isValidAddressBookName } from 'src/logic/addressBook/utils'
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { updateAddressBookEntry } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
|
||||
import { ADD_OR_UPDATE_SAFE } from 'src/logic/safe/store/actions/addOrUpdateSafe'
|
||||
|
||||
const watchedActions = [
|
||||
ADD_SAFE,
|
||||
UPDATE_SAFE,
|
||||
REMOVE_SAFE,
|
||||
ADD_OR_UPDATE_SAFE,
|
||||
ADD_SAFE_OWNER,
|
||||
REMOVE_SAFE_OWNER,
|
||||
REPLACE_SAFE_OWNER,
|
||||
|
@ -48,6 +56,7 @@ const safeStorageMware = (store) => (next) => async (action) => {
|
|||
const state = store.getState()
|
||||
const { dispatch } = store
|
||||
const safes = safesMapSelector(state)
|
||||
const addressBook = addressBookSelector(state)
|
||||
await saveSafes(safes.toJSON())
|
||||
|
||||
switch (action.type) {
|
||||
|
@ -56,19 +65,60 @@ const safeStorageMware = (store) => (next) => async (action) => {
|
|||
break
|
||||
}
|
||||
case ADD_SAFE: {
|
||||
const { safe, loadedFromStorage } = action.payload
|
||||
const safeAlreadyLoaded =
|
||||
loadedFromStorage || safes.find((safeIterator) => sameAddress(safeIterator.address, safe.address))
|
||||
|
||||
safe.owners.forEach((owner) => {
|
||||
const checksumEntry = makeAddressBookEntry({ address: checksumAddress(owner.address), name: owner.name })
|
||||
|
||||
const ownerWasAlreadyInAddressBook = checkIfEntryWasDeletedFromAddressBook(
|
||||
checksumEntry,
|
||||
addressBook,
|
||||
safeAlreadyLoaded,
|
||||
)
|
||||
|
||||
if (!ownerWasAlreadyInAddressBook) {
|
||||
dispatch(addAddressBookEntry(checksumEntry, { notifyEntryUpdate: false }))
|
||||
}
|
||||
const addressAlreadyExists = addressBook.find((entry) => sameAddress(entry.address, checksumEntry.address))
|
||||
if (isValidAddressBookName(checksumEntry.name) && addressAlreadyExists) {
|
||||
dispatch(updateAddressBookEntry(checksumEntry))
|
||||
}
|
||||
})
|
||||
const safeWasAlreadyInAddressBook = checkIfEntryWasDeletedFromAddressBook(
|
||||
{ address: safe.address, name: safe.name },
|
||||
addressBook,
|
||||
safeAlreadyLoaded,
|
||||
)
|
||||
|
||||
if (!safeWasAlreadyInAddressBook) {
|
||||
dispatch(
|
||||
addAddressBookEntry(makeAddressBookEntry({ address: safe.address, name: safe.name }), {
|
||||
notifyEntryUpdate: true,
|
||||
}),
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
case ADD_OR_UPDATE_SAFE: {
|
||||
const { safe } = action.payload
|
||||
const ownersArray = safe.owners.toJS()
|
||||
// Adds the owners to the address book
|
||||
ownersArray.forEach((owner) => {
|
||||
dispatch(addAddressBookEntry(makeAddressBookEntry({ ...owner, isOwner: true })))
|
||||
safe.owners.forEach((owner) => {
|
||||
const checksumEntry = makeAddressBookEntry({ address: checksumAddress(owner.address), name: owner.name })
|
||||
if (isValidAddressBookName(checksumEntry.name)) {
|
||||
dispatch(addOrUpdateAddressBookEntry(checksumEntry))
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
case UPDATE_SAFE: {
|
||||
const { activeTokens } = action.payload
|
||||
const { activeTokens, name, address } = action.payload
|
||||
if (activeTokens) {
|
||||
recalculateActiveTokens(state)
|
||||
}
|
||||
if (name) {
|
||||
dispatch(addOrUpdateAddressBookEntry(makeAddressBookEntry({ name, address })))
|
||||
}
|
||||
break
|
||||
}
|
||||
case SET_DEFAULT_SAFE: {
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import { handleActions } from 'redux-actions'
|
||||
|
||||
import { Transaction } from '../models/types/transactions'
|
||||
import { LOAD_MORE_TRANSACTIONS, LoadMoreTransactionsAction } from '../actions/allTransactions/pagination'
|
||||
import { Transaction } from 'src/logic/safe/store/models/types/transactions'
|
||||
import {
|
||||
LOAD_MORE_TRANSACTIONS,
|
||||
LoadMoreTransactionsAction,
|
||||
} from 'src/logic/safe/store/actions/allTransactions/pagination'
|
||||
|
||||
export const TRANSACTIONS = 'allTransactions'
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Map, Set } from 'immutable'
|
||||
import { Map, Set, List } from 'immutable'
|
||||
import { handleActions } from 'redux-actions'
|
||||
|
||||
import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from 'src/logic/safe/store/actions/activateTokenForAllSafes'
|
||||
|
@ -15,6 +15,7 @@ import { makeOwner } from 'src/logic/safe/store/models/owner'
|
|||
import makeSafe, { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||
import { SafeReducerMap } from 'src/routes/safe/store/reducer/types/safe'
|
||||
import { ADD_OR_UPDATE_SAFE } from 'src/logic/safe/store/actions/addOrUpdateSafe'
|
||||
|
||||
export const SAFE_REDUCER_ID = 'safes'
|
||||
export const DEFAULT_SAFE_INITIAL_STATE = 'NOT_ASKED'
|
||||
|
@ -42,6 +43,32 @@ export const buildSafe = (storedSafe: SafeRecordProps): SafeRecordProps => {
|
|||
}
|
||||
}
|
||||
|
||||
const updateSafeProps = (prevSafe, safe) => {
|
||||
return prevSafe.withMutations((record) => {
|
||||
// Every property is updated individually to overcome the issue with nested data being overwritten
|
||||
const safeProperties = Object.keys(safe)
|
||||
|
||||
// We check each safe property sent in action.payload
|
||||
safeProperties.forEach((key) => {
|
||||
if (safe[key] && typeof safe[key] === 'object') {
|
||||
if (safe[key].length) {
|
||||
// If type is array we update the array
|
||||
record.update(key, () => safe[key])
|
||||
} else if (safe[key].size) {
|
||||
// If type is Immutable List we replace current List
|
||||
// If type is Object we do a merge
|
||||
List.isList(safe[key])
|
||||
? record.update(key, (current) => current.set(safe[key]))
|
||||
: record.update(key, (current) => current.merge(safe[key]))
|
||||
}
|
||||
} else {
|
||||
// By default we overwrite the value. This is for strings, numbers and unset values
|
||||
record.set(key, safe[key])
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export default handleActions(
|
||||
{
|
||||
[UPDATE_SAFE]: (state: SafeReducerMap, action) => {
|
||||
|
@ -50,8 +77,8 @@ export default handleActions(
|
|||
|
||||
return state.updateIn(
|
||||
['safes', safeAddress],
|
||||
makeSafe({ name: 'LOADED SAFE', address: safeAddress }),
|
||||
(prevSafe) => prevSafe.merge(safe),
|
||||
makeSafe({ name: safe?.name || 'LOADED SAFE', address: safeAddress }),
|
||||
(prevSafe) => updateSafeProps(prevSafe, safe),
|
||||
)
|
||||
},
|
||||
[ACTIVATE_TOKEN_FOR_ALL_SAFES]: (state: SafeReducerMap, action) => {
|
||||
|
@ -82,6 +109,19 @@ export default handleActions(
|
|||
|
||||
return state.setIn(['safes', safe.address], makeSafe(safe))
|
||||
},
|
||||
[ADD_OR_UPDATE_SAFE]: (state: SafeReducerMap, action) => {
|
||||
const { safe } = action.payload
|
||||
|
||||
if (!state.hasIn(['safes', safe.address])) {
|
||||
return state.setIn(['safes', safe.address], makeSafe(safe))
|
||||
}
|
||||
|
||||
return state.updateIn(
|
||||
['safes', safe.address],
|
||||
makeSafe({ name: 'LOADED SAFE', address: safe.address }),
|
||||
(prevSafe) => updateSafeProps(prevSafe, safe),
|
||||
)
|
||||
},
|
||||
[REMOVE_SAFE]: (state: SafeReducerMap, action) => {
|
||||
const safeAddress = action.payload
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||
import { AbstractProvider } from 'web3-core'
|
||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||
|
||||
const EIP712_NOT_SUPPORTED_ERROR_MSG = "EIP712 is not supported by user's wallet"
|
||||
|
||||
|
@ -59,7 +60,7 @@ const generateTypedDataFrom = async ({
|
|||
}
|
||||
|
||||
export const getEIP712Signer = (version?: string) => async (txArgs) => {
|
||||
const web3: any = getWeb3()
|
||||
const web3 = getWeb3()
|
||||
const typedData = await generateTypedDataFrom(txArgs)
|
||||
|
||||
let method = 'eth_signTypedData_v3'
|
||||
|
@ -80,13 +81,14 @@ export const getEIP712Signer = (version?: string) => async (txArgs) => {
|
|||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
web3.currentProvider.sendAsync(signedTypedData, (err, signature) => {
|
||||
const provider = web3.currentProvider as AbstractProvider
|
||||
provider.sendAsync(signedTypedData, (err, signature) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
|
||||
if (signature.result == null) {
|
||||
if (signature?.result == null) {
|
||||
reject(new Error(EIP712_NOT_SUPPORTED_ERROR_MSG))
|
||||
return
|
||||
}
|
||||
|
|
|
@ -4,26 +4,13 @@ import { AbstractProvider } from 'web3-core/types'
|
|||
|
||||
const ETH_SIGN_NOT_SUPPORTED_ERROR_MSG = 'ETH_SIGN_NOT_SUPPORTED'
|
||||
|
||||
export const ethSigner = async ({
|
||||
baseGas,
|
||||
data,
|
||||
gasPrice,
|
||||
gasToken,
|
||||
nonce,
|
||||
operation,
|
||||
refundReceiver,
|
||||
safeInstance,
|
||||
safeTxGas,
|
||||
sender,
|
||||
to,
|
||||
valueInWei,
|
||||
}): Promise<string> => {
|
||||
type EthSignerArgs = {
|
||||
safeTxHash: string
|
||||
sender: string
|
||||
}
|
||||
|
||||
export const ethSigner = async ({ safeTxHash, sender }: EthSignerArgs): Promise<string> => {
|
||||
const web3 = await getWeb3()
|
||||
const txHash = await safeInstance.methods
|
||||
.getTransactionHash(to, valueInWei, data, operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, nonce)
|
||||
.call({
|
||||
from: sender,
|
||||
})
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
const provider = web3.currentProvider as AbstractProvider
|
||||
|
@ -31,7 +18,7 @@ export const ethSigner = async ({
|
|||
{
|
||||
jsonrpc: '2.0',
|
||||
method: 'eth_sign',
|
||||
params: [sender, txHash],
|
||||
params: [sender, safeTxHash],
|
||||
id: new Date().getTime(),
|
||||
},
|
||||
async function (err, signature) {
|
||||
|
|
|
@ -8,7 +8,7 @@ import { ethSigner } from './ethSigner'
|
|||
const SIGNERS = {
|
||||
EIP712_V3: getEIP712Signer('v3'),
|
||||
EIP712_V4: getEIP712Signer('v4'),
|
||||
EIP712: getEIP712Signer() as any,
|
||||
EIP712: getEIP712Signer(),
|
||||
ETH_SIGN: ethSigner,
|
||||
}
|
||||
|
||||
|
@ -18,13 +18,13 @@ const getSignersByWallet = (isHW) =>
|
|||
|
||||
export const SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES = '>=1.1.1'
|
||||
|
||||
export const tryOffchainSigning = async (txArgs, isHW) => {
|
||||
export const tryOffchainSigning = async (safeTxHash: string, txArgs, isHW: boolean): Promise<string> => {
|
||||
let signature
|
||||
|
||||
const signerByWallet = getSignersByWallet(isHW)
|
||||
for (const signingFunc of signerByWallet) {
|
||||
try {
|
||||
signature = await signingFunc(txArgs)
|
||||
signature = await signingFunc({ ...txArgs, safeTxHash })
|
||||
|
||||
break
|
||||
} catch (err) {
|
||||
|
|
|
@ -6,23 +6,16 @@ export const DEFAULT_SAFE_KEY = 'DEFAULT_SAFE'
|
|||
|
||||
type StoredSafes = Record<string, SafeRecordProps>
|
||||
|
||||
export const loadStoredSafes = async (): Promise<StoredSafes | undefined> => {
|
||||
const safes = await loadFromStorage<StoredSafes>(SAFES_KEY)
|
||||
|
||||
return safes
|
||||
export const loadStoredSafes = (): Promise<StoredSafes | undefined> => {
|
||||
return loadFromStorage<StoredSafes>(SAFES_KEY)
|
||||
}
|
||||
|
||||
export const getSafeName = async (safeAddress: string): Promise<string | undefined> => {
|
||||
const safes = await loadStoredSafes()
|
||||
if (!safes) {
|
||||
return undefined
|
||||
}
|
||||
const safe = safes[safeAddress]
|
||||
|
||||
return safe ? safe.name : undefined
|
||||
return safes?.[safeAddress]?.name
|
||||
}
|
||||
|
||||
export const saveSafes = async (safes) => {
|
||||
export const saveSafes = async (safes: StoredSafes): Promise<void> => {
|
||||
try {
|
||||
await saveToStorage(SAFES_KEY, safes)
|
||||
} catch (err) {
|
||||
|
|
|
@ -71,7 +71,7 @@ export const getCurrentMasterContractLastVersion = async (): Promise<string> =>
|
|||
|
||||
export const getSafeVersionInfo = async (safeAddress: string): Promise<SafeVersionInfo> => {
|
||||
try {
|
||||
const safeMaster = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const safeMaster = getGnosisSafeInstanceAt(safeAddress)
|
||||
const lastSafeVersion = await getCurrentMasterContractLastVersion()
|
||||
return checkIfSafeNeedsUpdate(safeMaster, lastSafeVersion)
|
||||
} catch (err) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import TableContainer from '@material-ui/core/TableContainer'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import CopyBtn from 'src/components/CopyBtn'
|
||||
|
@ -17,57 +17,13 @@ import Row from 'src/components/layout/Row'
|
|||
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 { border, disabled, extraSmallFontSize, lg, md, screenSm, sm } from 'src/theme/variables'
|
||||
|
||||
const styles = () => ({
|
||||
details: {
|
||||
padding: lg,
|
||||
borderRight: `solid 1px ${border}`,
|
||||
height: '100%',
|
||||
},
|
||||
owners: {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
ownerName: {
|
||||
marginBottom: '15px',
|
||||
minWidth: '100%',
|
||||
[`@media (min-width: ${screenSm}px)`]: {
|
||||
marginBottom: '0',
|
||||
minWidth: '0',
|
||||
},
|
||||
},
|
||||
ownerAddresses: {
|
||||
alignItems: 'center',
|
||||
marginLeft: `${sm}`,
|
||||
},
|
||||
address: {
|
||||
paddingLeft: '6px',
|
||||
marginRight: sm,
|
||||
},
|
||||
open: {
|
||||
paddingLeft: sm,
|
||||
width: 'auto',
|
||||
'&:hover': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
},
|
||||
title: {
|
||||
padding: `${md} ${lg}`,
|
||||
},
|
||||
owner: {
|
||||
padding: `0 ${lg}`,
|
||||
marginBottom: '12px',
|
||||
},
|
||||
header: {
|
||||
padding: `${sm} ${lg}`,
|
||||
color: disabled,
|
||||
fontSize: extraSmallFontSize,
|
||||
},
|
||||
name: {
|
||||
marginRight: `${sm}`,
|
||||
},
|
||||
})
|
||||
import { useSelector } from 'react-redux'
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
|
||||
import { formatAddressListToAddressBookNames } from 'src/logic/addressBook/utils'
|
||||
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { styles } from './styles'
|
||||
|
||||
const calculateSafeValues = (owners, threshold, values) => {
|
||||
const initialValues = { ...values }
|
||||
|
@ -78,9 +34,20 @@ const calculateSafeValues = (owners, threshold, values) => {
|
|||
return initialValues
|
||||
}
|
||||
|
||||
const useAddressBookForOwnersNames = (ownersList: string[]): AddressBookEntry[] => {
|
||||
const addressBook = useSelector(addressBookSelector)
|
||||
|
||||
return formatAddressListToAddressBookNames(addressBook, ownersList)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const OwnerListComponent = (props) => {
|
||||
const [owners, setOwners] = useState<string[]>([])
|
||||
const { classes, updateInitialProps, values } = props
|
||||
const classes = useStyles()
|
||||
const { updateInitialProps, values } = props
|
||||
|
||||
const ownersWithNames = useAddressBookForOwnersNames(owners)
|
||||
|
||||
useEffect(() => {
|
||||
let isCurrent = true
|
||||
|
@ -121,47 +88,48 @@ const OwnerListComponent = (props) => {
|
|||
</Row>
|
||||
<Hairline />
|
||||
<Block margin="md" padding="md">
|
||||
{owners.map((address, index) => (
|
||||
<Row className={classes.owner} key={address} data-testid="owner-row">
|
||||
<Col className={classes.ownerName} xs={4}>
|
||||
<Field
|
||||
className={classes.name}
|
||||
component={TextField}
|
||||
initialValue={`Owner #${index + 1}`}
|
||||
name={getOwnerNameBy(index)}
|
||||
placeholder="Owner Name*"
|
||||
text="Owner Name"
|
||||
type="text"
|
||||
validate={composeValidators(required, minMaxLength(1, 50))}
|
||||
testId={`load-safe-owner-name-${index}`}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={8}>
|
||||
<Row className={classes.ownerAddresses}>
|
||||
<Identicon address={address} diameter={32} />
|
||||
<Paragraph className={classes.address} color="disabled" noMargin size="md">
|
||||
{address}
|
||||
</Paragraph>
|
||||
<CopyBtn content={address} />
|
||||
<EtherscanBtn type="address" value={address} />
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
{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}
|
||||
name={getOwnerNameBy(index)}
|
||||
placeholder="Owner Name*"
|
||||
text="Owner Name"
|
||||
type="text"
|
||||
validate={composeValidators(required, minMaxLength(1, 50))}
|
||||
testId={`load-safe-owner-name-${index}`}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={8}>
|
||||
<Row className={classes.ownerAddresses}>
|
||||
<Identicon address={address} diameter={32} />
|
||||
<Paragraph className={classes.address} color="disabled" noMargin size="md">
|
||||
{address}
|
||||
</Paragraph>
|
||||
<CopyBtn content={address} />
|
||||
<EtherscanBtn type="address" value={address} />
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
})}
|
||||
</Block>
|
||||
</TableContainer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const OwnerListPage = withStyles(styles as any)(OwnerListComponent)
|
||||
|
||||
const OwnerList = ({ updateInitialProps }, network) =>
|
||||
function LoadSafeOwnerList(controls, { values }): React.ReactElement {
|
||||
return (
|
||||
<>
|
||||
<OpenPaper controls={controls} padding={false}>
|
||||
<OwnerListPage network={network} updateInitialProps={updateInitialProps} values={values} />
|
||||
<OwnerListComponent network={network} updateInitialProps={updateInitialProps} values={values} />
|
||||
</OpenPaper>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import { border, disabled, extraSmallFontSize, lg, md, screenSm, sm } from 'src/theme/variables'
|
||||
import { createStyles } from '@material-ui/core'
|
||||
|
||||
export const styles = createStyles({
|
||||
details: {
|
||||
padding: lg,
|
||||
borderRight: `solid 1px ${border}`,
|
||||
height: '100%',
|
||||
},
|
||||
owners: {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
ownerName: {
|
||||
marginBottom: '15px',
|
||||
minWidth: '100%',
|
||||
[`@media (min-width: ${screenSm}px)`]: {
|
||||
marginBottom: '0',
|
||||
minWidth: '0',
|
||||
},
|
||||
},
|
||||
ownerAddresses: {
|
||||
alignItems: 'center',
|
||||
marginLeft: `${sm}`,
|
||||
},
|
||||
address: {
|
||||
paddingLeft: '6px',
|
||||
marginRight: sm,
|
||||
},
|
||||
open: {
|
||||
paddingLeft: sm,
|
||||
width: 'auto',
|
||||
'&:hover': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
},
|
||||
title: {
|
||||
padding: `${md} ${lg}`,
|
||||
},
|
||||
owner: {
|
||||
padding: `0 ${lg}`,
|
||||
marginBottom: '12px',
|
||||
},
|
||||
header: {
|
||||
padding: `${sm} ${lg}`,
|
||||
color: disabled,
|
||||
fontSize: extraSmallFontSize,
|
||||
},
|
||||
name: {
|
||||
marginRight: `${sm}`,
|
||||
},
|
||||
})
|
|
@ -14,8 +14,8 @@ import { history } from 'src/store'
|
|||
import { SafeOwner, SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||
import { List } from 'immutable'
|
||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||
import { addSafe } from 'src/logic/safe/store/actions/addSafe'
|
||||
import { networkSelector, providerNameSelector, userAccountSelector } from 'src/logic/wallets/store/selectors'
|
||||
import { addOrUpdateSafe } from 'src/logic/safe/store/actions/addOrUpdateSafe'
|
||||
|
||||
export const loadSafe = async (
|
||||
safeName: string,
|
||||
|
@ -46,8 +46,8 @@ const Load = (): React.ReactElement => {
|
|||
const network = useSelector(networkSelector)
|
||||
const userAddress = useSelector(userAccountSelector)
|
||||
|
||||
const addSafeHandler = (safe: SafeRecordProps) => {
|
||||
dispatch(addSafe(safe))
|
||||
const addSafeHandler = async (safe: SafeRecordProps) => {
|
||||
await dispatch(addOrUpdateSafe(safe))
|
||||
}
|
||||
const onLoadSafeSubmit = async (values: LoadFormValues) => {
|
||||
let safeAddress = values[FIELD_LOAD_ADDRESS]
|
||||
|
@ -62,7 +62,7 @@ const Load = (): React.ReactElement => {
|
|||
safeAddress = checksumAddress(safeAddress)
|
||||
const ownerNames = getNamesFrom(values)
|
||||
|
||||
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const gnosisSafe = getGnosisSafeInstanceAt(safeAddress)
|
||||
const ownerAddresses = await gnosisSafe.methods.getOwners().call()
|
||||
const owners = getOwnersFrom(ownerNames, ownerAddresses.slice().sort())
|
||||
|
||||
|
|
|
@ -19,22 +19,43 @@ import {
|
|||
import Welcome from 'src/routes/welcome/components/Layout'
|
||||
import { history } from 'src/store'
|
||||
import { secondary, sm } from 'src/theme/variables'
|
||||
import { networkSelector, providerNameSelector, userAccountSelector } from 'src/logic/wallets/store/selectors'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { getNameFromAddressBook } from 'src/logic/addressBook/utils'
|
||||
|
||||
const { useEffect } = React
|
||||
|
||||
const getSteps = () => ['Name', 'Owners and confirmations', 'Review']
|
||||
|
||||
const initialValuesFrom = (userAccount, safeProps) => {
|
||||
type SafeProps = {
|
||||
name: string
|
||||
ownerAddresses: any
|
||||
ownerNames: string
|
||||
threshold: string
|
||||
}
|
||||
|
||||
type InitialValuesForm = {
|
||||
owner0Address?: string
|
||||
owner0Name?: string
|
||||
confirmations: string
|
||||
safeName?: string
|
||||
}
|
||||
|
||||
const useInitialValuesFrom = (userAccount: string, safeProps?: SafeProps): InitialValuesForm => {
|
||||
const addressBook = useSelector(addressBookSelector)
|
||||
const ownerName = getNameFromAddressBook(addressBook, userAccount, { filterOnlyValidName: true })
|
||||
|
||||
if (!safeProps) {
|
||||
return {
|
||||
[getOwnerNameBy(0)]: 'My Wallet',
|
||||
[getOwnerNameBy(0)]: ownerName || 'My Wallet',
|
||||
[getOwnerAddressBy(0)]: userAccount,
|
||||
[FIELD_CONFIRMATIONS]: '1',
|
||||
}
|
||||
}
|
||||
let obj = {}
|
||||
const { name, ownerAddresses, ownerNames, threshold } = safeProps
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
|
||||
for (const [index, value] of ownerAddresses.entries()) {
|
||||
const safeName = ownerNames[index] ? ownerNames[index] : 'My Wallet'
|
||||
obj = {
|
||||
|
@ -66,8 +87,17 @@ const formMutators = {
|
|||
},
|
||||
}
|
||||
|
||||
const Layout = (props) => {
|
||||
const { network, onCallSafeContractSubmit, provider, safeProps, userAccount } = props
|
||||
type LayoutProps = {
|
||||
onCallSafeContractSubmit: (formValues: unknown) => void
|
||||
safeProps?: SafeProps
|
||||
}
|
||||
|
||||
const Layout = (props: LayoutProps): React.ReactElement => {
|
||||
const { onCallSafeContractSubmit, safeProps } = props
|
||||
|
||||
const provider = useSelector(providerNameSelector)
|
||||
const network = useSelector(networkSelector)
|
||||
const userAccount = useSelector(userAccountSelector)
|
||||
|
||||
useEffect(() => {
|
||||
if (provider) {
|
||||
|
@ -77,7 +107,7 @@ const Layout = (props) => {
|
|||
|
||||
const steps = getSteps()
|
||||
|
||||
const initialValues = initialValuesFrom(userAccount, safeProps)
|
||||
const initialValues = useInitialValuesFrom(userAccount, safeProps)
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import TableContainer from '@material-ui/core/TableContainer'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
||||
import classNames from 'classnames'
|
||||
import * as React from 'react'
|
||||
|
||||
|
@ -22,7 +22,7 @@ import { background, border, lg, screenSm, sm } from 'src/theme/variables'
|
|||
|
||||
const { useEffect, useState } = React
|
||||
|
||||
const styles = () => ({
|
||||
const styles = createStyles({
|
||||
root: {
|
||||
minHeight: '300px',
|
||||
[`@media (min-width: ${screenSm}px)`]: {
|
||||
|
@ -84,7 +84,15 @@ const styles = () => ({
|
|||
},
|
||||
})
|
||||
|
||||
const ReviewComponent = ({ classes, userAccount, values }: any) => {
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
type ReviewComponentProps = {
|
||||
userAccount: string
|
||||
values: any
|
||||
}
|
||||
|
||||
const ReviewComponent = ({ userAccount, values }: ReviewComponentProps) => {
|
||||
const classes = useStyles()
|
||||
const [gasCosts, setGasCosts] = useState('< 0.001')
|
||||
const names = getNamesFrom(values)
|
||||
const addresses = getAccountsFrom(values)
|
||||
|
@ -198,12 +206,11 @@ const ReviewComponent = ({ classes, userAccount, values }: any) => {
|
|||
)
|
||||
}
|
||||
|
||||
const ReviewPage = withStyles(styles as any)(ReviewComponent)
|
||||
|
||||
const Review = () => (controls, { values }) => (
|
||||
// eslint-disable-next-line react/display-name
|
||||
const Review = () => (controls, props): React.ReactElement => (
|
||||
<>
|
||||
<OpenPaper controls={controls} padding={false}>
|
||||
<ReviewPage values={values} />
|
||||
<ReviewComponent {...props} />
|
||||
</OpenPaper>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import InputAdornment from '@material-ui/core/InputAdornment'
|
||||
import MenuItem from '@material-ui/core/MenuItem'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import CheckCircle from '@material-ui/icons/CheckCircle'
|
||||
import * as React from 'react'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
|
||||
import { styles } from './style'
|
||||
import { getAddressValidator } from './validators'
|
||||
|
||||
|
@ -38,6 +36,9 @@ import {
|
|||
getOwnerNameBy,
|
||||
} from 'src/routes/open/components/fields'
|
||||
import { getAccountsFrom } from 'src/routes/open/utils/safeDataExtractor'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { getNameFromAddressBook } from 'src/logic/addressBook/utils'
|
||||
|
||||
const { useState } = React
|
||||
|
||||
|
@ -63,10 +64,14 @@ export const calculateValuesAfterRemoving = (index, notRemovedOwners, values) =>
|
|||
return initialValues
|
||||
}
|
||||
|
||||
const SafeOwners = (props) => {
|
||||
const { classes, errors, form, otherAccounts, values } = props
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const SafeOwnersForm = (props): React.ReactElement => {
|
||||
const { errors, form, otherAccounts, values } = props
|
||||
const classes = useStyles()
|
||||
|
||||
const validOwners = getNumOwnersFrom(values)
|
||||
const addressBook = useSelector(addressBookSelector)
|
||||
|
||||
const [numOwners, setNumOwners] = useState(validOwners)
|
||||
const [qrModalOpen, setQrModalOpen] = useState(false)
|
||||
|
@ -125,6 +130,7 @@ const SafeOwners = (props) => {
|
|||
<Block margin="md" padding="md">
|
||||
{[...Array(Number(numOwners))].map((x, index) => {
|
||||
const addressName = getOwnerAddressBy(index)
|
||||
const ownerName = getOwnerNameBy(index)
|
||||
|
||||
return (
|
||||
<Row className={classes.owner} key={`owner${index}`} data-testid={`create-safe-owner-row`}>
|
||||
|
@ -132,7 +138,7 @@ const SafeOwners = (props) => {
|
|||
<Field
|
||||
className={classes.name}
|
||||
component={TextField}
|
||||
name={getOwnerNameBy(index)}
|
||||
name={ownerName}
|
||||
placeholder="Owner Name*"
|
||||
text="Owner Name"
|
||||
type="text"
|
||||
|
@ -142,8 +148,14 @@ const SafeOwners = (props) => {
|
|||
</Col>
|
||||
<Col className={classes.ownerAddress} xs={6}>
|
||||
<AddressInput
|
||||
fieldMutator={(val) => {
|
||||
form.mutators.setValue(addressName, val)
|
||||
fieldMutator={(newOwnerAddress) => {
|
||||
const newOwnerName = getNameFromAddressBook(addressBook, newOwnerAddress, {
|
||||
filterOnlyValidName: true,
|
||||
})
|
||||
form.mutators.setValue(addressName, newOwnerAddress)
|
||||
if (newOwnerName) {
|
||||
form.mutators.setValue(ownerName, newOwnerName)
|
||||
}
|
||||
}}
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
|
@ -224,8 +236,6 @@ const SafeOwners = (props) => {
|
|||
)
|
||||
}
|
||||
|
||||
const SafeOwnersForm = withStyles(styles as any)(withRouter(SafeOwners))
|
||||
|
||||
const SafeOwnersPage = ({ updateInitialProps }) =>
|
||||
function OpenSafeOwnersPage(controls, { errors, form, values }) {
|
||||
return (
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { disabled, extraSmallFontSize, lg, md, screenSm, sm } from 'src/theme/variables'
|
||||
import { createStyles } from '@material-ui/core'
|
||||
|
||||
export const styles = () => ({
|
||||
export const styles = createStyles({
|
||||
root: {
|
||||
display: 'flex',
|
||||
},
|
||||
|
|
|
@ -2,15 +2,9 @@ import { Loader } from '@gnosis.pm/safe-react-components'
|
|||
import queryString from 'query-string'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import { connect } from 'react-redux'
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom'
|
||||
|
||||
import Opening from '../../opening'
|
||||
import Layout from '../components/Layout'
|
||||
|
||||
import actions from './actions'
|
||||
import selector from './selector'
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import Opening from 'src/routes/opening'
|
||||
import Layout from 'src/routes/open/components/Layout'
|
||||
import Page from 'src/components/layout/Page'
|
||||
import { getSafeDeploymentTransaction } from 'src/logic/contracts/safeContracts'
|
||||
import { checkReceiptStatus } from 'src/logic/wallets/ethTransactions'
|
||||
|
@ -25,6 +19,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 { 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'
|
||||
|
||||
const SAFE_PENDING_CREATION_STORAGE_KEY = 'SAFE_PENDING_CREATION_STORAGE_KEY'
|
||||
|
||||
|
@ -39,13 +36,15 @@ const validateQueryParams = (ownerAddresses, ownerNames, threshold, safeName) =>
|
|||
if (Number.isNaN(Number(threshold))) {
|
||||
return false
|
||||
}
|
||||
if (threshold > ownerAddresses.length) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return threshold <= ownerAddresses.length
|
||||
}
|
||||
|
||||
export const getSafeProps = async (safeAddress, safeName, ownersNames, ownerAddresses) => {
|
||||
export const getSafeProps = async (
|
||||
safeAddress: string,
|
||||
safeName: string,
|
||||
ownersNames: string[],
|
||||
ownerAddresses: string[],
|
||||
): Promise<SafeRecordProps> => {
|
||||
const safeProps = await buildSafe(safeAddress, safeName)
|
||||
const owners = getOwnersFrom(ownersNames, ownerAddresses)
|
||||
safeProps.owners = owners
|
||||
|
@ -81,19 +80,14 @@ export const createSafe = (values, userAccount) => {
|
|||
return promiEvent
|
||||
}
|
||||
|
||||
interface OwnProps extends RouteComponentProps {
|
||||
userAccount: string
|
||||
network: string
|
||||
provider: string
|
||||
addSafe: any
|
||||
}
|
||||
|
||||
const Open = ({ addSafe, network, provider, userAccount }: OwnProps): React.ReactElement => {
|
||||
const Open = (): React.ReactElement => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [showProgress, setShowProgress] = useState(false)
|
||||
const [creationTxPromise, setCreationTxPromise] = useState()
|
||||
const [safeCreationPendingInfo, setSafeCreationPendingInfo] = useState<any>()
|
||||
const [safePropsFromUrl, setSafePropsFromUrl] = useState()
|
||||
const userAccount = useSelector(userAccountSelector)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
// #122: Allow to migrate an old Multisig by passing the parameters to the URL.
|
||||
|
@ -141,14 +135,15 @@ const Open = ({ addSafe, network, provider, userAccount }: OwnProps): React.Reac
|
|||
setShowProgress(true)
|
||||
}
|
||||
|
||||
const onSafeCreated = async (safeAddress) => {
|
||||
const onSafeCreated = async (safeAddress): Promise<void> => {
|
||||
const pendingCreation = await loadFromStorage<{ txHash: string }>(SAFE_PENDING_CREATION_STORAGE_KEY)
|
||||
|
||||
const name = getSafeNameFrom(pendingCreation)
|
||||
const ownersNames = getNamesFrom(pendingCreation)
|
||||
const ownerAddresses = getAccountsFrom(pendingCreation)
|
||||
const safeProps = await getSafeProps(safeAddress, name, ownersNames, ownerAddresses)
|
||||
addSafe(safeProps)
|
||||
|
||||
await dispatch(addOrUpdateSafe(safeProps))
|
||||
|
||||
ReactGA.event({
|
||||
category: 'User',
|
||||
|
@ -194,21 +189,14 @@ const Open = ({ addSafe, network, provider, userAccount }: OwnProps): React.Reac
|
|||
creationTxHash={safeCreationPendingInfo?.txHash}
|
||||
onCancel={onCancel}
|
||||
onRetry={onRetry}
|
||||
onSuccess={onSafeCreated as any}
|
||||
provider={provider}
|
||||
onSuccess={onSafeCreated}
|
||||
submittedPromise={creationTxPromise}
|
||||
/>
|
||||
) : (
|
||||
<Layout
|
||||
network={network}
|
||||
onCallSafeContractSubmit={createSafeProxy}
|
||||
provider={provider}
|
||||
safeProps={safePropsFromUrl}
|
||||
userAccount={userAccount}
|
||||
/>
|
||||
<Layout onCallSafeContractSubmit={createSafeProxy} safeProps={safePropsFromUrl} />
|
||||
)}
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(selector, actions)(withRouter(Open))
|
||||
export default Open
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import addSafe from 'src/logic/safe/store/actions/addSafe'
|
||||
|
||||
export default {
|
||||
addSafe,
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import { createStructuredSelector } from 'reselect'
|
||||
|
||||
import { networkSelector, providerNameSelector, userAccountSelector } from 'src/logic/wallets/store/selectors'
|
||||
|
||||
export default createStructuredSelector({
|
||||
provider: providerNameSelector,
|
||||
network: networkSelector,
|
||||
userAccount: userAccountSelector,
|
||||
})
|
|
@ -2,7 +2,7 @@ import { Loader, Stepper } from '@gnosis.pm/safe-react-components'
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { ErrorFooter } from './components/Footer'
|
||||
import { ErrorFooter } from 'src/routes/opening/components/Footer'
|
||||
import { isConfirmationStep, steps } from './steps'
|
||||
|
||||
import Button from 'src/components/layout/Button'
|
||||
|
@ -13,6 +13,8 @@ import { initContracts } from 'src/logic/contracts/safeContracts'
|
|||
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { background, connected } from 'src/theme/variables'
|
||||
import { providerNameSelector } from 'src/logic/wallets/store/selectors'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
const loaderDotsSvg = require('./assets/loader-dots.svg')
|
||||
const successSvg = require('./assets/success.svg')
|
||||
|
@ -102,16 +104,17 @@ const BackButton = styled(Button)`
|
|||
// onCancel: () => void
|
||||
// }
|
||||
|
||||
const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, provider, submittedPromise }: any) => {
|
||||
const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, submittedPromise }): React.ReactElement => {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [stepIndex, setStepIndex] = useState(0)
|
||||
const [safeCreationTxHash, setSafeCreationTxHash] = useState('')
|
||||
const [createdSafeAddress, setCreatedSafeAddress] = useState()
|
||||
const [createdSafeAddress, setCreatedSafeAddress] = useState('')
|
||||
|
||||
const [error, setError] = useState(false)
|
||||
const [intervalStarted, setIntervalStarted] = useState(false)
|
||||
const [waitingSafeDeployed, setWaitingSafeDeployed] = useState(false)
|
||||
const [continueButtonDisabled, setContinueButtonDisabled] = useState(false)
|
||||
const provider = useSelector(providerNameSelector)
|
||||
|
||||
const confirmationStep = isConfirmationStep(stepIndex)
|
||||
|
||||
|
|
|
@ -19,8 +19,8 @@ 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 { getAddressBook } from 'src/logic/addressBook/store/selectors'
|
||||
import { getAddressesListFromSafeAddressBook } from 'src/logic/addressBook/utils'
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { getAddressesListFromAddressBook } from 'src/logic/addressBook/utils'
|
||||
|
||||
export const CREATE_ENTRY_INPUT_NAME_ID = 'create-entry-input-name'
|
||||
export const CREATE_ENTRY_INPUT_ADDRESS_ID = 'create-entry-input-address'
|
||||
|
@ -42,8 +42,8 @@ const CreateEditEntryModalComponent = ({
|
|||
}
|
||||
}
|
||||
|
||||
const addressBook = useSelector(getAddressBook)
|
||||
const addressBookAddressesList = getAddressesListFromSafeAddressBook(addressBook)
|
||||
const addressBook = useSelector(addressBookSelector)
|
||||
const addressBookAddressesList = getAddressesListFromAddressBook(addressBook)
|
||||
const entryDoesntExist = uniqueAddress(addressBookAddressesList)
|
||||
|
||||
const formMutators = {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { List } from 'immutable'
|
||||
import { TableCellProps } from '@material-ui/core/TableCell/TableCell'
|
||||
|
||||
export const ADDRESS_BOOK_ROW_ID = 'address-book-row'
|
||||
export const TX_TABLE_ADDRESS_BOOK_ID = 'idAddressBook'
|
||||
|
@ -9,7 +10,17 @@ export const EDIT_ENTRY_BUTTON = 'edit-entry-btn'
|
|||
export const REMOVE_ENTRY_BUTTON = 'remove-entry-btn'
|
||||
export const SEND_ENTRY_BUTTON = 'send-entry-btn'
|
||||
|
||||
export const generateColumns = () => {
|
||||
type AddressBookColumn = {
|
||||
id: string
|
||||
order: boolean
|
||||
disablePadding?: boolean
|
||||
label: string
|
||||
width?: number
|
||||
custom?: boolean
|
||||
align?: TableCellProps['align']
|
||||
}
|
||||
|
||||
export const generateColumns = (): List<AddressBookColumn> => {
|
||||
const nameColumn = {
|
||||
id: AB_NAME_ID,
|
||||
order: false,
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import TableCell from '@material-ui/core/TableCell'
|
||||
import TableContainer from '@material-ui/core/TableContainer'
|
||||
import TableRow from '@material-ui/core/TableRow'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
// import CallMade from '@material-ui/icons/CallMade'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import cn from 'classnames'
|
||||
// import classNames from 'classnames/bind'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
|
@ -18,11 +16,11 @@ 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 { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { AddressBookEntry, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { addAddressBookEntry } from 'src/logic/addressBook/store/actions/addAddressBookEntry'
|
||||
import { removeAddressBookEntry } from 'src/logic/addressBook/store/actions/removeAddressBookEntry'
|
||||
import { updateAddressBookEntry } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
|
||||
import { getAddressBook } from 'src/logic/addressBook/store/selectors'
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { isUserAnOwnerOfAnySafe } from 'src/logic/wallets/ethAddresses'
|
||||
import CreateEditEntryModal from 'src/routes/safe/components/AddressBook/CreateEditEntryModal'
|
||||
import DeleteEntryModal from 'src/routes/safe/components/AddressBook/DeleteEntryModal'
|
||||
|
@ -38,19 +36,32 @@ import SendModal from 'src/routes/safe/components/Balances/SendModal'
|
|||
import OwnerAddressTableCell from 'src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell'
|
||||
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 RemoveOwnerIconDisabled from 'src/routes/safe/components/Settings/assets/icons/disabled-bin.svg'
|
||||
import { addressBookQueryParamsSelector, safesListSelector } from 'src/logic/safe/store/selectors'
|
||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||
import { grantedSelector } from 'src/routes/safe/container/selector'
|
||||
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
|
||||
import { getValidAddressBookName } from 'src/logic/addressBook/utils'
|
||||
|
||||
const AddressBookTable = ({ classes }) => {
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
interface AddressBookSelectedEntry extends AddressBookEntry {
|
||||
isNew?: boolean
|
||||
}
|
||||
|
||||
const AddressBookTable = (): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const columns = generateColumns()
|
||||
const autoColumns = columns.filter((c) => !c.custom)
|
||||
const dispatch = useDispatch()
|
||||
const safesList = useSelector(safesListSelector)
|
||||
const entryAddressToEditOrCreateNew = useSelector(addressBookQueryParamsSelector)
|
||||
const addressBook = useSelector(getAddressBook)
|
||||
const [selectedEntry, setSelectedEntry] = useState<any>(null)
|
||||
const addressBook = useSelector(addressBookSelector)
|
||||
const granted = useSelector(grantedSelector)
|
||||
const [selectedEntry, setSelectedEntry] = useState<{
|
||||
entry?: AddressBookSelectedEntry
|
||||
index?: number
|
||||
isOwnerAddress?: boolean
|
||||
} | null>(null)
|
||||
const [editCreateEntryModalOpen, setEditCreateEntryModalOpen] = useState(false)
|
||||
const [deleteEntryModalOpen, setDeleteEntryModalOpen] = useState(false)
|
||||
const [sendFundsModalOpen, setSendFundsModalOpen] = useState(false)
|
||||
|
@ -69,11 +80,10 @@ const AddressBookTable = ({ classes }) => {
|
|||
useEffect(() => {
|
||||
if (entryAddressToEditOrCreateNew) {
|
||||
const checksumEntryAdd = checksumAddress(entryAddressToEditOrCreateNew)
|
||||
const key = addressBook.findKey((entry) => entry.address === checksumEntryAdd)
|
||||
if (key && key >= 0) {
|
||||
const oldEntryIndex = addressBook.findIndex((entry) => entry.address === checksumEntryAdd)
|
||||
if (oldEntryIndex >= 0) {
|
||||
// Edit old entry
|
||||
const value = addressBook.get(key)
|
||||
setSelectedEntry({ entry: value, index: key })
|
||||
setSelectedEntry({ entry: addressBook[oldEntryIndex], index: oldEntryIndex })
|
||||
} else {
|
||||
// Create new entry
|
||||
setSelectedEntry({
|
||||
|
@ -107,7 +117,7 @@ const AddressBookTable = ({ classes }) => {
|
|||
}
|
||||
|
||||
const deleteEntryModalHandler = () => {
|
||||
const entryAddress = checksumAddress(selectedEntry.entry.address)
|
||||
const entryAddress = selectedEntry && selectedEntry.entry ? checksumAddress(selectedEntry.entry.address) : ''
|
||||
setSelectedEntry(null)
|
||||
setDeleteEntryModalOpen(false)
|
||||
dispatch(removeAddressBookEntry(entryAddress))
|
||||
|
@ -138,7 +148,7 @@ const AddressBookTable = ({ classes }) => {
|
|||
defaultRowsPerPage={25}
|
||||
disableLoadingOnEmptyTable
|
||||
label="Owners"
|
||||
size={addressBook?.size || 0}
|
||||
size={addressBook?.length || 0}
|
||||
>
|
||||
{(sortedData) =>
|
||||
sortedData.map((row, index) => {
|
||||
|
@ -151,20 +161,22 @@ const AddressBookTable = ({ classes }) => {
|
|||
key={index}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{autoColumns.map((column: any) => (
|
||||
<TableCell align={column.align} component="td" key={column.id} style={cellWidth(column.width)}>
|
||||
{column.id === AB_ADDRESS_ID ? (
|
||||
<OwnerAddressTableCell address={row[column.id]} showLinks />
|
||||
) : (
|
||||
row[column.id]
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
{autoColumns.map((column) => {
|
||||
return (
|
||||
<TableCell align={column.align} component="td" key={column.id} style={cellWidth(column.width)}>
|
||||
{column.id === AB_ADDRESS_ID ? (
|
||||
<OwnerAddressTableCell address={row[column.id]} showLinks />
|
||||
) : (
|
||||
getValidAddressBookName(row[column.id])
|
||||
)}
|
||||
</TableCell>
|
||||
)
|
||||
})}
|
||||
<TableCell component="td">
|
||||
<Row align="end" className={classes.actions}>
|
||||
<Img
|
||||
alt="Edit entry"
|
||||
className={classes.editEntryButton}
|
||||
className={granted ? classes.editEntryButton : classes.editEntryButtonNonOwner}
|
||||
onClick={() => {
|
||||
setSelectedEntry({
|
||||
entry: row,
|
||||
|
@ -177,33 +189,29 @@ const AddressBookTable = ({ classes }) => {
|
|||
/>
|
||||
<Img
|
||||
alt="Remove entry"
|
||||
className={userOwner ? classes.removeEntryButtonDisabled : classes.removeEntryButton}
|
||||
onClick={() => {
|
||||
if (!userOwner) {
|
||||
setSelectedEntry({ entry: row })
|
||||
setDeleteEntryModalOpen(true)
|
||||
}
|
||||
}}
|
||||
src={userOwner ? RemoveOwnerIconDisabled : RemoveOwnerIcon}
|
||||
testId={REMOVE_ENTRY_BUTTON}
|
||||
/>
|
||||
<Button
|
||||
className={classes.send}
|
||||
color="primary"
|
||||
className={granted ? classes.removeEntryButton : classes.removeEntryButtonNonOwner}
|
||||
onClick={() => {
|
||||
setSelectedEntry({ entry: row })
|
||||
setSendFundsModalOpen(true)
|
||||
setDeleteEntryModalOpen(true)
|
||||
}}
|
||||
size="small"
|
||||
testId={SEND_ENTRY_BUTTON}
|
||||
variant="contained"
|
||||
>
|
||||
{/* <CallMade
|
||||
alt="Send Transaction"
|
||||
className={classNames(classes.leftIcon, classes.iconSmall)}
|
||||
/> */}
|
||||
Send
|
||||
</Button>
|
||||
src={RemoveOwnerIcon}
|
||||
testId={REMOVE_ENTRY_BUTTON}
|
||||
/>
|
||||
{granted ? (
|
||||
<Button
|
||||
className={classes.send}
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setSelectedEntry({ entry: row })
|
||||
setSendFundsModalOpen(true)
|
||||
}}
|
||||
size="small"
|
||||
testId={SEND_ENTRY_BUTTON}
|
||||
variant="contained"
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
) : null}
|
||||
</Row>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
@ -236,4 +244,4 @@ const AddressBookTable = ({ classes }) => {
|
|||
)
|
||||
}
|
||||
|
||||
export default withStyles(styles as any)(AddressBookTable)
|
||||
export default AddressBookTable
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { lg, marginButtonImg, md, sm } from 'src/theme/variables'
|
||||
import { createStyles } from '@material-ui/core'
|
||||
|
||||
export const styles = () => ({
|
||||
export const styles = createStyles({
|
||||
formContainer: {
|
||||
minHeight: '250px',
|
||||
},
|
||||
|
@ -38,6 +39,9 @@ export const styles = () => ({
|
|||
cursor: 'pointer',
|
||||
marginBottom: marginButtonImg,
|
||||
},
|
||||
editEntryButtonNonOwner: {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
removeEntryButton: {
|
||||
marginLeft: lg,
|
||||
marginRight: lg,
|
||||
|
@ -50,6 +54,11 @@ export const styles = () => ({
|
|||
marginBottom: marginButtonImg,
|
||||
cursor: 'default',
|
||||
},
|
||||
removeEntryButtonNonOwner: {
|
||||
marginLeft: lg,
|
||||
marginRight: lg,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
message: {
|
||||
padding: `${md} 0`,
|
||||
maxHeight: '54px',
|
||||
|
|
|
@ -69,9 +69,8 @@ type OwnProps = {
|
|||
safeAddress: string
|
||||
safeName: string
|
||||
ethBalance: string
|
||||
onCancel: () => void
|
||||
onUserConfirm: (safeTxHash: string) => void
|
||||
onUserTxReject: () => void
|
||||
onTxReject: () => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
|
@ -82,16 +81,20 @@ const ConfirmTransactionModal = ({
|
|||
safeAddress,
|
||||
ethBalance,
|
||||
safeName,
|
||||
onCancel,
|
||||
onUserConfirm,
|
||||
onClose,
|
||||
onUserTxReject,
|
||||
onTxReject,
|
||||
}: OwnProps): React.ReactElement | null => {
|
||||
const dispatch = useDispatch()
|
||||
if (!isOpen) {
|
||||
return null
|
||||
}
|
||||
|
||||
const handleTxRejection = () => {
|
||||
onTxReject()
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleUserConfirmation = (safeTxHash: string): void => {
|
||||
onUserConfirm(safeTxHash)
|
||||
onClose()
|
||||
|
@ -113,10 +116,9 @@ const ConfirmTransactionModal = ({
|
|||
navigateToTransactionsTab: false,
|
||||
},
|
||||
handleUserConfirmation,
|
||||
onUserTxReject,
|
||||
handleTxRejection,
|
||||
),
|
||||
)
|
||||
onClose()
|
||||
}
|
||||
|
||||
const areTxsMalformed = txs.some((t) => !isTxValid(t))
|
||||
|
@ -165,13 +167,13 @@ const ConfirmTransactionModal = ({
|
|||
footer={
|
||||
<ModalFooterConfirmation
|
||||
cancelText="Cancel"
|
||||
handleCancel={onCancel}
|
||||
handleCancel={handleTxRejection}
|
||||
handleOk={confirmTransactions}
|
||||
okDisabled={areTxsMalformed}
|
||||
okText="Submit"
|
||||
/>
|
||||
}
|
||||
onClose={onClose}
|
||||
onClose={handleTxRejection}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
} from '@gnosis.pm/safe-apps-sdk'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { useEffect, useCallback, MutableRefObject } from 'react'
|
||||
import { getTxServiceHost } from 'src/config/'
|
||||
import {
|
||||
safeEthBalanceSelector,
|
||||
safeNameSelector,
|
||||
|
@ -85,7 +86,7 @@ const useIframeMessageHandler = (
|
|||
}
|
||||
|
||||
case SDK_MESSAGES.SAFE_APP_SDK_INITIALIZED: {
|
||||
const message = {
|
||||
const safeInfoMessage = {
|
||||
messageId: INTERFACE_MESSAGES.ON_SAFE_INFO,
|
||||
data: {
|
||||
safeAddress: safeAddress as string,
|
||||
|
@ -93,8 +94,15 @@ const useIframeMessageHandler = (
|
|||
ethBalance: ethBalance as string,
|
||||
},
|
||||
}
|
||||
const envInfoMessage = {
|
||||
messageId: INTERFACE_MESSAGES.ENV_INFO,
|
||||
data: {
|
||||
txServiceUrl: getTxServiceHost(),
|
||||
},
|
||||
}
|
||||
|
||||
sendMessageToIframe(message)
|
||||
sendMessageToIframe(safeInfoMessage)
|
||||
sendMessageToIframe(envInfoMessage)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
|
|
|
@ -102,7 +102,7 @@ const Apps = (): React.ReactElement => {
|
|||
)
|
||||
}
|
||||
|
||||
const onUserTxReject = () => {
|
||||
const onTxReject = () => {
|
||||
sendMessageToIframe(
|
||||
{ messageId: INTERFACE_MESSAGES.TRANSACTION_REJECTED, data: {} },
|
||||
confirmTransactionModal.requestId,
|
||||
|
@ -212,10 +212,9 @@ const Apps = (): React.ReactElement => {
|
|||
ethBalance={ethBalance as string}
|
||||
safeName={safeName as string}
|
||||
txs={confirmTransactionModal.txs}
|
||||
onCancel={closeConfirmationModal}
|
||||
onClose={closeConfirmationModal}
|
||||
onUserConfirm={onUserTxConfirm}
|
||||
onUserTxReject={onUserTxReject}
|
||||
onTxReject={onTxReject}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -38,6 +38,10 @@ export const staticAppsList: Array<{ url: string; disabled: boolean }> = [
|
|||
{ url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmQovvfYYMUXjZfNbysQDUEXR8nr55iJRwcYgJQGJR7KEA`, disabled: false },
|
||||
// TX-Builder
|
||||
{ url: `${gnosisAppsUrl}/tx-builder`, disabled: false },
|
||||
// Wallet-Connect
|
||||
{ url: `${gnosisAppsUrl}/walletConnect`, disabled: false },
|
||||
// Yearn Vaults
|
||||
{ url: `${process.env.REACT_APP_IPFS_GATEWAY}/Qme9HuPPhgCtgfj1CktvaDKhTesMueGCV2Kui1Sqna3Xs9`, disabled: false },
|
||||
]
|
||||
|
||||
export const getAppInfoFromOrigin = (origin: string): Record<string, string> | null => {
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import MuiTextField from '@material-ui/core/TextField'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import makeStyles from '@material-ui/core/styles/makeStyles'
|
||||
import Autocomplete from '@material-ui/lab/Autocomplete'
|
||||
import { List } from 'immutable'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { trimSpaces } from 'src/utils/strings'
|
||||
|
@ -11,10 +9,10 @@ import { styles } from './style'
|
|||
|
||||
import Identicon from 'src/components/Identicon'
|
||||
import { mustBeEthereumAddress, mustBeEthereumContractAddress } from 'src/components/forms/validator'
|
||||
import { getAddressBook } from 'src/logic/addressBook/store/selectors'
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { getAddressFromENS } from 'src/logic/wallets/getWeb3'
|
||||
import { isValidEnsName } from 'src/logic/wallets/ethAddresses'
|
||||
import { AddressBookEntryRecord } from 'src/logic/addressBook/model/addressBook'
|
||||
import { AddressBookEntry, AddressBookState } from 'src/logic/addressBook/model/addressBook'
|
||||
|
||||
export interface AddressBookProps {
|
||||
fieldMutator: (address: string) => void
|
||||
|
@ -22,7 +20,7 @@ export interface AddressBookProps {
|
|||
pristine: boolean
|
||||
recipientAddress?: string
|
||||
setSelectedEntry: (
|
||||
entry: { address?: string; name?: string } | React.SetStateAction<{ address?: string; name?: string }> | null,
|
||||
entry: { address?: string; name?: string } | React.SetStateAction<{ address?: string; name? }> | null,
|
||||
) => void
|
||||
setIsValidAddress: (valid: boolean) => void
|
||||
}
|
||||
|
@ -45,12 +43,10 @@ const textFieldInputStyle = makeStyles(() => ({
|
|||
},
|
||||
}))
|
||||
|
||||
const filterAddressBookWithContractAddresses = async (
|
||||
addressBook: List<AddressBookEntryRecord>,
|
||||
): Promise<List<AddressBookEntryRecord>> => {
|
||||
const filterAddressBookWithContractAddresses = async (addressBook: AddressBookState): Promise<AddressBookEntry[]> => {
|
||||
const abFlags = await Promise.all(
|
||||
addressBook.map(
|
||||
async ({ address }: AddressBookEntryRecord): Promise<boolean> => {
|
||||
async ({ address }: AddressBookEntry): Promise<boolean> => {
|
||||
return (await mustBeEthereumContractAddress(address)) === undefined
|
||||
},
|
||||
),
|
||||
|
@ -59,11 +55,6 @@ const filterAddressBookWithContractAddresses = async (
|
|||
return addressBook.filter((_, index) => abFlags[index])
|
||||
}
|
||||
|
||||
interface FilteredAddressBookEntry {
|
||||
name: string
|
||||
address: string
|
||||
}
|
||||
|
||||
const AddressBookInput = ({
|
||||
fieldMutator,
|
||||
isCustomTx,
|
||||
|
@ -71,22 +62,23 @@ const AddressBookInput = ({
|
|||
recipientAddress,
|
||||
setIsValidAddress,
|
||||
setSelectedEntry,
|
||||
}: AddressBookProps) => {
|
||||
}: AddressBookProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const addressBook = useSelector(getAddressBook)
|
||||
const addressBook = useSelector(addressBookSelector)
|
||||
const [isValidForm, setIsValidForm] = useState(true)
|
||||
const [validationText, setValidationText] = useState<string>('')
|
||||
const [inputTouched, setInputTouched] = useState(false)
|
||||
const [blurred, setBlurred] = useState(pristine)
|
||||
const [adbkList, setADBKList] = useState<List<FilteredAddressBookEntry>>(List([]))
|
||||
const [adbkList, setADBKList] = useState<AddressBookEntry[]>([])
|
||||
|
||||
const [inputAddValue, setInputAddValue] = useState(recipientAddress)
|
||||
|
||||
const onAddressInputChanged = async (value: string): Promise<void> => {
|
||||
const normalizedAddress = trimSpaces(value)
|
||||
const isENSDomain = isValidEnsName(normalizedAddress)
|
||||
setInputAddValue(normalizedAddress)
|
||||
let resolvedAddress = normalizedAddress
|
||||
let isValidText
|
||||
let addressErrorMessage
|
||||
if (inputTouched && !normalizedAddress) {
|
||||
setIsValidForm(false)
|
||||
setValidationText('Required')
|
||||
|
@ -94,13 +86,14 @@ const AddressBookInput = ({
|
|||
return
|
||||
}
|
||||
if (normalizedAddress) {
|
||||
if (isValidEnsName(normalizedAddress)) {
|
||||
if (isENSDomain) {
|
||||
resolvedAddress = await getAddressFromENS(normalizedAddress)
|
||||
setInputAddValue(resolvedAddress)
|
||||
}
|
||||
isValidText = mustBeEthereumAddress(resolvedAddress)
|
||||
if (isCustomTx && isValidText === undefined) {
|
||||
isValidText = await mustBeEthereumContractAddress(resolvedAddress)
|
||||
|
||||
addressErrorMessage = mustBeEthereumAddress(resolvedAddress)
|
||||
if (isCustomTx && addressErrorMessage === undefined) {
|
||||
addressErrorMessage = await mustBeEthereumContractAddress(resolvedAddress)
|
||||
}
|
||||
|
||||
// First removes the entries that are not contracts if the operation is custom tx
|
||||
|
@ -110,18 +103,31 @@ const AddressBookInput = ({
|
|||
const { address, name } = adbkEntry
|
||||
return (
|
||||
name.toLowerCase().includes(normalizedAddress.toLowerCase()) ||
|
||||
address.toLowerCase().includes(normalizedAddress.toLowerCase())
|
||||
address.toLowerCase().includes(resolvedAddress.toLowerCase())
|
||||
)
|
||||
})
|
||||
setADBKList(filteredADBK)
|
||||
if (!isValidText) {
|
||||
setSelectedEntry({ address: normalizedAddress })
|
||||
if (!addressErrorMessage) {
|
||||
// base case if isENSDomain we set the domain as the name
|
||||
// if address does not exist in address book we use blank name
|
||||
let addressName = isENSDomain ? normalizedAddress : ''
|
||||
|
||||
// if address is valid, and is in the address book, then we use the stored values
|
||||
if (filteredADBK.length === 1) {
|
||||
const addressBookContact = filteredADBK[0]
|
||||
addressName = addressBookContact.name ?? addressName
|
||||
}
|
||||
|
||||
setSelectedEntry({
|
||||
name: addressName,
|
||||
address: resolvedAddress,
|
||||
})
|
||||
}
|
||||
}
|
||||
setIsValidForm(isValidText === undefined)
|
||||
setValidationText(isValidText)
|
||||
setIsValidForm(addressErrorMessage === undefined)
|
||||
setValidationText(addressErrorMessage)
|
||||
fieldMutator(resolvedAddress)
|
||||
setIsValidAddress(isValidText === undefined)
|
||||
setIsValidAddress(addressErrorMessage === undefined)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -164,7 +170,7 @@ const AddressBookInput = ({
|
|||
freeSolo
|
||||
getOptionLabel={(adbkEntry) => adbkEntry.address || ''}
|
||||
id="free-solo-demo"
|
||||
onChange={(_, value: FilteredAddressBookEntry) => {
|
||||
onChange={(_, value: AddressBookEntry) => {
|
||||
let address = ''
|
||||
let name = ''
|
||||
if (value) {
|
||||
|
@ -180,7 +186,7 @@ const AddressBookInput = ({
|
|||
setBlurred(false)
|
||||
}}
|
||||
open={!blurred}
|
||||
options={adbkList.toArray()}
|
||||
options={adbkList}
|
||||
renderInput={(params) => (
|
||||
<MuiTextField
|
||||
{...params}
|
||||
|
@ -237,4 +243,4 @@ const AddressBookInput = ({
|
|||
)
|
||||
}
|
||||
|
||||
export default withStyles(styles as any)(AddressBookInput)
|
||||
export default AddressBookInput
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
import IconButton from '@material-ui/core/IconButton'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import classNames from 'classnames/bind'
|
||||
import * as React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import Collectible from '../assets/collectibles.svg'
|
||||
import Token from '../assets/token.svg'
|
||||
|
||||
import { mustBeEthereumContractAddress } from 'src/components/forms/validator'
|
||||
import Button from 'src/components/layout/Button'
|
||||
import Col from 'src/components/layout/Col'
|
||||
|
@ -15,54 +11,24 @@ import Hairline from 'src/components/layout/Hairline'
|
|||
import Img from 'src/components/layout/Img'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { safeFeaturesEnabledSelector } from 'src/logic/safe/store/selectors'
|
||||
import { useStyles } from 'src/routes/safe/components/Balances/SendModal/screens/ChooseTxType/style'
|
||||
import ContractInteractionIcon from 'src/routes/safe/components/Transactions/TxsTable/TxType/assets/custom.svg'
|
||||
import { safeSelector } from 'src/logic/safe/store/selectors'
|
||||
import { lg, md, sm } from 'src/theme/variables'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
heading: {
|
||||
padding: `${md} ${lg}`,
|
||||
justifyContent: 'space-between',
|
||||
boxSizing: 'border-box',
|
||||
maxHeight: '75px',
|
||||
},
|
||||
manage: {
|
||||
fontSize: lg,
|
||||
},
|
||||
disclaimer: {
|
||||
marginBottom: `-${md}`,
|
||||
paddingTop: md,
|
||||
textAlign: 'center',
|
||||
},
|
||||
disclaimerText: {
|
||||
fontSize: md,
|
||||
},
|
||||
closeIcon: {
|
||||
height: '35px',
|
||||
width: '35px',
|
||||
},
|
||||
buttonColumn: {
|
||||
padding: '52px 0',
|
||||
'& > button': {
|
||||
fontSize: md,
|
||||
fontFamily: 'Averta',
|
||||
},
|
||||
},
|
||||
firstButton: {
|
||||
boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)',
|
||||
marginBottom: 15,
|
||||
},
|
||||
iconSmall: {
|
||||
fontSize: 16,
|
||||
},
|
||||
leftIcon: {
|
||||
marginRight: sm,
|
||||
},
|
||||
})
|
||||
import Collectible from '../assets/collectibles.svg'
|
||||
import Token from '../assets/token.svg'
|
||||
|
||||
const ChooseTxType = ({ onClose, recipientAddress, setActiveScreen }) => {
|
||||
type ActiveScreen = 'sendFunds' | 'sendCollectible' | 'contractInteraction'
|
||||
|
||||
interface ChooseTxTypeProps {
|
||||
onClose: () => void
|
||||
recipientAddress: string
|
||||
setActiveScreen: React.Dispatch<React.SetStateAction<ActiveScreen>>
|
||||
}
|
||||
|
||||
const ChooseTxType = ({ onClose, recipientAddress, setActiveScreen }: ChooseTxTypeProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const { featuresEnabled } = useSelector(safeSelector) || {}
|
||||
const featuresEnabled = useSelector(safeFeaturesEnabledSelector)
|
||||
const erc721Enabled = featuresEnabled?.includes('ERC721')
|
||||
const [disableContractInteraction, setDisableContractInteraction] = React.useState(!!recipientAddress)
|
||||
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
||||
import { lg, md, sm } from 'src/theme/variables'
|
||||
|
||||
export const useStyles = makeStyles(
|
||||
createStyles({
|
||||
heading: {
|
||||
padding: `${md} ${lg}`,
|
||||
justifyContent: 'space-between',
|
||||
boxSizing: 'border-box',
|
||||
maxHeight: '75px',
|
||||
},
|
||||
manage: {
|
||||
fontSize: lg,
|
||||
},
|
||||
disclaimer: {
|
||||
marginBottom: `-${md}`,
|
||||
paddingTop: md,
|
||||
textAlign: 'center',
|
||||
},
|
||||
disclaimerText: {
|
||||
fontSize: md,
|
||||
},
|
||||
closeIcon: {
|
||||
height: '35px',
|
||||
width: '35px',
|
||||
},
|
||||
buttonColumn: {
|
||||
padding: '52px 0',
|
||||
'& > button': {
|
||||
fontSize: md,
|
||||
fontFamily: 'Averta',
|
||||
},
|
||||
},
|
||||
firstButton: {
|
||||
boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)',
|
||||
marginBottom: 15,
|
||||
},
|
||||
iconSmall: {
|
||||
fontSize: 16,
|
||||
},
|
||||
leftIcon: {
|
||||
marginRight: sm,
|
||||
},
|
||||
}),
|
||||
)
|
|
@ -20,8 +20,8 @@ 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 { getAddressBook } from 'src/logic/addressBook/store/selectors'
|
||||
import { getNameFromSafeAddressBook } from 'src/logic/addressBook/utils'
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { getNameFromAddressBook } from 'src/logic/addressBook/utils'
|
||||
import { nftTokensSelector, safeActiveSelectorMap } from 'src/logic/collectibles/store/selectors'
|
||||
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||
import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
|
||||
|
@ -41,14 +41,20 @@ const formMutators = {
|
|||
},
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(styles as any)
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, selectedToken = {} }) => {
|
||||
const SendCollectible = ({
|
||||
initialValues,
|
||||
onClose,
|
||||
onNext,
|
||||
recipientAddress,
|
||||
selectedToken = {},
|
||||
}): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const nftAssets = useSelector(safeActiveSelectorMap)
|
||||
const nftTokens = useSelector(nftTokensSelector)
|
||||
const addressBook = useSelector(getAddressBook)
|
||||
const [selectedEntry, setSelectedEntry] = useState({
|
||||
const addressBook = useSelector(addressBookSelector)
|
||||
const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string | null } | null>({
|
||||
address: recipientAddress || initialValues.recipientAddress,
|
||||
name: '',
|
||||
})
|
||||
|
@ -64,7 +70,7 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
|
|||
const handleSubmit = (values) => {
|
||||
// If the input wasn't modified, there was no mutation of the recipientAddress
|
||||
if (!values.recipientAddress) {
|
||||
values.recipientAddress = selectedEntry.address
|
||||
values.recipientAddress = selectedEntry?.address
|
||||
}
|
||||
|
||||
values.assetName = nftAssets[values.assetAddress].name
|
||||
|
@ -97,10 +103,10 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
|
|||
if (scannedAddress.startsWith('ethereum:')) {
|
||||
scannedAddress = scannedAddress.replace('ethereum:', '')
|
||||
}
|
||||
const scannedName = addressBook ? getNameFromSafeAddressBook(addressBook, scannedAddress) : ''
|
||||
const scannedName = addressBook ? getNameFromAddressBook(addressBook, scannedAddress) : ''
|
||||
mutators.setRecipient(scannedAddress)
|
||||
setSelectedEntry({
|
||||
name: scannedName || '',
|
||||
name: scannedName,
|
||||
address: scannedAddress,
|
||||
})
|
||||
closeQrModal()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { lg, md, secondaryText } from 'src/theme/variables'
|
||||
import { createStyles } from '@material-ui/core'
|
||||
|
||||
export const styles = () => ({
|
||||
export const styles = createStyles({
|
||||
heading: {
|
||||
padding: `${md} ${lg}`,
|
||||
justifyContent: 'flex-start',
|
||||
|
|
|
@ -25,8 +25,8 @@ 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 { getAddressBook } from 'src/logic/addressBook/store/selectors'
|
||||
import { getNameFromSafeAddressBook } from 'src/logic/addressBook/utils'
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { getNameFromAddressBook } from 'src/logic/addressBook/utils'
|
||||
|
||||
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||
import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
|
||||
|
@ -69,8 +69,8 @@ const SendFunds = ({
|
|||
}: SendFundsProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const tokens = useSelector(extendedSafeTokensSelector)
|
||||
const addressBook = useSelector(getAddressBook)
|
||||
const [selectedEntry, setSelectedEntry] = useState({
|
||||
const addressBook = useSelector(addressBookSelector)
|
||||
const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string | null } | null>({
|
||||
address: recipientAddress || initialValues.recipientAddress,
|
||||
name: '',
|
||||
})
|
||||
|
@ -88,7 +88,7 @@ const SendFunds = ({
|
|||
const submitValues = values
|
||||
// If the input wasn't modified, there was no mutation of the recipientAddress
|
||||
if (!values.recipientAddress) {
|
||||
submitValues.recipientAddress = selectedEntry.address
|
||||
submitValues.recipientAddress = selectedEntry?.address
|
||||
}
|
||||
onNext(submitValues)
|
||||
}
|
||||
|
@ -118,7 +118,7 @@ const SendFunds = ({
|
|||
if (scannedAddress.startsWith('ethereum:')) {
|
||||
scannedAddress = scannedAddress.replace('ethereum:', '')
|
||||
}
|
||||
const scannedName = addressBook ? getNameFromSafeAddressBook(addressBook, scannedAddress) : ''
|
||||
const scannedName = addressBook ? getNameFromAddressBook(addressBook, scannedAddress) : ''
|
||||
mutators.setRecipient(scannedAddress)
|
||||
setSelectedEntry({
|
||||
name: scannedName || '',
|
||||
|
|
|
@ -16,6 +16,7 @@ import createTransaction from 'src/logic/safe/store/actions/createTransaction'
|
|||
|
||||
import { safeOwnersSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
|
||||
const styles = () => ({
|
||||
biggerModalWindow: {
|
||||
|
@ -92,7 +93,7 @@ const AddOwner = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClose })
|
|||
try {
|
||||
await sendAddOwner(values, safeAddress, owners, enqueueSnackbar, closeSnackbar, dispatch)
|
||||
dispatch(
|
||||
addOrUpdateAddressBookEntry(values.ownerAddress, { name: values.ownerName, address: values.ownerAddress }),
|
||||
addOrUpdateAddressBookEntry(makeAddressBookEntry({ name: values.ownerName, address: values.ownerAddress })),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error while removing an owner', error)
|
||||
|
|
|
@ -20,12 +20,12 @@ import Hairline from 'src/components/layout/Hairline'
|
|||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { updateAddressBookEntry } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
|
||||
import { NOTIFICATIONS } from 'src/logic/notifications'
|
||||
import editSafeOwner from 'src/logic/safe/store/actions/editSafeOwner'
|
||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { sm } from 'src/theme/variables'
|
||||
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
|
||||
import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
|
||||
|
||||
export const RENAME_OWNER_INPUT_TEST_ID = 'rename-owner-input'
|
||||
export const SAVE_OWNER_CHANGES_BTN_TEST_ID = 'save-owner-changes-btn'
|
||||
|
@ -47,7 +47,7 @@ const EditOwnerComponent = ({ isOpen, onClose, ownerAddress, selectedOwnerName }
|
|||
const { ownerName } = values
|
||||
|
||||
dispatch(editSafeOwner({ safeAddress, ownerAddress, ownerName }))
|
||||
dispatch(updateAddressBookEntry(makeAddressBookEntry({ address: ownerAddress, name: ownerName })))
|
||||
dispatch(addOrUpdateAddressBookEntry(makeAddressBookEntry({ address: ownerAddress, name: ownerName })))
|
||||
dispatch(enqueueSnackbar(NOTIFICATIONS.OWNER_NAME_CHANGE_EXECUTED_MSG))
|
||||
|
||||
onClose()
|
||||
|
|
|
@ -6,6 +6,7 @@ import Block from 'src/components/layout/Block'
|
|||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import { useWindowDimensions } from 'src/logic/hooks/useWindowDimensions'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { getValidAddressBookName } from 'src/logic/addressBook/utils'
|
||||
|
||||
type OwnerAddressTableCellProps = {
|
||||
address: string
|
||||
|
@ -34,7 +35,7 @@ const OwnerAddressTableCell = (props: OwnerAddressTableCellProps): React.ReactEl
|
|||
<Identicon address={address} diameter={32} />
|
||||
{showLinks ? (
|
||||
<div style={{ marginLeft: 10, flexShrink: 1, minWidth: 0 }}>
|
||||
{!userName || userName === 'UNKNOWN' ? null : userName}
|
||||
{userName && getValidAddressBookName(userName)}
|
||||
<EtherScanLink knownAddress={knownAddress} type="address" value={address} cut={cut} />
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
@ -14,6 +14,7 @@ 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 { checksumAddress } from 'src/utils/checksumAddress'
|
||||
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
|
||||
const styles = () => ({
|
||||
biggerModalWindow: {
|
||||
|
@ -96,10 +97,7 @@ const ReplaceOwner = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClose
|
|||
await sendReplaceOwner(values, safeAddress, ownerAddress, enqueueSnackbar, closeSnackbar, threshold, dispatch)
|
||||
|
||||
dispatch(
|
||||
// Needs the `address` field because we need to provide the minimum required values to ADD a new entry
|
||||
// The reducer will update all the addressBooks stored, so we cannot decide what to do beforehand,
|
||||
// thus, we pass the minimum required fields (name and address)
|
||||
addOrUpdateAddressBookEntry(values.ownerAddress, { name: values.ownerName, address: values.ownerAddress }),
|
||||
addOrUpdateAddressBookEntry(makeAddressBookEntry({ address: values.ownerAddress, name: values.ownerName })),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error while removing an owner', error)
|
||||
|
|
|
@ -30,8 +30,8 @@ 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'
|
||||
import { AddressBookCollection } from 'src/logic/addressBook/store/reducer/addressBook'
|
||||
|
||||
export const RENAME_OWNER_BTN_TEST_ID = 'rename-owner-btn'
|
||||
export const REMOVE_OWNER_BTN_TEST_ID = 'remove-owner-btn'
|
||||
|
@ -42,7 +42,7 @@ export const OWNERS_ROW_TEST_ID = 'owners-row'
|
|||
const useStyles = makeStyles(styles)
|
||||
|
||||
type Props = {
|
||||
addressBook: unknown
|
||||
addressBook: AddressBookState
|
||||
granted: boolean
|
||||
owners: List<SafeOwner>
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ const ManageOwners = ({ addressBook, granted, owners }: Props): React.ReactEleme
|
|||
|
||||
const columns = generateColumns()
|
||||
const autoColumns = columns.filter((c) => !c.custom)
|
||||
const ownersAdbk = getOwnersWithNameFromAddressBook(addressBook as AddressBookCollection, owners)
|
||||
const ownersAdbk = getOwnersWithNameFromAddressBook(addressBook, owners)
|
||||
const ownerData = getOwnerData(ownersAdbk)
|
||||
|
||||
return (
|
||||
|
|
|
@ -23,7 +23,7 @@ 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 { getAddressBook } from 'src/logic/addressBook/store/selectors'
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { grantedSelector } from 'src/routes/safe/container/selector'
|
||||
import { safeNeedsUpdateSelector, safeOwnersSelector } from 'src/logic/safe/store/selectors'
|
||||
|
||||
|
@ -42,7 +42,7 @@ const Settings: React.FC = () => {
|
|||
const owners = useSelector(safeOwnersSelector)
|
||||
const needsUpdate = useSelector(safeNeedsUpdateSelector)
|
||||
const granted = useSelector(grantedSelector)
|
||||
const addressBook = useSelector(getAddressBook)
|
||||
const addressBook = useSelector(addressBookSelector)
|
||||
|
||||
const handleChange = (menuOptionIndex) => () => {
|
||||
setState((prevState) => ({ ...prevState, menuOptionIndex }))
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useSelector } from 'react-redux'
|
|||
import EtherscanLink from 'src/components/EtherscanLink'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Bold from 'src/components/layout/Bold'
|
||||
import { getNameFromAddressBook } from 'src/logic/addressBook/store/selectors'
|
||||
import { getNameFromAddressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import OwnerAddressTableCell from 'src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell'
|
||||
import { getIncomingTxAmount } from 'src/routes/safe/components/Transactions/TxsTable/columns'
|
||||
import { lg, md } from 'src/theme/variables'
|
||||
|
@ -35,7 +35,7 @@ const TransferDescription = ({ from, txFromName, value = '' }) => (
|
|||
|
||||
const IncomingTxDescription = ({ tx }) => {
|
||||
const classes = useStyles()
|
||||
const txFromName = useSelector((state) => getNameFromAddressBook(state, tx.from))
|
||||
const txFromName = useSelector((state) => getNameFromAddressBookSelector(state, tx.from))
|
||||
return (
|
||||
<Block className={classes.txDataContainer}>
|
||||
<TransferDescription from={tx.from} txFromName={txFromName} value={getIncomingTxAmount(tx, false)} />
|
||||
|
|
|
@ -16,7 +16,7 @@ import { styles } from './style'
|
|||
import Block from 'src/components/layout/Block'
|
||||
import Button from 'src/components/layout/Button'
|
||||
import Img from 'src/components/layout/Img'
|
||||
import { getNameFromAddressBook } from 'src/logic/addressBook/store/selectors'
|
||||
import { getNameFromAddressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { OwnersWithoutConfirmations } from './index'
|
||||
|
||||
export const CONFIRM_TX_BTN_TEST_ID = 'confirm-btn'
|
||||
|
@ -64,7 +64,7 @@ const OwnerComponent = (props: OwnerComponentProps): React.ReactElement => {
|
|||
showExecuteRejectBtn,
|
||||
confirmed,
|
||||
} = props
|
||||
const nameInAdbk = useSelector((state) => getNameFromAddressBook(state, owner))
|
||||
const nameInAdbk = useSelector((state) => getNameFromAddressBookSelector(state, owner))
|
||||
const classes = useStyles()
|
||||
const [imgCircle, setImgCircle] = React.useState(ConfirmSmallGreyCircle)
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { IconText, Text } from '@gnosis.pm/safe-react-components'
|
||||
import { IconText, Text, EthHashInfo } from '@gnosis.pm/safe-react-components'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
@ -12,18 +12,18 @@ import {
|
|||
MultiSendDetails,
|
||||
} from 'src/routes/safe/store/actions/transactions/utils/multiSendDecodedDetails'
|
||||
import Bold from 'src/components/layout/Bold'
|
||||
import OwnerAddressTableCell from 'src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell'
|
||||
import EtherscanLink from 'src/components/EtherscanLink'
|
||||
import { humanReadableValue } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
import Collapse from 'src/components/Collapse'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { getNameFromAddressBook } from 'src/logic/addressBook/store/selectors'
|
||||
import { getNameFromAddressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import LinkWithRef from 'src/components/layout/Link'
|
||||
import { shortVersionOf } from 'src/logic/wallets/ethAddresses'
|
||||
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
|
||||
import { DataDecoded } from 'src/routes/safe/store/models/types/transactions.d'
|
||||
import DividerLine from 'src/components/DividerLine'
|
||||
import { isArrayParameter } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils'
|
||||
import { getNetwork } from 'src/config'
|
||||
|
||||
export const TRANSACTIONS_DESC_CUSTOM_VALUE_TEST_ID = 'tx-description-custom-value'
|
||||
export const TRANSACTIONS_DESC_CUSTOM_DATA_TEST_ID = 'tx-description-custom-data'
|
||||
|
@ -34,9 +34,14 @@ const useStyles = makeStyles(styles)
|
|||
const TxDetailsMethodName = styled(Text)`
|
||||
text-indent: 4px;
|
||||
`
|
||||
const TxDetailsMethodParam = styled.div`
|
||||
text-indent: 8px;
|
||||
display: flex;
|
||||
const TxDetailsMethodParam = styled.div<{ isArrayParameter: boolean }>`
|
||||
padding-left: 8px;
|
||||
display: ${({ isArrayParameter }) => (isArrayParameter ? 'block' : 'flex')};
|
||||
align-items: center;
|
||||
|
||||
p:first-of-type {
|
||||
margin-right: ${({ isArrayParameter }) => (isArrayParameter ? '0' : '4px')};
|
||||
}
|
||||
`
|
||||
const TxDetailsContent = styled.div`
|
||||
padding: 8px 8px 8px 16px;
|
||||
|
@ -46,6 +51,10 @@ const TxInfo = styled.div`
|
|||
padding: 8px 8px 8px 16px;
|
||||
`
|
||||
|
||||
const StyledMethodName = styled(Text)`
|
||||
white-space: nowrap;
|
||||
`
|
||||
|
||||
const TxInfoDetails = ({ data }: { data: DataDecoded }): React.ReactElement => (
|
||||
<TxInfo>
|
||||
<TxDetailsMethodName size="lg" strong>
|
||||
|
@ -53,10 +62,10 @@ const TxInfoDetails = ({ data }: { data: DataDecoded }): React.ReactElement => (
|
|||
</TxDetailsMethodName>
|
||||
|
||||
{data.parameters.map((param, index) => (
|
||||
<TxDetailsMethodParam key={`${data.method}_param-${index}`}>
|
||||
<Text size="lg" strong>
|
||||
<TxDetailsMethodParam key={`${data.method}_param-${index}`} isArrayParameter={isArrayParameter(param.type)}>
|
||||
<StyledMethodName size="lg" strong>
|
||||
{param.name}({param.type}):
|
||||
</Text>
|
||||
</StyledMethodName>
|
||||
<Value method={data.method} type={param.type} value={param.value} />
|
||||
</TxDetailsMethodParam>
|
||||
))}
|
||||
|
@ -76,7 +85,7 @@ const MultiSendCustomDataAction = ({ tx, order }: { tx: MultiSendDetails; order:
|
|||
<TxDetailsContent>
|
||||
<TxInfo>
|
||||
<Bold>Send {humanReadableValue(tx.value)} ETH to:</Bold>
|
||||
<OwnerAddressTableCell address={tx.to} showLinks />
|
||||
<EthHashInfo hash={tx.to} showIdenticon showCopyBtn showEtherscanBtn network={getNetwork()} />
|
||||
</TxInfo>
|
||||
|
||||
{!!tx.data && <TxInfoDetails data={tx.data} />}
|
||||
|
@ -167,17 +176,21 @@ interface GenericCustomDataProps {
|
|||
|
||||
const GenericCustomData = ({ amount = '0', data, recipient, storedTx }: GenericCustomDataProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const recipientName = useSelector((state) => getNameFromAddressBook(state, recipient))
|
||||
const recipientName = useSelector((state) => getNameFromAddressBookSelector(state, recipient))
|
||||
|
||||
return (
|
||||
<Block>
|
||||
<Block data-testid={TRANSACTIONS_DESC_CUSTOM_VALUE_TEST_ID}>
|
||||
<Bold>Send {amount} to:</Bold>
|
||||
{recipientName ? (
|
||||
<OwnerAddressTableCell address={recipient} knownAddress showLinks userName={recipientName} />
|
||||
) : (
|
||||
<EtherscanLink knownAddress={false} type="address" value={recipient} />
|
||||
)}
|
||||
|
||||
<EthHashInfo
|
||||
hash={recipient}
|
||||
name={recipientName === 'UNKNOWN' ? undefined : recipientName}
|
||||
showIdenticon
|
||||
showCopyBtn
|
||||
showEtherscanBtn
|
||||
network={getNetwork()}
|
||||
/>
|
||||
</Block>
|
||||
|
||||
{!!storedTx?.dataDecoded && <TxActionData dataDecoded={storedTx.dataDecoded} />}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useSelector } from 'react-redux'
|
||||
import React from 'react'
|
||||
|
||||
import { getNameFromAddressBook } from 'src/logic/addressBook/store/selectors'
|
||||
import { getNameFromAddressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Bold from 'src/components/layout/Bold'
|
||||
import OwnerAddressTableCell from 'src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell'
|
||||
|
@ -21,7 +21,7 @@ interface RemovedOwnerProps {
|
|||
}
|
||||
|
||||
const RemovedOwner = ({ removedOwner }: RemovedOwnerProps): React.ReactElement => {
|
||||
const ownerChangedName = useSelector((state) => getNameFromAddressBook(state, removedOwner))
|
||||
const ownerChangedName = useSelector((state) => getNameFromAddressBookSelector(state, removedOwner))
|
||||
|
||||
return (
|
||||
<Block data-testid={TRANSACTIONS_DESC_REMOVE_OWNER_TEST_ID}>
|
||||
|
@ -40,7 +40,7 @@ interface AddedOwnerProps {
|
|||
}
|
||||
|
||||
const AddedOwner = ({ addedOwner }: AddedOwnerProps): React.ReactElement => {
|
||||
const ownerChangedName = useSelector((state) => getNameFromAddressBook(state, addedOwner))
|
||||
const ownerChangedName = useSelector((state) => getNameFromAddressBookSelector(state, addedOwner))
|
||||
|
||||
return (
|
||||
<Block data-testid={TRANSACTIONS_DESC_ADD_OWNER_TEST_ID}>
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
|||
import { useSelector } from 'react-redux'
|
||||
|
||||
import { TRANSACTIONS_DESC_SEND_TEST_ID } from './index'
|
||||
import { getNameFromAddressBook } from 'src/logic/addressBook/store/selectors'
|
||||
import { getNameFromAddressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Bold from 'src/components/layout/Bold'
|
||||
import OwnerAddressTableCell from 'src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell'
|
||||
|
@ -14,7 +14,7 @@ interface TransferDescriptionProps {
|
|||
}
|
||||
|
||||
const TransferDescription = ({ amount = '', recipient }: TransferDescriptionProps): React.ReactElement => {
|
||||
const recipientName = useSelector((state) => getNameFromAddressBook(state, recipient))
|
||||
const recipientName = useSelector((state) => getNameFromAddressBookSelector(state, recipient))
|
||||
return (
|
||||
<Block data-testid={TRANSACTIONS_DESC_SEND_TEST_ID}>
|
||||
<Bold>Send {amount} to:</Bold>
|
||||
|
|
|
@ -1,21 +1,15 @@
|
|||
import { Text } from '@gnosis.pm/safe-react-components'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { Text, EthHashInfo } from '@gnosis.pm/safe-react-components'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { styles } from './styles'
|
||||
|
||||
import { getNetwork } from 'src/config'
|
||||
import {
|
||||
isAddress,
|
||||
isArrayParameter,
|
||||
} from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils'
|
||||
import { useWindowDimensions } from 'src/logic/hooks/useWindowDimensions'
|
||||
import SafeEtherscanLink from 'src/components/EtherscanLink'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const NestedWrapper = styled.div`
|
||||
text-indent: 24px;
|
||||
padding-left: 4px;
|
||||
`
|
||||
|
||||
const StyledText = styled(Text)`
|
||||
|
@ -28,53 +22,38 @@ interface RenderValueProps {
|
|||
value: string | string[]
|
||||
}
|
||||
|
||||
const EtherscanLink = ({ method, type, value }: RenderValueProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const [cut, setCut] = React.useState(0)
|
||||
const { width } = useWindowDimensions()
|
||||
|
||||
React.useEffect(() => {
|
||||
if (width <= 900) {
|
||||
setCut(4)
|
||||
} else if (width <= 1024) {
|
||||
setCut(8)
|
||||
} else {
|
||||
setCut(12)
|
||||
}
|
||||
}, [width])
|
||||
|
||||
if (isArrayParameter(type)) {
|
||||
return (
|
||||
<NestedWrapper>
|
||||
{(value as string[]).map((value, index) => (
|
||||
<SafeEtherscanLink type="address" key={`${method}-value-${index}`} cut={cut} value={value} />
|
||||
))}
|
||||
</NestedWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
return <SafeEtherscanLink type="address" className={classes.address} cut={cut} value={value as string} />
|
||||
}
|
||||
|
||||
const GenericValue = ({ method, type, value }: RenderValueProps): React.ReactElement => {
|
||||
if (isArrayParameter(type)) {
|
||||
return (
|
||||
const getTextValue = (value: string) => <StyledText size="lg">{value}</StyledText>
|
||||
|
||||
const getArrayValue = (parentId: string, value: string[] | string) => (
|
||||
<div>
|
||||
[
|
||||
<NestedWrapper>
|
||||
{(value as string[]).map((value, index) => (
|
||||
<StyledText key={`${method}-value-${index}`} size="lg">
|
||||
{value}
|
||||
</StyledText>
|
||||
))}
|
||||
{(value as string[]).map((currentValue, index) => {
|
||||
const key = `${parentId}-value-${index}`
|
||||
return (
|
||||
<div key={key}>
|
||||
{Array.isArray(currentValue) ? getArrayValue(key, currentValue) : getTextValue(currentValue)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</NestedWrapper>
|
||||
)
|
||||
]
|
||||
</div>
|
||||
)
|
||||
|
||||
if (isArrayParameter(type) || Array.isArray(value)) {
|
||||
return getArrayValue(method, value)
|
||||
}
|
||||
|
||||
return <StyledText size="lg">{value as string}</StyledText>
|
||||
return getTextValue(value as string)
|
||||
}
|
||||
|
||||
const Value = ({ type, ...props }: RenderValueProps): React.ReactElement => {
|
||||
if (isAddress(type)) {
|
||||
return <EtherscanLink type={type} {...props} />
|
||||
return (
|
||||
<EthHashInfo hash={props.value as string} showCopyBtn showEtherscanBtn shortenHash={4} network={getNetwork()} />
|
||||
)
|
||||
}
|
||||
|
||||
return <GenericValue type={type} {...props} />
|
||||
|
|
|
@ -63,7 +63,7 @@ const ExpandedTx = ({ cancelTx, tx }: ExpandedTxProps): React.ReactElement => {
|
|||
<>
|
||||
<Block className={classes.expandedTxBlock}>
|
||||
<Row>
|
||||
<Col layout="column" xs={6}>
|
||||
<Col layout="column" xs={6} className={classes.col}>
|
||||
<Block className={cn(classes.txDataContainer, (isIncomingTx || isCreationTx) && classes.incomingTxBlock)}>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Bold className={classes.txHash}>Hash:</Bold>
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import { border, lg, md } from 'src/theme/variables'
|
||||
|
||||
const cssStyles = {
|
||||
col: {
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
},
|
||||
expandedTxBlock: {
|
||||
borderBottom: `2px solid ${border}`,
|
||||
},
|
||||
|
|
|
@ -6,7 +6,6 @@ import thunk from 'redux-thunk'
|
|||
|
||||
import addressBookMiddleware from 'src/logic/addressBook/store/middleware/addressBookMiddleware'
|
||||
import addressBook, { ADDRESS_BOOK_REDUCER_ID } from 'src/logic/addressBook/store/reducer/addressBook'
|
||||
import { AddressBookReducerMap } from 'src/logic/addressBook/store/reducer/types/addressBook.d'
|
||||
import {
|
||||
NFT_ASSETS_REDUCER_ID,
|
||||
NFT_TOKENS_REDUCER_ID,
|
||||
|
@ -19,8 +18,10 @@ import currencyValues, {
|
|||
CURRENCY_VALUES_KEY,
|
||||
CurrencyValuesState,
|
||||
} from 'src/logic/currencyValues/store/reducer/currencyValues'
|
||||
import { CurrentSessionState } from 'src/logic/currentSession/store/reducer/currentSession'
|
||||
import currentSession, { CURRENT_SESSION_REDUCER_ID } from 'src/logic/currentSession/store/reducer/currentSession'
|
||||
import currentSession, {
|
||||
CURRENT_SESSION_REDUCER_ID,
|
||||
CurrentSessionState,
|
||||
} from 'src/logic/currentSession/store/reducer/currentSession'
|
||||
import notifications, { NOTIFICATIONS_REDUCER_ID } from 'src/logic/notifications/store/reducer/notifications'
|
||||
import tokens, { TOKEN_REDUCER_ID, TokenState } from 'src/logic/tokens/store/reducer/tokens'
|
||||
import providerWatcher from 'src/logic/wallets/store/middlewares/providerWatcher'
|
||||
|
@ -38,7 +39,8 @@ import safe, { SAFE_REDUCER_ID } from 'src/logic/safe/store/reducer/safe'
|
|||
import transactions, { TRANSACTIONS_REDUCER_ID } from 'src/logic/safe/store/reducer/transactions'
|
||||
import { NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/OpenSea'
|
||||
import { SafeReducerMap } from 'src/routes/safe/store/reducer/types/safe'
|
||||
import allTransactions, { TRANSACTIONS, TransactionsState } from 'src/logic/safe/store/reducer/allTransactions'
|
||||
import allTransactions, { TRANSACTIONS, TransactionsState } from '../logic/safe/store/reducer/allTransactions'
|
||||
import { AddressBookState } from 'src/logic/addressBook/model/addressBook'
|
||||
|
||||
export const history = createHashHistory()
|
||||
|
||||
|
@ -86,7 +88,7 @@ export type AppReduxState = CombinedState<{
|
|||
[NOTIFICATIONS_REDUCER_ID]: Map<string, any>
|
||||
[CURRENCY_VALUES_KEY]: CurrencyValuesState
|
||||
[COOKIES_REDUCER_ID]: Map<string, any>
|
||||
[ADDRESS_BOOK_REDUCER_ID]: AddressBookReducerMap
|
||||
[ADDRESS_BOOK_REDUCER_ID]: AddressBookState
|
||||
[CURRENT_SESSION_REDUCER_ID]: CurrentSessionState
|
||||
[TRANSACTIONS]: TransactionsState
|
||||
router: RouterState
|
||||
|
|
Loading…
Reference in New Issue