Merge pull request #103 from gnosis/99-app-state

#99 Refactor redux store
This commit is contained in:
Mikhail Mikheev 2019-04-22 17:58:44 +04:00 committed by GitHub
commit a689c3b44d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
78 changed files with 1406 additions and 1156 deletions

View File

@ -80,9 +80,9 @@
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-final-form": "^4.1.0",
"react-hot-loader": "^4.8.2",
"react-hot-loader": "4.8.4",
"react-infinite-scroll-component": "^4.5.2",
"react-redux": "^6.0.1",
"react-redux": "7.0.2",
"react-router-dom": "^4.3.1",
"recompose": "^0.30.0",
"redux": "^4.0.1",
@ -117,11 +117,11 @@
"@babel/preset-flow": "^7.0.0-beta.40",
"@babel/preset-react": "^7.0.0-beta.40",
"@sambego/storybook-state": "^1.0.7",
"@storybook/addon-actions": "^5.0.6",
"@storybook/addon-knobs": "^5.0.6",
"@storybook/addon-links": "^5.0.6",
"@storybook/react": "^5.0.6",
"autoprefixer": "^9.4.10",
"@storybook/addon-actions": "5.0.9",
"@storybook/addon-knobs": "5.0.9",
"@storybook/addon-links": "5.0.9",
"@storybook/react": "5.0.9",
"autoprefixer": "9.5.1",
"babel-core": "^7.0.0-bridge.0",
"babel-eslint": "^10.0.1",
"babel-jest": "^24.1.0",
@ -134,38 +134,38 @@
"detect-port": "^1.2.2",
"eslint": "^5.16.0",
"eslint-config-airbnb": "^17.1.0",
"eslint-plugin-flowtype": "^3.4.2",
"eslint-plugin-import": "^2.9.0",
"eslint-plugin-flowtype": "3.6.1",
"eslint-plugin-import": "2.17.2",
"eslint-plugin-jest": "^22.3.0",
"eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-react": "^7.7.0",
"ethereumjs-abi": "^0.6.7",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"file-loader": "^3.0.1",
"flow-bin": "0.96.0",
"flow-bin": "0.97.0",
"fs-extra": "^7.0.1",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.0.4",
"jest": "^24.1.0",
"json-loader": "^0.5.7",
"mini-css-extract-plugin": "^0.5.0",
"mini-css-extract-plugin": "0.6.0",
"postcss-loader": "^3.0.0",
"postcss-mixins": "^6.2.0",
"postcss-simple-vars": "^5.0.2",
"pre-commit": "^1.2.2",
"prettier-eslint-cli": "^4.7.1",
"run-with-testrpc": "^0.3.0",
"run-with-testrpc": "0.3.1",
"storybook-host": "^5.0.3",
"storybook-router": "^0.3.3",
"style-loader": "^0.23.1",
"truffle": "^5.0.10",
"truffle": "5.0.12",
"truffle-contract": "^4.0.11",
"truffle-solidity-loader": "^0.1.10",
"truffle-solidity-loader": "0.1.12",
"uglifyjs-webpack-plugin": "^2.1.2",
"webpack": "^4.1.1",
"webpack-bundle-analyzer": "^3.1.0",
"webpack-bundle-analyzer": "3.3.2",
"webpack-cli": "^3.2.3",
"webpack-dev-server": "^3.1.0",
"webpack-dev-server": "3.3.1",
"webpack-manifest-plugin": "^2.0.0-rc.2"
}
}

View File

