diff --git a/src/routes/index.js b/src/routes/index.js index 4bc05bb0..42b6f74c 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -4,13 +4,18 @@ import Loadable from 'react-loadable' import { Switch, Redirect, Route } from 'react-router-dom' import Loader from '~/components/Loader' import Welcome from './welcome/container' -import { SAFELIST_ADDRESS, OPEN_ADDRESS, SAFE_PARAM_ADDRESS, WELCOME_ADDRESS } from './routes' +import { SAFELIST_ADDRESS, OPEN_ADDRESS, SAFE_PARAM_ADDRESS, WELCOME_ADDRESS, SETTINS_ADDRESS } from './routes' const Safe = Loadable({ loader: () => import('./safe/container'), loading: Loader, }) +const Settings = Loadable({ + loader: () => import('./tokens/container'), + loading: Loader, +}) + const SafeList = Loadable({ loader: () => import('./safeList/container'), loading: Loader, @@ -22,6 +27,9 @@ const Open = Loadable({ }) const SAFE_ADDRESS = `${SAFELIST_ADDRESS}/:${SAFE_PARAM_ADDRESS}` +const SAFE_SETTINGS = `${SAFE_ADDRESS}${SETTINS_ADDRESS}` + +export const settingsUrlFrom = (safeAddress: string) => `${SAFELIST_ADDRESS}/${safeAddress}/settings` const Routes = () => ( @@ -30,6 +38,7 @@ const Routes = () => ( + ) diff --git a/src/routes/routes.js b/src/routes/routes.js index ed521627..4ab227a3 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -3,3 +3,4 @@ export const SAFE_PARAM_ADDRESS = 'address' export const SAFELIST_ADDRESS = '/safes' export const OPEN_ADDRESS = '/open' export const WELCOME_ADDRESS = '/welcome' +export const SETTINS_ADDRESS = '/settings' diff --git a/src/routes/safe/component/Layout.jsx b/src/routes/safe/component/Layout.jsx index 4ae7803f..231731cf 100644 --- a/src/routes/safe/component/Layout.jsx +++ b/src/routes/safe/component/Layout.jsx @@ -7,11 +7,11 @@ import GnoSafe from './Safe' type Props = SelectorProps const Layout = ({ - safe, balances, provider, userAddress, + safe, activeTokens, provider, userAddress, }: Props) => ( { safe - ? + ? : } diff --git a/src/routes/safe/component/Layout.stories.js b/src/routes/safe/component/Layout.stories.js index 0c1cb5e9..86e8f8a5 100644 --- a/src/routes/safe/component/Layout.stories.js +++ b/src/routes/safe/component/Layout.stories.js @@ -4,7 +4,7 @@ import * as React from 'react' import { Map } from 'immutable' import styles from '~/components/layout/PageFrame/index.scss' import { SafeFactory } from '~/routes/safe/store/test/builder/safe.builder' -import { makeBalance } from '~/routes/safe/store/model/balance' +import { makeToken } from '~/routes/tokens/store/model/token' import Component from './Layout' @@ -14,7 +14,7 @@ const FrameDecorator = story => ( ) -const ethBalance = makeBalance({ +const ethBalance = makeToken({ address: '0', name: 'Ether', symbol: 'ETH', @@ -30,7 +30,7 @@ storiesOf('Routes /safe:address', module) userAddress="foo" safe={undefined} provider="METAMASK" - balances={Map()} + activeTokens={Map()} fetchBalance={() => {}} /> )) @@ -39,7 +39,7 @@ storiesOf('Routes /safe:address', module) userAddress="foo" safe={undefined} provider="" - balances={Map()} + activeTokens={Map()} fetchBalance={() => {}} /> )) @@ -51,7 +51,7 @@ storiesOf('Routes /safe:address', module) userAddress="foo" safe={safe} provider="METAMASK" - balances={Map().set('ETH', ethBalance)} + activeTokens={Map().set('ETH', ethBalance)} fetchBalance={() => {}} /> ) @@ -64,7 +64,7 @@ storiesOf('Routes /safe:address', module) userAddress="foo" safe={safe} provider="METAMASK" - balances={Map().set('ETH', ethBalance)} + activeTokens={Map().set('ETH', ethBalance)} fetchBalance={() => {}} /> ) diff --git a/src/routes/safe/component/Safe/BalanceInfo.jsx b/src/routes/safe/component/Safe/BalanceInfo.jsx index dc46ac49..10399168 100644 --- a/src/routes/safe/component/Safe/BalanceInfo.jsx +++ b/src/routes/safe/component/Safe/BalanceInfo.jsx @@ -1,7 +1,9 @@ // @flow import * as React from 'react' import classNames from 'classnames' +import Link from '~/components/layout/Link' import AccountBalance from '@material-ui/icons/AccountBalance' +import Settings from '@material-ui/icons/Settings' import Avatar from '@material-ui/core/Avatar' import Collapse from '@material-ui/core/Collapse' import IconButton from '@material-ui/core/IconButton' @@ -17,11 +19,13 @@ import { Map } from 'immutable' import Button from '~/components/layout/Button' import openHoc, { type Open } from '~/components/hoc/OpenHoc' import { type WithStyles } from '~/theme/mui' -import { type Balance } from '~/routes/safe/store/model/balance' +import { type Token } from '~/routes/tokens/store/model/token' +import { settingsUrlFrom } from '~/routes' type Props = Open & WithStyles & { - balances: Map, - onMoveFunds: (balance: Balance) => void, + safeAddress: string, + tokens: Map, + onMoveFunds: (token: Token) => void, } const styles = { @@ -33,9 +37,10 @@ const styles = { export const MOVE_FUNDS_BUTTON_TEXT = 'Move' const BalanceComponent = openHoc(({ - open, toggle, balances, classes, onMoveFunds, + open, toggle, tokens, classes, onMoveFunds, safeAddress, }: Props) => { - const hasBalances = balances.count() > 0 + const hasBalances = tokens.count() > 0 + const settingsUrl = settingsUrlFrom(safeAddress) return ( @@ -44,6 +49,11 @@ const BalanceComponent = openHoc(({ + + + + + {open ? @@ -53,18 +63,18 @@ const BalanceComponent = openHoc(({ - {balances.valueSeq().map((balance: Balance) => { - const symbol = balance.get('symbol') - const name = balance.get('name') - const disabled = Number(balance.get('funds')) === 0 - const onMoveFundsClick = () => onMoveFunds(balance) + {tokens.valueSeq().map((token: Token) => { + const symbol = token.get('symbol') + const name = token.get('name') + const disabled = Number(token.get('funds')) === 0 + const onMoveFundsClick = () => onMoveFunds(token) return ( - {name} + {name} - + diff --git a/src/routes/safe/component/Safe/index.jsx b/src/routes/safe/component/Safe/index.jsx index f3404c60..232cfee3 100644 --- a/src/routes/safe/component/Safe/index.jsx +++ b/src/routes/safe/component/Safe/index.jsx @@ -9,7 +9,7 @@ import Img from '~/components/layout/Img' import Paragraph from '~/components/layout/Paragraph' import Row from '~/components/layout/Row' import { type Safe } from '~/routes/safe/store/model/safe' -import { type Balance } from '~/routes/safe/store/model/balance' +import { type Token } from '~/routes/tokens/store/model/token' import Withdraw from '~/routes/safe/component/Withdraw' import Transactions from '~/routes/safe/component/Transactions' @@ -30,7 +30,7 @@ const safeIcon = require('./assets/gnosis_safe.svg') type SafeProps = { safe: Safe, - balances: Map, + tokens: Map, userAddress: string, } @@ -42,13 +42,13 @@ const listStyle = { width: '100%', } -const getEthBalanceFrom = (balances: Map) => { - const ethBalance = balances.get('ETH') - if (!ethBalance) { +const getEthBalanceFrom = (tokens: List) => { + const ethToken = tokens.filter(token => token.get('symbol') === 'ETH') + if (ethToken.count() === 0) { return 0 } - return Number(ethBalance.get('funds')) + return Number(ethToken.get(0).get('funds')) } class GnoSafe extends React.PureComponent { @@ -93,13 +93,13 @@ class GnoSafe extends React.PureComponent { this.setState({ component: }) } - onMoveTokens = (ercToken: Balance) => { + onMoveTokens = (ercToken: Token) => { const { safe } = this.props this.setState({ component: , @@ -107,15 +107,16 @@ class GnoSafe extends React.PureComponent { } render() { - const { safe, balances, userAddress } = this.props + const { safe, tokens, userAddress } = this.props const { component } = this.state - const ethBalance = getEthBalanceFrom(balances) + const ethBalance = getEthBalanceFrom(tokens) + const address = safe.get('address') return ( - + { onRemoveOwner={this.onRemoveOwner} /> -
+
diff --git a/src/routes/safe/component/SendToken/index.jsx b/src/routes/safe/component/SendToken/index.jsx index 3b60a08e..0258f977 100644 --- a/src/routes/safe/component/SendToken/index.jsx +++ b/src/routes/safe/component/SendToken/index.jsx @@ -5,11 +5,12 @@ import { connect } from 'react-redux' import Stepper from '~/components/Stepper' import { sleep } from '~/utils/timer' import { type Safe } from '~/routes/safe/store/model/safe' -import { type Balance } from '~/routes/safe/store/model/balance' +import { getStandardTokenContract } from '~/routes/tokens/store/actions/fetchTokens' +import { type Token } from '~/routes/tokens/store/model/token' import { createTransaction } from '~/wallets/createTransactions' -import { getStandardTokenContract } from '~/routes/safe/store/actions/fetchBalances' import { EMPTY_DATA } from '~/wallets/ethTransactions' import { toNative } from '~/wallets/tokens' +import { isEther } from '~/utils/tokens' import actions, { type Actions } from './actions' import selector, { type SelectorProps } from './selector' import SendTokenForm, { TKN_DESTINATION_PARAM, TKN_VALUE_PARAM } from './SendTokenForm' @@ -21,7 +22,7 @@ const getSteps = () => [ type Props = SelectorProps & Actions & { safe: Safe, - balance: Balance, + token: Token, onReset: () => void, } @@ -31,8 +32,6 @@ type State = { export const SEE_TXS_BUTTON_TEXT = 'VISIT TXS' -const isEther = (symbol: string) => symbol === 'ETH' - const getTransferData = async (tokenAddress: string, to: string, amount: BigNumber) => { const StandardToken = await getStandardTokenContract() const myToken = await StandardToken.at(tokenAddress) @@ -40,16 +39,16 @@ const getTransferData = async (tokenAddress: string, to: string, amount: BigNumb return myToken.contract.transfer.getData(to, amount) } -const processTokenTransfer = async (safe: Safe, balance: Balance, to: string, amount: number, userAddress: string) => { - const symbol = balance.get('symbol') +const processTokenTransfer = async (safe: Safe, token: Token, to: string, amount: number, userAddress: string) => { + const symbol = token.get('symbol') const nonce = Date.now() - const name = `Send ${amount} ${balance.get('symbol')} to ${to}` + const name = `Send ${amount} ${token.get('symbol')} to ${to}` const value = isEther(symbol) ? amount : 0 - const tokenAddress = balance.get('address') + const tokenAddress = token.get('address') const destination = isEther(symbol) ? to : tokenAddress const data = isEther(symbol) ? EMPTY_DATA - : await getTransferData(tokenAddress, to, await toNative(amount, balance.get('decimals'))) + : await getTransferData(tokenAddress, to, await toNative(amount, token.get('decimals'))) return createTransaction(safe, name, destination, value, nonce, userAddress, data) } @@ -61,12 +60,12 @@ class SendToken extends React.Component { onTransaction = async (values: Object) => { try { - const { safe, balance, userAddress } = this.props + const { safe, token, userAddress } = this.props const amount = values[TKN_VALUE_PARAM] const destination = values[TKN_DESTINATION_PARAM] - await processTokenTransfer(safe, balance, destination, amount, userAddress) + await processTokenTransfer(safe, token, destination, amount, userAddress) await sleep(1500) this.props.fetchTransactions() this.setState({ done: true }) @@ -84,10 +83,10 @@ class SendToken extends React.Component { render() { const { done } = this.state - const { balance } = this.props + const { token } = this.props const steps = getSteps() const finishedButton = - const symbol = balance.get('symbol') + const symbol = token.get('symbol') return ( @@ -98,7 +97,7 @@ class SendToken extends React.Component { steps={steps} onReset={this.onReset} > - + { SendTokenForm } diff --git a/src/routes/safe/container/actions.js b/src/routes/safe/container/actions.js index ff9a91b4..7924354b 100644 --- a/src/routes/safe/container/actions.js +++ b/src/routes/safe/container/actions.js @@ -1,13 +1,13 @@ // @flow import fetchSafe from '~/routes/safe/store/actions/fetchSafe' -import { fetchBalances } from '~/routes/safe/store/actions/fetchBalances' +import { fetchTokens } from '~/routes/tokens/store/actions/fetchTokens' export type Actions = { fetchSafe: typeof fetchSafe, - fetchBalances: typeof fetchBalances, + fetchTokens: typeof fetchTokens, } export default { fetchSafe, - fetchBalances, + fetchTokens, } diff --git a/src/routes/safe/container/index.jsx b/src/routes/safe/container/index.jsx index 2b1c9d0d..b980ec2a 100644 --- a/src/routes/safe/container/index.jsx +++ b/src/routes/safe/container/index.jsx @@ -16,12 +16,14 @@ const TIMEOUT = process.env.NODE_ENV === 'test' ? 1500 : 15000 class SafeView extends React.PureComponent { componentDidMount() { this.intervalId = setInterval(() => { - const { safe, fetchBalances, fetchSafe } = this.props + const { + safe, fetchTokens, fetchSafe, + } = this.props if (!safe) { return } const safeAddress = safe.get('address') - fetchBalances(safeAddress) + fetchTokens(safeAddress) fetchSafe(safe) }, TIMEOUT) } @@ -33,7 +35,7 @@ class SafeView extends React.PureComponent { if (this.props.safe) { const safeAddress = this.props.safe.get('address') - this.props.fetchBalances(safeAddress) + this.props.fetchTokens(safeAddress) } } @@ -45,13 +47,13 @@ class SafeView extends React.PureComponent { render() { const { - safe, provider, balances, granted, userAddress, + safe, provider, activeTokens, granted, userAddress, } = this.props return ( { granted - ? + ? : } diff --git a/src/routes/safe/container/selector.js b/src/routes/safe/container/selector.js index 45410f57..94b76a07 100644 --- a/src/routes/safe/container/selector.js +++ b/src/routes/safe/container/selector.js @@ -1,18 +1,19 @@ // @flow import { List, Map } from 'immutable' import { createSelector, createStructuredSelector, type Selector } from 'reselect' -import { balanceSelector, safeSelector, type RouterProps, type SafeSelectorProps } from '~/routes/safe/store/selectors' +import { safeSelector, type RouterProps, type SafeSelectorProps } from '~/routes/safe/store/selectors' import { providerNameSelector, userAccountSelector } from '~/wallets/store/selectors/index' import { type Safe } from '~/routes/safe/store/model/safe' import { type Owner } from '~/routes/safe/store/model/owner' import { type GlobalState } from '~/store/index' import { sameAddress } from '~/wallets/ethAddresses' -import { type Balance } from '~/routes/safe/store/model/balance' +import { activeTokensSelector } from '~/routes/tokens/store/selectors' +import { type Token } from '~/routes/tokens/store/model/token' export type SelectorProps = { safe: SafeSelectorProps, provider: string, - balances: Map, + activeTokens: Map, userAddress: string, } @@ -40,7 +41,7 @@ export const grantedSelector: Selector = crea export default createStructuredSelector({ safe: safeSelector, provider: providerNameSelector, - balances: balanceSelector, + activeTokens: activeTokensSelector, granted: grantedSelector, userAddress: userAccountSelector, }) diff --git a/src/routes/safe/store/actions/addBalances.js b/src/routes/safe/store/actions/addBalances.js deleted file mode 100644 index eb473d1e..00000000 --- a/src/routes/safe/store/actions/addBalances.js +++ /dev/null @@ -1,21 +0,0 @@ -// @flow -import { Map } from 'immutable' -import { createAction } from 'redux-actions' -import { type Balance } from '~/routes/safe/store/model/balance' - -export const ADD_BALANCES = 'ADD_BALANCES' - -type BalanceProps = { - safeAddress: string, - balances: Map, -} - -const addBalances = createAction( - ADD_BALANCES, - (safeAddress: string, balances: Map): BalanceProps => ({ - safeAddress, - balances, - }), -) - -export default addBalances diff --git a/src/routes/safe/store/actions/fetchBalances.js b/src/routes/safe/store/actions/fetchBalances.js deleted file mode 100644 index 76f2705a..00000000 --- a/src/routes/safe/store/actions/fetchBalances.js +++ /dev/null @@ -1,65 +0,0 @@ -// @flow -import { Map } from 'immutable' -import contract from 'truffle-contract' -import type { Dispatch as ReduxDispatch } from 'redux' -import StandardToken from '@gnosis.pm/util-contracts/build/contracts/StandardToken.json' -import { getBalanceInEtherOf, getWeb3 } from '~/wallets/getWeb3' -import { type GlobalState } from '~/store/index' -import { makeBalance, type Balance, type BalanceProps } from '~/routes/safe/store/model/balance' -import logo from '~/assets/icons/icon_etherTokens.svg' -import addBalances from './addBalances' - -export const getStandardTokenContract = async () => { - const web3 = getWeb3() - const erc20Token = await contract(StandardToken) - erc20Token.setProvider(web3.currentProvider) - - return erc20Token -} - -export const calculateBalanceOf = async (tokenAddress: string, address: string, decimals: number) => { - const erc20Token = await getStandardTokenContract() - - return erc20Token.at(tokenAddress) - .then(instance => instance.balanceOf(address).then(funds => funds.div(10 ** decimals).toString())) - .catch(() => '0') -} - -export const fetchBalances = (safeAddress: string) => async (dispatch: ReduxDispatch) => { - const balance = await getBalanceInEtherOf(safeAddress) - const ethBalance = makeBalance({ - address: '0', - name: 'Ether', - symbol: 'ETH', - decimals: 18, - logoUrl: logo, - funds: balance, - }) - - const header = new Headers({ - 'Access-Control-Allow-Origin': '*', - }) - - const sentData = { - mode: 'cors', - header, - } - - const response = await fetch('https://gist.githubusercontent.com/rmeissner/98911fcf74b0ea9731e2dae2441c97a4/raw/', sentData) - if (!response.ok) { - throw new Error('Error querying safe balances') - } - - const json = await response.json() - return Promise.all(json.map(async (item: BalanceProps) => { - const funds = await calculateBalanceOf(item.address, safeAddress, item.decimals) - return makeBalance({ ...item, funds }) - })).then((balancesRecords) => { - const balances: Map = Map().withMutations((map) => { - balancesRecords.forEach(record => map.set(record.get('symbol'), record)) - map.set('ETH', ethBalance) - }) - - return dispatch(addBalances(safeAddress, balances)) - }) -} diff --git a/src/routes/safe/store/actions/fetchSafe.js b/src/routes/safe/store/actions/fetchSafe.js index 9e9a48ff..7f41f8b9 100644 --- a/src/routes/safe/store/actions/fetchSafe.js +++ b/src/routes/safe/store/actions/fetchSafe.js @@ -37,7 +37,14 @@ export const buildSafe = async (storedSafe: Object) => { } export default (safe: Safe) => async (dispatch: ReduxDispatch) => { - const safeRecord = await buildSafe(safe.toJSON()) + try { + const safeRecord = await buildSafe(safe.toJSON()) - return dispatch(updateSafe(safeRecord)) + return dispatch(updateSafe(safeRecord)) + } catch (err) { + // eslint-disable-next-line + console.log("Error while updating safe information") + + return Promise.resolve() + } } diff --git a/src/routes/safe/store/actions/fetchSafes.js b/src/routes/safe/store/actions/fetchSafes.js index 196c1b40..45633a22 100644 --- a/src/routes/safe/store/actions/fetchSafes.js +++ b/src/routes/safe/store/actions/fetchSafes.js @@ -11,15 +11,23 @@ const buildSafesFrom = async (loadedSafes: Object): Promise> = const safes = Map() const keys = Object.keys(loadedSafes) - const safeRecords = await Promise.all(keys.map((address: string) => buildSafe(loadedSafes[address]))) + try { + const safeRecords = await Promise.all(keys.map((address: string) => buildSafe(loadedSafes[address]))) - return safes.withMutations(async (map) => { - safeRecords.forEach((safe: Safe) => map.set(safe.get('address'), safe)) - }) + return safes.withMutations(async (map) => { + safeRecords.forEach((safe: Safe) => map.set(safe.get('address'), safe)) + }) + } catch (err) { + // eslint-disable-next-line + console.log("Error while fetching safes information") + + return Map() + } } export default () => async (dispatch: ReduxDispatch) => { const storedSafes = load(SAFES_KEY) + const safes = storedSafes ? await buildSafesFrom(storedSafes) : Map() return dispatch(updateSafes(safes)) diff --git a/src/routes/safe/store/reducer/balances.js b/src/routes/safe/store/reducer/balances.js deleted file mode 100644 index 2bad95d0..00000000 --- a/src/routes/safe/store/reducer/balances.js +++ /dev/null @@ -1,20 +0,0 @@ -// @flow -import { Map } from 'immutable' -import { handleActions, type ActionType } from 'redux-actions' -import addBalances, { ADD_BALANCES } from '~/routes/safe/store/actions/addBalances' -import { type Balance } from '~/routes/safe/store/model/balance' - -export const BALANCE_REDUCER_ID = 'balances' - -export type State = Map> - -export default handleActions({ - [ADD_BALANCES]: (state: State, action: ActionType): State => - state.update(action.payload.safeAddress, (prevSafe: Map) => { - if (!prevSafe) { - return action.payload.balances - } - - return prevSafe.equals(action.payload.balances) ? prevSafe : action.payload.balances - }), -}, Map()) diff --git a/src/routes/safe/store/selectors/index.js b/src/routes/safe/store/selectors/index.js index b1a9703f..7bbdd8b2 100644 --- a/src/routes/safe/store/selectors/index.js +++ b/src/routes/safe/store/selectors/index.js @@ -6,11 +6,9 @@ import { type GlobalState } from '~/store/index' import { SAFE_PARAM_ADDRESS } from '~/routes/routes' import { type Safe } from '~/routes/safe/store/model/safe' import { safesMapSelector } from '~/routes/safeList/store/selectors' -import { BALANCE_REDUCER_ID } from '~/routes/safe/store/reducer/balances' import { type State as TransactionsState, TRANSACTIONS_REDUCER_ID } from '~/routes/safe/store/reducer/transactions' import { type Transaction } from '~/routes/safe/store/model/transaction' import { type Confirmation } from '~/routes/safe/store/model/confirmation' -import { type Balance } from '~/routes/safe/store/model/balance' export type RouterProps = { match: Match, @@ -26,14 +24,12 @@ type TransactionProps = { const safePropAddressSelector = (state: GlobalState, props: SafeProps) => props.safeAddress -const safeParamAddressSelector = (state: GlobalState, props: RouterProps) => props.match.params[SAFE_PARAM_ADDRESS] || '' - -const balancesSelector = (state: GlobalState) => state[BALANCE_REDUCER_ID] - const transactionsSelector = (state: GlobalState): TransactionsState => state[TRANSACTIONS_REDUCER_ID] const oneTransactionSelector = (state: GlobalState, props: TransactionProps) => props.transaction +export const safeParamAddressSelector = (state: GlobalState, props: RouterProps) => props.match.params[SAFE_PARAM_ADDRESS] || '' + export const safeTransactionsSelector: Selector> = createSelector( transactionsSelector, safePropAddressSelector, @@ -82,18 +78,6 @@ export const safeSelector: Selector }, ) -export const balanceSelector: Selector> = createSelector( - balancesSelector, - safeParamAddressSelector, - (balances: Map>, address: string) => { - if (!address) { - return Map() - } - - return balances.get(address) || Map() - }, -) - export default createStructuredSelector({ safe: safeSelector, }) diff --git a/src/routes/safe/store/test/confirmations.selector.js b/src/routes/safe/store/test/confirmations.selector.js index 3f1f924d..e1023985 100644 --- a/src/routes/safe/store/test/confirmations.selector.js +++ b/src/routes/safe/store/test/confirmations.selector.js @@ -29,7 +29,7 @@ const grantedSelectorTests = () => { const reduxStore = { safes: Map(), providers: makeProvider(), - balances: Map(), + tokens: Map(), transactions: Map(), } @@ -67,7 +67,7 @@ const grantedSelectorTests = () => { const reduxStore = { safes: Map(), providers: makeProvider(), - balances: Map(), + tokens: Map(), transactions: Map(), } @@ -82,7 +82,7 @@ const grantedSelectorTests = () => { const reduxStore = { safes: Map(), providers: makeProvider(), - balances: Map(), + tokens: Map(), transactions: Map(), } diff --git a/src/routes/safe/store/test/granted.selector.js b/src/routes/safe/store/test/granted.selector.js index ff52fe15..cf51cd68 100644 --- a/src/routes/safe/store/test/granted.selector.js +++ b/src/routes/safe/store/test/granted.selector.js @@ -26,7 +26,7 @@ const grantedSelectorTests = () => { const reduxStore = { [SAFE_REDUCER_ID]: map, providers: makeProvider(provider), - balances: undefined, + tokens: undefined, transactions: undefined, } @@ -47,7 +47,7 @@ const grantedSelectorTests = () => { const reduxStore = { [SAFE_REDUCER_ID]: map, providers: makeProvider(provider), - balances: undefined, + tokens: undefined, transactions: undefined, } @@ -68,7 +68,7 @@ const grantedSelectorTests = () => { const reduxStore = { [SAFE_REDUCER_ID]: map, providers: makeProvider(provider), - balances: undefined, + tokens: undefined, transactions: undefined, } diff --git a/src/routes/safe/store/test/safe.selector.js b/src/routes/safe/store/test/safe.selector.js index 22c027ae..f8f8b931 100644 --- a/src/routes/safe/store/test/safe.selector.js +++ b/src/routes/safe/store/test/safe.selector.js @@ -14,7 +14,7 @@ const safeSelectorTests = () => { const reduxStore = { [SAFE_REDUCER_ID]: Map(), providers: undefined, - balances: undefined, + tokens: undefined, transactions: undefined, } const match: Match = buildMathPropsFrom('fooAddress') @@ -38,7 +38,7 @@ const safeSelectorTests = () => { const reduxStore = { [SAFE_REDUCER_ID]: map, providers: undefined, - balances: undefined, + tokens: undefined, transactions: undefined, } diff --git a/src/routes/safe/store/test/transactions.selector.js b/src/routes/safe/store/test/transactions.selector.js index 5897ffd8..e45063f2 100644 --- a/src/routes/safe/store/test/transactions.selector.js +++ b/src/routes/safe/store/test/transactions.selector.js @@ -14,7 +14,7 @@ const grantedSelectorTests = () => { const reduxStore = { [SAFE_REDUCER_ID]: Map(), providers: makeProvider(), - balances: undefined, + tokens: undefined, transactions: Map(), } @@ -46,7 +46,7 @@ const grantedSelectorTests = () => { const reduxStore = { [SAFE_REDUCER_ID]: Map(), providers: makeProvider(), - balances: undefined, + tokens: undefined, transactions: Map({ fooAddress: List([transaction]) }), } @@ -81,7 +81,7 @@ const grantedSelectorTests = () => { const reduxStore = { [SAFE_REDUCER_ID]: Map(), providers: makeProvider(), - balances: undefined, + tokens: undefined, transactions: Map({ fooAddress: List([transaction]) }), } @@ -113,7 +113,7 @@ const grantedSelectorTests = () => { const reduxStore = { [SAFE_REDUCER_ID]: Map(), providers: makeProvider(), - balances: undefined, + tokens: undefined, transactions: Map({ fooAddress: List([transaction]) }), } diff --git a/src/routes/safe/test/Safe.withdraw.test.js b/src/routes/safe/test/Safe.withdraw.test.js deleted file mode 100644 index 5699ec58..00000000 --- a/src/routes/safe/test/Safe.withdraw.test.js +++ /dev/null @@ -1,114 +0,0 @@ -// @flow -import * as React from 'react' -import TestUtils from 'react-dom/test-utils' -import { Provider } from 'react-redux' -import { ConnectedRouter } from 'react-router-redux' -import { type Match } from 'react-router-dom' -import Button from '~/components/layout/Button' -import { aNewStore, history } from '~/store' -import { addEtherTo } from '~/test/utils/tokenMovements' -import { aDeployedSafe, executeWithdrawOn } from '~/routes/safe/store/test/builder/deployedSafe.builder' -import { SAFELIST_ADDRESS } from '~/routes/routes' -import SafeView from '~/routes/safe/component/Safe' -import AppRoutes from '~/routes' -import { WITHDRAW_BUTTON_TEXT } from '~/routes/safe/component/Safe/DailyLimit' -import WithdrawComponent, { SEE_TXS_BUTTON_TEXT } from '~/routes/safe/component/Withdraw' -import { getBalanceInEtherOf } from '~/wallets/getWeb3' -import { sleep } from '~/utils/timer' -import { getDailyLimitFrom } from '~/routes/safe/component/Withdraw/withdraw' -import { type DailyLimitProps } from '~/routes/safe/store/model/dailyLimit' -import { WITHDRAW_INDEX } from '~/test/builder/safe.dom.utils' -import { buildMathPropsFrom } from '~/test/utils/buildReactRouterProps' -import { safeSelector } from '~/routes/safe/store/selectors/index' -import { filterMoveButtonsFrom } from '~/test/builder/safe.dom.builder' - -describe('React DOM TESTS > Withdraw funds from safe', () => { - let SafeDom - let store - let address - beforeEach(async () => { - // create store - store = aNewStore() - // deploy safe updating store - address = await aDeployedSafe(store) - // navigate to SAFE route - history.push(`${SAFELIST_ADDRESS}/${address}`) - SafeDom = TestUtils.renderIntoDocument(( - - - - - - )) - }) - - it('should withdraw funds under dailyLimit without needing confirmations', async () => { - // add funds to safe - await addEtherTo(address, '0.1') - await sleep(3000) - const Safe = TestUtils.findRenderedComponentWithType(SafeDom, SafeView) - - // $FlowFixMe - const expandedButtons = TestUtils.scryRenderedDOMComponentsWithTag(Safe, 'button') - const buttons = filterMoveButtonsFrom(expandedButtons) - const addWithdrawButton = buttons[WITHDRAW_INDEX] - expect(addWithdrawButton.getElementsByTagName('span')[0].innerHTML).toEqual(WITHDRAW_BUTTON_TEXT) - TestUtils.Simulate.click(addWithdrawButton) - await sleep(4000) - const Withdraw = TestUtils.findRenderedComponentWithType(SafeDom, WithdrawComponent) - - // $FlowFixMe - const inputs = TestUtils.scryRenderedDOMComponentsWithTag(Withdraw, 'input') - const amountInEth = inputs[0] - const toAddress = inputs[1] - TestUtils.Simulate.change(amountInEth, { target: { value: '0.01' } }) - TestUtils.Simulate.change(toAddress, { target: { value: store.getState().providers.account } }) - - // $FlowFixMe - const form = TestUtils.findRenderedDOMComponentWithTag(Withdraw, 'form') - - TestUtils.Simulate.submit(form) // fill the form - TestUtils.Simulate.submit(form) // confirming data - await sleep(6000) - - const safeBalance = await getBalanceInEtherOf(address) - expect(safeBalance).toBe('0.09') - - // $FlowFixMe - const withdrawButtons = TestUtils.scryRenderedComponentsWithType(Withdraw, Button) - const visitTxsButton = withdrawButtons[0] - expect(visitTxsButton.props.children).toEqual(SEE_TXS_BUTTON_TEXT) - }) - - it('spentToday dailyLimitModule property is updated correctly', async () => { - // add funds to safe - await addEtherTo(address, '0.1') - - const match: Match = buildMathPropsFrom(address) - const safe = safeSelector(store.getState(), { match }) - if (!safe) throw new Error() - await executeWithdrawOn(safe, 0.01) - await executeWithdrawOn(safe, 0.01) - - const ethAddress = 0 - const dailyLimit: DailyLimitProps = await getDailyLimitFrom(address, ethAddress) - - // THEN - expect(dailyLimit.value).toBe(0.5) - expect(dailyLimit.spentToday).toBe(0.02) - }) - - it('Withdraw button disabled when balance is 0', async () => { - const Safe = TestUtils.findRenderedComponentWithType(SafeDom, SafeView) - // $FlowFixMe - const buttons = TestUtils.scryRenderedDOMComponentsWithTag(Safe, 'button') - const addWithdrawButton = buttons[WITHDRAW_INDEX] - expect(addWithdrawButton.getElementsByTagName('span')[0].innerHTML).toEqual(WITHDRAW_BUTTON_TEXT) - expect(addWithdrawButton.hasAttribute('disabled')).toBe(true) - - await addEtherTo(address, '0.1') - await sleep(1800) - - expect(addWithdrawButton.hasAttribute('disabled')).toBe(false) - }) -}) diff --git a/src/routes/safeList/store/test/safes.selector.js b/src/routes/safeList/store/test/safes.selector.js index b5946457..a5f20230 100644 --- a/src/routes/safeList/store/test/safes.selector.js +++ b/src/routes/safeList/store/test/safes.selector.js @@ -21,7 +21,7 @@ const safesListSelectorTests = () => { const reduxStore = { [PROVIDER_REDUCER_ID]: walletRecord, [SAFE_REDUCER_ID]: Map(), - balances: undefined, + tokens: undefined, transactions: undefined, } const emptyList = List([]) @@ -42,7 +42,7 @@ const safesListSelectorTests = () => { const reduxStore = { [PROVIDER_REDUCER_ID]: walletRecord, [SAFE_REDUCER_ID]: map, - balances: undefined, + tokens: undefined, transactions: undefined, } @@ -62,7 +62,7 @@ const safesListSelectorTests = () => { const reduxStore = { [PROVIDER_REDUCER_ID]: walletRecord, [SAFE_REDUCER_ID]: map, - balances: undefined, + tokens: undefined, transactions: undefined, } @@ -83,7 +83,7 @@ const safesListSelectorTests = () => { const reduxStore = { [SAFE_REDUCER_ID]: map, [PROVIDER_REDUCER_ID]: walletRecord, - balances: undefined, + tokens: undefined, transactions: undefined, } @@ -105,7 +105,7 @@ const safesListSelectorTests = () => { const reduxStore = { [SAFE_REDUCER_ID]: map, [PROVIDER_REDUCER_ID]: walletRecord, - balances: undefined, + tokens: undefined, transactions: undefined, } diff --git a/src/routes/tokens/component/Layout.jsx b/src/routes/tokens/component/Layout.jsx new file mode 100644 index 00000000..36016f13 --- /dev/null +++ b/src/routes/tokens/component/Layout.jsx @@ -0,0 +1,99 @@ +// @flow +import MuiList from '@material-ui/core/List' +import * as React from 'react' +import Block from '~/components/layout/Block' +import Col from '~/components/layout/Col' +import AccountBalanceWallet from '@material-ui/icons/AccountBalanceWallet' +import Link from '~/components/layout/Link' +import Bold from '~/components/layout/Bold' +import Img from '~/components/layout/Img' +import IconButton from '@material-ui/core/IconButton' +import Paragraph from '~/components/layout/Paragraph' +import Row from '~/components/layout/Row' +import { type Token } from '~/routes/tokens/store/model/token' +import { type SelectorProps } from '~/routes/tokens/container/selector' +import { type Actions } from '~/routes/tokens/container/actions' +import { SAFELIST_ADDRESS } from '~/routes/routes' +import TokenComponent from './Token' +// import AddToken from '~/routes/tokens/component/AddToken' +// import RemoveToken from '~/routes/tokens/component/RemoveToken' + +const safeIcon = require('~/routes/safe/component/Safe/assets/gnosis_safe.svg') + +type TokenProps = SelectorProps & Actions + +type State = { + component: React$Node, +} + +const listStyle = { + width: '100%', + paddingBottom: 0, +} + +class TokenLayout extends React.PureComponent { + state = { + component: undefined, + } + /* + onAddToken = () => { + const { addresses } = this.props + this.setState({ component: }) + } + + onRemoveToken = () => { + this.setState({ component: }) + } + */ + onEnableToken = (token: Token) => { + const { enableToken, safe } = this.props + const safeAddress = safe.get('address') + + enableToken(safeAddress, token) + } + + onDisableToken = (token: Token) => { + const { disableToken, safe } = this.props + const safeAddress = safe.get('address') + + disableToken(safeAddress, token) + } + + render() { + const { safe, safeAddress, tokens } = this.props + const { component } = this.state + const name = safe ? safe.get('name') : '' + + return ( + + + + {tokens.map((token: Token) => ())} + + + + + + + + + {name} + + + + + { component || Safe Icon } + + + + + ) + } +} + +export default TokenLayout diff --git a/src/routes/tokens/component/Token/index.jsx b/src/routes/tokens/component/Token/index.jsx new file mode 100644 index 00000000..b40a789d --- /dev/null +++ b/src/routes/tokens/component/Token/index.jsx @@ -0,0 +1,98 @@ +// @flow +import * as React from 'react' +import { type Token } from '~/routes/tokens/store/model/token' +import { withStyles } from '@material-ui/core/styles' +import Block from '~/components/layout/Block' +import Checkbox from '@material-ui/core/Checkbox' +import Card from '@material-ui/core/Card' +import CardContent from '@material-ui/core/CardContent' +import CardMedia from '@material-ui/core/CardMedia' +import Typography from '@material-ui/core/Typography' +import { isEther } from '~/utils/tokens' +// import Delete from '@material-ui/icons/Delete' +// import IconButton from '@material-ui/core/IconButton' +import { type WithStyles } from '~/theme/mui' + +type Props = WithStyles & { + token: Token, + onRemoveToken: (balance: Token)=> void, + onEnableToken: (token: Token) => void, + onDisableToken: (token: Token) => void, +} + +type State = { + checked: boolean, +} + +const styles = () => ({ + card: { + display: 'flex', + }, + details: { + display: 'flex', + flexDirection: 'column', + }, + content: { + flex: '1 0 auto', + }, + cover: { + width: 150, + margin: 10, + backgroundSize: '50%', + }, +}) + +class TokenComponent extends React.Component { + state = { + checked: this.props.token.get('status'), + } + + // onRemoveClick = () => this.props.onRemoveToken(this.props.token) + + handleChange = (e: SyntheticInputEvent) => { + const { checked } = e.target + + const callback = checked ? this.props.onEnableToken : this.props.onDisableToken + this.setState(() => ({ checked }), () => callback(this.props.token)) + } + + render() { + const { classes, token } = this.props + const name = token.get('name') + const symbol = token.get('symbol') + const disabled = isEther(symbol) + + return ( + + + + {name} + + + {symbol} + + + + {/* + + + + + + */} + + + ) + } +} + +export default withStyles(styles, { withTheme: true })(TokenComponent) diff --git a/src/routes/tokens/container/actions.js b/src/routes/tokens/container/actions.js new file mode 100644 index 00000000..d59f86bc --- /dev/null +++ b/src/routes/tokens/container/actions.js @@ -0,0 +1,15 @@ +// @flow +import enableToken from '~/routes/tokens/store/actions/enableToken' +import disableToken from '~/routes/tokens/store/actions/disableToken' +import { fetchTokens } from '~/routes/tokens/store/actions/fetchTokens' + +export type Actions = { + enableToken: typeof enableToken, + disableToken: typeof disableToken, +} + +export default { + enableToken, + disableToken, + fetchTokens, +} diff --git a/src/routes/tokens/container/index.jsx b/src/routes/tokens/container/index.jsx new file mode 100644 index 00000000..8a83a93e --- /dev/null +++ b/src/routes/tokens/container/index.jsx @@ -0,0 +1,43 @@ +// @flow +import * as React from 'react' +import { connect } from 'react-redux' +import Page from '~/components/layout/Page' +import Layout from '~/routes/tokens/component/Layout' +import { fetchTokens } from '~/routes/tokens/store/actions/fetchTokens' +import selector, { type SelectorProps } from './selector' +import actions, { type Actions } from './actions' + +type Props = Actions & SelectorProps & { + fetchTokens: typeof fetchTokens, +} + +class TokensView extends React.PureComponent { + componentDidUpdate() { + const { safeAddress } = this.props + + if (this.props.tokens.count() === 0) { + this.props.fetchTokens(safeAddress) + } + } + + render() { + const { + tokens, addresses, safe, safeAddress, disableToken, enableToken, + } = this.props + + return ( + + + + ) + } +} + +export default connect(selector, actions)(TokensView) diff --git a/src/routes/tokens/container/selector.js b/src/routes/tokens/container/selector.js new file mode 100644 index 00000000..dd490988 --- /dev/null +++ b/src/routes/tokens/container/selector.js @@ -0,0 +1,21 @@ +// @flow +import { List } from 'immutable' +import { createStructuredSelector } from 'reselect' +import { tokenListSelector, tokenAddressesSelector } from '~/routes/tokens/store/selectors' +import { type Safe } from '~/routes/safe/store/model/safe' +import { safeSelector, safeParamAddressSelector } from '~/routes/safe/store/selectors' +import { type Token } from '~/routes/tokens/store/model/token' + +export type SelectorProps = { + tokens: List, + addresses: List, + safe: Safe, + safeAddress: string, +} + +export default createStructuredSelector({ + safe: safeSelector, + safeAddress: safeParamAddressSelector, + tokens: tokenListSelector, + addresses: tokenAddressesSelector, +}) diff --git a/src/routes/tokens/store/actions/addTokens.js b/src/routes/tokens/store/actions/addTokens.js new file mode 100644 index 00000000..28705a89 --- /dev/null +++ b/src/routes/tokens/store/actions/addTokens.js @@ -0,0 +1,21 @@ +// @flow +import { Map } from 'immutable' +import { createAction } from 'redux-actions' +import { type Token } from '~/routes/tokens/store/model/token' + +export const ADD_TOKENS = 'ADD_TOKENS' + +type TokenProps = { + safeAddress: string, + tokens: Map, +} + +const addTokens = createAction( + ADD_TOKENS, + (safeAddress: string, tokens: Map): TokenProps => ({ + safeAddress, + tokens, + }), +) + +export default addTokens diff --git a/src/routes/tokens/store/actions/disableToken.js b/src/routes/tokens/store/actions/disableToken.js new file mode 100644 index 00000000..eae0db78 --- /dev/null +++ b/src/routes/tokens/store/actions/disableToken.js @@ -0,0 +1,16 @@ +// @flow +import { createAction } from 'redux-actions' +import { type Token } from '~/routes/tokens/store/model/token' + +export const DISABLE_TOKEN = 'DISABLE_TOKEN' + +const disableToken = createAction( + DISABLE_TOKEN, + (safeAddress: string, token: Token) => ({ + safeAddress, + symbol: token.get('symbol'), + address: token.get('address'), + }), +) + +export default disableToken diff --git a/src/routes/tokens/store/actions/enableToken.js b/src/routes/tokens/store/actions/enableToken.js new file mode 100644 index 00000000..9056e4dc --- /dev/null +++ b/src/routes/tokens/store/actions/enableToken.js @@ -0,0 +1,16 @@ +// @flow +import { createAction } from 'redux-actions' +import { type Token } from '~/routes/tokens/store/model/token' + +export const ENABLE_TOKEN = 'ENABLE_TOKEN' + +const enableToken = createAction( + ENABLE_TOKEN, + (safeAddress: string, token: Token) => ({ + safeAddress, + symbol: token.get('symbol'), + address: token.get('address'), + }), +) + +export default enableToken diff --git a/src/routes/tokens/store/actions/fetchTokens.js b/src/routes/tokens/store/actions/fetchTokens.js new file mode 100644 index 00000000..3ddc9b09 --- /dev/null +++ b/src/routes/tokens/store/actions/fetchTokens.js @@ -0,0 +1,67 @@ +// @flow +import { List, Map } from 'immutable' +import contract from 'truffle-contract' +import type { Dispatch as ReduxDispatch } from 'redux' +import StandardToken from '@gnosis.pm/util-contracts/build/contracts/StandardToken.json' +import { getWeb3 } from '~/wallets/getWeb3' +import { type GlobalState } from '~/store/index' +import { makeToken, type Token, type TokenProps } from '~/routes/tokens/store/model/token' +import { ensureOnce } from '~/utils/singleton' +import { getTokens } from '~/utils/localStorage/tokens' +import { getSafeEthToken } from '~/utils/tokens' +import { enhancedFetch } from '~/utils/fetch' +import addTokens from './addTokens' + +const createStandardTokenContract = async () => { + const web3 = getWeb3() + const erc20Token = await contract(StandardToken) + erc20Token.setProvider(web3.currentProvider) + + return erc20Token +} + +export const getStandardTokenContract = ensureOnce(createStandardTokenContract) + +export const calculateBalanceOf = async (tokenAddress: string, address: string, decimals: number) => { + const erc20Token = await getStandardTokenContract() + + return erc20Token.at(tokenAddress) + .then(instance => instance.balanceOf(address).then(funds => funds.div(10 ** decimals).toString())) + .catch(() => '0') +} + +export const fetchTokensData = async () => { + const url = 'https://gist.githubusercontent.com/rmeissner/98911fcf74b0ea9731e2dae2441c97a4/raw/' + const errMsg = 'Error querying safe balances' + return enhancedFetch(url, errMsg) +} + +export const fetchTokens = (safeAddress: string) => + async (dispatch: ReduxDispatch) => { + const tokens: List = getTokens(safeAddress) + const ethBalance = await getSafeEthToken(safeAddress) + + const json = await exports.fetchTokensData() + + try { + const balancesRecords = await Promise.all(json.map(async (item: TokenProps) => { + const status = tokens.includes(item.address) + const funds = status ? await calculateBalanceOf(item.address, safeAddress, item.decimals) : '0' + + return makeToken({ ...item, status, funds }) + })) + + const balances: Map = Map().withMutations((map) => { + balancesRecords.forEach(record => map.set(record.get('symbol'), record)) + + map.set('ETH', ethBalance) + }) + + return dispatch(addTokens(safeAddress, balances)) + } catch (err) { + // eslint-disable-next-line + console.log("Error fetching token balances... " + err) + + return Promise.resolve() + } + } diff --git a/src/routes/safe/store/model/balance.js b/src/routes/tokens/store/model/token.js similarity index 59% rename from src/routes/safe/store/model/balance.js rename to src/routes/tokens/store/model/token.js index c8adf28a..713a68e3 100644 --- a/src/routes/safe/store/model/balance.js +++ b/src/routes/tokens/store/model/token.js @@ -2,22 +2,26 @@ import { Record } from 'immutable' import type { RecordFactory, RecordOf } from 'immutable' -export type BalanceProps = { +export type TokenProps = { address: string, name: string, symbol: string, decimals: number, logoUrl: string, funds: string, + status: boolean, + removable: boolean, } -export const makeBalance: RecordFactory = Record({ +export const makeToken: RecordFactory = Record({ address: '', name: '', symbol: '', decimals: 0, logoUrl: '', funds: '0', + status: true, + removable: false, }) -export type Balance = RecordOf +export type Token = RecordOf diff --git a/src/routes/tokens/store/reducer/tokens.js b/src/routes/tokens/store/reducer/tokens.js new file mode 100644 index 00000000..d66c02da --- /dev/null +++ b/src/routes/tokens/store/reducer/tokens.js @@ -0,0 +1,50 @@ +// @flow +import { List, Map } from 'immutable' +import { handleActions, type ActionType } from 'redux-actions' +import addTokens, { ADD_TOKENS } from '~/routes/tokens/store/actions/addTokens' +import { type Token } from '~/routes/tokens/store/model/token' +import disableToken, { DISABLE_TOKEN } from '~/routes/tokens/store/actions/disableToken' +import enableToken, { ENABLE_TOKEN } from '~/routes/tokens/store/actions/enableToken' +import { setTokens, getTokens } from '~/utils/localStorage/tokens' +import { ensureOnce } from '~/utils/singleton' +import { calculateActiveErc20TokensFrom } from '~/utils/tokens' + +export const TOKEN_REDUCER_ID = 'tokens' + +export type State = Map> + +const setTokensOnce = ensureOnce(setTokens) + +export default handleActions({ + [ADD_TOKENS]: (state: State, action: ActionType): State => { + const { safeAddress, tokens } = action.payload + + const activeAddresses: List = calculateActiveErc20TokensFrom(tokens.toList()) + setTokensOnce(safeAddress, activeAddresses) + + return state.update(safeAddress, (prevSafe: Map) => { + if (!prevSafe) { + return tokens + } + + return prevSafe.equals(tokens) ? prevSafe : tokens + }) + }, + [DISABLE_TOKEN]: (state: State, action: ActionType): State => { + const { address, safeAddress, symbol } = action.payload + + const activeTokens = getTokens(safeAddress) + const index = activeTokens.indexOf(address) + setTokens(safeAddress, activeTokens.delete(index)) + + return state.setIn([safeAddress, symbol, 'status'], false) + }, + [ENABLE_TOKEN]: (state: State, action: ActionType): State => { + const { address, safeAddress, symbol } = action.payload + + const activeTokens = getTokens(safeAddress) + setTokens(safeAddress, activeTokens.push(address)) + + return state.setIn([safeAddress, symbol, 'status'], true) + }, +}, Map()) diff --git a/src/routes/tokens/store/selectors/index.js b/src/routes/tokens/store/selectors/index.js new file mode 100644 index 00000000..2339dcb5 --- /dev/null +++ b/src/routes/tokens/store/selectors/index.js @@ -0,0 +1,41 @@ +// @flow +import { List, Map } from 'immutable' +import { createSelector, type Selector } from 'reselect' +import { safeParamAddressSelector, type RouterProps } from '~/routes/safe/store/selectors' +import { type GlobalState } from '~/store' +import { TOKEN_REDUCER_ID } from '~/routes/tokens/store/reducer/tokens' +import { type Token } from '~/routes/tokens/store/model/token' + +const balancesSelector = (state: GlobalState) => state[TOKEN_REDUCER_ID] + +export const tokensSelector: Selector> = createSelector( + balancesSelector, + safeParamAddressSelector, + (tokens: Map>, address: string) => { + if (!address) { + return Map() + } + + return tokens.get(address) || Map() + }, +) + +export const tokenListSelector = createSelector( + tokensSelector, + (tokens: Map) => tokens.toList(), +) + +export const activeTokensSelector = createSelector( + tokenListSelector, + (tokens: List) => tokens.filter((token: Token) => token.get('status')), +) + +export const tokenAddressesSelector = createSelector( + tokenListSelector, + (balances: List) => { + const addresses = List().withMutations(list => + balances.map(token => list.push(token.address))) + + return addresses + }, +) diff --git a/src/store/index.js b/src/store/index.js index c968b44d..48924c8a 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -5,7 +5,7 @@ import { combineReducers, createStore, applyMiddleware, compose, type Reducer, t import thunk from 'redux-thunk' import provider, { PROVIDER_REDUCER_ID, type State as ProviderState } from '~/wallets/store/reducer/provider' import safe, { SAFE_REDUCER_ID, type State as SafeState } from '~/routes/safe/store/reducer/safe' -import balances, { BALANCE_REDUCER_ID, type State as BalancesState } from '~/routes/safe/store/reducer/balances' +import tokens, { TOKEN_REDUCER_ID, type State as TokensState } from '~/routes/tokens/store/reducer/tokens' import transactions, { type State as TransactionsState, transactionsInitialState, TRANSACTIONS_REDUCER_ID } from '~/routes/safe/store/reducer/transactions' export const history = createBrowserHistory() @@ -20,7 +20,7 @@ const finalCreateStore = composeEnhancers(applyMiddleware( export type GlobalState = { providers: ProviderState, safes: SafeState, - balances: BalancesState, + tokens: TokensState, transactions: TransactionsState, } @@ -28,7 +28,7 @@ const reducers: Reducer = combineReducers({ routing: routerReducer, [PROVIDER_REDUCER_ID]: provider, [SAFE_REDUCER_ID]: safe, - [BALANCE_REDUCER_ID]: balances, + [TOKEN_REDUCER_ID]: tokens, [TRANSACTIONS_REDUCER_ID]: transactions, }) diff --git a/src/test/builder/safe.dom.utils.js b/src/test/builder/safe.dom.utils.js index c11d7571..3b50c0d6 100644 --- a/src/test/builder/safe.dom.utils.js +++ b/src/test/builder/safe.dom.utils.js @@ -8,7 +8,7 @@ import { sleep } from '~/utils/timer' import { Provider } from 'react-redux' import { ConnectedRouter } from 'react-router-redux' import AppRoutes from '~/routes' -import { SAFELIST_ADDRESS } from '~/routes/routes' +import { SAFELIST_ADDRESS, SETTINS_ADDRESS } from '~/routes/routes' import { history, type GlobalState } from '~/store' import { EMPTY_DATA } from '~/wallets/ethTransactions' @@ -93,15 +93,25 @@ export const refreshTransactions = async (store: Store) => { await sleep(1500) } -export const travelToSafe = (store: Store, address: string): React$Component<{}> => { - history.push(`${SAFELIST_ADDRESS}/${address}`) - const SafeDom = TestUtils.renderIntoDocument(( +const createDom = (store: Store): React$Component<{}> => ( + TestUtils.renderIntoDocument(( )) +) - return SafeDom +export const travelToSafe = (store: Store, address: string): React$Component<{}> => { + history.push(`${SAFELIST_ADDRESS}/${address}`) + + return createDom(store) +} + +export const travelToTokens = (store: Store, address: string): React$Component<{}> => { + const url = `${SAFELIST_ADDRESS}/${address}${SETTINS_ADDRESS}` + history.push(url) + + return createDom(store) } diff --git a/src/test/builder/tokens.dom.utils.js b/src/test/builder/tokens.dom.utils.js new file mode 100644 index 00000000..3c46152e --- /dev/null +++ b/src/test/builder/tokens.dom.utils.js @@ -0,0 +1,29 @@ +// @flow +import * as TestUtils from 'react-dom/test-utils' +import { travelToTokens } from '~/test/builder/safe.dom.utils' +import { sleep } from '~/utils/timer' +import { type Token } from '~/routes/tokens/store/model/token' + +export const enableFirstToken = async (store: Store, safeAddress: string) => { + const TokensDom = await travelToTokens(store, safeAddress) + await sleep(400) + + // WHEN + const inputs = TestUtils.scryRenderedDOMComponentsWithTag(TokensDom, 'input') + + const ethTokenInput = inputs[2] + expect(ethTokenInput.hasAttribute('disabled')).toBe(true) + const firstTokenInput = inputs[0] + expect(firstTokenInput.hasAttribute('disabled')).toBe(false) + TestUtils.Simulate.change(firstTokenInput, { target: { checked: 'true' } }) +} + +export const testToken = (token: Token | typeof undefined, symbol: string, status: boolean, funds?: string) => { + if (!token) throw new Error() + expect(token.get('symbol')).toBe(symbol) + expect(token.get('status')).toBe(status) + if (funds) { + expect(token.get('funds')).toBe(funds) + } +} + diff --git a/src/test/safe.dom.tokens.test.js b/src/test/safe.dom.tokens.test.js index 5c6ce2e2..858524ea 100644 --- a/src/test/safe.dom.tokens.test.js +++ b/src/test/safe.dom.tokens.test.js @@ -1,10 +1,10 @@ // @flow import TestUtils from 'react-dom/test-utils' -import * as fetchBalancesAction from '~/routes/safe/store/actions/fetchBalances' +import * as fetchBalancesAction from '~/routes/tokens/store/actions/fetchTokens' import { aNewStore } from '~/store' import { aMinedSafe } from '~/test/builder/safe.redux.builder' -import { addTknTo, getTokenContract } from '~/test/utils/tokenMovements' +import { addTknTo, getFirstTokenContract } from '~/test/utils/tokenMovements' import { EXPAND_BALANCE_INDEX, travelToSafe } from '~/test/builder/safe.dom.utils' import { promisify } from '~/utils/promisify' import { getWeb3 } from '~/wallets/getWeb3' @@ -47,14 +47,14 @@ describe('DOM > Feature > SAFE ERC20 TOKENS', () => { const receiverFunds = await fetchBalancesAction.calculateBalanceOf(tokenAddress, receiver, 18) expect(Number(receiverFunds)).toBe(20) - const token = await getTokenContract(getWeb3(), accounts[0]) + const token = await getFirstTokenContract(getWeb3(), accounts[0]) const nativeSafeFunds = await token.balanceOf(safeAddress) expect(Number(nativeSafeFunds.valueOf())).toEqual(80 * (10 ** 18)) }) it('disables send token button when balance is 0', async () => { // GIVEN - const token = await getTokenContract(getWeb3(), accounts[0]) + const token = await getFirstTokenContract(getWeb3(), accounts[0]) await dispatchTknBalance(store, token.address, safeAddress) // WHEN diff --git a/src/test/safe.redux.balance.test.js b/src/test/safe.redux.balance.test.js index 07fe16e8..352e639c 100644 --- a/src/test/safe.redux.balance.test.js +++ b/src/test/safe.redux.balance.test.js @@ -1,10 +1,10 @@ // @flow import { Map } from 'immutable' -import { BALANCE_REDUCER_ID } from '~/routes/safe/store/reducer/balances' -import * as fetchBalancesAction from '~/routes/safe/store/actions/fetchBalances' +import * as fetchTokensAction from '~/routes/tokens/store/actions/fetchTokens' import { aNewStore } from '~/store' import { aMinedSafe } from '~/test/builder/safe.redux.builder' -import { type Balance } from '~/routes/safe/store/model/balance' +import { type Token } from '~/routes/tokens/store/model/token' +import { TOKEN_REDUCER_ID } from '~/routes/tokens/store/reducer/tokens' import { addEtherTo, addTknTo } from '~/test/utils/tokenMovements' import { dispatchTknBalance } from '~/test/utils/transactions/moveTokens.helper' @@ -21,13 +21,13 @@ describe('Safe - redux balance property', () => { const tokenList = ['WE', '<3', 'GNO', 'OMG', 'RDN'] // WHEN - await store.dispatch(fetchBalancesAction.fetchBalances(address)) + await store.dispatch(fetchTokensAction.fetchTokens(address)) // THEN - const balances: Map> | typeof undefined = store.getState()[BALANCE_REDUCER_ID] - if (!balances) throw new Error() + const tokens: Map> | typeof undefined = store.getState()[TOKEN_REDUCER_ID] + if (!tokens) throw new Error() - const safeBalances: Map | typeof undefined = balances.get(address) + const safeBalances: Map | typeof undefined = tokens.get(address) if (!safeBalances) throw new Error() expect(safeBalances.size).toBe(6) @@ -41,13 +41,13 @@ describe('Safe - redux balance property', () => { it('reducer should return 0.03456 ETH as funds to safe with 0.03456 ETH', async () => { // WHEN await addEtherTo(address, '0.03456') - await store.dispatch(fetchBalancesAction.fetchBalances(address)) + await store.dispatch(fetchTokensAction.fetchTokens(address)) // THEN - const balances: Map> | typeof undefined = store.getState()[BALANCE_REDUCER_ID] - if (!balances) throw new Error() + const tokens: Map> | typeof undefined = store.getState()[TOKEN_REDUCER_ID] + if (!tokens) throw new Error() - const safeBalances: Map | typeof undefined = balances.get(address) + const safeBalances: Map | typeof undefined = tokens.get(address) if (!safeBalances) throw new Error() expect(safeBalances.size).toBe(6) @@ -65,7 +65,7 @@ describe('Safe - redux balance property', () => { await dispatchTknBalance(store, tokenAddress, address) // THEN - const safeBalances = store.getState()[BALANCE_REDUCER_ID].get(address) + const safeBalances = store.getState()[TOKEN_REDUCER_ID].get(address) expect(safeBalances.size).toBe(1) const tknBalance = safeBalances.get('TKN') diff --git a/src/test/safe.redux.withdraw.test.js b/src/test/safe.redux.withdraw.test.js new file mode 100644 index 00000000..af8a1a80 --- /dev/null +++ b/src/test/safe.redux.withdraw.test.js @@ -0,0 +1,84 @@ +// @flow +import * as React from 'react' +import TestUtils from 'react-dom/test-utils' +import { Provider } from 'react-redux' +import { ConnectedRouter } from 'react-router-redux' +import { aNewStore, history } from '~/store' +import { addEtherTo } from '~/test/utils/tokenMovements' +import { executeWithdrawOn } from '~/routes/safe/store/test/builder/deployedSafe.builder' +import { SAFELIST_ADDRESS } from '~/routes/routes' +import SafeView from '~/routes/safe/component/Safe' +import AppRoutes from '~/routes' +import { WITHDRAW_BUTTON_TEXT } from '~/routes/safe/component/Safe/DailyLimit' +import { getBalanceInEtherOf } from '~/wallets/getWeb3' +import { sleep } from '~/utils/timer' +import { getDailyLimitFrom } from '~/routes/safe/component/Withdraw/withdraw' +import { type DailyLimitProps } from '~/routes/safe/store/model/dailyLimit' +import { WITHDRAW_INDEX } from '~/test/builder/safe.dom.utils' +import { aMinedSafe } from '~/test/builder/safe.redux.builder' +import { getSafeFrom } from '~/test/utils/safeHelper' +import { filterMoveButtonsFrom } from '~/test/builder/safe.dom.builder' +import { fetchTokens } from '~/routes/tokens/store/actions/fetchTokens' + +describe('React DOM TESTS > Withdraw funds from safe', () => { + let store + let safeAddress: string + beforeEach(async () => { + store = aNewStore() + safeAddress = await aMinedSafe(store) + }) + + it('should withdraw funds under dailyLimit without needing confirmations', async () => { + // add funds to safe + await addEtherTo(safeAddress, '0.1') + + const safe = getSafeFrom(store.getState(), safeAddress) + await executeWithdrawOn(safe, 0.01) + + const safeBalance = await getBalanceInEtherOf(safeAddress) + expect(safeBalance).toBe('0.09') + }) + + it('spentToday dailyLimitModule property is updated correctly', async () => { + // add funds to safe + await addEtherTo(safeAddress, '0.1') + + const safe = getSafeFrom(store.getState(), safeAddress) + await executeWithdrawOn(safe, 0.01) + await executeWithdrawOn(safe, 0.01) + + const ethAddress = 0 + const dailyLimit: DailyLimitProps = await getDailyLimitFrom(safeAddress, ethAddress) + + // THEN + expect(dailyLimit.value).toBe(0.5) + expect(dailyLimit.spentToday).toBe(0.02) + }) + + it('Withdraw button disabled when balance is 0', async () => { + // navigate to SAFE route + history.push(`${SAFELIST_ADDRESS}/${safeAddress}`) + const SafeDom = TestUtils.renderIntoDocument(( + + + + + + )) + + await sleep(300) + const Safe = TestUtils.findRenderedComponentWithType(SafeDom, SafeView) + // $FlowFixMe + const buttons = TestUtils.scryRenderedDOMComponentsWithTag(Safe, 'button') + const filteredButtons = filterMoveButtonsFrom(buttons) + const addWithdrawButton = filteredButtons[WITHDRAW_INDEX] + expect(addWithdrawButton.getElementsByTagName('span')[0].innerHTML).toEqual(WITHDRAW_BUTTON_TEXT) + expect(addWithdrawButton.hasAttribute('disabled')).toBe(true) + + await addEtherTo(safeAddress, '0.1') + await store.dispatch(fetchTokens(safeAddress)) + await sleep(150) + + expect(addWithdrawButton.hasAttribute('disabled')).toBe(false) + }) +}) diff --git a/src/test/tokens.dom.enabling.test.js b/src/test/tokens.dom.enabling.test.js new file mode 100644 index 00000000..0565f5c4 --- /dev/null +++ b/src/test/tokens.dom.enabling.test.js @@ -0,0 +1,126 @@ +// @flow +import * as TestUtils from 'react-dom/test-utils' +import { List } from 'immutable' +import { getWeb3 } from '~/wallets/getWeb3' +import { type Match } from 'react-router-dom' +import { promisify } from '~/utils/promisify' +import TokenComponent from '~/routes/tokens/component/Token' +import Checkbox from '@material-ui/core/Checkbox' +import { getFirstTokenContract, getSecondTokenContract, addTknTo } from '~/test/utils/tokenMovements' +import { aNewStore } from '~/store' +import { aMinedSafe } from '~/test/builder/safe.redux.builder' +import { travelToTokens } from '~/test/builder/safe.dom.utils' +import { sleep } from '~/utils/timer' +import { buildMathPropsFrom } from '~/test/utils/buildReactRouterProps' +import { tokenListSelector, activeTokensSelector } from '~/routes/tokens/store/selectors' +import { getTokens } from '~/utils/localStorage/tokens' +import { enableFirstToken, testToken } from '~/test/builder/tokens.dom.utils' +import * as fetchTokensModule from '~/routes/tokens/store/actions/fetchTokens' +import * as enhancedFetchModule from '~/utils/fetch' + +describe('DOM > Feature > Enable and disable default tokens', () => { + let web3 + let accounts + let firstErc20Token + let secondErc20Token + + beforeAll(async () => { + web3 = getWeb3() + accounts = await promisify(cb => web3.eth.getAccounts(cb)) + firstErc20Token = await getFirstTokenContract(web3, accounts[0]) + secondErc20Token = await getSecondTokenContract(web3, accounts[0]) + // $FlowFixMe + enhancedFetchModule.enhancedFetch = jest.fn() + enhancedFetchModule.enhancedFetch.mockImplementation(() => Promise.resolve([ + { + address: firstErc20Token.address, + name: 'First Token Example', + symbol: 'FTE', + decimals: 18, + logoUrl: 'https://upload.wikimedia.org/wikipedia/commons/c/c0/Earth_simple_icon.png', + }, + { + address: secondErc20Token.address, + name: 'Second Token Example', + symbol: 'STE', + decimals: 18, + logoUrl: 'https://upload.wikimedia.org/wikipedia/commons/c/c0/Earth_simple_icon.png', + }, + ])) + }) + + it('retrieves only ether as active token in first moment', async () => { + // GIVEN + const store = aNewStore() + const safeAddress = await aMinedSafe(store) + await store.dispatch(fetchTokensModule.fetchTokens(safeAddress)) + + // WHEN + const TokensDom = await travelToTokens(store, safeAddress) + await sleep(400) + + // THEN + const tokens = TestUtils.scryRenderedComponentsWithType(TokensDom, TokenComponent) + expect(tokens.length).toBe(3) + + testToken(tokens[0].props.token, 'FTE', false) + testToken(tokens[1].props.token, 'STE', false) + testToken(tokens[2].props.token, 'ETH', true) + + const ethCheckbox = TestUtils.findRenderedComponentWithType(tokens[2], Checkbox) + if (!ethCheckbox) throw new Error() + expect(ethCheckbox.props.disabled).toBe(true) + }) + + it('fetch balances of only enabled tokens', async () => { + // GIVEN + const store = aNewStore() + const safeAddress = await aMinedSafe(store) + await addTknTo(safeAddress, 50, firstErc20Token) + await addTknTo(safeAddress, 50, secondErc20Token) + await store.dispatch(fetchTokensModule.fetchTokens(safeAddress)) + await enableFirstToken(store, safeAddress) + + // THEN + const match: Match = buildMathPropsFrom(safeAddress) + const tokenList = tokenListSelector(store.getState(), { match }) + + testToken(tokenList.get(0), 'FTE', true) + testToken(tokenList.get(1), 'STE', false) + testToken(tokenList.get(2), 'ETH', true) + + const activeTokenList = activeTokensSelector(store.getState(), { match }) + expect(activeTokenList.count()).toBe(2) + + testToken(activeTokenList.get(0), 'FTE', true) + testToken(activeTokenList.get(1), 'ETH', true) + + await store.dispatch(fetchTokensModule.fetchTokens(safeAddress)) + + const fundedTokenList = tokenListSelector(store.getState(), { match }) + expect(fundedTokenList.count()).toBe(3) + + testToken(fundedTokenList.get(0), 'FTE', true, '50') + testToken(fundedTokenList.get(1), 'STE', false, '0') + testToken(fundedTokenList.get(2), 'ETH', true, '0') + }) + + it('localStorage always returns a list', async () => { + const store = aNewStore() + const safeAddress = await aMinedSafe(store) + let tokens: List = getTokens(safeAddress) + expect(tokens).toEqual(List([])) + + await store.dispatch(fetchTokensModule.fetchTokens(safeAddress)) + tokens = getTokens(safeAddress) + expect(tokens.count()).toBe(0) + + await enableFirstToken(store, safeAddress) + tokens = getTokens(safeAddress) + expect(tokens.count()).toBe(1) + + await store.dispatch(fetchTokensModule.fetchTokens(safeAddress)) + tokens = getTokens(safeAddress) + expect(tokens.count()).toBe(1) + }) +}) diff --git a/src/test/utils/tokenMovements.js b/src/test/utils/tokenMovements.js index 4353f65d..1b8ff359 100644 --- a/src/test/utils/tokenMovements.js +++ b/src/test/utils/tokenMovements.js @@ -39,13 +39,14 @@ const createTokenContract = async (web3: any, executor: string) => { return token.new({ from: executor, gas: '5000000' }) } -export const getTokenContract = ensureOnce(createTokenContract) +export const getFirstTokenContract = ensureOnce(createTokenContract) +export const getSecondTokenContract = ensureOnce(createTokenContract) -export const addTknTo = async (safe: string, value: number) => { +export const addTknTo = async (safe: string, value: number, tokenContract?: any) => { const web3 = getWeb3() const accounts = await promisify(cb => getWeb3().eth.getAccounts(cb)) - const myToken = await getTokenContract(web3, accounts[0]) + const myToken = tokenContract || await getFirstTokenContract(web3, accounts[0]) const nativeValue = await toNative(value, 18) await myToken.transfer(safe, nativeValue.valueOf(), { from: accounts[0], gas: '5000000' }) diff --git a/src/test/utils/transactions/moveTokens.helper.js b/src/test/utils/transactions/moveTokens.helper.js index e783a536..41bfa12e 100644 --- a/src/test/utils/transactions/moveTokens.helper.js +++ b/src/test/utils/transactions/moveTokens.helper.js @@ -2,12 +2,12 @@ import { Map } from 'immutable' import TestUtils from 'react-dom/test-utils' import { sleep } from '~/utils/timer' -import * as fetchBalancesAction from '~/routes/safe/store/actions/fetchBalances' +import * as fetchTokensAction from '~/routes/tokens/store/actions/fetchTokens' import { checkMinedTx, checkPendingTx, EXPAND_BALANCE_INDEX } from '~/test/builder/safe.dom.utils' -import { makeBalance, type Balance } from '~/routes/safe/store/model/balance' -import addBalances from '~/routes/safe/store/actions/addBalances' import { whenExecuted } from '~/test/utils/logTransactions' import SendToken from '~/routes/safe/component/SendToken' +import { makeToken, type Token } from '~/routes/tokens/store/model/token' +import addTokens from '~/routes/tokens/store/actions/addTokens' export const sendMoveTokensForm = async ( SafeDom: React$Component, @@ -44,9 +44,9 @@ export const sendMoveTokensForm = async ( } export const dispatchTknBalance = async (store: Store, tokenAddress: string, address: string) => { - const fetchBalancesMock = jest.spyOn(fetchBalancesAction, 'fetchBalances') - const funds = await fetchBalancesAction.calculateBalanceOf(tokenAddress, address, 18) - const balances: Map = Map().set('TKN', makeBalance({ + const fetchBalancesMock = jest.spyOn(fetchTokensAction, 'fetchTokens') + const funds = await fetchTokensAction.calculateBalanceOf(tokenAddress, address, 18) + const balances: Map = Map().set('TKN', makeToken({ address: tokenAddress, name: 'Token', symbol: 'TKN', @@ -54,8 +54,8 @@ export const dispatchTknBalance = async (store: Store, tokenAddress: string, add logoUrl: 'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true', funds, })) - fetchBalancesMock.mockImplementation(() => store.dispatch(addBalances(address, balances))) - await store.dispatch(fetchBalancesAction.fetchBalances(address)) + fetchBalancesMock.mockImplementation(() => store.dispatch(addTokens(address, balances))) + await store.dispatch(fetchTokensAction.fetchTokens(address)) fetchBalancesMock.mockRestore() } diff --git a/src/utils/fetch.js b/src/utils/fetch.js new file mode 100644 index 00000000..2777fdd4 --- /dev/null +++ b/src/utils/fetch.js @@ -0,0 +1,19 @@ +// @flow + +export const enhancedFetch = async (url: string, errMsg: string) => { + const header = new Headers({ + 'Access-Control-Allow-Origin': '*', + }) + + const sentData = { + mode: 'cors', + header, + } + + const response = await fetch(url, sentData) + if (!response.ok) { + throw new Error(errMsg) + } + + return response.json() +} diff --git a/src/utils/localStorage.js b/src/utils/localStorage/index.js similarity index 97% rename from src/utils/localStorage.js rename to src/utils/localStorage/index.js index 474e3dbb..f908a03a 100644 --- a/src/utils/localStorage.js +++ b/src/utils/localStorage/index.js @@ -5,6 +5,7 @@ import { type Owner } from '~/routes/safe/store/model/owner' export const SAFES_KEY = 'SAFES' export const TX_KEY = 'TX' export const OWNERS_KEY = 'OWNERS' +export const TOKENS_KEY = 'TOKENS' export const load = (key: string) => { try { diff --git a/src/utils/localStorage/tokens.js b/src/utils/localStorage/tokens.js new file mode 100644 index 00000000..a29fd3fb --- /dev/null +++ b/src/utils/localStorage/tokens.js @@ -0,0 +1,28 @@ +// @flow +import { List } from 'immutable' +import { load, TOKENS_KEY } from '~/utils/localStorage' + +const getTokensKey = (safeAddress: string) => `${TOKENS_KEY}-${safeAddress}` + +export const setTokens = (safeAddress: string, tokens: List) => { + try { + const serializedState = JSON.stringify(tokens) + const key = getTokensKey(safeAddress) + localStorage.setItem(key, serializedState) + } catch (err) { + // eslint-disable-next-line + console.log('Error storing tokens in localstorage') + } +} + +export const getTokens = (safeAddress: string): List => { + const key = getTokensKey(safeAddress) + const data = load(key) + + return data ? List(data) : List() +} + +export const storedTokensBefore = (safeAddress: string) => { + const key = getTokensKey(safeAddress) + return localStorage.getItem(key) === null +} diff --git a/src/utils/tokens.js b/src/utils/tokens.js new file mode 100644 index 00000000..92930af0 --- /dev/null +++ b/src/utils/tokens.js @@ -0,0 +1,39 @@ +// @flow +import { List } from 'immutable' +import logo from '~/assets/icons/icon_etherTokens.svg' +import { getBalanceInEtherOf } from '~/wallets/getWeb3' +import { makeToken, type Token } from '~/routes/tokens/store/model/token' + +export const isEther = (symbol: string) => symbol === 'ETH' + +export const getSafeEthToken = async (safeAddress: string) => { + const balance = await getBalanceInEtherOf(safeAddress) + + const ethBalance = makeToken({ + address: '0', + name: 'Ether', + symbol: 'ETH', + decimals: 18, + logoUrl: logo, + funds: balance, + }) + + return ethBalance +} + +export const calculateActiveErc20TokensFrom = (tokens: List) => { + const addresses = List().withMutations(list => + tokens.forEach((token: Token) => { + if (isEther(token.get('symbol'))) { + return + } + + if (!token.get('status')) { + return + } + + list.push(token.address) + })) + + return addresses +} diff --git a/src/wallets/ethTransactions.js b/src/wallets/ethTransactions.js index 9805f648..430dbdb5 100644 --- a/src/wallets/ethTransactions.js +++ b/src/wallets/ethTransactions.js @@ -2,6 +2,7 @@ import { BigNumber } from 'bignumber.js' import { getWeb3 } from '~/wallets/getWeb3' import { promisify } from '~/utils/promisify' +import { enhancedFetch } from '~/utils/fetch' // const MAINNET_NETWORK = 1 export const EMPTY_DATA = '0x' @@ -40,21 +41,9 @@ export const calculateGasPrice = async () => { return '20000000000' } - const header = new Headers({ - 'Access-Control-Allow-Origin': '*', - }) - - const sentData = { - mode: 'cors', - header, - } - - const response = await fetch('https://ethgasstation.info/json/ethgasAPI.json', sentData) - if (!response.ok) { - throw new Error('Error querying gast station') - } - - const json = await response.json() + const url = 'https://ethgasstation.info/json/ethgasAPI.json' + const errMsg = 'Error querying gas station' + const json = await enhancedFetch(url, errMsg) return new BigNumber(json.average).multipliedBy(1e8).toString() }