Merge branch 'development' of github.com:gnosis/safe-react into development

This commit is contained in:
Mati Dastugue 2020-09-23 23:26:13 -03:00
commit f99a18cabc
75 changed files with 1168 additions and 1647 deletions

View File

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

View File

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

View File

@ -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,
}))

View File

@ -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,
}
},
)

View File

@ -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,
}))

View File

@ -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,
}))

View File

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

View File

@ -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,
}))

View File

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

View File

@ -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,
}))

View File

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

View File

@ -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({}),
}),
[],
)

View File

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

View File

@ -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'
},
)

View File

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

View File

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

View File

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

View File

@ -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 },
})
})
})

View File

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

View File

@ -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,
}))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}`,
},
})

View File

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

View File

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

View File

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

View File

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

View File

@ -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',
},

View File

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

View File

@ -1,5 +0,0 @@
import addSafe from 'src/logic/safe/store/actions/addSafe'
export default {
addSafe,
}

View File

@ -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,
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}
/>
)
}

View File

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

View File

@ -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}
/>
</>
)

View File

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

View File

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

View File

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

View File

@ -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,
},
}),
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>
) : (

View File

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

View File

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

View File

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

View File

@ -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)} />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}`,
},

View File

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

908
yarn.lock

File diff suppressed because it is too large Load Diff