@ -11,12 +11,14 @@ These instructions will get you a copy of the project up and running on your loc
What things you need to install the software and how to install them
```
npm install truffle // recommended usage of -g flag
npm install ganache-cli // recommended usage of -g flag
npm install flow-type // recommended usage of -g flag
yarn add truffle // recommended usage of -g flag
yarn add ganache-cli // recommended usage of -g flag
yarn add flow-type // recommended usage of -g flag
git clone https://github.com/gnosis/safe-contracts.git
```
We use [yarn](https://yarnpkg.com) in our infrastacture, so we decided to go with yarn in the README
### Installing
A step by step series of examples that tell you have to get a development env running
@ -29,14 +31,14 @@ ganache-cli -b 3
Start the project in the other one
```
cd safe-contracts && truffle compile && truffle migrate && cd ..
npm install
npm start
yarn install
yarn start
```
## Running the tests
```
npm test
yarn test
```

View File

@ -6,7 +6,9 @@ import ReactDOM from 'react-dom'
import Root from '~/components/Root'
import { store } from '~/store'
import loadSafesFromStorage from '~/routes/safe/store/actions/loadSafesFromStorage'
import loadActiveTokens from '~/logic/tokens/store/actions/loadActiveTokens'
store.dispatch(loadActiveTokens())
store.dispatch(loadSafesFromStorage())
ReactDOM.render(<Root />, document.getElementById('root'))

View File

@ -1,10 +1,10 @@
// @flow
import { List } from 'immutable'
import { type Transaction } from '~/routes/safe/store/model/transaction'
import { type Transaction } from '~/routes/safe/store/models/transaction'
import { executeTransaction, approveTransaction } from '~/logic/safe/safeBlockchainOperations'
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { type Safe } from '~/routes/safe/store/model/safe'
import { type Safe } from '~/routes/safe/store/models/safe'
import { getGnosisSafeContract } from '~/logic/contracts/safeContracts'
import { storeSubject } from '~/utils/storage/transactions'

View File

@ -1,5 +1,5 @@
// @flow
import { type Owner } from '~/routes/safe/store/model/owner'
import { type Owner } from '~/routes/safe/store/models/owner'
import { List, Map } from 'immutable'
import { loadFromStorage, saveToStorage } from '~/utils/storage'

View File

@ -1,9 +1,9 @@
// @flow
import { createAction } from 'redux-actions'
import { type Token } from '~/logic/tokens/store/model/token'
import { setActiveTokens, getActiveTokens, setToken } from '~/logic/tokens/utils/tokensStorage'
import { saveActiveTokens, getActiveTokens, setToken } from '~/logic/tokens/utils/tokensStorage'
import type { Dispatch as ReduxDispatch } from 'redux'
import { type GlobalState } from '~/store/index'
import { type GlobalState } from '~/store/'
export const ADD_TOKEN = 'ADD_TOKEN'
@ -14,17 +14,16 @@ type AddTokenProps = {
export const addToken = createAction<string, *, *>(
ADD_TOKEN,
(safeAddress: string, token: Token): AddTokenProps => ({
safeAddress,
(token: Token): AddTokenProps => ({
token,
}),
)
const saveToken = (safeAddress: string, token: Token) => async (dispatch: ReduxDispatch<GlobalState>) => {
dispatch(addToken(safeAddress, token))
dispatch(addToken(token))
const activeTokens = await getActiveTokens(safeAddress)
await setActiveTokens(safeAddress, activeTokens.push(token.toJS()))
await saveActiveTokens(safeAddress, activeTokens.push(token.toJS()))
setToken(safeAddress, token)
}

View File

@ -1,21 +0,0 @@
// @flow
import { createAction } from 'redux-actions'
import { type Token } from '~/logic/tokens/store/model/token'
import type { Dispatch as ReduxDispatch } from 'redux'
import { type GlobalState } from '~/store/index'
import { removeFromActiveTokens } from '~/logic/tokens/utils/tokensStorage'
export const DISABLE_TOKEN = 'DISABLE_TOKEN'
export const disableToken = createAction<string, *, *>(DISABLE_TOKEN, (safeAddress: string, token: Token) => ({
safeAddress,
token,
}))
const hideToken = (safeAddress: string, token: Token) => async (dispatch: ReduxDispatch<GlobalState>) => {
dispatch(disableToken(safeAddress, token))
await removeFromActiveTokens(safeAddress, token)
}
export default hideToken

View File

@ -1,22 +0,0 @@
// @flow
import { createAction } from 'redux-actions'
import type { Dispatch as ReduxDispatch } from 'redux'
import { type GlobalState } from '~/store/index'
import { type Token } from '~/logic/tokens/store/model/token'
import { setActiveTokens, getActiveTokens } from '~/logic/tokens/utils/tokensStorage'
export const ENABLE_TOKEN = 'ENABLE_TOKEN'
export const enableToken = createAction<string, *, *>(ENABLE_TOKEN, (safeAddress: string, token: Token) => ({
safeAddress,
token,
}))
const setTokenEnabled = (safeAddress: string, token: Token) => async (dispatch: ReduxDispatch<GlobalState>) => {
dispatch(enableToken(safeAddress, token))
const activeTokens = await getActiveTokens(safeAddress)
await setActiveTokens(safeAddress, activeTokens.push(token))
}
export default setTokenEnabled

View File

@ -1,17 +1,14 @@
// @flow
import { List, Map } from 'immutable'
import { List } from 'immutable'
import contract from 'truffle-contract'
import axios from 'axios'
import { BigNumber } from 'bignumber.js'
import type { Dispatch as ReduxDispatch } from 'redux'
import StandardToken from '@gnosis.pm/util-contracts/build/contracts/GnosisStandardToken.json'
import HumanFriendlyToken from '@gnosis.pm/util-contracts/build/contracts/HumanFriendlyToken.json'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { type GlobalState } from '~/store/index'
import { makeToken, type Token, type TokenProps } from '~/logic/tokens/store/model/token'
import { makeToken, type TokenProps } from '~/logic/tokens/store/model/token'
import { ensureOnce } from '~/utils/singleton'
import { getActiveTokens, getTokens } from '~/logic/tokens/utils/tokensStorage'
import { getSafeEthToken } from '~/logic/tokens/utils/tokenHelpers'
import saveTokens from './saveTokens'
import { getRelayUrl } from '~/config/index'
@ -34,66 +31,28 @@ export const getHumanFriendlyToken = ensureOnce(createHumanFriendlyTokenContract
export const getStandardTokenContract = ensureOnce(createStandardTokenContract)
export const calculateBalanceOf = async (tokenAddress: string, address: string, decimals: number) => {
const erc20Token = await getStandardTokenContract()
let balance = 0
try {
const token = await erc20Token.at(tokenAddress)
balance = await token.balanceOf(address)
} catch (err) {
console.error('Failed to fetch token balances: ', err)
}
return new BigNumber(balance).div(10 ** decimals).toString()
}
export const fetchTokensData = async () => {
const fetchTokenList = async () => {
const apiUrl = getRelayUrl()
const url = `${apiUrl}/tokens`
const errMsg = 'Error querying safe balances'
return axios.get(url, errMsg)
}
export const fetchTokens = (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>) => {
const tokens: List<TokenProps> = await getActiveTokens(safeAddress)
const ethBalance = await getSafeEthToken(safeAddress)
const customTokens = await getTokens(safeAddress)
const {
data: { results },
} = await fetchTokensData()
export const fetchTokens = () => async (dispatch: ReduxDispatch<GlobalState>) => {
try {
const balancesRecords = await Promise.all(
results.map(async (item: TokenProps) => {
const status = tokens.findIndex(activeToken => activeToken.name === item.name) !== -1
const funds = status ? await calculateBalanceOf(item.address, safeAddress, item.decimals) : '0'
const {
data: { results: tokenList },
} = await fetchTokenList()
return makeToken({ ...item, status, funds })
}),
)
const tokens = List(tokenList.map((token: TokenProps) => makeToken(token)))
const customTokenRecords = await Promise.all(
customTokens.map(async (item: TokenProps) => {
const status = tokens.findIndex(activeToken => activeToken.name === item.name) !== -1
const funds = status ? await calculateBalanceOf(item.address, safeAddress, item.decimals) : '0'
return makeToken({ ...item, status, funds })
}),
)
const balances: Map<string, Token> = Map().withMutations((map) => {
balancesRecords.forEach(record => map.set(record.address, record))
customTokenRecords.forEach(record => map.set(record.address, record))
map.set(ethBalance.address, ethBalance)
})
return dispatch(saveTokens(safeAddress, balances))
dispatch(saveTokens(tokens))
} catch (err) {
// eslint-disable-next-line
console.log('Error fetching tokens... ' + err)
console.log('Error fetching token list ' + err)
return Promise.resolve()
}
}
export default fetchTokens

View File

@ -0,0 +1,24 @@
// @flow
import type { Dispatch as ReduxDispatch } from 'redux'
import { Map, List } from 'immutable'
import { type TokenProps, type Token, makeToken } from '~/logic/tokens/store/model/token'
import { type GlobalState } from '~/store/index'
import { getActiveTokens } from '~/logic/tokens/utils/tokensStorage'
import saveTokens from './saveTokens'
const loadActiveTokens = () => async (dispatch: ReduxDispatch<GlobalState>) => {
try {
const tokens: Map<string, TokenProps> = await getActiveTokens()
const tokenRecordsList: List<Token> = List(
Object.values(tokens).map(token => makeToken(token)),
)
dispatch(saveTokens(tokenRecordsList))
} catch (err) {
// eslint-disable-next-line
console.error('Error while loading active tokens from storage:', err)
}
}
export default loadActiveTokens

View File

@ -1,37 +1,19 @@
// @flow
import { Map, List } from 'immutable'
import { Map } from 'immutable'
import { createAction } from 'redux-actions'
import type { Dispatch as ReduxDispatch } from 'redux'
import { type Token } from '~/logic/tokens/store/model/token'
import { ensureOnceAsync } from '~/utils/singleton'
import { type GlobalState } from '~/store/index'
import { setActiveTokens } from '~/logic/tokens/utils/tokensStorage'
import { calculateActiveErc20TokensFrom } from '~/logic/tokens/utils/tokenHelpers'
export const ADD_TOKENS = 'ADD_TOKENS'
const setTokensOnce = ensureOnceAsync(setActiveTokens)
type TokenProps = {
safeAddress: string,
tokens: Map<string, Token>,
}
export const addTokens = createAction<string, *, *>(
const addTokens = createAction<string, *, *>(
ADD_TOKENS,
(safeAddress: string, tokens: Map<string, Token>): TokenProps => ({
safeAddress,
(tokens: Map<string, Token>): TokenProps => ({
tokens,
}),
)
const saveTokens = (safeAddress: string, tokens: Map<string, Token>) => async (
dispatch: ReduxDispatch<GlobalState>,
) => {
dispatch(addTokens(safeAddress, tokens))
const activeAddresses: List<Token> = calculateActiveErc20TokensFrom(tokens.toList())
await setTokensOnce(safeAddress, activeAddresses)
}
export default saveTokens
export default addTokens

View File

@ -8,9 +8,7 @@ export type TokenProps = {
symbol: string,
decimals: number,
logoUri: string,
funds: string,
status: boolean,
removable: boolean,
balance: string,
}
export const makeToken: RecordFactory<TokenProps> = Record({
@ -19,9 +17,9 @@ export const makeToken: RecordFactory<TokenProps> = Record({
symbol: '',
decimals: 0,
logoUri: '',
funds: '0',
status: true,
removable: false,
balance: undefined,
})
// balance is only set in extendedSafeTokensSelector when we display user's token balances
export type Token = RecordOf<TokenProps>

View File

@ -5,8 +5,6 @@ import { type Token } from '~/logic/tokens/store/model/token'
import { ADD_TOKEN } from '~/logic/tokens/store/actions/addToken'
import { REMOVE_TOKEN } from '~/logic/tokens/store/actions/removeToken'
import { ADD_TOKENS } from '~/logic/tokens/store/actions/saveTokens'
import { DISABLE_TOKEN } from '~/logic/tokens/store/actions/disableToken'
import { ENABLE_TOKEN } from '~/logic/tokens/store/actions/enableToken'
export const TOKEN_REDUCER_ID = 'tokens'
@ -15,39 +13,27 @@ export type State = Map<string, Map<string, Token>>
export default handleActions<State, *>(
{
[ADD_TOKENS]: (state: State, action: ActionType<Function>): State => {
const { safeAddress, tokens } = action.payload
const { tokens } = action.payload
return state.update(safeAddress, (prevSafe: Map<string, Token>) => {
if (!prevSafe) {
return tokens
}
return prevSafe.equals(tokens) ? prevSafe : tokens
const newState = state.withMutations((map) => {
tokens.forEach((token) => {
map.set(token.address, token)
})
})
return newState
},
[ADD_TOKEN]: (state: State, action: ActionType<Function>): State => {
const { safeAddress, token } = action.payload
const { token } = action.payload
const { address: tokenAddress } = token
return state.setIn([safeAddress, tokenAddress], token)
return state.set(tokenAddress, token)
},
[REMOVE_TOKEN]: (state: State, action: ActionType<Function>): State => {
const { safeAddress, token } = action.payload
const { token } = action.payload
const { address: tokenAddress } = token
return state.removeIn([safeAddress, tokenAddress])
},
[DISABLE_TOKEN]: (state: State, action: ActionType<Function>): State => {
const { safeAddress, token } = action.payload
const { address: tokenAddress } = token
return state.setIn([safeAddress, tokenAddress, 'status'], false)
},
[ENABLE_TOKEN]: (state: State, action: ActionType<Function>): State => {
const { safeAddress, token } = action.payload
const { address: tokenAddress } = token
return state.setIn([safeAddress, tokenAddress, 'status'], true)
return state.remove(tokenAddress)
},
},
Map(),

View File

@ -1,45 +1,19 @@
// @flow
import { List, Map } from 'immutable'
import { createSelector, type Selector } from 'reselect'
import { safeParamAddressSelector, type RouterProps } from '~/routes/safe/store/selectors'
import { type RouterProps } from '~/routes/safe/store/selectors'
import { type GlobalState } from '~/store'
import { TOKEN_REDUCER_ID } from '~/logic/tokens/store/reducer/tokens'
import { type Token } from '~/logic/tokens/store/model/token'
const balancesSelector = (state: GlobalState) => state[TOKEN_REDUCER_ID]
export const tokensSelector = (state: GlobalState) => state[TOKEN_REDUCER_ID]
export const tokensSelector: Selector<GlobalState, RouterProps, Map<string, Token>> = createSelector(
balancesSelector,
safeParamAddressSelector,
(tokens: Map<string, Map<string, Token>>, address: string) => {
if (!address) {
return Map()
}
return tokens.get(address) || Map()
},
)
export const tokenListSelector = createSelector(
export const tokenListSelector: Selector<GlobalState, Map<string, Token>, List<Token>> = createSelector(
tokensSelector,
(tokens: Map<string, Token>) => tokens.toList(),
)
export const activeTokensSelector = createSelector(
tokenListSelector,
(tokens: List<Token>) => tokens.filter((token: Token) => token.get('status')),
)
export const orderedTokenListSelector = createSelector(
export const orderedTokenListSelector: Selector<GlobalState, RouterProps, List<Token>> = createSelector(
tokenListSelector,
(tokens: List<Token>) => tokens.sortBy((token: Token) => token.get('symbol')),
)
export const tokenAddressesSelector = createSelector(
tokenListSelector,
(balances: List<Token>) => {
const addresses = List().withMutations(list => balances.map(token => list.push(token.address)))
return addresses
},
)

View File

@ -1,29 +1,26 @@
// @flow
import { List } from 'immutable'
import logo from '~/assets/icons/icon_etherTokens.svg'
import { getBalanceInEtherOf } from '~/logic/wallets/getWeb3'
import { makeToken, type Token } from '~/logic/tokens/store/model/token'
export const ETH_ADDRESS = '0'
export const ETH_ADDRESS = '0x000'
export const isEther = (symbol: string) => symbol === 'ETH'
export const getSafeEthToken = async (safeAddress: string) => {
const balance = await getBalanceInEtherOf(safeAddress)
const ethBalance = makeToken({
address: '0',
export const getEthAsToken = (balance: string) => {
const eth = makeToken({
address: ETH_ADDRESS,
name: 'Ether',
symbol: 'ETH',
decimals: 18,
logoUri: logo,
funds: balance,
balance,
})
return ethBalance
return eth
}
export const calculateActiveErc20TokensFrom = (tokens: List<Token>) => {
const addresses = List().withMutations(list => tokens.forEach((token: Token) => {
const activeTokens = List().withMutations(list => tokens.forEach((token: Token) => {
const isDeactivated = isEther(token.symbol) || !token.status
if (isDeactivated) {
return
@ -32,5 +29,5 @@ export const calculateActiveErc20TokensFrom = (tokens: List<Token>) => {
list.push(token)
}))
return addresses
return activeTokens
}

View File

@ -1,44 +1,41 @@
// @flow
import { List } from 'immutable'
import { List, Map } from 'immutable'
import { type Token, type TokenProps } from '~/logic/tokens/store/model/token'
import { loadFromStorage, saveToStorage } from '~/utils/storage'
export const ACTIVE_TOKENS_KEY = 'ACTIVE_TOKENS'
export const TOKENS_KEY = 'TOKENS'
export const CUSTOM_TOKENS_KEY = 'CUSTOM_TOKENS'
const getActiveTokensKey = (safeAddress: string) => `${ACTIVE_TOKENS_KEY}-${safeAddress}`
const getTokensKey = (safeAddress: string) => `${TOKENS_KEY}-${safeAddress}`
// Tokens which are active at least in one of used safes in the app should be saved to localstorage
// to avoid iterating a large amount of data of tokens from the backend
// Custom tokens should be saved too unless they're deleted (marking them as inactive doesn't count)
export const setActiveTokens = async (safeAddress: string, tokens: List<TokenProps>) => {
export const saveActiveTokens = async (tokens: Map<string, Token>) => {
try {
const key = getActiveTokensKey(safeAddress)
await saveToStorage(key, tokens.toJS())
await saveToStorage(ACTIVE_TOKENS_KEY, tokens.toJS())
} catch (err) {
// eslint-disable-next-line
console.log('Error storing tokens in localstorage')
}
}
export const getActiveTokens = async (safeAddress: string): Promise<List<TokenProps>> => {
const key = getActiveTokensKey(safeAddress)
const data = await loadFromStorage(key)
export const getActiveTokens = async (): Promise<Object<string, TokenProps>> => {
const data = await loadFromStorage(ACTIVE_TOKENS_KEY)
return data ? List(data) : List()
return data || {}
}
export const getTokens = async (safeAddress: string): Promise<List<TokenProps>> => {
const key = getTokensKey(safeAddress)
const data = await loadFromStorage(key)
export const getCustomTokens = async (): Promise<List<TokenProps>> => {
const data = await loadFromStorage(CUSTOM_TOKENS_KEY)
return data ? List(data) : List()
}
export const setToken = async (safeAddress: string, token: Token) => {
const data: List<TokenProps> = await getTokens(safeAddress)
const data: List<TokenProps> = await getCustomTokens()
try {
const key = getTokensKey(safeAddress)
await saveToStorage(key, data.push(token))
await saveToStorage(CUSTOM_TOKENS_KEY, data.push(token))
} catch (err) {
// eslint-disable-next-line
console.log('Error adding token in localstorage')
@ -46,12 +43,11 @@ export const setToken = async (safeAddress: string, token: Token) => {
}
export const removeTokenFromStorage = async (safeAddress: string, token: Token) => {
const data: List<TokenProps> = await getTokens(safeAddress)
const data: List<TokenProps> = await getCustomTokens()
try {
const index = data.indexOf(token)
const key = getTokensKey(safeAddress)
await saveToStorage(key, data.remove(index))
await saveToStorage(CUSTOM_TOKENS_KEY, data.remove(index))
} catch (err) {
// eslint-disable-next-line
console.log('Error removing token in localstorage')
@ -59,10 +55,10 @@ export const removeTokenFromStorage = async (safeAddress: string, token: Token)
}
export const removeFromActiveTokens = async (safeAddress: string, token: Token) => {
const activeTokens = await getActiveTokens(safeAddress)
const activeTokens = await getActiveTokens()
const index = activeTokens.findIndex(activeToken => activeToken.name === token.name)
if (index !== -1) {
await setActiveTokens(safeAddress, activeTokens.delete(index))
await saveActiveTokens(safeAddress, activeTokens.delete(index))
}
}

View File

@ -8,19 +8,19 @@ import { loadFromStorage } from '~/utils/storage'
import { SAFELIST_ADDRESS } from '~/routes/routes'
import { history } from '~/store'
import selector, { type SelectorProps } from './selector'
import actions, { type Actions, type UpdateSafe } from './actions'
import actions, { type Actions } from './actions'
import Layout from '../components/Layout'
import { FIELD_LOAD_NAME, FIELD_LOAD_ADDRESS } from '../components/fields'
type Props = SelectorProps & Actions
export const loadSafe = async (safeName: string, safeAddress: string, updateSafe: UpdateSafe) => {
const safeRecord = await buildSafe(safeAddress, safeName)
export const loadSafe = async (safeName: string, safeAddress: string, addSafe: Function) => {
const safeProps = await buildSafe(safeAddress, safeName)
await updateSafe(safeRecord)
await addSafe(safeProps)
const storedSafes = await loadFromStorage(SAFES_KEY) || {}
storedSafes[safeAddress] = safeRecord.toJSON()
const storedSafes = (await loadFromStorage(SAFES_KEY)) || {}
storedSafes[safeAddress] = safeProps
saveSafes(storedSafes)
}
@ -28,11 +28,12 @@ export const loadSafe = async (safeName: string, safeAddress: string, updateSafe
class Load extends React.Component<Props> {
onLoadSafeSubmit = async (values: Object) => {
try {
const { updateSafe } = this.props
const { addSafe } = this.props
const safeName = values[FIELD_LOAD_NAME]
const safeAddress = values[FIELD_LOAD_ADDRESS]
await loadSafe(safeName, safeAddress, updateSafe)
await loadSafe(safeName, safeAddress, addSafe)
const url = `${SAFELIST_ADDRESS}/${safeAddress}`
history.push(url)
} catch (error) {
@ -42,9 +43,7 @@ class Load extends React.Component<Props> {
}
render() {
const {
provider, network, userAddress,
} = this.props
const { provider, network, userAddress } = this.props
return (
<Page>
@ -59,4 +58,7 @@ class Load extends React.Component<Props> {
}
}
export default connect(selector, actions)(Load)
export default connect<Object, Object, ?Function, ?Object>(
selector,
actions,
)(Load)

View File

@ -1,12 +1,10 @@
// @flow
import updateSafe from '~/routes/safe/store/actions/updateSafe'
export type UpdateSafe = typeof updateSafe
import { addSafe } from '~/routes/safe/store/actions/addSafe'
export type Actions = {
updateSafe: typeof updateSafe,
addSafe: Function,
}
export default {
updateSafe,
addSafe,
}

View File

@ -3,8 +3,8 @@ import * as React from 'react'
import { List } from 'immutable'
import Stepper from '~/components/Stepper'
import { connect } from 'react-redux'
import { type Safe } from '~/routes/safe/store/model/safe'
import { type Owner, makeOwner } from '~/routes/safe/store/model/owner'
import { type Safe } from '~/routes/safe/store/models/safe'
import { type Owner, makeOwner } from '~/routes/safe/store/models/owner'
import { setOwners } from '~/utils/storage'
import { getSafeEthereumInstance, createTransaction } from '~/logic/safe/safeFrontendOperations'
import AddOwnerForm, { NAME_PARAM, OWNER_ADDRESS_PARAM, INCREASE_PARAM } from './AddOwnerForm'

View File

@ -1,13 +1,13 @@
// @flow
import enableToken from '~/logic/tokens/store/actions/enableToken'
import disableToken from '~/logic/tokens/store/actions/disableToken'
import fetchTokens from '~/logic/tokens/store/actions/fetchTokens'
import updateActiveTokens from '~/routes/safe/store/actions/updateActiveTokens'
export type Actions = {
enableToken: typeof enableToken,
disableToken: typeof disableToken,
fetchTokens: Function,
updateActiveTokens: Function,
}
export default {
enableToken,
disableToken,
fetchTokens,
updateActiveTokens,
}

View File

@ -1,10 +1,9 @@
// @flow
import * as React from 'react'
import { connect } from 'react-redux'
import { List } from 'immutable'
import { List, Set } from 'immutable'
import classNames from 'classnames/bind'
import SearchBar from 'material-ui-search-bar'
import InfiniteScroll from 'react-infinite-scroll-component'
import { withStyles } from '@material-ui/core/styles'
import MuiList from '@material-ui/core/List'
import Img from '~/components/layout/Img'
@ -23,6 +22,7 @@ import Divider from '~/components/layout/Divider'
import Hairline from '~/components/layout/Hairline'
import Spacer from '~/components/Spacer'
import Row from '~/components/layout/Row'
import { ETH_ADDRESS } from '~/logic/tokens/utils/tokenHelpers'
import { type Token } from '~/logic/tokens/store/model/token'
import actions, { type Actions } from './actions'
import TokenPlaceholder from './assets/token_placeholder.png'
@ -33,10 +33,12 @@ type Props = Actions & {
classes: Object,
tokens: List<Token>,
safeAddress: string,
activeTokens: List<Token>,
}
type State = {
filter: string,
activeTokensAddresses: Set<string>,
}
const filterBy = (filter: string, tokens: List<Token>): List<Token> => tokens.filter(
@ -45,9 +47,44 @@ const filterBy = (filter: string, tokens: List<Token>): List<Token> => tokens.fi
|| token.name.toLowerCase().includes(filter.toLowerCase()),
)
// OPTIMIZATION IDEA (Thanks Andre)
// Calculate active tokens on component mount, store it in component state
// After user closes modal, dispatch an action so we dont have 100500 actions
// And selectors dont recalculate
class Tokens extends React.Component<Props, State> {
state = {
filter: '',
activeTokensAddresses: Set([]),
activeTokensCalculated: false,
}
componentDidMount() {
const { fetchTokens, safeAddress } = this.props
fetchTokens(safeAddress)
}
static getDerivedStateFromProps(nextProps, prevState) {
// I moved this logic here because if placed in ComponentDidMount
// the user would see Switches switch and this method fires before the component mounts
if (!prevState.activeTokensCalculated) {
const { activeTokens } = nextProps
return {
activeTokensAddresses: Set(activeTokens.map(({ address }) => address)),
activeTokensCalculated: true,
}
}
return null
}
componentWillUnmount() {
const { activeTokensAddresses } = this.state
const { updateActiveTokens, safeAddress } = this.props
updateActiveTokens(safeAddress, activeTokensAddresses.toList())
}
onCancelSearch = () => {
@ -58,14 +95,17 @@ class Tokens extends React.Component<Props, State> {
this.setState(() => ({ filter: value }))
}
onSwitch = (token: Token) => (e: SyntheticInputEvent<HTMLInputElement>) => {
const { checked } = e.target
const { safeAddress, enableToken, disableToken } = this.props
onSwitch = (token: Token) => () => {
const { activeTokensAddresses } = this.state
if (checked) {
enableToken(safeAddress, token)
if (activeTokensAddresses.has(token.address)) {
this.setState({
activeTokensAddresses: activeTokensAddresses.remove(token.address),
})
} else {
disableToken(safeAddress, token)
this.setState({
activeTokensAddresses: activeTokensAddresses.add(token.address),
})
}
}
@ -76,7 +116,7 @@ class Tokens extends React.Component<Props, State> {
render() {
const { onClose, classes, tokens } = this.props
const { filter } = this.state
const { filter, activeTokensAddresses } = this.state
const searchClasses = {
input: classes.searchInput,
root: classes.searchRoot,
@ -117,17 +157,23 @@ class Tokens extends React.Component<Props, State> {
<Hairline />
</Block>
<MuiList className={classes.list}>
{filteredTokens.map((token: Token) => (
{filteredTokens.map((token: Token) => {
const isActive = activeTokensAddresses.has(token.address)
return (
<ListItem key={token.address} className={classes.token}>
<ListItemIcon>
<Img src={token.logoUri} height={28} alt={token.name} onError={this.setImageToPlaceholder} />
</ListItemIcon>
<ListItemText primary={token.symbol} secondary={token.name} />
{token.address !== ETH_ADDRESS && (
<ListItemSecondaryAction>
<Switch onChange={this.onSwitch(token)} checked={token.status} />
<Switch onChange={this.onSwitch(token)} checked={isActive} />
</ListItemSecondaryAction>
)}
</ListItem>
))}
)
})}
</MuiList>
</React.Fragment>
)

View File

@ -17,9 +17,9 @@ export type BalanceRow = SortRow<BalanceData>
export const getBalanceData = (activeTokens: List<Token>): Array<BalanceRow> => {
const rows = activeTokens.map((token: Token) => ({
[BALANCE_TABLE_ASSET_ID]: token.get('name'),
[BALANCE_TABLE_BALANCE_ID]: `${token.get('funds')} ${token.get('symbol')}`,
[buildOrderFieldFrom(BALANCE_TABLE_BALANCE_ID)]: Number(token.get('funds')),
[BALANCE_TABLE_ASSET_ID]: token.name,
[BALANCE_TABLE_BALANCE_ID]: `${token.balance} ${token.symbol}`,
[buildOrderFieldFrom(BALANCE_TABLE_BALANCE_ID)]: Number(token.balance),
[FIXED]: token.get('symbol') === 'ETH',
}))

View File

@ -102,7 +102,12 @@ class Balances extends React.Component<Props, State> {
handleClose={this.onHide('Token')}
open={showToken}
>
<Tokens tokens={tokens} onClose={this.onHide('Token')} safeAddress={safeAddress} />
<Tokens
tokens={tokens}
onClose={this.onHide('Token')}
safeAddress={safeAddress}
activeTokens={activeTokens}
/>
</Modal>
</Col>
</Row>

View File

@ -94,8 +94,8 @@ class Layout extends React.Component<Props, State> {
if (!safe) {
return <NoSafe provider={provider} text="Safe not found" />
}
// <GnoSafe safe={safe} tokens={activeTokens} userAddress={userAddress} />
const address = safe.get('address')
const { address, ethBalance } = safe
return (
<React.Fragment>
@ -129,7 +129,13 @@ class Layout extends React.Component<Props, State> {
</Row>
<Hairline color="#c8ced4" />
{tabIndex === 0 && (
<Balances tokens={tokens} activeTokens={activeTokens} granted={granted} safeAddress={address} />
<Balances
ethBalance={ethBalance}
tokens={tokens}
activeTokens={activeTokens}
granted={granted}
safeAddress={address}
/>
)}
</React.Fragment>
)

View File

@ -2,7 +2,7 @@
import * as React from 'react'
import Stepper from '~/components/Stepper'
import { connect } from 'react-redux'
import { type Safe } from '~/routes/safe/store/model/safe'
import { type Safe } from '~/routes/safe/store/models/safe'
import { getSafeEthereumInstance, createTransaction } from '~/logic/safe/safeFrontendOperations'
import RemoveOwnerForm, { DECREASE_PARAM } from './RemoveOwnerForm'
import Review from './Review'

View File

@ -2,7 +2,7 @@
import { List } from 'immutable'
import { createStructuredSelector, createSelector } from 'reselect'
import { userAccountSelector } from '~/logic/wallets/store/selectors'
import { type Transaction } from '~/routes/safe/store/model/transaction'
import { type Transaction } from '~/routes/safe/store/models/transaction'
import { safeTransactionsSelector } from '~/routes/safe/store/selectors/index'
const pendingTransactionsSelector = createSelector(

View File

@ -15,7 +15,7 @@ import Delete from '@material-ui/icons/Delete'
import Person from '@material-ui/icons/Person'
import ExpandLess from '@material-ui/icons/ExpandLess'
import ExpandMore from '@material-ui/icons/ExpandMore'
import { type OwnerProps } from '~/routes/safe/store/model/owner'
import { type OwnerProps } from '~/routes/safe/store/models/owner'
import { type WithStyles } from '~/theme/mui'
import { sameAddress } from '~/logic/wallets/ethAddresses'

View File

@ -8,7 +8,7 @@ import Bold from '~/components/layout/Bold'
import Img from '~/components/layout/Img'
import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row'
import { type Safe } from '~/routes/safe/store/model/safe'
import { type Safe } from '~/routes/safe/store/models/safe'
import { type Token } from '~/logic/tokens/store/model/token'
import Transactions from '~/routes/safe/component/Transactions'

View File

@ -4,7 +4,7 @@ import { BigNumber } from 'bignumber.js'
import { connect } from 'react-redux'
import Stepper from '~/components/Stepper'
import { sleep } from '~/utils/timer'
import { type Safe } from '~/routes/safe/store/model/safe'
import { type Safe } from '~/routes/safe/store/models/safe'
import { getStandardTokenContract } from '~/logic/tokens/store/actions/fetchTokens'
import { type Token } from '~/logic/tokens/store/model/token'
import { isEther } from '~/logic/tokens/utils/tokens'

View File

@ -6,7 +6,7 @@ import OpenPaper from '~/components/Stepper/OpenPaper'
import Field from '~/components/forms/Field'
import TextField from '~/components/forms/TextField'
import { composeValidators, minValue, maxValue, mustBeInteger, required } from '~/components/forms/validator'
import { type Safe } from '~/routes/safe/store/model/safe'
import { type Safe } from '~/routes/safe/store/models/safe'
export const THRESHOLD_PARAM = 'threshold'

View File

@ -3,7 +3,7 @@ import * as React from 'react'
import Stepper from '~/components/Stepper'
import { connect } from 'react-redux'
import { getSafeEthereumInstance, createTransaction } from '~/logic/safe/safeFrontendOperations'
import { type Safe } from '~/routes/safe/store/model/safe'
import { type Safe } from '~/routes/safe/store/models/safe'
import ThresholdForm, { THRESHOLD_PARAM } from './ThresholdForm'
import selector, { type SelectorProps } from './selector'
import actions, { type Actions } from './actions'

View File

@ -14,7 +14,7 @@ import Person from '@material-ui/icons/Person'
import ExpandLess from '@material-ui/icons/ExpandLess'
import ExpandMore from '@material-ui/icons/ExpandMore'
import { type WithStyles } from '~/theme/mui'
import { type Confirmation, type ConfirmationProps } from '~/routes/safe/store/model/confirmation'
import { type Confirmation, type ConfirmationProps } from '~/routes/safe/store/models/confirmation'
const styles = {
nested: {

View File

@ -9,7 +9,7 @@ import ListItemText from '~/components/List/ListItemText'
import Avatar from '@material-ui/core/Avatar'
import Group from '@material-ui/icons/Group'
import MailOutline from '@material-ui/icons/MailOutline'
import { type Confirmation } from '~/routes/safe/store/model/confirmation'
import { type Confirmation } from '~/routes/safe/store/models/confirmation'
import Confirmations from './Confirmations'
type Props = {

View File

@ -16,11 +16,11 @@ import Atm from '@material-ui/icons/LocalAtm'
import DoneAll from '@material-ui/icons/DoneAll'
import CompareArrows from '@material-ui/icons/CompareArrows'
import Collapsed from '~/routes/safe/component/Transactions/Collapsed'
import { type Transaction } from '~/routes/safe/store/model/transaction'
import { type Transaction } from '~/routes/safe/store/models/transaction'
import Hairline from '~/components/layout/Hairline/index'
import Button from '~/components/layout/Button'
import { sameAddress } from '~/logic/wallets/ethAddresses'
import { type Confirmation } from '~/routes/safe/store/model/confirmation'
import { type Confirmation } from '~/routes/safe/store/models/confirmation'
import selector, { type SelectorProps } from './selector'
type Props = Open &

View File

@ -2,9 +2,9 @@
import { createStructuredSelector } from 'reselect'
import { confirmationsTransactionSelector } from '~/routes/safe/store/selectors/index'
import { userAccountSelector } from '~/logic/wallets/store/selectors'
import { type Transaction } from '~/routes/safe/store/model/transaction'
import { type Transaction } from '~/routes/safe/store/models/transaction'
import { type GlobalState } from '~/store'
import { type Confirmation } from '~/routes/safe/store/model/confirmation'
import { type Confirmation } from '~/routes/safe/store/models/confirmation'
export type SelectorProps = {
confirmed: confirmationsTransactionSelector,

View File

@ -2,11 +2,11 @@
import * as React from 'react'
import { List } from 'immutable'
import { connect } from 'react-redux'
import { type Transaction } from '~/routes/safe/store/model/transaction'
import { type Transaction } from '~/routes/safe/store/models/transaction'
import NoTransactions from '~/routes/safe/component/Transactions/NoTransactions'
import GnoTransaction from '~/routes/safe/component/Transactions/Transaction'
import { sameAddress } from '~/logic/wallets/ethAddresses'
import { type Confirmation } from '~/routes/safe/store/model/confirmation'
import { type Confirmation } from '~/routes/safe/store/models/confirmation'
import { processTransaction } from '~/logic/safe/safeFrontendOperations'
import selector, { type SelectorProps } from './selector'
import actions, { type Actions } from './actions'

View File

@ -1,7 +1,7 @@
// @flow
import { List } from 'immutable'
import { createStructuredSelector } from 'reselect'
import { type Transaction } from '~/routes/safe/store/model/transaction'
import { type Transaction } from '~/routes/safe/store/models/transaction'
import { safeTransactionsSelector } from '~/routes/safe/store/selectors/index'
import { userAccountSelector } from '~/logic/wallets/store/selectors'

View File

@ -1,13 +1,16 @@
// @flow
import fetchSafe from '~/routes/safe/store/actions/fetchSafe'
import { fetchTokens } from '~/logic/tokens/store/actions/fetchTokens'
import loadActiveTokens from '~/logic/tokens/store/actions/loadActiveTokens'
import fetchTokenBalances from '~/routes/safe/store/actions/fetchTokenBalances'
export type Actions = {
fetchSafe: typeof fetchSafe,
fetchTokens: typeof fetchTokens,
loadActiveTokens: typeof loadActiveTokens,
fetchTokenBalances: typeof fetchTokenBalances,
}
export default {
fetchSafe,
fetchTokens,
loadActiveTokens,
fetchTokenBalances,
}

View File

@ -6,34 +6,34 @@ import Layout from '~/routes/safe/component/Layout'
import selector, { type SelectorProps } from './selector'
import actions, { type Actions } from './actions'
type Props = Actions & SelectorProps & {
export type Props = Actions &
SelectorProps & {
granted: boolean,
}
}
const TIMEOUT = process.env.NODE_ENV === 'test' ? 1500 : 15000
class SafeView extends React.PureComponent<Props> {
class SafeView extends React.Component<Props> {
componentDidMount() {
const { safeUrl, fetchTokens, fetchSafe } = this.props
const {
fetchSafe, loadActiveTokens, activeTokens, safeUrl, fetchTokenBalances,
} = this.props
fetchSafe(safeUrl)
fetchTokens(safeUrl)
// loadActiveTokens(safeUrl)
fetchTokenBalances(safeUrl, activeTokens)
this.intervalId = setInterval(() => {
fetchTokens(safeUrl)
fetchSafe(safeUrl)
// update in another function so it uses up-to-date props values
this.checkForUpdates()
}, TIMEOUT)
}
componentDidUpdate(prevProps) {
const { safe, fetchTokens } = this.props
const { activeTokens } = this.props
if (prevProps.safe) {
return
}
if (safe) {
const safeAddress = safe.get('address')
fetchTokens(safeAddress)
if (activeTokens.size > prevProps.activeTokens.size) {
this.checkForUpdates()
}
}
@ -41,6 +41,15 @@ class SafeView extends React.PureComponent<Props> {
clearInterval(this.intervalId)
}
checkForUpdates() {
const {
safeUrl, activeTokens, fetchSafe, fetchTokenBalances,
} = this.props
fetchSafe(safeUrl, true)
fetchTokenBalances(safeUrl, activeTokens)
}
intervalId: IntervalID
render() {
@ -64,4 +73,7 @@ class SafeView extends React.PureComponent<Props> {
}
}
export default connect(selector, actions)(SafeView)
export default connect<Object, Object, ?Function, ?Object>(
selector,
actions,
)(SafeView)

View File

@ -1,15 +1,23 @@
// @flow
import { List } from 'immutable'
import { List, Map } from 'immutable'
import { createSelector, createStructuredSelector, type Selector } from 'reselect'
import { safeSelector, type RouterProps, type SafeSelectorProps } from '~/routes/safe/store/selectors'
import {
safeSelector,
safeActiveTokensSelector,
safeBalancesSelector,
type RouterProps,
type SafeSelectorProps,
} from '~/routes/safe/store/selectors'
import { providerNameSelector, userAccountSelector, networkSelector } from '~/logic/wallets/store/selectors'
import { type Safe } from '~/routes/safe/store/model/safe'
import { type Owner } from '~/routes/safe/store/model/owner'
import { type Safe } from '~/routes/safe/store/models/safe'
import { type Owner } from '~/routes/safe/store/models/owner'
import { type GlobalState } from '~/store'
import { sameAddress } from '~/logic/wallets/ethAddresses'
import { activeTokensSelector, orderedTokenListSelector } from '~/logic/tokens/store/selectors'
import { orderedTokenListSelector, tokensSelector } from '~/logic/tokens/store/selectors'
import { type Token } from '~/logic/tokens/store/model/token'
import { type TokenBalance } from '~/routes/safe/store/models/tokenBalance'
import { safeParamAddressSelector } from '../store/selectors'
import { getEthAsToken } from '~/logic/tokens/utils/tokenHelpers'
export type SelectorProps = {
safe: SafeSelectorProps,
@ -42,11 +50,52 @@ export const grantedSelector: Selector<GlobalState, RouterProps, boolean> = crea
},
)
export default createStructuredSelector({
type UserToken = {
address: string,
balance: string,
}
const safeEthAsTokenSelector: Selector<GlobalState, RouterProps, ?Token> = createSelector(
safeSelector,
(safe: Safe) => {
if (!safe) {
return undefined
}
return getEthAsToken(safe.ethBalance)
},
)
const extendedSafeTokensSelector: Selector<GlobalState, RouterProps, List<Token>> = createSelector(
safeActiveTokensSelector,
safeBalancesSelector,
tokensSelector,
safeEthAsTokenSelector,
(safeTokens: List<string>, balances: List<TokenBalance>, tokensList: Map<string, Token>, ethAsToken: Token) => {
const extendedTokens = Map().withMutations((map) => {
safeTokens.forEach((tokenAddress: string) => {
const baseToken = tokensList.get(tokenAddress)
const tokenBalance = balances.find(tknBalance => tknBalance.address === tokenAddress)
if (baseToken) {
map.set(tokenAddress, baseToken.set('balance', tokenBalance ? tokenBalance.balance : '0'))
}
})
if (ethAsToken) {
map.set(ethAsToken.address, ethAsToken)
}
})
return extendedTokens.toList()
},
)
export default createStructuredSelector<Object, *>({
safe: safeSelector,
provider: providerNameSelector,
tokens: orderedTokenListSelector,
activeTokens: activeTokensSelector,
activeTokens: extendedSafeTokensSelector,
granted: grantedSelector,
userAddress: userAccountSelector,
network: networkSelector,

View File

@ -1,12 +1,10 @@
// @flow
import { List } from 'immutable'
import { createAction } from 'redux-actions'
import { type Safe, makeSafe } from '~/routes/safe/store/model/safe'
import { saveSafes, setOwners } from '~/logic/safe/utils'
import { makeOwner, type Owner } from '~/routes/safe/store/model/owner'
import type { Dispatch as ReduxDispatch, GetState } from 'redux'
import { type GlobalState } from '~/store/index'
import { safesMapSelector } from '~/routes/safeList/store/selectors/index'
import type { Dispatch as ReduxDispatch } from 'redux'
import { type GlobalState } from '~/store'
import SafeRecord, { type Safe } from '~/routes/safe/store/models/safe'
import { makeOwner, type Owner } from '~/routes/safe/store/models/owner'
export const ADD_SAFE = 'ADD_SAFE'
@ -20,7 +18,7 @@ type ActionReturn = {
safe: Safe,
}
export const addSafe = createAction<string, *, *>(
export const addSafe = createAction<string, Function, ActionReturn>(
ADD_SAFE,
(safe: Safe): ActionReturn => ({
safe,
@ -33,21 +31,15 @@ const saveSafe = (
threshold: number,
ownersName: string[],
ownersAddress: string[],
) => async (dispatch: ReduxDispatch<GlobalState>, getState: GetState<GlobalState>) => {
) => async (dispatch: ReduxDispatch<GlobalState>) => {
const owners: List<Owner> = buildOwnersFrom(ownersName, ownersAddress)
const state: GlobalState = getState()
const safe: Safe = makeSafe({
const safe: Safe = SafeRecord({
name,
address,
threshold,
owners,
})
const safes = safesMapSelector(state)
const newSafes = safes.set(address, safe)
setOwners(address, owners)
saveSafes(newSafes.toJSON())
dispatch(addSafe(safe))
}

View File

@ -2,14 +2,18 @@
import type { Dispatch as ReduxDispatch } from 'redux'
import { List, Map } from 'immutable'
import { type GlobalState } from '~/store/index'
import { makeOwner } from '~/routes/safe/store/model/owner'
import { type SafeProps, makeSafe } from '~/routes/safe/store/model/safe'
import updateSafe from '~/routes/safe/store/actions/updateSafe'
import { makeOwner } from '~/routes/safe/store/models/owner'
import type { SafeProps } from '~/routes/safe/store/models/safe'
import { addSafe } from '~/routes/safe/store/actions/addSafe'
import { getOwners, getSafeName } from '~/logic/safe/utils'
import { getGnosisSafeContract } from '~/logic/contracts/safeContracts'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { getWeb3, getBalanceInEtherOf } from '~/logic/wallets/getWeb3'
import updateSafe from '~/routes/safe/store/actions/updateSafe'
const buildOwnersFrom = (safeOwners: string[], storedOwners: Map<string, string>) => safeOwners.map((ownerAddress: string) => {
const buildOwnersFrom = (
safeOwners: string[],
storedOwners: Map<string, string>, // eslint-disable-next-line
) => safeOwners.map((ownerAddress: string) => {
const ownerName = storedOwners.get(ownerAddress.toLowerCase()) || 'UNKNOWN'
return makeOwner({ name: ownerName, address: ownerAddress })
})
@ -18,6 +22,7 @@ export const buildSafe = async (safeAddress: string, safeName: string) => {
const web3 = getWeb3()
const SafeContract = await getGnosisSafeContract(web3)
const gnosisSafe = await SafeContract.at(safeAddress)
const ethBalance = await getBalanceInEtherOf(safeAddress)
const threshold = Number(await gnosisSafe.getThreshold())
const owners = List(buildOwnersFrom(await gnosisSafe.getOwners(), await getOwners(safeAddress)))
@ -27,17 +32,22 @@ export const buildSafe = async (safeAddress: string, safeName: string) => {
name: safeName,
threshold,
owners,
ethBalance,
}
return makeSafe(safe)
return safe
}
export default (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>) => {
export default (safeAddress: string, update: boolean = false) => async (dispatch: ReduxDispatch<GlobalState>) => {
try {
const safeName = await getSafeName(safeAddress) || 'LOADED SAFE'
const safeRecord = await buildSafe(safeAddress, safeName)
const safeName = (await getSafeName(safeAddress)) || 'LOADED SAFE'
const safeProps: SafeProps = await buildSafe(safeAddress, safeName)
return dispatch(updateSafe(safeRecord))
if (update) {
dispatch(updateSafe(safeProps))
} else {
dispatch(addSafe(safeProps))
}
} catch (err) {
// eslint-disable-next-line
console.error('Error while updating safe information: ', err)

View File

@ -0,0 +1,53 @@
// @flow
import type { Dispatch as ReduxDispatch } from 'redux'
import { List } from 'immutable'
import { BigNumber } from 'bignumber.js'
import { type GlobalState } from '~/store/index'
import { type Token } from '~/logic/tokens/store/model/token'
import TokenBalanceRecord from '~/routes/safe/store/models/tokenBalance'
import { getStandardTokenContract } from '~/logic/tokens/store/actions/fetchTokens'
import updateSafe from './updateSafe'
import { ETH_ADDRESS } from '~/logic/tokens/utils/tokenHelpers'
export const calculateBalanceOf = async (tokenAddress: string, safeAddress: string, decimals: number = 18) => {
if (tokenAddress === ETH_ADDRESS) {
return '0'
}
const erc20Token = await getStandardTokenContract()
let balance = 0
try {
const token = await erc20Token.at(tokenAddress)
balance = await token.balanceOf(safeAddress)
} catch (err) {
console.error('Failed to fetch token balances: ', err)
}
return new BigNumber(balance).div(10 ** decimals).toString()
}
const fetchTokenBalances = (safeAddress: string, tokens: List<Token>) => async (
dispatch: ReduxDispatch<GlobalState>,
) => {
if (!safeAddress || !tokens || !tokens.size) {
return
}
try {
const withBalances = await Promise.all(
tokens
.map(async token => TokenBalanceRecord({
address: token.address,
balance: await calculateBalanceOf(token.address, safeAddress, token.decimals),
})),
)
dispatch(updateSafe({ address: safeAddress, balances: List(withBalances) }))
} catch (err) {
// eslint-disable-next-line
console.error('Error while loading active tokens from storage:', err)
}
}
export default fetchTokenBalances

View File

@ -3,9 +3,9 @@ import { List, Map } from 'immutable'
import axios from 'axios'
import type { Dispatch as ReduxDispatch } from 'redux'
import { type GlobalState } from '~/store/index'
import { makeOwner } from '~/routes/safe/store/model/owner'
import { makeTransaction, type Transaction } from '~/routes/safe/store/model/transaction'
import { makeConfirmation } from '~/routes/safe/store/model/confirmation'
import { makeOwner } from '~/routes/safe/store/models/owner'
import { makeTransaction, type Transaction } from '~/routes/safe/store/models/transaction'
import { makeConfirmation } from '~/routes/safe/store/models/confirmation'
import { loadSafeSubjects } from '~/utils/storage/transactions'
import { buildTxServiceUrlFrom, type TxServiceType } from '~/logic/safe/safeTxHistory'
import { getOwners } from '~/logic/safe/utils'

View File

@ -2,12 +2,12 @@
import type { Dispatch as ReduxDispatch } from 'redux'
import { type GlobalState } from '~/store/index'
import { SAFES_KEY } from '~/logic/safe/utils'
import { type SafeProps } from '~/routes/safe/store/model/safe'
import { type SafeProps } from '~/routes/safe/store/models/safe'
import { loadFromStorage } from '~/utils/storage'
import { addSafe } from './addSafe'
import { buildSafe } from '~/routes/safe/store/reducer/safe'
export default () => async (dispatch: ReduxDispatch<GlobalState>) => {
const loadSafesFromStorage = () => async (dispatch: ReduxDispatch<GlobalState>) => {
try {
const safes: ?{ [string]: SafeProps } = await loadFromStorage(SAFES_KEY)
@ -23,3 +23,5 @@ export default () => async (dispatch: ReduxDispatch<GlobalState>) => {
return Promise.resolve()
}
export default loadSafesFromStorage

View File

@ -0,0 +1,23 @@
// @flow
import { List } from 'immutable'
import type { Dispatch as ReduxDispatch } from 'redux'
import { type GlobalState } from '~/store'
import updateSafe from './updateSafe'
// the selector uses ownProps argument/router props to get the address of the safe
// so in order to use it I had to recreate the same structure
// const generateMatchProps = (safeAddress: string) => ({
// match: {
// params: {
// [SAFE_PARAM_ADDRESS]: safeAddress,
// },
// },
// })
const updateActiveTokens = (safeAddress: string, activeTokens: List<string>) => async (
dispatch: ReduxDispatch<GlobalState>,
) => {
dispatch(updateSafe({ address: safeAddress, activeTokens }))
}
export default updateActiveTokens

View File

@ -1,8 +0,0 @@
// @flow
import { createAction } from 'redux-actions'
export const UPDATE_SAFES = 'UPDATE_SAFES'
const updateSafesInBatch = createAction<string, *>(UPDATE_SAFES)
export default updateSafesInBatch

View File

@ -0,0 +1,48 @@
// @flow
import { ADD_SAFE } from '~/routes/safe/store/actions/addSafe'
import { UPDATE_SAFE } from '~/routes/safe/store/actions/updateSafe'
import type { Store, AnyAction } from 'redux'
import { type GlobalState } from '~/store/'
import { saveSafes, setOwners } from '~/logic/safe/utils'
import { safesMapSelector } from '~/routes/safeList/store/selectors'
import { getActiveTokensAddressesForAllSafes } from '~/routes/safe/store/selectors'
import { tokensSelector } from '~/logic/tokens/store/selectors'
import type { Token } from '~/logic/tokens/store/model/token'
import { saveActiveTokens } from '~/logic/tokens/utils/tokensStorage'
const watchedActions = [ADD_SAFE, UPDATE_SAFE]
const safeStorageMware = (store: Store<GlobalState>) => (next: Function) => async (action: AnyAction) => {
const handledAction = next(action)
if (watchedActions.includes(action.type)) {
const state: GlobalState = store.getState()
const safes = safesMapSelector(state)
saveSafes(safes.toJSON())
// recalculate active tokens
if (action.payload.activeTokens) {
const tokens = tokensSelector(state)
const activeTokenAddresses = getActiveTokensAddressesForAllSafes(state)
const activeTokens = tokens.withMutations((map) => {
map.forEach((token: Token) => {
if (!activeTokenAddresses.has(token.address)) {
map.remove(token.address)
}
})
})
saveActiveTokens(activeTokens)
}
if (action.type === ADD_SAFE) {
const { safe } = action.payload
setOwners(safe.address, safe.owners)
}
}
return handledAction
}
export default safeStorageMware

View File

@ -1,22 +0,0 @@
// @flow
import { List, Record } from 'immutable'
import type { RecordFactory, RecordOf } from 'immutable'
import type { Owner } from '~/routes/safe/store/model/owner'
export type SafeProps = {
name: string,
address: string,
threshold: number,
owners: List<Owner>,
}
export const makeSafe: RecordFactory<SafeProps> = Record({
name: '',
address: '',
threshold: 0,
owners: List([]),
})
export type Safe = RecordOf<SafeProps>
// Useage const someRecord: Safe = makeSafe({ name: ... })

View File

@ -1,7 +1,7 @@
// @flow
import { Record } from 'immutable'
import type { RecordFactory, RecordOf } from 'immutable'
import { makeOwner, type Owner } from '~/routes/safe/store/model/owner'
import { makeOwner, type Owner } from '~/routes/safe/store/models/owner'
import { type TxServiceType } from '~/logic/safe/safeTxHistory'
export type ConfirmationProps = {

View File

@ -0,0 +1,29 @@
// @flow
import { List, Record } from 'immutable'
import type { RecordFactory, RecordOf } from 'immutable'
import type { Owner } from '~/routes/safe/store/models/owner'
import TokenBalance from '~/routes/safe/store/models/tokenBalance'
export type SafeProps = {
name: string,
address: string,
threshold: number,
owners: List<Owner>,
balances?: List<TokenBalance>,
activeTokens?: List<string>,
ethBalance?: string,
}
const SafeRecord: RecordFactory<SafeProps> = Record({
name: '',
address: '',
threshold: 0,
ethBalance: 0,
owners: List([]),
activeTokens: List([]),
balances: List([]),
})
export type Safe = RecordOf<SafeProps>
export default SafeRecord

View File

@ -0,0 +1,17 @@
// @flow
import { Record } from 'immutable'
import type { RecordFactory, RecordOf } from 'immutable'
export type TokenBalanceProps = {
address: string,
balance: string,
}
const TokenBalanceRecord: RecordFactory<TokenBalanceProps> = Record({
address: '',
balance: '0',
})
export type TokenBalance = RecordOf<TokenBalanceProps>
export default TokenBalanceRecord

View File

@ -1,7 +1,7 @@
// @flow
import { List, Record } from 'immutable'
import type { RecordFactory, RecordOf } from 'immutable'
import { type Confirmation } from '~/routes/safe/store/model/confirmation'
import { type Confirmation } from '~/routes/safe/store/models/confirmation'
export type TransactionProps = {
name: string,

View File

@ -1,9 +1,10 @@
// @flow
import { Map } from 'immutable'
import { Map, List } from 'immutable'
import { handleActions, type ActionType } from 'redux-actions'
import { ADD_SAFE, buildOwnersFrom } from '~/routes/safe/store/actions/addSafe'
import { type Safe, type SafeProps, makeSafe } from '~/routes/safe/store/model/safe'
import { type OwnerProps } from '~/routes/safe/store/model/owner'
import SafeRecord, { type Safe, type SafeProps } from '~/routes/safe/store/models/safe'
import TokenBalance from '~/routes/safe/store/models/tokenBalance'
import { type OwnerProps } from '~/routes/safe/store/models/owner'
import { loadFromStorage } from '~/utils/storage'
import { SAFES_KEY } from '~/logic/safe/utils'
import { UPDATE_SAFE } from '~/routes/safe/store/actions/updateSafe'
@ -16,15 +17,17 @@ export const buildSafe = (storedSafe: SafeProps) => {
const names = storedSafe.owners.map((owner: OwnerProps) => owner.name)
const addresses = storedSafe.owners.map((owner: OwnerProps) => owner.address)
const owners = buildOwnersFrom(Array.from(names), Array.from(addresses))
const activeTokens = List(storedSafe.activeTokens)
const balances = storedSafe.balances.map(balance => TokenBalance(balance))
const safe: SafeProps = {
address: storedSafe.address,
name: storedSafe.name,
threshold: storedSafe.threshold,
...storedSafe,
owners,
balances,
activeTokens,
}
return makeSafe(safe)
return safe
}
const buildSafesFrom = (loadedSafes: Object): Map<string, Safe> => {
@ -35,7 +38,7 @@ const buildSafesFrom = (loadedSafes: Object): Map<string, Safe> => {
const safeRecords = keys.map((address: string) => buildSafe(loadedSafes[address]))
return safes.withMutations(async (map) => {
safeRecords.forEach((safe: Safe) => map.set(safe.get('address'), safe))
safeRecords.forEach((safe: SafeProps) => map.set(safe.address, safe))
})
} catch (err) {
// eslint-disable-next-line
@ -56,19 +59,22 @@ export default handleActions<State, *>(
{
[UPDATE_SAFE]: (state: State, action: ActionType<Function>): State => {
const safe = action.payload
const safeAddress = safe.get('address')
const safeAddress = safe.address
const hasSafe = !!state.get(safeAddress)
if (hasSafe) {
return state.update(safeAddress, prevSafe => (prevSafe.equals(safe) ? prevSafe : safe))
}
return state.set(safeAddress, safe)
return state.update(safeAddress, prevSafe => prevSafe.merge(safe))
},
[ADD_SAFE]: (state: State, action: ActionType<Function>): State => {
const { safe }: { safe: Safe } = action.payload
const { safe }: { safe: SafeProps } = action.payload
return state.set(safe.address, safe)
// if you add a new safe it needs to be set as a record
// in case of update it shouldn't, because a record would be initialized
// with initial props and it would overwrite existing ones
if (state.has(safe.address)) {
return state.update(safe.address, prevSafe => prevSafe.merge(safe))
}
return state.set(safe.address, SafeRecord(safe))
},
},
Map(),

View File

@ -2,7 +2,7 @@
import { List, Map } from 'immutable'
import { handleActions, type ActionType } from 'redux-actions'
import addTransactions, { ADD_TRANSACTIONS } from '~/routes/safe/store/actions/addTransactions'
import { type Transaction } from '~/routes/safe/store/model/transaction'
import { type Transaction } from '~/routes/safe/store/models/transaction'
export const TRANSACTIONS_REDUCER_ID = 'transactions'

View File

@ -1,14 +1,15 @@
// @flow
import { Map, List } from 'immutable'
import { Map, List, Set } from 'immutable'
import { type Match } from 'react-router-dom'
import { createSelector, createStructuredSelector, type Selector } from 'reselect'
import { type GlobalState } from '~/store/index'
import { SAFE_PARAM_ADDRESS } from '~/routes/routes'
import { type Safe } from '~/routes/safe/store/model/safe'
import { type Safe } from '~/routes/safe/store/models/safe'
import { safesMapSelector } from '~/routes/safeList/store/selectors'
import { type State as TransactionsState, TRANSACTIONS_REDUCER_ID } from '~/routes/safe/store/reducer/transactions'
import { type Transaction } from '~/routes/safe/store/model/transaction'
import { type Confirmation } from '~/routes/safe/store/model/confirmation'
import { type Transaction } from '~/routes/safe/store/models/transaction'
import { type Confirmation } from '~/routes/safe/store/models/confirmation'
import { safesListSelector } from '~/routes/safeList/store/selectors/'
export type RouterProps = {
match: Match,
@ -78,6 +79,44 @@ export const safeSelector: Selector<GlobalState, RouterProps, SafeSelectorProps>
},
)
export default createStructuredSelector({
export const safeActiveTokensSelector: Selector<GlobalState, RouterProps, List<string>> = createSelector(
safeSelector,
(safe: Safe) => {
if (!safe) {
return List()
}
return safe.activeTokens
},
)
export const safeBalancesSelector: Selector<GlobalState, RouterProps, Map<string, string>> = createSelector(
safeSelector,
(safe: Safe) => {
if (!safe) {
return List()
}
return safe.balances
},
)
export const getActiveTokensAddressesForAllSafes: Selector<GlobalState, any, Set<string>> = createSelector(
safesListSelector,
(safes: List<Safe>) => {
const addresses = Set().withMutations((set) => {
safes.forEach((safe: Safe) => {
safe.activeTokens.forEach((tokenAddress) => {
set.add(tokenAddress)
})
})
})
return addresses
},
)
export default createStructuredSelector<Object, *>({
safe: safeSelector,
tokens: safeActiveTokensSelector,
})

View File

@ -1,12 +1,12 @@
// @flow
import { makeSafe, type Safe } from '~/routes/safe/store/model/safe'
import SafeRecord, { type Safe } from '~/routes/safe/store/models/safe'
import { buildOwnersFrom } from '~/routes/safe/store/actions/addSafe'
class SafeBuilder {
safe: Safe
constructor() {
this.safe = makeSafe()
this.safe = SafeRecord()
}
withAddress(address: string) {

View File

@ -2,7 +2,7 @@
import { Map } from 'immutable'
import { type Match } from 'react-router-dom'
import { SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe'
import { type Safe } from '~/routes/safe/store/model/safe'
import { type Safe } from '~/routes/safe/store/models/safe'
import { SafeFactory } from '~/routes/safe/store/test/builder/safe.builder'
import { buildMathPropsFrom } from '~/test/utils/buildReactRouterProps'
import { getProviderInfo } from '~/logic/wallets/getWeb3'

View File

@ -2,7 +2,7 @@
import { Map } from 'immutable'
import { type Match } from 'react-router-dom'
import { SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe'
import { type Safe } from '~/routes/safe/store/model/safe'
import { type Safe } from '~/routes/safe/store/models/safe'
import { SafeFactory } from '~/routes/safe/store/test/builder/safe.builder'
import { buildMathPropsFrom } from '~/test/utils/buildReactRouterProps'
import { safeSelector } from '../selectors'

View File

@ -2,7 +2,7 @@
import { List } from 'immutable'
import * as React from 'react'
import NoSafe from '~/components/NoSafe'
import { type Safe } from '~/routes/safe/store/model/safe'
import { type Safe } from '~/routes/safe/store/models/safe'
import SafeTable from '~/routes/safeList/components/SafeTable'
type Props = {

View File

@ -6,7 +6,7 @@ import Link from '~/components/layout/Link'
import Table, {
TableBody, TableCell, TableHead, TableRow,
} from '~/components/layout/Table'
import { type Safe } from '~/routes/safe/store/model/safe'
import { type Safe } from '~/routes/safe/store/models/safe'
import { SAFELIST_ADDRESS } from '~/routes/routes'
type Props = {

View File

@ -3,7 +3,7 @@ import { List } from 'immutable'
import * as React from 'react'
import { connect } from 'react-redux'
import Page from '~/components/layout/Page'
import { type Safe } from '~/routes/safe/store/model/safe'
import { type Safe } from '~/routes/safe/store/models/safe'
import Layout from '../components/Layout'
import selector from './selector'

View File

@ -1,9 +1,9 @@
// @flow
import { createStructuredSelector } from 'reselect'
import { safesByOwnerSelector } from '~/routes/safeList/store/selectors'
import { safesListSelector } from '~/routes/safeList/store/selectors'
import { providerNameSelector } from '~/logic/wallets/store/selectors'
export default createStructuredSelector({
safes: safesByOwnerSelector,
export default createStructuredSelector<Object, *>({
safes: safesListSelector,
provider: providerNameSelector,
})

View File

@ -2,23 +2,12 @@
import { List, Map } from 'immutable'
import { createSelector, type Selector } from 'reselect'
import { type GlobalState } from '~/store/index'
import { type Safe } from '~/routes/safe/store/model/safe'
import { userAccountSelector } from '~/logic/wallets/store/selectors'
import { type Owner } from '~/routes/safe/store/model/owner'
import { sameAddress } from '~/logic/wallets/ethAddresses'
import { type Safe } from '~/routes/safe/store/models/safe'
import { SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe'
export const safesMapSelector = (state: GlobalState): Map<string, Safe> => state[SAFE_REDUCER_ID]
const safesListSelector: Selector<GlobalState, {}, List<Safe>> = createSelector(
export const safesListSelector: Selector<GlobalState, {}, List<Safe>> = createSelector(
safesMapSelector,
(safes: Map<string, Safe>): List<Safe> => safes.toList(),
)
export const safesByOwnerSelector: Selector<GlobalState, {}, List<Safe>> = createSelector(
userAccountSelector,
safesListSelector,
(userAddress: string, safes: List<Safe>): List<Safe> => safes.filter(
(safe: Safe) => safe.owners.filter((owner: Owner) => sameAddress(owner.get('address'), userAddress)).count() > 0,
),
)

View File

@ -1,7 +1,7 @@
// @flow
import { List, Map } from 'immutable'
import { SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe'
import { type Safe } from '~/routes/safe/store/model/safe'
import { type Safe } from '~/routes/safe/store/models/safe'
import { getProviderInfo } from '~/logic/wallets/getWeb3'
import { SafeFactory } from '~/routes/safe/store/test/builder/safe.builder'
import { PROVIDER_REDUCER_ID } from '~/logic/wallets/store/reducer/provider'

View File

@ -7,6 +7,7 @@ import {
import thunk from 'redux-thunk'
import provider, { PROVIDER_REDUCER_ID, type State as ProviderState } from '~/logic/wallets/store/reducer/provider'
import safe, { SAFE_REDUCER_ID, type State as SafeState } from '~/routes/safe/store/reducer/safe'
import safeStorage from '~/routes/safe/store/middleware/safeStorage'
import tokens, { TOKEN_REDUCER_ID, type State as TokensState } from '~/logic/tokens/store/reducer/tokens'
import transactions, {
type State as TransactionsState,
@ -17,7 +18,7 @@ export const history = createBrowserHistory()
// eslint-disable-next-line
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const finalCreateStore = composeEnhancers(applyMiddleware(thunk, routerMiddleware(history)))
const finalCreateStore = composeEnhancers(applyMiddleware(thunk, routerMiddleware(history), safeStorage))
export type GlobalState = {
providers: ProviderState,

View File

@ -1,5 +1,5 @@
// @flow
import { makeSafe, type Safe } from '~/routes/safe/store/model/safe'
import SafeRecord, { type Safe } from '~/routes/safe/store/models/safe'
import addSafe, { buildOwnersFrom } from '~/routes/safe/store/actions/addSafe'
import {
FIELD_NAME,
@ -18,7 +18,7 @@ class SafeBuilder {
safe: Safe
constructor() {
this.safe = makeSafe()
this.safe = SafeRecord()
}
withAddress(address: string) {

View File

@ -1,12 +1,12 @@
// @flow
import { Map, List } from 'immutable'
import { type Safe } from '~/routes/safe/store/model/safe'
import { type Safe } from '~/routes/safe/store/models/safe'
import { aNewStore } from '~/store'
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
import updateSafe from '~/routes/safe/store/actions/updateSafe'
import { loadSafe } from '~/routes/load/container/Load'
import { safesMapSelector } from '~/routes/safeList/store/selectors'
import { makeOwner, type Owner } from '~/routes/safe/store/model/owner'
import { makeOwner, type Owner } from '~/routes/safe/store/models/owner'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { safesInitialState } from '~/routes/safe/store/reducer/safe'
import { setOwners, OWNERS_KEY } from '~/utils/storage'

View File

@ -4,7 +4,7 @@ import { aNewStore } from '~/store'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { confirmationsTransactionSelector, safeTransactionsSelector } from '~/routes/safe/store/selectors'
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
import { type Safe } from '~/routes/safe/store/model/safe'
import { type Safe } from '~/routes/safe/store/models/safe'
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
import { NAME_PARAM, OWNER_ADDRESS_PARAM, INCREASE_PARAM } from '~/routes/safe/component/AddOwner/AddOwnerForm'

View File

@ -1,10 +1,10 @@
// @flow
import { List } from 'immutable'
import { getSafeEthereumInstance, createTransaction } from '~/logic/safe/safeFrontendOperations'
import { type Safe } from '~/routes/safe/store/model/safe'
import { makeOwner } from '~/routes/safe/store/model/owner'
import { type Safe } from '~/routes/safe/store/models/safe'
import { makeOwner } from '~/routes/safe/store/models/owner'
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
import { makeConfirmation } from '~/routes/safe/store/model/confirmation'
import { makeConfirmation } from '~/routes/safe/store/models/confirmation'
import { aNewStore } from '~/store'
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
import { getSafeFrom } from '~/test/utils/safeHelper'

View File

@ -10,7 +10,7 @@ import { aMinedSafe } from '~/test/builder/safe.redux.builder'
import { travelToTokens } from '~/test/builder/safe.dom.utils'
import { sleep } from '~/utils/timer'
import { buildMathPropsFrom } from '~/test/utils/buildReactRouterProps'
import { tokenListSelector, activeTokensSelector } from '~/logic/tokens/store/selectors'
import { tokenListSelector } from '~/logic/tokens/store/selectors'
import { getActiveTokenAddresses } from '~/logic/tokens/utils/tokensStorage'
import { enableFirstToken, testToken } from '~/test/builder/tokens.dom.utils'
import * as fetchTokensModule from '~/logic/tokens/store/actions/fetchTokens'

View File

@ -10,7 +10,6 @@ import { testToken } from '~/test/builder/tokens.dom.utils'
import * as fetchTokensModule from '~/logic/tokens/store/actions/fetchTokens'
import * as enhancedFetchModule from '~/utils/fetch'
import addToken from '~/logic/tokens/store/actions/addToken'
import { activeTokensSelector } from '~/logic/tokens/store/selectors'
describe('DOM > Feature > Add new ERC 20 Tokens', () => {
// let web3

View File

@ -10,7 +10,6 @@ import { testToken } from '~/test/builder/tokens.dom.utils'
import * as fetchTokensModule from '~/logic/tokens/store/actions/fetchTokens'
import * as enhancedFetchModule from '~/utils/fetch'
import addToken from '~/logic/tokens/store/actions/addToken'
import { activeTokensSelector, tokenListSelector } from '~/logic/tokens/store/selectors'
import removeTokenAction from '~/logic/tokens/store/actions/removeToken'
import { makeToken } from '~/logic/tokens/store/model/token'

View File

@ -1,7 +1,7 @@
// @flow
import { List, Map } from 'immutable'
import { type Confirmation } from '~/routes/safe/store/model/confirmation'
import { type Transaction } from '~/routes/safe/store/model/transaction'
import { type Confirmation } from '~/routes/safe/store/models/confirmation'
import { type Transaction } from '~/routes/safe/store/models/transaction'
import { sameAddress } from '~/logic/wallets/ethAddresses'
export const testSizeOfSafesWith = (transactions: Map<string, List<Transaction>>, size: number) => {

View File

@ -3,7 +3,7 @@ import { buildMathPropsFrom } from '~/test/utils/buildReactRouterProps'
import { safeSelector } from '~/routes/safe/store/selectors/index'
import { type Match } from 'react-router-dom'
import { type GlobalState } from '~/store'
import { type Safe } from '~/routes/safe/store/model/safe'
import { type Safe } from '~/routes/safe/store/models/safe'
export const getSafeFrom = (state: GlobalState, safeAddress: string): Safe => {
const match: Match = buildMathPropsFrom(safeAddress)

View File

@ -1,9 +1,10 @@
// @flow
import {
ImmortalStorage, CookieStore, IndexedDbStore, LocalStorageStore, SessionStorageStore,
} from 'immortal-db'
import { ImmortalStorage, IndexedDbStore, LocalStorageStore } from 'immortal-db'
const stores = [CookieStore, IndexedDbStore, LocalStorageStore, SessionStorageStore]
// Don't use sessionStorage and cookieStorage
// https://github.com/gruns/ImmortalDB/issues/22
// https://github.com/gruns/ImmortalDB/issues/6
const stores = [IndexedDbStore, LocalStorageStore]
export const storage = new ImmortalStorage(stores)
const PREFIX = 'v1'

1455
yarn.lock

File diff suppressed because it is too large Load Diff