Merge pull request #51 from gnosis/feature/WA-232-disable-tokens
WA-232 - Feature: Enable & disable default safe's tokens
This commit is contained in:
commit
d2a99f858d
|
@ -4,13 +4,18 @@ import Loadable from 'react-loadable'
|
||||||
import { Switch, Redirect, Route } from 'react-router-dom'
|
import { Switch, Redirect, Route } from 'react-router-dom'
|
||||||
import Loader from '~/components/Loader'
|
import Loader from '~/components/Loader'
|
||||||
import Welcome from './welcome/container'
|
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({
|
const Safe = Loadable({
|
||||||
loader: () => import('./safe/container'),
|
loader: () => import('./safe/container'),
|
||||||
loading: Loader,
|
loading: Loader,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const Settings = Loadable({
|
||||||
|
loader: () => import('./tokens/container'),
|
||||||
|
loading: Loader,
|
||||||
|
})
|
||||||
|
|
||||||
const SafeList = Loadable({
|
const SafeList = Loadable({
|
||||||
loader: () => import('./safeList/container'),
|
loader: () => import('./safeList/container'),
|
||||||
loading: Loader,
|
loading: Loader,
|
||||||
|
@ -22,6 +27,9 @@ const Open = Loadable({
|
||||||
})
|
})
|
||||||
|
|
||||||
const SAFE_ADDRESS = `${SAFELIST_ADDRESS}/:${SAFE_PARAM_ADDRESS}`
|
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 = () => (
|
const Routes = () => (
|
||||||
<Switch>
|
<Switch>
|
||||||
|
@ -30,6 +38,7 @@ const Routes = () => (
|
||||||
<Route exact path={OPEN_ADDRESS} component={Open} />
|
<Route exact path={OPEN_ADDRESS} component={Open} />
|
||||||
<Route exact path={SAFELIST_ADDRESS} component={SafeList} />
|
<Route exact path={SAFELIST_ADDRESS} component={SafeList} />
|
||||||
<Route exact path={SAFE_ADDRESS} component={Safe} />
|
<Route exact path={SAFE_ADDRESS} component={Safe} />
|
||||||
|
<Route exact path={SAFE_SETTINGS} component={Settings} />
|
||||||
</Switch>
|
</Switch>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -3,3 +3,4 @@ export const SAFE_PARAM_ADDRESS = 'address'
|
||||||
export const SAFELIST_ADDRESS = '/safes'
|
export const SAFELIST_ADDRESS = '/safes'
|
||||||
export const OPEN_ADDRESS = '/open'
|
export const OPEN_ADDRESS = '/open'
|
||||||
export const WELCOME_ADDRESS = '/welcome'
|
export const WELCOME_ADDRESS = '/welcome'
|
||||||
|
export const SETTINS_ADDRESS = '/settings'
|
||||||
|
|
|
@ -7,11 +7,11 @@ import GnoSafe from './Safe'
|
||||||
type Props = SelectorProps
|
type Props = SelectorProps
|
||||||
|
|
||||||
const Layout = ({
|
const Layout = ({
|
||||||
safe, balances, provider, userAddress,
|
safe, activeTokens, provider, userAddress,
|
||||||
}: Props) => (
|
}: Props) => (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{ safe
|
{ safe
|
||||||
? <GnoSafe safe={safe} balances={balances} userAddress={userAddress} />
|
? <GnoSafe safe={safe} tokens={activeTokens} userAddress={userAddress} />
|
||||||
: <NoSafe provider={provider} text="Not found safe" />
|
: <NoSafe provider={provider} text="Not found safe" />
|
||||||
}
|
}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import * as React from 'react'
|
||||||
import { Map } from 'immutable'
|
import { Map } from 'immutable'
|
||||||
import styles from '~/components/layout/PageFrame/index.scss'
|
import styles from '~/components/layout/PageFrame/index.scss'
|
||||||
import { SafeFactory } from '~/routes/safe/store/test/builder/safe.builder'
|
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'
|
import Component from './Layout'
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ const FrameDecorator = story => (
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
const ethBalance = makeBalance({
|
const ethBalance = makeToken({
|
||||||
address: '0',
|
address: '0',
|
||||||
name: 'Ether',
|
name: 'Ether',
|
||||||
symbol: 'ETH',
|
symbol: 'ETH',
|
||||||
|
@ -30,7 +30,7 @@ storiesOf('Routes /safe:address', module)
|
||||||
userAddress="foo"
|
userAddress="foo"
|
||||||
safe={undefined}
|
safe={undefined}
|
||||||
provider="METAMASK"
|
provider="METAMASK"
|
||||||
balances={Map()}
|
activeTokens={Map()}
|
||||||
fetchBalance={() => {}}
|
fetchBalance={() => {}}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
@ -39,7 +39,7 @@ storiesOf('Routes /safe:address', module)
|
||||||
userAddress="foo"
|
userAddress="foo"
|
||||||
safe={undefined}
|
safe={undefined}
|
||||||
provider=""
|
provider=""
|
||||||
balances={Map()}
|
activeTokens={Map()}
|
||||||
fetchBalance={() => {}}
|
fetchBalance={() => {}}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
@ -51,7 +51,7 @@ storiesOf('Routes /safe:address', module)
|
||||||
userAddress="foo"
|
userAddress="foo"
|
||||||
safe={safe}
|
safe={safe}
|
||||||
provider="METAMASK"
|
provider="METAMASK"
|
||||||
balances={Map().set('ETH', ethBalance)}
|
activeTokens={Map().set('ETH', ethBalance)}
|
||||||
fetchBalance={() => {}}
|
fetchBalance={() => {}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -64,7 +64,7 @@ storiesOf('Routes /safe:address', module)
|
||||||
userAddress="foo"
|
userAddress="foo"
|
||||||
safe={safe}
|
safe={safe}
|
||||||
provider="METAMASK"
|
provider="METAMASK"
|
||||||
balances={Map().set('ETH', ethBalance)}
|
activeTokens={Map().set('ETH', ethBalance)}
|
||||||
fetchBalance={() => {}}
|
fetchBalance={() => {}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
import Link from '~/components/layout/Link'
|
||||||
import AccountBalance from '@material-ui/icons/AccountBalance'
|
import AccountBalance from '@material-ui/icons/AccountBalance'
|
||||||
|
import Settings from '@material-ui/icons/Settings'
|
||||||
import Avatar from '@material-ui/core/Avatar'
|
import Avatar from '@material-ui/core/Avatar'
|
||||||
import Collapse from '@material-ui/core/Collapse'
|
import Collapse from '@material-ui/core/Collapse'
|
||||||
import IconButton from '@material-ui/core/IconButton'
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
|
@ -17,11 +19,13 @@ import { Map } from 'immutable'
|
||||||
import Button from '~/components/layout/Button'
|
import Button from '~/components/layout/Button'
|
||||||
import openHoc, { type Open } from '~/components/hoc/OpenHoc'
|
import openHoc, { type Open } from '~/components/hoc/OpenHoc'
|
||||||
import { type WithStyles } from '~/theme/mui'
|
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 & {
|
type Props = Open & WithStyles & {
|
||||||
balances: Map<string, Balance>,
|
safeAddress: string,
|
||||||
onMoveFunds: (balance: Balance) => void,
|
tokens: Map<string, Token>,
|
||||||
|
onMoveFunds: (token: Token) => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
|
@ -33,9 +37,10 @@ const styles = {
|
||||||
export const MOVE_FUNDS_BUTTON_TEXT = 'Move'
|
export const MOVE_FUNDS_BUTTON_TEXT = 'Move'
|
||||||
|
|
||||||
const BalanceComponent = openHoc(({
|
const BalanceComponent = openHoc(({
|
||||||
open, toggle, balances, classes, onMoveFunds,
|
open, toggle, tokens, classes, onMoveFunds, safeAddress,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const hasBalances = balances.count() > 0
|
const hasBalances = tokens.count() > 0
|
||||||
|
const settingsUrl = settingsUrlFrom(safeAddress)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
@ -44,6 +49,11 @@ const BalanceComponent = openHoc(({
|
||||||
<AccountBalance />
|
<AccountBalance />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<ListItemText primary="Balance" secondary="List of different token balances" />
|
<ListItemText primary="Balance" secondary="List of different token balances" />
|
||||||
|
<ListItemIcon>
|
||||||
|
<IconButton to={settingsUrl} disabled={!hasBalances} component={Link} className={classes.button}>
|
||||||
|
<Settings />
|
||||||
|
</IconButton>
|
||||||
|
</ListItemIcon>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
{open
|
{open
|
||||||
? <IconButton disableRipple><ExpandLess /></IconButton>
|
? <IconButton disableRipple><ExpandLess /></IconButton>
|
||||||
|
@ -53,18 +63,18 @@ const BalanceComponent = openHoc(({
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Collapse in={open} timeout="auto">
|
<Collapse in={open} timeout="auto">
|
||||||
<List component="div" disablePadding>
|
<List component="div" disablePadding>
|
||||||
{balances.valueSeq().map((balance: Balance) => {
|
{tokens.valueSeq().map((token: Token) => {
|
||||||
const symbol = balance.get('symbol')
|
const symbol = token.get('symbol')
|
||||||
const name = balance.get('name')
|
const name = token.get('name')
|
||||||
const disabled = Number(balance.get('funds')) === 0
|
const disabled = Number(token.get('funds')) === 0
|
||||||
const onMoveFundsClick = () => onMoveFunds(balance)
|
const onMoveFundsClick = () => onMoveFunds(token)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem key={symbol} className={classNames(classes.nested, symbol)}>
|
<ListItem key={symbol} className={classNames(classes.nested, symbol)}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Img src={balance.get('logoUrl')} height={30} alt={name} />
|
<Img src={token.get('logoUrl')} height={30} alt={name} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary={name} secondary={`${balance.get('funds')} ${symbol}`} />
|
<ListItemText primary={name} secondary={`${token.get('funds')} ${symbol}`} />
|
||||||
<Button variant="raised" color="primary" onClick={onMoveFundsClick} disabled={disabled}>
|
<Button variant="raised" color="primary" onClick={onMoveFundsClick} disabled={disabled}>
|
||||||
{MOVE_FUNDS_BUTTON_TEXT}
|
{MOVE_FUNDS_BUTTON_TEXT}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -9,7 +9,7 @@ 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/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 Withdraw from '~/routes/safe/component/Withdraw'
|
||||||
import Transactions from '~/routes/safe/component/Transactions'
|
import Transactions from '~/routes/safe/component/Transactions'
|
||||||
|
@ -30,7 +30,7 @@ const safeIcon = require('./assets/gnosis_safe.svg')
|
||||||
|
|
||||||
type SafeProps = {
|
type SafeProps = {
|
||||||
safe: Safe,
|
safe: Safe,
|
||||||
balances: Map<string, Balance>,
|
tokens: Map<string, Token>,
|
||||||
userAddress: string,
|
userAddress: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,13 +42,13 @@ const listStyle = {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
}
|
}
|
||||||
|
|
||||||
const getEthBalanceFrom = (balances: Map<string, Balance>) => {
|
const getEthBalanceFrom = (tokens: List<Token>) => {
|
||||||
const ethBalance = balances.get('ETH')
|
const ethToken = tokens.filter(token => token.get('symbol') === 'ETH')
|
||||||
if (!ethBalance) {
|
if (ethToken.count() === 0) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return Number(ethBalance.get('funds'))
|
return Number(ethToken.get(0).get('funds'))
|
||||||
}
|
}
|
||||||
|
|
||||||
class GnoSafe extends React.PureComponent<SafeProps, State> {
|
class GnoSafe extends React.PureComponent<SafeProps, State> {
|
||||||
|
@ -93,13 +93,13 @@ class GnoSafe extends React.PureComponent<SafeProps, State> {
|
||||||
this.setState({ component: <RemoveOwner safeAddress={safe.get('address')} threshold={safe.get('threshold')} safe={safe} name={name} userToRemove={address} /> })
|
this.setState({ component: <RemoveOwner safeAddress={safe.get('address')} threshold={safe.get('threshold')} safe={safe} name={name} userToRemove={address} /> })
|
||||||
}
|
}
|
||||||
|
|
||||||
onMoveTokens = (ercToken: Balance) => {
|
onMoveTokens = (ercToken: Token) => {
|
||||||
const { safe } = this.props
|
const { safe } = this.props
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
component: <SendToken
|
component: <SendToken
|
||||||
safe={safe}
|
safe={safe}
|
||||||
balance={ercToken}
|
token={ercToken}
|
||||||
key={ercToken.get('symbol')}
|
key={ercToken.get('symbol')}
|
||||||
onReset={this.onListTransactions}
|
onReset={this.onListTransactions}
|
||||||
/>,
|
/>,
|
||||||
|
@ -107,15 +107,16 @@ class GnoSafe extends React.PureComponent<SafeProps, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { safe, balances, userAddress } = this.props
|
const { safe, tokens, userAddress } = this.props
|
||||||
const { component } = this.state
|
const { component } = this.state
|
||||||
const ethBalance = getEthBalanceFrom(balances)
|
const ethBalance = getEthBalanceFrom(tokens)
|
||||||
|
const address = safe.get('address')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row grow>
|
<Row grow>
|
||||||
<Col sm={12} top="xs" md={5} margin="xl" overflow>
|
<Col sm={12} top="xs" md={5} margin="xl" overflow>
|
||||||
<List style={listStyle}>
|
<List style={listStyle}>
|
||||||
<BalanceInfo balances={balances} onMoveFunds={this.onMoveTokens} />
|
<BalanceInfo tokens={tokens} onMoveFunds={this.onMoveTokens} safeAddress={address} />
|
||||||
<Owners
|
<Owners
|
||||||
owners={safe.owners}
|
owners={safe.owners}
|
||||||
onAddOwner={this.onAddOwner}
|
onAddOwner={this.onAddOwner}
|
||||||
|
@ -123,7 +124,7 @@ class GnoSafe extends React.PureComponent<SafeProps, State> {
|
||||||
onRemoveOwner={this.onRemoveOwner}
|
onRemoveOwner={this.onRemoveOwner}
|
||||||
/>
|
/>
|
||||||
<Confirmations confirmations={safe.get('threshold')} onEditThreshold={this.onEditThreshold} />
|
<Confirmations confirmations={safe.get('threshold')} onEditThreshold={this.onEditThreshold} />
|
||||||
<Address address={safe.get('address')} />
|
<Address address={address} />
|
||||||
<DailyLimit balance={ethBalance} dailyLimit={safe.get('dailyLimit')} onWithdraw={this.onWithdraw} onEditDailyLimit={this.onEditDailyLimit} />
|
<DailyLimit balance={ethBalance} dailyLimit={safe.get('dailyLimit')} onWithdraw={this.onWithdraw} onEditDailyLimit={this.onEditDailyLimit} />
|
||||||
<MultisigTx onSeeTxs={this.onListTransactions} />
|
<MultisigTx onSeeTxs={this.onListTransactions} />
|
||||||
</List>
|
</List>
|
||||||
|
|
|
@ -5,11 +5,12 @@ 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/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 { createTransaction } from '~/wallets/createTransactions'
|
||||||
import { getStandardTokenContract } from '~/routes/safe/store/actions/fetchBalances'
|
|
||||||
import { EMPTY_DATA } from '~/wallets/ethTransactions'
|
import { EMPTY_DATA } from '~/wallets/ethTransactions'
|
||||||
import { toNative } from '~/wallets/tokens'
|
import { toNative } from '~/wallets/tokens'
|
||||||
|
import { isEther } from '~/utils/tokens'
|
||||||
import actions, { type Actions } from './actions'
|
import actions, { type Actions } from './actions'
|
||||||
import selector, { type SelectorProps } from './selector'
|
import selector, { type SelectorProps } from './selector'
|
||||||
import SendTokenForm, { TKN_DESTINATION_PARAM, TKN_VALUE_PARAM } from './SendTokenForm'
|
import SendTokenForm, { TKN_DESTINATION_PARAM, TKN_VALUE_PARAM } from './SendTokenForm'
|
||||||
|
@ -21,7 +22,7 @@ const getSteps = () => [
|
||||||
|
|
||||||
type Props = SelectorProps & Actions & {
|
type Props = SelectorProps & Actions & {
|
||||||
safe: Safe,
|
safe: Safe,
|
||||||
balance: Balance,
|
token: Token,
|
||||||
onReset: () => void,
|
onReset: () => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,8 +32,6 @@ type State = {
|
||||||
|
|
||||||
export const SEE_TXS_BUTTON_TEXT = 'VISIT TXS'
|
export const SEE_TXS_BUTTON_TEXT = 'VISIT TXS'
|
||||||
|
|
||||||
const isEther = (symbol: string) => symbol === 'ETH'
|
|
||||||
|
|
||||||
const getTransferData = async (tokenAddress: string, to: string, amount: BigNumber) => {
|
const getTransferData = async (tokenAddress: string, to: string, amount: BigNumber) => {
|
||||||
const StandardToken = await getStandardTokenContract()
|
const StandardToken = await getStandardTokenContract()
|
||||||
const myToken = await StandardToken.at(tokenAddress)
|
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)
|
return myToken.contract.transfer.getData(to, amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
const processTokenTransfer = async (safe: Safe, balance: Balance, to: string, amount: number, userAddress: string) => {
|
const processTokenTransfer = async (safe: Safe, token: Token, to: string, amount: number, userAddress: string) => {
|
||||||
const symbol = balance.get('symbol')
|
const symbol = token.get('symbol')
|
||||||
const nonce = Date.now()
|
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 value = isEther(symbol) ? amount : 0
|
||||||
const tokenAddress = balance.get('address')
|
const tokenAddress = token.get('address')
|
||||||
const destination = isEther(symbol) ? to : tokenAddress
|
const destination = isEther(symbol) ? to : tokenAddress
|
||||||
const data = isEther(symbol)
|
const data = isEther(symbol)
|
||||||
? EMPTY_DATA
|
? 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)
|
return createTransaction(safe, name, destination, value, nonce, userAddress, data)
|
||||||
}
|
}
|
||||||
|
@ -61,12 +60,12 @@ class SendToken extends React.Component<Props, State> {
|
||||||
|
|
||||||
onTransaction = async (values: Object) => {
|
onTransaction = async (values: Object) => {
|
||||||
try {
|
try {
|
||||||
const { safe, balance, userAddress } = this.props
|
const { safe, token, userAddress } = this.props
|
||||||
|
|
||||||
const amount = values[TKN_VALUE_PARAM]
|
const amount = values[TKN_VALUE_PARAM]
|
||||||
const destination = values[TKN_DESTINATION_PARAM]
|
const destination = values[TKN_DESTINATION_PARAM]
|
||||||
|
|
||||||
await processTokenTransfer(safe, balance, destination, amount, userAddress)
|
await processTokenTransfer(safe, token, destination, amount, userAddress)
|
||||||
await sleep(1500)
|
await sleep(1500)
|
||||||
this.props.fetchTransactions()
|
this.props.fetchTransactions()
|
||||||
this.setState({ done: true })
|
this.setState({ done: true })
|
||||||
|
@ -84,10 +83,10 @@ class SendToken extends React.Component<Props, State> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { done } = this.state
|
const { done } = this.state
|
||||||
const { balance } = this.props
|
const { token } = this.props
|
||||||
const steps = getSteps()
|
const steps = getSteps()
|
||||||
const finishedButton = <Stepper.FinishButton title={SEE_TXS_BUTTON_TEXT} />
|
const finishedButton = <Stepper.FinishButton title={SEE_TXS_BUTTON_TEXT} />
|
||||||
const symbol = balance.get('symbol')
|
const symbol = token.get('symbol')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
@ -98,7 +97,7 @@ class SendToken extends React.Component<Props, State> {
|
||||||
steps={steps}
|
steps={steps}
|
||||||
onReset={this.onReset}
|
onReset={this.onReset}
|
||||||
>
|
>
|
||||||
<Stepper.Page funds={balance.get('funds')} symbol={symbol}>
|
<Stepper.Page funds={token.get('funds')} symbol={symbol}>
|
||||||
{ SendTokenForm }
|
{ SendTokenForm }
|
||||||
</Stepper.Page>
|
</Stepper.Page>
|
||||||
<Stepper.Page symbol={symbol}>
|
<Stepper.Page symbol={symbol}>
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
// @flow
|
// @flow
|
||||||
import fetchSafe from '~/routes/safe/store/actions/fetchSafe'
|
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 = {
|
export type Actions = {
|
||||||
fetchSafe: typeof fetchSafe,
|
fetchSafe: typeof fetchSafe,
|
||||||
fetchBalances: typeof fetchBalances,
|
fetchTokens: typeof fetchTokens,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
fetchSafe,
|
fetchSafe,
|
||||||
fetchBalances,
|
fetchTokens,
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,12 +16,14 @@ const TIMEOUT = process.env.NODE_ENV === 'test' ? 1500 : 15000
|
||||||
class SafeView extends React.PureComponent<Props> {
|
class SafeView extends React.PureComponent<Props> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.intervalId = setInterval(() => {
|
this.intervalId = setInterval(() => {
|
||||||
const { safe, fetchBalances, fetchSafe } = this.props
|
const {
|
||||||
|
safe, fetchTokens, fetchSafe,
|
||||||
|
} = this.props
|
||||||
if (!safe) {
|
if (!safe) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const safeAddress = safe.get('address')
|
const safeAddress = safe.get('address')
|
||||||
fetchBalances(safeAddress)
|
fetchTokens(safeAddress)
|
||||||
fetchSafe(safe)
|
fetchSafe(safe)
|
||||||
}, TIMEOUT)
|
}, TIMEOUT)
|
||||||
}
|
}
|
||||||
|
@ -33,7 +35,7 @@ class SafeView extends React.PureComponent<Props> {
|
||||||
|
|
||||||
if (this.props.safe) {
|
if (this.props.safe) {
|
||||||
const safeAddress = this.props.safe.get('address')
|
const safeAddress = this.props.safe.get('address')
|
||||||
this.props.fetchBalances(safeAddress)
|
this.props.fetchTokens(safeAddress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,13 +47,13 @@ class SafeView extends React.PureComponent<Props> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
safe, provider, balances, granted, userAddress,
|
safe, provider, activeTokens, granted, userAddress,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
{ granted
|
{ granted
|
||||||
? <Layout balances={balances} provider={provider} safe={safe} userAddress={userAddress} />
|
? <Layout activeTokens={activeTokens} provider={provider} safe={safe} userAddress={userAddress} />
|
||||||
: <NoRights />
|
: <NoRights />
|
||||||
}
|
}
|
||||||
</Page>
|
</Page>
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { List, Map } from 'immutable'
|
import { List, Map } from 'immutable'
|
||||||
import { createSelector, createStructuredSelector, type Selector } from 'reselect'
|
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 { providerNameSelector, userAccountSelector } from '~/wallets/store/selectors/index'
|
||||||
import { type Safe } from '~/routes/safe/store/model/safe'
|
import { type Safe } from '~/routes/safe/store/model/safe'
|
||||||
import { type Owner } from '~/routes/safe/store/model/owner'
|
import { type Owner } from '~/routes/safe/store/model/owner'
|
||||||
import { type GlobalState } from '~/store/index'
|
import { type GlobalState } from '~/store/index'
|
||||||
import { sameAddress } from '~/wallets/ethAddresses'
|
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 = {
|
export type SelectorProps = {
|
||||||
safe: SafeSelectorProps,
|
safe: SafeSelectorProps,
|
||||||
provider: string,
|
provider: string,
|
||||||
balances: Map<string, Balance>,
|
activeTokens: Map<string, Token>,
|
||||||
userAddress: string,
|
userAddress: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +41,7 @@ export const grantedSelector: Selector<GlobalState, RouterProps, boolean> = crea
|
||||||
export default createStructuredSelector({
|
export default createStructuredSelector({
|
||||||
safe: safeSelector,
|
safe: safeSelector,
|
||||||
provider: providerNameSelector,
|
provider: providerNameSelector,
|
||||||
balances: balanceSelector,
|
activeTokens: activeTokensSelector,
|
||||||
granted: grantedSelector,
|
granted: grantedSelector,
|
||||||
userAddress: userAccountSelector,
|
userAddress: userAccountSelector,
|
||||||
})
|
})
|
||||||
|
|
|
@ -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<string, Balance>,
|
|
||||||
}
|
|
||||||
|
|
||||||
const addBalances = createAction(
|
|
||||||
ADD_BALANCES,
|
|
||||||
(safeAddress: string, balances: Map<string, Balance>): BalanceProps => ({
|
|
||||||
safeAddress,
|
|
||||||
balances,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
export default addBalances
|
|
|
@ -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<GlobalState>) => {
|
|
||||||
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<string, Balance> = Map().withMutations((map) => {
|
|
||||||
balancesRecords.forEach(record => map.set(record.get('symbol'), record))
|
|
||||||
map.set('ETH', ethBalance)
|
|
||||||
})
|
|
||||||
|
|
||||||
return dispatch(addBalances(safeAddress, balances))
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -37,7 +37,14 @@ export const buildSafe = async (storedSafe: Object) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (safe: Safe) => async (dispatch: ReduxDispatch<GlobalState>) => {
|
export default (safe: Safe) => async (dispatch: ReduxDispatch<GlobalState>) => {
|
||||||
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,15 +11,23 @@ const buildSafesFrom = async (loadedSafes: Object): Promise<Map<string, Safe>> =
|
||||||
const safes = Map()
|
const safes = Map()
|
||||||
|
|
||||||
const keys = Object.keys(loadedSafes)
|
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) => {
|
return safes.withMutations(async (map) => {
|
||||||
safeRecords.forEach((safe: Safe) => map.set(safe.get('address'), safe))
|
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<GlobalState>) => {
|
export default () => async (dispatch: ReduxDispatch<GlobalState>) => {
|
||||||
const storedSafes = load(SAFES_KEY)
|
const storedSafes = load(SAFES_KEY)
|
||||||
|
|
||||||
const safes = storedSafes ? await buildSafesFrom(storedSafes) : Map()
|
const safes = storedSafes ? await buildSafesFrom(storedSafes) : Map()
|
||||||
|
|
||||||
return dispatch(updateSafes(safes))
|
return dispatch(updateSafes(safes))
|
||||||
|
|
|
@ -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<string, Map<string, Balance>>
|
|
||||||
|
|
||||||
export default handleActions({
|
|
||||||
[ADD_BALANCES]: (state: State, action: ActionType<typeof addBalances>): State =>
|
|
||||||
state.update(action.payload.safeAddress, (prevSafe: Map<string, Balance>) => {
|
|
||||||
if (!prevSafe) {
|
|
||||||
return action.payload.balances
|
|
||||||
}
|
|
||||||
|
|
||||||
return prevSafe.equals(action.payload.balances) ? prevSafe : action.payload.balances
|
|
||||||
}),
|
|
||||||
}, Map())
|
|
|
@ -6,11 +6,9 @@ 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/model/safe'
|
||||||
import { safesMapSelector } from '~/routes/safeList/store/selectors'
|
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 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/model/transaction'
|
||||||
import { type Confirmation } from '~/routes/safe/store/model/confirmation'
|
import { type Confirmation } from '~/routes/safe/store/model/confirmation'
|
||||||
import { type Balance } from '~/routes/safe/store/model/balance'
|
|
||||||
|
|
||||||
export type RouterProps = {
|
export type RouterProps = {
|
||||||
match: Match,
|
match: Match,
|
||||||
|
@ -26,14 +24,12 @@ type TransactionProps = {
|
||||||
|
|
||||||
const safePropAddressSelector = (state: GlobalState, props: SafeProps) => props.safeAddress
|
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 transactionsSelector = (state: GlobalState): TransactionsState => state[TRANSACTIONS_REDUCER_ID]
|
||||||
|
|
||||||
const oneTransactionSelector = (state: GlobalState, props: TransactionProps) => props.transaction
|
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<GlobalState, SafeProps, List<Transaction>> = createSelector(
|
export const safeTransactionsSelector: Selector<GlobalState, SafeProps, List<Transaction>> = createSelector(
|
||||||
transactionsSelector,
|
transactionsSelector,
|
||||||
safePropAddressSelector,
|
safePropAddressSelector,
|
||||||
|
@ -82,18 +78,6 @@ export const safeSelector: Selector<GlobalState, RouterProps, SafeSelectorProps>
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
export const balanceSelector: Selector<GlobalState, RouterProps, Map<string, Balance>> = createSelector(
|
|
||||||
balancesSelector,
|
|
||||||
safeParamAddressSelector,
|
|
||||||
(balances: Map<string, Map<string, Balance>>, address: string) => {
|
|
||||||
if (!address) {
|
|
||||||
return Map()
|
|
||||||
}
|
|
||||||
|
|
||||||
return balances.get(address) || Map()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
export default createStructuredSelector({
|
export default createStructuredSelector({
|
||||||
safe: safeSelector,
|
safe: safeSelector,
|
||||||
})
|
})
|
||||||
|
|
|
@ -29,7 +29,7 @@ const grantedSelectorTests = () => {
|
||||||
const reduxStore = {
|
const reduxStore = {
|
||||||
safes: Map(),
|
safes: Map(),
|
||||||
providers: makeProvider(),
|
providers: makeProvider(),
|
||||||
balances: Map(),
|
tokens: Map(),
|
||||||
transactions: Map(),
|
transactions: Map(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ const grantedSelectorTests = () => {
|
||||||
const reduxStore = {
|
const reduxStore = {
|
||||||
safes: Map(),
|
safes: Map(),
|
||||||
providers: makeProvider(),
|
providers: makeProvider(),
|
||||||
balances: Map(),
|
tokens: Map(),
|
||||||
transactions: Map(),
|
transactions: Map(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,7 +82,7 @@ const grantedSelectorTests = () => {
|
||||||
const reduxStore = {
|
const reduxStore = {
|
||||||
safes: Map(),
|
safes: Map(),
|
||||||
providers: makeProvider(),
|
providers: makeProvider(),
|
||||||
balances: Map(),
|
tokens: Map(),
|
||||||
transactions: Map(),
|
transactions: Map(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ const grantedSelectorTests = () => {
|
||||||
const reduxStore = {
|
const reduxStore = {
|
||||||
[SAFE_REDUCER_ID]: map,
|
[SAFE_REDUCER_ID]: map,
|
||||||
providers: makeProvider(provider),
|
providers: makeProvider(provider),
|
||||||
balances: undefined,
|
tokens: undefined,
|
||||||
transactions: undefined,
|
transactions: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ const grantedSelectorTests = () => {
|
||||||
const reduxStore = {
|
const reduxStore = {
|
||||||
[SAFE_REDUCER_ID]: map,
|
[SAFE_REDUCER_ID]: map,
|
||||||
providers: makeProvider(provider),
|
providers: makeProvider(provider),
|
||||||
balances: undefined,
|
tokens: undefined,
|
||||||
transactions: undefined,
|
transactions: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ const grantedSelectorTests = () => {
|
||||||
const reduxStore = {
|
const reduxStore = {
|
||||||
[SAFE_REDUCER_ID]: map,
|
[SAFE_REDUCER_ID]: map,
|
||||||
providers: makeProvider(provider),
|
providers: makeProvider(provider),
|
||||||
balances: undefined,
|
tokens: undefined,
|
||||||
transactions: undefined,
|
transactions: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ const safeSelectorTests = () => {
|
||||||
const reduxStore = {
|
const reduxStore = {
|
||||||
[SAFE_REDUCER_ID]: Map(),
|
[SAFE_REDUCER_ID]: Map(),
|
||||||
providers: undefined,
|
providers: undefined,
|
||||||
balances: undefined,
|
tokens: undefined,
|
||||||
transactions: undefined,
|
transactions: undefined,
|
||||||
}
|
}
|
||||||
const match: Match = buildMathPropsFrom('fooAddress')
|
const match: Match = buildMathPropsFrom('fooAddress')
|
||||||
|
@ -38,7 +38,7 @@ const safeSelectorTests = () => {
|
||||||
const reduxStore = {
|
const reduxStore = {
|
||||||
[SAFE_REDUCER_ID]: map,
|
[SAFE_REDUCER_ID]: map,
|
||||||
providers: undefined,
|
providers: undefined,
|
||||||
balances: undefined,
|
tokens: undefined,
|
||||||
transactions: undefined,
|
transactions: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ const grantedSelectorTests = () => {
|
||||||
const reduxStore = {
|
const reduxStore = {
|
||||||
[SAFE_REDUCER_ID]: Map(),
|
[SAFE_REDUCER_ID]: Map(),
|
||||||
providers: makeProvider(),
|
providers: makeProvider(),
|
||||||
balances: undefined,
|
tokens: undefined,
|
||||||
transactions: Map(),
|
transactions: Map(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ const grantedSelectorTests = () => {
|
||||||
const reduxStore = {
|
const reduxStore = {
|
||||||
[SAFE_REDUCER_ID]: Map(),
|
[SAFE_REDUCER_ID]: Map(),
|
||||||
providers: makeProvider(),
|
providers: makeProvider(),
|
||||||
balances: undefined,
|
tokens: undefined,
|
||||||
transactions: Map({ fooAddress: List([transaction]) }),
|
transactions: Map({ fooAddress: List([transaction]) }),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,7 +81,7 @@ const grantedSelectorTests = () => {
|
||||||
const reduxStore = {
|
const reduxStore = {
|
||||||
[SAFE_REDUCER_ID]: Map(),
|
[SAFE_REDUCER_ID]: Map(),
|
||||||
providers: makeProvider(),
|
providers: makeProvider(),
|
||||||
balances: undefined,
|
tokens: undefined,
|
||||||
transactions: Map({ fooAddress: List([transaction]) }),
|
transactions: Map({ fooAddress: List([transaction]) }),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,7 +113,7 @@ const grantedSelectorTests = () => {
|
||||||
const reduxStore = {
|
const reduxStore = {
|
||||||
[SAFE_REDUCER_ID]: Map(),
|
[SAFE_REDUCER_ID]: Map(),
|
||||||
providers: makeProvider(),
|
providers: makeProvider(),
|
||||||
balances: undefined,
|
tokens: undefined,
|
||||||
transactions: Map({ fooAddress: List([transaction]) }),
|
transactions: Map({ fooAddress: List([transaction]) }),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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((
|
|
||||||
<Provider store={store}>
|
|
||||||
<ConnectedRouter history={history}>
|
|
||||||
<AppRoutes />
|
|
||||||
</ConnectedRouter>
|
|
||||||
</Provider>
|
|
||||||
))
|
|
||||||
})
|
|
||||||
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -21,7 +21,7 @@ const safesListSelectorTests = () => {
|
||||||
const reduxStore = {
|
const reduxStore = {
|
||||||
[PROVIDER_REDUCER_ID]: walletRecord,
|
[PROVIDER_REDUCER_ID]: walletRecord,
|
||||||
[SAFE_REDUCER_ID]: Map(),
|
[SAFE_REDUCER_ID]: Map(),
|
||||||
balances: undefined,
|
tokens: undefined,
|
||||||
transactions: undefined,
|
transactions: undefined,
|
||||||
}
|
}
|
||||||
const emptyList = List([])
|
const emptyList = List([])
|
||||||
|
@ -42,7 +42,7 @@ const safesListSelectorTests = () => {
|
||||||
const reduxStore = {
|
const reduxStore = {
|
||||||
[PROVIDER_REDUCER_ID]: walletRecord,
|
[PROVIDER_REDUCER_ID]: walletRecord,
|
||||||
[SAFE_REDUCER_ID]: map,
|
[SAFE_REDUCER_ID]: map,
|
||||||
balances: undefined,
|
tokens: undefined,
|
||||||
transactions: undefined,
|
transactions: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ const safesListSelectorTests = () => {
|
||||||
const reduxStore = {
|
const reduxStore = {
|
||||||
[PROVIDER_REDUCER_ID]: walletRecord,
|
[PROVIDER_REDUCER_ID]: walletRecord,
|
||||||
[SAFE_REDUCER_ID]: map,
|
[SAFE_REDUCER_ID]: map,
|
||||||
balances: undefined,
|
tokens: undefined,
|
||||||
transactions: undefined,
|
transactions: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ const safesListSelectorTests = () => {
|
||||||
const reduxStore = {
|
const reduxStore = {
|
||||||
[SAFE_REDUCER_ID]: map,
|
[SAFE_REDUCER_ID]: map,
|
||||||
[PROVIDER_REDUCER_ID]: walletRecord,
|
[PROVIDER_REDUCER_ID]: walletRecord,
|
||||||
balances: undefined,
|
tokens: undefined,
|
||||||
transactions: undefined,
|
transactions: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,7 +105,7 @@ const safesListSelectorTests = () => {
|
||||||
const reduxStore = {
|
const reduxStore = {
|
||||||
[SAFE_REDUCER_ID]: map,
|
[SAFE_REDUCER_ID]: map,
|
||||||
[PROVIDER_REDUCER_ID]: walletRecord,
|
[PROVIDER_REDUCER_ID]: walletRecord,
|
||||||
balances: undefined,
|
tokens: undefined,
|
||||||
transactions: undefined,
|
transactions: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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<TokenProps, State> {
|
||||||
|
state = {
|
||||||
|
component: undefined,
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
onAddToken = () => {
|
||||||
|
const { addresses } = this.props
|
||||||
|
this.setState({ component: <AddToken/> })
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemoveToken = () => {
|
||||||
|
this.setState({ component: <RemoveToken /> })
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
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 (
|
||||||
|
<Row grow>
|
||||||
|
<Col sm={12} top="xs" md={5} margin="xl" overflow>
|
||||||
|
<MuiList style={listStyle}>
|
||||||
|
{tokens.map((token: Token) => (<TokenComponent
|
||||||
|
key={token.get('symbol')}
|
||||||
|
token={token}
|
||||||
|
onDisableToken={this.onDisableToken}
|
||||||
|
onEnableToken={this.onEnableToken}
|
||||||
|
/>))}
|
||||||
|
</MuiList>
|
||||||
|
</Col>
|
||||||
|
<Col sm={12} center="xs" md={7} margin="xl" layout="column">
|
||||||
|
<Block margin="xl">
|
||||||
|
<Paragraph size="lg" noMargin align="right">
|
||||||
|
<IconButton to={`${SAFELIST_ADDRESS}/${safeAddress}`} component={Link}>
|
||||||
|
<AccountBalanceWallet />
|
||||||
|
</IconButton>
|
||||||
|
<Bold>{name}</Bold>
|
||||||
|
</Paragraph>
|
||||||
|
</Block>
|
||||||
|
<Row grow>
|
||||||
|
<Col sm={12} center={component ? undefined : 'sm'} middle={component ? undefined : 'sm'} layout="column">
|
||||||
|
{ component || <Img alt="Safe Icon" src={safeIcon} height={330} /> }
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TokenLayout
|
|
@ -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<Props, State> {
|
||||||
|
state = {
|
||||||
|
checked: this.props.token.get('status'),
|
||||||
|
}
|
||||||
|
|
||||||
|
// onRemoveClick = () => this.props.onRemoveToken(this.props.token)
|
||||||
|
|
||||||
|
handleChange = (e: SyntheticInputEvent<HTMLInputElement>) => {
|
||||||
|
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 (
|
||||||
|
<Card className={classes.card}>
|
||||||
|
<Block className={classes.details}>
|
||||||
|
<CardContent className={classes.content}>
|
||||||
|
<Typography variant="headline">{name}</Typography>
|
||||||
|
<Typography variant="subheading" color="textSecondary">
|
||||||
|
<Checkbox
|
||||||
|
disabled={disabled}
|
||||||
|
checked={!!this.state.checked}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
{symbol}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Block>
|
||||||
|
{/*
|
||||||
|
<Block className={classes.controls}>
|
||||||
|
<IconButton aria-label="Delete" onClick={this.onRemoveClick}>
|
||||||
|
<Delete />
|
||||||
|
</IconButton>
|
||||||
|
</Block>
|
||||||
|
*/}
|
||||||
|
<CardMedia
|
||||||
|
className={classes.cover}
|
||||||
|
image={token.get('logoUrl')}
|
||||||
|
title={name}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles, { withTheme: true })(TokenComponent)
|
|
@ -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,
|
||||||
|
}
|
|
@ -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<Props> {
|
||||||
|
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 (
|
||||||
|
<Page>
|
||||||
|
<Layout
|
||||||
|
tokens={tokens}
|
||||||
|
addresses={addresses}
|
||||||
|
safe={safe}
|
||||||
|
safeAddress={safeAddress}
|
||||||
|
disableToken={disableToken}
|
||||||
|
enableToken={enableToken}
|
||||||
|
/>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(selector, actions)(TokensView)
|
|
@ -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<Token>,
|
||||||
|
addresses: List<String>,
|
||||||
|
safe: Safe,
|
||||||
|
safeAddress: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createStructuredSelector({
|
||||||
|
safe: safeSelector,
|
||||||
|
safeAddress: safeParamAddressSelector,
|
||||||
|
tokens: tokenListSelector,
|
||||||
|
addresses: tokenAddressesSelector,
|
||||||
|
})
|
|
@ -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<string, Token>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const addTokens = createAction(
|
||||||
|
ADD_TOKENS,
|
||||||
|
(safeAddress: string, tokens: Map<string, Token>): TokenProps => ({
|
||||||
|
safeAddress,
|
||||||
|
tokens,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export default addTokens
|
|
@ -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
|
|
@ -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
|
|
@ -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<GlobalState>) => {
|
||||||
|
const tokens: List<string> = 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<string, Token> = 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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,22 +2,26 @@
|
||||||
import { Record } from 'immutable'
|
import { Record } from 'immutable'
|
||||||
import type { RecordFactory, RecordOf } from 'immutable'
|
import type { RecordFactory, RecordOf } from 'immutable'
|
||||||
|
|
||||||
export type BalanceProps = {
|
export type TokenProps = {
|
||||||
address: string,
|
address: string,
|
||||||
name: string,
|
name: string,
|
||||||
symbol: string,
|
symbol: string,
|
||||||
decimals: number,
|
decimals: number,
|
||||||
logoUrl: string,
|
logoUrl: string,
|
||||||
funds: string,
|
funds: string,
|
||||||
|
status: boolean,
|
||||||
|
removable: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeBalance: RecordFactory<BalanceProps> = Record({
|
export const makeToken: RecordFactory<TokenProps> = Record({
|
||||||
address: '',
|
address: '',
|
||||||
name: '',
|
name: '',
|
||||||
symbol: '',
|
symbol: '',
|
||||||
decimals: 0,
|
decimals: 0,
|
||||||
logoUrl: '',
|
logoUrl: '',
|
||||||
funds: '0',
|
funds: '0',
|
||||||
|
status: true,
|
||||||
|
removable: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
export type Balance = RecordOf<BalanceProps>
|
export type Token = RecordOf<TokenProps>
|
|
@ -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<string, Map<string, Token>>
|
||||||
|
|
||||||
|
const setTokensOnce = ensureOnce(setTokens)
|
||||||
|
|
||||||
|
export default handleActions({
|
||||||
|
[ADD_TOKENS]: (state: State, action: ActionType<typeof addTokens>): State => {
|
||||||
|
const { safeAddress, tokens } = action.payload
|
||||||
|
|
||||||
|
const activeAddresses: List<Token> = calculateActiveErc20TokensFrom(tokens.toList())
|
||||||
|
setTokensOnce(safeAddress, activeAddresses)
|
||||||
|
|
||||||
|
return state.update(safeAddress, (prevSafe: Map<string, Token>) => {
|
||||||
|
if (!prevSafe) {
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
return prevSafe.equals(tokens) ? prevSafe : tokens
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[DISABLE_TOKEN]: (state: State, action: ActionType<typeof disableToken>): 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<typeof enableToken>): State => {
|
||||||
|
const { address, safeAddress, symbol } = action.payload
|
||||||
|
|
||||||
|
const activeTokens = getTokens(safeAddress)
|
||||||
|
setTokens(safeAddress, activeTokens.push(address))
|
||||||
|
|
||||||
|
return state.setIn([safeAddress, symbol, 'status'], true)
|
||||||
|
},
|
||||||
|
}, Map())
|
|
@ -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<GlobalState, RouterProps, Map<string, Token>> = createSelector(
|
||||||
|
balancesSelector,
|
||||||
|
safeParamAddressSelector,
|
||||||
|
(tokens: Map<string, Map<string, Token>>, address: string) => {
|
||||||
|
if (!address) {
|
||||||
|
return Map()
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens.get(address) || Map()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export const tokenListSelector = createSelector(
|
||||||
|
tokensSelector,
|
||||||
|
(tokens: Map<string, Token>) => tokens.toList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const activeTokensSelector = createSelector(
|
||||||
|
tokenListSelector,
|
||||||
|
(tokens: List<Token>) => tokens.filter((token: Token) => token.get('status')),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const tokenAddressesSelector = createSelector(
|
||||||
|
tokenListSelector,
|
||||||
|
(balances: List<Token>) => {
|
||||||
|
const addresses = List().withMutations(list =>
|
||||||
|
balances.map(token => list.push(token.address)))
|
||||||
|
|
||||||
|
return addresses
|
||||||
|
},
|
||||||
|
)
|
|
@ -5,7 +5,7 @@ import { combineReducers, createStore, applyMiddleware, compose, type Reducer, t
|
||||||
import thunk from 'redux-thunk'
|
import thunk from 'redux-thunk'
|
||||||
import provider, { PROVIDER_REDUCER_ID, type State as ProviderState } from '~/wallets/store/reducer/provider'
|
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 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'
|
import transactions, { type State as TransactionsState, transactionsInitialState, TRANSACTIONS_REDUCER_ID } from '~/routes/safe/store/reducer/transactions'
|
||||||
|
|
||||||
export const history = createBrowserHistory()
|
export const history = createBrowserHistory()
|
||||||
|
@ -20,7 +20,7 @@ const finalCreateStore = composeEnhancers(applyMiddleware(
|
||||||
export type GlobalState = {
|
export type GlobalState = {
|
||||||
providers: ProviderState,
|
providers: ProviderState,
|
||||||
safes: SafeState,
|
safes: SafeState,
|
||||||
balances: BalancesState,
|
tokens: TokensState,
|
||||||
transactions: TransactionsState,
|
transactions: TransactionsState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ const reducers: Reducer<GlobalState> = combineReducers({
|
||||||
routing: routerReducer,
|
routing: routerReducer,
|
||||||
[PROVIDER_REDUCER_ID]: provider,
|
[PROVIDER_REDUCER_ID]: provider,
|
||||||
[SAFE_REDUCER_ID]: safe,
|
[SAFE_REDUCER_ID]: safe,
|
||||||
[BALANCE_REDUCER_ID]: balances,
|
[TOKEN_REDUCER_ID]: tokens,
|
||||||
[TRANSACTIONS_REDUCER_ID]: transactions,
|
[TRANSACTIONS_REDUCER_ID]: transactions,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { sleep } from '~/utils/timer'
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
import { ConnectedRouter } from 'react-router-redux'
|
import { ConnectedRouter } from 'react-router-redux'
|
||||||
import AppRoutes from '~/routes'
|
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 { history, type GlobalState } from '~/store'
|
||||||
import { EMPTY_DATA } from '~/wallets/ethTransactions'
|
import { EMPTY_DATA } from '~/wallets/ethTransactions'
|
||||||
|
|
||||||
|
@ -93,15 +93,25 @@ export const refreshTransactions = async (store: Store<GlobalState>) => {
|
||||||
await sleep(1500)
|
await sleep(1500)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const travelToSafe = (store: Store, address: string): React$Component<{}> => {
|
const createDom = (store: Store): React$Component<{}> => (
|
||||||
history.push(`${SAFELIST_ADDRESS}/${address}`)
|
TestUtils.renderIntoDocument((
|
||||||
const SafeDom = TestUtils.renderIntoDocument((
|
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ConnectedRouter history={history}>
|
<ConnectedRouter history={history}>
|
||||||
<AppRoutes />
|
<AppRoutes />
|
||||||
</ConnectedRouter>
|
</ConnectedRouter>
|
||||||
</Provider>
|
</Provider>
|
||||||
))
|
))
|
||||||
|
)
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import TestUtils from 'react-dom/test-utils'
|
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 { aNewStore } from '~/store'
|
||||||
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
|
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 { EXPAND_BALANCE_INDEX, travelToSafe } from '~/test/builder/safe.dom.utils'
|
||||||
import { promisify } from '~/utils/promisify'
|
import { promisify } from '~/utils/promisify'
|
||||||
import { getWeb3 } from '~/wallets/getWeb3'
|
import { getWeb3 } from '~/wallets/getWeb3'
|
||||||
|
@ -47,14 +47,14 @@ describe('DOM > Feature > SAFE ERC20 TOKENS', () => {
|
||||||
const receiverFunds = await fetchBalancesAction.calculateBalanceOf(tokenAddress, receiver, 18)
|
const receiverFunds = await fetchBalancesAction.calculateBalanceOf(tokenAddress, receiver, 18)
|
||||||
expect(Number(receiverFunds)).toBe(20)
|
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)
|
const nativeSafeFunds = await token.balanceOf(safeAddress)
|
||||||
expect(Number(nativeSafeFunds.valueOf())).toEqual(80 * (10 ** 18))
|
expect(Number(nativeSafeFunds.valueOf())).toEqual(80 * (10 ** 18))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('disables send token button when balance is 0', async () => {
|
it('disables send token button when balance is 0', async () => {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
const token = await getTokenContract(getWeb3(), accounts[0])
|
const token = await getFirstTokenContract(getWeb3(), accounts[0])
|
||||||
await dispatchTknBalance(store, token.address, safeAddress)
|
await dispatchTknBalance(store, token.address, safeAddress)
|
||||||
|
|
||||||
// WHEN
|
// WHEN
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { Map } from 'immutable'
|
import { Map } from 'immutable'
|
||||||
import { BALANCE_REDUCER_ID } from '~/routes/safe/store/reducer/balances'
|
import * as fetchTokensAction from '~/routes/tokens/store/actions/fetchTokens'
|
||||||
import * as fetchBalancesAction from '~/routes/safe/store/actions/fetchBalances'
|
|
||||||
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 { 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 { addEtherTo, addTknTo } from '~/test/utils/tokenMovements'
|
||||||
import { dispatchTknBalance } from '~/test/utils/transactions/moveTokens.helper'
|
import { dispatchTknBalance } from '~/test/utils/transactions/moveTokens.helper'
|
||||||
|
|
||||||
|
@ -21,13 +21,13 @@ describe('Safe - redux balance property', () => {
|
||||||
const tokenList = ['WE', '<3', 'GNO', 'OMG', 'RDN']
|
const tokenList = ['WE', '<3', 'GNO', 'OMG', 'RDN']
|
||||||
|
|
||||||
// WHEN
|
// WHEN
|
||||||
await store.dispatch(fetchBalancesAction.fetchBalances(address))
|
await store.dispatch(fetchTokensAction.fetchTokens(address))
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
const balances: Map<string, Map<string, Balance>> | typeof undefined = store.getState()[BALANCE_REDUCER_ID]
|
const tokens: Map<string, Map<string, Token>> | typeof undefined = store.getState()[TOKEN_REDUCER_ID]
|
||||||
if (!balances) throw new Error()
|
if (!tokens) throw new Error()
|
||||||
|
|
||||||
const safeBalances: Map<string, Balance> | typeof undefined = balances.get(address)
|
const safeBalances: Map<string, Token> | typeof undefined = tokens.get(address)
|
||||||
if (!safeBalances) throw new Error()
|
if (!safeBalances) throw new Error()
|
||||||
expect(safeBalances.size).toBe(6)
|
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 () => {
|
it('reducer should return 0.03456 ETH as funds to safe with 0.03456 ETH', async () => {
|
||||||
// WHEN
|
// WHEN
|
||||||
await addEtherTo(address, '0.03456')
|
await addEtherTo(address, '0.03456')
|
||||||
await store.dispatch(fetchBalancesAction.fetchBalances(address))
|
await store.dispatch(fetchTokensAction.fetchTokens(address))
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
const balances: Map<string, Map<string, Balance>> | typeof undefined = store.getState()[BALANCE_REDUCER_ID]
|
const tokens: Map<string, Map<string, Token>> | typeof undefined = store.getState()[TOKEN_REDUCER_ID]
|
||||||
if (!balances) throw new Error()
|
if (!tokens) throw new Error()
|
||||||
|
|
||||||
const safeBalances: Map<string, Balance> | typeof undefined = balances.get(address)
|
const safeBalances: Map<string, Token> | typeof undefined = tokens.get(address)
|
||||||
if (!safeBalances) throw new Error()
|
if (!safeBalances) throw new Error()
|
||||||
expect(safeBalances.size).toBe(6)
|
expect(safeBalances.size).toBe(6)
|
||||||
|
|
||||||
|
@ -65,7 +65,7 @@ describe('Safe - redux balance property', () => {
|
||||||
await dispatchTknBalance(store, tokenAddress, address)
|
await dispatchTknBalance(store, tokenAddress, address)
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
const safeBalances = store.getState()[BALANCE_REDUCER_ID].get(address)
|
const safeBalances = store.getState()[TOKEN_REDUCER_ID].get(address)
|
||||||
expect(safeBalances.size).toBe(1)
|
expect(safeBalances.size).toBe(1)
|
||||||
|
|
||||||
const tknBalance = safeBalances.get('TKN')
|
const tknBalance = safeBalances.get('TKN')
|
||||||
|
|
|
@ -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((
|
||||||
|
<Provider store={store}>
|
||||||
|
<ConnectedRouter history={history}>
|
||||||
|
<AppRoutes />
|
||||||
|
</ConnectedRouter>
|
||||||
|
</Provider>
|
||||||
|
))
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
|
@ -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<string> = 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)
|
||||||
|
})
|
||||||
|
})
|
|
@ -39,13 +39,14 @@ const createTokenContract = async (web3: any, executor: string) => {
|
||||||
return token.new({ from: executor, gas: '5000000' })
|
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 web3 = getWeb3()
|
||||||
const accounts = await promisify(cb => getWeb3().eth.getAccounts(cb))
|
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)
|
const nativeValue = await toNative(value, 18)
|
||||||
await myToken.transfer(safe, nativeValue.valueOf(), { from: accounts[0], gas: '5000000' })
|
await myToken.transfer(safe, nativeValue.valueOf(), { from: accounts[0], gas: '5000000' })
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,12 @@
|
||||||
import { Map } from 'immutable'
|
import { Map } from 'immutable'
|
||||||
import TestUtils from 'react-dom/test-utils'
|
import TestUtils from 'react-dom/test-utils'
|
||||||
import { sleep } from '~/utils/timer'
|
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 { 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 { whenExecuted } from '~/test/utils/logTransactions'
|
||||||
import SendToken from '~/routes/safe/component/SendToken'
|
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 (
|
export const sendMoveTokensForm = async (
|
||||||
SafeDom: React$Component<any, any>,
|
SafeDom: React$Component<any, any>,
|
||||||
|
@ -44,9 +44,9 @@ export const sendMoveTokensForm = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dispatchTknBalance = async (store: Store, tokenAddress: string, address: string) => {
|
export const dispatchTknBalance = async (store: Store, tokenAddress: string, address: string) => {
|
||||||
const fetchBalancesMock = jest.spyOn(fetchBalancesAction, 'fetchBalances')
|
const fetchBalancesMock = jest.spyOn(fetchTokensAction, 'fetchTokens')
|
||||||
const funds = await fetchBalancesAction.calculateBalanceOf(tokenAddress, address, 18)
|
const funds = await fetchTokensAction.calculateBalanceOf(tokenAddress, address, 18)
|
||||||
const balances: Map<string, Balance> = Map().set('TKN', makeBalance({
|
const balances: Map<string, Token> = Map().set('TKN', makeToken({
|
||||||
address: tokenAddress,
|
address: tokenAddress,
|
||||||
name: 'Token',
|
name: 'Token',
|
||||||
symbol: 'TKN',
|
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',
|
logoUrl: 'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true',
|
||||||
funds,
|
funds,
|
||||||
}))
|
}))
|
||||||
fetchBalancesMock.mockImplementation(() => store.dispatch(addBalances(address, balances)))
|
fetchBalancesMock.mockImplementation(() => store.dispatch(addTokens(address, balances)))
|
||||||
await store.dispatch(fetchBalancesAction.fetchBalances(address))
|
await store.dispatch(fetchTokensAction.fetchTokens(address))
|
||||||
fetchBalancesMock.mockRestore()
|
fetchBalancesMock.mockRestore()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import { type Owner } from '~/routes/safe/store/model/owner'
|
||||||
export const SAFES_KEY = 'SAFES'
|
export const SAFES_KEY = 'SAFES'
|
||||||
export const TX_KEY = 'TX'
|
export const TX_KEY = 'TX'
|
||||||
export const OWNERS_KEY = 'OWNERS'
|
export const OWNERS_KEY = 'OWNERS'
|
||||||
|
export const TOKENS_KEY = 'TOKENS'
|
||||||
|
|
||||||
export const load = (key: string) => {
|
export const load = (key: string) => {
|
||||||
try {
|
try {
|
|
@ -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<string>) => {
|
||||||
|
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<string> => {
|
||||||
|
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
|
||||||
|
}
|
|
@ -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<Token>) => {
|
||||||
|
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
|
||||||
|
}
|
|
@ -2,6 +2,7 @@
|
||||||
import { BigNumber } from 'bignumber.js'
|
import { BigNumber } from 'bignumber.js'
|
||||||
import { getWeb3 } from '~/wallets/getWeb3'
|
import { getWeb3 } from '~/wallets/getWeb3'
|
||||||
import { promisify } from '~/utils/promisify'
|
import { promisify } from '~/utils/promisify'
|
||||||
|
import { enhancedFetch } from '~/utils/fetch'
|
||||||
|
|
||||||
// const MAINNET_NETWORK = 1
|
// const MAINNET_NETWORK = 1
|
||||||
export const EMPTY_DATA = '0x'
|
export const EMPTY_DATA = '0x'
|
||||||
|
@ -40,21 +41,9 @@ export const calculateGasPrice = async () => {
|
||||||
return '20000000000'
|
return '20000000000'
|
||||||
}
|
}
|
||||||
|
|
||||||
const header = new Headers({
|
const url = 'https://ethgasstation.info/json/ethgasAPI.json'
|
||||||
'Access-Control-Allow-Origin': '*',
|
const errMsg = 'Error querying gas station'
|
||||||
})
|
const json = await enhancedFetch(url, errMsg)
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
return new BigNumber(json.average).multipliedBy(1e8).toString()
|
return new BigNumber(json.average).multipliedBy(1e8).toString()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue