Merge pull request #52 from gnosis/feature/WA-232-add-custom-tokens
WA-232 - Feature: Add custom ERC 20 tokens
This commit is contained in:
commit
cb3e5f5445
|
@ -1,7 +1,7 @@
|
||||||
import 'babel-polyfill'
|
import 'babel-polyfill'
|
||||||
import { addDecorator, configure } from '@storybook/react'
|
import { addDecorator, configure } from '@storybook/react'
|
||||||
import { withKnobs } from '@storybook/addon-knobs'
|
import { withKnobs } from '@storybook/addon-knobs'
|
||||||
import { MuiThemeProvider } from 'material-ui/styles'
|
import { MuiThemeProvider } from '@material-ui/core/styles'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
import StoryRouter from 'storybook-router'
|
import StoryRouter from 'storybook-router'
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -6,16 +6,16 @@ type ControlProps = {
|
||||||
next: string,
|
next: string,
|
||||||
onPrevious: () => void,
|
onPrevious: () => void,
|
||||||
firstPage: boolean,
|
firstPage: boolean,
|
||||||
submitting: boolean,
|
disabled: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ControlButtons = ({
|
const ControlButtons = ({
|
||||||
next, firstPage, onPrevious, submitting,
|
next, firstPage, onPrevious, disabled,
|
||||||
}: ControlProps) => (
|
}: ControlProps) => (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={firstPage || submitting}
|
disabled={firstPage || disabled}
|
||||||
onClick={onPrevious}
|
onClick={onPrevious}
|
||||||
>
|
>
|
||||||
Back
|
Back
|
||||||
|
@ -24,7 +24,7 @@ const ControlButtons = ({
|
||||||
variant="raised"
|
variant="raised"
|
||||||
color="primary"
|
color="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={submitting}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{next}
|
{next}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -37,16 +37,16 @@ type Props = {
|
||||||
onPrevious: () => void,
|
onPrevious: () => void,
|
||||||
firstPage: boolean,
|
firstPage: boolean,
|
||||||
lastPage: boolean,
|
lastPage: boolean,
|
||||||
submitting: boolean,
|
disabled: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
const Controls = ({
|
const Controls = ({
|
||||||
finishedTx, finishedButton, onPrevious, firstPage, lastPage, submitting,
|
finishedTx, finishedButton, onPrevious, firstPage, lastPage, disabled,
|
||||||
}: Props) => (
|
}: Props) => (
|
||||||
finishedTx
|
finishedTx
|
||||||
? <React.Fragment>{finishedButton}</React.Fragment>
|
? <React.Fragment>{finishedButton}</React.Fragment>
|
||||||
: <ControlButtons
|
: <ControlButtons
|
||||||
submitting={submitting}
|
disabled={disabled}
|
||||||
next={lastPage ? 'Finish' : 'Next'}
|
next={lastPage ? 'Finish' : 'Next'}
|
||||||
firstPage={firstPage}
|
firstPage={firstPage}
|
||||||
onPrevious={onPrevious}
|
onPrevious={onPrevious}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import Controls from './Controls'
|
||||||
export { default as Step } from './Step'
|
export { default as Step } from './Step'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
disabledWhenValidating?: boolean,
|
||||||
classes: Object,
|
classes: Object,
|
||||||
steps: string[],
|
steps: string[],
|
||||||
finishedTransaction: boolean,
|
finishedTransaction: boolean,
|
||||||
|
@ -30,6 +31,7 @@ type State = {
|
||||||
|
|
||||||
type PageProps = {
|
type PageProps = {
|
||||||
children: Function,
|
children: Function,
|
||||||
|
prepareNextInitialProps: (values: Object) => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
class GnoStepper extends React.PureComponent<Props, State> {
|
class GnoStepper extends React.PureComponent<Props, State> {
|
||||||
|
@ -62,8 +64,10 @@ class GnoStepper extends React.PureComponent<Props, State> {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPageProps = (pages: React$Node): PageProps => React.Children.toArray(pages)[this.state.page].props
|
||||||
|
|
||||||
getActivePageFrom = (pages: React$Node) => {
|
getActivePageFrom = (pages: React$Node) => {
|
||||||
const activePageProps = React.Children.toArray(pages)[this.state.page].props
|
const activePageProps = this.getPageProps(pages)
|
||||||
const { children, ...props } = activePageProps
|
const { children, ...props } = activePageProps
|
||||||
|
|
||||||
return children(props)
|
return children(props)
|
||||||
|
@ -76,18 +80,28 @@ class GnoStepper extends React.PureComponent<Props, State> {
|
||||||
return activePage.props.validate ? activePage.props.validate(values) : {}
|
return activePage.props.validate ? activePage.props.validate(values) : {}
|
||||||
}
|
}
|
||||||
|
|
||||||
next = (values: Object) =>
|
next = async (values: Object) => {
|
||||||
|
const activePageProps = this.getPageProps(this.props.children)
|
||||||
|
const { prepareNextInitialProps } = activePageProps
|
||||||
|
|
||||||
|
let pageInitialProps
|
||||||
|
if (prepareNextInitialProps) {
|
||||||
|
pageInitialProps = await prepareNextInitialProps(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalValues = { ...values, ...pageInitialProps }
|
||||||
this.setState(state => ({
|
this.setState(state => ({
|
||||||
page: Math.min(state.page + 1, React.Children.count(this.props.children) - 1),
|
page: Math.min(state.page + 1, React.Children.count(this.props.children) - 1),
|
||||||
values,
|
values: finalValues,
|
||||||
}))
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
previous = () =>
|
previous = () =>
|
||||||
this.setState(state => ({
|
this.setState(state => ({
|
||||||
page: Math.max(state.page - 1, 0),
|
page: Math.max(state.page - 1, 0),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
handleSubmit = (values: Object) => {
|
handleSubmit = async (values: Object) => {
|
||||||
const { children, onSubmit } = this.props
|
const { children, onSubmit } = this.props
|
||||||
const { page } = this.state
|
const { page } = this.state
|
||||||
const isLastPage = page === React.Children.count(children) - 1
|
const isLastPage = page === React.Children.count(children) - 1
|
||||||
|
@ -100,7 +114,7 @@ class GnoStepper extends React.PureComponent<Props, State> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
steps, children, finishedTransaction, finishedButton, classes,
|
steps, children, finishedTransaction, finishedButton, classes, disabledWhenValidating = false,
|
||||||
} = this.props
|
} = this.props
|
||||||
const { page, values } = this.state
|
const { page, values } = this.state
|
||||||
const activePage = this.getActivePageFrom(children)
|
const activePage = this.getActivePageFrom(children)
|
||||||
|
@ -123,11 +137,14 @@ class GnoStepper extends React.PureComponent<Props, State> {
|
||||||
validation={this.validate}
|
validation={this.validate}
|
||||||
render={activePage}
|
render={activePage}
|
||||||
>
|
>
|
||||||
{(submitting: boolean) => (
|
{(submitting: boolean, validating: boolean) => {
|
||||||
|
const disabled = disabledWhenValidating ? submitting || validating : submitting
|
||||||
|
|
||||||
|
return (
|
||||||
<Row align="end" margin="lg" grow>
|
<Row align="end" margin="lg" grow>
|
||||||
<Col xs={12} center="xs">
|
<Col xs={12} center="xs">
|
||||||
<Controls
|
<Controls
|
||||||
submitting={submitting}
|
disabled={disabled}
|
||||||
finishedTx={finishedTransaction}
|
finishedTx={finishedTransaction}
|
||||||
finishedButton={finished}
|
finishedButton={finished}
|
||||||
onPrevious={this.previous}
|
onPrevious={this.previous}
|
||||||
|
@ -136,7 +153,8 @@ class GnoStepper extends React.PureComponent<Props, State> {
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)
|
||||||
|
}}
|
||||||
</GnoForm>
|
</GnoForm>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import * as React from 'react'
|
||||||
import { Field } from 'react-final-form'
|
import { Field } from 'react-final-form'
|
||||||
|
|
||||||
export default Field
|
// $FlowFixMe
|
||||||
|
const GnoField = ({ ...props }): React.Element<*> => <Field {...props} />
|
||||||
|
|
||||||
|
export default GnoField
|
||||||
|
|
|
@ -35,7 +35,7 @@ const GnoForm = ({
|
||||||
render={({ handleSubmit, ...rest }) => (
|
render={({ handleSubmit, ...rest }) => (
|
||||||
<form onSubmit={handleSubmit} style={stylesBasedOn(padding)}>
|
<form onSubmit={handleSubmit} style={stylesBasedOn(padding)}>
|
||||||
{render(rest)}
|
{render(rest)}
|
||||||
{children(rest.submitting, rest.submitSucceeded)}
|
{children(rest.submitting, rest.validating)}
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -20,6 +20,16 @@ export const greaterThan = (min: number) => (value: string) => {
|
||||||
return `Should be greater than ${min}`
|
return `Should be greater than ${min}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const regexQuery = /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/i
|
||||||
|
const url = new RegExp(regexQuery)
|
||||||
|
export const mustBeUrl = (value: string) => {
|
||||||
|
if (url.test(value)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Please, provide a valid url'
|
||||||
|
}
|
||||||
|
|
||||||
export const minValue = (min: number) => (value: string) => {
|
export const minValue = (min: number) => (value: string) => {
|
||||||
if (Number.isNaN(Number(value)) || Number.parseFloat(value) >= Number(min)) {
|
if (Number.isNaN(Number(value)) || Number.parseFloat(value) >= Number(min)) {
|
||||||
return undefined
|
return undefined
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Field } from 'react-final-form'
|
import Field from '~/components/forms/Field'
|
||||||
import TextField from '~/components/forms/TextField'
|
import TextField from '~/components/forms/TextField'
|
||||||
import { composeValidators, minValue, mustBeInteger, required } from '~/components/forms/validator'
|
import { composeValidators, minValue, mustBeInteger, required } from '~/components/forms/validator'
|
||||||
import Block from '~/components/layout/Block'
|
import Block from '~/components/layout/Block'
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { storiesOf } from '@storybook/react'
|
import { storiesOf } from '@storybook/react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Map } from 'immutable'
|
import { List } 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 { makeToken } from '~/routes/tokens/store/model/token'
|
import { makeToken } from '~/routes/tokens/store/model/token'
|
||||||
|
@ -30,7 +30,7 @@ storiesOf('Routes /safe:address', module)
|
||||||
userAddress="foo"
|
userAddress="foo"
|
||||||
safe={undefined}
|
safe={undefined}
|
||||||
provider="METAMASK"
|
provider="METAMASK"
|
||||||
activeTokens={Map()}
|
activeTokens={List([])}
|
||||||
fetchBalance={() => {}}
|
fetchBalance={() => {}}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
@ -39,7 +39,7 @@ storiesOf('Routes /safe:address', module)
|
||||||
userAddress="foo"
|
userAddress="foo"
|
||||||
safe={undefined}
|
safe={undefined}
|
||||||
provider=""
|
provider=""
|
||||||
activeTokens={Map()}
|
activeTokens={List([])}
|
||||||
fetchBalance={() => {}}
|
fetchBalance={() => {}}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
@ -51,7 +51,7 @@ storiesOf('Routes /safe:address', module)
|
||||||
userAddress="foo"
|
userAddress="foo"
|
||||||
safe={safe}
|
safe={safe}
|
||||||
provider="METAMASK"
|
provider="METAMASK"
|
||||||
activeTokens={Map().set('ETH', ethBalance)}
|
activeTokens={List([]).push(ethBalance)}
|
||||||
fetchBalance={() => {}}
|
fetchBalance={() => {}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -64,7 +64,7 @@ storiesOf('Routes /safe:address', module)
|
||||||
userAddress="foo"
|
userAddress="foo"
|
||||||
safe={safe}
|
safe={safe}
|
||||||
provider="METAMASK"
|
provider="METAMASK"
|
||||||
activeTokens={Map().set('ETH', ethBalance)}
|
activeTokens={List([]).push(ethBalance)}
|
||||||
fetchBalance={() => {}}
|
fetchBalance={() => {}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import List from '@material-ui/core/List'
|
import ListComponent from '@material-ui/core/List'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Map } from 'immutable'
|
import { List } from 'immutable'
|
||||||
import Block from '~/components/layout/Block'
|
import Block from '~/components/layout/Block'
|
||||||
import Col from '~/components/layout/Col'
|
import Col from '~/components/layout/Col'
|
||||||
import Bold from '~/components/layout/Bold'
|
import Bold from '~/components/layout/Bold'
|
||||||
|
@ -30,7 +30,7 @@ const safeIcon = require('./assets/gnosis_safe.svg')
|
||||||
|
|
||||||
type SafeProps = {
|
type SafeProps = {
|
||||||
safe: Safe,
|
safe: Safe,
|
||||||
tokens: Map<string, Token>,
|
tokens: List<Token>,
|
||||||
userAddress: string,
|
userAddress: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,12 +43,17 @@ const listStyle = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const getEthBalanceFrom = (tokens: List<Token>) => {
|
const getEthBalanceFrom = (tokens: List<Token>) => {
|
||||||
const ethToken = tokens.filter(token => token.get('symbol') === 'ETH')
|
const filteredTokens = tokens.filter(token => token.get('symbol') === 'ETH')
|
||||||
if (ethToken.count() === 0) {
|
if (filteredTokens.count() === 0) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return Number(ethToken.get(0).get('funds'))
|
const ethToken = filteredTokens.get(0)
|
||||||
|
if (!ethToken) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return Number(ethToken.get('funds'))
|
||||||
}
|
}
|
||||||
|
|
||||||
class GnoSafe extends React.PureComponent<SafeProps, State> {
|
class GnoSafe extends React.PureComponent<SafeProps, State> {
|
||||||
|
@ -100,7 +105,7 @@ class GnoSafe extends React.PureComponent<SafeProps, State> {
|
||||||
component: <SendToken
|
component: <SendToken
|
||||||
safe={safe}
|
safe={safe}
|
||||||
token={ercToken}
|
token={ercToken}
|
||||||
key={ercToken.get('symbol')}
|
key={ercToken.get('address')}
|
||||||
onReset={this.onListTransactions}
|
onReset={this.onListTransactions}
|
||||||
/>,
|
/>,
|
||||||
})
|
})
|
||||||
|
@ -115,7 +120,7 @@ class GnoSafe extends React.PureComponent<SafeProps, State> {
|
||||||
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}>
|
<ListComponent style={listStyle}>
|
||||||
<BalanceInfo tokens={tokens} onMoveFunds={this.onMoveTokens} safeAddress={address} />
|
<BalanceInfo tokens={tokens} onMoveFunds={this.onMoveTokens} safeAddress={address} />
|
||||||
<Owners
|
<Owners
|
||||||
owners={safe.owners}
|
owners={safe.owners}
|
||||||
|
@ -127,7 +132,7 @@ class GnoSafe extends React.PureComponent<SafeProps, State> {
|
||||||
<Address address={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>
|
</ListComponent>
|
||||||
</Col>
|
</Col>
|
||||||
<Col sm={12} center="xs" md={7} margin="xl" layout="column">
|
<Col sm={12} center="xs" md={7} margin="xl" layout="column">
|
||||||
<Block margin="xl">
|
<Block margin="xl">
|
||||||
|
|
|
@ -42,7 +42,7 @@ const getTransferData = async (tokenAddress: string, to: string, amount: BigNumb
|
||||||
const processTokenTransfer = async (safe: Safe, token: Token, to: string, amount: number, userAddress: string) => {
|
const processTokenTransfer = async (safe: Safe, token: Token, to: string, amount: number, userAddress: string) => {
|
||||||
const symbol = token.get('symbol')
|
const symbol = token.get('symbol')
|
||||||
const nonce = Date.now()
|
const nonce = Date.now()
|
||||||
const name = `Send ${amount} ${token.get('symbol')} to ${to}`
|
const name = `Send ${amount} ${symbol} to ${to}`
|
||||||
const value = isEther(symbol) ? amount : 0
|
const value = isEther(symbol) ? amount : 0
|
||||||
const tokenAddress = token.get('address')
|
const tokenAddress = token.get('address')
|
||||||
const destination = isEther(symbol) ? to : tokenAddress
|
const destination = isEther(symbol) ? to : tokenAddress
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { List, Map } from 'immutable'
|
import { List } from 'immutable'
|
||||||
import { createSelector, createStructuredSelector, type Selector } from 'reselect'
|
import { createSelector, createStructuredSelector, type Selector } from 'reselect'
|
||||||
import { safeSelector, type RouterProps, type SafeSelectorProps } from '~/routes/safe/store/selectors'
|
import { safeSelector, 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'
|
||||||
|
@ -13,7 +13,7 @@ import { type Token } from '~/routes/tokens/store/model/token'
|
||||||
export type SelectorProps = {
|
export type SelectorProps = {
|
||||||
safe: SafeSelectorProps,
|
safe: SafeSelectorProps,
|
||||||
provider: string,
|
provider: string,
|
||||||
activeTokens: Map<string, Token>,
|
activeTokens: List<Token>,
|
||||||
userAddress: string,
|
userAddress: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
// @flow
|
||||||
|
import { storiesOf } from '@storybook/react'
|
||||||
|
import * as React from 'react'
|
||||||
|
import { List } from 'immutable'
|
||||||
|
import styles from '~/components/layout/PageFrame/index.scss'
|
||||||
|
import AddTokenForm from './index'
|
||||||
|
|
||||||
|
const FrameDecorator = story => (
|
||||||
|
<div className={styles.frame} style={{ textAlign: 'center' }}>
|
||||||
|
{ story() }
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
storiesOf('Components', module)
|
||||||
|
.addDecorator(FrameDecorator)
|
||||||
|
.add('AddTokenForm', () => (
|
||||||
|
// $FlowFixMe
|
||||||
|
<AddTokenForm tokens={List([]).toArray()} safeAddress="" />
|
||||||
|
))
|
|
@ -0,0 +1,56 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from 'react'
|
||||||
|
import Field from '~/components/forms/Field'
|
||||||
|
import TextField from '~/components/forms/TextField'
|
||||||
|
import { composeValidators, required, mustBeEthereumAddress, uniqueAddress } from '~/components/forms/validator'
|
||||||
|
import Block from '~/components/layout/Block'
|
||||||
|
import Heading from '~/components/layout/Heading'
|
||||||
|
import { promisify } from '~/utils/promisify'
|
||||||
|
import { getWeb3 } from '~/wallets/getWeb3'
|
||||||
|
import { EMPTY_DATA } from '~/wallets/ethTransactions'
|
||||||
|
import { getStandardTokenContract } from '~/routes/tokens/store/actions/fetchTokens'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
addresses: string[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TOKEN_ADRESS_PARAM = 'tokenAddress'
|
||||||
|
|
||||||
|
export const token = async (tokenAddress: string) => {
|
||||||
|
const code = await promisify(cb => getWeb3().eth.getCode(tokenAddress, cb))
|
||||||
|
const isDeployed = code !== EMPTY_DATA
|
||||||
|
|
||||||
|
if (!isDeployed) {
|
||||||
|
return 'Specified address is not deployed on the current network'
|
||||||
|
}
|
||||||
|
|
||||||
|
const erc20Token = await getStandardTokenContract()
|
||||||
|
const instance = await erc20Token.at(tokenAddress)
|
||||||
|
const supply = await instance.totalSupply()
|
||||||
|
|
||||||
|
if (Number(supply) === 0) {
|
||||||
|
return 'Specified address is not a valid standard token'
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const FirstPage = ({ addresses }: Props) => () => (
|
||||||
|
<Block margin="md">
|
||||||
|
<Heading tag="h2" margin="lg">
|
||||||
|
Add Custom ERC20 Token
|
||||||
|
</Heading>
|
||||||
|
<Block margin="md">
|
||||||
|
<Field
|
||||||
|
name={TOKEN_ADRESS_PARAM}
|
||||||
|
component={TextField}
|
||||||
|
type="text"
|
||||||
|
validate={composeValidators(required, mustBeEthereumAddress, uniqueAddress(addresses), token)}
|
||||||
|
placeholder="ERC20 Token Address*"
|
||||||
|
text="ERC20 Token Address"
|
||||||
|
/>
|
||||||
|
</Block>
|
||||||
|
</Block>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default FirstPage
|
|
@ -0,0 +1,45 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from 'react'
|
||||||
|
import CircularProgress from '@material-ui/core/CircularProgress'
|
||||||
|
import Block from '~/components/layout/Block'
|
||||||
|
import Bold from '~/components/layout/Bold'
|
||||||
|
import Heading from '~/components/layout/Heading'
|
||||||
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
|
import { TOKEN_ADRESS_PARAM } from '~/routes/tokens/component/AddToken/FirstPage'
|
||||||
|
import { TOKEN_LOGO_URL_PARAM, TOKEN_NAME_PARAM, TOKEN_SYMBOL_PARAM, TOKEN_DECIMALS_PARAM } from '~/routes/tokens/component/AddToken/SecondPage'
|
||||||
|
|
||||||
|
type FormProps = {
|
||||||
|
values: Object,
|
||||||
|
submitting: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
const spinnerStyle = {
|
||||||
|
minHeight: '50px',
|
||||||
|
}
|
||||||
|
|
||||||
|
const Review = () => ({ values, submitting }: FormProps) => (
|
||||||
|
<Block>
|
||||||
|
<Heading tag="h2">Review ERC20 Token operation</Heading>
|
||||||
|
<Paragraph align="left">
|
||||||
|
<Bold>Token address: </Bold> {values[TOKEN_ADRESS_PARAM]}
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph align="left">
|
||||||
|
<Bold>Token name: </Bold> {values[TOKEN_NAME_PARAM]}
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph align="left">
|
||||||
|
<Bold>Token symbol: </Bold> {values[TOKEN_SYMBOL_PARAM]}
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph align="left">
|
||||||
|
<Bold>Token decimals: </Bold> {values[TOKEN_DECIMALS_PARAM]}
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph align="left">
|
||||||
|
<Bold>Token logo: </Bold> {values[TOKEN_LOGO_URL_PARAM]}
|
||||||
|
</Paragraph>
|
||||||
|
<Block style={spinnerStyle}>
|
||||||
|
{ submitting && <CircularProgress size={50} /> }
|
||||||
|
</Block>
|
||||||
|
</Block>
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
export default Review
|
|
@ -0,0 +1,62 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from 'react'
|
||||||
|
import Field from '~/components/forms/Field'
|
||||||
|
import TextField from '~/components/forms/TextField'
|
||||||
|
import { composeValidators, required, mustBeInteger, mustBeUrl } from '~/components/forms/validator'
|
||||||
|
import Block from '~/components/layout/Block'
|
||||||
|
import Heading from '~/components/layout/Heading'
|
||||||
|
|
||||||
|
export const TOKEN_NAME_PARAM = 'tokenName'
|
||||||
|
export const TOKEN_SYMBOL_PARAM = 'tokenSymbol'
|
||||||
|
export const TOKEN_DECIMALS_PARAM = 'tokenDecimals'
|
||||||
|
export const TOKEN_LOGO_URL_PARAM = 'tokenLogo'
|
||||||
|
|
||||||
|
const SecondPage = () => () => (
|
||||||
|
<Block margin="md">
|
||||||
|
<Heading tag="h2" margin="lg">
|
||||||
|
Complete Custom Token information
|
||||||
|
</Heading>
|
||||||
|
<Block margin="md">
|
||||||
|
<Field
|
||||||
|
name={TOKEN_NAME_PARAM}
|
||||||
|
component={TextField}
|
||||||
|
type="text"
|
||||||
|
validate={required}
|
||||||
|
placeholder="ERC20 Token Name*"
|
||||||
|
text="ERC20 Token Name"
|
||||||
|
/>
|
||||||
|
</Block>
|
||||||
|
<Block margin="md">
|
||||||
|
<Field
|
||||||
|
name={TOKEN_SYMBOL_PARAM}
|
||||||
|
component={TextField}
|
||||||
|
type="text"
|
||||||
|
validate={required}
|
||||||
|
placeholder="ERC20 Token Symbol*"
|
||||||
|
text="ERC20 Token Symbol"
|
||||||
|
/>
|
||||||
|
</Block>
|
||||||
|
<Block margin="md">
|
||||||
|
<Field
|
||||||
|
name={TOKEN_DECIMALS_PARAM}
|
||||||
|
component={TextField}
|
||||||
|
type="text"
|
||||||
|
validate={composeValidators(required, mustBeInteger)}
|
||||||
|
placeholder="ERC20 Token Decimals*"
|
||||||
|
text="ERC20 Token Decimals"
|
||||||
|
/>
|
||||||
|
</Block>
|
||||||
|
<Block margin="md">
|
||||||
|
<Field
|
||||||
|
name={TOKEN_LOGO_URL_PARAM}
|
||||||
|
component={TextField}
|
||||||
|
type="text"
|
||||||
|
validate={composeValidators(required, mustBeUrl)}
|
||||||
|
placeholder="ERC20 Token Logo url*"
|
||||||
|
text="ERC20 Token Logo"
|
||||||
|
/>
|
||||||
|
</Block>
|
||||||
|
</Block>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default SecondPage
|
|
@ -0,0 +1,128 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from 'react'
|
||||||
|
import Stepper from '~/components/Stepper'
|
||||||
|
import { getHumanFriendlyToken } from '~/routes/tokens/store/actions/fetchTokens'
|
||||||
|
import FirstPage, { TOKEN_ADRESS_PARAM } from '~/routes/tokens/component/AddToken/FirstPage'
|
||||||
|
import SecondPage, { TOKEN_SYMBOL_PARAM, TOKEN_DECIMALS_PARAM, TOKEN_LOGO_URL_PARAM, TOKEN_NAME_PARAM } from '~/routes/tokens/component/AddToken/SecondPage'
|
||||||
|
import { makeToken, type Token } from '~/routes/tokens/store/model/token'
|
||||||
|
import addTokenAction from '~/routes/tokens/store/actions/addToken'
|
||||||
|
import { getWeb3 } from '~/wallets/getWeb3'
|
||||||
|
import { promisify } from '~/utils/promisify'
|
||||||
|
import { EMPTY_DATA } from '~/wallets/ethTransactions'
|
||||||
|
import Review from './Review'
|
||||||
|
|
||||||
|
export const getSteps = () => [
|
||||||
|
'Fill Add Token Form', 'Check optional attributes', 'Review Information',
|
||||||
|
]
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
tokens: string[],
|
||||||
|
safeAddress: string,
|
||||||
|
addToken: typeof addTokenAction,
|
||||||
|
}
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
done: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ADD_TOKEN_RESET_BUTTON_TEXT = 'RESET'
|
||||||
|
|
||||||
|
export const addTokenFnc = async (values: Object, addToken: typeof addTokenAction, safeAddress: string) => {
|
||||||
|
const address = values[TOKEN_ADRESS_PARAM]
|
||||||
|
const name = values[TOKEN_NAME_PARAM]
|
||||||
|
const symbol = values[TOKEN_SYMBOL_PARAM]
|
||||||
|
const decimals = values[TOKEN_DECIMALS_PARAM]
|
||||||
|
const logo = values[TOKEN_LOGO_URL_PARAM]
|
||||||
|
|
||||||
|
const token: Token = makeToken({
|
||||||
|
address,
|
||||||
|
name,
|
||||||
|
symbol,
|
||||||
|
decimals: Number(decimals),
|
||||||
|
logoUrl: logo,
|
||||||
|
status: true,
|
||||||
|
removable: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
return addToken(safeAddress, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
class AddToken extends React.Component<Props, State> {
|
||||||
|
state = {
|
||||||
|
done: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
onAddToken = async (values: Object) => {
|
||||||
|
const { addToken, safeAddress } = this.props
|
||||||
|
|
||||||
|
const result = addTokenFnc(values, addToken, safeAddress)
|
||||||
|
this.setState({ done: true })
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
onReset = () => {
|
||||||
|
this.setState({ done: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchInitialPropsSecondPage = async (values: Object) => {
|
||||||
|
const tokenAddress = values[TOKEN_ADRESS_PARAM]
|
||||||
|
const erc20Token = await getHumanFriendlyToken()
|
||||||
|
const instance = await erc20Token.at(tokenAddress)
|
||||||
|
|
||||||
|
const dataName = await instance.contract.name.getData()
|
||||||
|
const nameResult = await promisify(cb => getWeb3().eth.call({ to: tokenAddress, data: dataName }, cb))
|
||||||
|
const hasName = nameResult !== EMPTY_DATA
|
||||||
|
|
||||||
|
const dataSymbol = await instance.contract.symbol.getData()
|
||||||
|
const symbolResult = await promisify(cb => getWeb3().eth.call({ to: tokenAddress, data: dataSymbol }, cb))
|
||||||
|
const hasSymbol = symbolResult !== EMPTY_DATA
|
||||||
|
|
||||||
|
const dataDecimals = await instance.contract.decimals.getData()
|
||||||
|
const decimalsResult = await promisify(cb => getWeb3().eth.call({ to: tokenAddress, data: dataDecimals }, cb))
|
||||||
|
const hasDecimals = decimalsResult !== EMPTY_DATA
|
||||||
|
|
||||||
|
|
||||||
|
const name = hasName ? await instance.name() : undefined
|
||||||
|
const symbol = hasSymbol ? await instance.symbol() : undefined
|
||||||
|
const decimals = hasDecimals ? `${await instance.decimals()}` : undefined
|
||||||
|
|
||||||
|
return ({
|
||||||
|
[TOKEN_SYMBOL_PARAM]: symbol,
|
||||||
|
[TOKEN_DECIMALS_PARAM]: decimals,
|
||||||
|
[TOKEN_NAME_PARAM]: name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { tokens, safeAddress } = this.props
|
||||||
|
const { done } = this.state
|
||||||
|
const steps = getSteps()
|
||||||
|
const finishedButton = <Stepper.FinishButton title={ADD_TOKEN_RESET_BUTTON_TEXT} />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Stepper
|
||||||
|
finishedTransaction={done}
|
||||||
|
finishedButton={finishedButton}
|
||||||
|
onSubmit={this.onAddToken}
|
||||||
|
steps={steps}
|
||||||
|
onReset={this.onReset}
|
||||||
|
disabledWhenValidating
|
||||||
|
>
|
||||||
|
<Stepper.Page addresses={tokens} prepareNextInitialProps={this.fetchInitialPropsSecondPage}>
|
||||||
|
{ FirstPage }
|
||||||
|
</Stepper.Page>
|
||||||
|
<Stepper.Page safeAddress={safeAddress}>
|
||||||
|
{ SecondPage }
|
||||||
|
</Stepper.Page>
|
||||||
|
<Stepper.Page>
|
||||||
|
{ Review }
|
||||||
|
</Stepper.Page>
|
||||||
|
</Stepper>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddToken
|
|
@ -4,6 +4,7 @@ import * as React from 'react'
|
||||||
import Block from '~/components/layout/Block'
|
import Block from '~/components/layout/Block'
|
||||||
import Col from '~/components/layout/Col'
|
import Col from '~/components/layout/Col'
|
||||||
import AccountBalanceWallet from '@material-ui/icons/AccountBalanceWallet'
|
import AccountBalanceWallet from '@material-ui/icons/AccountBalanceWallet'
|
||||||
|
import AddCircle from '@material-ui/icons/AddCircle'
|
||||||
import Link from '~/components/layout/Link'
|
import Link from '~/components/layout/Link'
|
||||||
import Bold from '~/components/layout/Bold'
|
import Bold from '~/components/layout/Bold'
|
||||||
import Img from '~/components/layout/Img'
|
import Img from '~/components/layout/Img'
|
||||||
|
@ -14,8 +15,8 @@ import { type Token } from '~/routes/tokens/store/model/token'
|
||||||
import { type SelectorProps } from '~/routes/tokens/container/selector'
|
import { type SelectorProps } from '~/routes/tokens/container/selector'
|
||||||
import { type Actions } from '~/routes/tokens/container/actions'
|
import { type Actions } from '~/routes/tokens/container/actions'
|
||||||
import { SAFELIST_ADDRESS } from '~/routes/routes'
|
import { SAFELIST_ADDRESS } from '~/routes/routes'
|
||||||
|
import AddToken from '~/routes/tokens/component/AddToken'
|
||||||
import TokenComponent from './Token'
|
import TokenComponent from './Token'
|
||||||
// import AddToken from '~/routes/tokens/component/AddToken'
|
|
||||||
// import RemoveToken from '~/routes/tokens/component/RemoveToken'
|
// import RemoveToken from '~/routes/tokens/component/RemoveToken'
|
||||||
|
|
||||||
const safeIcon = require('~/routes/safe/component/Safe/assets/gnosis_safe.svg')
|
const safeIcon = require('~/routes/safe/component/Safe/assets/gnosis_safe.svg')
|
||||||
|
@ -35,12 +36,20 @@ class TokenLayout extends React.PureComponent<TokenProps, State> {
|
||||||
state = {
|
state = {
|
||||||
component: undefined,
|
component: undefined,
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
onAddToken = () => {
|
onAddToken = () => {
|
||||||
const { addresses } = this.props
|
const { addresses, safeAddress, addToken } = this.props
|
||||||
this.setState({ component: <AddToken/> })
|
|
||||||
|
this.setState({
|
||||||
|
component: <AddToken
|
||||||
|
addToken={addToken}
|
||||||
|
tokens={addresses.toArray()}
|
||||||
|
safeAddress={safeAddress}
|
||||||
|
/>,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
onRemoveToken = () => {
|
onRemoveToken = () => {
|
||||||
this.setState({ component: <RemoveToken /> })
|
this.setState({ component: <RemoveToken /> })
|
||||||
}
|
}
|
||||||
|
@ -68,12 +77,14 @@ class TokenLayout extends React.PureComponent<TokenProps, State> {
|
||||||
<Row grow>
|
<Row grow>
|
||||||
<Col sm={12} top="xs" md={5} margin="xl" overflow>
|
<Col sm={12} top="xs" md={5} margin="xl" overflow>
|
||||||
<MuiList style={listStyle}>
|
<MuiList style={listStyle}>
|
||||||
{tokens.map((token: Token) => (<TokenComponent
|
{tokens.map((token: Token) => (
|
||||||
key={token.get('symbol')}
|
<TokenComponent
|
||||||
|
key={token.get('address')}
|
||||||
token={token}
|
token={token}
|
||||||
onDisableToken={this.onDisableToken}
|
onDisableToken={this.onDisableToken}
|
||||||
onEnableToken={this.onEnableToken}
|
onEnableToken={this.onEnableToken}
|
||||||
/>))}
|
/>
|
||||||
|
))}
|
||||||
</MuiList>
|
</MuiList>
|
||||||
</Col>
|
</Col>
|
||||||
<Col sm={12} center="xs" md={7} margin="xl" layout="column">
|
<Col sm={12} center="xs" md={7} margin="xl" layout="column">
|
||||||
|
@ -82,6 +93,9 @@ class TokenLayout extends React.PureComponent<TokenProps, State> {
|
||||||
<IconButton to={`${SAFELIST_ADDRESS}/${safeAddress}`} component={Link}>
|
<IconButton to={`${SAFELIST_ADDRESS}/${safeAddress}`} component={Link}>
|
||||||
<AccountBalanceWallet />
|
<AccountBalanceWallet />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
<IconButton onClick={this.onAddToken}>
|
||||||
|
<AddCircle />
|
||||||
|
</IconButton>
|
||||||
<Bold>{name}</Bold>
|
<Bold>{name}</Bold>
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import addToken from '~/routes/tokens/store/actions/addToken'
|
||||||
import enableToken from '~/routes/tokens/store/actions/enableToken'
|
import enableToken from '~/routes/tokens/store/actions/enableToken'
|
||||||
import disableToken from '~/routes/tokens/store/actions/disableToken'
|
import disableToken from '~/routes/tokens/store/actions/disableToken'
|
||||||
import { fetchTokens } from '~/routes/tokens/store/actions/fetchTokens'
|
import { fetchTokens } from '~/routes/tokens/store/actions/fetchTokens'
|
||||||
|
@ -6,9 +7,11 @@ import { fetchTokens } from '~/routes/tokens/store/actions/fetchTokens'
|
||||||
export type Actions = {
|
export type Actions = {
|
||||||
enableToken: typeof enableToken,
|
enableToken: typeof enableToken,
|
||||||
disableToken: typeof disableToken,
|
disableToken: typeof disableToken,
|
||||||
|
addToken: typeof addToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
addToken,
|
||||||
enableToken,
|
enableToken,
|
||||||
disableToken,
|
disableToken,
|
||||||
fetchTokens,
|
fetchTokens,
|
||||||
|
|
|
@ -22,7 +22,7 @@ class TokensView extends React.PureComponent<Props> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
tokens, addresses, safe, safeAddress, disableToken, enableToken,
|
tokens, addresses, safe, safeAddress, disableToken, enableToken, addToken,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -34,6 +34,7 @@ class TokensView extends React.PureComponent<Props> {
|
||||||
safeAddress={safeAddress}
|
safeAddress={safeAddress}
|
||||||
disableToken={disableToken}
|
disableToken={disableToken}
|
||||||
enableToken={enableToken}
|
enableToken={enableToken}
|
||||||
|
addToken={addToken}
|
||||||
/>
|
/>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { type Token } from '~/routes/tokens/store/model/token'
|
||||||
|
|
||||||
export type SelectorProps = {
|
export type SelectorProps = {
|
||||||
tokens: List<Token>,
|
tokens: List<Token>,
|
||||||
addresses: List<String>,
|
addresses: List<string>,
|
||||||
safe: Safe,
|
safe: Safe,
|
||||||
safeAddress: string,
|
safeAddress: string,
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
// @flow
|
||||||
|
import { createAction } from 'redux-actions'
|
||||||
|
import { type Token } from '~/routes/tokens/store/model/token'
|
||||||
|
|
||||||
|
export const ADD_TOKEN = 'ADD_TOKEN'
|
||||||
|
|
||||||
|
type AddTokenProps = {
|
||||||
|
safeAddress: string,
|
||||||
|
token: Token,
|
||||||
|
}
|
||||||
|
|
||||||
|
const addToken = createAction(
|
||||||
|
ADD_TOKEN,
|
||||||
|
(safeAddress: string, token: Token): AddTokenProps => ({
|
||||||
|
safeAddress,
|
||||||
|
token,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export default addToken
|
|
@ -8,7 +8,6 @@ const disableToken = createAction(
|
||||||
DISABLE_TOKEN,
|
DISABLE_TOKEN,
|
||||||
(safeAddress: string, token: Token) => ({
|
(safeAddress: string, token: Token) => ({
|
||||||
safeAddress,
|
safeAddress,
|
||||||
symbol: token.get('symbol'),
|
|
||||||
address: token.get('address'),
|
address: token.get('address'),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,7 +8,6 @@ const enableToken = createAction(
|
||||||
ENABLE_TOKEN,
|
ENABLE_TOKEN,
|
||||||
(safeAddress: string, token: Token) => ({
|
(safeAddress: string, token: Token) => ({
|
||||||
safeAddress,
|
safeAddress,
|
||||||
symbol: token.get('symbol'),
|
|
||||||
address: token.get('address'),
|
address: token.get('address'),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,11 +3,12 @@ import { List, Map } from 'immutable'
|
||||||
import contract from 'truffle-contract'
|
import contract from 'truffle-contract'
|
||||||
import type { Dispatch as ReduxDispatch } from 'redux'
|
import type { Dispatch as ReduxDispatch } from 'redux'
|
||||||
import StandardToken from '@gnosis.pm/util-contracts/build/contracts/StandardToken.json'
|
import StandardToken from '@gnosis.pm/util-contracts/build/contracts/StandardToken.json'
|
||||||
|
import HumanFriendlyToken from '@gnosis.pm/util-contracts/build/contracts/HumanFriendlyToken.json'
|
||||||
import { getWeb3 } from '~/wallets/getWeb3'
|
import { getWeb3 } from '~/wallets/getWeb3'
|
||||||
import { type GlobalState } from '~/store/index'
|
import { type GlobalState } from '~/store/index'
|
||||||
import { makeToken, type Token, type TokenProps } from '~/routes/tokens/store/model/token'
|
import { makeToken, type Token, type TokenProps } from '~/routes/tokens/store/model/token'
|
||||||
import { ensureOnce } from '~/utils/singleton'
|
import { ensureOnce } from '~/utils/singleton'
|
||||||
import { getTokens } from '~/utils/localStorage/tokens'
|
import { getActiveTokenAddresses, getTokens } from '~/utils/localStorage/tokens'
|
||||||
import { getSafeEthToken } from '~/utils/tokens'
|
import { getSafeEthToken } from '~/utils/tokens'
|
||||||
import { enhancedFetch } from '~/utils/fetch'
|
import { enhancedFetch } from '~/utils/fetch'
|
||||||
import addTokens from './addTokens'
|
import addTokens from './addTokens'
|
||||||
|
@ -19,6 +20,15 @@ const createStandardTokenContract = async () => {
|
||||||
|
|
||||||
return erc20Token
|
return erc20Token
|
||||||
}
|
}
|
||||||
|
const createHumanFriendlyTokenContract = async () => {
|
||||||
|
const web3 = getWeb3()
|
||||||
|
const humanErc20Token = await contract(HumanFriendlyToken)
|
||||||
|
humanErc20Token.setProvider(web3.currentProvider)
|
||||||
|
|
||||||
|
return humanErc20Token
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getHumanFriendlyToken = ensureOnce(createHumanFriendlyTokenContract)
|
||||||
|
|
||||||
export const getStandardTokenContract = ensureOnce(createStandardTokenContract)
|
export const getStandardTokenContract = ensureOnce(createStandardTokenContract)
|
||||||
|
|
||||||
|
@ -38,9 +48,9 @@ export const fetchTokensData = async () => {
|
||||||
|
|
||||||
export const fetchTokens = (safeAddress: string) =>
|
export const fetchTokens = (safeAddress: string) =>
|
||||||
async (dispatch: ReduxDispatch<GlobalState>) => {
|
async (dispatch: ReduxDispatch<GlobalState>) => {
|
||||||
const tokens: List<string> = getTokens(safeAddress)
|
const tokens: List<string> = getActiveTokenAddresses(safeAddress)
|
||||||
const ethBalance = await getSafeEthToken(safeAddress)
|
const ethBalance = await getSafeEthToken(safeAddress)
|
||||||
|
const customTokens = getTokens(safeAddress)
|
||||||
const json = await exports.fetchTokensData()
|
const json = await exports.fetchTokensData()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -51,10 +61,18 @@ export const fetchTokens = (safeAddress: string) =>
|
||||||
return makeToken({ ...item, status, funds })
|
return makeToken({ ...item, status, funds })
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const balances: Map<string, Token> = Map().withMutations((map) => {
|
const customTokenRecords = await Promise.all(customTokens.map(async (item: TokenProps) => {
|
||||||
balancesRecords.forEach(record => map.set(record.get('symbol'), record))
|
const status = tokens.includes(item.address)
|
||||||
|
const funds = status ? await calculateBalanceOf(item.address, safeAddress, item.decimals) : '0'
|
||||||
|
|
||||||
map.set('ETH', ethBalance)
|
return makeToken({ ...item, status, funds })
|
||||||
|
}))
|
||||||
|
|
||||||
|
const balances: Map<string, Token> = Map().withMutations((map) => {
|
||||||
|
balancesRecords.forEach(record => map.set(record.get('address'), record))
|
||||||
|
customTokenRecords.forEach(record => map.set(record.get('address'), record))
|
||||||
|
|
||||||
|
map.set(ethBalance.get('address'), ethBalance)
|
||||||
})
|
})
|
||||||
|
|
||||||
return dispatch(addTokens(safeAddress, balances))
|
return dispatch(addTokens(safeAddress, balances))
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { List, Map } from 'immutable'
|
import { List, Map } from 'immutable'
|
||||||
import { handleActions, type ActionType } from 'redux-actions'
|
import { handleActions, type ActionType } from 'redux-actions'
|
||||||
|
import addToken, { ADD_TOKEN } from '~/routes/tokens/store/actions/addToken'
|
||||||
import addTokens, { ADD_TOKENS } from '~/routes/tokens/store/actions/addTokens'
|
import addTokens, { ADD_TOKENS } from '~/routes/tokens/store/actions/addTokens'
|
||||||
import { type Token } from '~/routes/tokens/store/model/token'
|
import { type Token } from '~/routes/tokens/store/model/token'
|
||||||
import disableToken, { DISABLE_TOKEN } from '~/routes/tokens/store/actions/disableToken'
|
import disableToken, { DISABLE_TOKEN } from '~/routes/tokens/store/actions/disableToken'
|
||||||
import enableToken, { ENABLE_TOKEN } from '~/routes/tokens/store/actions/enableToken'
|
import enableToken, { ENABLE_TOKEN } from '~/routes/tokens/store/actions/enableToken'
|
||||||
import { setTokens, getTokens } from '~/utils/localStorage/tokens'
|
import { setActiveTokenAddresses, getActiveTokenAddresses, setToken } from '~/utils/localStorage/tokens'
|
||||||
import { ensureOnce } from '~/utils/singleton'
|
import { ensureOnce } from '~/utils/singleton'
|
||||||
import { calculateActiveErc20TokensFrom } from '~/utils/tokens'
|
import { calculateActiveErc20TokensFrom } from '~/utils/tokens'
|
||||||
|
|
||||||
|
@ -13,7 +14,7 @@ export const TOKEN_REDUCER_ID = 'tokens'
|
||||||
|
|
||||||
export type State = Map<string, Map<string, Token>>
|
export type State = Map<string, Map<string, Token>>
|
||||||
|
|
||||||
const setTokensOnce = ensureOnce(setTokens)
|
const setTokensOnce = ensureOnce(setActiveTokenAddresses)
|
||||||
|
|
||||||
export default handleActions({
|
export default handleActions({
|
||||||
[ADD_TOKENS]: (state: State, action: ActionType<typeof addTokens>): State => {
|
[ADD_TOKENS]: (state: State, action: ActionType<typeof addTokens>): State => {
|
||||||
|
@ -30,21 +31,30 @@ export default handleActions({
|
||||||
return prevSafe.equals(tokens) ? prevSafe : tokens
|
return prevSafe.equals(tokens) ? prevSafe : tokens
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
[ADD_TOKEN]: (state: State, action: ActionType<typeof addToken>): State => {
|
||||||
|
const { safeAddress, token } = action.payload
|
||||||
|
|
||||||
|
const tokenAddress = token.get('address')
|
||||||
|
const activeTokens = getActiveTokenAddresses(safeAddress)
|
||||||
|
setActiveTokenAddresses(safeAddress, activeTokens.push(tokenAddress))
|
||||||
|
setToken(safeAddress, token)
|
||||||
|
return state.setIn([safeAddress, tokenAddress], token)
|
||||||
|
},
|
||||||
[DISABLE_TOKEN]: (state: State, action: ActionType<typeof disableToken>): State => {
|
[DISABLE_TOKEN]: (state: State, action: ActionType<typeof disableToken>): State => {
|
||||||
const { address, safeAddress, symbol } = action.payload
|
const { address, safeAddress } = action.payload
|
||||||
|
|
||||||
const activeTokens = getTokens(safeAddress)
|
const activeTokens = getActiveTokenAddresses(safeAddress)
|
||||||
const index = activeTokens.indexOf(address)
|
const index = activeTokens.indexOf(address)
|
||||||
setTokens(safeAddress, activeTokens.delete(index))
|
setActiveTokenAddresses(safeAddress, activeTokens.delete(index))
|
||||||
|
|
||||||
return state.setIn([safeAddress, symbol, 'status'], false)
|
return state.setIn([safeAddress, address, 'status'], false)
|
||||||
},
|
},
|
||||||
[ENABLE_TOKEN]: (state: State, action: ActionType<typeof enableToken>): State => {
|
[ENABLE_TOKEN]: (state: State, action: ActionType<typeof enableToken>): State => {
|
||||||
const { address, safeAddress, symbol } = action.payload
|
const { address, safeAddress } = action.payload
|
||||||
|
|
||||||
const activeTokens = getTokens(safeAddress)
|
const activeTokens = getActiveTokenAddresses(safeAddress)
|
||||||
setTokens(safeAddress, activeTokens.push(address))
|
setActiveTokenAddresses(safeAddress, activeTokens.push(address))
|
||||||
|
|
||||||
return state.setIn([safeAddress, symbol, 'status'], true)
|
return state.setIn([safeAddress, address, 'status'], true)
|
||||||
},
|
},
|
||||||
}, Map())
|
}, Map())
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { type Token } from '~/routes/tokens/store/model/token'
|
||||||
import { TOKEN_REDUCER_ID } from '~/routes/tokens/store/reducer/tokens'
|
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'
|
||||||
|
import { ETH_ADDRESS } from '~/utils/tokens'
|
||||||
|
|
||||||
describe('Safe - redux balance property', () => {
|
describe('Safe - redux balance property', () => {
|
||||||
let store
|
let store
|
||||||
|
@ -18,7 +19,16 @@ describe('Safe - redux balance property', () => {
|
||||||
|
|
||||||
it('reducer should return 0 to just deployed safe', async () => {
|
it('reducer should return 0 to just deployed safe', async () => {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
const tokenList = ['WE', '<3', 'GNO', 'OMG', 'RDN']
|
const tokenList = [
|
||||||
|
'0x975be7f72cea31fd83d0cb2a197f9136f38696b7', // WE
|
||||||
|
'0xb3a4bc89d8517e0e2c9b66703d09d3029ffa1e6d', // <3
|
||||||
|
'0x5f92161588c6178130ede8cbdc181acec66a9731', // GNO
|
||||||
|
'0xb63d06025d580a94d59801f2513f5d309c079559', // OMG
|
||||||
|
'0x3615757011112560521536258c1E7325Ae3b48AE', // RDN
|
||||||
|
'0xc778417E063141139Fce010982780140Aa0cD5Ab', // Wrapped Ether
|
||||||
|
'0x979861dF79C7408553aAF20c01Cfb3f81CCf9341', // OLY
|
||||||
|
'0', // ETH
|
||||||
|
]
|
||||||
|
|
||||||
// WHEN
|
// WHEN
|
||||||
await store.dispatch(fetchTokensAction.fetchTokens(address))
|
await store.dispatch(fetchTokensAction.fetchTokens(address))
|
||||||
|
@ -29,7 +39,7 @@ describe('Safe - redux balance property', () => {
|
||||||
|
|
||||||
const safeBalances: Map<string, Token> | typeof undefined = tokens.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(8)
|
||||||
|
|
||||||
tokenList.forEach((token: string) => {
|
tokenList.forEach((token: string) => {
|
||||||
const record = safeBalances.get(token)
|
const record = safeBalances.get(token)
|
||||||
|
@ -49,9 +59,9 @@ describe('Safe - redux balance property', () => {
|
||||||
|
|
||||||
const safeBalances: Map<string, Token> | typeof undefined = tokens.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(8)
|
||||||
|
|
||||||
const ethBalance = safeBalances.get('ETH')
|
const ethBalance = safeBalances.get(ETH_ADDRESS)
|
||||||
if (!ethBalance) throw new Error()
|
if (!ethBalance) throw new Error()
|
||||||
expect(ethBalance.get('funds')).toBe('0.03456')
|
expect(ethBalance.get('funds')).toBe('0.03456')
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
// @flow
|
||||||
|
import * as TestUtils from 'react-dom/test-utils'
|
||||||
|
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 { getFirstTokenContract, getSecondTokenContract } 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 } from '~/routes/tokens/store/selectors'
|
||||||
|
import { testToken } from '~/test/builder/tokens.dom.utils'
|
||||||
|
import * as fetchTokensModule from '~/routes/tokens/store/actions/fetchTokens'
|
||||||
|
import * as enhancedFetchModule from '~/utils/fetch'
|
||||||
|
import { clickOnAddToken, fillAddress, fillHumanReadableInfo } from '~/test/utils/tokens/addToken.helper'
|
||||||
|
|
||||||
|
describe('DOM > Feature > Add new ERC 20 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',
|
||||||
|
},
|
||||||
|
]))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds a second erc 20 token filling the form', async () => {
|
||||||
|
// GIVEN
|
||||||
|
const store = aNewStore()
|
||||||
|
const safeAddress = await aMinedSafe(store)
|
||||||
|
await store.dispatch(fetchTokensModule.fetchTokens(safeAddress))
|
||||||
|
const TokensDom = await travelToTokens(store, safeAddress)
|
||||||
|
await sleep(400)
|
||||||
|
const tokens = TestUtils.scryRenderedComponentsWithType(TokensDom, TokenComponent)
|
||||||
|
expect(tokens.length).toBe(2)
|
||||||
|
testToken(tokens[0].props.token, 'FTE', false)
|
||||||
|
testToken(tokens[1].props.token, 'ETH', true)
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
await clickOnAddToken(TokensDom)
|
||||||
|
await fillAddress(TokensDom, secondErc20Token)
|
||||||
|
await fillHumanReadableInfo(TokensDom)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
const match: Match = buildMathPropsFrom(safeAddress)
|
||||||
|
const tokenList = tokenListSelector(store.getState(), { match })
|
||||||
|
expect(tokenList.count()).toBe(3)
|
||||||
|
|
||||||
|
testToken(tokenList.get(0), 'FTE', false)
|
||||||
|
testToken(tokenList.get(1), 'ETH', true)
|
||||||
|
testToken(tokenList.get(2), 'TKN', true)
|
||||||
|
})
|
||||||
|
})
|
|
@ -13,7 +13,7 @@ import { travelToTokens } from '~/test/builder/safe.dom.utils'
|
||||||
import { sleep } from '~/utils/timer'
|
import { sleep } from '~/utils/timer'
|
||||||
import { buildMathPropsFrom } from '~/test/utils/buildReactRouterProps'
|
import { buildMathPropsFrom } from '~/test/utils/buildReactRouterProps'
|
||||||
import { tokenListSelector, activeTokensSelector } from '~/routes/tokens/store/selectors'
|
import { tokenListSelector, activeTokensSelector } from '~/routes/tokens/store/selectors'
|
||||||
import { getTokens } from '~/utils/localStorage/tokens'
|
import { getActiveTokenAddresses } from '~/utils/localStorage/tokens'
|
||||||
import { enableFirstToken, testToken } from '~/test/builder/tokens.dom.utils'
|
import { enableFirstToken, testToken } from '~/test/builder/tokens.dom.utils'
|
||||||
import * as fetchTokensModule from '~/routes/tokens/store/actions/fetchTokens'
|
import * as fetchTokensModule from '~/routes/tokens/store/actions/fetchTokens'
|
||||||
import * as enhancedFetchModule from '~/utils/fetch'
|
import * as enhancedFetchModule from '~/utils/fetch'
|
||||||
|
@ -79,12 +79,16 @@ describe('DOM > Feature > Enable and disable default tokens', () => {
|
||||||
await addTknTo(safeAddress, 50, firstErc20Token)
|
await addTknTo(safeAddress, 50, firstErc20Token)
|
||||||
await addTknTo(safeAddress, 50, secondErc20Token)
|
await addTknTo(safeAddress, 50, secondErc20Token)
|
||||||
await store.dispatch(fetchTokensModule.fetchTokens(safeAddress))
|
await store.dispatch(fetchTokensModule.fetchTokens(safeAddress))
|
||||||
|
|
||||||
|
const match: Match = buildMathPropsFrom(safeAddress)
|
||||||
|
let tokenList = tokenListSelector(store.getState(), { match })
|
||||||
|
expect(tokenList.count()).toBe(3)
|
||||||
|
|
||||||
await enableFirstToken(store, safeAddress)
|
await enableFirstToken(store, safeAddress)
|
||||||
|
tokenList = tokenListSelector(store.getState(), { match })
|
||||||
|
expect(tokenList.count()).toBe(3) // assuring the enableToken do not add extra info
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
const match: Match = buildMathPropsFrom(safeAddress)
|
|
||||||
const tokenList = tokenListSelector(store.getState(), { match })
|
|
||||||
|
|
||||||
testToken(tokenList.get(0), 'FTE', true)
|
testToken(tokenList.get(0), 'FTE', true)
|
||||||
testToken(tokenList.get(1), 'STE', false)
|
testToken(tokenList.get(1), 'STE', false)
|
||||||
testToken(tokenList.get(2), 'ETH', true)
|
testToken(tokenList.get(2), 'ETH', true)
|
||||||
|
@ -108,19 +112,19 @@ describe('DOM > Feature > Enable and disable default tokens', () => {
|
||||||
it('localStorage always returns a list', async () => {
|
it('localStorage always returns a list', async () => {
|
||||||
const store = aNewStore()
|
const store = aNewStore()
|
||||||
const safeAddress = await aMinedSafe(store)
|
const safeAddress = await aMinedSafe(store)
|
||||||
let tokens: List<string> = getTokens(safeAddress)
|
let tokens: List<string> = getActiveTokenAddresses(safeAddress)
|
||||||
expect(tokens).toEqual(List([]))
|
expect(tokens).toEqual(List([]))
|
||||||
|
|
||||||
await store.dispatch(fetchTokensModule.fetchTokens(safeAddress))
|
await store.dispatch(fetchTokensModule.fetchTokens(safeAddress))
|
||||||
tokens = getTokens(safeAddress)
|
tokens = getActiveTokenAddresses(safeAddress)
|
||||||
expect(tokens.count()).toBe(0)
|
expect(tokens.count()).toBe(0)
|
||||||
|
|
||||||
await enableFirstToken(store, safeAddress)
|
await enableFirstToken(store, safeAddress)
|
||||||
tokens = getTokens(safeAddress)
|
tokens = getActiveTokenAddresses(safeAddress)
|
||||||
expect(tokens.count()).toBe(1)
|
expect(tokens.count()).toBe(1)
|
||||||
|
|
||||||
await store.dispatch(fetchTokensModule.fetchTokens(safeAddress))
|
await store.dispatch(fetchTokensModule.fetchTokens(safeAddress))
|
||||||
tokens = getTokens(safeAddress)
|
tokens = getActiveTokenAddresses(safeAddress)
|
||||||
expect(tokens.count()).toBe(1)
|
expect(tokens.count()).toBe(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
// @flow
|
||||||
|
import { getWeb3 } from '~/wallets/getWeb3'
|
||||||
|
import { type Match } from 'react-router-dom'
|
||||||
|
import { promisify } from '~/utils/promisify'
|
||||||
|
import { getFirstTokenContract, getSecondTokenContract } from '~/test/utils/tokenMovements'
|
||||||
|
import { aNewStore } from '~/store'
|
||||||
|
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
|
||||||
|
import { travelToSafe } from '~/test/builder/safe.dom.utils'
|
||||||
|
import { buildMathPropsFrom } from '~/test/utils/buildReactRouterProps'
|
||||||
|
import { testToken } from '~/test/builder/tokens.dom.utils'
|
||||||
|
import * as fetchTokensModule from '~/routes/tokens/store/actions/fetchTokens'
|
||||||
|
import * as enhancedFetchModule from '~/utils/fetch'
|
||||||
|
import { TOKEN_ADRESS_PARAM } from '~/routes/tokens/component/AddToken/FirstPage'
|
||||||
|
import { TOKEN_NAME_PARAM, TOKEN_DECIMALS_PARAM, TOKEN_SYMBOL_PARAM, TOKEN_LOGO_URL_PARAM } from '~/routes/tokens/component/AddToken/SecondPage'
|
||||||
|
import addToken from '~/routes/tokens/store/actions/addToken'
|
||||||
|
import { addTokenFnc } from '~/routes/tokens/component/AddToken'
|
||||||
|
import { activeTokensSelector } from '~/routes/tokens/store/selectors'
|
||||||
|
|
||||||
|
describe('DOM > Feature > Add new ERC 20 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',
|
||||||
|
},
|
||||||
|
]))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('persist added custom ERC 20 tokens as active when reloading the page', async () => {
|
||||||
|
// GIVEN
|
||||||
|
const store = aNewStore()
|
||||||
|
const safeAddress = await aMinedSafe(store)
|
||||||
|
await store.dispatch(fetchTokensModule.fetchTokens(safeAddress))
|
||||||
|
|
||||||
|
const values = {
|
||||||
|
[TOKEN_ADRESS_PARAM]: secondErc20Token.address,
|
||||||
|
[TOKEN_NAME_PARAM]: 'Custom ERC20 Token',
|
||||||
|
[TOKEN_SYMBOL_PARAM]: 'CTS',
|
||||||
|
[TOKEN_DECIMALS_PARAM]: '10',
|
||||||
|
[TOKEN_LOGO_URL_PARAM]: 'https://example.com',
|
||||||
|
}
|
||||||
|
|
||||||
|
const customAddTokensFn: any = (...args) => store.dispatch(addToken(...args))
|
||||||
|
await addTokenFnc(values, customAddTokensFn, safeAddress)
|
||||||
|
travelToSafe(store, safeAddress)
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
const reloadedStore = aNewStore()
|
||||||
|
await reloadedStore.dispatch(fetchTokensModule.fetchTokens(safeAddress))
|
||||||
|
travelToSafe(reloadedStore, safeAddress) // reload
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
const match: Match = buildMathPropsFrom(safeAddress)
|
||||||
|
const activeTokenList = activeTokensSelector(reloadedStore.getState(), { match })
|
||||||
|
expect(activeTokenList.count()).toBe(2)
|
||||||
|
|
||||||
|
testToken(activeTokenList.get(0), 'CTS', true)
|
||||||
|
testToken(activeTokenList.get(1), 'ETH', true)
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,6 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { getGnosisSafeInstanceAt } from '~/wallets/safeContracts'
|
import { getGnosisSafeInstanceAt } from '~/wallets/safeContracts'
|
||||||
import Stepper from '~/components/Stepper'
|
import GnoStepper from '~/components/Stepper'
|
||||||
|
import Stepper from '@material-ui/core/Stepper'
|
||||||
import TestUtils from 'react-dom/test-utils'
|
import TestUtils from 'react-dom/test-utils'
|
||||||
|
|
||||||
export const printOutApprove = async (
|
export const printOutApprove = async (
|
||||||
|
@ -29,9 +30,11 @@ export const printOutApprove = async (
|
||||||
|
|
||||||
const MAX_TIMES_EXECUTED = 35
|
const MAX_TIMES_EXECUTED = 35
|
||||||
const INTERVAL = 500
|
const INTERVAL = 500
|
||||||
|
|
||||||
type FinsihedTx = {
|
type FinsihedTx = {
|
||||||
finishedTransaction: boolean,
|
finishedTransaction: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const whenExecuted = (
|
export const whenExecuted = (
|
||||||
SafeDom: React$Component<any, any>,
|
SafeDom: React$Component<any, any>,
|
||||||
ParentComponent: React$ElementType,
|
ParentComponent: React$ElementType,
|
||||||
|
@ -45,9 +48,9 @@ export const whenExecuted = (
|
||||||
|
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
const SafeComponent = TestUtils.findRenderedComponentWithType(SafeDom, ParentComponent)
|
const SafeComponent = TestUtils.findRenderedComponentWithType(SafeDom, ParentComponent)
|
||||||
type StepperType = React$Component<FinsihedTx, any>
|
type GnoStepperType = React$Component<FinsihedTx, any>
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
const StepperComponent: StepperType = TestUtils.findRenderedComponentWithType(SafeComponent, Stepper)
|
const StepperComponent: GnoStepperType = TestUtils.findRenderedComponentWithType(SafeComponent, GnoStepper)
|
||||||
|
|
||||||
if (StepperComponent.props.finishedTransaction === true) {
|
if (StepperComponent.props.finishedTransaction === true) {
|
||||||
clearInterval(interval)
|
clearInterval(interval)
|
||||||
|
@ -56,3 +59,32 @@ export const whenExecuted = (
|
||||||
times += 1
|
times += 1
|
||||||
}, INTERVAL)
|
}, INTERVAL)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
type MiddleStep = {
|
||||||
|
activeStep: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const whenOnNext = (
|
||||||
|
SafeDom: React$Component<any, any>,
|
||||||
|
ParentComponent: React$ElementType,
|
||||||
|
position: number,
|
||||||
|
): Promise<void> => new Promise((resolve, reject) => {
|
||||||
|
let times = 0
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (times >= MAX_TIMES_EXECUTED) {
|
||||||
|
clearInterval(interval)
|
||||||
|
reject()
|
||||||
|
}
|
||||||
|
|
||||||
|
// $FlowFixMe
|
||||||
|
const SafeComponent = TestUtils.findRenderedComponentWithType(SafeDom, ParentComponent)
|
||||||
|
type StepperType = React$Component<MiddleStep, any>
|
||||||
|
// $FlowFixMe
|
||||||
|
const StepperComponent: StepperType = TestUtils.findRenderedComponentWithType(SafeComponent, Stepper)
|
||||||
|
if (StepperComponent.props.activeStep === position) {
|
||||||
|
clearInterval(interval)
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
times += 1
|
||||||
|
}, INTERVAL)
|
||||||
|
})
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { getProviderInfo, getBalanceInEtherOf, getWeb3 } from '~/wallets/getWeb3
|
||||||
import { promisify } from '~/utils/promisify'
|
import { promisify } from '~/utils/promisify'
|
||||||
import withdraw, { DESTINATION_PARAM, VALUE_PARAM } from '~/routes/safe/component/Withdraw/withdraw'
|
import withdraw, { DESTINATION_PARAM, VALUE_PARAM } from '~/routes/safe/component/Withdraw/withdraw'
|
||||||
import { type Safe } from '~/routes/safe/store/model/safe'
|
import { type Safe } from '~/routes/safe/store/model/safe'
|
||||||
import Token from '#/test/FixedSupplyToken.json'
|
import Token from '#/test/TestToken.json'
|
||||||
import { ensureOnce } from '~/utils/singleton'
|
import { ensureOnce } from '~/utils/singleton'
|
||||||
import { toNative } from '~/wallets/tokens'
|
import { toNative } from '~/wallets/tokens'
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
// @flow
|
||||||
|
import { sleep } from '~/utils/timer'
|
||||||
|
import * as TestUtils from 'react-dom/test-utils'
|
||||||
|
import AddToken from '~/routes/tokens/component/AddToken'
|
||||||
|
import { whenOnNext, whenExecuted } from '~/test/utils/logTransactions'
|
||||||
|
|
||||||
|
export const clickOnAddToken = async (TokensDom: React$Component<any, any>) => {
|
||||||
|
const buttons = TestUtils.scryRenderedDOMComponentsWithTag(TokensDom, 'button')
|
||||||
|
expect(buttons.length).toBe(1)
|
||||||
|
TestUtils.Simulate.click(buttons[0])
|
||||||
|
await sleep(400)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fillAddress = async (TokensDom: React$Component<any, any>, secondErc20Token: any) => {
|
||||||
|
// fill the form
|
||||||
|
const AddTokenComponent = TestUtils.findRenderedComponentWithType(TokensDom, AddToken)
|
||||||
|
if (!AddTokenComponent) throw new Error()
|
||||||
|
|
||||||
|
const inputs = TestUtils.scryRenderedDOMComponentsWithTag(AddTokenComponent, 'input')
|
||||||
|
expect(inputs.length).toBe(1)
|
||||||
|
const tokenAddressInput = inputs[0]
|
||||||
|
TestUtils.Simulate.change(tokenAddressInput, { target: { value: `${secondErc20Token.address}` } })
|
||||||
|
// $FlowFixMe
|
||||||
|
const form = TestUtils.findRenderedDOMComponentWithTag(AddTokenComponent, 'form')
|
||||||
|
// submit it
|
||||||
|
TestUtils.Simulate.submit(form)
|
||||||
|
|
||||||
|
return whenOnNext(TokensDom, AddToken, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fillHumanReadableInfo = async (TokensDom: React$Component<any, any>) => {
|
||||||
|
// fill the form
|
||||||
|
const AddTokenComponent = TestUtils.findRenderedComponentWithType(TokensDom, AddToken)
|
||||||
|
if (!AddTokenComponent) throw new Error()
|
||||||
|
|
||||||
|
const inputs = TestUtils.scryRenderedDOMComponentsWithTag(AddTokenComponent, 'input')
|
||||||
|
expect(inputs.length).toBe(4)
|
||||||
|
TestUtils.Simulate.change(inputs[3], { target: { value: 'https://my.token.image/foo' } })
|
||||||
|
const form = TestUtils.findRenderedDOMComponentWithTag(AddTokenComponent, 'form')
|
||||||
|
|
||||||
|
// submit it
|
||||||
|
TestUtils.Simulate.submit(form)
|
||||||
|
TestUtils.Simulate.submit(form)
|
||||||
|
|
||||||
|
return whenExecuted(TokensDom, AddToken)
|
||||||
|
}
|
|
@ -5,7 +5,6 @@ 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 {
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { List } from 'immutable'
|
import { List } from 'immutable'
|
||||||
import { load, TOKENS_KEY } from '~/utils/localStorage'
|
import { load } from '~/utils/localStorage'
|
||||||
|
import { type Token, type TokenProps } from '~/routes/tokens/store/model/token'
|
||||||
|
|
||||||
|
export const ACTIVE_TOKENS_KEY = 'ACTIVE_TOKENS'
|
||||||
|
export const TOKENS_KEY = 'TOKENS'
|
||||||
|
|
||||||
|
const getActiveTokensKey = (safeAddress: string) => `${ACTIVE_TOKENS_KEY}-${safeAddress}`
|
||||||
const getTokensKey = (safeAddress: string) => `${TOKENS_KEY}-${safeAddress}`
|
const getTokensKey = (safeAddress: string) => `${TOKENS_KEY}-${safeAddress}`
|
||||||
|
|
||||||
export const setTokens = (safeAddress: string, tokens: List<string>) => {
|
export const setActiveTokenAddresses = (safeAddress: string, tokens: List<string>) => {
|
||||||
try {
|
try {
|
||||||
const serializedState = JSON.stringify(tokens)
|
const serializedState = JSON.stringify(tokens)
|
||||||
const key = getTokensKey(safeAddress)
|
const key = getActiveTokensKey(safeAddress)
|
||||||
localStorage.setItem(key, serializedState)
|
localStorage.setItem(key, serializedState)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
|
@ -15,14 +20,34 @@ export const setTokens = (safeAddress: string, tokens: List<string>) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTokens = (safeAddress: string): List<string> => {
|
export const getActiveTokenAddresses = (safeAddress: string): List<string> => {
|
||||||
const key = getTokensKey(safeAddress)
|
const key = getActiveTokensKey(safeAddress)
|
||||||
const data = load(key)
|
const data = load(key)
|
||||||
|
|
||||||
return data ? List(data) : List()
|
return data ? List(data) : List()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const storedTokensBefore = (safeAddress: string) => {
|
export const storedTokensBefore = (safeAddress: string) => {
|
||||||
const key = getTokensKey(safeAddress)
|
const key = getActiveTokensKey(safeAddress)
|
||||||
return localStorage.getItem(key) === null
|
return localStorage.getItem(key) === null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getTokens = (safeAddress: string): List<TokenProps> => {
|
||||||
|
const key = getTokensKey(safeAddress)
|
||||||
|
const data = load(key)
|
||||||
|
|
||||||
|
return data ? List(data) : List()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setToken = (safeAddress: string, token: Token) => {
|
||||||
|
const data: List<TokenProps> = getTokens(safeAddress)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const serializedState = JSON.stringify(data.push(token))
|
||||||
|
const key = getTokensKey(safeAddress)
|
||||||
|
localStorage.setItem(key, serializedState)
|
||||||
|
} catch (err) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.log('Error adding token in localstorage')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import logo from '~/assets/icons/icon_etherTokens.svg'
|
||||||
import { getBalanceInEtherOf } from '~/wallets/getWeb3'
|
import { getBalanceInEtherOf } from '~/wallets/getWeb3'
|
||||||
import { makeToken, type Token } from '~/routes/tokens/store/model/token'
|
import { makeToken, type Token } from '~/routes/tokens/store/model/token'
|
||||||
|
|
||||||
|
export const ETH_ADDRESS = '0'
|
||||||
export const isEther = (symbol: string) => symbol === 'ETH'
|
export const isEther = (symbol: string) => symbol === 'ETH'
|
||||||
|
|
||||||
export const getSafeEthToken = async (safeAddress: string) => {
|
export const getSafeEthToken = async (safeAddress: string) => {
|
||||||
|
|
|
@ -5448,8 +5448,8 @@ fill-range@^4.0.0:
|
||||||
to-regex-range "^2.1.0"
|
to-regex-range "^2.1.0"
|
||||||
|
|
||||||
final-form@^4.2.1:
|
final-form@^4.2.1:
|
||||||
version "4.8.2"
|
version "4.8.3"
|
||||||
resolved "https://registry.yarnpkg.com/final-form/-/final-form-4.8.2.tgz#905de0a5107f889b1eb86dbbdea96f2d40fcb20d"
|
resolved "https://registry.yarnpkg.com/final-form/-/final-form-4.8.3.tgz#86a03da6cd6459ed8fe3737dbd9dc87ed40c11d7"
|
||||||
|
|
||||||
finalhandler@1.1.1:
|
finalhandler@1.1.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
|
@ -9799,8 +9799,8 @@ react-event-listener@^0.6.0:
|
||||||
warning "^4.0.1"
|
warning "^4.0.1"
|
||||||
|
|
||||||
react-final-form@^3.1.2:
|
react-final-form@^3.1.2:
|
||||||
version "3.6.2"
|
version "3.6.4"
|
||||||
resolved "https://registry.yarnpkg.com/react-final-form/-/react-final-form-3.6.2.tgz#fc61b5abfc8e4afb0fd4dea1a4b70cca07aa3e05"
|
resolved "https://registry.yarnpkg.com/react-final-form/-/react-final-form-3.6.4.tgz#1ca37935c2af0bc659a53b293dd84a75d2381548"
|
||||||
|
|
||||||
react-fuzzy@^0.5.2:
|
react-fuzzy@^0.5.2:
|
||||||
version "0.5.2"
|
version "0.5.2"
|
||||||
|
|
Loading…
Reference in New Issue