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": "^16.8.6",
"react-dom": "^16.8.6", "react-dom": "^16.8.6",
"react-final-form": "^4.1.0", "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-infinite-scroll-component": "^4.5.2",
"react-redux": "^6.0.1", "react-redux": "7.0.2",
"react-router-dom": "^4.3.1", "react-router-dom": "^4.3.1",
"recompose": "^0.30.0", "recompose": "^0.30.0",
"redux": "^4.0.1", "redux": "^4.0.1",
@ -117,11 +117,11 @@
"@babel/preset-flow": "^7.0.0-beta.40", "@babel/preset-flow": "^7.0.0-beta.40",
"@babel/preset-react": "^7.0.0-beta.40", "@babel/preset-react": "^7.0.0-beta.40",
"@sambego/storybook-state": "^1.0.7", "@sambego/storybook-state": "^1.0.7",
"@storybook/addon-actions": "^5.0.6", "@storybook/addon-actions": "5.0.9",
"@storybook/addon-knobs": "^5.0.6", "@storybook/addon-knobs": "5.0.9",
"@storybook/addon-links": "^5.0.6", "@storybook/addon-links": "5.0.9",
"@storybook/react": "^5.0.6", "@storybook/react": "5.0.9",
"autoprefixer": "^9.4.10", "autoprefixer": "9.5.1",
"babel-core": "^7.0.0-bridge.0", "babel-core": "^7.0.0-bridge.0",
"babel-eslint": "^10.0.1", "babel-eslint": "^10.0.1",
"babel-jest": "^24.1.0", "babel-jest": "^24.1.0",
@ -134,38 +134,38 @@
"detect-port": "^1.2.2", "detect-port": "^1.2.2",
"eslint": "^5.16.0", "eslint": "^5.16.0",
"eslint-config-airbnb": "^17.1.0", "eslint-config-airbnb": "^17.1.0",
"eslint-plugin-flowtype": "^3.4.2", "eslint-plugin-flowtype": "3.6.1",
"eslint-plugin-import": "^2.9.0", "eslint-plugin-import": "2.17.2",
"eslint-plugin-jest": "^22.3.0", "eslint-plugin-jest": "^22.3.0",
"eslint-plugin-jsx-a11y": "^6.0.3", "eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-react": "^7.7.0", "eslint-plugin-react": "^7.7.0",
"ethereumjs-abi": "^0.6.7", "ethereumjs-abi": "^0.6.7",
"extract-text-webpack-plugin": "^4.0.0-beta.0", "extract-text-webpack-plugin": "^4.0.0-beta.0",
"file-loader": "^3.0.1", "file-loader": "^3.0.1",
"flow-bin": "0.96.0", "flow-bin": "0.97.0",
"fs-extra": "^7.0.1", "fs-extra": "^7.0.1",
"html-loader": "^0.5.5", "html-loader": "^0.5.5",
"html-webpack-plugin": "^3.0.4", "html-webpack-plugin": "^3.0.4",
"jest": "^24.1.0", "jest": "^24.1.0",
"json-loader": "^0.5.7", "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-loader": "^3.0.0",
"postcss-mixins": "^6.2.0", "postcss-mixins": "^6.2.0",
"postcss-simple-vars": "^5.0.2", "postcss-simple-vars": "^5.0.2",
"pre-commit": "^1.2.2", "pre-commit": "^1.2.2",
"prettier-eslint-cli": "^4.7.1", "prettier-eslint-cli": "^4.7.1",
"run-with-testrpc": "^0.3.0", "run-with-testrpc": "0.3.1",
"storybook-host": "^5.0.3", "storybook-host": "^5.0.3",
"storybook-router": "^0.3.3", "storybook-router": "^0.3.3",
"style-loader": "^0.23.1", "style-loader": "^0.23.1",
"truffle": "^5.0.10", "truffle": "5.0.12",
"truffle-contract": "^4.0.11", "truffle-contract": "^4.0.11",
"truffle-solidity-loader": "^0.1.10", "truffle-solidity-loader": "0.1.12",
"uglifyjs-webpack-plugin": "^2.1.2", "uglifyjs-webpack-plugin": "^2.1.2",
"webpack": "^4.1.1", "webpack": "^4.1.1",
"webpack-bundle-analyzer": "^3.1.0", "webpack-bundle-analyzer": "3.3.2",
"webpack-cli": "^3.2.3", "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" "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 What things you need to install the software and how to install them
``` ```
npm install truffle // recommended usage of -g flag yarn add truffle // recommended usage of -g flag
npm install ganache-cli // recommended usage of -g flag yarn add ganache-cli // recommended usage of -g flag
npm install flow-type // recommended usage of -g flag yarn add flow-type // recommended usage of -g flag
git clone https://github.com/gnosis/safe-contracts.git 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 ### Installing
A step by step series of examples that tell you have to get a development env running 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 Start the project in the other one
``` ```
cd safe-contracts && truffle compile && truffle migrate && cd .. cd safe-contracts && truffle compile && truffle migrate && cd ..
npm install yarn install
npm start yarn start
``` ```
## Running the tests ## Running the tests
``` ```
npm test yarn test
``` ```

View File

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

View File

@ -1,10 +1,10 @@
// @flow // @flow
import { List } from 'immutable' 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 { executeTransaction, approveTransaction } from '~/logic/safe/safeBlockchainOperations'
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions' import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
import { getWeb3 } from '~/logic/wallets/getWeb3' 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 { getGnosisSafeContract } from '~/logic/contracts/safeContracts'
import { storeSubject } from '~/utils/storage/transactions' import { storeSubject } from '~/utils/storage/transactions'

View File

@ -1,5 +1,5 @@
// @flow // @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 { List, Map } from 'immutable'
import { loadFromStorage, saveToStorage } from '~/utils/storage' import { loadFromStorage, saveToStorage } from '~/utils/storage'

View File

@ -1,9 +1,9 @@
// @flow // @flow
import { createAction } from 'redux-actions' import { createAction } from 'redux-actions'
import { type Token } from '~/logic/tokens/store/model/token' 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 { Dispatch as ReduxDispatch } from 'redux'
import { type GlobalState } from '~/store/index' import { type GlobalState } from '~/store/'
export const ADD_TOKEN = 'ADD_TOKEN' export const ADD_TOKEN = 'ADD_TOKEN'
@ -14,17 +14,16 @@ type AddTokenProps = {
export const addToken = createAction<string, *, *>( export const addToken = createAction<string, *, *>(
ADD_TOKEN, ADD_TOKEN,
(safeAddress: string, token: Token): AddTokenProps => ({ (token: Token): AddTokenProps => ({
safeAddress,
token, token,
}), }),
) )
const saveToken = (safeAddress: string, token: Token) => async (dispatch: ReduxDispatch<GlobalState>) => { const saveToken = (safeAddress: string, token: Token) => async (dispatch: ReduxDispatch<GlobalState>) => {
dispatch(addToken(safeAddress, token)) dispatch(addToken(token))
const activeTokens = await getActiveTokens(safeAddress) const activeTokens = await getActiveTokens(safeAddress)
await setActiveTokens(safeAddress, activeTokens.push(token.toJS())) await saveActiveTokens(safeAddress, activeTokens.push(token.toJS()))
setToken(safeAddress, token) 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 // @flow
import { List, Map } from 'immutable' import { List } from 'immutable'
import contract from 'truffle-contract' import contract from 'truffle-contract'
import axios from 'axios' import axios from 'axios'
import { BigNumber } from 'bignumber.js'
import type { Dispatch as ReduxDispatch } from 'redux' import type { Dispatch as ReduxDispatch } from 'redux'
import StandardToken from '@gnosis.pm/util-contracts/build/contracts/GnosisStandardToken.json' import StandardToken from '@gnosis.pm/util-contracts/build/contracts/GnosisStandardToken.json'
import HumanFriendlyToken from '@gnosis.pm/util-contracts/build/contracts/HumanFriendlyToken.json' import HumanFriendlyToken from '@gnosis.pm/util-contracts/build/contracts/HumanFriendlyToken.json'
import { getWeb3 } from '~/logic/wallets/getWeb3' import { getWeb3 } from '~/logic/wallets/getWeb3'
import { type GlobalState } from '~/store/index' 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 { ensureOnce } from '~/utils/singleton'
import { getActiveTokens, getTokens } from '~/logic/tokens/utils/tokensStorage'
import { getSafeEthToken } from '~/logic/tokens/utils/tokenHelpers'
import saveTokens from './saveTokens' import saveTokens from './saveTokens'
import { getRelayUrl } from '~/config/index' import { getRelayUrl } from '~/config/index'
@ -34,66 +31,28 @@ export const getHumanFriendlyToken = ensureOnce(createHumanFriendlyTokenContract
export const getStandardTokenContract = ensureOnce(createStandardTokenContract) export const getStandardTokenContract = ensureOnce(createStandardTokenContract)
export const calculateBalanceOf = async (tokenAddress: string, address: string, decimals: number) => { const fetchTokenList = async () => {
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 apiUrl = getRelayUrl() const apiUrl = getRelayUrl()
const url = `${apiUrl}/tokens` const url = `${apiUrl}/tokens`
const errMsg = 'Error querying safe balances' const errMsg = 'Error querying safe balances'
return axios.get(url, errMsg) return axios.get(url, errMsg)
} }
export const fetchTokens = (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>) => { export const fetchTokens = () => 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()
try { try {
const balancesRecords = await Promise.all( const {
results.map(async (item: TokenProps) => { data: { results: tokenList },
const status = tokens.findIndex(activeToken => activeToken.name === item.name) !== -1 } = await fetchTokenList()
const funds = status ? await calculateBalanceOf(item.address, safeAddress, item.decimals) : '0'
return makeToken({ ...item, status, funds }) const tokens = List(tokenList.map((token: TokenProps) => makeToken(token)))
}),
)
const customTokenRecords = await Promise.all( dispatch(saveTokens(tokens))
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))
} catch (err) { } catch (err) {
// eslint-disable-next-line // eslint-disable-next-line
console.log('Error fetching tokens... ' + err) console.log('Error fetching token list ' + err)
return Promise.resolve() 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 // @flow
import { Map, List } from 'immutable' import { Map } from 'immutable'
import { createAction } from 'redux-actions' import { createAction } from 'redux-actions'
import type { Dispatch as ReduxDispatch } from 'redux'
import { type Token } from '~/logic/tokens/store/model/token' 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' export const ADD_TOKENS = 'ADD_TOKENS'
const setTokensOnce = ensureOnceAsync(setActiveTokens)
type TokenProps = { type TokenProps = {
safeAddress: string,
tokens: Map<string, Token>, tokens: Map<string, Token>,
} }
export const addTokens = createAction<string, *, *>( const addTokens = createAction<string, *, *>(
ADD_TOKENS, ADD_TOKENS,
(safeAddress: string, tokens: Map<string, Token>): TokenProps => ({ (tokens: Map<string, Token>): TokenProps => ({
safeAddress,
tokens, tokens,
}), }),
) )
const saveTokens = (safeAddress: string, tokens: Map<string, Token>) => async ( export default addTokens
dispatch: ReduxDispatch<GlobalState>,
) => {
dispatch(addTokens(safeAddress, tokens))
const activeAddresses: List<Token> = calculateActiveErc20TokensFrom(tokens.toList())
await setTokensOnce(safeAddress, activeAddresses)
}
export default saveTokens

View File

@ -8,9 +8,7 @@ export type TokenProps = {
symbol: string, symbol: string,
decimals: number, decimals: number,
logoUri: string, logoUri: string,
funds: string, balance: string,
status: boolean,
removable: boolean,
} }
export const makeToken: RecordFactory<TokenProps> = Record({ export const makeToken: RecordFactory<TokenProps> = Record({
@ -19,9 +17,9 @@ export const makeToken: RecordFactory<TokenProps> = Record({
symbol: '', symbol: '',
decimals: 0, decimals: 0,
logoUri: '', logoUri: '',
funds: '0', balance: undefined,
status: true,
removable: false,
}) })
// balance is only set in extendedSafeTokensSelector when we display user's token balances
export type Token = RecordOf<TokenProps> 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 { ADD_TOKEN } from '~/logic/tokens/store/actions/addToken'
import { REMOVE_TOKEN } from '~/logic/tokens/store/actions/removeToken' import { REMOVE_TOKEN } from '~/logic/tokens/store/actions/removeToken'
import { ADD_TOKENS } from '~/logic/tokens/store/actions/saveTokens' 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' export const TOKEN_REDUCER_ID = 'tokens'
@ -15,39 +13,27 @@ export type State = Map<string, Map<string, Token>>
export default handleActions<State, *>( export default handleActions<State, *>(
{ {
[ADD_TOKENS]: (state: State, action: ActionType<Function>): 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>) => { const newState = state.withMutations((map) => {
if (!prevSafe) { tokens.forEach((token) => {
return tokens map.set(token.address, token)
}
return prevSafe.equals(tokens) ? prevSafe : tokens
}) })
})
return newState
}, },
[ADD_TOKEN]: (state: State, action: ActionType<Function>): State => { [ADD_TOKEN]: (state: State, action: ActionType<Function>): State => {
const { safeAddress, token } = action.payload const { token } = action.payload
const { address: tokenAddress } = token const { address: tokenAddress } = token
return state.setIn([safeAddress, tokenAddress], token) return state.set(tokenAddress, token)
}, },
[REMOVE_TOKEN]: (state: State, action: ActionType<Function>): State => { [REMOVE_TOKEN]: (state: State, action: ActionType<Function>): State => {
const { safeAddress, token } = action.payload const { token } = action.payload
const { address: tokenAddress } = token const { address: tokenAddress } = token
return state.removeIn([safeAddress, tokenAddress]) return state.remove(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)
}, },
}, },
Map(), Map(),

View File

@ -1,45 +1,19 @@
// @flow // @flow
import { List, Map } from 'immutable' import { List, Map } from 'immutable'
import { createSelector, type Selector } from 'reselect' 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 { type GlobalState } from '~/store'
import { TOKEN_REDUCER_ID } from '~/logic/tokens/store/reducer/tokens' import { TOKEN_REDUCER_ID } from '~/logic/tokens/store/reducer/tokens'
import { type Token } from '~/logic/tokens/store/model/token' 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( export const tokenListSelector: Selector<GlobalState, Map<string, Token>, List<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(
tokensSelector, tokensSelector,
(tokens: Map<string, Token>) => tokens.toList(), (tokens: Map<string, Token>) => tokens.toList(),
) )
export const activeTokensSelector = createSelector( export const orderedTokenListSelector: Selector<GlobalState, RouterProps, List<Token>> = createSelector(
tokenListSelector,
(tokens: List<Token>) => tokens.filter((token: Token) => token.get('status')),
)
export const orderedTokenListSelector = createSelector(
tokenListSelector, tokenListSelector,
(tokens: List<Token>) => tokens.sortBy((token: Token) => token.get('symbol')), (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 // @flow
import { List } from 'immutable' import { List } from 'immutable'
import logo from '~/assets/icons/icon_etherTokens.svg' import logo from '~/assets/icons/icon_etherTokens.svg'
import { getBalanceInEtherOf } from '~/logic/wallets/getWeb3'
import { makeToken, type Token } from '~/logic/tokens/store/model/token' 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 isEther = (symbol: string) => symbol === 'ETH'
export const getSafeEthToken = async (safeAddress: string) => { export const getEthAsToken = (balance: string) => {
const balance = await getBalanceInEtherOf(safeAddress) const eth = makeToken({
address: ETH_ADDRESS,
const ethBalance = makeToken({
address: '0',
name: 'Ether', name: 'Ether',
symbol: 'ETH', symbol: 'ETH',
decimals: 18, decimals: 18,
logoUri: logo, logoUri: logo,
funds: balance, balance,
}) })
return ethBalance return eth
} }
export const calculateActiveErc20TokensFrom = (tokens: List<Token>) => { 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 const isDeactivated = isEther(token.symbol) || !token.status
if (isDeactivated) { if (isDeactivated) {
return return
@ -32,5 +29,5 @@ export const calculateActiveErc20TokensFrom = (tokens: List<Token>) => {
list.push(token) list.push(token)
})) }))
return addresses return activeTokens
} }

View File

@ -1,44 +1,41 @@
// @flow // @flow
import { List } from 'immutable' import { List, Map } from 'immutable'
import { type Token, type TokenProps } from '~/logic/tokens/store/model/token' import { type Token, type TokenProps } from '~/logic/tokens/store/model/token'
import { loadFromStorage, saveToStorage } from '~/utils/storage' import { loadFromStorage, saveToStorage } from '~/utils/storage'
export const ACTIVE_TOKENS_KEY = 'ACTIVE_TOKENS' 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}` // Tokens which are active at least in one of used safes in the app should be saved to localstorage
const getTokensKey = (safeAddress: string) => `${TOKENS_KEY}-${safeAddress}` // 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 { try {
const key = getActiveTokensKey(safeAddress) await saveToStorage(ACTIVE_TOKENS_KEY, tokens.toJS())
await saveToStorage(key, tokens.toJS())
} catch (err) { } catch (err) {
// eslint-disable-next-line // eslint-disable-next-line
console.log('Error storing tokens in localstorage') console.log('Error storing tokens in localstorage')
} }
} }
export const getActiveTokens = async (safeAddress: string): Promise<List<TokenProps>> => { export const getActiveTokens = async (): Promise<Object<string, TokenProps>> => {
const key = getActiveTokensKey(safeAddress) const data = await loadFromStorage(ACTIVE_TOKENS_KEY)
const data = await loadFromStorage(key)
return data ? List(data) : List() return data || {}
} }
export const getTokens = async (safeAddress: string): Promise<List<TokenProps>> => { export const getCustomTokens = async (): Promise<List<TokenProps>> => {
const key = getTokensKey(safeAddress) const data = await loadFromStorage(CUSTOM_TOKENS_KEY)
const data = await loadFromStorage(key)
return data ? List(data) : List() return data ? List(data) : List()
} }
export const setToken = async (safeAddress: string, token: Token) => { export const setToken = async (safeAddress: string, token: Token) => {
const data: List<TokenProps> = await getTokens(safeAddress) const data: List<TokenProps> = await getCustomTokens()
try { try {
const key = getTokensKey(safeAddress) await saveToStorage(CUSTOM_TOKENS_KEY, data.push(token))
await saveToStorage(key, data.push(token))
} catch (err) { } catch (err) {
// eslint-disable-next-line // eslint-disable-next-line
console.log('Error adding token in localstorage') 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) => { export const removeTokenFromStorage = async (safeAddress: string, token: Token) => {
const data: List<TokenProps> = await getTokens(safeAddress) const data: List<TokenProps> = await getCustomTokens()
try { try {
const index = data.indexOf(token) const index = data.indexOf(token)
const key = getTokensKey(safeAddress) await saveToStorage(CUSTOM_TOKENS_KEY, data.remove(index))
await saveToStorage(key, data.remove(index))
} catch (err) { } catch (err) {
// eslint-disable-next-line // eslint-disable-next-line
console.log('Error removing token in localstorage') 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) => { 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) const index = activeTokens.findIndex(activeToken => activeToken.name === token.name)
if (index !== -1) { 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 { SAFELIST_ADDRESS } from '~/routes/routes'
import { history } from '~/store' import { history } from '~/store'
import selector, { type SelectorProps } from './selector' 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 Layout from '../components/Layout'
import { FIELD_LOAD_NAME, FIELD_LOAD_ADDRESS } from '../components/fields' import { FIELD_LOAD_NAME, FIELD_LOAD_ADDRESS } from '../components/fields'
type Props = SelectorProps & Actions type Props = SelectorProps & Actions
export const loadSafe = async (safeName: string, safeAddress: string, updateSafe: UpdateSafe) => { export const loadSafe = async (safeName: string, safeAddress: string, addSafe: Function) => {
const safeRecord = await buildSafe(safeAddress, safeName) const safeProps = await buildSafe(safeAddress, safeName)
await updateSafe(safeRecord) await addSafe(safeProps)
const storedSafes = await loadFromStorage(SAFES_KEY) || {} const storedSafes = (await loadFromStorage(SAFES_KEY)) || {}
storedSafes[safeAddress] = safeRecord.toJSON() storedSafes[safeAddress] = safeProps
saveSafes(storedSafes) saveSafes(storedSafes)
} }
@ -28,11 +28,12 @@ export const loadSafe = async (safeName: string, safeAddress: string, updateSafe
class Load extends React.Component<Props> { class Load extends React.Component<Props> {
onLoadSafeSubmit = async (values: Object) => { onLoadSafeSubmit = async (values: Object) => {
try { try {
const { updateSafe } = this.props const { addSafe } = this.props
const safeName = values[FIELD_LOAD_NAME] const safeName = values[FIELD_LOAD_NAME]
const safeAddress = values[FIELD_LOAD_ADDRESS] const safeAddress = values[FIELD_LOAD_ADDRESS]
await loadSafe(safeName, safeAddress, updateSafe) await loadSafe(safeName, safeAddress, addSafe)
const url = `${SAFELIST_ADDRESS}/${safeAddress}` const url = `${SAFELIST_ADDRESS}/${safeAddress}`
history.push(url) history.push(url)
} catch (error) { } catch (error) {
@ -42,9 +43,7 @@ class Load extends React.Component<Props> {
} }
render() { render() {
const { const { provider, network, userAddress } = this.props
provider, network, userAddress,
} = this.props
return ( return (
<Page> <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 // @flow
import updateSafe from '~/routes/safe/store/actions/updateSafe' import { addSafe } from '~/routes/safe/store/actions/addSafe'
export type UpdateSafe = typeof updateSafe
export type Actions = { export type Actions = {
updateSafe: typeof updateSafe, addSafe: Function,
} }
export default { export default {
updateSafe, addSafe,
} }

View File

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

View File

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

View File

@ -1,10 +1,9 @@
// @flow // @flow
import * as React from 'react' import * as React from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { List } from 'immutable' import { List, Set } from 'immutable'
import classNames from 'classnames/bind' import classNames from 'classnames/bind'
import SearchBar from 'material-ui-search-bar' import SearchBar from 'material-ui-search-bar'
import InfiniteScroll from 'react-infinite-scroll-component'
import { withStyles } from '@material-ui/core/styles' import { withStyles } from '@material-ui/core/styles'
import MuiList from '@material-ui/core/List' import MuiList from '@material-ui/core/List'
import Img from '~/components/layout/Img' import Img from '~/components/layout/Img'
@ -23,6 +22,7 @@ import Divider from '~/components/layout/Divider'
import Hairline from '~/components/layout/Hairline' import Hairline from '~/components/layout/Hairline'
import Spacer from '~/components/Spacer' import Spacer from '~/components/Spacer'
import Row from '~/components/layout/Row' import Row from '~/components/layout/Row'
import { ETH_ADDRESS } from '~/logic/tokens/utils/tokenHelpers'
import { type Token } from '~/logic/tokens/store/model/token' import { type Token } from '~/logic/tokens/store/model/token'
import actions, { type Actions } from './actions' import actions, { type Actions } from './actions'
import TokenPlaceholder from './assets/token_placeholder.png' import TokenPlaceholder from './assets/token_placeholder.png'
@ -33,10 +33,12 @@ type Props = Actions & {
classes: Object, classes: Object,
tokens: List<Token>, tokens: List<Token>,
safeAddress: string, safeAddress: string,
activeTokens: List<Token>,
} }
type State = { type State = {
filter: string, filter: string,
activeTokensAddresses: Set<string>,
} }
const filterBy = (filter: string, tokens: List<Token>): List<Token> => tokens.filter( 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()), || 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> { class Tokens extends React.Component<Props, State> {
state = { state = {
filter: '', 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 = () => { onCancelSearch = () => {
@ -58,14 +95,17 @@ class Tokens extends React.Component<Props, State> {
this.setState(() => ({ filter: value })) this.setState(() => ({ filter: value }))
} }
onSwitch = (token: Token) => (e: SyntheticInputEvent<HTMLInputElement>) => { onSwitch = (token: Token) => () => {
const { checked } = e.target const { activeTokensAddresses } = this.state
const { safeAddress, enableToken, disableToken } = this.props
if (checked) { if (activeTokensAddresses.has(token.address)) {
enableToken(safeAddress, token) this.setState({
activeTokensAddresses: activeTokensAddresses.remove(token.address),
})
} else { } else {
disableToken(safeAddress, token) this.setState({
activeTokensAddresses: activeTokensAddresses.add(token.address),
})
} }
} }
@ -76,7 +116,7 @@ class Tokens extends React.Component<Props, State> {
render() { render() {
const { onClose, classes, tokens } = this.props const { onClose, classes, tokens } = this.props
const { filter } = this.state const { filter, activeTokensAddresses } = this.state
const searchClasses = { const searchClasses = {
input: classes.searchInput, input: classes.searchInput,
root: classes.searchRoot, root: classes.searchRoot,
@ -117,17 +157,23 @@ class Tokens extends React.Component<Props, State> {
<Hairline /> <Hairline />
</Block> </Block>
<MuiList className={classes.list}> <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}> <ListItem key={token.address} className={classes.token}>
<ListItemIcon> <ListItemIcon>
<Img src={token.logoUri} height={28} alt={token.name} onError={this.setImageToPlaceholder} /> <Img src={token.logoUri} height={28} alt={token.name} onError={this.setImageToPlaceholder} />
</ListItemIcon> </ListItemIcon>
<ListItemText primary={token.symbol} secondary={token.name} /> <ListItemText primary={token.symbol} secondary={token.name} />
{token.address !== ETH_ADDRESS && (
<ListItemSecondaryAction> <ListItemSecondaryAction>
<Switch onChange={this.onSwitch(token)} checked={token.status} /> <Switch onChange={this.onSwitch(token)} checked={isActive} />
</ListItemSecondaryAction> </ListItemSecondaryAction>
)}
</ListItem> </ListItem>
))} )
})}
</MuiList> </MuiList>
</React.Fragment> </React.Fragment>
) )

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
import * as React from 'react' import * as React from 'react'
import Stepper from '~/components/Stepper' import Stepper from '~/components/Stepper'
import { connect } from 'react-redux' 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 { getSafeEthereumInstance, createTransaction } from '~/logic/safe/safeFrontendOperations'
import RemoveOwnerForm, { DECREASE_PARAM } from './RemoveOwnerForm' import RemoveOwnerForm, { DECREASE_PARAM } from './RemoveOwnerForm'
import Review from './Review' import Review from './Review'

View File

@ -2,7 +2,7 @@
import { List } from 'immutable' import { List } from 'immutable'
import { createStructuredSelector, createSelector } from 'reselect' import { createStructuredSelector, createSelector } from 'reselect'
import { userAccountSelector } from '~/logic/wallets/store/selectors' 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' import { safeTransactionsSelector } from '~/routes/safe/store/selectors/index'
const pendingTransactionsSelector = createSelector( const pendingTransactionsSelector = createSelector(

View File

@ -15,7 +15,7 @@ import Delete from '@material-ui/icons/Delete'
import Person from '@material-ui/icons/Person' import Person from '@material-ui/icons/Person'
import ExpandLess from '@material-ui/icons/ExpandLess' import ExpandLess from '@material-ui/icons/ExpandLess'
import ExpandMore from '@material-ui/icons/ExpandMore' 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 { type WithStyles } from '~/theme/mui'
import { sameAddress } from '~/logic/wallets/ethAddresses' 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 Img from '~/components/layout/Img'
import Paragraph from '~/components/layout/Paragraph' import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row' 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 { type Token } from '~/logic/tokens/store/model/token'
import Transactions from '~/routes/safe/component/Transactions' import Transactions from '~/routes/safe/component/Transactions'

View File

@ -4,7 +4,7 @@ import { BigNumber } from 'bignumber.js'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import Stepper from '~/components/Stepper' import Stepper from '~/components/Stepper'
import { sleep } from '~/utils/timer' 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 { getStandardTokenContract } from '~/logic/tokens/store/actions/fetchTokens'
import { type Token } from '~/logic/tokens/store/model/token' import { type Token } from '~/logic/tokens/store/model/token'
import { isEther } from '~/logic/tokens/utils/tokens' 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 Field from '~/components/forms/Field'
import TextField from '~/components/forms/TextField' import TextField from '~/components/forms/TextField'
import { composeValidators, minValue, maxValue, mustBeInteger, required } from '~/components/forms/validator' 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' export const THRESHOLD_PARAM = 'threshold'

View File

@ -3,7 +3,7 @@ import * as React from 'react'
import Stepper from '~/components/Stepper' import Stepper from '~/components/Stepper'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { getSafeEthereumInstance, createTransaction } from '~/logic/safe/safeFrontendOperations' 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 ThresholdForm, { THRESHOLD_PARAM } from './ThresholdForm'
import selector, { type SelectorProps } from './selector' import selector, { type SelectorProps } from './selector'
import actions, { type Actions } from './actions' 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 ExpandLess from '@material-ui/icons/ExpandLess'
import ExpandMore from '@material-ui/icons/ExpandMore' import ExpandMore from '@material-ui/icons/ExpandMore'
import { type WithStyles } from '~/theme/mui' 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 = { const styles = {
nested: { nested: {

View File

@ -9,7 +9,7 @@ import ListItemText from '~/components/List/ListItemText'
import Avatar from '@material-ui/core/Avatar' import Avatar from '@material-ui/core/Avatar'
import Group from '@material-ui/icons/Group' import Group from '@material-ui/icons/Group'
import MailOutline from '@material-ui/icons/MailOutline' 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' import Confirmations from './Confirmations'
type Props = { type Props = {

View File

@ -16,11 +16,11 @@ import Atm from '@material-ui/icons/LocalAtm'
import DoneAll from '@material-ui/icons/DoneAll' import DoneAll from '@material-ui/icons/DoneAll'
import CompareArrows from '@material-ui/icons/CompareArrows' import CompareArrows from '@material-ui/icons/CompareArrows'
import Collapsed from '~/routes/safe/component/Transactions/Collapsed' 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 Hairline from '~/components/layout/Hairline/index'
import Button from '~/components/layout/Button' import Button from '~/components/layout/Button'
import { sameAddress } from '~/logic/wallets/ethAddresses' 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' import selector, { type SelectorProps } from './selector'
type Props = Open & type Props = Open &

View File

@ -2,9 +2,9 @@
import { createStructuredSelector } from 'reselect' import { createStructuredSelector } from 'reselect'
import { confirmationsTransactionSelector } from '~/routes/safe/store/selectors/index' import { confirmationsTransactionSelector } from '~/routes/safe/store/selectors/index'
import { userAccountSelector } from '~/logic/wallets/store/selectors' 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 GlobalState } from '~/store'
import { type Confirmation } from '~/routes/safe/store/model/confirmation' import { type Confirmation } from '~/routes/safe/store/models/confirmation'
export type SelectorProps = { export type SelectorProps = {
confirmed: confirmationsTransactionSelector, confirmed: confirmationsTransactionSelector,

View File

@ -2,11 +2,11 @@
import * as React from 'react' import * as React from 'react'
import { List } from 'immutable' import { List } from 'immutable'
import { connect } from 'react-redux' 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 NoTransactions from '~/routes/safe/component/Transactions/NoTransactions'
import GnoTransaction from '~/routes/safe/component/Transactions/Transaction' import GnoTransaction from '~/routes/safe/component/Transactions/Transaction'
import { sameAddress } from '~/logic/wallets/ethAddresses' 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 { processTransaction } from '~/logic/safe/safeFrontendOperations'
import selector, { type SelectorProps } from './selector' import selector, { type SelectorProps } from './selector'
import actions, { type Actions } from './actions' import actions, { type Actions } from './actions'

View File

@ -1,7 +1,7 @@
// @flow // @flow
import { List } from 'immutable' import { List } from 'immutable'
import { createStructuredSelector } from 'reselect' 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 { safeTransactionsSelector } from '~/routes/safe/store/selectors/index'
import { userAccountSelector } from '~/logic/wallets/store/selectors' import { userAccountSelector } from '~/logic/wallets/store/selectors'

View File

@ -1,13 +1,16 @@
// @flow // @flow
import fetchSafe from '~/routes/safe/store/actions/fetchSafe' 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 = { export type Actions = {
fetchSafe: typeof fetchSafe, fetchSafe: typeof fetchSafe,
fetchTokens: typeof fetchTokens, loadActiveTokens: typeof loadActiveTokens,
fetchTokenBalances: typeof fetchTokenBalances,
} }
export default { export default {
fetchSafe, fetchSafe,
fetchTokens, loadActiveTokens,
fetchTokenBalances,
} }

View File

@ -6,34 +6,34 @@ import Layout from '~/routes/safe/component/Layout'
import selector, { type SelectorProps } from './selector' import selector, { type SelectorProps } from './selector'
import actions, { type Actions } from './actions' import actions, { type Actions } from './actions'
type Props = Actions & SelectorProps & { export type Props = Actions &
SelectorProps & {
granted: boolean, granted: boolean,
} }
const TIMEOUT = process.env.NODE_ENV === 'test' ? 1500 : 15000 const TIMEOUT = process.env.NODE_ENV === 'test' ? 1500 : 15000
class SafeView extends React.PureComponent<Props> { class SafeView extends React.Component<Props> {
componentDidMount() { componentDidMount() {
const { safeUrl, fetchTokens, fetchSafe } = this.props const {
fetchSafe, loadActiveTokens, activeTokens, safeUrl, fetchTokenBalances,
} = this.props
fetchSafe(safeUrl) fetchSafe(safeUrl)
fetchTokens(safeUrl) // loadActiveTokens(safeUrl)
fetchTokenBalances(safeUrl, activeTokens)
this.intervalId = setInterval(() => { this.intervalId = setInterval(() => {
fetchTokens(safeUrl) // update in another function so it uses up-to-date props values
fetchSafe(safeUrl) this.checkForUpdates()
}, TIMEOUT) }, TIMEOUT)
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { safe, fetchTokens } = this.props const { activeTokens } = this.props
if (prevProps.safe) { if (activeTokens.size > prevProps.activeTokens.size) {
return this.checkForUpdates()
}
if (safe) {
const safeAddress = safe.get('address')
fetchTokens(safeAddress)
} }
} }
@ -41,6 +41,15 @@ class SafeView extends React.PureComponent<Props> {
clearInterval(this.intervalId) clearInterval(this.intervalId)
} }
checkForUpdates() {
const {
safeUrl, activeTokens, fetchSafe, fetchTokenBalances,
} = this.props
fetchSafe(safeUrl, true)
fetchTokenBalances(safeUrl, activeTokens)
}
intervalId: IntervalID intervalId: IntervalID
render() { 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 // @flow
import { List } from 'immutable' import { List, Map } from 'immutable'
import { createSelector, createStructuredSelector, type Selector } from 'reselect' 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 { providerNameSelector, userAccountSelector, networkSelector } from '~/logic/wallets/store/selectors'
import { type Safe } from '~/routes/safe/store/model/safe' import { type Safe } from '~/routes/safe/store/models/safe'
import { type Owner } from '~/routes/safe/store/model/owner' import { type Owner } from '~/routes/safe/store/models/owner'
import { type GlobalState } from '~/store' import { type GlobalState } from '~/store'
import { sameAddress } from '~/logic/wallets/ethAddresses' 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 Token } from '~/logic/tokens/store/model/token'
import { type TokenBalance } from '~/routes/safe/store/models/tokenBalance'
import { safeParamAddressSelector } from '../store/selectors' import { safeParamAddressSelector } from '../store/selectors'
import { getEthAsToken } from '~/logic/tokens/utils/tokenHelpers'
export type SelectorProps = { export type SelectorProps = {
safe: SafeSelectorProps, 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, safe: safeSelector,
provider: providerNameSelector, provider: providerNameSelector,
tokens: orderedTokenListSelector, tokens: orderedTokenListSelector,
activeTokens: activeTokensSelector, activeTokens: extendedSafeTokensSelector,
granted: grantedSelector, granted: grantedSelector,
userAddress: userAccountSelector, userAddress: userAccountSelector,
network: networkSelector, network: networkSelector,

View File

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

View File

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

View File

@ -2,12 +2,12 @@
import type { Dispatch as ReduxDispatch } from 'redux' import type { Dispatch as ReduxDispatch } from 'redux'
import { type GlobalState } from '~/store/index' import { type GlobalState } from '~/store/index'
import { SAFES_KEY } from '~/logic/safe/utils' 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 { loadFromStorage } from '~/utils/storage'
import { addSafe } from './addSafe' import { addSafe } from './addSafe'
import { buildSafe } from '~/routes/safe/store/reducer/safe' import { buildSafe } from '~/routes/safe/store/reducer/safe'
export default () => async (dispatch: ReduxDispatch<GlobalState>) => { const loadSafesFromStorage = () => async (dispatch: ReduxDispatch<GlobalState>) => {
try { try {
const safes: ?{ [string]: SafeProps } = await loadFromStorage(SAFES_KEY) const safes: ?{ [string]: SafeProps } = await loadFromStorage(SAFES_KEY)
@ -23,3 +23,5 @@ export default () => async (dispatch: ReduxDispatch<GlobalState>) => {
return Promise.resolve() 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 // @flow
import { Record } from 'immutable' import { Record } from 'immutable'
import type { RecordFactory, RecordOf } 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' import { type TxServiceType } from '~/logic/safe/safeTxHistory'
export type ConfirmationProps = { 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 // @flow
import { List, Record } from 'immutable' import { List, Record } from 'immutable'
import type { RecordFactory, RecordOf } 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 = { export type TransactionProps = {
name: string, name: string,

View File

@ -1,9 +1,10 @@
// @flow // @flow
import { Map } from 'immutable' import { Map, List } from 'immutable'
import { handleActions, type ActionType } from 'redux-actions' import { handleActions, type ActionType } from 'redux-actions'
import { ADD_SAFE, buildOwnersFrom } from '~/routes/safe/store/actions/addSafe' import { ADD_SAFE, buildOwnersFrom } from '~/routes/safe/store/actions/addSafe'
import { type Safe, type SafeProps, makeSafe } from '~/routes/safe/store/model/safe' import SafeRecord, { type Safe, type SafeProps } from '~/routes/safe/store/models/safe'
import { type OwnerProps } from '~/routes/safe/store/model/owner' import TokenBalance from '~/routes/safe/store/models/tokenBalance'
import { type OwnerProps } from '~/routes/safe/store/models/owner'
import { loadFromStorage } from '~/utils/storage' import { loadFromStorage } from '~/utils/storage'
import { SAFES_KEY } from '~/logic/safe/utils' import { SAFES_KEY } from '~/logic/safe/utils'
import { UPDATE_SAFE } from '~/routes/safe/store/actions/updateSafe' 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 names = storedSafe.owners.map((owner: OwnerProps) => owner.name)
const addresses = storedSafe.owners.map((owner: OwnerProps) => owner.address) const addresses = storedSafe.owners.map((owner: OwnerProps) => owner.address)
const owners = buildOwnersFrom(Array.from(names), Array.from(addresses)) 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 = { const safe: SafeProps = {
address: storedSafe.address, ...storedSafe,
name: storedSafe.name,
threshold: storedSafe.threshold,
owners, owners,
balances,
activeTokens,
} }
return makeSafe(safe) return safe
} }
const buildSafesFrom = (loadedSafes: Object): Map<string, 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])) const safeRecords = keys.map((address: string) => buildSafe(loadedSafes[address]))
return safes.withMutations(async (map) => { 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) { } catch (err) {
// eslint-disable-next-line // eslint-disable-next-line
@ -56,19 +59,22 @@ export default handleActions<State, *>(
{ {
[UPDATE_SAFE]: (state: State, action: ActionType<Function>): State => { [UPDATE_SAFE]: (state: State, action: ActionType<Function>): State => {
const safe = action.payload const safe = action.payload
const safeAddress = safe.get('address') const safeAddress = safe.address
const hasSafe = !!state.get(safeAddress) return state.update(safeAddress, prevSafe => prevSafe.merge(safe))
if (hasSafe) {
return state.update(safeAddress, prevSafe => (prevSafe.equals(safe) ? prevSafe : safe))
}
return state.set(safeAddress, safe)
}, },
[ADD_SAFE]: (state: State, action: ActionType<Function>): State => { [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(), Map(),

View File

@ -2,7 +2,7 @@
import { List, Map } from 'immutable' import { List, Map } from 'immutable'
import { handleActions, type ActionType } from 'redux-actions' import { handleActions, type ActionType } from 'redux-actions'
import addTransactions, { ADD_TRANSACTIONS } from '~/routes/safe/store/actions/addTransactions' 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' export const TRANSACTIONS_REDUCER_ID = 'transactions'

View File

@ -1,14 +1,15 @@
// @flow // @flow
import { Map, List } from 'immutable' import { Map, List, Set } from 'immutable'
import { type Match } from 'react-router-dom' import { type Match } from 'react-router-dom'
import { createSelector, createStructuredSelector, type Selector } from 'reselect' import { createSelector, createStructuredSelector, type Selector } from 'reselect'
import { type GlobalState } from '~/store/index' import { type GlobalState } from '~/store/index'
import { SAFE_PARAM_ADDRESS } from '~/routes/routes' 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 { safesMapSelector } from '~/routes/safeList/store/selectors'
import { type State as TransactionsState, TRANSACTIONS_REDUCER_ID } from '~/routes/safe/store/reducer/transactions' 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 Transaction } from '~/routes/safe/store/models/transaction'
import { type Confirmation } from '~/routes/safe/store/model/confirmation' import { type Confirmation } from '~/routes/safe/store/models/confirmation'
import { safesListSelector } from '~/routes/safeList/store/selectors/'
export type RouterProps = { export type RouterProps = {
match: Match, 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, safe: safeSelector,
tokens: safeActiveTokensSelector,
}) })

View File

@ -1,12 +1,12 @@
// @flow // @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' import { buildOwnersFrom } from '~/routes/safe/store/actions/addSafe'
class SafeBuilder { class SafeBuilder {
safe: Safe safe: Safe
constructor() { constructor() {
this.safe = makeSafe() this.safe = SafeRecord()
} }
withAddress(address: string) { withAddress(address: string) {

View File

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

View File

@ -2,7 +2,7 @@
import { Map } from 'immutable' import { Map } from 'immutable'
import { type Match } from 'react-router-dom' import { type Match } from 'react-router-dom'
import { SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe' 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 { SafeFactory } from '~/routes/safe/store/test/builder/safe.builder'
import { buildMathPropsFrom } from '~/test/utils/buildReactRouterProps' import { buildMathPropsFrom } from '~/test/utils/buildReactRouterProps'
import { safeSelector } from '../selectors' import { safeSelector } from '../selectors'

View File

@ -2,7 +2,7 @@
import { List } from 'immutable' import { List } from 'immutable'
import * as React from 'react' import * as React from 'react'
import NoSafe from '~/components/NoSafe' 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' import SafeTable from '~/routes/safeList/components/SafeTable'
type Props = { type Props = {

View File

@ -6,7 +6,7 @@ import Link from '~/components/layout/Link'
import Table, { import Table, {
TableBody, TableCell, TableHead, TableRow, TableBody, TableCell, TableHead, TableRow,
} from '~/components/layout/Table' } 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' import { SAFELIST_ADDRESS } from '~/routes/routes'
type Props = { type Props = {

View File

@ -3,7 +3,7 @@ import { List } from 'immutable'
import * as React from 'react' import * as React from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import Page from '~/components/layout/Page' 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 Layout from '../components/Layout'
import selector from './selector' import selector from './selector'

View File

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

View File

@ -2,23 +2,12 @@
import { List, Map } from 'immutable' import { List, Map } from 'immutable'
import { createSelector, type Selector } from 'reselect' import { createSelector, type Selector } from 'reselect'
import { type GlobalState } from '~/store/index' import { type GlobalState } from '~/store/index'
import { type Safe } from '~/routes/safe/store/model/safe' import { type Safe } from '~/routes/safe/store/models/safe'
import { userAccountSelector } from '~/logic/wallets/store/selectors'
import { type Owner } from '~/routes/safe/store/model/owner'
import { sameAddress } from '~/logic/wallets/ethAddresses'
import { SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe' import { SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe'
export const safesMapSelector = (state: GlobalState): Map<string, Safe> => state[SAFE_REDUCER_ID] 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, safesMapSelector,
(safes: Map<string, Safe>): List<Safe> => safes.toList(), (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 // @flow
import { List, Map } from 'immutable' import { List, Map } from 'immutable'
import { SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe' 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 { getProviderInfo } from '~/logic/wallets/getWeb3'
import { SafeFactory } from '~/routes/safe/store/test/builder/safe.builder' import { SafeFactory } from '~/routes/safe/store/test/builder/safe.builder'
import { PROVIDER_REDUCER_ID } from '~/logic/wallets/store/reducer/provider' import { PROVIDER_REDUCER_ID } from '~/logic/wallets/store/reducer/provider'

View File

@ -7,6 +7,7 @@ import {
import thunk from 'redux-thunk' import thunk from 'redux-thunk'
import provider, { PROVIDER_REDUCER_ID, type State as ProviderState } from '~/logic/wallets/store/reducer/provider' 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 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 tokens, { TOKEN_REDUCER_ID, type State as TokensState } from '~/logic/tokens/store/reducer/tokens'
import transactions, { import transactions, {
type State as TransactionsState, type State as TransactionsState,
@ -17,7 +18,7 @@ export const history = createBrowserHistory()
// eslint-disable-next-line // eslint-disable-next-line
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose 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 = { export type GlobalState = {
providers: ProviderState, providers: ProviderState,

View File

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

View File

@ -1,12 +1,12 @@
// @flow // @flow
import { Map, List } from 'immutable' 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 { aNewStore } from '~/store'
import { aMinedSafe } from '~/test/builder/safe.redux.builder' import { aMinedSafe } from '~/test/builder/safe.redux.builder'
import updateSafe from '~/routes/safe/store/actions/updateSafe' import updateSafe from '~/routes/safe/store/actions/updateSafe'
import { loadSafe } from '~/routes/load/container/Load' import { loadSafe } from '~/routes/load/container/Load'
import { safesMapSelector } from '~/routes/safeList/store/selectors' 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 { getWeb3 } from '~/logic/wallets/getWeb3'
import { safesInitialState } from '~/routes/safe/store/reducer/safe' import { safesInitialState } from '~/routes/safe/store/reducer/safe'
import { setOwners, OWNERS_KEY } from '~/utils/storage' import { setOwners, OWNERS_KEY } from '~/utils/storage'

View File

@ -4,7 +4,7 @@ import { aNewStore } from '~/store'
import { getWeb3 } from '~/logic/wallets/getWeb3' import { getWeb3 } from '~/logic/wallets/getWeb3'
import { confirmationsTransactionSelector, safeTransactionsSelector } from '~/routes/safe/store/selectors' import { confirmationsTransactionSelector, safeTransactionsSelector } from '~/routes/safe/store/selectors'
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions' 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 { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
import { aMinedSafe } from '~/test/builder/safe.redux.builder' import { aMinedSafe } from '~/test/builder/safe.redux.builder'
import { NAME_PARAM, OWNER_ADDRESS_PARAM, INCREASE_PARAM } from '~/routes/safe/component/AddOwner/AddOwnerForm' import { NAME_PARAM, OWNER_ADDRESS_PARAM, INCREASE_PARAM } from '~/routes/safe/component/AddOwner/AddOwnerForm'

View File

@ -1,10 +1,10 @@
// @flow // @flow
import { List } from 'immutable' import { List } from 'immutable'
import { getSafeEthereumInstance, createTransaction } from '~/logic/safe/safeFrontendOperations' 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 { makeOwner } from '~/routes/safe/store/model/owner' import { makeOwner } from '~/routes/safe/store/models/owner'
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions' 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 { aNewStore } from '~/store'
import { aMinedSafe } from '~/test/builder/safe.redux.builder' import { aMinedSafe } from '~/test/builder/safe.redux.builder'
import { getSafeFrom } from '~/test/utils/safeHelper' 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 { travelToTokens } from '~/test/builder/safe.dom.utils'
import { sleep } from '~/utils/timer' import { sleep } from '~/utils/timer'
import { buildMathPropsFrom } from '~/test/utils/buildReactRouterProps' 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 { getActiveTokenAddresses } from '~/logic/tokens/utils/tokensStorage'
import { enableFirstToken, testToken } from '~/test/builder/tokens.dom.utils' import { enableFirstToken, testToken } from '~/test/builder/tokens.dom.utils'
import * as fetchTokensModule from '~/logic/tokens/store/actions/fetchTokens' 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 fetchTokensModule from '~/logic/tokens/store/actions/fetchTokens'
import * as enhancedFetchModule from '~/utils/fetch' import * as enhancedFetchModule from '~/utils/fetch'
import addToken from '~/logic/tokens/store/actions/addToken' import addToken from '~/logic/tokens/store/actions/addToken'
import { activeTokensSelector } from '~/logic/tokens/store/selectors'
describe('DOM > Feature > Add new ERC 20 Tokens', () => { describe('DOM > Feature > Add new ERC 20 Tokens', () => {
// let web3 // 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 fetchTokensModule from '~/logic/tokens/store/actions/fetchTokens'
import * as enhancedFetchModule from '~/utils/fetch' import * as enhancedFetchModule from '~/utils/fetch'
import addToken from '~/logic/tokens/store/actions/addToken' 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 removeTokenAction from '~/logic/tokens/store/actions/removeToken'
import { makeToken } from '~/logic/tokens/store/model/token' import { makeToken } from '~/logic/tokens/store/model/token'

View File

@ -1,7 +1,7 @@
// @flow // @flow
import { List, Map } from 'immutable' import { List, Map } from 'immutable'
import { type Confirmation } from '~/routes/safe/store/model/confirmation' import { type Confirmation } from '~/routes/safe/store/models/confirmation'
import { type Transaction } from '~/routes/safe/store/model/transaction' import { type Transaction } from '~/routes/safe/store/models/transaction'
import { sameAddress } from '~/logic/wallets/ethAddresses' import { sameAddress } from '~/logic/wallets/ethAddresses'
export const testSizeOfSafesWith = (transactions: Map<string, List<Transaction>>, size: number) => { 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 { safeSelector } from '~/routes/safe/store/selectors/index'
import { type Match } from 'react-router-dom' import { type Match } from 'react-router-dom'
import { type GlobalState } from '~/store' 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 => { export const getSafeFrom = (state: GlobalState, safeAddress: string): Safe => {
const match: Match = buildMathPropsFrom(safeAddress) const match: Match = buildMathPropsFrom(safeAddress)

View File

@ -1,9 +1,10 @@
// @flow // @flow
import { import { ImmortalStorage, IndexedDbStore, LocalStorageStore } from 'immortal-db'
ImmortalStorage, CookieStore, IndexedDbStore, LocalStorageStore, SessionStorageStore,
} 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) export const storage = new ImmortalStorage(stores)
const PREFIX = 'v1' const PREFIX = 'v1'

1455
yarn.lock

File diff suppressed because it is too large Load Diff