Commit 71376cf7 authored by Noah Zinsmeister's avatar Noah Zinsmeister

set up eslint and prettier; run prettier

parent da47f33b
{
"semi": false,
"singleQuote": true,
"printWidth": 120
}
...@@ -42,9 +42,18 @@ ...@@ -42,9 +42,18 @@
"build": "react-scripts build", "build": "react-scripts build",
"build:rinkeby": "REACT_APP_NETWORK_ID=4 REACT_APP_NETWORK='Rinkeby Test Network' yarn build", "build:rinkeby": "REACT_APP_NETWORK_ID=4 REACT_APP_NETWORK='Rinkeby Test Network' yarn build",
"test": "react-scripts test --env=jsdom", "test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject" "eject": "react-scripts eject",
"lint:base": "yarn eslint './src/**/*.{js,jsx}'",
"format:base": "yarn prettier './src/**/*.{js,jsx,scss}'",
"lint": "yarn lint:base --fix",
"format": "yarn format:base --write",
"check:lint": "yarn lint:base",
"check:format": "yarn format:base --check",
"check:all": "yarn check:lint && yarn check:format"
},
"devDependencies": {
"prettier": "^1.17.0"
}, },
"devDependencies": {},
"browserslist": [ "browserslist": [
">0.2%", ">0.2%",
"not dead", "not dead",
......
import React, { Component } from 'react'; import React, { Component } from 'react'
import PropTypes from 'prop-types'; import PropTypes from 'prop-types'
import c from 'classnames'; import c from 'classnames'
import QrCode from '../QrCode'; import QrCode from '../QrCode'
import './address-input-panel.scss'; import './address-input-panel.scss'
class AddressInputPanel extends Component { class AddressInputPanel extends Component {
static propTypes = { static propTypes = {
title: PropTypes.string, title: PropTypes.string,
onChange: PropTypes.func, onChange: PropTypes.func,
value: PropTypes.string, value: PropTypes.string,
errorMessage: PropTypes.string, errorMessage: PropTypes.string
}; }
static defaultProps = { static defaultProps = {
onChange() {}, onChange() {},
value: '', value: ''
}; }
render() { render() {
const { const { t, title, onChange, value, errorMessage } = this.props
t,
title,
onChange,
value,
errorMessage,
} = this.props;
return ( return (
<div className="currency-input-panel"> <div className="currency-input-panel">
<div className={c('currency-input-panel__container address-input-panel__recipient-row', { <div
'currency-input-panel__container--error': errorMessage, className={c('currency-input-panel__container address-input-panel__recipient-row', {
})}> 'currency-input-panel__container--error': errorMessage
})}
>
<div className="address-input-panel__input-container"> <div className="address-input-panel__input-container">
<div className="currency-input-panel__label-row"> <div className="currency-input-panel__label-row">
<div className="currency-input-panel__label-container"> <div className="currency-input-panel__label-container">
<span className="currency-input-panel__label">{title || t("recipientAddress")}</span> <span className="currency-input-panel__label">{title || t('recipientAddress')}</span>
</div> </div>
</div> </div>
<div className="currency-input-panel__input-row"> <div className="currency-input-panel__input-row">
<input <input
type="text" type="text"
className={c('address-input-panel__input',{ className={c('address-input-panel__input', {
'address-input-panel__input--error': errorMessage, 'address-input-panel__input--error': errorMessage
})} })}
placeholder="0x1234..." placeholder="0x1234..."
onChange={e => onChange(e.target.value)} onChange={e => onChange(e.target.value)}
...@@ -59,4 +55,4 @@ class AddressInputPanel extends Component { ...@@ -59,4 +55,4 @@ class AddressInputPanel extends Component {
} }
} }
export default AddressInputPanel; export default AddressInputPanel
@import "../../variables.scss"; @import '../../variables.scss';
.contextual-info { .contextual-info {
&__summary-wrapper { &__summary-wrapper {
......
import React, { Component } from 'react'; import React, { Component } from 'react'
import PropTypes from 'prop-types'; import PropTypes from 'prop-types'
import c from 'classnames'; import c from 'classnames'
import DropdownBlue from "../../assets/images/dropdown-blue.svg"; import DropdownBlue from '../../assets/images/dropdown-blue.svg'
import DropupBlue from "../../assets/images/dropup-blue.svg"; import DropupBlue from '../../assets/images/dropup-blue.svg'
import './contextual-info.scss'; import './contextual-info.scss'
class ContextualInfo extends Component { class ContextualInfo extends Component {
static propTypes = { static propTypes = {
openDetailsText: PropTypes.string, openDetailsText: PropTypes.string,
renderTransactionDetails: PropTypes.func, renderTransactionDetails: PropTypes.func,
contextualInfo: PropTypes.string, contextualInfo: PropTypes.string,
isError: PropTypes.bool, isError: PropTypes.bool
}; }
static defaultProps = { static defaultProps = {
openDetailsText: 'Transaction Details', openDetailsText: 'Transaction Details',
closeDetailsText: 'Hide Details', closeDetailsText: 'Hide Details',
renderTransactionDetails() {}, renderTransactionDetails() {},
contextualInfo: '', contextualInfo: '',
isError: false, isError: false
}; }
state = { state = {
showDetails: false, showDetails: false
}; }
renderDetails() { renderDetails() {
if (!this.state.showDetails) { if (!this.state.showDetails) {
return null; return null
} }
return ( return <div className="contextual-info__details">{this.props.renderTransactionDetails()}</div>
<div className="contextual-info__details">
{this.props.renderTransactionDetails()}
</div>
);
} }
render() { render() {
const { const { openDetailsText, closeDetailsText, contextualInfo, isError } = this.props
openDetailsText,
closeDetailsText,
contextualInfo,
isError,
} = this.props;
if (contextualInfo) { if (contextualInfo) {
return ( return (
<div className={c({ 'contextual-info--error': isError }, 'contextual-info__summary-wrapper')}> <div className={c({ 'contextual-info--error': isError }, 'contextual-info__summary-wrapper')}>
<div>{contextualInfo}</div> <div>{contextualInfo}</div>
</div> </div>
); )
} }
return [ return [
<div <div
key="open-details" key="open-details"
className="contextual-info__summary-wrapper contextual-info__open-details-container" className="contextual-info__summary-wrapper contextual-info__open-details-container"
onClick={() => this.setState((prevState) => { onClick={() =>
this.setState(prevState => {
return { showDetails: !prevState.showDetails } return { showDetails: !prevState.showDetails }
})} })
}
> >
{!this.state.showDetails ? ( {!this.state.showDetails ? (
<> <>
<span>{openDetailsText}</span> <span>{openDetailsText}</span>
<img src={DropdownBlue} alt='dropdown' /> <img src={DropdownBlue} alt="dropdown" />
</> </>
) : ( ) : (
<> <>
<span>{closeDetailsText}</span> <span>{closeDetailsText}</span>
<img src={DropupBlue} alt='dropup' /> <img src={DropupBlue} alt="dropup" />
</> </>
)} )}
</div>, </div>,
...@@ -79,4 +72,4 @@ class ContextualInfo extends Component { ...@@ -79,4 +72,4 @@ class ContextualInfo extends Component {
} }
} }
export default ContextualInfo; export default ContextualInfo
...@@ -9,20 +9,20 @@ ...@@ -9,20 +9,20 @@
&__container { &__container {
border-radius: 1.25rem; border-radius: 1.25rem;
box-shadow: 0 0 0 .5px $mercury-gray; box-shadow: 0 0 0 0.5px $mercury-gray;
background-color: $white; background-color: $white;
transition: box-shadow 200ms ease-in-out; transition: box-shadow 200ms ease-in-out;
&--error { &--error {
box-shadow: 0 0 0 .5px $salmon-red; box-shadow: 0 0 0 0.5px $salmon-red;
} }
&:focus-within { &:focus-within {
box-shadow: 0 0 .5px .5px $malibu-blue; box-shadow: 0 0 0.5px 0.5px $malibu-blue;
} }
&--error:focus-within { &--error:focus-within {
box-shadow: 0 0 .5px .5px $salmon-red; box-shadow: 0 0 0.5px 0.5px $salmon-red;
} }
} }
...@@ -30,9 +30,9 @@ ...@@ -30,9 +30,9 @@
@extend %row-nowrap; @extend %row-nowrap;
align-items: center; align-items: center;
color: $dove-gray; color: $dove-gray;
font-size: .75rem; font-size: 0.75rem;
line-height: 1rem; line-height: 1rem;
padding: .75rem 1rem; padding: 0.75rem 1rem;
} }
&__label-container { &__label-container {
...@@ -44,14 +44,14 @@ ...@@ -44,14 +44,14 @@
} }
&__label-description { &__label-description {
opacity: .75; opacity: 0.75;
margin-left: .25rem; margin-left: 0.25rem;
} }
&__input-row { &__input-row {
@extend %row-nowrap; @extend %row-nowrap;
align-items: center; align-items: center;
padding: .25rem .85rem .75rem; padding: 0.25rem 0.85rem 0.75rem;
} }
&__input { &__input {
...@@ -62,7 +62,7 @@ ...@@ -62,7 +62,7 @@
} }
&[type='number'] { &[type='number'] {
-moz-appearance:textfield; -moz-appearance: textfield;
} }
&::-webkit-outer-spin-button, &::-webkit-outer-spin-button,
...@@ -92,14 +92,14 @@ ...@@ -92,14 +92,14 @@
user-select: none; user-select: none;
&:active { &:active {
background-color: rgba($zumthor-blue, .8); background-color: rgba($zumthor-blue, 0.8);
} }
&--selected { &--selected {
background-color: $concrete-gray; background-color: $concrete-gray;
border-color: $mercury-gray; border-color: $mercury-gray;
color: $black; color: $black;
padding: 0 .5rem; padding: 0 0.5rem;
.currency-input-panel__dropdown-icon { .currency-input-panel__dropdown-icon {
background-image: url(../../assets/images/dropdown.svg); background-image: url(../../assets/images/dropdown.svg);
...@@ -128,18 +128,18 @@ ...@@ -128,18 +128,18 @@
user-select: none; user-select: none;
&--pending { &--pending {
line-height: .9; line-height: 0.9;
.loader { .loader {
height: .5rem; height: 0.5rem;
width: .5rem; width: 0.5rem;
} }
} }
} }
&__dropdown-icon { &__dropdown-icon {
height: 1rem; height: 1rem;
width: .75rem; width: 0.75rem;
margin-left: .7rem; margin-left: 0.7rem;
background-image: url(../../assets/images/dropdown-blue.svg); background-image: url(../../assets/images/dropdown-blue.svg);
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: contain; background-size: contain;
...@@ -147,13 +147,12 @@ ...@@ -147,13 +147,12 @@
} }
&__selected-token-logo { &__selected-token-logo {
margin-right: .4rem; margin-right: 0.4rem;
border-radius: 1rem; border-radius: 1rem;
object-fit: contain; object-fit: contain;
} }
} }
.token-modal { .token-modal {
background-color: $white; background-color: $white;
position: relative; position: relative;
...@@ -176,7 +175,7 @@ ...@@ -176,7 +175,7 @@
} }
&__search-icon { &__search-icon {
margin-right: .2rem; margin-right: 0.2rem;
} }
&__token-list { &__token-list {
...@@ -189,7 +188,7 @@ ...@@ -189,7 +188,7 @@
@extend %row-nowrap; @extend %row-nowrap;
align-items: center; align-items: center;
padding: 1rem 1.5rem; padding: 1rem 1.5rem;
margin: .25rem .5rem; margin: 0.25rem 0.5rem;
justify-content: space-between; justify-content: space-between;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
...@@ -235,7 +234,7 @@ ...@@ -235,7 +234,7 @@
justify-content: center; justify-content: center;
background-color: $malibu-blue; background-color: $malibu-blue;
&:hover { &:hover {
background-color: lighten($malibu-blue, 1) background-color: lighten($malibu-blue, 1);
} }
&:active { &:active {
...@@ -253,7 +252,7 @@ ...@@ -253,7 +252,7 @@
color: $dove-gray; color: $dove-gray;
} }
@media only screen and (min-width : 768px) { @media only screen and (min-width: 768px) {
max-width: 560px; max-width: 560px;
max-height: 768px; max-height: 768px;
position: absolute; position: absolute;
...@@ -269,7 +268,6 @@ ...@@ -269,7 +268,6 @@
} }
} }
.token-modal-appear { .token-modal-appear {
bottom: 0; bottom: 0;
} }
......
import React, { Component } from 'react'; import React, { Component } from 'react'
import { connect } from 'react-redux'; import { connect } from 'react-redux'
import PropTypes from 'prop-types'; import PropTypes from 'prop-types'
import { CSSTransitionGroup } from "react-transition-group"; import { CSSTransitionGroup } from 'react-transition-group'
import classnames from 'classnames'; import classnames from 'classnames'
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom'
import { withNamespaces } from 'react-i18next'; import { withNamespaces } from 'react-i18next'
import Fuse from '../../helpers/fuse'; import Fuse from '../../helpers/fuse'
import Modal from '../Modal'; import Modal from '../Modal'
import TokenLogo from '../TokenLogo'; import TokenLogo from '../TokenLogo'
import SearchIcon from '../../assets/images/magnifying-glass.svg'; import SearchIcon from '../../assets/images/magnifying-glass.svg'
import { selectors, addPendingTx } from "../../ducks/web3connect"; import { selectors, addPendingTx } from '../../ducks/web3connect'
import { addApprovalTx } from "../../ducks/pending"; import { addApprovalTx } from '../../ducks/pending'
import { addExchange } from "../../ducks/addresses"; import { addExchange } from '../../ducks/addresses'
import { BigNumber as BN } from 'bignumber.js'; import { BigNumber as BN } from 'bignumber.js'
import './currency-panel.scss'; import './currency-panel.scss'
import ERC20_ABI from '../../abi/erc20'; import ERC20_ABI from '../../abi/erc20'
import FACTORY_ABI from '../../abi/factory'; import FACTORY_ABI from '../../abi/factory'
const FUSE_OPTIONS = { const FUSE_OPTIONS = {
includeMatches: false, includeMatches: false,
threshold: 0.0, threshold: 0.0,
tokenize:true, tokenize: true,
location: 0, location: 0,
distance: 100, distance: 100,
maxPatternLength: 45, maxPatternLength: 45,
minMatchCharLength: 1, minMatchCharLength: 1,
keys: [ keys: [{ name: 'address', weight: 0.8 }, { name: 'label', weight: 0.5 }]
{name:"address",weight:0.8}, }
{name:"label",weight:0.5},
]
};
const TOKEN_ADDRESS_TO_LABEL = { ETH: 'ETH' }; const TOKEN_ADDRESS_TO_LABEL = { ETH: 'ETH' }
class CurrencyInputPanel extends Component { class CurrencyInputPanel extends Component {
static propTypes = { static propTypes = {
...@@ -44,10 +41,10 @@ class CurrencyInputPanel extends Component { ...@@ -44,10 +41,10 @@ class CurrencyInputPanel extends Component {
onCurrencySelected: PropTypes.func, onCurrencySelected: PropTypes.func,
onValueChange: PropTypes.func, onValueChange: PropTypes.func,
tokenAddresses: PropTypes.shape({ tokenAddresses: PropTypes.shape({
addresses: PropTypes.array.isRequired, addresses: PropTypes.array.isRequired
}).isRequired, }).isRequired,
exchangeAddresses: PropTypes.shape({ exchangeAddresses: PropTypes.shape({
fromToken: PropTypes.object.isRequired, fromToken: PropTypes.object.isRequired
}).isRequired, }).isRequired,
factoryAddress: PropTypes.string, factoryAddress: PropTypes.string,
selectedTokens: PropTypes.array.isRequired, selectedTokens: PropTypes.array.isRequired,
...@@ -59,52 +56,52 @@ class CurrencyInputPanel extends Component { ...@@ -59,52 +56,52 @@ class CurrencyInputPanel extends Component {
addExchange: PropTypes.func.isRequired, addExchange: PropTypes.func.isRequired,
filteredTokens: PropTypes.arrayOf(PropTypes.string), filteredTokens: PropTypes.arrayOf(PropTypes.string),
disableUnlock: PropTypes.bool, disableUnlock: PropTypes.bool,
renderInput: PropTypes.func, renderInput: PropTypes.func
}; }
static defaultProps = { static defaultProps = {
selectedTokens: [], selectedTokens: [],
filteredTokens: [], filteredTokens: [],
onCurrencySelected() {}, onCurrencySelected() {},
onValueChange() {}, onValueChange() {},
selectedTokenAddress: '', selectedTokenAddress: ''
}; }
state = { state = {
isShowingModal: false, isShowingModal: false,
searchQuery: '', searchQuery: '',
loadingExchange: false, loadingExchange: false
}; }
createTokenList = () => { createTokenList = () => {
const { filteredTokens } = this.props; const { filteredTokens } = this.props
let tokens = this.props.tokenAddresses.addresses; let tokens = this.props.tokenAddresses.addresses
let tokenList = [ { value: 'ETH', label: 'ETH', address: 'ETH' } ]; let tokenList = [{ value: 'ETH', label: 'ETH', address: 'ETH' }]
for (let i = 0; i < tokens.length; i++) { for (let i = 0; i < tokens.length; i++) {
let entry = { value: '', label: '' }; let entry = { value: '', label: '' }
entry.value = tokens[i][0]; entry.value = tokens[i][0]
entry.label = tokens[i][0]; entry.label = tokens[i][0]
entry.address = tokens[i][1]; entry.address = tokens[i][1]
tokenList.push(entry); tokenList.push(entry)
TOKEN_ADDRESS_TO_LABEL[tokens[i][1]] = tokens[i][0]; TOKEN_ADDRESS_TO_LABEL[tokens[i][1]] = tokens[i][0]
} }
return tokenList.filter(({ address }) => !filteredTokens.includes(address)); return tokenList.filter(({ address }) => !filteredTokens.includes(address))
}; }
onTokenSelect = (address) => { onTokenSelect = address => {
this.setState({ this.setState({
searchQuery: '', searchQuery: '',
isShowingModal: false, isShowingModal: false
}); })
this.props.onCurrencySelected(address); this.props.onCurrencySelected(address)
}; }
renderTokenList() { renderTokenList() {
const tokens = this.createTokenList(); const tokens = this.createTokenList()
const { loadingExchange, searchQuery } = this.state; const { loadingExchange, searchQuery } = this.state
const { const {
t, t,
selectedTokens, selectedTokens,
...@@ -115,8 +112,8 @@ class CurrencyInputPanel extends Component { ...@@ -115,8 +112,8 @@ class CurrencyInputPanel extends Component {
factoryAddress, factoryAddress,
exchangeAddresses: { fromToken }, exchangeAddresses: { fromToken },
addExchange, addExchange,
history, history
} = this.props; } = this.props
if (loadingExchange) { if (loadingExchange) {
return ( return (
...@@ -124,52 +121,52 @@ class CurrencyInputPanel extends Component { ...@@ -124,52 +121,52 @@ class CurrencyInputPanel extends Component {
<div className="loader" /> <div className="loader" />
<div>Searching for Exchange...</div> <div>Searching for Exchange...</div>
</div> </div>
); )
} }
if (web3 && web3.utils && web3.utils.isAddress(searchQuery)) { if (web3 && web3.utils && web3.utils.isAddress(searchQuery)) {
const tokenAddress = searchQuery; const tokenAddress = searchQuery
const { label } = selectors().getBalance(account, tokenAddress); const { label } = selectors().getBalance(account, tokenAddress)
const factory = new web3.eth.Contract(FACTORY_ABI, factoryAddress); const factory = new web3.eth.Contract(FACTORY_ABI, factoryAddress)
const exchangeAddress = fromToken[tokenAddress]; const exchangeAddress = fromToken[tokenAddress]
if (!exchangeAddress) { if (!exchangeAddress) {
this.setState({loadingExchange: true}); this.setState({ loadingExchange: true })
factory.methods.getExchange(tokenAddress).call((err, data) => { factory.methods.getExchange(tokenAddress).call((err, data) => {
if (!err && data !== '0x0000000000000000000000000000000000000000') { if (!err && data !== '0x0000000000000000000000000000000000000000') {
addExchange({ label, tokenAddress, exchangeAddress: data }); addExchange({ label, tokenAddress, exchangeAddress: data })
} }
this.setState({loadingExchange: false}); this.setState({ loadingExchange: false })
}); })
return; return
} }
} }
if (disableTokenSelect) { if (disableTokenSelect) {
return; return
} }
let results; let results
if (!searchQuery) { if (!searchQuery) {
results = tokens; results = tokens
} else { } else {
const fuse = new Fuse(tokens, FUSE_OPTIONS); const fuse = new Fuse(tokens, FUSE_OPTIONS)
results = fuse.search(this.state.searchQuery); results = fuse.search(this.state.searchQuery)
} }
if (!results.length && web3 && web3.utils && web3.utils.isAddress(searchQuery)) { if (!results.length && web3 && web3.utils && web3.utils.isAddress(searchQuery)) {
const { label } = selectors().getBalance(account, searchQuery); const { label } = selectors().getBalance(account, searchQuery)
return [ return [
<div key="token-modal-no-exchange" className="token-modal__token-row token-modal__token-row--no-exchange"> <div key="token-modal-no-exchange" className="token-modal__token-row token-modal__token-row--no-exchange">
<div>{t("noExchange")}</div> <div>{t('noExchange')}</div>
</div>, </div>,
<div <div
key="token-modal-create-exchange" key="token-modal-create-exchange"
className="token-modal__token-row token-modal__token-row--create-exchange" className="token-modal__token-row token-modal__token-row--create-exchange"
onClick={() => { onClick={() => {
this.setState({ isShowingModal: false }); this.setState({ isShowingModal: false })
history.push(`/create-exchange/${searchQuery}`); history.push(`/create-exchange/${searchQuery}`)
}} }}
> >
<div>{`Create exchange for ${label}`}</div> <div>{`Create exchange for ${label}`}</div>
...@@ -180,34 +177,32 @@ class CurrencyInputPanel extends Component { ...@@ -180,34 +177,32 @@ class CurrencyInputPanel extends Component {
if (!results.length) { if (!results.length) {
return ( return (
<div className="token-modal__token-row token-modal__token-row--no-exchange"> <div className="token-modal__token-row token-modal__token-row--no-exchange">
<div>{t("noExchange")}</div> <div>{t('noExchange')}</div>
</div> </div>
) )
} }
return results.map(({ label, address }) => { return results.map(({ label, address }) => {
const isSelected = selectedTokens.indexOf(address) > -1; const isSelected = selectedTokens.indexOf(address) > -1
return ( return (
<div <div
key={label} key={label}
className={ className={classnames('token-modal__token-row', {
classnames('token-modal__token-row', { 'token-modal__token-row--selected': isSelected
'token-modal__token-row--selected': isSelected, })}
})
}
onClick={() => this.onTokenSelect(address)} onClick={() => this.onTokenSelect(address)}
> >
<TokenLogo className="token-modal__token-logo" address={address} /> <TokenLogo className="token-modal__token-logo" address={address} />
<div className="token-modal__token-label" >{label}</div> <div className="token-modal__token-label">{label}</div>
</div> </div>
); )
}); })
} }
renderModal() { renderModal() {
if (!this.state.isShowingModal) { if (!this.state.isShowingModal) {
return null; return null
} }
return ( return (
...@@ -224,21 +219,19 @@ class CurrencyInputPanel extends Component { ...@@ -224,21 +219,19 @@ class CurrencyInputPanel extends Component {
<div className="token-modal__search-container"> <div className="token-modal__search-container">
<input <input
type="text" type="text"
placeholder={this.props.t("searchOrPaste")} placeholder={this.props.t('searchOrPaste')}
className="token-modal__search-input" className="token-modal__search-input"
onChange={e => { onChange={e => {
this.setState({ searchQuery: e.target.value }); this.setState({ searchQuery: e.target.value })
}} }}
/> />
<img src={SearchIcon} className="token-modal__search-icon" alt='search' /> <img src={SearchIcon} className="token-modal__search-icon" alt="search" />
</div>
<div className="token-modal__token-list">
{this.renderTokenList()}
</div> </div>
<div className="token-modal__token-list">{this.renderTokenList()}</div>
</div> </div>
</CSSTransitionGroup> </CSSTransitionGroup>
</Modal> </Modal>
); )
} }
renderUnlockButton() { renderUnlockButton() {
...@@ -254,69 +247,58 @@ class CurrencyInputPanel extends Component { ...@@ -254,69 +247,58 @@ class CurrencyInputPanel extends Component {
pendingApprovals, pendingApprovals,
value, value,
addApprovalTx, addApprovalTx,
addPendingTx, addPendingTx
} = this.props; } = this.props
if (disableUnlock || !selectedTokenAddress || selectedTokenAddress === 'ETH') { if (disableUnlock || !selectedTokenAddress || selectedTokenAddress === 'ETH') {
return; return
} }
const { value: allowance, decimals, label } = selectors().getApprovals(selectedTokenAddress, account, fromToken[selectedTokenAddress]); const { value: allowance, decimals, label } = selectors().getApprovals(
selectedTokenAddress,
if ( account,
!label || fromToken[selectedTokenAddress]
(
allowance.isGreaterThanOrEqualTo(BN((value || 0) * 10 ** decimals)) &&
!BN(allowance).isZero()
) )
) {
return; if (!label || (allowance.isGreaterThanOrEqualTo(BN((value || 0) * 10 ** decimals)) && !BN(allowance).isZero())) {
return
} }
const approvalTxId = pendingApprovals[selectedTokenAddress]; const approvalTxId = pendingApprovals[selectedTokenAddress]
if (approvalTxId && transactions.pending.includes(approvalTxId)) { if (approvalTxId && transactions.pending.includes(approvalTxId)) {
return ( return (
<button <button className="currency-input-panel__sub-currency-select currency-input-panel__sub-currency-select--pending">
className='currency-input-panel__sub-currency-select currency-input-panel__sub-currency-select--pending'
>
<div className="loader" /> <div className="loader" />
{t("pending")} {t('pending')}
</button> </button>
); )
} }
return ( return (
<button <button
className='currency-input-panel__sub-currency-select' className="currency-input-panel__sub-currency-select"
onClick={() => { onClick={() => {
const contract = new web3.eth.Contract(ERC20_ABI, selectedTokenAddress); const contract = new web3.eth.Contract(ERC20_ABI, selectedTokenAddress)
const amount = BN(10 ** decimals).multipliedBy(10 ** 8).toFixed(0); const amount = BN(10 ** decimals)
contract.methods.approve(fromToken[selectedTokenAddress], amount) .multipliedBy(10 ** 8)
.send({ from: account }, (err, data) => { .toFixed(0)
contract.methods.approve(fromToken[selectedTokenAddress], amount).send({ from: account }, (err, data) => {
if (!err && data) { if (!err && data) {
addPendingTx(data); addPendingTx(data)
addApprovalTx({ tokenAddress: selectedTokenAddress, txId: data}); addApprovalTx({ tokenAddress: selectedTokenAddress, txId: data })
} }
}); })
}} }}
> >
{t("unlock")} {t('unlock')}
</button> </button>
); )
} }
renderInput() { renderInput() {
const { const { t, errorMessage, value, onValueChange, selectedTokenAddress, disableTokenSelect, renderInput } = this.props
t,
errorMessage,
value,
onValueChange,
selectedTokenAddress,
disableTokenSelect,
renderInput,
} = this.props;
if (typeof renderInput === 'function') { if (typeof renderInput === 'function') {
return renderInput(); return renderInput()
} }
return ( return (
...@@ -324,72 +306,64 @@ class CurrencyInputPanel extends Component { ...@@ -324,72 +306,64 @@ class CurrencyInputPanel extends Component {
<input <input
type="number" type="number"
min="0" min="0"
className={classnames('currency-input-panel__input',{ className={classnames('currency-input-panel__input', {
'currency-input-panel__input--error': errorMessage, 'currency-input-panel__input--error': errorMessage
})} })}
placeholder="0.0" placeholder="0.0"
onChange={e => onValueChange(e.target.value)} onChange={e => onValueChange(e.target.value)}
onKeyPress={e => { onKeyPress={e => {
const charCode = e.which ? e.which : e.keyCode; const charCode = e.which ? e.which : e.keyCode
// Prevent 'minus' character // Prevent 'minus' character
if (charCode === 45) { if (charCode === 45) {
e.preventDefault(); e.preventDefault()
e.stopPropagation(); e.stopPropagation()
} }
}} }}
value={value} value={value}
/> />
{ this.renderUnlockButton() } {this.renderUnlockButton()}
<button <button
className={classnames("currency-input-panel__currency-select", { className={classnames('currency-input-panel__currency-select', {
'currency-input-panel__currency-select--selected': selectedTokenAddress, 'currency-input-panel__currency-select--selected': selectedTokenAddress,
'currency-input-panel__currency-select--disabled': disableTokenSelect, 'currency-input-panel__currency-select--disabled': disableTokenSelect
})} })}
onClick={() => { onClick={() => {
if (!disableTokenSelect) { if (!disableTokenSelect) {
this.setState({ isShowingModal: true }); this.setState({ isShowingModal: true })
} }
}} }}
> >
{ {selectedTokenAddress ? (
selectedTokenAddress <TokenLogo className="currency-input-panel__selected-token-logo" address={selectedTokenAddress} />
? ( ) : null}
<TokenLogo {TOKEN_ADDRESS_TO_LABEL[selectedTokenAddress] || t('selectToken')}
className="currency-input-panel__selected-token-logo"
address={selectedTokenAddress}
/>
)
: null
}
{ TOKEN_ADDRESS_TO_LABEL[selectedTokenAddress] || t("selectToken") }
<span className="currency-input-panel__dropdown-icon" /> <span className="currency-input-panel__dropdown-icon" />
</button> </button>
</div> </div>
); )
} }
render() { render() {
const { const { title, description, extraText, errorMessage } = this.props
title,
description,
extraText,
errorMessage,
} = this.props;
return ( return (
<div className="currency-input-panel"> <div className="currency-input-panel">
<div className={classnames('currency-input-panel__container', { <div
'currency-input-panel__container--error': errorMessage, className={classnames('currency-input-panel__container', {
})}> 'currency-input-panel__container--error': errorMessage
})}
>
<div className="currency-input-panel__label-row"> <div className="currency-input-panel__label-row">
<div className="currency-input-panel__label-container"> <div className="currency-input-panel__label-container">
<span className="currency-input-panel__label">{title}</span> <span className="currency-input-panel__label">{title}</span>
<span className="currency-input-panel__label-description">{description}</span> <span className="currency-input-panel__label-description">{description}</span>
</div> </div>
<span className={classnames('currency-input-panel__extra-text', { <span
'currency-input-panel__extra-text--error': errorMessage, className={classnames('currency-input-panel__extra-text', {
})}> 'currency-input-panel__extra-text--error': errorMessage
})}
>
{extraText} {extraText}
</span> </span>
</div> </div>
...@@ -412,13 +386,13 @@ export default withRouter( ...@@ -412,13 +386,13 @@ export default withRouter(
approvals: state.web3connect.approvals, approvals: state.web3connect.approvals,
transactions: state.web3connect.transactions, transactions: state.web3connect.transactions,
web3: state.web3connect.web3, web3: state.web3connect.web3,
pendingApprovals: state.pending.approvals, pendingApprovals: state.pending.approvals
}), }),
dispatch => ({ dispatch => ({
selectors: () => dispatch(selectors()), selectors: () => dispatch(selectors()),
addExchange: opts => dispatch(addExchange(opts)), addExchange: opts => dispatch(addExchange(opts)),
addPendingTx: opts => dispatch(addPendingTx(opts)), addPendingTx: opts => dispatch(addPendingTx(opts)),
addApprovalTx: opts => dispatch(addApprovalTx(opts)), addApprovalTx: opts => dispatch(addApprovalTx(opts))
}), })
)(withNamespaces()(CurrencyInputPanel)) )(withNamespaces()(CurrencyInputPanel))
); )
@import "../../variables.scss"; @import '../../variables.scss';
.header { .header {
@extend %col-nowrap; @extend %col-nowrap;
&__top { &__top {
@extend %row-nowrap; @extend %row-nowrap;
padding: 1.25rem .75rem; padding: 1.25rem 0.75rem;
align-items: center; align-items: center;
border-bottom: 1px solid $concrete-gray; border-bottom: 1px solid $concrete-gray;
} }
...@@ -28,22 +28,22 @@ ...@@ -28,22 +28,22 @@
} }
&--inactive { &--inactive {
opacity: .5; opacity: 0.5;
} }
&__dialog { &__dialog {
@extend %col-nowrap; @extend %col-nowrap;
border-radius: .875rem; border-radius: 0.875rem;
border: 1px solid $mercury-gray; border: 1px solid $mercury-gray;
margin: 1rem .75rem 0 .75rem; margin: 1rem 0.75rem 0 0.75rem;
padding: 1.5rem 1rem; padding: 1.5rem 1rem;
text-align: center; text-align: center;
display: none; display: none;
&__description { &__description {
font-size: .75rem; font-size: 0.75rem;
color: $dove-gray; color: $dove-gray;
margin-top: .4rem; margin-top: 0.4rem;
} }
&--disconnected { &--disconnected {
...@@ -68,7 +68,7 @@ ...@@ -68,7 +68,7 @@
} }
} }
@media only screen and (min-width : 768px) { @media only screen and (min-width: 768px) {
//position: fixed; //position: fixed;
top: 0px; top: 0px;
left: 0px; left: 0px;
......
import React, { Component } from 'react'; import React, { Component } from 'react'
import PropTypes from 'prop-types'; import PropTypes from 'prop-types'
import { connect } from 'react-redux'; import { connect } from 'react-redux'
import classnames from 'classnames'; import classnames from 'classnames'
import UAParser from 'ua-parser-js'; import UAParser from 'ua-parser-js'
import { withNamespaces } from 'react-i18next'; import { withNamespaces } from 'react-i18next'
import Logo from '../Logo'; import Logo from '../Logo'
import CoinbaseWalletLogo from '../../assets/images/coinbase-wallet-logo.png'; import CoinbaseWalletLogo from '../../assets/images/coinbase-wallet-logo.png'
import TrustLogo from '../../assets/images/trust-wallet-logo.svg'; import TrustLogo from '../../assets/images/trust-wallet-logo.svg'
import BraveLogo from '../../assets/images/brave-logo.svg'; import BraveLogo from '../../assets/images/brave-logo.svg'
import MetamaskLogo from '../../assets/images/metamask-logo.svg'; import MetamaskLogo from '../../assets/images/metamask-logo.svg'
import Web3Status from '../Web3Status'; import Web3Status from '../Web3Status'
import "./header.scss"; import './header.scss'
const links = { const links = {
coinbaseWallet: { coinbaseWallet: {
...@@ -19,135 +19,132 @@ const links = { ...@@ -19,135 +19,132 @@ const links = {
ios: 'https://itunes.apple.com/us/app/coinbase-wallet/id1278383455' ios: 'https://itunes.apple.com/us/app/coinbase-wallet/id1278383455'
}, },
trust: { trust: {
android: 'https://links.trustwalletapp.com/a/key_live_lfvIpVeI9TFWxPCqwU8rZnogFqhnzs4D?&event=openURL&url=https://uniswap.exchange/swap', android:
ios: 'https://links.trustwalletapp.com/a/key_live_lfvIpVeI9TFWxPCqwU8rZnogFqhnzs4D?&event=openURL&url=https://uniswap.exchange/swap', 'https://links.trustwalletapp.com/a/key_live_lfvIpVeI9TFWxPCqwU8rZnogFqhnzs4D?&event=openURL&url=https://uniswap.exchange/swap',
ios:
'https://links.trustwalletapp.com/a/key_live_lfvIpVeI9TFWxPCqwU8rZnogFqhnzs4D?&event=openURL&url=https://uniswap.exchange/swap'
}, },
metamask: { metamask: {
chrome: 'https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn', chrome: 'https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn'
}, },
brave: { brave: {
android: 'https://play.google.com/store/apps/details?id=com.brave.browser', android: 'https://play.google.com/store/apps/details?id=com.brave.browser',
ios: 'https://itunes.apple.com/us/app/brave-browser-fast-adblocker/id1052879175', ios: 'https://itunes.apple.com/us/app/brave-browser-fast-adblocker/id1052879175'
}, }
}; }
const ua = new UAParser(window.navigator.userAgent); const ua = new UAParser(window.navigator.userAgent)
function getTrustLink() { function getTrustLink() {
const os = ua.getOS(); const os = ua.getOS()
if (os.name === 'Android') { if (os.name === 'Android') {
return links.trust.android; return links.trust.android
} }
if (os.name === 'iOS') { if (os.name === 'iOS') {
return links.trust.ios; return links.trust.ios
} }
} }
function getCoinbaseWalletLink() { function getCoinbaseWalletLink() {
const os = ua.getOS(); const os = ua.getOS()
if (os.name === 'Android') { if (os.name === 'Android') {
return links.coinbaseWallet.android; return links.coinbaseWallet.android
} }
if (os.name === 'iOS') { if (os.name === 'iOS') {
return links.coinbaseWallet.ios; return links.coinbaseWallet.ios
} }
} }
function getBraveLink() { function getBraveLink() {
const os = ua.getOS(); const os = ua.getOS()
if (os.name === 'Mac OS') { if (os.name === 'Mac OS') {
return links.brave.ios; return links.brave.ios
} }
return links.brave.android; return links.brave.android
} }
function getMetamaskLink() { function getMetamaskLink() {
return links.metamask.chrome; return links.metamask.chrome
} }
function isMobile() { function isMobile() {
return ua.getDevice().type === 'mobile'; return ua.getDevice().type === 'mobile'
} }
class BlockingWarning extends Component { class BlockingWarning extends Component {
render () { render() {
const { const { t, isConnected, initialized, networkId } = this.props
t, let content = []
isConnected,
initialized,
networkId,
} = this.props;
let content = [];
const correctNetworkId = process.env.REACT_APP_NETWORK_ID || 1; const correctNetworkId = process.env.REACT_APP_NETWORK_ID || 1
const correctNetwork = process.env.REACT_APP_NETWORK || 'Main Ethereum Network'; const correctNetwork = process.env.REACT_APP_NETWORK || 'Main Ethereum Network'
const wrongNetwork = networkId !== correctNetworkId; const wrongNetwork = networkId !== correctNetworkId
if (wrongNetwork && initialized) { if (wrongNetwork && initialized) {
content = [ content = [
<div key="warning-title">{t("wrongNetwork")}</div>, <div key="warning-title">{t('wrongNetwork')}</div>,
<div key="warning-desc" className="header__dialog__description"> <div key="warning-desc" className="header__dialog__description">
{t("switchNetwork", {correctNetwork})} {t('switchNetwork', { correctNetwork })}
</div>, </div>
]; ]
} }
if (!isConnected && initialized) { if (!isConnected && initialized) {
content = [ content = [
<div key="warning-title">{t("noWallet")}</div>, <div key="warning-title">{t('noWallet')}</div>,
<div key="warning-desc" className="header__dialog__description"> <div key="warning-desc" className="header__dialog__description">
{ {isMobile() ? t('installWeb3MobileBrowser') : t('installMetamask')}
isMobile()
? t("installWeb3MobileBrowser")
: t("installMetamask")
}
</div>, </div>,
<div key="warning-logos" className="header__download"> <div key="warning-logos" className="header__download">
{ {isMobile()
isMobile() ? [
? ( <img
[ alt="coinbase"
<img alt='coinbase' src={CoinbaseWalletLogo} key="coinbase-wallet" onClick={() => window.open(getCoinbaseWalletLink(), '_blank')} />, src={CoinbaseWalletLogo}
<img alt='trust' src={TrustLogo} key="trust" onClick={() => window.open(getTrustLink(), '_blank')} /> key="coinbase-wallet"
onClick={() => window.open(getCoinbaseWalletLink(), '_blank')}
/>,
<img alt="trust" src={TrustLogo} key="trust" onClick={() => window.open(getTrustLink(), '_blank')} />
] ]
) : [
: ( <img
[ alt="metamask"
<img alt='metamask' src={MetamaskLogo} key="metamask" onClick={() => window.open(getMetamaskLink(), '_blank')} />, src={MetamaskLogo}
<img alt='brave' src={BraveLogo} key="brave" onClick={() => window.open(getBraveLink(), '_blank')} /> key="metamask"
onClick={() => window.open(getMetamaskLink(), '_blank')}
/>,
<img alt="brave" src={BraveLogo} key="brave" onClick={() => window.open(getBraveLink(), '_blank')} />
]}
</div>
] ]
)
}
</div>,
];
} }
return ( return (
<div <div
className={classnames('header__dialog', { className={classnames('header__dialog', {
'header__dialog--disconnected': (!isConnected || wrongNetwork) && initialized, 'header__dialog--disconnected': (!isConnected || wrongNetwork) && initialized
})} })}
> >
{content} {content}
</div> </div>
); )
} }
} }
function Header (props) { function Header(props) {
return ( return (
<div className="header"> <div className="header">
<BlockingWarning {...props} /> <BlockingWarning {...props} />
<div <div
className={classnames('header__top', { className={classnames('header__top', {
'header--inactive': !props.isConnected, 'header--inactive': !props.isConnected
})} })}
> >
<Logo /> <Logo />
...@@ -162,15 +159,13 @@ function Header (props) { ...@@ -162,15 +159,13 @@ function Header (props) {
Header.propTypes = { Header.propTypes = {
currentAddress: PropTypes.string, currentAddress: PropTypes.string,
isConnected: PropTypes.bool.isRequired, isConnected: PropTypes.bool.isRequired
}; }
export default connect( export default connect(state => ({
state => ({
currentAddress: state.web3connect.account, currentAddress: state.web3connect.account,
initialized: state.web3connect.initialized, initialized: state.web3connect.initialized,
isConnected: !!state.web3connect.account, isConnected: !!state.web3connect.account,
web3: state.web3connect.web3, web3: state.web3connect.web3,
networkId: state.web3connect.networkId, networkId: state.web3connect.networkId
}), }))(withNamespaces()(Header))
)(withNamespaces()(Header));
import React from 'react'; import React from 'react'
import "./logo.scss"; import './logo.scss'
export default function Logo(props) { export default function Logo(props) {
return ( return (
<div className="logo"> <div className="logo">
<span role="img" aria-label="logo">🦄</span> <span role="img" aria-label="logo">
🦄
</span>
</div> </div>
); )
} }
import React, { Component } from 'react'; import React, { Component } from 'react'
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'; import PropTypes from 'prop-types'
import { CSSTransitionGroup } from 'react-transition-group'; import { CSSTransitionGroup } from 'react-transition-group'
import './modal.scss'; import './modal.scss'
const modalRoot = document.querySelector('#modal-root'); const modalRoot = document.querySelector('#modal-root')
export default class Modal extends Component { export default class Modal extends Component {
static propTypes = { static propTypes = {
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired
}; }
componentDidMount() { componentDidMount() {
// The portal element is inserted in the DOM tree after // The portal element is inserted in the DOM tree after
...@@ -28,7 +28,7 @@ export default class Modal extends Component { ...@@ -28,7 +28,7 @@ export default class Modal extends Component {
setTimeout(() => { setTimeout(() => {
// modalRoot.style.display = 'none'; // modalRoot.style.display = 'none';
// modalRoot.removeChild(this.el); // modalRoot.removeChild(this.el);
}, 500); }, 500)
} }
render() { render() {
...@@ -46,7 +46,7 @@ export default class Modal extends Component { ...@@ -46,7 +46,7 @@ export default class Modal extends Component {
</CSSTransitionGroup> </CSSTransitionGroup>
{this.props.children} {this.props.children}
</div>, </div>,
modalRoot, modalRoot
); )
} }
} }
...@@ -4,9 +4,8 @@ ...@@ -4,9 +4,8 @@
position: relative; position: relative;
height: 100vh; height: 100vh;
width: 100vw; width: 100vw;
background-color: rgba($black, .6); background-color: rgba($black, 0.6);
z-index: 1000; z-index: 1000;
} }
.modal-container-appear { .modal-container-appear {
......
@import "../../variables"; @import '../../variables';
.beta-message { .beta-message {
@extend %row-nowrap; @extend %row-nowrap;
flex: 1 0 auto; flex: 1 0 auto;
align-items: center; align-items: center;
position: relative; position: relative;
padding: .5rem 1rem; padding: 0.5rem 1rem;
margin-bottom: 1rem; margin-bottom: 1rem;
border: 1px solid rgba($wisteria-purple, .4); border: 1px solid rgba($wisteria-purple, 0.4);
background-color: rgba($wisteria-purple, .1); background-color: rgba($wisteria-purple, 0.1);
border-radius: 2rem; border-radius: 2rem;
font-size: .75rem; font-size: 0.75rem;
line-height: 1rem; line-height: 1rem;
text-align: center; text-align: center;
color: $wisteria-purple; color: $wisteria-purple;
&:after { &:after {
content: '✕'; content: '✕';
top: .5rem; top: 0.5rem;
right: 1rem; right: 1rem;
position: absolute; position: absolute;
color: $wisteria-purple; color: $wisteria-purple;
......
import React, { Component } from 'react'; import React, { Component } from 'react'
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom'
import { connect } from 'react-redux'; import { connect } from 'react-redux'
import PropTypes from 'prop-types'; import PropTypes from 'prop-types'
import { withNamespaces } from 'react-i18next'; import { withNamespaces } from 'react-i18next'
import { dismissBetaMessage } from '../../ducks/app'; import { dismissBetaMessage } from '../../ducks/app'
import {Tab, Tabs} from "../Tab"; import { Tab, Tabs } from '../Tab'
import './beta-message.scss'; import './beta-message.scss'
class NavigationTabs extends Component { class NavigationTabs extends Component {
static propTypes = { static propTypes = {
history: PropTypes.shape({ history: PropTypes.shape({
push: PropTypes.func.isRequired, push: PropTypes.func.isRequired
}), }),
className: PropTypes.string, className: PropTypes.string,
dismissBetaMessage: PropTypes.func.isRequired, dismissBetaMessage: PropTypes.func.isRequired,
showBetaMessage: PropTypes.bool.isRequired, showBetaMessage: PropTypes.bool.isRequired
}; }
constructor(props) { constructor(props) {
super(props); super(props)
this.state = { this.state = {
selectedPath: this.props.location.pathname, selectedPath: this.props.location.pathname,
className: '', className: '',
showWarning: true, showWarning: true
}; }
} }
renderTab(name, path, regex) { renderTab(name, path, regex) {
const { push } = this.props.history; const { push } = this.props.history
return ( return <Tab text={name} onClick={() => push(path)} isSelected={regex.test(this.props.location.pathname)} />
<Tab
text={name}
onClick={() => push(path)}
isSelected={regex.test(this.props.location.pathname)}
/>
)
} }
render() { render() {
const { t, showBetaMessage, className, dismissBetaMessage } = this.props; const { t, showBetaMessage, className, dismissBetaMessage } = this.props
return ( return (
<div> <div>
<Tabs className={className}> <Tabs className={className}>
{ this.renderTab(t("swap"), '/swap', /swap/) } {this.renderTab(t('swap'), '/swap', /swap/)}
{ this.renderTab(t("send"), '/send', /send/) } {this.renderTab(t('send'), '/send', /send/)}
{ this.renderTab(t("pool"), '/add-liquidity', /add-liquidity|remove-liquidity|create-exchange/) } {this.renderTab(t('pool'), '/add-liquidity', /add-liquidity|remove-liquidity|create-exchange/)}
</Tabs> </Tabs>
{ {showBetaMessage && (
showBetaMessage && (
<div className="beta-message" onClick={dismissBetaMessage}> <div className="beta-message" onClick={dismissBetaMessage}>
<span role='img' aria-label='warning'>💀</span> {t("betaWarning")} <span role="img" aria-label="warning">
💀
</span>{' '}
{t('betaWarning')}
</div> </div>
) )}
}
</div> </div>
); )
} }
} }
export default withRouter( export default withRouter(
connect( connect(
state => ({ state => ({
showBetaMessage: state.app.showBetaMessage, showBetaMessage: state.app.showBetaMessage
}), }),
dispatch => ({ dispatch => ({
dismissBetaMessage: () => dispatch(dismissBetaMessage()), dismissBetaMessage: () => dispatch(dismissBetaMessage())
}), })
)(withNamespaces()(NavigationTabs)) )(withNamespaces()(NavigationTabs))
); )
import React from 'react'; import React from 'react'
import './oversized-panel.scss'; import './oversized-panel.scss'
export default function OversizedPanel(props) { export default function OversizedPanel(props) {
return ( return (
<div className="oversized-panel"> <div className="oversized-panel">
{ props.hideTop || <div className="oversized-panel__top" /> } {props.hideTop || <div className="oversized-panel__top" />}
{props.children} {props.children}
{ props.hideBottom || <div className="oversized-panel__bottom" /> } {props.hideBottom || <div className="oversized-panel__bottom" />}
</div> </div>
); )
} }
...@@ -5,12 +5,12 @@ ...@@ -5,12 +5,12 @@
background-color: $concrete-gray; background-color: $concrete-gray;
width: calc(100% - 1rem); width: calc(100% - 1rem);
margin: 0 auto; margin: 0 auto;
border-radius: .625rem; border-radius: 0.625rem;
&__top { &__top {
content: ""; content: '';
position: absolute; position: absolute;
top: -.5rem; top: -0.5rem;
left: 0; left: 0;
height: 1rem; height: 1rem;
width: 100%; width: 100%;
......
import React, { Component } from 'react'; import React, { Component } from 'react'
import PropTypes from 'prop-types'; import PropTypes from 'prop-types'
import { CSSTransitionGroup } from "react-transition-group"; import { CSSTransitionGroup } from 'react-transition-group'
import Modal from '../Modal'; import Modal from '../Modal'
import QrCodeSVG from '../../assets/images/qr-code.svg'; import QrCodeSVG from '../../assets/images/qr-code.svg'
import QrScanner from '../../libraries/qr-scanner'; import QrScanner from '../../libraries/qr-scanner'
import './qr-code.scss'; import './qr-code.scss'
class QrCode extends Component { class QrCode extends Component {
static propTypes = { static propTypes = {
onValueReceived: PropTypes.func, onValueReceived: PropTypes.func
}; }
static defaultProps = { static defaultProps = {
onValueReceived() {}, onValueReceived() {}
}; }
state = { state = {
videoOpen: false, videoOpen: false,
stream: null, stream: null
}; }
componentDidUpdate() { componentDidUpdate() {
const { videoOpen, stream } = this.state; const { videoOpen, stream } = this.state
if (videoOpen && !stream && this.videoRef) { if (videoOpen && !stream && this.videoRef) {
this.startStreamingVideo(this.videoRef) this.startStreamingVideo(this.videoRef)
} else if (!videoOpen && stream) { } else if (!videoOpen && stream) {
this.setState({stream: null}); this.setState({ stream: null })
} }
} }
startStreamingVideo(videoRef) { startStreamingVideo(videoRef) {
if (navigator && navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { if (navigator && navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia({video: { facingMode: 'user'}, audio: false}) navigator.mediaDevices
.then((stream) => { .getUserMedia({ video: { facingMode: 'user' }, audio: false })
videoRef.srcObject = stream; .then(stream => {
new QrScanner(videoRef, (val) => { videoRef.srcObject = stream
this.closeVideo(); new QrScanner(videoRef, val => {
this.props.onValueReceived(val); this.closeVideo()
this.props.onValueReceived(val)
}) })
this.setState({ this.setState({
stream: stream.getTracks()[0] stream: stream.getTracks()[0]
});
}) })
.catch((error) => { })
this.closeVideo(); .catch(error => {
console.error(error); this.closeVideo()
}); console.error(error)
})
} }
} }
openVideo = () => { openVideo = () => {
this.setState({videoOpen: true}); this.setState({ videoOpen: true })
} }
closeVideo = () => { closeVideo = () => {
if (this.state.stream) { if (this.state.stream) {
this.state.stream.stop(); this.state.stream.stop()
}
this.setState({ videoOpen: false, stream: null })
this.videoRef = null
} }
this.setState({videoOpen: false, stream: null});
this.videoRef = null;
};
setVideoRef = (element) => { setVideoRef = element => {
this.videoRef = element; this.videoRef = element
} }
renderQrReader() { renderQrReader() {
...@@ -80,31 +81,29 @@ class QrCode extends Component { ...@@ -80,31 +81,29 @@ class QrCode extends Component {
transitionEnterTimeout={200} transitionEnterTimeout={200}
> >
<div className="qr-code__modal"> <div className="qr-code__modal">
<video <video playsInline muted autoPlay ref={this.setVideoRef} className="qr-code__video" />
playsInline
muted
autoPlay
ref={this.setVideoRef}
className="qr-code__video"
>
</video>
</div> </div>
</CSSTransitionGroup> </CSSTransitionGroup>
</Modal> </Modal>
); )
} }
return null; return null
} }
render() { render() {
return [ return [
<img key="icon" src={QrCodeSVG} alt='code' onClick={() => { <img
this.state.videoOpen ? this.closeVideo() : this.openVideo(); key="icon"
}} />, src={QrCodeSVG}
alt="code"
onClick={() => {
this.state.videoOpen ? this.closeVideo() : this.openVideo()
}}
/>,
this.renderQrReader() this.renderQrReader()
] ]
} }
} }
export default QrCode; export default QrCode
.qr-code { .qr-code {
&__video { &__video {
height: 100%; height: 100%;
......
import React from 'react'; import React from 'react'
import PropTypes from 'prop-types'; import PropTypes from 'prop-types'
import classnames from 'classnames'; import classnames from 'classnames'
import './tab.scss'; import './tab.scss'
export const Tabs = props => { export const Tabs = props => {
return ( return <div className={classnames('tabs', props.className)}>{props.children}</div>
<div className={classnames("tabs", props.className)}> }
{ props.children }
</div>
);
};
export const Tab = props => { export const Tab = props => {
return ( return (
<div <div
className={classnames("tab", { className={classnames('tab', {
'tab--selected': props.isSelected, 'tab--selected': props.isSelected
})} })}
onClick={props.onClick} onClick={props.onClick}
> >
{ props.text ? <span>{props.text}</span> : null } {props.text ? <span>{props.text}</span> : null}
</div> </div>
); )
}; }
Tab.propTypes = { Tab.propTypes = {
className: PropTypes.string, className: PropTypes.string,
text: PropTypes.string, text: PropTypes.string,
isSelected: PropTypes.bool, isSelected: PropTypes.bool,
onClick: PropTypes.func, onClick: PropTypes.func
}; }
Tab.defaultProps = { Tab.defaultProps = {
className: '', className: ''
}; }
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
height: 2.5rem; height: 2.5rem;
background-color: $concrete-gray; background-color: $concrete-gray;
border-radius: 3rem; border-radius: 3rem;
box-shadow: 0 0 0 .5px darken($concrete-gray, 5); box-shadow: 0 0 0 0.5px darken($concrete-gray, 5);
.tab:first-child { .tab:first-child {
//margin-left: -1px; //margin-left: -1px;
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
&--selected { &--selected {
background-color: $white; background-color: $white;
border-radius: 3rem; border-radius: 3rem;
box-shadow: 0 0 .5px .5px $mercury-gray; box-shadow: 0 0 0.5px 0.5px $mercury-gray;
font-weight: 500; font-weight: 500;
span { span {
......
import React, { Component } from 'react'; import React, { Component } from 'react'
import PropTypes from 'prop-types'; import PropTypes from 'prop-types'
import EthereumLogo from '../../assets/images/ethereum-logo.svg'; import EthereumLogo from '../../assets/images/ethereum-logo.svg'
const RINKEBY_TOKEN_MAP = { const RINKEBY_TOKEN_MAP = {
'0xDA5B056Cfb861282B4b59d29c9B395bcC238D29B': '0x0d8775f648430679a709e98d2b0cb6250d2887ef', '0xDA5B056Cfb861282B4b59d29c9B395bcC238D29B': '0x0d8775f648430679a709e98d2b0cb6250d2887ef',
'0x2448eE2641d78CC42D7AD76498917359D961A783': '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', '0x2448eE2641d78CC42D7AD76498917359D961A783': '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359',
'0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85': '0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2', '0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85': '0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2',
'0x879884c3C46A24f56089f3bBbe4d5e38dB5788C0': '0xd26114cd6ee289accf82350c8d8487fedb8a0c07', '0x879884c3C46A24f56089f3bBbe4d5e38dB5788C0': '0xd26114cd6ee289accf82350c8d8487fedb8a0c07',
'0xF22e3F33768354c9805d046af3C0926f27741B43': '0xe41d2489571d322189246dafa5ebde1f4699f498', '0xF22e3F33768354c9805d046af3C0926f27741B43': '0xe41d2489571d322189246dafa5ebde1f4699f498'
}; }
const TOKEN_ICON_API = 'https://raw.githubusercontent.com/TrustWallet/tokens/master/tokens'; const TOKEN_ICON_API = 'https://raw.githubusercontent.com/TrustWallet/tokens/master/tokens'
const BAD_IMAGES = {}; const BAD_IMAGES = {}
export default class TokenLogo extends Component { export default class TokenLogo extends Component {
static propTypes = { static propTypes = {
address: PropTypes.string, address: PropTypes.string,
size: PropTypes.string, size: PropTypes.string,
className: PropTypes.string, className: PropTypes.string
}; }
static defaultProps = { static defaultProps = {
address: '', address: '',
size: '1rem', size: '1rem',
className: '', className: ''
}; }
state = { state = {
error: false, error: false
}; }
render() { render() {
const { address, size, className } = this.props; const { address, size, className } = this.props
// let path = GenericTokenLogo; // let path = GenericTokenLogo;
let path = ''; let path = ''
const mainAddress = RINKEBY_TOKEN_MAP[address] ? RINKEBY_TOKEN_MAP[address] : address; const mainAddress = RINKEBY_TOKEN_MAP[address] ? RINKEBY_TOKEN_MAP[address] : address
if (mainAddress === 'ETH') { if (mainAddress === 'ETH') {
path = EthereumLogo; path = EthereumLogo
} }
if (!this.state.error && !BAD_IMAGES[mainAddress] && mainAddress !== 'ETH') { if (!this.state.error && !BAD_IMAGES[mainAddress] && mainAddress !== 'ETH') {
path = `${TOKEN_ICON_API}/${mainAddress.toLowerCase()}.png`; path = `${TOKEN_ICON_API}/${mainAddress.toLowerCase()}.png`
} }
if (!path) { if (!path) {
return <div className={className} style={{ width: size, fontSize: size }}><span role='img' aria-label='thinking'>🤔</span></div> return (
<div className={className} style={{ width: size, fontSize: size }}>
<span role="img" aria-label="thinking">
🤔
</span>
</div>
)
} }
return ( return (
<img <img
alt='images' alt="images"
src={path} src={path}
className={className} className={className}
style={{ style={{
width: size, width: size,
height: size, height: size
}} }}
onError={() => { onError={() => {
this.setState({ error: true }); this.setState({ error: true })
BAD_IMAGES[mainAddress] = true; BAD_IMAGES[mainAddress] = true
}} }}
/> />
); )
} }
} }
import React, { Component } from 'react'; import React, { Component } from 'react'
import PropTypes from 'prop-types'; import PropTypes from 'prop-types'
import { connect } from 'react-redux'; import { connect } from 'react-redux'
import classnames from 'classnames'; import classnames from 'classnames'
import Jazzicon from 'jazzicon'; import Jazzicon from 'jazzicon'
import { CSSTransitionGroup } from "react-transition-group"; import { CSSTransitionGroup } from 'react-transition-group'
import { withNamespaces } from 'react-i18next'; import { withNamespaces } from 'react-i18next'
import { ethers } from 'ethers' import { ethers } from 'ethers'
import './web3-status.scss'; import './web3-status.scss'
import Modal from '../Modal'; import Modal from '../Modal'
function getEtherscanLink(tx) { function getEtherscanLink(tx) {
return `https://etherscan.io/tx/${tx}`; return `https://etherscan.io/tx/${tx}`
} }
console.log(ethers) console.log(ethers)
class Web3Status extends Component { class Web3Status extends Component {
state = { state = {
isShowingModal: false, isShowingModal: false
}; }
handleClick = () => { handleClick = () => {
if (this.props.pending.length && !this.state.isShowingModal) { if (this.props.pending.length && !this.state.isShowingModal) {
this.setState({isShowingModal: true}); this.setState({ isShowingModal: true })
}
} }
};
renderPendingTransactions() { renderPendingTransactions() {
return this.props.pending.map((transaction) => { return this.props.pending.map(transaction => {
return ( return (
<div <div
key={transaction} key={transaction}
className={classnames('pending-modal__transaction-row')} className={classnames('pending-modal__transaction-row')}
onClick={() => window.open(getEtherscanLink(transaction), '_blank')} onClick={() => window.open(getEtherscanLink(transaction), '_blank')}
> >
<div className="pending-modal__transaction-label"> <div className="pending-modal__transaction-label">{transaction}</div>
{transaction}
</div>
<div className="pending-modal__pending-indicator"> <div className="pending-modal__pending-indicator">
<div className="loader" /> {this.props.t("pending")} <div className="loader" /> {this.props.t('pending')}
</div> </div>
</div> </div>
); )
}); })
} }
renderModal() { renderModal() {
if (!this.state.isShowingModal) { if (!this.state.isShowingModal) {
return null; return null
} }
return ( return (
...@@ -68,84 +66,82 @@ class Web3Status extends Component { ...@@ -68,84 +66,82 @@ class Web3Status extends Component {
</div> </div>
</CSSTransitionGroup> </CSSTransitionGroup>
</Modal> </Modal>
); )
} }
render() { render() {
const { t, address, pending, confirmed } = this.props; const { t, address, pending, confirmed } = this.props
const hasPendingTransactions = !!pending.length; const hasPendingTransactions = !!pending.length
const hasConfirmedTransactions = !!confirmed.length; const hasConfirmedTransactions = !!confirmed.length
return ( return (
<div <div
className={classnames("web3-status", { className={classnames('web3-status', {
'web3-status__connected': this.props.isConnected, 'web3-status__connected': this.props.isConnected,
'web3-status--pending': hasPendingTransactions, 'web3-status--pending': hasPendingTransactions,
'web3-status--confirmed': hasConfirmedTransactions, 'web3-status--confirmed': hasConfirmedTransactions
})} })}
onClick={this.handleClick} onClick={this.handleClick}
> >
<div className="web3-status__text"> <div className="web3-status__text">
{hasPendingTransactions ? getPendingText(pending, t("pending")) : getText(address, t("disconnected")) } {hasPendingTransactions ? getPendingText(pending, t('pending')) : getText(address, t('disconnected'))}
</div> </div>
<div <div
className="web3-status__identicon" className="web3-status__identicon"
ref={el => { ref={el => {
if (!el) { if (!el) {
return; return
} }
if (!address || address.length < 42 || !ethers.utils.isHexString(address)) { if (!address || address.length < 42 || !ethers.utils.isHexString(address)) {
return; return
} }
el.innerHTML = ''; el.innerHTML = ''
el.appendChild(Jazzicon(16, parseInt(address.slice(2), 16))); el.appendChild(Jazzicon(16, parseInt(address.slice(2), 16)))
}} }}
/> />
{this.renderModal()} {this.renderModal()}
</div> </div>
); )
} }
} }
function getPendingText(pendingTransactions, pendingLabel) { function getPendingText(pendingTransactions, pendingLabel) {
return ( return (
<div className="web3-status__pending-container"> <div className="web3-status__pending-container">
<div className="loader" /> <div className="loader" />
<span key="text">{pendingTransactions.length} {pendingLabel}</span> <span key="text">
{pendingTransactions.length} {pendingLabel}
</span>
</div> </div>
); )
} }
function getText(text, disconnectedText) { function getText(text, disconnectedText) {
if (!text || text.length < 42 || !ethers.utils.isHexString(text)) { if (!text || text.length < 42 || !ethers.utils.isHexString(text)) {
return disconnectedText; return disconnectedText
} }
const address = ethers.utils.getAddress(text); const address = ethers.utils.getAddress(text)
return `${address.substring(0, 6)}...${address.substring(38)}`; return `${address.substring(0, 6)}...${address.substring(38)}`
} }
Web3Status.propTypes = { Web3Status.propTypes = {
isConnected: PropTypes.bool, isConnected: PropTypes.bool,
address: PropTypes.string, address: PropTypes.string
}; }
Web3Status.defaultProps = { Web3Status.defaultProps = {
isConnected: false, isConnected: false,
address: 'Disconnected', address: 'Disconnected'
}; }
export default connect( export default connect(state => {
state => {
return { return {
address: state.web3connect.account, address: state.web3connect.account,
isConnected: !!(state.web3connect.web3 && state.web3connect.account), isConnected: !!(state.web3connect.web3 && state.web3connect.account),
pending: state.web3connect.transactions.pending, pending: state.web3connect.transactions.pending,
confirmed: state.web3connect.transactions.confirmed, confirmed: state.web3connect.transactions.confirmed
};
} }
)(withNamespaces()(Web3Status)); })(withNamespaces()(Web3Status))
@import "../../variables.scss"; @import '../../variables.scss';
.web3-status { .web3-status {
@extend %row-nowrap; @extend %row-nowrap;
height: 2rem; height: 2rem;
font-size: .9rem; font-size: 0.9rem;
align-items: center; align-items: center;
border: 1px solid $mercury-gray; border: 1px solid $mercury-gray;
padding: .5rem; padding: 0.5rem;
border-radius: 2rem; border-radius: 2rem;
color: $dove-gray; color: $dove-gray;
font-weight: 400; font-weight: 400;
...@@ -22,9 +22,9 @@ ...@@ -22,9 +22,9 @@
&__text { &__text {
flex: 1 1 auto; flex: 1 1 auto;
overflow: hidden; overflow: hidden;
margin-right: .75rem; margin-right: 0.75rem;
margin-left: .25rem; margin-left: 0.25rem;
font-size: .75rem; font-size: 0.75rem;
} }
&__pending-container { &__pending-container {
...@@ -39,7 +39,6 @@ ...@@ -39,7 +39,6 @@
} }
} }
.pending-modal { .pending-modal {
background-color: $white; background-color: $white;
position: relative; position: relative;
...@@ -80,12 +79,12 @@ ...@@ -80,12 +79,12 @@
color: $royal-blue; color: $royal-blue;
border: 1px solid $royal-blue; border: 1px solid $royal-blue;
background-color: $zumthor-blue; background-color: $zumthor-blue;
padding: .5rem .75rem;; padding: 0.5rem 0.75rem;
border-radius: 100px; border-radius: 100px;
font-size: .75rem; font-size: 0.75rem;
& > .loading { & > .loading {
margin-right: .5rem; margin-right: 0.5rem;
} }
} }
...@@ -96,7 +95,6 @@ ...@@ -96,7 +95,6 @@
} }
} }
.pending-modal-appear { .pending-modal-appear {
bottom: 0; bottom: 0;
} }
......
// string literals for actions // string literals for actions
// set global web3 object // set global web3 object
export const INITIALIZE_GLOBAL_WEB3 = 'INITIALIZE_GLOBAL_WEB3'; export const INITIALIZE_GLOBAL_WEB3 = 'INITIALIZE_GLOBAL_WEB3'
// web3 actions, all set from action creator to reducer to app // web3 actions, all set from action creator to reducer to app
export const SET_WEB3_CONNECTION_STATUS = 'WEB3_CONNECTION_STATUS'; export const SET_WEB3_CONNECTION_STATUS = 'WEB3_CONNECTION_STATUS'
export const CHECK_WEB3_CONNECTION = 'CHECK_WEB3_CONNECTION'; export const CHECK_WEB3_CONNECTION = 'CHECK_WEB3_CONNECTION'
export const SET_CURRENT_MASK_ADDRESS = 'SET_CURRENT_MASK_ADDRESS'; export const SET_CURRENT_MASK_ADDRESS = 'SET_CURRENT_MASK_ADDRESS'
export const METAMASK_LOCKED = 'METAMASK_LOCKED'; export const METAMASK_LOCKED = 'METAMASK_LOCKED'
export const METAMASK_UNLOCKED = 'METAMASK_UNLOCKED'; export const METAMASK_UNLOCKED = 'METAMASK_UNLOCKED'
export const SET_INTERACTION_STATE = 'SET_INTERACTION_STATE'; export const SET_INTERACTION_STATE = 'SET_INTERACTION_STATE'
export const SET_NETWORK_MESSAGE = 'SET_NETWORK_MESSAGE'; export const SET_NETWORK_MESSAGE = 'SET_NETWORK_MESSAGE'
export const SET_BLOCK_TIMESTAMP = 'SET_BLOCK_TIMESTAMP'; export const SET_BLOCK_TIMESTAMP = 'SET_BLOCK_TIMESTAMP'
export const SET_EXCHANGE_TYPE = 'SET_EXCHANGE_TYPE'; export const SET_EXCHANGE_TYPE = 'SET_EXCHANGE_TYPE'
// actions to toggle divs // actions to toggle divs
export const TOGGLE_ABOUT = 'TOGGLE_ABOUT'; export const TOGGLE_ABOUT = 'TOGGLE_ABOUT'
export const TOGGLE_INVEST = 'TOGGLE_INVEST'; export const TOGGLE_INVEST = 'TOGGLE_INVEST'
// CONTRACT actions in actions, action creator, reducer // CONTRACT actions in actions, action creator, reducer
export const FACTORY_CONTRACT_READY = 'FACTORY_CONTRACT_READY'; export const FACTORY_CONTRACT_READY = 'FACTORY_CONTRACT_READY'
export const EXCHANGE_CONTRACT_READY = 'EXCHANGE_CONTRACT_READY'; export const EXCHANGE_CONTRACT_READY = 'EXCHANGE_CONTRACT_READY'
export const TOKEN_CONTRACT_READY = 'TOKEN_CONTRACT_READY'; export const TOKEN_CONTRACT_READY = 'TOKEN_CONTRACT_READY'
// actions for the exchange // actions for the exchange
export const SET_INPUT_BALANCE = 'SET_INPUT_BALANCE'; export const SET_INPUT_BALANCE = 'SET_INPUT_BALANCE'
export const SET_OUTPUT_BALANCE = 'SET_OUTPUT_BALANCE'; export const SET_OUTPUT_BALANCE = 'SET_OUTPUT_BALANCE'
export const SET_INPUT_TOKEN = 'SET_INPUT_TOKEN'; export const SET_INPUT_TOKEN = 'SET_INPUT_TOKEN'
export const SET_OUTPUT_TOKEN = 'SET_OUTPUT_TOKEN'; export const SET_OUTPUT_TOKEN = 'SET_OUTPUT_TOKEN'
export const SET_ETH_POOL_1 = 'SET_ETH_POOL_1'; export const SET_ETH_POOL_1 = 'SET_ETH_POOL_1'
export const SET_ETH_POOL_2 = 'SET_ETH_POOL_2'; export const SET_ETH_POOL_2 = 'SET_ETH_POOL_2'
export const SET_TOKEN_POOL_1 = 'SET_TOKEN_POOL_1'; export const SET_TOKEN_POOL_1 = 'SET_TOKEN_POOL_1'
export const SET_TOKEN_POOL_2 = 'SET_TOKEN_POOL_2'; export const SET_TOKEN_POOL_2 = 'SET_TOKEN_POOL_2'
export const SET_ALLOWANCE_APPROVAL_STATE = 'SET_ALLOWANCE_APPROVAL_STATE'; export const SET_ALLOWANCE_APPROVAL_STATE = 'SET_ALLOWANCE_APPROVAL_STATE'
export const SET_EXCHANGE_INPUT_VALUE = 'SET_EXCHANGE_INPUT_VALUE'; export const SET_EXCHANGE_INPUT_VALUE = 'SET_EXCHANGE_INPUT_VALUE'
export const SET_EXCHANGE_OUTPUT_VALUE = 'SET_EXCHANGE_OUTPUT_VALUE'; export const SET_EXCHANGE_OUTPUT_VALUE = 'SET_EXCHANGE_OUTPUT_VALUE'
export const SET_EXCHANGE_RATE = 'SET_EXCHANGE_RATE'; export const SET_EXCHANGE_RATE = 'SET_EXCHANGE_RATE'
export const SET_EXCHANGE_FEE = 'SET_EXCHANGE_FEE'; export const SET_EXCHANGE_FEE = 'SET_EXCHANGE_FEE'
export const SET_INVEST_TOKEN = 'SET_INVEST_TOKEN'; export const SET_INVEST_TOKEN = 'SET_INVEST_TOKEN'
export const SET_INVEST_ETH_POOL = 'SET_INVEST_ETH'; export const SET_INVEST_ETH_POOL = 'SET_INVEST_ETH'
export const SET_INVEST_TOKEN_POOL = 'SET_INVEST_TOKENS'; export const SET_INVEST_TOKEN_POOL = 'SET_INVEST_TOKENS'
export const SET_INVEST_TOKEN_ALLOWANCE = 'SET_INVEST_TOKEN_ALLOWANCE'; export const SET_INVEST_TOKEN_ALLOWANCE = 'SET_INVEST_TOKEN_ALLOWANCE'
export const SET_INVEST_SHARES = 'SET_INVEST_SHARES'; export const SET_INVEST_SHARES = 'SET_INVEST_SHARES'
export const SET_USER_SHARES = 'SET_USER_SHARES'; export const SET_USER_SHARES = 'SET_USER_SHARES'
export const SET_INVEST_TOKEN_BALANCE = 'SET_INVEST_TOKEN_BALANCE'; export const SET_INVEST_TOKEN_BALANCE = 'SET_INVEST_TOKEN_BALANCE'
export const SET_INVEST_ETH_BALANCE = 'SET_INVEST_ETH_BALANCE'; export const SET_INVEST_ETH_BALANCE = 'SET_INVEST_ETH_BALANCE'
export const SET_INVEST_SHARES_INPUT = 'SET_INVEST_SHARES_INPUT'; export const SET_INVEST_SHARES_INPUT = 'SET_INVEST_SHARES_INPUT'
export const SET_INVEST_ETH_REQUIRED = 'SET_INVEST_ETH_REQUIRED'; export const SET_INVEST_ETH_REQUIRED = 'SET_INVEST_ETH_REQUIRED'
export const SET_INVEST_TOKENS_REQUIRED = 'SET_INVEST_TOKENS_REQUIRED'; export const SET_INVEST_TOKENS_REQUIRED = 'SET_INVEST_TOKENS_REQUIRED'
export const SET_INVEST_CHECKED = 'SET_INVEST_CHECKED'; export const SET_INVEST_CHECKED = 'SET_INVEST_CHECKED'
export const INSUFFICIENT_BALANCE = 'Insufficient balance'; export const INSUFFICIENT_BALANCE = 'Insufficient balance'
...@@ -2,30 +2,30 @@ const RINKEBY = { ...@@ -2,30 +2,30 @@ const RINKEBY = {
factoryAddress: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36', factoryAddress: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36',
exchangeAddresses: { exchangeAddresses: {
addresses: [ addresses: [
['BAT','0x9B913956036a3462330B0642B20D3879ce68b450'], ['BAT', '0x9B913956036a3462330B0642B20D3879ce68b450'],
['DAI','0x77dB9C915809e7BE439D2AB21032B1b8B58F6891'], ['DAI', '0x77dB9C915809e7BE439D2AB21032B1b8B58F6891'],
['MKR','0x93bB63aFe1E0180d0eF100D774B473034fd60C36'], ['MKR', '0x93bB63aFe1E0180d0eF100D774B473034fd60C36'],
['OMG','0x26C226EBb6104676E593F8A070aD6f25cDa60F8D'], ['OMG', '0x26C226EBb6104676E593F8A070aD6f25cDa60F8D']
// ['ZRX','0xaBD44a1D1b9Fb0F39fE1D1ee6b1e2a14916D067D'], // ['ZRX','0xaBD44a1D1b9Fb0F39fE1D1ee6b1e2a14916D067D'],
], ],
fromToken: { fromToken: {
'0xDA5B056Cfb861282B4b59d29c9B395bcC238D29B': '0x9B913956036a3462330B0642B20D3879ce68b450', '0xDA5B056Cfb861282B4b59d29c9B395bcC238D29B': '0x9B913956036a3462330B0642B20D3879ce68b450',
'0x2448eE2641d78CC42D7AD76498917359D961A783': '0x77dB9C915809e7BE439D2AB21032B1b8B58F6891', '0x2448eE2641d78CC42D7AD76498917359D961A783': '0x77dB9C915809e7BE439D2AB21032B1b8B58F6891',
'0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85': '0x93bB63aFe1E0180d0eF100D774B473034fd60C36', '0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85': '0x93bB63aFe1E0180d0eF100D774B473034fd60C36',
'0x879884c3C46A24f56089f3bBbe4d5e38dB5788C0': '0x26C226EBb6104676E593F8A070aD6f25cDa60F8D', '0x879884c3C46A24f56089f3bBbe4d5e38dB5788C0': '0x26C226EBb6104676E593F8A070aD6f25cDa60F8D'
// '0xF22e3F33768354c9805d046af3C0926f27741B43': '0xaBD44a1D1b9Fb0F39fE1D1ee6b1e2a14916D067D', // '0xF22e3F33768354c9805d046af3C0926f27741B43': '0xaBD44a1D1b9Fb0F39fE1D1ee6b1e2a14916D067D',
}, }
}, },
tokenAddresses: { tokenAddresses: {
addresses: [ addresses: [
['BAT','0xDA5B056Cfb861282B4b59d29c9B395bcC238D29B'], ['BAT', '0xDA5B056Cfb861282B4b59d29c9B395bcC238D29B'],
['DAI','0x2448eE2641d78CC42D7AD76498917359D961A783'], ['DAI', '0x2448eE2641d78CC42D7AD76498917359D961A783'],
['MKR','0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85'], ['MKR', '0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85'],
['OMG','0x879884c3C46A24f56089f3bBbe4d5e38dB5788C0'], ['OMG', '0x879884c3C46A24f56089f3bBbe4d5e38dB5788C0']
// ['ZRX','0xF22e3F33768354c9805d046af3C0926f27741B43'], // ['ZRX','0xF22e3F33768354c9805d046af3C0926f27741B43'],
], ]
}, }
}; }
const MAIN = { const MAIN = {
factoryAddress: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95', factoryAddress: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95',
...@@ -73,7 +73,7 @@ const MAIN = { ...@@ -73,7 +73,7 @@ const MAIN = {
['WETH', '0xA2881A90Bf33F03E7a3f803765Cd2ED5c8928dFb'], ['WETH', '0xA2881A90Bf33F03E7a3f803765Cd2ED5c8928dFb'],
['XCHF', '0x8dE0d002DC83478f479dC31F76cB0a8aa7CcEa17'], ['XCHF', '0x8dE0d002DC83478f479dC31F76cB0a8aa7CcEa17'],
['ZIL', '0x7dc095A5CF7D6208CC680fA9866F80a53911041a'], ['ZIL', '0x7dc095A5CF7D6208CC680fA9866F80a53911041a'],
['ZRX', '0xaE76c84C9262Cdb9abc0C2c8888e62Db8E22A0bF'], ['ZRX', '0xaE76c84C9262Cdb9abc0C2c8888e62Db8E22A0bF']
], ],
fromToken: { fromToken: {
'0x960b236A07cf122663c4303350609A66A7B288C0': '0x077d52B047735976dfdA76feF74d4d988AC25196', '0x960b236A07cf122663c4303350609A66A7B288C0': '0x077d52B047735976dfdA76feF74d4d988AC25196',
...@@ -118,8 +118,8 @@ const MAIN = { ...@@ -118,8 +118,8 @@ const MAIN = {
'0x05f4a42e251f2d52b8ed15E9FEdAacFcEF1FAD27': '0x7dc095A5CF7D6208CC680fA9866F80a53911041a', '0x05f4a42e251f2d52b8ed15E9FEdAacFcEF1FAD27': '0x7dc095A5CF7D6208CC680fA9866F80a53911041a',
'0xE41d2489571d322189246DaFA5ebDe1F4699F498': '0xaE76c84C9262Cdb9abc0C2c8888e62Db8E22A0bF', '0xE41d2489571d322189246DaFA5ebDe1F4699F498': '0xaE76c84C9262Cdb9abc0C2c8888e62Db8E22A0bF',
'0x3772f9716Cf6D7a09edE3587738AA2af5577483a': '0x5d8888a212d033cff5f2e0ac24ad91a5495bad62', '0x3772f9716Cf6D7a09edE3587738AA2af5577483a': '0x5d8888a212d033cff5f2e0ac24ad91a5495bad62',
'0x0cbe2df57ca9191b64a7af3baa3f946fa7df2f25': '0xa1ecdcca26150cf69090280ee2ee32347c238c7b', '0x0cbe2df57ca9191b64a7af3baa3f946fa7df2f25': '0xa1ecdcca26150cf69090280ee2ee32347c238c7b'
}, }
}, },
tokenAddresses: { tokenAddresses: {
addresses: [ addresses: [
...@@ -165,25 +165,27 @@ const MAIN = { ...@@ -165,25 +165,27 @@ const MAIN = {
['WETH', '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'], ['WETH', '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'],
['XCHF', '0xB4272071eCAdd69d933AdcD19cA99fe80664fc08'], ['XCHF', '0xB4272071eCAdd69d933AdcD19cA99fe80664fc08'],
['ZIL', '0x05f4a42e251f2d52b8ed15E9FEdAacFcEF1FAD27'], ['ZIL', '0x05f4a42e251f2d52b8ed15E9FEdAacFcEF1FAD27'],
['ZRX', '0xE41d2489571d322189246DaFA5ebDe1F4699F498'], ['ZRX', '0xE41d2489571d322189246DaFA5ebDe1F4699F498']
], ]
}, }
}; }
const SET_ADDRESSES = 'app/addresses/setAddresses'; const SET_ADDRESSES = 'app/addresses/setAddresses'
const ADD_EXCHANGE = 'app/addresses/addExchange'; const ADD_EXCHANGE = 'app/addresses/addExchange'
const initialState = RINKEBY; const initialState = RINKEBY
export const addExchange = ({label, exchangeAddress, tokenAddress}) => (dispatch, getState) => { export const addExchange = ({ label, exchangeAddress, tokenAddress }) => (dispatch, getState) => {
const { addresses: { tokenAddresses, exchangeAddresses } } = getState(); const {
addresses: { tokenAddresses, exchangeAddresses }
} = getState()
if (tokenAddresses.addresses.filter(([ symbol ]) => symbol === label).length) { if (tokenAddresses.addresses.filter(([symbol]) => symbol === label).length) {
return; return
} }
if (exchangeAddresses.fromToken[tokenAddresses]) { if (exchangeAddresses.fromToken[tokenAddresses]) {
return; return
} }
dispatch({ dispatch({
...@@ -191,68 +193,62 @@ export const addExchange = ({label, exchangeAddress, tokenAddress}) => (dispatch ...@@ -191,68 +193,62 @@ export const addExchange = ({label, exchangeAddress, tokenAddress}) => (dispatch
payload: { payload: {
label, label,
exchangeAddress, exchangeAddress,
tokenAddress, tokenAddress
}, }
}); })
}; }
export const setAddresses = networkId => { export const setAddresses = networkId => {
switch(networkId) { switch (networkId) {
// Main Net // Main Net
case 1: case 1:
case '1': case '1':
return { return {
type: SET_ADDRESSES, type: SET_ADDRESSES,
payload: MAIN, payload: MAIN
}; }
// Rinkeby // Rinkeby
case 4: case 4:
case '4': case '4':
default: default:
return { return {
type: SET_ADDRESSES, type: SET_ADDRESSES,
payload: RINKEBY, payload: RINKEBY
};
} }
}; }
}
export default (state = initialState, { type, payload }) => { export default (state = initialState, { type, payload }) => {
switch (type) { switch (type) {
case SET_ADDRESSES: case SET_ADDRESSES:
return payload; return payload
case ADD_EXCHANGE: case ADD_EXCHANGE:
return handleAddExchange(state, { payload }); return handleAddExchange(state, { payload })
default: default:
return state; return state
} }
} }
function handleAddExchange(state, { payload }) { function handleAddExchange(state, { payload }) {
const { label, tokenAddress, exchangeAddress } = payload; const { label, tokenAddress, exchangeAddress } = payload
if (!label || !tokenAddress || !exchangeAddress) { if (!label || !tokenAddress || !exchangeAddress) {
return state; return state
} }
return { return {
...state, ...state,
exchangeAddresses: { exchangeAddresses: {
...state.exchangeAddresses, ...state.exchangeAddresses,
addresses: [ addresses: [...state.exchangeAddresses.addresses, [label, exchangeAddress]],
...state.exchangeAddresses.addresses,
[label, exchangeAddress]
],
fromToken: { fromToken: {
...state.exchangeAddresses.fromToken, ...state.exchangeAddresses.fromToken,
[tokenAddress]: exchangeAddress, [tokenAddress]: exchangeAddress
}, }
}, },
tokenAddresses: { tokenAddresses: {
...state.tokenAddresses, ...state.tokenAddresses,
addresses: [ addresses: [...state.tokenAddresses.addresses, [label, tokenAddress]]
...state.tokenAddresses.addresses, }
[label, tokenAddress] }
],
},
};
} }
const DISMISS_BETA_MESSAGE = 'app/app/dismissBetaMessage'; const DISMISS_BETA_MESSAGE = 'app/app/dismissBetaMessage'
const initialState = { const initialState = {
showBetaMessage: true, showBetaMessage: true
}; }
export const dismissBetaMessage = () => ({ type: DISMISS_BETA_MESSAGE }); export const dismissBetaMessage = () => ({ type: DISMISS_BETA_MESSAGE })
export default function appReducer(state = initialState, { type, payload }) { export default function appReducer(state = initialState, { type, payload }) {
switch (type) { switch (type) {
case DISMISS_BETA_MESSAGE: case DISMISS_BETA_MESSAGE:
return { ...state, showBetaMessage: false }; return { ...state, showBetaMessage: false }
default: default:
return state; return state
} }
} }
import { combineReducers } from 'redux'; import { combineReducers } from 'redux'
import addresses from './addresses'; import addresses from './addresses'
import app from './app'; import app from './app'
import pending from './pending'; import pending from './pending'
import web3connect from './web3connect'; import web3connect from './web3connect'
export default combineReducers({ export default combineReducers({
app, app,
addresses, addresses,
pending, pending,
web3connect, web3connect
}); })
const ADD_APPROVAL_TX = 'app/send/addApprovalTx'; const ADD_APPROVAL_TX = 'app/send/addApprovalTx'
const getInitialState = () => { const getInitialState = () => {
return { return {
approvals: {}, approvals: {}
}; }
}; }
export const addApprovalTx = ({ tokenAddress, txId }) => ({ export const addApprovalTx = ({ tokenAddress, txId }) => ({
type: ADD_APPROVAL_TX, type: ADD_APPROVAL_TX,
payload: { tokenAddress, txId }, payload: { tokenAddress, txId }
}); })
export default function sendReducer(state = getInitialState(), { type, payload }) { export default function sendReducer(state = getInitialState(), { type, payload }) {
switch (type) { switch (type) {
...@@ -17,10 +17,10 @@ export default function sendReducer(state = getInitialState(), { type, payload } ...@@ -17,10 +17,10 @@ export default function sendReducer(state = getInitialState(), { type, payload }
return { return {
approvals: { approvals: {
...state.approvals, ...state.approvals,
[payload.tokenAddress]: payload.txId, [payload.tokenAddress]: payload.txId
}
} }
};
default: default:
return state; return state
} }
} }
import React, { Component } from 'react'; import React, { Component } from 'react'
import PropTypes from 'prop-types'; import PropTypes from 'prop-types'
import { connect } from 'react-redux'; import { connect } from 'react-redux'
import {BigNumber as BN} from 'bignumber.js'; import { BigNumber as BN } from 'bignumber.js'
import Web3 from 'web3'; import Web3 from 'web3'
import ERC20_ABI from "../abi/erc20"; import ERC20_ABI from '../abi/erc20'
import ERC20_WITH_BYTES_ABI from "../abi/erc20_symbol_bytes32"; import ERC20_WITH_BYTES_ABI from '../abi/erc20_symbol_bytes32'
export const INITIALIZE = 'we3connect/initialize'; export const INITIALIZE = 'we3connect/initialize'
export const UPDATE_ACCOUNT = 'we3connect/updateAccount'; export const UPDATE_ACCOUNT = 'we3connect/updateAccount'
export const WATCH_ETH_BALANCE = 'web3connect/watchEthBalance'; export const WATCH_ETH_BALANCE = 'web3connect/watchEthBalance'
export const WATCH_TOKEN_BALANCE = 'web3connect/watchTokenBalance'; export const WATCH_TOKEN_BALANCE = 'web3connect/watchTokenBalance'
export const UPDATE_ETH_BALANCE = 'web3connect/updateEthBalance'; export const UPDATE_ETH_BALANCE = 'web3connect/updateEthBalance'
export const UPDATE_TOKEN_BALANCE = 'web3connect/updateTokenBalance'; export const UPDATE_TOKEN_BALANCE = 'web3connect/updateTokenBalance'
export const WATCH_APPROVALS = 'web3connect/watchApprovals'; export const WATCH_APPROVALS = 'web3connect/watchApprovals'
export const UPDATE_APPROVALS = 'web3connect/updateApprovals'; export const UPDATE_APPROVALS = 'web3connect/updateApprovals'
export const ADD_CONTRACT = 'web3connect/addContract'; export const ADD_CONTRACT = 'web3connect/addContract'
export const UPDATE_NETWORK_ID = 'web3connect/updateNetworkId'; export const UPDATE_NETWORK_ID = 'web3connect/updateNetworkId'
export const ADD_PENDING_TX = 'web3connect/addPendingTx'; export const ADD_PENDING_TX = 'web3connect/addPendingTx'
export const REMOVE_PENDING_TX = 'web3connect/removePendingTx'; export const REMOVE_PENDING_TX = 'web3connect/removePendingTx'
export const ADD_CONFIRMED_TX = 'web3connect/addConfirmedTx'; export const ADD_CONFIRMED_TX = 'web3connect/addConfirmedTx'
const initialState = { const initialState = {
web3: null, web3: null,
...@@ -26,181 +26,183 @@ const initialState = { ...@@ -26,181 +26,183 @@ const initialState = {
initialized: false, initialized: false,
account: '', account: '',
balances: { balances: {
ethereum: {}, ethereum: {}
}, },
approvals: { approvals: {
'0x0': { '0x0': {
TOKEN_OWNER: { TOKEN_OWNER: {
SPENDER: {}, SPENDER: {}
}, }
}, }
}, },
transactions: { transactions: {
pending: [], pending: [],
confirmed: [], confirmed: []
}, },
watched: { watched: {
balances: { balances: {
ethereum: [], ethereum: []
}, },
approvals: {}, approvals: {}
}, },
contracts: {}, contracts: {}
}; }
// selectors // selectors
export const selectors = () => (dispatch, getState) => { export const selectors = () => (dispatch, getState) => {
const state = getState().web3connect; const state = getState().web3connect
const getTokenBalance = (tokenAddress, address) => { const getTokenBalance = (tokenAddress, address) => {
const tokenBalances = state.balances[tokenAddress] || {}; const tokenBalances = state.balances[tokenAddress] || {}
const balance = tokenBalances[address]; const balance = tokenBalances[address]
if (!balance) { if (!balance) {
dispatch(watchBalance({ balanceOf: address, tokenAddress })); dispatch(watchBalance({ balanceOf: address, tokenAddress }))
return Balance(0); return Balance(0)
}
return balance
} }
return balance;
};
const getBalance = (address, tokenAddress) => { const getBalance = (address, tokenAddress) => {
if (process.env.NODE_ENV !== 'production' && !tokenAddress) { if (process.env.NODE_ENV !== 'production' && !tokenAddress) {
console.warn('No token address found - return ETH balance'); console.warn('No token address found - return ETH balance')
} }
if (!tokenAddress || tokenAddress === 'ETH') { if (!tokenAddress || tokenAddress === 'ETH') {
const balance = state.balances.ethereum[address]; const balance = state.balances.ethereum[address]
if (!balance) { if (!balance) {
dispatch(watchBalance({ balanceOf: address })); dispatch(watchBalance({ balanceOf: address }))
return Balance(0, 'ETH'); return Balance(0, 'ETH')
} }
return balance; return balance
} else if (tokenAddress) { } else if (tokenAddress) {
return getTokenBalance(tokenAddress, address); return getTokenBalance(tokenAddress, address)
} }
return Balance(NaN); return Balance(NaN)
}; }
const getApprovals = (tokenAddress, tokenOwner, spender) => { const getApprovals = (tokenAddress, tokenOwner, spender) => {
const token = state.approvals[tokenAddress] || {}; const token = state.approvals[tokenAddress] || {}
const owner = token[tokenOwner] || {}; const owner = token[tokenOwner] || {}
if (!owner[spender]) { if (!owner[spender]) {
dispatch(watchApprovals({ tokenAddress, tokenOwner, spender })); dispatch(watchApprovals({ tokenAddress, tokenOwner, spender }))
return Balance(0); return Balance(0)
} }
return owner[spender]; return owner[spender]
}; }
return { return {
getBalance, getBalance,
getTokenBalance, getTokenBalance,
getApprovals, getApprovals
}; }
}; }
const Balance = (value, label = '', decimals = 0) => ({ const Balance = (value, label = '', decimals = 0) => ({
value: BN(value), value: BN(value),
label: label.toUpperCase(), label: label.toUpperCase(),
decimals: +decimals, decimals: +decimals
}); })
export const initialize = () => (dispatch, getState) => { export const initialize = () => (dispatch, getState) => {
const { web3connect } = getState(); const { web3connect } = getState()
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
if (web3connect.web3) { if (web3connect.web3) {
resolve(web3connect.web3); resolve(web3connect.web3)
return; return
} }
if (typeof window.ethereum !== 'undefined') { if (typeof window.ethereum !== 'undefined') {
try { try {
const web3 = new Web3(window.ethereum); const web3 = new Web3(window.ethereum)
await window.ethereum.enable(); await window.ethereum.enable()
dispatch({ dispatch({
type: INITIALIZE, type: INITIALIZE,
payload: web3, payload: web3
}); })
resolve(web3); resolve(web3)
return; return
} catch (error) { } catch (error) {
console.error('User denied access.'); console.error('User denied access.')
dispatch({ type: INITIALIZE }); dispatch({ type: INITIALIZE })
reject(); reject()
return; return
} }
} }
if (typeof window.web3 !== 'undefined') { if (typeof window.web3 !== 'undefined') {
const web3 = new Web3(window.web3.currentProvider); const web3 = new Web3(window.web3.currentProvider)
dispatch({ dispatch({
type: INITIALIZE, type: INITIALIZE,
payload: web3, payload: web3
}); })
resolve(web3); resolve(web3)
return; return
} }
dispatch({ type: INITIALIZE }); dispatch({ type: INITIALIZE })
reject(); reject()
}) })
}; }
export const watchBalance = ({ balanceOf, tokenAddress }) => (dispatch, getState) => { export const watchBalance = ({ balanceOf, tokenAddress }) => (dispatch, getState) => {
if (!balanceOf) { if (!balanceOf) {
return; return
} }
const { web3connect } = getState(); const { web3connect } = getState()
const { watched } = web3connect; const { watched } = web3connect
if (!tokenAddress) { if (!tokenAddress) {
if (watched.balances.ethereum.includes(balanceOf)) { if (watched.balances.ethereum.includes(balanceOf)) {
return; return
} }
dispatch({ dispatch({
type: WATCH_ETH_BALANCE, type: WATCH_ETH_BALANCE,
payload: balanceOf, payload: balanceOf
}); })
setTimeout(() => dispatch(sync()), 0); setTimeout(() => dispatch(sync()), 0)
} else if (tokenAddress) { } else if (tokenAddress) {
if (watched.balances[tokenAddress] && watched.balances[tokenAddress].includes(balanceOf)) { if (watched.balances[tokenAddress] && watched.balances[tokenAddress].includes(balanceOf)) {
return; return
} }
dispatch({ dispatch({
type: WATCH_TOKEN_BALANCE, type: WATCH_TOKEN_BALANCE,
payload: { payload: {
tokenAddress, tokenAddress,
balanceOf, balanceOf
}, }
}); })
setTimeout(() => dispatch(sync()), 0); setTimeout(() => dispatch(sync()), 0)
} }
}; }
export const watchApprovals = ({ tokenAddress, tokenOwner, spender }) => (dispatch, getState) => { export const watchApprovals = ({ tokenAddress, tokenOwner, spender }) => (dispatch, getState) => {
const { web3connect: { watched } } = getState(); const {
const token = watched.approvals[tokenAddress] || {}; web3connect: { watched }
const owner = token[tokenOwner] || []; } = getState()
const token = watched.approvals[tokenAddress] || {}
const owner = token[tokenOwner] || []
if (owner.includes(spender)) { if (owner.includes(spender)) {
return; return
} }
return dispatch({ return dispatch({
type: WATCH_APPROVALS, type: WATCH_APPROVALS,
payload: { payload: {
tokenAddress, tokenAddress,
tokenOwner, tokenOwner,
spender, spender
}, }
}); })
}; }
export const addPendingTx = txId => ({ export const addPendingTx = txId => ({
type: ADD_PENDING_TX, type: ADD_PENDING_TX,
payload: txId, payload: txId
}); })
export const updateApprovals = ({ tokenAddress, tokenOwner, spender, balance }) => ({ export const updateApprovals = ({ tokenAddress, tokenOwner, spender, balance }) => ({
type: UPDATE_APPROVALS, type: UPDATE_APPROVALS,
...@@ -208,90 +210,100 @@ export const updateApprovals = ({ tokenAddress, tokenOwner, spender, balance }) ...@@ -208,90 +210,100 @@ export const updateApprovals = ({ tokenAddress, tokenOwner, spender, balance })
tokenAddress, tokenAddress,
tokenOwner, tokenOwner,
spender, spender,
balance, balance
}, }
}); })
export const sync = () => async (dispatch, getState) => { export const sync = () => async (dispatch, getState) => {
const { getBalance, getApprovals } = dispatch(selectors()); const { getBalance, getApprovals } = dispatch(selectors())
const web3 = await dispatch(initialize()); const web3 = await dispatch(initialize())
const { const {
account, account,
watched, watched,
contracts, contracts,
networkId, networkId,
transactions: { pending }, transactions: { pending }
} = getState().web3connect; } = getState().web3connect
// Sync Account // Sync Account
const accounts = await web3.eth.getAccounts(); const accounts = await web3.eth.getAccounts()
if (account !== accounts[0]) { if (account !== accounts[0]) {
dispatch({ type: UPDATE_ACCOUNT, payload: accounts[0] }); dispatch({ type: UPDATE_ACCOUNT, payload: accounts[0] })
dispatch(watchBalance({ balanceOf: accounts[0] })); dispatch(watchBalance({ balanceOf: accounts[0] }))
} }
if (!networkId) { if (!networkId) {
dispatch({ dispatch({
type: UPDATE_NETWORK_ID, type: UPDATE_NETWORK_ID,
payload: await web3.eth.net.getId(), payload: await web3.eth.net.getId()
}); })
} }
// Sync Ethereum Balances // Sync Ethereum Balances
watched.balances.ethereum.forEach(async address => { watched.balances.ethereum.forEach(async address => {
const balance = await web3.eth.getBalance(address); const balance = await web3.eth.getBalance(address)
const { value } = getBalance(address); const { value } = getBalance(address)
if (value.isEqualTo(BN(balance))) { if (value.isEqualTo(BN(balance))) {
return; return
} }
dispatch({ dispatch({
type: UPDATE_ETH_BALANCE, type: UPDATE_ETH_BALANCE,
payload: { payload: {
balance: Balance(balance, 'ETH', 18), balance: Balance(balance, 'ETH', 18),
balanceOf: address, balanceOf: address
}, }
})
}) })
});
// Sync Token Balances // Sync Token Balances
Object.keys(watched.balances) Object.keys(watched.balances).forEach(tokenAddress => {
.forEach(tokenAddress => {
if (tokenAddress === 'ethereum') { if (tokenAddress === 'ethereum') {
return; return
} }
const contract = contracts[tokenAddress] || new web3.eth.Contract(ERC20_ABI, tokenAddress); const contract = contracts[tokenAddress] || new web3.eth.Contract(ERC20_ABI, tokenAddress)
const contractBytes32 = contracts[tokenAddress] || new web3.eth.Contract(ERC20_WITH_BYTES_ABI, tokenAddress); const contractBytes32 = contracts[tokenAddress] || new web3.eth.Contract(ERC20_WITH_BYTES_ABI, tokenAddress)
if (!contracts[tokenAddress]) { if (!contracts[tokenAddress]) {
dispatch({ dispatch({
type: ADD_CONTRACT, type: ADD_CONTRACT,
payload: { payload: {
address: tokenAddress, address: tokenAddress,
contract: contract, contract: contract
}, }
}); })
} }
const watchlist = watched.balances[tokenAddress] || []; const watchlist = watched.balances[tokenAddress] || []
watchlist.forEach(async address => { watchlist.forEach(async address => {
const tokenBalance = getBalance(address, tokenAddress); const tokenBalance = getBalance(address, tokenAddress)
const balance = await contract.methods.balanceOf(address).call(); const balance = await contract.methods.balanceOf(address).call()
const decimals = tokenBalance.decimals || await contract.methods.decimals().call(); const decimals = tokenBalance.decimals || (await contract.methods.decimals().call())
let symbol = tokenBalance.symbol; let symbol = tokenBalance.symbol
try { try {
symbol = symbol || await contract.methods.symbol().call().catch(); symbol =
symbol ||
(await contract.methods
.symbol()
.call()
.catch())
} catch (e) { } catch (e) {
try { try {
symbol = symbol || web3.utils.hexToString(await contractBytes32.methods.symbol().call().catch()); symbol =
} catch (err) { symbol ||
} web3.utils.hexToString(
await contractBytes32.methods
.symbol()
.call()
.catch()
)
} catch (err) {}
} }
if (tokenBalance.value.isEqualTo(BN(balance)) && tokenBalance.label && tokenBalance.decimals) { if (tokenBalance.value.isEqualTo(BN(balance)) && tokenBalance.label && tokenBalance.decimals) {
return; return
} }
dispatch({ dispatch({
...@@ -299,90 +311,86 @@ export const sync = () => async (dispatch, getState) => { ...@@ -299,90 +311,86 @@ export const sync = () => async (dispatch, getState) => {
payload: { payload: {
tokenAddress, tokenAddress,
balanceOf: address, balanceOf: address,
balance: Balance(balance, symbol, decimals), balance: Balance(balance, symbol, decimals)
}, }
}); })
}); })
}); })
// Update Approvals // Update Approvals
Object.entries(watched.approvals) Object.entries(watched.approvals).forEach(([tokenAddress, token]) => {
.forEach(([tokenAddress, token]) => { const contract = contracts[tokenAddress] || new web3.eth.Contract(ERC20_ABI, tokenAddress)
const contract = contracts[tokenAddress] || new web3.eth.Contract(ERC20_ABI, tokenAddress); const contractBytes32 = contracts[tokenAddress] || new web3.eth.Contract(ERC20_WITH_BYTES_ABI, tokenAddress)
const contractBytes32 = contracts[tokenAddress] || new web3.eth.Contract(ERC20_WITH_BYTES_ABI, tokenAddress);
Object.entries(token) Object.entries(token).forEach(([tokenOwnerAddress, tokenOwner]) => {
.forEach(([ tokenOwnerAddress, tokenOwner ]) => {
tokenOwner.forEach(async spenderAddress => { tokenOwner.forEach(async spenderAddress => {
const approvalBalance = getApprovals(tokenAddress, tokenOwnerAddress, spenderAddress); const approvalBalance = getApprovals(tokenAddress, tokenOwnerAddress, spenderAddress)
const balance = await contract.methods.allowance(tokenOwnerAddress, spenderAddress).call(); const balance = await contract.methods.allowance(tokenOwnerAddress, spenderAddress).call()
const decimals = approvalBalance.decimals || await contract.methods.decimals().call(); const decimals = approvalBalance.decimals || (await contract.methods.decimals().call())
let symbol = approvalBalance.label; let symbol = approvalBalance.label
try { try {
symbol = symbol || await contract.methods.symbol().call(); symbol = symbol || (await contract.methods.symbol().call())
} catch (e) { } catch (e) {
try { try {
symbol = symbol || web3.utils.hexToString(await contractBytes32.methods.symbol().call()); symbol = symbol || web3.utils.hexToString(await contractBytes32.methods.symbol().call())
} catch (err) { } catch (err) {}
}
} }
if (approvalBalance.label && approvalBalance.value.isEqualTo(BN(balance))) { if (approvalBalance.label && approvalBalance.value.isEqualTo(BN(balance))) {
return; return
} }
dispatch(updateApprovals({ dispatch(
updateApprovals({
tokenAddress, tokenAddress,
tokenOwner: tokenOwnerAddress, tokenOwner: tokenOwnerAddress,
spender: spenderAddress, spender: spenderAddress,
balance: Balance(balance, symbol, decimals), balance: Balance(balance, symbol, decimals)
})); })
}); )
}); })
}); })
})
pending.forEach(async txId => { pending.forEach(async txId => {
try { try {
const data = await web3.eth.getTransactionReceipt(txId) || {}; const data = (await web3.eth.getTransactionReceipt(txId)) || {}
// If data is an empty obj, then it's still pending. // If data is an empty obj, then it's still pending.
if (!('status' in data)) { if (!('status' in data)) {
return; return
} }
dispatch({ dispatch({
type: REMOVE_PENDING_TX, type: REMOVE_PENDING_TX,
payload: txId, payload: txId
}); })
if (data.status) { if (data.status) {
dispatch({ dispatch({
type: ADD_CONFIRMED_TX, type: ADD_CONFIRMED_TX,
payload: txId, payload: txId
}); })
} else { } else {
// TODO: dispatch ADD_REJECTED_TX // TODO: dispatch ADD_REJECTED_TX
} }
} catch (err) { } catch (err) {
dispatch({ dispatch({
type: REMOVE_PENDING_TX, type: REMOVE_PENDING_TX,
payload: txId, payload: txId
}); })
// TODO: dispatch ADD_REJECTED_TX // TODO: dispatch ADD_REJECTED_TX
} }
})
}); }
};
export const startWatching = () => async (dispatch, getState) => { export const startWatching = () => async (dispatch, getState) => {
const { account } = getState().web3connect; const { account } = getState().web3connect
const timeout = !account const timeout = !account ? 1000 : 5000
? 1000
: 5000;
dispatch(sync()); dispatch(sync())
setTimeout(() => dispatch(startWatching()), timeout); setTimeout(() => dispatch(startWatching()), timeout)
}; }
export default function web3connectReducer(state = initialState, { type, payload }) { export default function web3connectReducer(state = initialState, { type, payload }) {
switch (type) { switch (type) {
...@@ -390,13 +398,13 @@ export default function web3connectReducer(state = initialState, { type, payload ...@@ -390,13 +398,13 @@ export default function web3connectReducer(state = initialState, { type, payload
return { return {
...state, ...state,
web3: payload, web3: payload,
initialized: true, initialized: true
}; }
case UPDATE_ACCOUNT: case UPDATE_ACCOUNT:
return { return {
...state, ...state,
account: payload, account: payload
}; }
case WATCH_ETH_BALANCE: case WATCH_ETH_BALANCE:
return { return {
...state, ...state,
...@@ -404,14 +412,14 @@ export default function web3connectReducer(state = initialState, { type, payload ...@@ -404,14 +412,14 @@ export default function web3connectReducer(state = initialState, { type, payload
...state.watched, ...state.watched,
balances: { balances: {
...state.watched.balances, ...state.watched.balances,
ethereum: [ ...state.watched.balances.ethereum, payload ], ethereum: [...state.watched.balances.ethereum, payload]
}, }
}, }
}; }
case WATCH_TOKEN_BALANCE: case WATCH_TOKEN_BALANCE:
const { watched } = state; const { watched } = state
const { balances } = watched; const { balances } = watched
const watchlist = balances[payload.tokenAddress] || []; const watchlist = balances[payload.tokenAddress] || []
return { return {
...state, ...state,
...@@ -419,10 +427,10 @@ export default function web3connectReducer(state = initialState, { type, payload ...@@ -419,10 +427,10 @@ export default function web3connectReducer(state = initialState, { type, payload
...watched, ...watched,
balances: { balances: {
...balances, ...balances,
[payload.tokenAddress]: [ ...watchlist, payload.balanceOf ], [payload.tokenAddress]: [...watchlist, payload.balanceOf]
}, }
}, }
}; }
case UPDATE_ETH_BALANCE: case UPDATE_ETH_BALANCE:
return { return {
...state, ...state,
...@@ -430,33 +438,33 @@ export default function web3connectReducer(state = initialState, { type, payload ...@@ -430,33 +438,33 @@ export default function web3connectReducer(state = initialState, { type, payload
...state.balances, ...state.balances,
ethereum: { ethereum: {
...state.balances.ethereum, ...state.balances.ethereum,
[payload.balanceOf]: payload.balance, [payload.balanceOf]: payload.balance
}, }
}, }
}; }
case UPDATE_TOKEN_BALANCE: case UPDATE_TOKEN_BALANCE:
const tokenBalances = state.balances[payload.tokenAddress] || {}; const tokenBalances = state.balances[payload.tokenAddress] || {}
return { return {
...state, ...state,
balances: { balances: {
...state.balances, ...state.balances,
[payload.tokenAddress]: { [payload.tokenAddress]: {
...tokenBalances, ...tokenBalances,
[payload.balanceOf]: payload.balance, [payload.balanceOf]: payload.balance
}, }
}, }
}; }
case ADD_CONTRACT: case ADD_CONTRACT:
return { return {
...state, ...state,
contracts: { contracts: {
...state.contracts, ...state.contracts,
[payload.address]: payload.contract, [payload.address]: payload.contract
}, }
}; }
case WATCH_APPROVALS: case WATCH_APPROVALS:
const token = state.watched.approvals[payload.tokenAddress] || {}; const token = state.watched.approvals[payload.tokenAddress] || {}
const tokenOwner = token[payload.tokenOwner] || []; const tokenOwner = token[payload.tokenOwner] || []
return { return {
...state, ...state,
...@@ -466,14 +474,14 @@ export default function web3connectReducer(state = initialState, { type, payload ...@@ -466,14 +474,14 @@ export default function web3connectReducer(state = initialState, { type, payload
...state.watched.approvals, ...state.watched.approvals,
[payload.tokenAddress]: { [payload.tokenAddress]: {
...token, ...token,
[payload.tokenOwner]: [ ...tokenOwner, payload.spender ], [payload.tokenOwner]: [...tokenOwner, payload.spender]
}, }
}, }
}, }
}; }
case UPDATE_APPROVALS: case UPDATE_APPROVALS:
const erc20 = state.approvals[payload.tokenAddress] || {}; const erc20 = state.approvals[payload.tokenAddress] || {}
const erc20Owner = erc20[payload.tokenOwner] || {}; const erc20Owner = erc20[payload.tokenOwner] || {}
return { return {
...state, ...state,
...@@ -483,72 +491,71 @@ export default function web3connectReducer(state = initialState, { type, payload ...@@ -483,72 +491,71 @@ export default function web3connectReducer(state = initialState, { type, payload
...erc20, ...erc20,
[payload.tokenOwner]: { [payload.tokenOwner]: {
...erc20Owner, ...erc20Owner,
[payload.spender]: payload.balance, [payload.spender]: payload.balance
}, }
}, }
}, }
}; }
case UPDATE_NETWORK_ID: case UPDATE_NETWORK_ID:
return { ...state, networkId: payload }; return { ...state, networkId: payload }
case ADD_PENDING_TX: case ADD_PENDING_TX:
return { return {
...state, ...state,
transactions: { transactions: {
...state.transactions, ...state.transactions,
pending: [ ...state.transactions.pending, payload ], pending: [...state.transactions.pending, payload]
}, }
}; }
case REMOVE_PENDING_TX: case REMOVE_PENDING_TX:
return { return {
...state, ...state,
transactions: { transactions: {
...state.transactions, ...state.transactions,
pending: state.transactions.pending.filter(id => id !== payload), pending: state.transactions.pending.filter(id => id !== payload)
}, }
}; }
case ADD_CONFIRMED_TX: case ADD_CONFIRMED_TX:
if (state.transactions.confirmed.includes(payload)) { if (state.transactions.confirmed.includes(payload)) {
return state; return state
} }
return { return {
...state, ...state,
transactions: { transactions: {
...state.transactions, ...state.transactions,
confirmed: [ ...state.transactions.confirmed, payload ], confirmed: [...state.transactions.confirmed, payload]
}, }
}; }
default: default:
return state; return state
} }
} }
// Connect Component // Connect Component
export class _Web3Connect extends Component { export class _Web3Connect extends Component {
static propTypes = { static propTypes = {
initialize: PropTypes.func.isRequired, initialize: PropTypes.func.isRequired
}; }
static defaultProps = { static defaultProps = {
initialize() {} initialize() {}
}; }
componentWillMount() { componentWillMount() {
this.props.initialize() this.props.initialize().then(this.props.startWatching())
.then(this.props.startWatching());
} }
render() { render() {
return <noscript />; return <noscript />
} }
} }
export const Web3Connect = connect( export const Web3Connect = connect(
({ web3connect }) => ({ ({ web3connect }) => ({
web3: web3connect.web3, web3: web3connect.web3
}), }),
dispatch => ({ dispatch => ({
initialize: () => dispatch(initialize()), initialize: () => dispatch(initialize()),
startWatching: () => dispatch(startWatching()), startWatching: () => dispatch(startWatching())
}), })
)(_Web3Connect); )(_Web3Connect)
export default function (matchmask = [], minMatchCharLength = 1) { export default function(matchmask = [], minMatchCharLength = 1) {
let matchedIndices = [] let matchedIndices = []
let start = -1 let start = -1
let end = -1 let end = -1
...@@ -10,7 +10,7 @@ export default function (matchmask = [], minMatchCharLength = 1) { ...@@ -10,7 +10,7 @@ export default function (matchmask = [], minMatchCharLength = 1) {
start = i start = i
} else if (!match && start !== -1) { } else if (!match && start !== -1) {
end = i - 1 end = i - 1
if ((end - start) + 1 >= minMatchCharLength) { if (end - start + 1 >= minMatchCharLength) {
matchedIndices.push([start, end]) matchedIndices.push([start, end])
} }
start = -1 start = -1
...@@ -18,7 +18,7 @@ export default function (matchmask = [], minMatchCharLength = 1) { ...@@ -18,7 +18,7 @@ export default function (matchmask = [], minMatchCharLength = 1) {
} }
// (i-1 - start) + 1 => i - start // (i-1 - start) + 1 => i - start
if (matchmask[i - 1] && (i - start) >= minMatchCharLength) { if (matchmask[i - 1] && i - start >= minMatchCharLength) {
matchedIndices.push([start, i - 1]) matchedIndices.push([start, i - 1])
} }
......
export default function (pattern) { export default function(pattern) {
let mask = {} let mask = {}
let len = pattern.length let len = pattern.length
......
// eslint-disable-next-line no-useless-escape // eslint-disable-next-line no-useless-escape
const SPECIAL_CHARS_REGEX = /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g const SPECIAL_CHARS_REGEX = /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g
export default function (text, pattern, tokenSeparator = / +/g) { export default function(text, pattern, tokenSeparator = / +/g) {
let regex = new RegExp(pattern.replace(SPECIAL_CHARS_REGEX, '\\$&').replace(tokenSeparator, '|')) let regex = new RegExp(pattern.replace(SPECIAL_CHARS_REGEX, '\\$&').replace(tokenSeparator, '|'))
let matches = text.match(regex) let matches = text.match(regex)
let isMatch = !!matches let isMatch = !!matches
......
export default function (pattern, { errors = 0, currentLocation = 0, expectedLocation = 0, distance = 100 }) { export default function(pattern, { errors = 0, currentLocation = 0, expectedLocation = 0, distance = 100 }) {
const accuracy = errors / pattern.length const accuracy = errors / pattern.length
const proximity = Math.abs(expectedLocation - currentLocation) const proximity = Math.abs(expectedLocation - currentLocation)
...@@ -7,5 +7,5 @@ export default function (pattern, { errors = 0, currentLocation = 0, expectedLoc ...@@ -7,5 +7,5 @@ export default function (pattern, { errors = 0, currentLocation = 0, expectedLoc
return proximity ? 1.0 : accuracy return proximity ? 1.0 : accuracy
} }
return accuracy + (proximity / distance) return accuracy + proximity / distance
} }
import bitapScore from './bitap_score'; import bitapScore from './bitap_score'
import matchedIndices from './bitap_matched_indices'; import matchedIndices from './bitap_matched_indices'
export default function (text, pattern, patternAlphabet, { location = 0, distance = 100, threshold = 0.6, findAllMatches = false, minMatchCharLength = 1 }) { export default function(
text,
pattern,
patternAlphabet,
{ location = 0, distance = 100, threshold = 0.6, findAllMatches = false, minMatchCharLength = 1 }
) {
const expectedLocation = location const expectedLocation = location
// Set starting location at beginning text and initialize the alphabet. // Set starting location at beginning text and initialize the alphabet.
const textLen = text.length const textLen = text.length
...@@ -63,7 +68,7 @@ export default function (text, pattern, patternAlphabet, { location = 0, distanc ...@@ -63,7 +68,7 @@ export default function (text, pattern, patternAlphabet, { location = 0, distanc
currentLocation: expectedLocation + binMid, currentLocation: expectedLocation + binMid,
expectedLocation, expectedLocation,
distance distance
}); })
if (score <= currentThreshold) { if (score <= currentThreshold) {
binMin = binMid binMin = binMid
...@@ -98,7 +103,7 @@ export default function (text, pattern, patternAlphabet, { location = 0, distanc ...@@ -98,7 +103,7 @@ export default function (text, pattern, patternAlphabet, { location = 0, distanc
// Subsequent passes: fuzzy match // Subsequent passes: fuzzy match
if (i !== 0) { if (i !== 0) {
bitArr[j] |= (((lastBitArr[j + 1] | lastBitArr[j]) << 1) | 1) | lastBitArr[j + 1] bitArr[j] |= ((lastBitArr[j + 1] | lastBitArr[j]) << 1) | 1 | lastBitArr[j + 1]
} }
if (bitArr[j] & mask) { if (bitArr[j] & mask) {
......
import bitapRegexSearch from './bitap_regex_search'; import bitapRegexSearch from './bitap_regex_search'
import bitapSearch from './bitap_search'; import bitapSearch from './bitap_search'
import patternAlphabet from './bitap_pattern_alphabet'; import patternAlphabet from './bitap_pattern_alphabet'
class Bitap { class Bitap {
constructor (pattern, { constructor(
pattern,
{
// Approximately where in the text is the pattern expected to be found? // Approximately where in the text is the pattern expected to be found?
location = 0, location = 0,
// Determines how close the match must be to the fuzzy location (specified above). // Determines how close the match must be to the fuzzy location (specified above).
...@@ -26,7 +28,8 @@ class Bitap { ...@@ -26,7 +28,8 @@ class Bitap {
findAllMatches = false, findAllMatches = false,
// Minimum number of characters that must be matched before a result is considered a match // Minimum number of characters that must be matched before a result is considered a match
minMatchCharLength = 1 minMatchCharLength = 1
}) { }
) {
this.options = { this.options = {
location, location,
distance, distance,
...@@ -45,7 +48,7 @@ class Bitap { ...@@ -45,7 +48,7 @@ class Bitap {
} }
} }
search (text) { search(text) {
if (!this.options.isCaseSensitive) { if (!this.options.isCaseSensitive) {
text = text.toLowerCase() text = text.toLowerCase()
} }
......
module.exports = obj => !Array.isArray ? Object.prototype.toString.call(obj) === '[object Array]' : Array.isArray(obj) module.exports = obj => (!Array.isArray ? Object.prototype.toString.call(obj) === '[object Array]' : Array.isArray(obj))
import Bitap from'./bitap'; import Bitap from './bitap'
const deepValue = require('./helpers/deep_value') const deepValue = require('./helpers/deep_value')
const isArray = require('./helpers/is_array') const isArray = require('./helpers/is_array')
class Fuse { class Fuse {
constructor (list, { constructor(
list,
{
// Approximately where in the text is the pattern expected to be found? // Approximately where in the text is the pattern expected to be found?
location = 0, location = 0,
// Determines how close the match must be to the fuzzy location (specified above). // Determines how close the match must be to the fuzzy location (specified above).
...@@ -37,7 +39,7 @@ class Fuse { ...@@ -37,7 +39,7 @@ class Fuse {
// The default will search nested paths *ie foo.bar.baz* // The default will search nested paths *ie foo.bar.baz*
getFn = deepValue, getFn = deepValue,
// Default sort function // Default sort function
sortFn = (a, b) => (a.score - b.score), sortFn = (a, b) => a.score - b.score,
// When true, the search algorithm will search individual words **and** the full string, // When true, the search algorithm will search individual words **and** the full string,
// computing the final score as a function of both. Note that when `tokenize` is `true`, // computing the final score as a function of both. Note that when `tokenize` is `true`,
// the `threshold`, `distance`, and `location` are inconsequential for individual tokens. // the `threshold`, `distance`, and `location` are inconsequential for individual tokens.
...@@ -51,7 +53,8 @@ class Fuse { ...@@ -51,7 +53,8 @@ class Fuse {
// Will print to the console. Useful for debugging. // Will print to the console. Useful for debugging.
verbose = false verbose = false
}) { }
) {
this.options = { this.options = {
location, location,
distance, distance,
...@@ -76,18 +79,15 @@ class Fuse { ...@@ -76,18 +79,15 @@ class Fuse {
this.setCollection(list) this.setCollection(list)
} }
setCollection (list) { setCollection(list) {
this.list = list this.list = list
return list return list
} }
search (pattern) { search(pattern) {
this._log(`---------\nSearch pattern: "${pattern}"`) this._log(`---------\nSearch pattern: "${pattern}"`)
const { const { tokenSearchers, fullSearcher } = this._prepareSearchers(pattern)
tokenSearchers,
fullSearcher
} = this._prepareSearchers(pattern)
let { weights, results } = this._search(tokenSearchers, fullSearcher) let { weights, results } = this._search(tokenSearchers, fullSearcher)
...@@ -100,7 +100,7 @@ class Fuse { ...@@ -100,7 +100,7 @@ class Fuse {
return this._format(results) return this._format(results)
} }
_prepareSearchers (pattern = '') { _prepareSearchers(pattern = '') {
const tokenSearchers = [] const tokenSearchers = []
if (this.options.tokenize) { if (this.options.tokenize) {
...@@ -116,7 +116,7 @@ class Fuse { ...@@ -116,7 +116,7 @@ class Fuse {
return { tokenSearchers, fullSearcher } return { tokenSearchers, fullSearcher }
} }
_search (tokenSearchers = [], fullSearcher) { _search(tokenSearchers = [], fullSearcher) {
const list = this.list const list = this.list
const resultMap = {} const resultMap = {}
const results = [] const results = []
...@@ -126,17 +126,20 @@ class Fuse { ...@@ -126,17 +126,20 @@ class Fuse {
if (typeof list[0] === 'string') { if (typeof list[0] === 'string') {
// Iterate over every item // Iterate over every item
for (let i = 0, len = list.length; i < len; i += 1) { for (let i = 0, len = list.length; i < len; i += 1) {
this._analyze({ this._analyze(
{
key: '', key: '',
value: list[i], value: list[i],
record: i, record: i,
index: i index: i
}, { },
{
resultMap, resultMap,
results, results,
tokenSearchers, tokenSearchers,
fullSearcher fullSearcher
}) }
)
} }
return { weights: null, results } return { weights: null, results }
...@@ -152,7 +155,7 @@ class Fuse { ...@@ -152,7 +155,7 @@ class Fuse {
let key = this.options.keys[j] let key = this.options.keys[j]
if (typeof key !== 'string') { if (typeof key !== 'string') {
weights[key.name] = { weights[key.name] = {
weight: (1 - key.weight) || 1 weight: 1 - key.weight || 1
} }
if (key.weight <= 0 || key.weight > 1) { if (key.weight <= 0 || key.weight > 1) {
throw new Error('Key weight has to be > 0 and <= 1') throw new Error('Key weight has to be > 0 and <= 1')
...@@ -164,24 +167,30 @@ class Fuse { ...@@ -164,24 +167,30 @@ class Fuse {
} }
} }
this._analyze({ this._analyze(
{
key, key,
value: this.options.getFn(item, key), value: this.options.getFn(item, key),
record: item, record: item,
index: i index: i
}, { },
{
resultMap, resultMap,
results, results,
tokenSearchers, tokenSearchers,
fullSearcher fullSearcher
}) }
)
} }
} }
return { weights, results } return { weights, results }
} }
_analyze ({ key, arrayIndex = -1, value, record, index }, { tokenSearchers = [], fullSearcher = [], resultMap = {}, results = [] }) { _analyze(
{ key, arrayIndex = -1, value, record, index },
{ tokenSearchers = [], fullSearcher = [], resultMap = {}, results = [] }
) {
// Check if the texvaluet can be searched // Check if the texvaluet can be searched
if (value === undefined || value === null) { if (value === undefined || value === null) {
return return
...@@ -250,7 +259,8 @@ class Fuse { ...@@ -250,7 +259,8 @@ class Fuse {
this._log('Score average:', finalScore) this._log('Score average:', finalScore)
let checkTextMatches = (this.options.tokenize && this.options.matchAllTokens) ? numTextMatches >= tokenSearchers.length : true let checkTextMatches =
this.options.tokenize && this.options.matchAllTokens ? numTextMatches >= tokenSearchers.length : true
this._log(`\nCheck Matches: ${checkTextMatches}`) this._log(`\nCheck Matches: ${checkTextMatches}`)
...@@ -272,13 +282,15 @@ class Fuse { ...@@ -272,13 +282,15 @@ class Fuse {
// Add it to the raw result list // Add it to the raw result list
resultMap[index] = { resultMap[index] = {
item: record, item: record,
output: [{ output: [
{
key, key,
arrayIndex, arrayIndex,
value, value,
score: finalScore, score: finalScore,
matchedIndices: mainSearchResult.matchedIndices matchedIndices: mainSearchResult.matchedIndices
}] }
]
} }
results.push(resultMap[index]) results.push(resultMap[index])
...@@ -286,23 +298,26 @@ class Fuse { ...@@ -286,23 +298,26 @@ class Fuse {
} }
} else if (isArray(value)) { } else if (isArray(value)) {
for (let i = 0, len = value.length; i < len; i += 1) { for (let i = 0, len = value.length; i < len; i += 1) {
this._analyze({ this._analyze(
{
key, key,
arrayIndex: i, arrayIndex: i,
value: value[i], value: value[i],
record, record,
index index
}, { },
{
resultMap, resultMap,
results, results,
tokenSearchers, tokenSearchers,
fullSearcher fullSearcher
}) }
)
} }
} }
} }
_computeScore (weights, results) { _computeScore(weights, results) {
this._log('\n\nComputing score:\n') this._log('\n\nComputing score:\n')
for (let i = 0, len = results.length; i < len; i += 1) { for (let i = 0, len = results.length; i < len; i += 1) {
...@@ -314,7 +329,7 @@ class Fuse { ...@@ -314,7 +329,7 @@ class Fuse {
for (let j = 0; j < scoreLen; j += 1) { for (let j = 0; j < scoreLen; j += 1) {
let weight = weights ? weights[output[j].key].weight : 1 let weight = weights ? weights[output[j].key].weight : 1
let score = weight === 1 ? output[j].score : (output[j].score || 0.001) let score = weight === 1 ? output[j].score : output[j].score || 0.001
let nScore = score * weight let nScore = score * weight
if (weight !== 1) { if (weight !== 1) {
...@@ -331,12 +346,12 @@ class Fuse { ...@@ -331,12 +346,12 @@ class Fuse {
} }
} }
_sort (results) { _sort(results) {
this._log('\n\nSorting....') this._log('\n\nSorting....')
results.sort(this.options.sortFn) results.sort(this.options.sortFn)
} }
_format (results) { _format(results) {
const finalOutput = [] const finalOutput = []
if (this.options.verbose) { if (this.options.verbose) {
...@@ -404,7 +419,7 @@ class Fuse { ...@@ -404,7 +419,7 @@ class Fuse {
return finalOutput return finalOutput
} }
_log () { _log() {
if (this.options.verbose) { if (this.options.verbose) {
console.log(...arguments) console.log(...arguments)
} }
......
export function retry(func, retryCount=5) { export function retry(func, retryCount = 5) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
func().then((...args) => { func().then(
resolve(...args); (...args) => {
}, () => { resolve(...args)
},
() => {
if (retryCount === 0) { if (retryCount === 0) {
return reject(); return reject()
} }
setTimeout(() => retry(func, retryCount - 1).then(resolve, reject), 50); setTimeout(() => retry(func, retryCount - 1).then(resolve, reject), 50)
}); }
}); )
})
} }
export default function promisify(web3, methodName, ...args) { export default function promisify(web3, methodName, ...args) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!web3) { if (!web3) {
reject(new Error('No Web3 object')); reject(new Error('No Web3 object'))
return; return
} }
const method = web3.eth[methodName]; const method = web3.eth[methodName]
if (!method) { if (!method) {
reject(new Error(`Cannot find web3.eth.${methodName}`)); reject(new Error(`Cannot find web3.eth.${methodName}`))
return; return
} }
method(...args, (error, data) => { method(...args, (error, data) => {
if (error) { if (error) {
reject(error); reject(error)
return; return
} }
resolve(data); resolve(data)
})
}) })
});
} }
import promisify from "./web3-promisfy"; import promisify from './web3-promisfy'
export function getBlockDeadline(web3, deadline) { export function getBlockDeadline(web3, deadline) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
const blockNumber = await promisify(web3, 'getBlockNumber'); const blockNumber = await promisify(web3, 'getBlockNumber')
if (!blockNumber && blockNumber !== 0) { if (!blockNumber && blockNumber !== 0) {
return reject(); return reject()
} }
const block = await promisify(web3, 'getBlock', blockNumber); const block = await promisify(web3, 'getBlock', blockNumber)
if (!block) { if (!block) {
return reject(); return reject()
} }
resolve(block.timestamp + deadline); resolve(block.timestamp + deadline)
}); })
} }
import i18n from "i18next"; import i18n from 'i18next'
import Backend from 'i18next-xhr-backend'; import Backend from 'i18next-xhr-backend'
import LanguageDetector from 'i18next-browser-languagedetector'; import LanguageDetector from 'i18next-browser-languagedetector'
import { reactI18nextModule } from "react-i18next"; import { reactI18nextModule } from 'react-i18next'
const resources = { const resources = {
loadPath: `./locales/{{lng}}.json` loadPath: `./locales/{{lng}}.json`
...@@ -20,13 +20,13 @@ i18n ...@@ -20,13 +20,13 @@ i18n
// for all options read: https://www.i18next.com/overview/configuration-options // for all options read: https://www.i18next.com/overview/configuration-options
.init({ .init({
backend: resources, backend: resources,
fallbackLng: "en", fallbackLng: 'en',
keySeparator: false, keySeparator: false,
interpolation: { interpolation: {
escapeValue: false escapeValue: false
} }
}); })
export default i18n; export default i18n
import React from 'react'; import React from 'react'
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'; import { Provider } from 'react-redux'
import ReactGA from 'react-ga'; import ReactGA from 'react-ga'
import './i18n'; import './i18n'
import App from './pages/App'; import App from './pages/App'
import store from './store'; import store from './store'
import './index.scss'; import './index.scss'
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
// ReactGA.initialize('UA-128182339-02'); // ReactGA.initialize('UA-128182339-02');
} else { } else {
ReactGA.initialize('UA-128182339-1'); ReactGA.initialize('UA-128182339-1')
} }
ReactGA.pageview(window.location.pathname + window.location.search); ReactGA.pageview(window.location.pathname + window.location.search)
window.addEventListener('load', function() { window.addEventListener('load', function() {
ReactDOM.render( ReactDOM.render(
<Provider store={store}> <Provider store={store}>
<App /> <App />
</Provider> </Provider>,
, document.getElementById('root') document.getElementById('root')
); )
}); })
@import url('https://rsms.me/inter/inter-ui.css'); @import url('https://rsms.me/inter/inter-ui.css');
@import "./variables.scss"; @import './variables.scss';
html, body { html,
body {
margin: 0; margin: 0;
padding: 0; padding: 0;
font-family: "Inter UI", sans-serif; font-family: 'Inter UI', sans-serif;
font-size: 16px; font-size: 16px;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
...@@ -22,7 +23,7 @@ html, body { ...@@ -22,7 +23,7 @@ html, body {
background-color: $white; background-color: $white;
z-index: 100; z-index: 100;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0); -webkit-tap-highlight-color: rgba(255, 255, 255, 0);
@media only screen and (min-width : 768px) { @media only screen and (min-width: 768px) {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
...@@ -40,13 +41,17 @@ html, body { ...@@ -40,13 +41,17 @@ html, body {
border: 1px solid transparent; /* Light grey */ border: 1px solid transparent; /* Light grey */
border-top: 1px solid $royal-blue; /* Blue */ border-top: 1px solid $royal-blue; /* Blue */
border-radius: 50%; border-radius: 50%;
width: .75rem; width: 0.75rem;
height: .75rem; height: 0.75rem;
margin-right: .25rem; margin-right: 0.25rem;
animation: spin 1s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite; animation: spin 1s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite;
} }
@keyframes spin { @keyframes spin {
0% { transform: rotate(0deg); } 0% {
100% { transform: rotate(360deg); } transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
} }
...@@ -2,17 +2,203 @@ ...@@ -2,17 +2,203 @@
/* /*
@asset(/libraries/qr-scanner/qr-scanner-worker.min.js) */ @asset(/libraries/qr-scanner/qr-scanner-worker.min.js) */
'use strict';export default class QrScanner{constructor(video,onDecode,canvasSize=QrScanner.DEFAULT_CANVAS_SIZE){this.$video=video;this.$canvas=document.createElement("canvas");this._onDecode=onDecode;this._active=false;this.$canvas.width=canvasSize;this.$canvas.height=canvasSize;this._sourceRect={x:0,y:0,width:canvasSize,height:canvasSize};this.$video.addEventListener("canplay",()=>this._updateSourceRect());this.$video.addEventListener("play",()=>{this._updateSourceRect();this._scanFrame()},false); 'use strict'
this._qrWorker=new Worker(QrScanner.WORKER_PATH)}_updateSourceRect(){const smallestDimension=Math.min(this.$video.videoWidth,this.$video.videoHeight);const sourceRectSize=Math.round(2/3*smallestDimension);this._sourceRect.width=this._sourceRect.height=sourceRectSize;this._sourceRect.x=(this.$video.videoWidth-sourceRectSize)/2;this._sourceRect.y=(this.$video.videoHeight-sourceRectSize)/2}_scanFrame(){if(this.$video.paused||this.$video.ended)return false;requestAnimationFrame(()=>{QrScanner.scanImage(this.$video, export default class QrScanner {
this._sourceRect,this._qrWorker,this.$canvas,true).then(this._onDecode,(error)=>{if(error!=="QR code not found.")console.error(error)}).then(()=>this._scanFrame())})}_getCameraStream(facingMode,exact=false){const constraintsToTry=[{width:{min:1024}},{width:{min:768}},{}];if(facingMode){if(exact)facingMode={exact:facingMode};constraintsToTry.forEach((constraint)=>constraint.facingMode=facingMode)}return this._getMatchingCameraStream(constraintsToTry)}_getMatchingCameraStream(constraintsToTry){if(constraintsToTry.length=== constructor(video, onDecode, canvasSize = QrScanner.DEFAULT_CANVAS_SIZE) {
0)return Promise.reject("Camera not found.");return navigator.mediaDevices.getUserMedia({video:constraintsToTry.shift()}).catch(()=>this._getMatchingCameraStream(constraintsToTry))}start(){if(this._active)return Promise.resolve();this._active=true;clearTimeout(this._offTimeout);let facingMode="environment";return this._getCameraStream("environment",true).catch(()=>{facingMode="user";return this._getCameraStream()}).then((stream)=>{this.$video.srcObject=stream;this._setVideoMirror(facingMode)}).catch((e)=> this.$video = video
{this._active=false;throw e;})}stop(){if(!this._active)return;this._active=false;this.$video.pause();this._offTimeout=setTimeout(()=>{this.$video.srcObject.getTracks()[0].stop();this.$video.srcObject=null},3E3)}_setVideoMirror(facingMode){const scaleFactor=facingMode==="user"?-1:1;this.$video.style.transform="scaleX("+scaleFactor+")"}setGrayscaleWeights(red,green,blue){this._qrWorker.postMessage({type:"grayscaleWeights",data:{red,green,blue}})}static scanImage(imageOrFileOrUrl,sourceRect=null,worker= this.$canvas = document.createElement('canvas')
null,canvas=null,fixedCanvasSize=false,alsoTryWithoutSourceRect=false){const promise=new Promise((resolve,reject)=>{worker=worker||new Worker(QrScanner.WORKER_PATH);let timeout,onMessage,onError;onMessage=(event)=>{if(event.data.type!=="qrResult")return;worker.removeEventListener("message",onMessage);worker.removeEventListener("error",onError);clearTimeout(timeout);if(event.data.data!==null)resolve(event.data.data);else reject("QR code not found.")};onError=()=>{worker.removeEventListener("message", this._onDecode = onDecode
onMessage);worker.removeEventListener("error",onError);clearTimeout(timeout);reject("Worker error.")};worker.addEventListener("message",onMessage);worker.addEventListener("error",onError);timeout=setTimeout(onError,3E3);QrScanner._loadImage(imageOrFileOrUrl).then((image)=>{const imageData=QrScanner._getImageData(image,sourceRect,canvas,fixedCanvasSize);worker.postMessage({type:"decode",data:imageData},[imageData.data.buffer])}).catch(reject)});if(sourceRect&&alsoTryWithoutSourceRect)return promise.catch(()=> this._active = false
QrScanner.scanImage(imageOrFileOrUrl,null,worker,canvas,fixedCanvasSize));else return promise}static _getImageData(image,sourceRect=null,canvas=null,fixedCanvasSize=false){canvas=canvas||document.createElement("canvas");const sourceRectX=sourceRect&&sourceRect.x?sourceRect.x:0;const sourceRectY=sourceRect&&sourceRect.y?sourceRect.y:0;const sourceRectWidth=sourceRect&&sourceRect.width?sourceRect.width:image.width||image.videoWidth;const sourceRectHeight=sourceRect&&sourceRect.height?sourceRect.height: this.$canvas.width = canvasSize
image.height||image.videoHeight;if(!fixedCanvasSize&&(canvas.width!==sourceRectWidth||canvas.height!==sourceRectHeight)){canvas.width=sourceRectWidth;canvas.height=sourceRectHeight}const context=canvas.getContext("2d",{alpha:false});context.imageSmoothingEnabled=false;context.drawImage(image,sourceRectX,sourceRectY,sourceRectWidth,sourceRectHeight,0,0,canvas.width,canvas.height);return context.getImageData(0,0,canvas.width,canvas.height)}static _loadImage(imageOrFileOrUrl){if(imageOrFileOrUrl instanceof this.$canvas.height = canvasSize
HTMLCanvasElement||imageOrFileOrUrl instanceof HTMLVideoElement||window.ImageBitmap&&imageOrFileOrUrl instanceof window.ImageBitmap||window.OffscreenCanvas&&imageOrFileOrUrl instanceof window.OffscreenCanvas)return Promise.resolve(imageOrFileOrUrl);else if(imageOrFileOrUrl instanceof Image)return QrScanner._awaitImageLoad(imageOrFileOrUrl).then(()=>imageOrFileOrUrl);else if(imageOrFileOrUrl instanceof File||imageOrFileOrUrl instanceof URL||typeof imageOrFileOrUrl==="string"){const image=new Image; this._sourceRect = { x: 0, y: 0, width: canvasSize, height: canvasSize }
if(imageOrFileOrUrl instanceof File)image.src=URL.createObjectURL(imageOrFileOrUrl);else image.src=imageOrFileOrUrl;return QrScanner._awaitImageLoad(image).then(()=>{if(imageOrFileOrUrl instanceof File)URL.revokeObjectURL(image.src);return image})}else return Promise.reject("Unsupported image type.")}static _awaitImageLoad(image){return new Promise((resolve,reject)=>{if(image.complete&&image.naturalWidth!==0)resolve();else{let onLoad,onError;onLoad=()=>{image.removeEventListener("load",onLoad);image.removeEventListener("error", this.$video.addEventListener('canplay', () => this._updateSourceRect())
onError);resolve()};onError=()=>{image.removeEventListener("load",onLoad);image.removeEventListener("error",onError);reject("Image load error")};image.addEventListener("load",onLoad);image.addEventListener("error",onError)}})}}QrScanner.DEFAULT_CANVAS_SIZE=400;QrScanner.WORKER_PATH="/libraries/qr-scanner/qr-scanner-worker.min.js"; this.$video.addEventListener(
'play',
() => {
this._updateSourceRect()
this._scanFrame()
},
false
)
this._qrWorker = new Worker(QrScanner.WORKER_PATH)
}
_updateSourceRect() {
const smallestDimension = Math.min(this.$video.videoWidth, this.$video.videoHeight)
const sourceRectSize = Math.round((2 / 3) * smallestDimension)
this._sourceRect.width = this._sourceRect.height = sourceRectSize
this._sourceRect.x = (this.$video.videoWidth - sourceRectSize) / 2
this._sourceRect.y = (this.$video.videoHeight - sourceRectSize) / 2
}
_scanFrame() {
if (this.$video.paused || this.$video.ended) return false
requestAnimationFrame(() => {
QrScanner.scanImage(this.$video, this._sourceRect, this._qrWorker, this.$canvas, true)
.then(this._onDecode, error => {
if (error !== 'QR code not found.') console.error(error)
})
.then(() => this._scanFrame())
})
}
_getCameraStream(facingMode, exact = false) {
const constraintsToTry = [{ width: { min: 1024 } }, { width: { min: 768 } }, {}]
if (facingMode) {
if (exact) facingMode = { exact: facingMode }
constraintsToTry.forEach(constraint => (constraint.facingMode = facingMode))
}
return this._getMatchingCameraStream(constraintsToTry)
}
_getMatchingCameraStream(constraintsToTry) {
if (constraintsToTry.length === 0) return Promise.reject('Camera not found.')
return navigator.mediaDevices
.getUserMedia({ video: constraintsToTry.shift() })
.catch(() => this._getMatchingCameraStream(constraintsToTry))
}
start() {
if (this._active) return Promise.resolve()
this._active = true
clearTimeout(this._offTimeout)
let facingMode = 'environment'
return this._getCameraStream('environment', true)
.catch(() => {
facingMode = 'user'
return this._getCameraStream()
})
.then(stream => {
this.$video.srcObject = stream
this._setVideoMirror(facingMode)
})
.catch(e => {
this._active = false
throw e
})
}
stop() {
if (!this._active) return
this._active = false
this.$video.pause()
this._offTimeout = setTimeout(() => {
this.$video.srcObject.getTracks()[0].stop()
this.$video.srcObject = null
}, 3e3)
}
_setVideoMirror(facingMode) {
const scaleFactor = facingMode === 'user' ? -1 : 1
this.$video.style.transform = 'scaleX(' + scaleFactor + ')'
}
setGrayscaleWeights(red, green, blue) {
this._qrWorker.postMessage({ type: 'grayscaleWeights', data: { red, green, blue } })
}
static scanImage(
imageOrFileOrUrl,
sourceRect = null,
worker = null,
canvas = null,
fixedCanvasSize = false,
alsoTryWithoutSourceRect = false
) {
const promise = new Promise((resolve, reject) => {
worker = worker || new Worker(QrScanner.WORKER_PATH)
let timeout, onMessage, onError
onMessage = event => {
if (event.data.type !== 'qrResult') return
worker.removeEventListener('message', onMessage)
worker.removeEventListener('error', onError)
clearTimeout(timeout)
if (event.data.data !== null) resolve(event.data.data)
else reject('QR code not found.')
}
onError = () => {
worker.removeEventListener('message', onMessage)
worker.removeEventListener('error', onError)
clearTimeout(timeout)
reject('Worker error.')
}
worker.addEventListener('message', onMessage)
worker.addEventListener('error', onError)
timeout = setTimeout(onError, 3e3)
QrScanner._loadImage(imageOrFileOrUrl)
.then(image => {
const imageData = QrScanner._getImageData(image, sourceRect, canvas, fixedCanvasSize)
worker.postMessage({ type: 'decode', data: imageData }, [imageData.data.buffer])
})
.catch(reject)
})
if (sourceRect && alsoTryWithoutSourceRect)
return promise.catch(() => QrScanner.scanImage(imageOrFileOrUrl, null, worker, canvas, fixedCanvasSize))
else return promise
}
static _getImageData(image, sourceRect = null, canvas = null, fixedCanvasSize = false) {
canvas = canvas || document.createElement('canvas')
const sourceRectX = sourceRect && sourceRect.x ? sourceRect.x : 0
const sourceRectY = sourceRect && sourceRect.y ? sourceRect.y : 0
const sourceRectWidth = sourceRect && sourceRect.width ? sourceRect.width : image.width || image.videoWidth
const sourceRectHeight = sourceRect && sourceRect.height ? sourceRect.height : image.height || image.videoHeight
if (!fixedCanvasSize && (canvas.width !== sourceRectWidth || canvas.height !== sourceRectHeight)) {
canvas.width = sourceRectWidth
canvas.height = sourceRectHeight
}
const context = canvas.getContext('2d', { alpha: false })
context.imageSmoothingEnabled = false
context.drawImage(
image,
sourceRectX,
sourceRectY,
sourceRectWidth,
sourceRectHeight,
0,
0,
canvas.width,
canvas.height
)
return context.getImageData(0, 0, canvas.width, canvas.height)
}
static _loadImage(imageOrFileOrUrl) {
if (
imageOrFileOrUrl instanceof HTMLCanvasElement ||
imageOrFileOrUrl instanceof HTMLVideoElement ||
(window.ImageBitmap && imageOrFileOrUrl instanceof window.ImageBitmap) ||
(window.OffscreenCanvas && imageOrFileOrUrl instanceof window.OffscreenCanvas)
)
return Promise.resolve(imageOrFileOrUrl)
else if (imageOrFileOrUrl instanceof Image)
return QrScanner._awaitImageLoad(imageOrFileOrUrl).then(() => imageOrFileOrUrl)
else if (
imageOrFileOrUrl instanceof File ||
imageOrFileOrUrl instanceof URL ||
typeof imageOrFileOrUrl === 'string'
) {
const image = new Image()
if (imageOrFileOrUrl instanceof File) image.src = URL.createObjectURL(imageOrFileOrUrl)
else image.src = imageOrFileOrUrl
return QrScanner._awaitImageLoad(image).then(() => {
if (imageOrFileOrUrl instanceof File) URL.revokeObjectURL(image.src)
return image
})
} else return Promise.reject('Unsupported image type.')
}
static _awaitImageLoad(image) {
return new Promise((resolve, reject) => {
if (image.complete && image.naturalWidth !== 0) resolve()
else {
let onLoad, onError
onLoad = () => {
image.removeEventListener('load', onLoad)
image.removeEventListener('error', onError)
resolve()
}
onError = () => {
image.removeEventListener('load', onLoad)
image.removeEventListener('error', onError)
reject('Image load error')
}
image.addEventListener('load', onLoad)
image.addEventListener('error', onError)
}
})
}
}
QrScanner.DEFAULT_CANVAS_SIZE = 400
QrScanner.WORKER_PATH = '/libraries/qr-scanner/qr-scanner-worker.min.js'
//# sourceMappingURL=qr-scanner.min.js.map //# sourceMappingURL=qr-scanner.min.js.map
import React, { Component } from 'react'; import React, { Component } from 'react'
import { connect } from 'react-redux'; import { connect } from 'react-redux'
import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom'; import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom'
import MediaQuery from 'react-responsive'; import MediaQuery from 'react-responsive'
import { Web3Connect, startWatching, initialize } from '../ducks/web3connect'; import { Web3Connect, startWatching, initialize } from '../ducks/web3connect'
import { setAddresses } from '../ducks/addresses'; import { setAddresses } from '../ducks/addresses'
import Header from '../components/Header'; import Header from '../components/Header'
import Swap from './Swap'; import Swap from './Swap'
import Send from './Send'; import Send from './Send'
import Pool from './Pool'; import Pool from './Pool'
import './App.scss'; import './App.scss'
class App extends Component { class App extends Component {
componentWillMount() { componentWillMount() {
const { initialize, startWatching} = this.props; const { initialize, startWatching } = this.props
initialize().then(startWatching); initialize().then(startWatching)
}; }
componentWillUpdate() { componentWillUpdate() {
const { web3, setAddresses } = this.props; const { web3, setAddresses } = this.props
if (this.hasSetNetworkId || !web3 || !web3.eth || !web3.eth.net || !web3.eth.net.getId) { if (this.hasSetNetworkId || !web3 || !web3.eth || !web3.eth.net || !web3.eth.net.getId) {
return; return
} }
web3.eth.net.getId((err, networkId) => { web3.eth.net.getId((err, networkId) => {
if (!err && !this.hasSetNetworkId) { if (!err && !this.hasSetNetworkId) {
setAddresses(networkId); setAddresses(networkId)
this.hasSetNetworkId = true; this.hasSetNetworkId = true
} }
}); })
} }
render() { render() {
if (!this.props.initialized) { if (!this.props.initialized) {
return <noscript />; return <noscript />
} }
return ( return (
...@@ -56,7 +56,7 @@ class App extends Component { ...@@ -56,7 +56,7 @@ class App extends Component {
</Switch> </Switch>
</BrowserRouter> </BrowserRouter>
</div> </div>
); )
} }
} }
...@@ -64,11 +64,11 @@ export default connect( ...@@ -64,11 +64,11 @@ export default connect(
state => ({ state => ({
account: state.web3connect.account, account: state.web3connect.account,
initialized: state.web3connect.initialized, initialized: state.web3connect.initialized,
web3: state.web3connect.web3, web3: state.web3connect.web3
}), }),
dispatch => ({ dispatch => ({
setAddresses: networkId => dispatch(setAddresses(networkId)), setAddresses: networkId => dispatch(setAddresses(networkId)),
initialize: () => dispatch(initialize()), initialize: () => dispatch(initialize()),
startWatching: () => dispatch(startWatching()), startWatching: () => dispatch(startWatching())
}), })
)(App); )(App)
@import "../variables.scss"; @import '../variables.scss';
.app { .app {
&__wrapper { &__wrapper {
......
import React from 'react'; import React from 'react'
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom'
import App from './App'; import App from './App'
it('renders without crashing', () => { it('renders without crashing', () => {
const div = document.createElement('div'); const div = document.createElement('div')
ReactDOM.render(<App />, div); ReactDOM.render(<App />, div)
ReactDOM.unmountComponentAtNode(div); ReactDOM.unmountComponentAtNode(div)
}); })
import React, { Component } from 'react'; import React, { Component } from 'react'
import { connect } from 'react-redux'; import { connect } from 'react-redux'
import PropTypes from 'prop-types'; import PropTypes from 'prop-types'
import classnames from "classnames"; import classnames from 'classnames'
import { withNamespaces } from 'react-i18next'; import { withNamespaces } from 'react-i18next'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'; import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import OversizedPanel from '../../components/OversizedPanel'; import OversizedPanel from '../../components/OversizedPanel'
import ContextualInfo from '../../components/ContextualInfo'; import ContextualInfo from '../../components/ContextualInfo'
import NavigationTabs from '../../components/NavigationTabs'; import NavigationTabs from '../../components/NavigationTabs'
import { selectors, addPendingTx } from '../../ducks/web3connect'; import { selectors, addPendingTx } from '../../ducks/web3connect'
import PlusBlue from '../../assets/images/plus-blue.svg'; import PlusBlue from '../../assets/images/plus-blue.svg'
import PlusGrey from '../../assets/images/plus-grey.svg'; import PlusGrey from '../../assets/images/plus-grey.svg'
import { getBlockDeadline } from '../../helpers/web3-utils'; import { getBlockDeadline } from '../../helpers/web3-utils'
import { retry } from '../../helpers/promise-utils'; import { retry } from '../../helpers/promise-utils'
import ModeSelector from './ModeSelector'; import ModeSelector from './ModeSelector'
import {BigNumber as BN} from 'bignumber.js'; import { BigNumber as BN } from 'bignumber.js'
import EXCHANGE_ABI from '../../abi/exchange'; import EXCHANGE_ABI from '../../abi/exchange'
import "./pool.scss"; import './pool.scss'
import ReactGA from "react-ga"; import ReactGA from 'react-ga'
const INPUT = 0; const INPUT = 0
const OUTPUT = 1; const OUTPUT = 1
class AddLiquidity extends Component { class AddLiquidity extends Component {
static propTypes = { static propTypes = {
...@@ -28,9 +28,9 @@ class AddLiquidity extends Component { ...@@ -28,9 +28,9 @@ class AddLiquidity extends Component {
selectors: PropTypes.func.isRequired, selectors: PropTypes.func.isRequired,
balances: PropTypes.object.isRequired, balances: PropTypes.object.isRequired,
exchangeAddresses: PropTypes.shape({ exchangeAddresses: PropTypes.shape({
fromToken: PropTypes.object.isRequired, fromToken: PropTypes.object.isRequired
}).isRequired, }).isRequired
}; }
state = { state = {
inputValue: '', inputValue: '',
...@@ -38,22 +38,23 @@ class AddLiquidity extends Component { ...@@ -38,22 +38,23 @@ class AddLiquidity extends Component {
inputCurrency: 'ETH', inputCurrency: 'ETH',
outputCurrency: '', outputCurrency: '',
lastEditedField: '', lastEditedField: '',
totalSupply: BN(0), totalSupply: BN(0)
}; }
reset = () => { reset = () => {
this.setState({ this.setState({
inputValue: '', inputValue: '',
outputValue: '', outputValue: '',
lastEditedField: '', lastEditedField: ''
}); })
}; }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const { t, isConnected, account, exchangeAddresses, balances, web3 } = this.props; const { t, isConnected, account, exchangeAddresses, balances, web3 } = this.props
const { inputValue, outputValue, inputCurrency, outputCurrency, lastEditedField } = this.state; const { inputValue, outputValue, inputCurrency, outputCurrency, lastEditedField } = this.state
return isConnected !== nextProps.isConnected || return (
isConnected !== nextProps.isConnected ||
t !== nextProps.t || t !== nextProps.t ||
account !== nextProps.account || account !== nextProps.account ||
exchangeAddresses !== nextProps.exchangeAddresses || exchangeAddresses !== nextProps.exchangeAddresses ||
...@@ -63,308 +64,337 @@ class AddLiquidity extends Component { ...@@ -63,308 +64,337 @@ class AddLiquidity extends Component {
outputValue !== nextState.outputValue || outputValue !== nextState.outputValue ||
inputCurrency !== nextState.inputCurrency || inputCurrency !== nextState.inputCurrency ||
outputCurrency !== nextState.outputCurrency || outputCurrency !== nextState.outputCurrency ||
lastEditedField !== nextState.lastEditedField; lastEditedField !== nextState.lastEditedField
)
} }
componentWillReceiveProps() { componentWillReceiveProps() {
this.recalcForm(); this.recalcForm()
} }
recalcForm = async () => { recalcForm = async () => {
const { outputCurrency, inputValue, outputValue, lastEditedField, totalSupply: oldTotalSupply } = this.state
const { const {
outputCurrency, exchangeAddresses: { fromToken },
inputValue, web3
outputValue, } = this.props
lastEditedField, const exchangeAddress = fromToken[outputCurrency]
totalSupply: oldTotalSupply, const exchangeRate = this.getExchangeRate()
} = this.state; const append = {}
const { exchangeAddresses: { fromToken }, web3 } = this.props;
const exchangeAddress = fromToken[outputCurrency];
const exchangeRate = this.getExchangeRate();
const append = {};
if (!outputCurrency || this.isNewExchange() || !web3) { if (!outputCurrency || this.isNewExchange() || !web3) {
return; return
} }
const exchange = new web3.eth.Contract(EXCHANGE_ABI, exchangeAddress); const exchange = new web3.eth.Contract(EXCHANGE_ABI, exchangeAddress)
const totalSupply = await exchange.methods.totalSupply().call(); const totalSupply = await exchange.methods.totalSupply().call()
if (!oldTotalSupply.isEqualTo(BN(totalSupply))) { if (!oldTotalSupply.isEqualTo(BN(totalSupply))) {
append.totalSupply = BN(totalSupply); append.totalSupply = BN(totalSupply)
} }
if (lastEditedField === INPUT) { if (lastEditedField === INPUT) {
const newOutputValue = exchangeRate.multipliedBy(inputValue).toFixed(7); const newOutputValue = exchangeRate.multipliedBy(inputValue).toFixed(7)
if (newOutputValue !== outputValue) { if (newOutputValue !== outputValue) {
append.outputValue = newOutputValue; append.outputValue = newOutputValue
} }
} }
if (lastEditedField === OUTPUT) { if (lastEditedField === OUTPUT) {
const newInputValue = BN(outputValue).dividedBy(exchangeRate).toFixed(7); const newInputValue = BN(outputValue)
.dividedBy(exchangeRate)
.toFixed(7)
if (newInputValue !== inputValue) { if (newInputValue !== inputValue) {
append.inputValue = newInputValue; append.inputValue = newInputValue
} }
} }
this.setState(append); this.setState(append)
}; }
getBalance(currency) { getBalance(currency) {
const { t, selectors, account } = this.props; const { t, selectors, account } = this.props
if (!currency) { if (!currency) {
return ''; return ''
} }
const { value, decimals } = selectors().getBalance(account, currency); const { value, decimals } = selectors().getBalance(account, currency)
if (!decimals) { if (!decimals) {
return ''; return ''
} }
const balanceInput = value.dividedBy(10 ** decimals).toFixed(4); const balanceInput = value.dividedBy(10 ** decimals).toFixed(4)
return t("balance", { balanceInput }); return t('balance', { balanceInput })
} }
isUnapproved() { isUnapproved() {
const { account, exchangeAddresses, selectors } = this.props; const { account, exchangeAddresses, selectors } = this.props
const { outputCurrency, outputValue } = this.state; const { outputCurrency, outputValue } = this.state
if (!outputCurrency) { if (!outputCurrency) {
return false; return false
} }
const { value: allowance, label, decimals } = selectors().getApprovals( const { value: allowance, label, decimals } = selectors().getApprovals(
outputCurrency, outputCurrency,
account, account,
exchangeAddresses.fromToken[outputCurrency] exchangeAddresses.fromToken[outputCurrency]
); )
if (label && allowance.isLessThan(BN(outputValue * 10 ** decimals || 0))) { if (label && allowance.isLessThan(BN(outputValue * 10 ** decimals || 0))) {
return true; return true
} }
return false; return false
} }
onAddLiquidity = async () => { onAddLiquidity = async () => {
const { account, web3, exchangeAddresses: { fromToken }, selectors } = this.props; const {
const { inputValue, outputValue, outputCurrency } = this.state; account,
const exchange = new web3.eth.Contract(EXCHANGE_ABI, fromToken[outputCurrency]); web3,
exchangeAddresses: { fromToken },
const ethAmount = BN(inputValue).multipliedBy(10 ** 18); selectors
const { decimals } = selectors().getTokenBalance(outputCurrency, fromToken[outputCurrency]); } = this.props
const tokenAmount = BN(outputValue).multipliedBy(10 ** decimals); const { inputValue, outputValue, outputCurrency } = this.state
const { value: ethReserve } = selectors().getBalance(fromToken[outputCurrency]); const exchange = new web3.eth.Contract(EXCHANGE_ABI, fromToken[outputCurrency])
const totalLiquidity = await exchange.methods.totalSupply().call();
const liquidityMinted = BN(totalLiquidity).multipliedBy(ethAmount.dividedBy(ethReserve)); const ethAmount = BN(inputValue).multipliedBy(10 ** 18)
let deadline; const { decimals } = selectors().getTokenBalance(outputCurrency, fromToken[outputCurrency])
const tokenAmount = BN(outputValue).multipliedBy(10 ** decimals)
const { value: ethReserve } = selectors().getBalance(fromToken[outputCurrency])
const totalLiquidity = await exchange.methods.totalSupply().call()
const liquidityMinted = BN(totalLiquidity).multipliedBy(ethAmount.dividedBy(ethReserve))
let deadline
try { try {
deadline = await retry(() => getBlockDeadline(web3, 600)); deadline = await retry(() => getBlockDeadline(web3, 600))
} catch(e) { } catch (e) {
// TODO: Handle error. // TODO: Handle error.
return; return
} }
const MAX_LIQUIDITY_SLIPPAGE = 0.025; const MAX_LIQUIDITY_SLIPPAGE = 0.025
const minLiquidity = this.isNewExchange() ? BN(0) : liquidityMinted.multipliedBy(1 - MAX_LIQUIDITY_SLIPPAGE); const minLiquidity = this.isNewExchange() ? BN(0) : liquidityMinted.multipliedBy(1 - MAX_LIQUIDITY_SLIPPAGE)
const maxTokens = this.isNewExchange() ? tokenAmount : tokenAmount.multipliedBy(1 + MAX_LIQUIDITY_SLIPPAGE); const maxTokens = this.isNewExchange() ? tokenAmount : tokenAmount.multipliedBy(1 + MAX_LIQUIDITY_SLIPPAGE)
try { try {
exchange.methods.addLiquidity(minLiquidity.toFixed(0), maxTokens.toFixed(0), deadline).send({ exchange.methods.addLiquidity(minLiquidity.toFixed(0), maxTokens.toFixed(0), deadline).send(
{
from: account, from: account,
value: ethAmount.toFixed(0) value: ethAmount.toFixed(0)
}, (err, data) => { },
this.reset(); (err, data) => {
this.props.addPendingTx(data); this.reset()
this.props.addPendingTx(data)
if (data) { if (data) {
ReactGA.event({ ReactGA.event({
category: 'Pool', category: 'Pool',
action: 'AddLiquidity', action: 'AddLiquidity'
}); })
}
} }
}); )
} catch (err) { } catch (err) {
console.error(err); console.error(err)
}
} }
};
onInputChange = value => { onInputChange = value => {
const { inputCurrency, outputCurrency } = this.state; const { inputCurrency, outputCurrency } = this.state
const exchangeRate = this.getExchangeRate(); const exchangeRate = this.getExchangeRate()
let outputValue; let outputValue
if (inputCurrency === 'ETH' && outputCurrency && outputCurrency !== 'ETH') { if (inputCurrency === 'ETH' && outputCurrency && outputCurrency !== 'ETH') {
outputValue = exchangeRate.multipliedBy(value).toFixed(7); outputValue = exchangeRate.multipliedBy(value).toFixed(7)
} }
if (outputCurrency === 'ETH' && inputCurrency && inputCurrency !== 'ETH') { if (outputCurrency === 'ETH' && inputCurrency && inputCurrency !== 'ETH') {
outputValue = BN(value).dividedBy(exchangeRate).toFixed(7); outputValue = BN(value)
.dividedBy(exchangeRate)
.toFixed(7)
} }
const append = { const append = {
inputValue: value, inputValue: value,
lastEditedField: INPUT, lastEditedField: INPUT
}; }
if (!this.isNewExchange()) { if (!this.isNewExchange()) {
append.outputValue = outputValue; append.outputValue = outputValue
} }
this.setState(append); this.setState(append)
}; }
onOutputChange = value => { onOutputChange = value => {
const { inputCurrency, outputCurrency } = this.state; const { inputCurrency, outputCurrency } = this.state
const exchangeRate = this.getExchangeRate(); const exchangeRate = this.getExchangeRate()
let inputValue; let inputValue
if (inputCurrency === 'ETH' && outputCurrency && outputCurrency !== 'ETH') { if (inputCurrency === 'ETH' && outputCurrency && outputCurrency !== 'ETH') {
inputValue = BN(value).dividedBy(exchangeRate).toFixed(7); inputValue = BN(value)
.dividedBy(exchangeRate)
.toFixed(7)
} }
if (outputCurrency === 'ETH' && inputCurrency && inputCurrency !== 'ETH') { if (outputCurrency === 'ETH' && inputCurrency && inputCurrency !== 'ETH') {
inputValue = exchangeRate.multipliedBy(value).toFixed(7); inputValue = exchangeRate.multipliedBy(value).toFixed(7)
} }
const append = { const append = {
outputValue: value, outputValue: value,
lastEditedField: INPUT, lastEditedField: INPUT
}; }
if (!this.isNewExchange()) { if (!this.isNewExchange()) {
append.inputValue = inputValue; append.inputValue = inputValue
} }
this.setState(append); this.setState(append)
}; }
isNewExchange() { isNewExchange() {
const { selectors, exchangeAddresses: { fromToken } } = this.props; const {
const { inputCurrency, outputCurrency } = this.state; selectors,
const eth = [inputCurrency, outputCurrency].filter(currency => currency === 'ETH')[0]; exchangeAddresses: { fromToken }
const token = [inputCurrency, outputCurrency].filter(currency => currency !== 'ETH')[0]; } = this.props
const { inputCurrency, outputCurrency } = this.state
const eth = [inputCurrency, outputCurrency].filter(currency => currency === 'ETH')[0]
const token = [inputCurrency, outputCurrency].filter(currency => currency !== 'ETH')[0]
if (!eth || !token) { if (!eth || !token) {
return false; return false
} }
const { value: tokenValue, decimals } = selectors().getBalance(fromToken[token], token); const { value: tokenValue, decimals } = selectors().getBalance(fromToken[token], token)
const { value: ethValue } = selectors().getBalance(fromToken[token], eth); const { value: ethValue } = selectors().getBalance(fromToken[token], eth)
return tokenValue.isZero() && ethValue.isZero() && decimals !== 0; return tokenValue.isZero() && ethValue.isZero() && decimals !== 0
} }
getExchangeRate() { getExchangeRate() {
const { selectors, exchangeAddresses: { fromToken } } = this.props; const {
const { inputCurrency, outputCurrency } = this.state; selectors,
const eth = [inputCurrency, outputCurrency].filter(currency => currency === 'ETH')[0]; exchangeAddresses: { fromToken }
const token = [inputCurrency, outputCurrency].filter(currency => currency !== 'ETH')[0]; } = this.props
const { inputCurrency, outputCurrency } = this.state
const eth = [inputCurrency, outputCurrency].filter(currency => currency === 'ETH')[0]
const token = [inputCurrency, outputCurrency].filter(currency => currency !== 'ETH')[0]
if (!eth || !token) { if (!eth || !token) {
return; return
} }
const { value: tokenValue, decimals } = selectors().getBalance(fromToken[token], token); const { value: tokenValue, decimals } = selectors().getBalance(fromToken[token], token)
const { value: ethValue } = selectors().getBalance(fromToken[token], eth); const { value: ethValue } = selectors().getBalance(fromToken[token], eth)
return tokenValue.multipliedBy(10 ** (18 - decimals)).dividedBy(ethValue); return tokenValue.multipliedBy(10 ** (18 - decimals)).dividedBy(ethValue)
} }
validate() { validate() {
const { t, selectors, account } = this.props; const { t, selectors, account } = this.props
const { const { inputValue, outputValue, inputCurrency, outputCurrency } = this.state
inputValue, outputValue,
inputCurrency, outputCurrency, let inputError
} = this.state; let outputError
let isValid = true
let inputError; const inputIsZero = BN(inputValue).isZero()
let outputError; const outputIsZero = BN(outputValue).isZero()
let isValid = true;
const inputIsZero = BN(inputValue).isZero(); if (
const outputIsZero = BN(outputValue).isZero(); !inputValue ||
inputIsZero ||
if (!inputValue || inputIsZero || !outputValue || outputIsZero || !inputCurrency || !outputCurrency || this.isUnapproved()) { !outputValue ||
isValid = false; outputIsZero ||
} !inputCurrency ||
!outputCurrency ||
const { value: ethValue } = selectors().getBalance(account, inputCurrency); this.isUnapproved()
const { value: tokenValue, decimals } = selectors().getBalance(account, outputCurrency); ) {
isValid = false
}
const { value: ethValue } = selectors().getBalance(account, inputCurrency)
const { value: tokenValue, decimals } = selectors().getBalance(account, outputCurrency)
if (ethValue.isLessThan(BN(inputValue * 10 ** 18))) { if (ethValue.isLessThan(BN(inputValue * 10 ** 18))) {
inputError = t("insufficientBalance"); inputError = t('insufficientBalance')
} }
if (tokenValue.isLessThan(BN(outputValue * 10 ** decimals))) { if (tokenValue.isLessThan(BN(outputValue * 10 ** decimals))) {
outputError = t("insufficientBalance"); outputError = t('insufficientBalance')
} }
return { return {
inputError, inputError,
outputError, outputError,
isValid: isValid && !inputError && !outputError, isValid: isValid && !inputError && !outputError
}; }
} }
renderInfo() { renderInfo() {
const t = this.props.t; const t = this.props.t
const blank = ( const blank = (
<div className="pool__summary-panel"> <div className="pool__summary-panel">
<div className="pool__exchange-rate-wrapper"> <div className="pool__exchange-rate-wrapper">
<span className="pool__exchange-rate">{t("exchangeRate")}</span> <span className="pool__exchange-rate">{t('exchangeRate')}</span>
<span> - </span> <span> - </span>
</div> </div>
<div className="pool__exchange-rate-wrapper"> <div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t("currentPoolSize")}</span> <span className="swap__exchange-rate">{t('currentPoolSize')}</span>
<span> - </span> <span> - </span>
</div> </div>
<div className="pool__exchange-rate-wrapper"> <div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t("yourPoolShare")}</span> <span className="swap__exchange-rate">{t('yourPoolShare')}</span>
<span> - </span> <span> - </span>
</div> </div>
</div> </div>
); )
const { selectors, exchangeAddresses: { fromToken }, account } = this.props; const {
const { getBalance } = selectors(); selectors,
const { inputCurrency, outputCurrency, inputValue, outputValue, totalSupply } = this.state; exchangeAddresses: { fromToken },
const eth = [inputCurrency, outputCurrency].filter(currency => currency === 'ETH')[0]; account
const token = [inputCurrency, outputCurrency].filter(currency => currency !== 'ETH')[0]; } = this.props
const exchangeAddress = fromToken[token]; const { getBalance } = selectors()
const { inputCurrency, outputCurrency, inputValue, outputValue, totalSupply } = this.state
const eth = [inputCurrency, outputCurrency].filter(currency => currency === 'ETH')[0]
const token = [inputCurrency, outputCurrency].filter(currency => currency !== 'ETH')[0]
const exchangeAddress = fromToken[token]
if (!eth || !token || !exchangeAddress) { if (!eth || !token || !exchangeAddress) {
return blank; return blank
} }
const { value: tokenValue, decimals, label } = getBalance(exchangeAddress, token); const { value: tokenValue, decimals, label } = getBalance(exchangeAddress, token)
const { value: ethValue } = getBalance(exchangeAddress); const { value: ethValue } = getBalance(exchangeAddress)
const { value: liquidityBalance } = getBalance(account, exchangeAddress); const { value: liquidityBalance } = getBalance(account, exchangeAddress)
const ownership = liquidityBalance.dividedBy(totalSupply); const ownership = liquidityBalance.dividedBy(totalSupply)
const ethPer = ethValue.dividedBy(totalSupply); const ethPer = ethValue.dividedBy(totalSupply)
const tokenPer = tokenValue.dividedBy(totalSupply); const tokenPer = tokenValue.dividedBy(totalSupply)
const ownedEth = ethPer.multipliedBy(liquidityBalance).dividedBy(10 ** 18); const ownedEth = ethPer.multipliedBy(liquidityBalance).dividedBy(10 ** 18)
const ownedToken = tokenPer.multipliedBy(liquidityBalance).dividedBy(10 ** decimals); const ownedToken = tokenPer.multipliedBy(liquidityBalance).dividedBy(10 ** decimals)
if (!label || !decimals) { if (!label || !decimals) {
return blank; return blank
} }
if (this.isNewExchange()) { if (this.isNewExchange()) {
const rate = BN(outputValue).dividedBy(inputValue); const rate = BN(outputValue).dividedBy(inputValue)
const rateText = rate.isNaN() ? '---' : rate.toFixed(4); const rateText = rate.isNaN() ? '---' : rate.toFixed(4)
return ( return (
<div className="pool__summary-panel"> <div className="pool__summary-panel">
<div className="pool__exchange-rate-wrapper"> <div className="pool__exchange-rate-wrapper">
<span className="pool__exchange-rate">{t("exchangeRate")}</span> <span className="pool__exchange-rate">{t('exchangeRate')}</span>
<span>{`1 ETH = ${rateText} ${label}`}</span> <span>{`1 ETH = ${rateText} ${label}`}</span>
</div> </div>
<div className="pool__exchange-rate-wrapper"> <div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t("currentPoolSize")}</span> <span className="swap__exchange-rate">{t('currentPoolSize')}</span>
<span>{` ${ethValue.dividedBy(10 ** 18).toFixed(2)} ${eth} + ${tokenValue.dividedBy(10 ** decimals).toFixed(2)} ${label}`}</span> <span>{` ${ethValue.dividedBy(10 ** 18).toFixed(2)} ${eth} + ${tokenValue
.dividedBy(10 ** decimals)
.toFixed(2)} ${label}`}</span>
</div> </div>
<div className="pool__exchange-rate-wrapper"> <div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate"> <span className="swap__exchange-rate">
{t("yourPoolShare")} ({ownership.multipliedBy(100).toFixed(2)}%) {t('yourPoolShare')} ({ownership.multipliedBy(100).toFixed(2)}%)
</span> </span>
<span>{`${ownedEth.toFixed(2)} ETH + ${ownedToken.toFixed(2)} ${label}`}</span> <span>{`${ownedEth.toFixed(2)} ETH + ${ownedToken.toFixed(2)} ${label}`}</span>
</div> </div>
...@@ -373,22 +403,27 @@ class AddLiquidity extends Component { ...@@ -373,22 +403,27 @@ class AddLiquidity extends Component {
} }
if (tokenValue.dividedBy(ethValue).isNaN()) { if (tokenValue.dividedBy(ethValue).isNaN()) {
return blank; return blank
} }
return ( return (
<div className="pool__summary-panel"> <div className="pool__summary-panel">
<div className="pool__exchange-rate-wrapper"> <div className="pool__exchange-rate-wrapper">
<span className="pool__exchange-rate">{t("exchangeRate")}</span> <span className="pool__exchange-rate">{t('exchangeRate')}</span>
<span>{`1 ETH = ${tokenValue.multipliedBy(10 ** (18 - decimals)).dividedBy(ethValue).toFixed(4)} ${label}`}</span> <span>{`1 ETH = ${tokenValue
.multipliedBy(10 ** (18 - decimals))
.dividedBy(ethValue)
.toFixed(4)} ${label}`}</span>
</div> </div>
<div className="pool__exchange-rate-wrapper"> <div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t("currentPoolSize")}</span> <span className="swap__exchange-rate">{t('currentPoolSize')}</span>
<span>{` ${ethValue.dividedBy(10 ** 18).toFixed(2)} ${eth} + ${tokenValue.dividedBy(10 ** decimals).toFixed(2)} ${label}`}</span> <span>{` ${ethValue.dividedBy(10 ** 18).toFixed(2)} ${eth} + ${tokenValue
.dividedBy(10 ** decimals)
.toFixed(2)} ${label}`}</span>
</div> </div>
<div className="pool__exchange-rate-wrapper"> <div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate"> <span className="swap__exchange-rate">
{t("yourPoolShare")} ({ownership.multipliedBy(100).toFixed(2)}%) {t('yourPoolShare')} ({ownership.multipliedBy(100).toFixed(2)}%)
</span> </span>
<span>{`${ownedEth.toFixed(2)} ETH + ${ownedToken.toFixed(2)} ${label}`}</span> <span>{`${ownedEth.toFixed(2)} ETH + ${ownedToken.toFixed(2)} ${label}`}</span>
</div> </div>
...@@ -397,93 +432,114 @@ class AddLiquidity extends Component { ...@@ -397,93 +432,114 @@ class AddLiquidity extends Component {
} }
renderSummary(inputError, outputError) { renderSummary(inputError, outputError) {
const { t, selectors, exchangeAddresses: { fromToken } } = this.props;
const { const {
inputValue, t,
outputValue, selectors,
inputCurrency, exchangeAddresses: { fromToken }
outputCurrency, } = this.props
} = this.state; const { inputValue, outputValue, inputCurrency, outputCurrency } = this.state
const inputIsZero = BN(inputValue).isZero(); const inputIsZero = BN(inputValue).isZero()
const outputIsZero = BN(outputValue).isZero(); const outputIsZero = BN(outputValue).isZero()
let contextualInfo = ''; let contextualInfo = ''
let isError = false; let isError = false
const { label } = selectors().getTokenBalance(outputCurrency, fromToken[outputCurrency]); const { label } = selectors().getTokenBalance(outputCurrency, fromToken[outputCurrency])
if (inputError || outputError) { if (inputError || outputError) {
contextualInfo = inputError || outputError; contextualInfo = inputError || outputError
isError = true; isError = true
} else if (!inputCurrency || !outputCurrency) { } else if (!inputCurrency || !outputCurrency) {
contextualInfo = t("selectTokenCont"); contextualInfo = t('selectTokenCont')
} else if (inputCurrency === outputCurrency) { } else if (inputCurrency === outputCurrency) {
contextualInfo = t("differentToken"); contextualInfo = t('differentToken')
} else if (![inputCurrency, outputCurrency].includes('ETH')) { } else if (![inputCurrency, outputCurrency].includes('ETH')) {
contextualInfo = t("mustBeETH"); contextualInfo = t('mustBeETH')
} else if (inputIsZero || outputIsZero) { } else if (inputIsZero || outputIsZero) {
contextualInfo = t("noZero"); contextualInfo = t('noZero')
} else if (this.isUnapproved()) { } else if (this.isUnapproved()) {
contextualInfo = t("unlockTokenCont"); contextualInfo = t('unlockTokenCont')
} else if (!inputValue || !outputValue) { } else if (!inputValue || !outputValue) {
contextualInfo = t("enterCurrencyOrLabelCont", {inputCurrency, label}); contextualInfo = t('enterCurrencyOrLabelCont', { inputCurrency, label })
} }
return ( return (
<ContextualInfo <ContextualInfo
key="context-info" key="context-info"
openDetailsText={t("transactionDetails")} openDetailsText={t('transactionDetails')}
closeDetailsText={t("hideDetails")} closeDetailsText={t('hideDetails')}
contextualInfo={contextualInfo} contextualInfo={contextualInfo}
isError={isError} isError={isError}
renderTransactionDetails={this.renderTransactionDetails} renderTransactionDetails={this.renderTransactionDetails}
/> />
); )
} }
renderTransactionDetails = () => { renderTransactionDetails = () => {
const { t, selectors, exchangeAddresses: { fromToken }, account } = this.props;
const { const {
inputValue, t,
outputValue, selectors,
outputCurrency, exchangeAddresses: { fromToken },
totalSupply, account
} = this.state; } = this.props
const { inputValue, outputValue, outputCurrency, totalSupply } = this.state
ReactGA.event({ ReactGA.event({
category: 'TransactionDetail', category: 'TransactionDetail',
action: 'Open', action: 'Open'
}); })
const { value: tokenReserve, label } = selectors().getTokenBalance(outputCurrency, fromToken[outputCurrency]); const { value: tokenReserve, label } = selectors().getTokenBalance(outputCurrency, fromToken[outputCurrency])
const { value: ethReserve } = selectors().getBalance(fromToken[outputCurrency]); const { value: ethReserve } = selectors().getBalance(fromToken[outputCurrency])
const { decimals: poolTokenDecimals } = selectors().getBalance(account, fromToken[outputCurrency]); const { decimals: poolTokenDecimals } = selectors().getBalance(account, fromToken[outputCurrency])
if (this.isNewExchange()) { if (this.isNewExchange()) {
return ( return (
<div> <div>
<div className="pool__summary-item">{t("youAreAdding")} {b(`${inputValue} ETH`)} {t("and")} {b(`${outputValue} ${label}`)} {t("intoPool")}</div> <div className="pool__summary-item">
<div className="pool__summary-item">{t("youAreSettingExRate")} {b(`1 ETH = ${BN(outputValue).dividedBy(inputValue).toFixed(4)} ${label}`)}.</div> {t('youAreAdding')} {b(`${inputValue} ETH`)} {t('and')} {b(`${outputValue} ${label}`)} {t('intoPool')}
<div className="pool__summary-item">{t("youWillMint")} {b(`${inputValue}`)} {t("liquidityTokens")}</div>
<div className="pool__summary-item">{t("totalSupplyIs0")}</div>
</div> </div>
); <div className="pool__summary-item">
{t('youAreSettingExRate')}{' '}
{b(
`1 ETH = ${BN(outputValue)
.dividedBy(inputValue)
.toFixed(4)} ${label}`
)}
.
</div>
<div className="pool__summary-item">
{t('youWillMint')} {b(`${inputValue}`)} {t('liquidityTokens')}
</div>
<div className="pool__summary-item">{t('totalSupplyIs0')}</div>
</div>
)
} }
const SLIPPAGE = 0.025; const SLIPPAGE = 0.025
const minOutput = BN(outputValue).multipliedBy(1 - SLIPPAGE); const minOutput = BN(outputValue).multipliedBy(1 - SLIPPAGE)
const maxOutput = BN(outputValue).multipliedBy(1 + SLIPPAGE); const maxOutput = BN(outputValue).multipliedBy(1 + SLIPPAGE)
// const minPercentage = minOutput.dividedBy(minOutput.plus(tokenReserve)).multipliedBy(100); // const minPercentage = minOutput.dividedBy(minOutput.plus(tokenReserve)).multipliedBy(100);
// const maxPercentage = maxOutput.dividedBy(maxOutput.plus(tokenReserve)).multipliedBy(100); // const maxPercentage = maxOutput.dividedBy(maxOutput.plus(tokenReserve)).multipliedBy(100);
const liquidityMinted = BN(inputValue).multipliedBy(totalSupply.dividedBy(ethReserve)); const liquidityMinted = BN(inputValue).multipliedBy(totalSupply.dividedBy(ethReserve))
const adjTotalSupply = totalSupply.dividedBy(10 ** poolTokenDecimals); const adjTotalSupply = totalSupply.dividedBy(10 ** poolTokenDecimals)
return ( return (
<div> <div>
<div className="pool__summary-modal__item">{t("youAreAdding")} {b(`${+BN(inputValue).toFixed(7)} ETH`)} {t("and")} {b(`${+minOutput.toFixed(7)} - ${+maxOutput.toFixed(7)} ${label}`)} {t("intoPool")}</div> <div className="pool__summary-modal__item">
<div className="pool__summary-modal__item">{t("youWillMint")} {b(+liquidityMinted.toFixed(7))} {t("liquidityTokens")}</div> {t('youAreAdding')} {b(`${+BN(inputValue).toFixed(7)} ETH`)} {t('and')}{' '}
<div className="pool__summary-modal__item">{t("totalSupplyIs")} {b(+adjTotalSupply.toFixed(7))}</div> {b(`${+minOutput.toFixed(7)} - ${+maxOutput.toFixed(7)} ${label}`)} {t('intoPool')}
<div className="pool__summary-modal__item">{t("tokenWorth")} {b(+ethReserve.dividedBy(totalSupply).toFixed(7))} ETH {t("and")} {b(+tokenReserve.dividedBy(totalSupply).toFixed(7))} {label}</div>
</div> </div>
); <div className="pool__summary-modal__item">
{t('youWillMint')} {b(+liquidityMinted.toFixed(7))} {t('liquidityTokens')}
</div>
<div className="pool__summary-modal__item">
{t('totalSupplyIs')} {b(+adjTotalSupply.toFixed(7))}
</div>
<div className="pool__summary-modal__item">
{t('tokenWorth')} {b(+ethReserve.dividedBy(totalSupply).toFixed(7))} ETH {t('and')}{' '}
{b(+tokenReserve.dividedBy(totalSupply).toFixed(7))} {label}
</div>
</div>
)
} }
render() { render() {
...@@ -491,49 +547,41 @@ class AddLiquidity extends Component { ...@@ -491,49 +547,41 @@ class AddLiquidity extends Component {
t, t,
isConnected, isConnected,
exchangeAddresses: { fromToken }, exchangeAddresses: { fromToken },
selectors, selectors
} = this.props; } = this.props
const { const { inputValue, outputValue, inputCurrency, outputCurrency } = this.state
inputValue,
outputValue,
inputCurrency,
outputCurrency,
} = this.state;
const { inputError, outputError, isValid } = this.validate(); const { inputError, outputError, isValid } = this.validate()
const { label } = selectors().getTokenBalance(outputCurrency, fromToken[outputCurrency]); const { label } = selectors().getTokenBalance(outputCurrency, fromToken[outputCurrency])
return [ return [
<div <div
key="content" key="content"
className={classnames('swap__content', { className={classnames('swap__content', {
'swap--inactive': !isConnected, 'swap--inactive': !isConnected
})} })}
> >
<NavigationTabs <NavigationTabs
className={classnames('header__navigation', { className={classnames('header__navigation', {
'header--inactive': !isConnected, 'header--inactive': !isConnected
})} })}
/> />
{ {this.isNewExchange() ? (
this.isNewExchange()
? (
<div className="pool__new-exchange-warning"> <div className="pool__new-exchange-warning">
<div className="pool__new-exchange-warning-text"> <div className="pool__new-exchange-warning-text">
<span role='img' aria-label='liquidity'>🚰</span> {t("firstLiquidity")} <span role="img" aria-label="liquidity">
</div> 🚰
<div className="pool__new-exchange-warning-text"> </span>{' '}
{ t("initialExchangeRate", { label }) } {t('firstLiquidity')}
</div> </div>
<div className="pool__new-exchange-warning-text">{t('initialExchangeRate', { label })}</div>
</div> </div>
) ) : null}
: null <ModeSelector title={t('addLiquidity')} />
}
<ModeSelector title={t("addLiquidity")}/>
<CurrencyInputPanel <CurrencyInputPanel
title={t("deposit")} title={t('deposit')}
extraText={this.getBalance(inputCurrency)} extraText={this.getBalance(inputCurrency)}
onValueChange={this.onInputChange} onValueChange={this.onInputChange}
selectedTokenAddress="ETH" selectedTokenAddress="ETH"
...@@ -543,58 +591,60 @@ class AddLiquidity extends Component { ...@@ -543,58 +591,60 @@ class AddLiquidity extends Component {
/> />
<OversizedPanel> <OversizedPanel>
<div className="swap__down-arrow-background"> <div className="swap__down-arrow-background">
<img className="swap__down-arrow" src={isValid ? PlusBlue : PlusGrey} alt='plus' /> <img className="swap__down-arrow" src={isValid ? PlusBlue : PlusGrey} alt="plus" />
</div> </div>
</OversizedPanel> </OversizedPanel>
<CurrencyInputPanel <CurrencyInputPanel
title={t("deposit")} title={t('deposit')}
description={this.isNewExchange() ? `(${t("estimated")})` : ''} description={this.isNewExchange() ? `(${t('estimated')})` : ''}
extraText={this.getBalance(outputCurrency)} extraText={this.getBalance(outputCurrency)}
selectedTokenAddress={outputCurrency} selectedTokenAddress={outputCurrency}
onCurrencySelected={currency => { onCurrencySelected={currency => {
this.setState({ this.setState(
outputCurrency: currency, {
}, this.recalcForm); outputCurrency: currency
},
this.recalcForm
)
}} }}
onValueChange={this.onOutputChange} onValueChange={this.onOutputChange}
value={outputValue} value={outputValue}
errorMessage={outputError} errorMessage={outputError}
filteredTokens={[ 'ETH' ]} filteredTokens={['ETH']}
/> />
<OversizedPanel hideBottom> <OversizedPanel hideBottom>{this.renderInfo()}</OversizedPanel>
{ this.renderInfo() } {this.renderSummary(inputError, outputError)}
</OversizedPanel>
{ this.renderSummary(inputError, outputError) }
<div className="pool__cta-container"> <div className="pool__cta-container">
<button <button
className={classnames('pool__cta-btn', { className={classnames('pool__cta-btn', {
'swap--inactive': !this.props.isConnected, 'swap--inactive': !this.props.isConnected,
'pool__cta-btn--inactive': !isValid, 'pool__cta-btn--inactive': !isValid
})} })}
disabled={!isValid} disabled={!isValid}
onClick={this.onAddLiquidity} onClick={this.onAddLiquidity}
> >
{t("addLiquidity")} {t('addLiquidity')}
</button> </button>
</div> </div>
</div> </div>
]; ]
} }
} }
export default connect( export default connect(
state => ({ state => ({
isConnected: Boolean(state.web3connect.account) && state.web3connect.networkId === (process.env.REACT_APP_NETWORK_ID||1), isConnected:
Boolean(state.web3connect.account) && state.web3connect.networkId === (process.env.REACT_APP_NETWORK_ID || 1),
account: state.web3connect.account, account: state.web3connect.account,
balances: state.web3connect.balances, balances: state.web3connect.balances,
web3: state.web3connect.web3, web3: state.web3connect.web3,
exchangeAddresses: state.addresses.exchangeAddresses, exchangeAddresses: state.addresses.exchangeAddresses
}), }),
dispatch => ({ dispatch => ({
selectors: () => dispatch(selectors()), selectors: () => dispatch(selectors()),
addPendingTx: id => dispatch(addPendingTx(id)), addPendingTx: id => dispatch(addPendingTx(id))
}) })
)(withNamespaces()(AddLiquidity)); )(withNamespaces()(AddLiquidity))
function b(text) { function b(text) {
return <span className="swap__highlight-text">{text}</span> return <span className="swap__highlight-text">{text}</span>
......
import React, { Component } from 'react'; import React, { Component } from 'react'
import { connect } from 'react-redux'; import { connect } from 'react-redux'
import PropTypes from 'prop-types'; import PropTypes from 'prop-types'
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom'
import { withNamespaces } from 'react-i18next'; import { withNamespaces } from 'react-i18next'
import {selectors, addPendingTx} from "../../ducks/web3connect"; import { selectors, addPendingTx } from '../../ducks/web3connect'
import classnames from "classnames"; import classnames from 'classnames'
import NavigationTabs from "../../components/NavigationTabs"; import NavigationTabs from '../../components/NavigationTabs'
import ModeSelector from "./ModeSelector"; import ModeSelector from './ModeSelector'
import AddressInputPanel from "../../components/AddressInputPanel"; import AddressInputPanel from '../../components/AddressInputPanel'
import OversizedPanel from "../../components/OversizedPanel"; import OversizedPanel from '../../components/OversizedPanel'
import FACTORY_ABI from "../../abi/factory"; import FACTORY_ABI from '../../abi/factory'
import {addExchange} from "../../ducks/addresses"; import { addExchange } from '../../ducks/addresses'
import ReactGA from "react-ga"; import ReactGA from 'react-ga'
class CreateExchange extends Component { class CreateExchange extends Component {
static propTypes = { static propTypes = {
...@@ -22,23 +22,27 @@ class CreateExchange extends Component { ...@@ -22,23 +22,27 @@ class CreateExchange extends Component {
isConnected: PropTypes.bool.isRequired, isConnected: PropTypes.bool.isRequired,
factoryAddress: PropTypes.string.isRequired, factoryAddress: PropTypes.string.isRequired,
exchangeAddresses: PropTypes.shape({ exchangeAddresses: PropTypes.shape({
fromToken: PropTypes.object.isRequired, fromToken: PropTypes.object.isRequired
}).isRequired, }).isRequired
}; }
constructor(props) { constructor(props) {
super(props); super(props)
const { match: { params: { tokenAddress } } } = this.props; const {
match: {
params: { tokenAddress }
}
} = this.props
this.state = { this.state = {
tokenAddress, tokenAddress,
label: '', label: '',
decimals: 0, decimals: 0
}; }
} }
validate() { validate() {
const { tokenAddress } = this.state; const { tokenAddress } = this.state
const { const {
t, t,
web3, web3,
...@@ -46,104 +50,104 @@ class CreateExchange extends Component { ...@@ -46,104 +50,104 @@ class CreateExchange extends Component {
selectors, selectors,
factoryAddress, factoryAddress,
exchangeAddresses: { fromToken }, exchangeAddresses: { fromToken },
addExchange, addExchange
} = this.props; } = this.props
let isValid = true; let isValid = true
let errorMessage = ''; let errorMessage = ''
if (!tokenAddress) { if (!tokenAddress) {
return { return {
isValid: false, isValid: false
}; }
} }
if (web3 && web3.utils && !web3.utils.isAddress(tokenAddress)) { if (web3 && web3.utils && !web3.utils.isAddress(tokenAddress)) {
return { return {
isValid: false, isValid: false,
errorMessage: t("invalidTokenAddress"), errorMessage: t('invalidTokenAddress')
}; }
} }
const { label, decimals } = selectors().getBalance(account, tokenAddress); const { label, decimals } = selectors().getBalance(account, tokenAddress)
const factory = new web3.eth.Contract(FACTORY_ABI, factoryAddress); const factory = new web3.eth.Contract(FACTORY_ABI, factoryAddress)
const exchangeAddress = fromToken[tokenAddress]; const exchangeAddress = fromToken[tokenAddress]
if (!exchangeAddress) { if (!exchangeAddress) {
factory.methods.getExchange(tokenAddress).call((err, data) => { factory.methods.getExchange(tokenAddress).call((err, data) => {
if (!err && data !== '0x0000000000000000000000000000000000000000') { if (!err && data !== '0x0000000000000000000000000000000000000000') {
addExchange({ label, tokenAddress, exchangeAddress: data }); addExchange({ label, tokenAddress, exchangeAddress: data })
} }
}); })
} else { } else {
errorMessage = t("exchangeExists", { label }); errorMessage = t('exchangeExists', { label })
} }
if (!label) { if (!label) {
errorMessage = t("invalidSymbol"); errorMessage = t('invalidSymbol')
} }
if (!decimals) { if (!decimals) {
errorMessage = t("invalidDecimals"); errorMessage = t('invalidDecimals')
} }
return { return {
isValid: isValid && !errorMessage, isValid: isValid && !errorMessage,
errorMessage, errorMessage
}; }
} }
onChange = tokenAddress => { onChange = tokenAddress => {
const { selectors, account, web3 } = this.props; const { selectors, account, web3 } = this.props
if (web3 && web3.utils && web3.utils.isAddress(tokenAddress)) { if (web3 && web3.utils && web3.utils.isAddress(tokenAddress)) {
const { label, decimals } = selectors().getBalance(account, tokenAddress); const { label, decimals } = selectors().getBalance(account, tokenAddress)
this.setState({ this.setState({
label, label,
decimals, decimals,
tokenAddress, tokenAddress
}); })
} else { } else {
this.setState({ this.setState({
label: '', label: '',
decimals: 0, decimals: 0,
tokenAddress, tokenAddress
}); })
}
} }
};
onCreateExchange = () => { onCreateExchange = () => {
const { tokenAddress } = this.state; const { tokenAddress } = this.state
const { account, web3, factoryAddress } = this.props; const { account, web3, factoryAddress } = this.props
if (web3 && web3.utils && !web3.utils.isAddress(tokenAddress)) { if (web3 && web3.utils && !web3.utils.isAddress(tokenAddress)) {
return; return
} }
const factory = new web3.eth.Contract(FACTORY_ABI, factoryAddress); const factory = new web3.eth.Contract(FACTORY_ABI, factoryAddress)
factory.methods.createExchange(tokenAddress).send({ from: account }, (err, data) => { factory.methods.createExchange(tokenAddress).send({ from: account }, (err, data) => {
if (!err) { if (!err) {
this.setState({ this.setState({
label: '', label: '',
decimals: 0, decimals: 0,
tokenAddress: '', tokenAddress: ''
}); })
this.props.addPendingTx(data); this.props.addPendingTx(data)
ReactGA.event({ ReactGA.event({
category: 'Pool', category: 'Pool',
action: 'CreateExchange', action: 'CreateExchange'
}); })
} }
}) })
}; }
renderSummary() { renderSummary() {
const { tokenAddress } = this.state; const { tokenAddress } = this.state
const { errorMessage } = this.validate(); const { errorMessage } = this.validate()
if (!tokenAddress) { if (!tokenAddress) {
return ( return (
<div className="create-exchange__summary-panel"> <div className="create-exchange__summary-panel">
<div className="create-exchange__summary-text">{this.props.t("enterTokenCont")}</div> <div className="create-exchange__summary-text">{this.props.t('enterTokenCont')}</div>
</div> </div>
) )
} }
...@@ -156,36 +160,36 @@ class CreateExchange extends Component { ...@@ -156,36 +160,36 @@ class CreateExchange extends Component {
) )
} }
return null; return null
} }
render() { render() {
const { tokenAddress } = this.state; const { tokenAddress } = this.state
const { t, isConnected, account, selectors, web3 } = this.props; const { t, isConnected, account, selectors, web3 } = this.props
const { isValid, errorMessage } = this.validate(); const { isValid, errorMessage } = this.validate()
let label, decimals; let label, decimals
if (web3 && web3.utils && web3.utils.isAddress(tokenAddress)) { if (web3 && web3.utils && web3.utils.isAddress(tokenAddress)) {
const { label: _label, decimals: _decimals } = selectors().getBalance(account, tokenAddress); const { label: _label, decimals: _decimals } = selectors().getBalance(account, tokenAddress)
label = _label; label = _label
decimals = _decimals; decimals = _decimals
} }
return ( return (
<div <div
key="content" key="content"
className={classnames('swap__content', { className={classnames('swap__content', {
'swap--inactive': !isConnected, 'swap--inactive': !isConnected
})} })}
> >
<NavigationTabs <NavigationTabs
className={classnames('header__navigation', { className={classnames('header__navigation', {
'header--inactive': !isConnected, 'header--inactive': !isConnected
})} })}
/> />
<ModeSelector title={t("createExchange")} /> <ModeSelector title={t('createExchange')} />
<AddressInputPanel <AddressInputPanel
title={t("tokenAddress")} title={t('tokenAddress')}
value={tokenAddress} value={tokenAddress}
onChange={this.onChange} onChange={this.onChange}
errorMessage={errorMessage} errorMessage={errorMessage}
...@@ -193,46 +197,47 @@ class CreateExchange extends Component { ...@@ -193,46 +197,47 @@ class CreateExchange extends Component {
<OversizedPanel hideBottom> <OversizedPanel hideBottom>
<div className="pool__summary-panel"> <div className="pool__summary-panel">
<div className="pool__exchange-rate-wrapper"> <div className="pool__exchange-rate-wrapper">
<span className="pool__exchange-rate">{t("label")}</span> <span className="pool__exchange-rate">{t('label')}</span>
<span>{label || ' - '}</span> <span>{label || ' - '}</span>
</div> </div>
<div className="pool__exchange-rate-wrapper"> <div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t("decimals")}</span> <span className="swap__exchange-rate">{t('decimals')}</span>
<span>{decimals || ' - '}</span> <span>{decimals || ' - '}</span>
</div> </div>
</div> </div>
</OversizedPanel> </OversizedPanel>
{ this.renderSummary() } {this.renderSummary()}
<div className="pool__cta-container"> <div className="pool__cta-container">
<button <button
className={classnames('pool__cta-btn', { className={classnames('pool__cta-btn', {
'swap--inactive': !isConnected, 'swap--inactive': !isConnected
})} })}
disabled={!isValid} disabled={!isValid}
onClick={this.onCreateExchange} onClick={this.onCreateExchange}
> >
{t("createExchange")} {t('createExchange')}
</button> </button>
</div> </div>
</div> </div>
); )
} }
} }
export default withRouter( export default withRouter(
connect( connect(
state => ({ state => ({
isConnected: Boolean(state.web3connect.account) && state.web3connect.networkId === (process.env.REACT_APP_NETWORK_ID||1), isConnected:
Boolean(state.web3connect.account) && state.web3connect.networkId === (process.env.REACT_APP_NETWORK_ID || 1),
account: state.web3connect.account, account: state.web3connect.account,
balances: state.web3connect.balances, balances: state.web3connect.balances,
web3: state.web3connect.web3, web3: state.web3connect.web3,
exchangeAddresses: state.addresses.exchangeAddresses, exchangeAddresses: state.addresses.exchangeAddresses,
factoryAddress: state.addresses.factoryAddress, factoryAddress: state.addresses.factoryAddress
}), }),
dispatch => ({ dispatch => ({
selectors: () => dispatch(selectors()), selectors: () => dispatch(selectors()),
addExchange: opts => dispatch(addExchange(opts)), addExchange: opts => dispatch(addExchange(opts)),
addPendingTx: id => dispatch(addPendingTx(id)), addPendingTx: id => dispatch(addPendingTx(id))
}) })
)(withNamespaces()(CreateExchange)) )(withNamespaces()(CreateExchange))
); )
import React, { Component } from 'react'; import React, { Component } from 'react'
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom'
import { withNamespaces } from 'react-i18next'; import { withNamespaces } from 'react-i18next'
import OversizedPanel from "../../components/OversizedPanel"; import OversizedPanel from '../../components/OversizedPanel'
import Dropdown from "../../assets/images/dropdown-blue.svg"; import Dropdown from '../../assets/images/dropdown-blue.svg'
import Modal from "../../components/Modal"; import Modal from '../../components/Modal'
import {CSSTransitionGroup} from "react-transition-group"; import { CSSTransitionGroup } from 'react-transition-group'
const ADD = 'Add Liquidity'; const ADD = 'Add Liquidity'
const REMOVE = 'Remove Liquidity'; const REMOVE = 'Remove Liquidity'
const CREATE = 'Create Exchange'; const CREATE = 'Create Exchange'
class ModeSelector extends Component { class ModeSelector extends Component {
state = { state = {
isShowingModal: false, isShowingModal: false,
selected: ADD, selected: ADD
}; }
changeView(view) { changeView(view) {
const { history } = this.props; const { history } = this.props
this.setState({ this.setState({
isShowingModal: false, isShowingModal: false,
selected: view, selected: view
}); })
switch (view) { switch (view) {
case ADD: case ADD:
return history.push('/add-liquidity'); return history.push('/add-liquidity')
case REMOVE: case REMOVE:
return history.push('/remove-liquidity'); return history.push('/remove-liquidity')
case CREATE: case CREATE:
return history.push('/create-exchange'); return history.push('/create-exchange')
default: default:
return; return
} }
} }
renderModal() { renderModal() {
if (!this.state.isShowingModal) { if (!this.state.isShowingModal) {
return; return
} }
return ( return (
...@@ -52,41 +52,27 @@ class ModeSelector extends Component { ...@@ -52,41 +52,27 @@ class ModeSelector extends Component {
transitionEnterTimeout={200} transitionEnterTimeout={200}
> >
<div className="pool-modal"> <div className="pool-modal">
<div <div className="pool-modal__item" onClick={() => this.changeView(ADD)}>
className="pool-modal__item" {this.props.t('addLiquidity')}
onClick={() => this.changeView(ADD)}
>
{this.props.t("addLiquidity")}
</div> </div>
<div <div className="pool-modal__item" onClick={() => this.changeView(REMOVE)}>
className="pool-modal__item" {this.props.t('removeLiquidity')}
onClick={() => this.changeView(REMOVE)}
>
{this.props.t("removeLiquidity")}
</div> </div>
<div <div className="pool-modal__item" onClick={() => this.changeView(CREATE)}>
className="pool-modal__item" {this.props.t('createExchange')}
onClick={() => this.changeView(CREATE)}
>
{this.props.t("createExchange")}
</div> </div>
</div> </div>
</CSSTransitionGroup> </CSSTransitionGroup>
</Modal> </Modal>
); )
} }
render() { render() {
return ( return (
<OversizedPanel hideTop> <OversizedPanel hideTop>
<div <div className="pool__liquidity-container" onClick={() => this.setState({ isShowingModal: true })}>
className="pool__liquidity-container" <span className="pool__liquidity-label">{this.props.title}</span>
onClick={() => this.setState({ isShowingModal: true })} <img src={Dropdown} alt="dropdown" />
>
<span className="pool__liquidity-label">
{this.props.title}
</span>
<img src={Dropdown} alt='dropdown' />
</div> </div>
{this.renderModal()} {this.renderModal()}
</OversizedPanel> </OversizedPanel>
...@@ -94,4 +80,4 @@ class ModeSelector extends Component { ...@@ -94,4 +80,4 @@ class ModeSelector extends Component {
} }
} }
export default withRouter(withNamespaces()(ModeSelector)); export default withRouter(withNamespaces()(ModeSelector))
import React, { Component } from 'react'; import React, { Component } from 'react'
import PropTypes from 'prop-types'; import PropTypes from 'prop-types'
import classnames from "classnames"; import classnames from 'classnames'
import { connect } from 'react-redux'; import { connect } from 'react-redux'
import { BigNumber as BN } from 'bignumber.js'; import { BigNumber as BN } from 'bignumber.js'
import { withNamespaces } from 'react-i18next'; import { withNamespaces } from 'react-i18next'
import NavigationTabs from "../../components/NavigationTabs"; import NavigationTabs from '../../components/NavigationTabs'
import ModeSelector from "./ModeSelector"; import ModeSelector from './ModeSelector'
import CurrencyInputPanel from "../../components/CurrencyInputPanel"; import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import { selectors, addPendingTx } from '../../ducks/web3connect'; import { selectors, addPendingTx } from '../../ducks/web3connect'
import ContextualInfo from "../../components/ContextualInfo"; import ContextualInfo from '../../components/ContextualInfo'
import OversizedPanel from "../../components/OversizedPanel"; import OversizedPanel from '../../components/OversizedPanel'
import ArrowDownBlue from "../../assets/images/arrow-down-blue.svg"; import ArrowDownBlue from '../../assets/images/arrow-down-blue.svg'
import ArrowDownGrey from "../../assets/images/arrow-down-grey.svg"; import ArrowDownGrey from '../../assets/images/arrow-down-grey.svg'
import { getBlockDeadline } from '../../helpers/web3-utils'; import { getBlockDeadline } from '../../helpers/web3-utils'
import { retry } from '../../helpers/promise-utils'; import { retry } from '../../helpers/promise-utils'
import EXCHANGE_ABI from "../../abi/exchange"; import EXCHANGE_ABI from '../../abi/exchange'
import ReactGA from "react-ga"; import ReactGA from 'react-ga'
class RemoveLiquidity extends Component { class RemoveLiquidity extends Component {
static propTypes = { static propTypes = {
...@@ -23,220 +23,242 @@ class RemoveLiquidity extends Component { ...@@ -23,220 +23,242 @@ class RemoveLiquidity extends Component {
balances: PropTypes.object, balances: PropTypes.object,
web3: PropTypes.object, web3: PropTypes.object,
exchangeAddresses: PropTypes.shape({ exchangeAddresses: PropTypes.shape({
fromToken: PropTypes.object.isRequired, fromToken: PropTypes.object.isRequired
}).isRequired, }).isRequired
}; }
state = { state = {
tokenAddress: '', tokenAddress: '',
value: '', value: '',
totalSupply: BN(0), totalSupply: BN(0)
}; }
reset() { reset() {
this.setState({ this.setState({
value: '', value: ''
}); })
} }
validate() { validate() {
const { tokenAddress, value } = this.state; const { tokenAddress, value } = this.state
const { t, account, selectors, exchangeAddresses: { fromToken }, web3 } = this.props; const {
const exchangeAddress = fromToken[tokenAddress]; t,
account,
selectors,
exchangeAddresses: { fromToken },
web3
} = this.props
const exchangeAddress = fromToken[tokenAddress]
if (!web3 || !exchangeAddress || !account || !value) { if (!web3 || !exchangeAddress || !account || !value) {
return { return {
isValid: false, isValid: false
}; }
} }
const { getBalance } = selectors(); const { getBalance } = selectors()
const { value: liquidityBalance, decimals: liquidityDecimals } = getBalance(account, exchangeAddress); const { value: liquidityBalance, decimals: liquidityDecimals } = getBalance(account, exchangeAddress)
if (liquidityBalance.isLessThan(BN(value).multipliedBy(10 ** liquidityDecimals))) { if (liquidityBalance.isLessThan(BN(value).multipliedBy(10 ** liquidityDecimals))) {
return { isValid: false, errorMessage: t("insufficientBalance") }; return { isValid: false, errorMessage: t('insufficientBalance') }
} }
return { return {
isValid: true, isValid: true
}; }
} }
onTokenSelect = async tokenAddress => { onTokenSelect = async tokenAddress => {
const { exchangeAddresses: { fromToken }, web3 } = this.props; const {
const exchangeAddress = fromToken[tokenAddress]; exchangeAddresses: { fromToken },
this.setState({ tokenAddress }); web3
} = this.props
const exchangeAddress = fromToken[tokenAddress]
this.setState({ tokenAddress })
if (!web3 || !exchangeAddress) { if (!web3 || !exchangeAddress) {
return; return
} }
const exchange = new web3.eth.Contract(EXCHANGE_ABI, exchangeAddress); const exchange = new web3.eth.Contract(EXCHANGE_ABI, exchangeAddress)
const totalSupply = await exchange.methods.totalSupply().call(); const totalSupply = await exchange.methods.totalSupply().call()
this.setState({ this.setState({
totalSupply: BN(totalSupply), totalSupply: BN(totalSupply)
}); })
}; }
onInputChange = value => { onInputChange = value => {
this.setState({ value }); this.setState({ value })
}; }
onRemoveLiquidity = async () => { onRemoveLiquidity = async () => {
const { tokenAddress, value: input, totalSupply } = this.state; const { tokenAddress, value: input, totalSupply } = this.state
const { const {
exchangeAddresses: { fromToken }, exchangeAddresses: { fromToken },
web3, web3,
selectors, selectors,
account, account
} = this.props; } = this.props
const exchangeAddress = fromToken[tokenAddress]; const exchangeAddress = fromToken[tokenAddress]
const { getBalance } = selectors(); const { getBalance } = selectors()
if (!web3 || !exchangeAddress) { if (!web3 || !exchangeAddress) {
return; return
} }
const exchange = new web3.eth.Contract(EXCHANGE_ABI, exchangeAddress); const exchange = new web3.eth.Contract(EXCHANGE_ABI, exchangeAddress)
const SLIPPAGE = .02; const SLIPPAGE = 0.02
const { decimals } = getBalance(account, exchangeAddress); const { decimals } = getBalance(account, exchangeAddress)
const { value: ethReserve } = getBalance(exchangeAddress); const { value: ethReserve } = getBalance(exchangeAddress)
const { value: tokenReserve } = getBalance(exchangeAddress, tokenAddress); const { value: tokenReserve } = getBalance(exchangeAddress, tokenAddress)
const amount = BN(input).multipliedBy(10 ** decimals); const amount = BN(input).multipliedBy(10 ** decimals)
const ownership = amount.dividedBy(totalSupply); const ownership = amount.dividedBy(totalSupply)
const ethWithdrawn = ethReserve.multipliedBy(ownership); const ethWithdrawn = ethReserve.multipliedBy(ownership)
const tokenWithdrawn = tokenReserve.multipliedBy(ownership); const tokenWithdrawn = tokenReserve.multipliedBy(ownership)
let deadline; let deadline
try { try {
deadline = await retry(() => getBlockDeadline(web3, 600)); deadline = await retry(() => getBlockDeadline(web3, 600))
} catch(e) { } catch (e) {
// TODO: Handle error. // TODO: Handle error.
return; return
} }
exchange.methods.removeLiquidity( exchange.methods
.removeLiquidity(
amount.toFixed(0), amount.toFixed(0),
ethWithdrawn.multipliedBy(1 - SLIPPAGE).toFixed(0), ethWithdrawn.multipliedBy(1 - SLIPPAGE).toFixed(0),
tokenWithdrawn.multipliedBy(1 - SLIPPAGE).toFixed(0), tokenWithdrawn.multipliedBy(1 - SLIPPAGE).toFixed(0),
deadline, deadline
).send({ from: account }, (err, data) => { )
.send({ from: account }, (err, data) => {
if (data) { if (data) {
this.reset(); this.reset()
this.props.addPendingTx(data); this.props.addPendingTx(data)
ReactGA.event({ ReactGA.event({
category: 'Pool', category: 'Pool',
action: 'RemoveLiquidity', action: 'RemoveLiquidity'
}); })
}
})
} }
});
};
getBalance = () => { getBalance = () => {
const { const {
exchangeAddresses: { fromToken }, exchangeAddresses: { fromToken },
account, account,
web3, web3,
selectors, selectors
} = this.props; } = this.props
const { tokenAddress } = this.state; const { tokenAddress } = this.state
if (!web3) { if (!web3) {
return ''; return ''
} }
const exchangeAddress = fromToken[tokenAddress]; const exchangeAddress = fromToken[tokenAddress]
if (!exchangeAddress) { if (!exchangeAddress) {
return ''; return ''
} }
const { value, decimals } = selectors().getBalance(account, exchangeAddress); const { value, decimals } = selectors().getBalance(account, exchangeAddress)
if (!decimals) { if (!decimals) {
return ''; return ''
} }
return `Balance: ${value.dividedBy(10 ** decimals).toFixed(7)}`; return `Balance: ${value.dividedBy(10 ** decimals).toFixed(7)}`
}; }
renderSummary(errorMessage) { renderSummary(errorMessage) {
const { t, selectors, exchangeAddresses: { fromToken } } = this.props;
const { const {
value: input, t,
tokenAddress, selectors,
} = this.state; exchangeAddresses: { fromToken }
const inputIsZero = BN(input).isZero(); } = this.props
let contextualInfo = ''; const { value: input, tokenAddress } = this.state
let isError = false; const inputIsZero = BN(input).isZero()
let contextualInfo = ''
let isError = false
if (errorMessage) { if (errorMessage) {
contextualInfo = errorMessage; contextualInfo = errorMessage
isError = true; isError = true
} else if (!tokenAddress) { } else if (!tokenAddress) {
contextualInfo = t("selectTokenCont"); contextualInfo = t('selectTokenCont')
} else if (inputIsZero) { } else if (inputIsZero) {
contextualInfo = t("noZero"); contextualInfo = t('noZero')
} else if (!input) { } else if (!input) {
const { label } = selectors().getTokenBalance(tokenAddress, fromToken[tokenAddress]); const { label } = selectors().getTokenBalance(tokenAddress, fromToken[tokenAddress])
contextualInfo = t("enterLabelCont", { label }); contextualInfo = t('enterLabelCont', { label })
} }
return ( return (
<ContextualInfo <ContextualInfo
key="context-info" key="context-info"
openDetailsText={t("transactionDetails")} openDetailsText={t('transactionDetails')}
closeDetailsText={t("hideDetails")} closeDetailsText={t('hideDetails')}
contextualInfo={contextualInfo} contextualInfo={contextualInfo}
isError={isError} isError={isError}
renderTransactionDetails={this.renderTransactionDetails} renderTransactionDetails={this.renderTransactionDetails}
/> />
); )
} }
renderTransactionDetails = () => { renderTransactionDetails = () => {
const { tokenAddress, value: input, totalSupply } = this.state; const { tokenAddress, value: input, totalSupply } = this.state
const { const {
t, t,
exchangeAddresses: { fromToken }, exchangeAddresses: { fromToken },
selectors, selectors,
account, account
} = this.props; } = this.props
const exchangeAddress = fromToken[tokenAddress]; const exchangeAddress = fromToken[tokenAddress]
const { getBalance } = selectors(); const { getBalance } = selectors()
if (!exchangeAddress) { if (!exchangeAddress) {
return null; return null
} }
ReactGA.event({ ReactGA.event({
category: 'TransactionDetail', category: 'TransactionDetail',
action: 'Open', action: 'Open'
}); })
const SLIPPAGE = 0.025; const SLIPPAGE = 0.025
const { decimals } = getBalance(account, exchangeAddress); const { decimals } = getBalance(account, exchangeAddress)
const { value: ethReserve } = getBalance(exchangeAddress); const { value: ethReserve } = getBalance(exchangeAddress)
const { value: tokenReserve, label } = getBalance(exchangeAddress, tokenAddress); const { value: tokenReserve, label } = getBalance(exchangeAddress, tokenAddress)
const ethPer = ethReserve.dividedBy(totalSupply); const ethPer = ethReserve.dividedBy(totalSupply)
const tokenPer = tokenReserve.dividedBy(totalSupply); const tokenPer = tokenReserve.dividedBy(totalSupply)
const ethWithdrawn = ethPer.multipliedBy(input); const ethWithdrawn = ethPer.multipliedBy(input)
const tokenWithdrawn = tokenPer.multipliedBy(input); const tokenWithdrawn = tokenPer.multipliedBy(input)
const minTokenWithdrawn = tokenWithdrawn.multipliedBy(1 - SLIPPAGE).toFixed(7); const minTokenWithdrawn = tokenWithdrawn.multipliedBy(1 - SLIPPAGE).toFixed(7)
const maxTokenWithdrawn = tokenWithdrawn.multipliedBy(1 + SLIPPAGE).toFixed(7); const maxTokenWithdrawn = tokenWithdrawn.multipliedBy(1 + SLIPPAGE).toFixed(7)
const adjTotalSupply = totalSupply.dividedBy(10 ** decimals).minus(input); const adjTotalSupply = totalSupply.dividedBy(10 ** decimals).minus(input)
return ( return (
<div> <div>
<div className="pool__summary-modal__item">{t("youAreRemoving")} {b(`${+BN(ethWithdrawn).toFixed(7)} ETH`)} {t("and")} {b(`${+minTokenWithdrawn} - ${+maxTokenWithdrawn} ${label}`)} {t("outPool")}</div> <div className="pool__summary-modal__item">
<div className="pool__summary-modal__item">{t("youWillRemove")} {b(+input)} {t("liquidityTokens")}</div> {t('youAreRemoving')} {b(`${+BN(ethWithdrawn).toFixed(7)} ETH`)} {t('and')}{' '}
<div className="pool__summary-modal__item">{t("totalSupplyIs")} {b(+adjTotalSupply.toFixed(7))}</div> {b(`${+minTokenWithdrawn} - ${+maxTokenWithdrawn} ${label}`)} {t('outPool')}
<div className="pool__summary-modal__item">{t("tokenWorth")} {b(+ethReserve.dividedBy(totalSupply).toFixed(7))} ETH {t("and")} {b(+tokenReserve.dividedBy(totalSupply).toFixed(7))} {label}</div> </div>
<div className="pool__summary-modal__item">
{t('youWillRemove')} {b(+input)} {t('liquidityTokens')}
</div> </div>
); <div className="pool__summary-modal__item">
{t('totalSupplyIs')} {b(+adjTotalSupply.toFixed(7))}
</div>
<div className="pool__summary-modal__item">
{t('tokenWorth')} {b(+ethReserve.dividedBy(totalSupply).toFixed(7))} ETH {t('and')}{' '}
{b(+tokenReserve.dividedBy(totalSupply).toFixed(7))} {label}
</div>
</div>
)
} }
renderOutput() { renderOutput() {
...@@ -245,80 +267,77 @@ class RemoveLiquidity extends Component { ...@@ -245,80 +267,77 @@ class RemoveLiquidity extends Component {
exchangeAddresses: { fromToken }, exchangeAddresses: { fromToken },
account, account,
web3, web3,
selectors, selectors
} = this.props; } = this.props
const { getBalance } = selectors(); const { getBalance } = selectors()
const { tokenAddress, totalSupply, value: input } = this.state; const { tokenAddress, totalSupply, value: input } = this.state
const blank = [ const blank = [
<CurrencyInputPanel <CurrencyInputPanel
key="remove-liquidity-input" key="remove-liquidity-input"
title={t("output")} title={t('output')}
description={`(${t("estimated")})`} description={`(${t('estimated')})`}
renderInput={() => ( renderInput={() => <div className="remove-liquidity__output" />}
<div className="remove-liquidity__output"></div>
)}
disableTokenSelect disableTokenSelect
disableUnlock disableUnlock
/>, />,
<OversizedPanel key="remove-liquidity-input-under" hideBottom> <OversizedPanel key="remove-liquidity-input-under" hideBottom>
<div className="pool__summary-panel"> <div className="pool__summary-panel">
<div className="pool__exchange-rate-wrapper"> <div className="pool__exchange-rate-wrapper">
<span className="pool__exchange-rate">{t("exchangeRate")}</span> <span className="pool__exchange-rate">{t('exchangeRate')}</span>
<span> - </span> <span> - </span>
</div> </div>
<div className="pool__exchange-rate-wrapper"> <div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t("currentPoolSize")}</span> <span className="swap__exchange-rate">{t('currentPoolSize')}</span>
<span> - </span> <span> - </span>
</div> </div>
<div className="pool__exchange-rate-wrapper"> <div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t("yourPoolShare")}</span> <span className="swap__exchange-rate">{t('yourPoolShare')}</span>
<span> - </span> <span> - </span>
</div> </div>
</div> </div>
</OversizedPanel> </OversizedPanel>
]; ]
const exchangeAddress = fromToken[tokenAddress]; const exchangeAddress = fromToken[tokenAddress]
if (!exchangeAddress || !web3) { if (!exchangeAddress || !web3) {
return blank; return blank
} }
const { value: liquidityBalance } = getBalance(account, exchangeAddress); const { value: liquidityBalance } = getBalance(account, exchangeAddress)
const { value: ethReserve } = getBalance(exchangeAddress); const { value: ethReserve } = getBalance(exchangeAddress)
const { value: tokenReserve, decimals: tokenDecimals, label } = getBalance(exchangeAddress, tokenAddress); const { value: tokenReserve, decimals: tokenDecimals, label } = getBalance(exchangeAddress, tokenAddress)
if (!tokenDecimals) { if (!tokenDecimals) {
return blank; return blank
} }
const ownership = liquidityBalance.dividedBy(totalSupply); const ownership = liquidityBalance.dividedBy(totalSupply)
const ethPer = ethReserve.dividedBy(totalSupply); const ethPer = ethReserve.dividedBy(totalSupply)
const tokenPer = tokenReserve.multipliedBy(10 ** (18 - tokenDecimals)).dividedBy(totalSupply); const tokenPer = tokenReserve.multipliedBy(10 ** (18 - tokenDecimals)).dividedBy(totalSupply)
const exchangeRate = tokenReserve.multipliedBy(10 ** (18 - tokenDecimals)).div(ethReserve); const exchangeRate = tokenReserve.multipliedBy(10 ** (18 - tokenDecimals)).div(ethReserve)
const ownedEth = ethPer.multipliedBy(liquidityBalance).dividedBy(10 ** 18); const ownedEth = ethPer.multipliedBy(liquidityBalance).dividedBy(10 ** 18)
const ownedToken = tokenPer.multipliedBy(liquidityBalance).dividedBy(10 ** tokenDecimals); const ownedToken = tokenPer.multipliedBy(liquidityBalance).dividedBy(10 ** tokenDecimals)
return [ return [
<CurrencyInputPanel <CurrencyInputPanel
title={t("output")} title={t('output')}
description={`(${t("estimated")})`} description={`(${t('estimated')})`}
key="remove-liquidity-input" key="remove-liquidity-input"
renderInput={() => input renderInput={() =>
? ( input ? (
<div className="remove-liquidity__output"> <div className="remove-liquidity__output">
<div className="remove-liquidity__output-text"> <div className="remove-liquidity__output-text">{`${ethPer.multipliedBy(input).toFixed(3)} ETH`}</div>
{`${ethPer.multipliedBy(input).toFixed(3)} ETH`}
</div>
<div className="remove-liquidity__output-plus"> + </div> <div className="remove-liquidity__output-plus"> + </div>
<div className="remove-liquidity__output-text"> <div className="remove-liquidity__output-text">
{`${tokenPer.multipliedBy(input).toFixed(3)} ${label}`} {`${tokenPer.multipliedBy(input).toFixed(3)} ${label}`}
</div> </div>
</div> </div>
) : (
<div className="remove-liquidity__output" />
) )
: <div className="remove-liquidity__output" />
} }
disableTokenSelect disableTokenSelect
disableUnlock disableUnlock
...@@ -326,46 +345,46 @@ class RemoveLiquidity extends Component { ...@@ -326,46 +345,46 @@ class RemoveLiquidity extends Component {
<OversizedPanel key="remove-liquidity-input-under" hideBottom> <OversizedPanel key="remove-liquidity-input-under" hideBottom>
<div className="pool__summary-panel"> <div className="pool__summary-panel">
<div className="pool__exchange-rate-wrapper"> <div className="pool__exchange-rate-wrapper">
<span className="pool__exchange-rate">{t("exchangeRate")}</span> <span className="pool__exchange-rate">{t('exchangeRate')}</span>
<span> <span>{`1 ETH = ${exchangeRate.toFixed(4)} ${label}`}</span>
{`1 ETH = ${exchangeRate.toFixed(4)} ${label}`}
</span>
</div> </div>
<div className="pool__exchange-rate-wrapper"> <div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t("currentPoolSize")}</span> <span className="swap__exchange-rate">{t('currentPoolSize')}</span>
<span>{`${ethReserve.dividedBy(10 ** 18).toFixed(2)} ETH + ${tokenReserve.dividedBy(10 ** tokenDecimals).toFixed(2)} ${label}`}</span> <span>{`${ethReserve.dividedBy(10 ** 18).toFixed(2)} ETH + ${tokenReserve
.dividedBy(10 ** tokenDecimals)
.toFixed(2)} ${label}`}</span>
</div> </div>
<div className="pool__exchange-rate-wrapper"> <div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate"> <span className="swap__exchange-rate">
{t("yourPoolShare")} ({ownership.multipliedBy(100).toFixed(2)}%) {t('yourPoolShare')} ({ownership.multipliedBy(100).toFixed(2)}%)
</span> </span>
<span>{`${ownedEth.toFixed(2)} ETH + ${ownedToken.toFixed(2)} ${label}`}</span> <span>{`${ownedEth.toFixed(2)} ETH + ${ownedToken.toFixed(2)} ${label}`}</span>
</div> </div>
</div> </div>
</OversizedPanel> </OversizedPanel>
]; ]
} }
render() { render() {
const { t, isConnected } = this.props; const { t, isConnected } = this.props
const { tokenAddress, value } = this.state; const { tokenAddress, value } = this.state
const { isValid, errorMessage } = this.validate(); const { isValid, errorMessage } = this.validate()
return [ return [
<div <div
key="content" key="content"
className={classnames('swap__content', { className={classnames('swap__content', {
'swap--inactive': !isConnected, 'swap--inactive': !isConnected
})} })}
> >
<NavigationTabs <NavigationTabs
className={classnames('header__navigation', { className={classnames('header__navigation', {
'header--inactive': !isConnected, 'header--inactive': !isConnected
})} })}
/> />
<ModeSelector title={t("removeLiquidity")} /> <ModeSelector title={t('removeLiquidity')} />
<CurrencyInputPanel <CurrencyInputPanel
title={t("poolTokens")} title={t('poolTokens')}
extraText={this.getBalance(tokenAddress)} extraText={this.getBalance(tokenAddress)}
onValueChange={this.onInputChange} onValueChange={this.onInputChange}
value={value} value={value}
...@@ -376,41 +395,42 @@ class RemoveLiquidity extends Component { ...@@ -376,41 +395,42 @@ class RemoveLiquidity extends Component {
/> />
<OversizedPanel> <OversizedPanel>
<div className="swap__down-arrow-background"> <div className="swap__down-arrow-background">
<img className="swap__down-arrow" src={isValid ? ArrowDownBlue : ArrowDownGrey} alt='arrow' /> <img className="swap__down-arrow" src={isValid ? ArrowDownBlue : ArrowDownGrey} alt="arrow" />
</div> </div>
</OversizedPanel> </OversizedPanel>
{ this.renderOutput() } {this.renderOutput()}
{ this.renderSummary(errorMessage) } {this.renderSummary(errorMessage)}
<div className="pool__cta-container"> <div className="pool__cta-container">
<button <button
className={classnames('pool__cta-btn', { className={classnames('pool__cta-btn', {
'swap--inactive': !isConnected, 'swap--inactive': !isConnected,
'pool__cta-btn--inactive': !isValid, 'pool__cta-btn--inactive': !isValid
})} })}
disabled={!isValid} disabled={!isValid}
onClick={this.onRemoveLiquidity} onClick={this.onRemoveLiquidity}
> >
{t("removeLiquidity")} {t('removeLiquidity')}
</button> </button>
</div> </div>
</div> </div>
]; ]
} }
} }
export default connect( export default connect(
state => ({ state => ({
isConnected: Boolean(state.web3connect.account) && state.web3connect.networkId === (process.env.REACT_APP_NETWORK_ID||1), isConnected:
Boolean(state.web3connect.account) && state.web3connect.networkId === (process.env.REACT_APP_NETWORK_ID || 1),
web3: state.web3connect.web3, web3: state.web3connect.web3,
balances: state.web3connect.balances, balances: state.web3connect.balances,
account: state.web3connect.account, account: state.web3connect.account,
exchangeAddresses: state.addresses.exchangeAddresses, exchangeAddresses: state.addresses.exchangeAddresses
}), }),
dispatch => ({ dispatch => ({
selectors: () => dispatch(selectors()), selectors: () => dispatch(selectors()),
addPendingTx: id => dispatch(addPendingTx(id)), addPendingTx: id => dispatch(addPendingTx(id))
}) })
)(withNamespaces()(RemoveLiquidity)); )(withNamespaces()(RemoveLiquidity))
function b(text) { function b(text) {
return <span className="swap__highlight-text">{text}</span> return <span className="swap__highlight-text">{text}</span>
......
import React, { Component } from 'react'; import React, { Component } from 'react'
import Header from '../../components/Header'; import Header from '../../components/Header'
import AddLiquidity from './AddLiquidity'; import AddLiquidity from './AddLiquidity'
import CreateExchange from './CreateExchange'; import CreateExchange from './CreateExchange'
import RemoveLiquidity from './RemoveLiquidity'; import RemoveLiquidity from './RemoveLiquidity'
import { Switch, Route } from 'react-router-dom'; import { Switch, Route } from 'react-router-dom'
import "./pool.scss"; import './pool.scss'
import MediaQuery from "react-responsive"; import MediaQuery from 'react-responsive'
import ReactGA from "react-ga"; import ReactGA from 'react-ga'
class Pool extends Component { class Pool extends Component {
componentWillMount() { componentWillMount() {
ReactGA.pageview(window.location.pathname + window.location.search); ReactGA.pageview(window.location.pathname + window.location.search)
} }
render() { render() {
return ( return (
...@@ -25,8 +24,8 @@ class Pool extends Component { ...@@ -25,8 +24,8 @@ class Pool extends Component {
<Route exact path="/create-exchange/:tokenAddress?" component={CreateExchange} /> <Route exact path="/create-exchange/:tokenAddress?" component={CreateExchange} />
</Switch> </Switch>
</div> </div>
); )
} }
} }
export default Pool; export default Pool
@import "../../variables.scss"; @import '../../variables.scss';
.pool { .pool {
@extend %col-nowrap; @extend %col-nowrap;
...@@ -8,16 +8,16 @@ ...@@ -8,16 +8,16 @@
&__liquidity-container { &__liquidity-container {
@extend %row-nowrap; @extend %row-nowrap;
align-items: center; align-items: center;
font-size: .75rem; font-size: 0.75rem;
padding: .625rem 1rem; padding: 0.625rem 1rem;
font-size: .75rem; font-size: 0.75rem;
color: $royal-blue; color: $royal-blue;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
img { img {
height: .75rem; height: 0.75rem;
width: .75rem; width: 0.75rem;
} }
} }
...@@ -38,8 +38,8 @@ ...@@ -38,8 +38,8 @@
@extend %row-nowrap; @extend %row-nowrap;
align-items: center; align-items: center;
color: $dove-gray; color: $dove-gray;
font-size: .75rem; font-size: 0.75rem;
padding: .25rem 1rem 0; padding: 0.25rem 1rem 0;
} }
&__exchange-rate { &__exchange-rate {
...@@ -64,25 +64,25 @@ ...@@ -64,25 +64,25 @@
&__new-exchange-warning { &__new-exchange-warning {
padding: 1rem; padding: 1rem;
margin-bottom: 2rem; margin-bottom: 2rem;
border: 1px solid rgba($pizazz-orange, .4); border: 1px solid rgba($pizazz-orange, 0.4);
background-color: rgba($pizazz-orange, .1); background-color: rgba($pizazz-orange, 0.1);
border-radius: 1rem; border-radius: 1rem;
} }
&__new-exchange-warning-text { &__new-exchange-warning-text {
font-size: .75rem; font-size: 0.75rem;
line-height: 1rem; line-height: 1rem;
text-align: center; text-align: center;
&:first-child { &:first-child {
padding-bottom: .3rem; padding-bottom: 0.3rem;
font-weight: 500; font-weight: 500;
} }
} }
&__summary-item { &__summary-item {
&:not(:last-child) { &:not(:last-child) {
margin-bottom: .5rem; margin-bottom: 0.5rem;
} }
} }
} }
...@@ -97,9 +97,9 @@ ...@@ -97,9 +97,9 @@
border-top-left-radius: 1rem; border-top-left-radius: 1rem;
border-top-right-radius: 1rem; border-top-right-radius: 1rem;
transition: 250ms ease-in-out; transition: 250ms ease-in-out;
padding: 1rem 0 .5rem; padding: 1rem 0 0.5rem;
@media only screen and (min-width : 768px) { @media only screen and (min-width: 768px) {
max-width: 560px; max-width: 560px;
position: absolute; position: absolute;
margin-left: auto; margin-left: auto;
...@@ -149,7 +149,7 @@ ...@@ -149,7 +149,7 @@
} }
&__summary-text { &__summary-text {
font-size: .75rem; font-size: 0.75rem;
} }
&--error { &--error {
...@@ -166,7 +166,7 @@ ...@@ -166,7 +166,7 @@
&__output-text { &__output-text {
font-size: 1.25rem; font-size: 1.25rem;
line-height: 1.5rem; line-height: 1.5rem;
padding: 1rem .75rem; padding: 1rem 0.75rem;
} }
&__output-plus { &__output-plus {
......
import React, { Component } from 'react'; import React, { Component } from 'react'
import { connect } from 'react-redux'; import { connect } from 'react-redux'
import PropTypes from 'prop-types'; import PropTypes from 'prop-types'
import classnames from 'classnames'; import classnames from 'classnames'
import {BigNumber as BN} from "bignumber.js"; import { BigNumber as BN } from 'bignumber.js'
import { withNamespaces } from 'react-i18next'; import { withNamespaces } from 'react-i18next'
import { selectors, addPendingTx } from '../../ducks/web3connect'; import { selectors, addPendingTx } from '../../ducks/web3connect'
import Header from '../../components/Header'; import Header from '../../components/Header'
import NavigationTabs from '../../components/NavigationTabs'; import NavigationTabs from '../../components/NavigationTabs'
import AddressInputPanel from '../../components/AddressInputPanel'; import AddressInputPanel from '../../components/AddressInputPanel'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'; import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import ContextualInfo from '../../components/ContextualInfo'; import ContextualInfo from '../../components/ContextualInfo'
import OversizedPanel from '../../components/OversizedPanel'; import OversizedPanel from '../../components/OversizedPanel'
import ArrowDownBlue from '../../assets/images/arrow-down-blue.svg'; import ArrowDownBlue from '../../assets/images/arrow-down-blue.svg'
import ArrowDownGrey from '../../assets/images/arrow-down-grey.svg'; import ArrowDownGrey from '../../assets/images/arrow-down-grey.svg'
import { getBlockDeadline } from '../../helpers/web3-utils'; import { getBlockDeadline } from '../../helpers/web3-utils'
import { retry } from '../../helpers/promise-utils'; import { retry } from '../../helpers/promise-utils'
import EXCHANGE_ABI from '../../abi/exchange'; import EXCHANGE_ABI from '../../abi/exchange'
import "./send.scss"; import './send.scss'
import MediaQuery from "react-responsive"; import MediaQuery from 'react-responsive'
import ReactGA from "react-ga"; import ReactGA from 'react-ga'
const INPUT = 0; const INPUT = 0
const OUTPUT = 1; const OUTPUT = 1
class Send extends Component { class Send extends Component {
static propTypes = { static propTypes = {
account: PropTypes.string, account: PropTypes.string,
isConnected: PropTypes.bool.isRequired, isConnected: PropTypes.bool.isRequired,
selectors: PropTypes.func.isRequired, selectors: PropTypes.func.isRequired,
web3: PropTypes.object.isRequired, web3: PropTypes.object.isRequired
}; }
state = { state = {
inputValue: '', inputValue: '',
...@@ -39,15 +39,15 @@ class Send extends Component { ...@@ -39,15 +39,15 @@ class Send extends Component {
outputCurrency: '', outputCurrency: '',
inputAmountB: '', inputAmountB: '',
lastEditedField: '', lastEditedField: '',
recipient: '', recipient: ''
}; }
componentWillMount() { componentWillMount() {
ReactGA.pageview(window.location.pathname + window.location.search); ReactGA.pageview(window.location.pathname + window.location.search)
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
return true; return true
} }
reset() { reset() {
...@@ -56,116 +56,125 @@ class Send extends Component { ...@@ -56,116 +56,125 @@ class Send extends Component {
outputValue: '', outputValue: '',
inputAmountB: '', inputAmountB: '',
lastEditedField: '', lastEditedField: '',
recipient: '', recipient: ''
}); })
} }
componentWillReceiveProps() { componentWillReceiveProps() {
this.recalcForm(); this.recalcForm()
} }
validate() { validate() {
const { selectors, account, web3 } = this.props; const { selectors, account, web3 } = this.props
const { const { inputValue, outputValue, inputCurrency, outputCurrency, recipient } = this.state
inputValue, outputValue,
inputCurrency, outputCurrency, let inputError = ''
recipient, let outputError = ''
} = this.state; let isValid = true
const validRecipientAddress = web3 && web3.utils.isAddress(recipient)
let inputError = ''; const inputIsZero = BN(inputValue).isZero()
let outputError = ''; const outputIsZero = BN(outputValue).isZero()
let isValid = true;
const validRecipientAddress = web3 && web3.utils.isAddress(recipient); if (
const inputIsZero = BN(inputValue).isZero(); !inputValue ||
const outputIsZero = BN(outputValue).isZero(); inputIsZero ||
!outputValue ||
if (!inputValue || inputIsZero || !outputValue || outputIsZero || !inputCurrency || !outputCurrency || !recipient || this.isUnapproved() || !validRecipientAddress) { outputIsZero ||
isValid = false; !inputCurrency ||
} !outputCurrency ||
!recipient ||
const { value: inputBalance, decimals: inputDecimals } = selectors().getBalance(account, inputCurrency); this.isUnapproved() ||
!validRecipientAddress
) {
isValid = false
}
const { value: inputBalance, decimals: inputDecimals } = selectors().getBalance(account, inputCurrency)
if (inputBalance.isLessThan(BN(inputValue * 10 ** inputDecimals))) { if (inputBalance.isLessThan(BN(inputValue * 10 ** inputDecimals))) {
inputError = this.props.t("insufficientBalance"); inputError = this.props.t('insufficientBalance')
} }
if (inputValue === 'N/A') { if (inputValue === 'N/A') {
inputError = this.props.t("inputNotValid"); inputError = this.props.t('inputNotValid')
} }
return { return {
inputError, inputError,
outputError, outputError,
isValid: isValid && !inputError && !outputError, isValid: isValid && !inputError && !outputError
}; }
} }
flipInputOutput = () => { flipInputOutput = () => {
const { state } = this; const { state } = this
this.setState({ this.setState(
{
inputValue: state.outputValue, inputValue: state.outputValue,
outputValue: state.inputValue, outputValue: state.inputValue,
inputCurrency: state.outputCurrency, inputCurrency: state.outputCurrency,
outputCurrency: state.inputCurrency, outputCurrency: state.inputCurrency,
lastEditedField: state.lastEditedField === INPUT ? OUTPUT : INPUT lastEditedField: state.lastEditedField === INPUT ? OUTPUT : INPUT
}, () => this.recalcForm()); },
() => this.recalcForm()
)
} }
isUnapproved() { isUnapproved() {
const { account, exchangeAddresses, selectors } = this.props; const { account, exchangeAddresses, selectors } = this.props
const { inputCurrency, inputValue } = this.state; const { inputCurrency, inputValue } = this.state
if (!inputCurrency || inputCurrency === 'ETH') { if (!inputCurrency || inputCurrency === 'ETH') {
return false; return false
} }
const { value: allowance, label, decimals } = selectors().getApprovals( const { value: allowance, label, decimals } = selectors().getApprovals(
inputCurrency, inputCurrency,
account, account,
exchangeAddresses.fromToken[inputCurrency] exchangeAddresses.fromToken[inputCurrency]
); )
if (label && allowance.isLessThan(BN(inputValue * 10 ** decimals || 0))) { if (label && allowance.isLessThan(BN(inputValue * 10 ** decimals || 0))) {
return true; return true
} }
return false; return false
} }
recalcForm() { recalcForm() {
const { inputCurrency, outputCurrency, lastEditedField } = this.state; const { inputCurrency, outputCurrency, lastEditedField } = this.state
if (!inputCurrency || !outputCurrency) { if (!inputCurrency || !outputCurrency) {
return; return
} }
const editedValue = lastEditedField === INPUT ? this.state.inputValue : this.state.outputValue; const editedValue = lastEditedField === INPUT ? this.state.inputValue : this.state.outputValue
if (BN(editedValue).isZero()) { if (BN(editedValue).isZero()) {
return; return
} }
if (inputCurrency === outputCurrency) { if (inputCurrency === outputCurrency) {
this.setState({ this.setState({
inputValue: '', inputValue: '',
outputValue: '', outputValue: ''
}); })
return; return
} }
if (inputCurrency !== 'ETH' && outputCurrency !== 'ETH') { if (inputCurrency !== 'ETH' && outputCurrency !== 'ETH') {
this.recalcTokenTokenForm(); this.recalcTokenTokenForm()
return; return
} }
this.recalcEthTokenForm(); this.recalcEthTokenForm()
} }
recalcTokenTokenForm = () => { recalcTokenTokenForm = () => {
const { const {
exchangeAddresses: { fromToken }, exchangeAddresses: { fromToken },
selectors, selectors
} = this.props; } = this.props
const { const {
inputValue: oldInputValue, inputValue: oldInputValue,
...@@ -174,109 +183,109 @@ class Send extends Component { ...@@ -174,109 +183,109 @@ class Send extends Component {
outputCurrency, outputCurrency,
lastEditedField, lastEditedField,
exchangeRate: oldExchangeRate, exchangeRate: oldExchangeRate,
inputAmountB: oldInputAmountB, inputAmountB: oldInputAmountB
} = this.state; } = this.state
const exchangeAddressA = fromToken[inputCurrency]; const exchangeAddressA = fromToken[inputCurrency]
const exchangeAddressB = fromToken[outputCurrency]; const exchangeAddressB = fromToken[outputCurrency]
const { value: inputReserveA, decimals: inputDecimalsA } = selectors().getBalance(exchangeAddressA, inputCurrency); const { value: inputReserveA, decimals: inputDecimalsA } = selectors().getBalance(exchangeAddressA, inputCurrency)
const { value: outputReserveA }= selectors().getBalance(exchangeAddressA, 'ETH'); const { value: outputReserveA } = selectors().getBalance(exchangeAddressA, 'ETH')
const { value: inputReserveB } = selectors().getBalance(exchangeAddressB, 'ETH'); const { value: inputReserveB } = selectors().getBalance(exchangeAddressB, 'ETH')
const { value: outputReserveB, decimals: outputDecimalsB }= selectors().getBalance(exchangeAddressB, outputCurrency); const { value: outputReserveB, decimals: outputDecimalsB } = selectors().getBalance(
exchangeAddressB,
outputCurrency
)
if (lastEditedField === INPUT) { if (lastEditedField === INPUT) {
if (!oldInputValue) { if (!oldInputValue) {
return this.setState({ return this.setState({
outputValue: '', outputValue: '',
exchangeRate: BN(0), exchangeRate: BN(0)
}); })
} }
const inputAmountA = BN(oldInputValue).multipliedBy(10 ** inputDecimalsA); const inputAmountA = BN(oldInputValue).multipliedBy(10 ** inputDecimalsA)
const outputAmountA = calculateEtherTokenOutput({ const outputAmountA = calculateEtherTokenOutput({
inputAmount: inputAmountA, inputAmount: inputAmountA,
inputReserve: inputReserveA, inputReserve: inputReserveA,
outputReserve: outputReserveA, outputReserve: outputReserveA
}); })
// Redundant Variable for readability of the formala // Redundant Variable for readability of the formala
// OutputAmount from the first send becomes InputAmount of the second send // OutputAmount from the first send becomes InputAmount of the second send
const inputAmountB = outputAmountA; const inputAmountB = outputAmountA
const outputAmountB = calculateEtherTokenOutput({ const outputAmountB = calculateEtherTokenOutput({
inputAmount: inputAmountB, inputAmount: inputAmountB,
inputReserve: inputReserveB, inputReserve: inputReserveB,
outputReserve: outputReserveB, outputReserve: outputReserveB
}); })
const outputValue = outputAmountB.dividedBy(BN(10 ** outputDecimalsB)).toFixed(7); const outputValue = outputAmountB.dividedBy(BN(10 ** outputDecimalsB)).toFixed(7)
const exchangeRate = BN(outputValue).dividedBy(BN(oldInputValue)); const exchangeRate = BN(outputValue).dividedBy(BN(oldInputValue))
const appendState = {}; const appendState = {}
if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) { if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) {
appendState.exchangeRate = exchangeRate; appendState.exchangeRate = exchangeRate
} }
if (outputValue !== oldOutputValue) { if (outputValue !== oldOutputValue) {
appendState.outputValue = outputValue; appendState.outputValue = outputValue
} }
this.setState(appendState); this.setState(appendState)
} }
if (lastEditedField === OUTPUT) { if (lastEditedField === OUTPUT) {
if (!oldOutputValue) { if (!oldOutputValue) {
return this.setState({ return this.setState({
inputValue: '', inputValue: '',
exchangeRate: BN(0), exchangeRate: BN(0)
}); })
} }
const outputAmountB = BN(oldOutputValue).multipliedBy(10 ** outputDecimalsB); const outputAmountB = BN(oldOutputValue).multipliedBy(10 ** outputDecimalsB)
const inputAmountB = calculateEtherTokenInput({ const inputAmountB = calculateEtherTokenInput({
outputAmount: outputAmountB, outputAmount: outputAmountB,
inputReserve: inputReserveB, inputReserve: inputReserveB,
outputReserve: outputReserveB, outputReserve: outputReserveB
}); })
// Redundant Variable for readability of the formala // Redundant Variable for readability of the formala
// InputAmount from the first send becomes OutputAmount of the second send // InputAmount from the first send becomes OutputAmount of the second send
const outputAmountA = inputAmountB; const outputAmountA = inputAmountB
const inputAmountA = calculateEtherTokenInput({ const inputAmountA = calculateEtherTokenInput({
outputAmount: outputAmountA, outputAmount: outputAmountA,
inputReserve: inputReserveA, inputReserve: inputReserveA,
outputReserve: outputReserveA, outputReserve: outputReserveA
}); })
const inputValue = inputAmountA.isNegative() const inputValue = inputAmountA.isNegative() ? 'N/A' : inputAmountA.dividedBy(BN(10 ** inputDecimalsA)).toFixed(7)
? 'N/A' const exchangeRate = BN(oldOutputValue).dividedBy(BN(inputValue))
: inputAmountA.dividedBy(BN(10 ** inputDecimalsA)).toFixed(7);
const exchangeRate = BN(oldOutputValue).dividedBy(BN(inputValue));
const appendState = {}; const appendState = {}
if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) { if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) {
appendState.exchangeRate = exchangeRate; appendState.exchangeRate = exchangeRate
} }
if (inputValue !== oldInputValue) { if (inputValue !== oldInputValue) {
appendState.inputValue = inputValue; appendState.inputValue = inputValue
} }
if (!inputAmountB.isEqualTo(BN(oldInputAmountB))) { if (!inputAmountB.isEqualTo(BN(oldInputAmountB))) {
appendState.inputAmountB = inputAmountB; appendState.inputAmountB = inputAmountB
} }
this.setState(appendState); this.setState(appendState)
}
} }
};
recalcEthTokenForm = () => { recalcEthTokenForm = () => {
const { const {
exchangeAddresses: { fromToken }, exchangeAddresses: { fromToken },
selectors, selectors
} = this.props; } = this.props
const { const {
inputValue: oldInputValue, inputValue: oldInputValue,
...@@ -284,83 +293,87 @@ class Send extends Component { ...@@ -284,83 +293,87 @@ class Send extends Component {
inputCurrency, inputCurrency,
outputCurrency, outputCurrency,
lastEditedField, lastEditedField,
exchangeRate: oldExchangeRate, exchangeRate: oldExchangeRate
} = this.state; } = this.state
const tokenAddress = [inputCurrency, outputCurrency].filter(currency => currency !== 'ETH')[0]; const tokenAddress = [inputCurrency, outputCurrency].filter(currency => currency !== 'ETH')[0]
const exchangeAddress = fromToken[tokenAddress]; const exchangeAddress = fromToken[tokenAddress]
if (!exchangeAddress) { if (!exchangeAddress) {
return; return
} }
const { value: inputReserve, decimals: inputDecimals } = selectors().getBalance(exchangeAddress, inputCurrency); const { value: inputReserve, decimals: inputDecimals } = selectors().getBalance(exchangeAddress, inputCurrency)
const { value: outputReserve, decimals: outputDecimals }= selectors().getBalance(exchangeAddress, outputCurrency); const { value: outputReserve, decimals: outputDecimals } = selectors().getBalance(exchangeAddress, outputCurrency)
if (lastEditedField === INPUT) { if (lastEditedField === INPUT) {
if (!oldInputValue) { if (!oldInputValue) {
return this.setState({ return this.setState({
outputValue: '', outputValue: '',
exchangeRate: BN(0), exchangeRate: BN(0)
}); })
} }
const inputAmount = BN(oldInputValue).multipliedBy(10 ** inputDecimals); const inputAmount = BN(oldInputValue).multipliedBy(10 ** inputDecimals)
const outputAmount = calculateEtherTokenOutput({ inputAmount, inputReserve, outputReserve }); const outputAmount = calculateEtherTokenOutput({ inputAmount, inputReserve, outputReserve })
const outputValue = outputAmount.dividedBy(BN(10 ** outputDecimals)).toFixed(7); const outputValue = outputAmount.dividedBy(BN(10 ** outputDecimals)).toFixed(7)
const exchangeRate = BN(outputValue).dividedBy(BN(oldInputValue)); const exchangeRate = BN(outputValue).dividedBy(BN(oldInputValue))
const appendState = {}; const appendState = {}
if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) { if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) {
appendState.exchangeRate = exchangeRate; appendState.exchangeRate = exchangeRate
} }
if (outputValue !== oldOutputValue) { if (outputValue !== oldOutputValue) {
appendState.outputValue = outputValue; appendState.outputValue = outputValue
} }
this.setState(appendState); this.setState(appendState)
} else if (lastEditedField === OUTPUT) { } else if (lastEditedField === OUTPUT) {
if (!oldOutputValue) { if (!oldOutputValue) {
return this.setState({ return this.setState({
inputValue: '', inputValue: '',
exchangeRate: BN(0), exchangeRate: BN(0)
}); })
} }
const outputAmount = BN(oldOutputValue).multipliedBy(10 ** outputDecimals); const outputAmount = BN(oldOutputValue).multipliedBy(10 ** outputDecimals)
const inputAmount = calculateEtherTokenInput({ outputAmount, inputReserve, outputReserve }); const inputAmount = calculateEtherTokenInput({ outputAmount, inputReserve, outputReserve })
const inputValue = inputAmount.isNegative() const inputValue = inputAmount.isNegative() ? 'N/A' : inputAmount.dividedBy(BN(10 ** inputDecimals)).toFixed(7)
? 'N/A' const exchangeRate = BN(oldOutputValue).dividedBy(BN(inputValue))
: inputAmount.dividedBy(BN(10 ** inputDecimals)).toFixed(7);
const exchangeRate = BN(oldOutputValue).dividedBy(BN(inputValue));
const appendState = {}; const appendState = {}
if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) { if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) {
appendState.exchangeRate = exchangeRate; appendState.exchangeRate = exchangeRate
} }
if (inputValue !== oldInputValue) { if (inputValue !== oldInputValue) {
appendState.inputValue = inputValue; appendState.inputValue = inputValue
} }
this.setState(appendState); this.setState(appendState)
}
} }
};
updateInput = amount => { updateInput = amount => {
this.setState({ this.setState(
{
inputValue: amount, inputValue: amount,
lastEditedField: INPUT, lastEditedField: INPUT
}, this.recalcForm); },
}; this.recalcForm
)
}
updateOutput = amount => { updateOutput = amount => {
this.setState({ this.setState(
{
outputValue: amount, outputValue: amount,
lastEditedField: OUTPUT, lastEditedField: OUTPUT
}, this.recalcForm); },
}; this.recalcForm
)
}
onSend = async () => { onSend = async () => {
const { const {
...@@ -368,8 +381,8 @@ class Send extends Component { ...@@ -368,8 +381,8 @@ class Send extends Component {
account, account,
web3, web3,
selectors, selectors,
addPendingTx, addPendingTx
} = this.props; } = this.props
const { const {
inputValue, inputValue,
outputValue, outputValue,
...@@ -377,83 +390,98 @@ class Send extends Component { ...@@ -377,83 +390,98 @@ class Send extends Component {
outputCurrency, outputCurrency,
inputAmountB, inputAmountB,
lastEditedField, lastEditedField,
recipient, recipient
} = this.state; } = this.state
const ALLOWED_SLIPPAGE = 0.025; const ALLOWED_SLIPPAGE = 0.025
const TOKEN_ALLOWED_SLIPPAGE = 0.04; const TOKEN_ALLOWED_SLIPPAGE = 0.04
const type = getSendType(inputCurrency, outputCurrency); const type = getSendType(inputCurrency, outputCurrency)
const { decimals: inputDecimals } = selectors().getBalance(account, inputCurrency); const { decimals: inputDecimals } = selectors().getBalance(account, inputCurrency)
const { decimals: outputDecimals } = selectors().getBalance(account, outputCurrency); const { decimals: outputDecimals } = selectors().getBalance(account, outputCurrency)
let deadline; let deadline
try { try {
deadline = await retry(() => getBlockDeadline(web3, 600)); deadline = await retry(() => getBlockDeadline(web3, 600))
} catch(e) { } catch (e) {
// TODO: Handle error. // TODO: Handle error.
return; return
} }
if (lastEditedField === INPUT) { if (lastEditedField === INPUT) {
ReactGA.event({ ReactGA.event({
category: type, category: type,
action: 'TransferInput', action: 'TransferInput'
}); })
// send input // send input
switch(type) { switch (type) {
case 'ETH_TO_TOKEN': case 'ETH_TO_TOKEN':
new web3.eth.Contract(EXCHANGE_ABI, fromToken[outputCurrency]) new web3.eth.Contract(EXCHANGE_ABI, fromToken[outputCurrency]).methods
.methods
.ethToTokenTransferInput( .ethToTokenTransferInput(
BN(outputValue).multipliedBy(10 ** outputDecimals).multipliedBy(1 - ALLOWED_SLIPPAGE).toFixed(0), BN(outputValue)
.multipliedBy(10 ** outputDecimals)
.multipliedBy(1 - ALLOWED_SLIPPAGE)
.toFixed(0),
deadline, deadline,
recipient, recipient
) )
.send({ .send(
{
from: account, from: account,
value: BN(inputValue).multipliedBy(10 ** 18).toFixed(0), value: BN(inputValue)
}, (err, data) => { .multipliedBy(10 ** 18)
.toFixed(0)
},
(err, data) => {
if (!err) { if (!err) {
addPendingTx(data); addPendingTx(data)
this.reset(); this.reset()
} }
}); }
break; )
break
case 'TOKEN_TO_ETH': case 'TOKEN_TO_ETH':
new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency]) new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency]).methods
.methods
.tokenToEthTransferInput( .tokenToEthTransferInput(
BN(inputValue).multipliedBy(10 ** inputDecimals).toFixed(0), BN(inputValue)
BN(outputValue).multipliedBy(10 ** outputDecimals).multipliedBy(1 - ALLOWED_SLIPPAGE).toFixed(0), .multipliedBy(10 ** inputDecimals)
.toFixed(0),
BN(outputValue)
.multipliedBy(10 ** outputDecimals)
.multipliedBy(1 - ALLOWED_SLIPPAGE)
.toFixed(0),
deadline, deadline,
recipient, recipient
) )
.send({ from: account }, (err, data) => { .send({ from: account }, (err, data) => {
if (!err) { if (!err) {
addPendingTx(data); addPendingTx(data)
this.reset(); this.reset()
} }
}); })
break; break
case 'TOKEN_TO_TOKEN': case 'TOKEN_TO_TOKEN':
new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency]) new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency]).methods
.methods
.tokenToTokenTransferInput( .tokenToTokenTransferInput(
BN(inputValue).multipliedBy(10 ** inputDecimals).toFixed(0), BN(inputValue)
BN(outputValue).multipliedBy(10 ** outputDecimals).multipliedBy(1 - TOKEN_ALLOWED_SLIPPAGE).toFixed(0), .multipliedBy(10 ** inputDecimals)
.toFixed(0),
BN(outputValue)
.multipliedBy(10 ** outputDecimals)
.multipliedBy(1 - TOKEN_ALLOWED_SLIPPAGE)
.toFixed(0),
'1', '1',
deadline, deadline,
recipient, recipient,
outputCurrency, outputCurrency
) )
.send({ from: account }, (err, data) => { .send({ from: account }, (err, data) => {
if (!err) { if (!err) {
addPendingTx(data); addPendingTx(data)
this.reset(); this.reset()
} }
}); })
break; break
default: default:
break; break
} }
} }
...@@ -461,261 +489,266 @@ class Send extends Component { ...@@ -461,261 +489,266 @@ class Send extends Component {
// send output // send output
ReactGA.event({ ReactGA.event({
category: type, category: type,
action: 'TransferOutput', action: 'TransferOutput'
}); })
switch (type) { switch (type) {
case 'ETH_TO_TOKEN': case 'ETH_TO_TOKEN':
new web3.eth.Contract(EXCHANGE_ABI, fromToken[outputCurrency]) new web3.eth.Contract(EXCHANGE_ABI, fromToken[outputCurrency]).methods
.methods
.ethToTokenTransferOutput( .ethToTokenTransferOutput(
BN(outputValue).multipliedBy(10 ** outputDecimals).toFixed(0), BN(outputValue)
.multipliedBy(10 ** outputDecimals)
.toFixed(0),
deadline, deadline,
recipient, recipient
) )
.send({ .send(
{
from: account, from: account,
value: BN(inputValue).multipliedBy(10 ** inputDecimals).multipliedBy(1 + ALLOWED_SLIPPAGE).toFixed(0), value: BN(inputValue)
}, (err, data) => { .multipliedBy(10 ** inputDecimals)
.multipliedBy(1 + ALLOWED_SLIPPAGE)
.toFixed(0)
},
(err, data) => {
if (!err) { if (!err) {
addPendingTx(data); addPendingTx(data)
this.reset(); this.reset()
}
} }
}); )
break; break
case 'TOKEN_TO_ETH': case 'TOKEN_TO_ETH':
new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency]) new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency]).methods
.methods
.tokenToEthTransferOutput( .tokenToEthTransferOutput(
BN(outputValue).multipliedBy(10 ** outputDecimals).toFixed(0), BN(outputValue)
BN(inputValue).multipliedBy(10 ** inputDecimals).multipliedBy(1 + ALLOWED_SLIPPAGE).toFixed(0), .multipliedBy(10 ** outputDecimals)
.toFixed(0),
BN(inputValue)
.multipliedBy(10 ** inputDecimals)
.multipliedBy(1 + ALLOWED_SLIPPAGE)
.toFixed(0),
deadline, deadline,
recipient, recipient
) )
.send({ from: account }, (err, data) => { .send({ from: account }, (err, data) => {
if (!err) { if (!err) {
addPendingTx(data); addPendingTx(data)
this.reset(); this.reset()
} }
}); })
break; break
case 'TOKEN_TO_TOKEN': case 'TOKEN_TO_TOKEN':
if (!inputAmountB) { if (!inputAmountB) {
return; return
} }
new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency]) new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency]).methods
.methods
.tokenToTokenTransferOutput( .tokenToTokenTransferOutput(
BN(outputValue).multipliedBy(10 ** outputDecimals).toFixed(0), BN(outputValue)
BN(inputValue).multipliedBy(10 ** inputDecimals).multipliedBy(1 + TOKEN_ALLOWED_SLIPPAGE).toFixed(0), .multipliedBy(10 ** outputDecimals)
.toFixed(0),
BN(inputValue)
.multipliedBy(10 ** inputDecimals)
.multipliedBy(1 + TOKEN_ALLOWED_SLIPPAGE)
.toFixed(0),
inputAmountB.multipliedBy(1.2).toFixed(0), inputAmountB.multipliedBy(1.2).toFixed(0),
deadline, deadline,
recipient, recipient,
outputCurrency, outputCurrency
) )
.send({ from: account }, (err, data) => { .send({ from: account }, (err, data) => {
if (!err) { if (!err) {
addPendingTx(data); addPendingTx(data)
this.reset(); this.reset()
} }
}); })
break; break
default: default:
break; break
}
} }
} }
};
renderSummary(inputError, outputError) { renderSummary(inputError, outputError) {
const { const { inputValue, inputCurrency, outputValue, outputCurrency, recipient } = this.state
inputValue, const { t, web3 } = this.props
inputCurrency,
outputValue,
outputCurrency,
recipient,
} = this.state;
const { t, web3 } = this.props;
const { selectors, account } = this.props; const { selectors, account } = this.props
const { label: inputLabel } = selectors().getBalance(account, inputCurrency); const { label: inputLabel } = selectors().getBalance(account, inputCurrency)
const { label: outputLabel } = selectors().getBalance(account, outputCurrency); const { label: outputLabel } = selectors().getBalance(account, outputCurrency)
const validRecipientAddress = web3 && web3.utils.isAddress(recipient); const validRecipientAddress = web3 && web3.utils.isAddress(recipient)
const inputIsZero = BN(inputValue).isZero(); const inputIsZero = BN(inputValue).isZero()
const outputIsZero = BN(outputValue).isZero(); const outputIsZero = BN(outputValue).isZero()
let contextualInfo = ''; let contextualInfo = ''
let isError = false; let isError = false
if (inputError || outputError) { if (inputError || outputError) {
contextualInfo = inputError || outputError; contextualInfo = inputError || outputError
isError = true; isError = true
} else if (!inputCurrency || !outputCurrency) { } else if (!inputCurrency || !outputCurrency) {
contextualInfo = t("selectTokenCont"); contextualInfo = t('selectTokenCont')
} else if (inputCurrency === outputCurrency) { } else if (inputCurrency === outputCurrency) {
contextualInfo = t("differentToken"); contextualInfo = t('differentToken')
} else if (!inputValue || !outputValue) { } else if (!inputValue || !outputValue) {
const missingCurrencyValue = !inputValue ? inputLabel : outputLabel; const missingCurrencyValue = !inputValue ? inputLabel : outputLabel
contextualInfo = t("enterValueCont", {missingCurrencyValue}); contextualInfo = t('enterValueCont', { missingCurrencyValue })
} else if (inputIsZero || outputIsZero) { } else if (inputIsZero || outputIsZero) {
contextualInfo = t("noLiquidity"); contextualInfo = t('noLiquidity')
} else if (this.isUnapproved()) { } else if (this.isUnapproved()) {
contextualInfo = t("unlockTokenCont"); contextualInfo = t('unlockTokenCont')
} else if (!recipient) { } else if (!recipient) {
contextualInfo = t("noRecipient"); contextualInfo = t('noRecipient')
} else if (!validRecipientAddress) { } else if (!validRecipientAddress) {
contextualInfo = t("invalidRecipient"); contextualInfo = t('invalidRecipient')
} }
return ( return (
<ContextualInfo <ContextualInfo
openDetailsText={t("transactionDetails")} openDetailsText={t('transactionDetails')}
closeDetailsText={t("hideDetails")} closeDetailsText={t('hideDetails')}
contextualInfo={contextualInfo} contextualInfo={contextualInfo}
isError={isError} isError={isError}
renderTransactionDetails={this.renderTransactionDetails} renderTransactionDetails={this.renderTransactionDetails}
/> />
); )
} }
renderTransactionDetails = () => { renderTransactionDetails = () => {
const { const { inputValue, inputCurrency, outputValue, outputCurrency, recipient, lastEditedField } = this.state
inputValue, const { t, selectors, account } = this.props
inputCurrency,
outputValue,
outputCurrency,
recipient,
lastEditedField,
} = this.state;
const { t, selectors, account } = this.props;
ReactGA.event({ ReactGA.event({
category: 'TransactionDetail', category: 'TransactionDetail',
action: 'Open', action: 'Open'
}); })
const ALLOWED_SLIPPAGE = 0.025; const ALLOWED_SLIPPAGE = 0.025
const TOKEN_ALLOWED_SLIPPAGE = 0.04; const TOKEN_ALLOWED_SLIPPAGE = 0.04
const type = getSendType(inputCurrency, outputCurrency); const type = getSendType(inputCurrency, outputCurrency)
const { label: inputLabel } = selectors().getBalance(account, inputCurrency); const { label: inputLabel } = selectors().getBalance(account, inputCurrency)
const { label: outputLabel } = selectors().getBalance(account, outputCurrency); const { label: outputLabel } = selectors().getBalance(account, outputCurrency)
// const label = lastEditedField === INPUT ? outputLabel : inputLabel; // const label = lastEditedField === INPUT ? outputLabel : inputLabel;
let minOutput; let minOutput
let maxInput; let maxInput
if (lastEditedField === INPUT) { if (lastEditedField === INPUT) {
switch(type) { switch (type) {
case 'ETH_TO_TOKEN': case 'ETH_TO_TOKEN':
minOutput = BN(outputValue).multipliedBy(1 - ALLOWED_SLIPPAGE).toFixed(7); minOutput = BN(outputValue)
break; .multipliedBy(1 - ALLOWED_SLIPPAGE)
.toFixed(7)
break
case 'TOKEN_TO_ETH': case 'TOKEN_TO_ETH':
minOutput = BN(outputValue).multipliedBy(1 - ALLOWED_SLIPPAGE).toFixed(7); minOutput = BN(outputValue)
break; .multipliedBy(1 - ALLOWED_SLIPPAGE)
.toFixed(7)
break
case 'TOKEN_TO_TOKEN': case 'TOKEN_TO_TOKEN':
minOutput = BN(outputValue).multipliedBy(1 - TOKEN_ALLOWED_SLIPPAGE).toFixed(7); minOutput = BN(outputValue)
break; .multipliedBy(1 - TOKEN_ALLOWED_SLIPPAGE)
.toFixed(7)
break
default: default:
break; break
} }
} }
if (lastEditedField === OUTPUT) { if (lastEditedField === OUTPUT) {
switch (type) { switch (type) {
case 'ETH_TO_TOKEN': case 'ETH_TO_TOKEN':
maxInput = BN(inputValue).multipliedBy(1 + ALLOWED_SLIPPAGE).toFixed(7); maxInput = BN(inputValue)
break; .multipliedBy(1 + ALLOWED_SLIPPAGE)
.toFixed(7)
break
case 'TOKEN_TO_ETH': case 'TOKEN_TO_ETH':
maxInput = BN(inputValue).multipliedBy(1 + ALLOWED_SLIPPAGE).toFixed(7); maxInput = BN(inputValue)
break; .multipliedBy(1 + ALLOWED_SLIPPAGE)
.toFixed(7)
break
case 'TOKEN_TO_TOKEN': case 'TOKEN_TO_TOKEN':
maxInput = BN(inputValue).multipliedBy(1 + TOKEN_ALLOWED_SLIPPAGE).toFixed(7); maxInput = BN(inputValue)
break; .multipliedBy(1 + TOKEN_ALLOWED_SLIPPAGE)
.toFixed(7)
break
default: default:
break; break
} }
} }
const recipientText = b(`${recipient.slice(0, 6)}...${recipient.slice(-4)}`); const recipientText = b(`${recipient.slice(0, 6)}...${recipient.slice(-4)}`)
if (lastEditedField === INPUT) { if (lastEditedField === INPUT) {
return ( return (
<div> <div>
<div> <div>
{t("youAreSending")} {b(`${+inputValue} ${inputLabel}`)}. {t('youAreSending')} {b(`${+inputValue} ${inputLabel}`)}.
</div> </div>
<div className="send__last-summary-text"> <div className="send__last-summary-text">
{recipientText} {t("willReceive")} {b(`${+minOutput} ${outputLabel}`)} {t("orTransFail")} {recipientText} {t('willReceive')} {b(`${+minOutput} ${outputLabel}`)} {t('orTransFail')}
</div> </div>
</div> </div>
); )
} else { } else {
return ( return (
<div> <div>
<div> <div>
{t("youAreSending")} {b(`${+outputValue} ${outputLabel}`)} {t("to")} {recipientText}. {t('youAreSending')} {b(`${+outputValue} ${outputLabel}`)} {t('to')} {recipientText}.
{/*You are selling between {b(`${+inputValue} ${inputLabel}`)} to {b(`${+maxInput} ${inputLabel}`)}.*/} {/*You are selling between {b(`${+inputValue} ${inputLabel}`)} to {b(`${+maxInput} ${inputLabel}`)}.*/}
</div> </div>
<div className="send__last-summary-text"> <div className="send__last-summary-text">
{/*{b(`${recipient.slice(0, 6)}...${recipient.slice(-4)}`)} will receive {b(`${+outputValue} ${outputLabel}`)}.*/} {/*{b(`${recipient.slice(0, 6)}...${recipient.slice(-4)}`)} will receive {b(`${+outputValue} ${outputLabel}`)}.*/}
{t("itWillCost")} {b(`${+maxInput} ${inputLabel}`)} {t("orTransFail")} {t('itWillCost')} {b(`${+maxInput} ${inputLabel}`)} {t('orTransFail')}
</div> </div>
</div> </div>
); )
} }
} }
renderExchangeRate() { renderExchangeRate() {
const { t, account, selectors } = this.props; const { t, account, selectors } = this.props
const { exchangeRate, inputCurrency, outputCurrency } = this.state; const { exchangeRate, inputCurrency, outputCurrency } = this.state
const { label: inputLabel } = selectors().getBalance(account, inputCurrency); const { label: inputLabel } = selectors().getBalance(account, inputCurrency)
const { label: outputLabel } = selectors().getBalance(account, outputCurrency); const { label: outputLabel } = selectors().getBalance(account, outputCurrency)
if (!exchangeRate || exchangeRate.isNaN() || !inputCurrency || !outputCurrency) { if (!exchangeRate || exchangeRate.isNaN() || !inputCurrency || !outputCurrency) {
return ( return (
<OversizedPanel hideBottom> <OversizedPanel hideBottom>
<div className="swap__exchange-rate-wrapper"> <div className="swap__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t("exchangeRate")}</span> <span className="swap__exchange-rate">{t('exchangeRate')}</span>
<span> - </span> <span> - </span>
</div> </div>
</OversizedPanel> </OversizedPanel>
); )
} }
return ( return (
<OversizedPanel hideBottom> <OversizedPanel hideBottom>
<div className="swap__exchange-rate-wrapper"> <div className="swap__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t("exchangeRate")}</span> <span className="swap__exchange-rate">{t('exchangeRate')}</span>
<span> <span>{`1 ${inputLabel} = ${exchangeRate.toFixed(7)} ${outputLabel}`}</span>
{`1 ${inputLabel} = ${exchangeRate.toFixed(7)} ${outputLabel}`}
</span>
</div> </div>
</OversizedPanel> </OversizedPanel>
); )
} }
renderBalance(currency, balance, decimals) { renderBalance(currency, balance, decimals) {
if (!currency || decimals === 0) { if (!currency || decimals === 0) {
return ''; return ''
} }
const balanceInput = balance.dividedBy(BN(10 ** decimals)).toFixed(4) const balanceInput = balance.dividedBy(BN(10 ** decimals)).toFixed(4)
return this.props.t("balance", { balanceInput }) return this.props.t('balance', { balanceInput })
} }
render() { render() {
const { t, selectors, account } = this.props; const { t, selectors, account } = this.props
const { const { lastEditedField, inputCurrency, outputCurrency, inputValue, outputValue, recipient } = this.state
lastEditedField, const estimatedText = `(${t('estimated')})`
inputCurrency,
outputCurrency,
inputValue,
outputValue,
recipient,
} = this.state;
const estimatedText = `(${t("estimated")})`;
const { value: inputBalance, decimals: inputDecimals } = selectors().getBalance(account, inputCurrency); const { value: inputBalance, decimals: inputDecimals } = selectors().getBalance(account, inputCurrency)
const { value: outputBalance, decimals: outputDecimals } = selectors().getBalance(account, outputCurrency); const { value: outputBalance, decimals: outputDecimals } = selectors().getBalance(account, outputCurrency)
const { inputError, outputError, isValid } = this.validate(); const { inputError, outputError, isValid } = this.validate()
return ( return (
<div className="send"> <div className="send">
...@@ -724,17 +757,17 @@ class Send extends Component { ...@@ -724,17 +757,17 @@ class Send extends Component {
</MediaQuery> </MediaQuery>
<div <div
className={classnames('swap__content', { className={classnames('swap__content', {
'swap--inactive': !this.props.isConnected, 'swap--inactive': !this.props.isConnected
})} })}
> >
<NavigationTabs <NavigationTabs
className={classnames('header__navigation', { className={classnames('header__navigation', {
'header--inactive': !this.props.isConnected, 'header--inactive': !this.props.isConnected
})} })}
/> />
<CurrencyInputPanel <CurrencyInputPanel
title={t("input")} title={t('input')}
description={lastEditedField === OUTPUT ? estimatedText : ''} description={lastEditedField === OUTPUT ? estimatedText : ''}
extraText={this.renderBalance(inputCurrency, inputBalance, inputDecimals)} extraText={this.renderBalance(inputCurrency, inputBalance, inputDecimals)}
onCurrencySelected={inputCurrency => this.setState({ inputCurrency }, this.recalcForm)} onCurrencySelected={inputCurrency => this.setState({ inputCurrency }, this.recalcForm)}
...@@ -746,11 +779,16 @@ class Send extends Component { ...@@ -746,11 +779,16 @@ class Send extends Component {
/> />
<OversizedPanel> <OversizedPanel>
<div className="swap__down-arrow-background"> <div className="swap__down-arrow-background">
<img onClick={this.flipInputOutput} className="swap__down-arrow swap__down-arrow--clickable" alt='arrow' src={isValid ? ArrowDownBlue : ArrowDownGrey} /> <img
onClick={this.flipInputOutput}
className="swap__down-arrow swap__down-arrow--clickable"
alt="arrow"
src={isValid ? ArrowDownBlue : ArrowDownGrey}
/>
</div> </div>
</OversizedPanel> </OversizedPanel>
<CurrencyInputPanel <CurrencyInputPanel
title={t("output")} title={t('output')}
description={lastEditedField === INPUT ? estimatedText : ''} description={lastEditedField === INPUT ? estimatedText : ''}
extraText={this.renderBalance(outputCurrency, outputBalance, outputDecimals)} extraText={this.renderBalance(outputCurrency, outputBalance, outputDecimals)}
onCurrencySelected={outputCurrency => this.setState({ outputCurrency }, this.recalcForm)} onCurrencySelected={outputCurrency => this.setState({ outputCurrency }, this.recalcForm)}
...@@ -763,85 +801,93 @@ class Send extends Component { ...@@ -763,85 +801,93 @@ class Send extends Component {
/> />
<OversizedPanel> <OversizedPanel>
<div className="swap__down-arrow-background"> <div className="swap__down-arrow-background">
<img className="swap__down-arrow" src={isValid ? ArrowDownBlue : ArrowDownGrey} alt='arrow' /> <img className="swap__down-arrow" src={isValid ? ArrowDownBlue : ArrowDownGrey} alt="arrow" />
</div> </div>
</OversizedPanel> </OversizedPanel>
<AddressInputPanel <AddressInputPanel
t={this.props.t} t={this.props.t}
value={recipient} value={recipient}
onChange={address => this.setState({recipient: address})} onChange={address => this.setState({ recipient: address })}
/> />
{ this.renderExchangeRate() } {this.renderExchangeRate()}
{ this.renderSummary(inputError, outputError) } {this.renderSummary(inputError, outputError)}
<div className="swap__cta-container"> <div className="swap__cta-container">
<button <button
className={classnames('swap__cta-btn', { className={classnames('swap__cta-btn', {
'swap--inactive': !this.props.isConnected, 'swap--inactive': !this.props.isConnected
})} })}
disabled={!isValid} disabled={!isValid}
onClick={this.onSend} onClick={this.onSend}
> >
{t("send")} {t('send')}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
); )
} }
} }
export default connect( export default connect(
state => ({ state => ({
balances: state.web3connect.balances, balances: state.web3connect.balances,
isConnected: !!state.web3connect.account && state.web3connect.networkId === (process.env.REACT_APP_NETWORK_ID||1), isConnected: !!state.web3connect.account && state.web3connect.networkId === (process.env.REACT_APP_NETWORK_ID || 1),
account: state.web3connect.account, account: state.web3connect.account,
web3: state.web3connect.web3, web3: state.web3connect.web3,
exchangeAddresses: state.addresses.exchangeAddresses, exchangeAddresses: state.addresses.exchangeAddresses
}), }),
dispatch => ({ dispatch => ({
selectors: () => dispatch(selectors()), selectors: () => dispatch(selectors()),
addPendingTx: id => dispatch(addPendingTx(id)), addPendingTx: id => dispatch(addPendingTx(id))
}), })
)(withNamespaces()(Send)); )(withNamespaces()(Send))
const b = text => <span className="swap__highlight-text">{text}</span>; const b = text => <span className="swap__highlight-text">{text}</span>
function calculateEtherTokenOutput({ inputAmount: rawInput, inputReserve: rawReserveIn, outputReserve: rawReserveOut }) { function calculateEtherTokenOutput({
const inputAmount = BN(rawInput); inputAmount: rawInput,
const inputReserve = BN(rawReserveIn); inputReserve: rawReserveIn,
const outputReserve = BN(rawReserveOut); outputReserve: rawReserveOut
}) {
const inputAmount = BN(rawInput)
const inputReserve = BN(rawReserveIn)
const outputReserve = BN(rawReserveOut)
if (inputAmount.isLessThan(BN(10 ** 9))) { if (inputAmount.isLessThan(BN(10 ** 9))) {
console.warn(`inputAmount is only ${inputAmount.toFixed(0)}. Did you forget to multiply by 10 ** decimals?`); console.warn(`inputAmount is only ${inputAmount.toFixed(0)}. Did you forget to multiply by 10 ** decimals?`)
} }
const numerator = inputAmount.multipliedBy(outputReserve).multipliedBy(997); const numerator = inputAmount.multipliedBy(outputReserve).multipliedBy(997)
const denominator = inputReserve.multipliedBy(1000).plus(inputAmount.multipliedBy(997)); const denominator = inputReserve.multipliedBy(1000).plus(inputAmount.multipliedBy(997))
return numerator.dividedBy(denominator); return numerator.dividedBy(denominator)
} }
function calculateEtherTokenInput({ outputAmount: rawOutput, inputReserve: rawReserveIn, outputReserve: rawReserveOut }) { function calculateEtherTokenInput({
const outputAmount = BN(rawOutput); outputAmount: rawOutput,
const inputReserve = BN(rawReserveIn); inputReserve: rawReserveIn,
const outputReserve = BN(rawReserveOut); outputReserve: rawReserveOut
}) {
const outputAmount = BN(rawOutput)
const inputReserve = BN(rawReserveIn)
const outputReserve = BN(rawReserveOut)
if (outputAmount.isLessThan(BN(10 ** 9))) { if (outputAmount.isLessThan(BN(10 ** 9))) {
console.warn(`inputAmount is only ${outputAmount.toFixed(0)}. Did you forget to multiply by 10 ** decimals?`); console.warn(`inputAmount is only ${outputAmount.toFixed(0)}. Did you forget to multiply by 10 ** decimals?`)
} }
const numerator = outputAmount.multipliedBy(inputReserve).multipliedBy(1000); const numerator = outputAmount.multipliedBy(inputReserve).multipliedBy(1000)
const denominator = outputReserve.minus(outputAmount).multipliedBy(997); const denominator = outputReserve.minus(outputAmount).multipliedBy(997)
return (numerator.dividedBy(denominator)).plus(1); return numerator.dividedBy(denominator).plus(1)
} }
function getSendType(inputCurrency, outputCurrency) { function getSendType(inputCurrency, outputCurrency) {
if (!inputCurrency || !outputCurrency) { if (!inputCurrency || !outputCurrency) {
return; return
} }
if (inputCurrency === outputCurrency) { if (inputCurrency === outputCurrency) {
return; return
} }
if (inputCurrency !== 'ETH' && outputCurrency !== 'ETH') { if (inputCurrency !== 'ETH' && outputCurrency !== 'ETH') {
...@@ -849,12 +895,12 @@ function getSendType(inputCurrency, outputCurrency) { ...@@ -849,12 +895,12 @@ function getSendType(inputCurrency, outputCurrency) {
} }
if (inputCurrency === 'ETH') { if (inputCurrency === 'ETH') {
return 'ETH_TO_TOKEN'; return 'ETH_TO_TOKEN'
} }
if (outputCurrency === 'ETH') { if (outputCurrency === 'ETH') {
return 'TOKEN_TO_ETH'; return 'TOKEN_TO_ETH'
} }
return; return
} }
@import "../../variables.scss"; @import '../../variables.scss';
.send { .send {
@extend %col-nowrap; @extend %col-nowrap;
......
import React, { Component } from 'react'; import React, { Component } from 'react'
import { connect } from 'react-redux'; import { connect } from 'react-redux'
import PropTypes from 'prop-types'; import PropTypes from 'prop-types'
import classnames from 'classnames'; import classnames from 'classnames'
import {BigNumber as BN} from "bignumber.js"; import { BigNumber as BN } from 'bignumber.js'
import MediaQuery from 'react-responsive'; import MediaQuery from 'react-responsive'
import ReactGA from 'react-ga'; import ReactGA from 'react-ga'
import { withNamespaces } from 'react-i18next'; import { withNamespaces } from 'react-i18next'
import { selectors, addPendingTx } from '../../ducks/web3connect'; import { selectors, addPendingTx } from '../../ducks/web3connect'
import Header from '../../components/Header'; import Header from '../../components/Header'
import NavigationTabs from '../../components/NavigationTabs'; import NavigationTabs from '../../components/NavigationTabs'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'; import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import ContextualInfo from '../../components/ContextualInfo'; import ContextualInfo from '../../components/ContextualInfo'
import OversizedPanel from '../../components/OversizedPanel'; import OversizedPanel from '../../components/OversizedPanel'
import ArrowDownBlue from '../../assets/images/arrow-down-blue.svg'; import ArrowDownBlue from '../../assets/images/arrow-down-blue.svg'
import ArrowDownGrey from '../../assets/images/arrow-down-grey.svg'; import ArrowDownGrey from '../../assets/images/arrow-down-grey.svg'
import { getBlockDeadline } from '../../helpers/web3-utils'; import { getBlockDeadline } from '../../helpers/web3-utils'
import { retry } from '../../helpers/promise-utils'; import { retry } from '../../helpers/promise-utils'
import EXCHANGE_ABI from '../../abi/exchange'; import EXCHANGE_ABI from '../../abi/exchange'
import "./swap.scss"; import './swap.scss'
const INPUT = 0; const INPUT = 0
const OUTPUT = 1; const OUTPUT = 1
class Swap extends Component { class Swap extends Component {
static propTypes = { static propTypes = {
...@@ -29,8 +29,8 @@ class Swap extends Component { ...@@ -29,8 +29,8 @@ class Swap extends Component {
isConnected: PropTypes.bool.isRequired, isConnected: PropTypes.bool.isRequired,
selectors: PropTypes.func.isRequired, selectors: PropTypes.func.isRequired,
addPendingTx: PropTypes.func.isRequired, addPendingTx: PropTypes.func.isRequired,
web3: PropTypes.object.isRequired, web3: PropTypes.object.isRequired
}; }
state = { state = {
inputValue: '', inputValue: '',
...@@ -38,15 +38,15 @@ class Swap extends Component { ...@@ -38,15 +38,15 @@ class Swap extends Component {
inputCurrency: 'ETH', inputCurrency: 'ETH',
outputCurrency: '', outputCurrency: '',
inputAmountB: '', inputAmountB: '',
lastEditedField: '', lastEditedField: ''
}; }
componentWillMount() { componentWillMount() {
ReactGA.pageview(window.location.pathname + window.location.search); ReactGA.pageview(window.location.pathname + window.location.search)
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
return true; return true
} }
reset() { reset() {
...@@ -54,115 +54,123 @@ class Swap extends Component { ...@@ -54,115 +54,123 @@ class Swap extends Component {
inputValue: '', inputValue: '',
outputValue: '', outputValue: '',
inputAmountB: '', inputAmountB: '',
lastEditedField: '', lastEditedField: ''
}); })
} }
componentWillReceiveProps() { componentWillReceiveProps() {
this.recalcForm(); this.recalcForm()
} }
validate() { validate() {
const { selectors, account } = this.props; const { selectors, account } = this.props
const { const { inputValue, outputValue, inputCurrency, outputCurrency } = this.state
inputValue, outputValue,
inputCurrency, outputCurrency, let inputError = ''
} = this.state; let outputError = ''
let isValid = true
let inputError = ''; let isUnapproved = this.isUnapproved()
let outputError = ''; const inputIsZero = BN(inputValue).isZero()
let isValid = true; const outputIsZero = BN(outputValue).isZero()
let isUnapproved = this.isUnapproved();
const inputIsZero = BN(inputValue).isZero(); if (
const outputIsZero = BN(outputValue).isZero(); !inputValue ||
inputIsZero ||
if (!inputValue || inputIsZero || !outputValue || outputIsZero || !inputCurrency || !outputCurrency || isUnapproved) { !outputValue ||
isValid = false; outputIsZero ||
} !inputCurrency ||
!outputCurrency ||
const { value: inputBalance, decimals: inputDecimals } = selectors().getBalance(account, inputCurrency); isUnapproved
) {
isValid = false
}
const { value: inputBalance, decimals: inputDecimals } = selectors().getBalance(account, inputCurrency)
if (inputBalance.isLessThan(BN(inputValue * 10 ** inputDecimals))) { if (inputBalance.isLessThan(BN(inputValue * 10 ** inputDecimals))) {
inputError = this.props.t("insufficientBalance"); inputError = this.props.t('insufficientBalance')
} }
if (inputValue === 'N/A') { if (inputValue === 'N/A') {
inputError = this.props.t("inputNotValid"); inputError = this.props.t('inputNotValid')
} }
return { return {
inputError, inputError,
outputError, outputError,
isValid: isValid && !inputError && !outputError, isValid: isValid && !inputError && !outputError
}; }
} }
flipInputOutput = () => { flipInputOutput = () => {
const { state } = this; const { state } = this
this.setState({ this.setState(
{
inputValue: state.outputValue, inputValue: state.outputValue,
outputValue: state.inputValue, outputValue: state.inputValue,
inputCurrency: state.outputCurrency, inputCurrency: state.outputCurrency,
outputCurrency: state.inputCurrency, outputCurrency: state.inputCurrency,
lastEditedField: state.lastEditedField === INPUT ? OUTPUT : INPUT lastEditedField: state.lastEditedField === INPUT ? OUTPUT : INPUT
}, () => this.recalcForm()); },
() => this.recalcForm()
)
} }
isUnapproved() { isUnapproved() {
const { account, exchangeAddresses, selectors } = this.props; const { account, exchangeAddresses, selectors } = this.props
const { inputCurrency, inputValue } = this.state; const { inputCurrency, inputValue } = this.state
if (!inputCurrency || inputCurrency === 'ETH') { if (!inputCurrency || inputCurrency === 'ETH') {
return false; return false
} }
const { value: allowance, label, decimals } = selectors().getApprovals( const { value: allowance, label, decimals } = selectors().getApprovals(
inputCurrency, inputCurrency,
account, account,
exchangeAddresses.fromToken[inputCurrency] exchangeAddresses.fromToken[inputCurrency]
); )
if (label && allowance.isLessThan(BN(inputValue * 10 ** decimals || 0))) { if (label && allowance.isLessThan(BN(inputValue * 10 ** decimals || 0))) {
return true; return true
} }
return false; return false
} }
recalcForm() { recalcForm() {
const { inputCurrency, outputCurrency, lastEditedField } = this.state; const { inputCurrency, outputCurrency, lastEditedField } = this.state
if (!inputCurrency || !outputCurrency) { if (!inputCurrency || !outputCurrency) {
return; return
} }
const editedValue = lastEditedField === INPUT ? this.state.inputValue : this.state.outputValue; const editedValue = lastEditedField === INPUT ? this.state.inputValue : this.state.outputValue
if (BN(editedValue).isZero()) { if (BN(editedValue).isZero()) {
return; return
} }
if (inputCurrency === outputCurrency) { if (inputCurrency === outputCurrency) {
this.setState({ this.setState({
inputValue: '', inputValue: '',
outputValue: '', outputValue: ''
}); })
return; return
} }
if (inputCurrency !== 'ETH' && outputCurrency !== 'ETH') { if (inputCurrency !== 'ETH' && outputCurrency !== 'ETH') {
this.recalcTokenTokenForm(); this.recalcTokenTokenForm()
return; return
} }
this.recalcEthTokenForm(); this.recalcEthTokenForm()
} }
recalcTokenTokenForm = () => { recalcTokenTokenForm = () => {
const { const {
exchangeAddresses: { fromToken }, exchangeAddresses: { fromToken },
selectors, selectors
} = this.props; } = this.props
const { const {
inputValue: oldInputValue, inputValue: oldInputValue,
...@@ -171,109 +179,109 @@ class Swap extends Component { ...@@ -171,109 +179,109 @@ class Swap extends Component {
outputCurrency, outputCurrency,
lastEditedField, lastEditedField,
exchangeRate: oldExchangeRate, exchangeRate: oldExchangeRate,
inputAmountB: oldInputAmountB, inputAmountB: oldInputAmountB
} = this.state; } = this.state
const exchangeAddressA = fromToken[inputCurrency]; const exchangeAddressA = fromToken[inputCurrency]
const exchangeAddressB = fromToken[outputCurrency]; const exchangeAddressB = fromToken[outputCurrency]
const { value: inputReserveA, decimals: inputDecimalsA } = selectors().getBalance(exchangeAddressA, inputCurrency); const { value: inputReserveA, decimals: inputDecimalsA } = selectors().getBalance(exchangeAddressA, inputCurrency)
const { value: outputReserveA }= selectors().getBalance(exchangeAddressA, 'ETH'); const { value: outputReserveA } = selectors().getBalance(exchangeAddressA, 'ETH')
const { value: inputReserveB } = selectors().getBalance(exchangeAddressB, 'ETH'); const { value: inputReserveB } = selectors().getBalance(exchangeAddressB, 'ETH')
const { value: outputReserveB, decimals: outputDecimalsB }= selectors().getBalance(exchangeAddressB, outputCurrency); const { value: outputReserveB, decimals: outputDecimalsB } = selectors().getBalance(
exchangeAddressB,
outputCurrency
)
if (lastEditedField === INPUT) { if (lastEditedField === INPUT) {
if (!oldInputValue) { if (!oldInputValue) {
return this.setState({ return this.setState({
outputValue: '', outputValue: '',
exchangeRate: BN(0), exchangeRate: BN(0)
}); })
} }
const inputAmountA = BN(oldInputValue).multipliedBy(10 ** inputDecimalsA); const inputAmountA = BN(oldInputValue).multipliedBy(10 ** inputDecimalsA)
const outputAmountA = calculateEtherTokenOutput({ const outputAmountA = calculateEtherTokenOutput({
inputAmount: inputAmountA, inputAmount: inputAmountA,
inputReserve: inputReserveA, inputReserve: inputReserveA,
outputReserve: outputReserveA, outputReserve: outputReserveA
}); })
// Redundant Variable for readability of the formala // Redundant Variable for readability of the formala
// OutputAmount from the first swap becomes InputAmount of the second swap // OutputAmount from the first swap becomes InputAmount of the second swap
const inputAmountB = outputAmountA; const inputAmountB = outputAmountA
const outputAmountB = calculateEtherTokenOutput({ const outputAmountB = calculateEtherTokenOutput({
inputAmount: inputAmountB, inputAmount: inputAmountB,
inputReserve: inputReserveB, inputReserve: inputReserveB,
outputReserve: outputReserveB, outputReserve: outputReserveB
}); })
const outputValue = outputAmountB.dividedBy(BN(10 ** outputDecimalsB)).toFixed(7); const outputValue = outputAmountB.dividedBy(BN(10 ** outputDecimalsB)).toFixed(7)
const exchangeRate = BN(outputValue).dividedBy(BN(oldInputValue)); const exchangeRate = BN(outputValue).dividedBy(BN(oldInputValue))
const appendState = {}; const appendState = {}
if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) { if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) {
appendState.exchangeRate = exchangeRate; appendState.exchangeRate = exchangeRate
} }
if (outputValue !== oldOutputValue) { if (outputValue !== oldOutputValue) {
appendState.outputValue = outputValue; appendState.outputValue = outputValue
} }
this.setState(appendState); this.setState(appendState)
} }
if (lastEditedField === OUTPUT) { if (lastEditedField === OUTPUT) {
if (!oldOutputValue) { if (!oldOutputValue) {
return this.setState({ return this.setState({
inputValue: '', inputValue: '',
exchangeRate: BN(0), exchangeRate: BN(0)
}); })
} }
const outputAmountB = BN(oldOutputValue).multipliedBy(10 ** outputDecimalsB); const outputAmountB = BN(oldOutputValue).multipliedBy(10 ** outputDecimalsB)
const inputAmountB = calculateEtherTokenInput({ const inputAmountB = calculateEtherTokenInput({
outputAmount: outputAmountB, outputAmount: outputAmountB,
inputReserve: inputReserveB, inputReserve: inputReserveB,
outputReserve: outputReserveB, outputReserve: outputReserveB
}); })
// Redundant Variable for readability of the formala // Redundant Variable for readability of the formala
// InputAmount from the first swap becomes OutputAmount of the second swap // InputAmount from the first swap becomes OutputAmount of the second swap
const outputAmountA = inputAmountB; const outputAmountA = inputAmountB
const inputAmountA = calculateEtherTokenInput({ const inputAmountA = calculateEtherTokenInput({
outputAmount: outputAmountA, outputAmount: outputAmountA,
inputReserve: inputReserveA, inputReserve: inputReserveA,
outputReserve: outputReserveA, outputReserve: outputReserveA
}); })
const inputValue = inputAmountA.isNegative() const inputValue = inputAmountA.isNegative() ? 'N/A' : inputAmountA.dividedBy(BN(10 ** inputDecimalsA)).toFixed(7)
? 'N/A' const exchangeRate = BN(oldOutputValue).dividedBy(BN(inputValue))
: inputAmountA.dividedBy(BN(10 ** inputDecimalsA)).toFixed(7);
const exchangeRate = BN(oldOutputValue).dividedBy(BN(inputValue));
const appendState = {}; const appendState = {}
if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) { if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) {
appendState.exchangeRate = exchangeRate; appendState.exchangeRate = exchangeRate
} }
if (inputValue !== oldInputValue) { if (inputValue !== oldInputValue) {
appendState.inputValue = inputValue; appendState.inputValue = inputValue
} }
if (!inputAmountB.isEqualTo(BN(oldInputAmountB))) { if (!inputAmountB.isEqualTo(BN(oldInputAmountB))) {
appendState.inputAmountB = inputAmountB; appendState.inputAmountB = inputAmountB
} }
this.setState(appendState); this.setState(appendState)
}
} }
};
recalcEthTokenForm = () => { recalcEthTokenForm = () => {
const { const {
exchangeAddresses: { fromToken }, exchangeAddresses: { fromToken },
selectors, selectors
} = this.props; } = this.props
const { const {
inputValue: oldInputValue, inputValue: oldInputValue,
...@@ -281,83 +289,87 @@ class Swap extends Component { ...@@ -281,83 +289,87 @@ class Swap extends Component {
inputCurrency, inputCurrency,
outputCurrency, outputCurrency,
lastEditedField, lastEditedField,
exchangeRate: oldExchangeRate, exchangeRate: oldExchangeRate
} = this.state; } = this.state
const tokenAddress = [inputCurrency, outputCurrency].filter(currency => currency !== 'ETH')[0]; const tokenAddress = [inputCurrency, outputCurrency].filter(currency => currency !== 'ETH')[0]
const exchangeAddress = fromToken[tokenAddress]; const exchangeAddress = fromToken[tokenAddress]
if (!exchangeAddress) { if (!exchangeAddress) {
return; return
} }
const { value: inputReserve, decimals: inputDecimals } = selectors().getBalance(exchangeAddress, inputCurrency); const { value: inputReserve, decimals: inputDecimals } = selectors().getBalance(exchangeAddress, inputCurrency)
const { value: outputReserve, decimals: outputDecimals }= selectors().getBalance(exchangeAddress, outputCurrency); const { value: outputReserve, decimals: outputDecimals } = selectors().getBalance(exchangeAddress, outputCurrency)
if (lastEditedField === INPUT) { if (lastEditedField === INPUT) {
if (!oldInputValue) { if (!oldInputValue) {
return this.setState({ return this.setState({
outputValue: '', outputValue: '',
exchangeRate: BN(0), exchangeRate: BN(0)
}); })
} }
const inputAmount = BN(oldInputValue).multipliedBy(10 ** inputDecimals); const inputAmount = BN(oldInputValue).multipliedBy(10 ** inputDecimals)
const outputAmount = calculateEtherTokenOutput({ inputAmount, inputReserve, outputReserve }); const outputAmount = calculateEtherTokenOutput({ inputAmount, inputReserve, outputReserve })
const outputValue = outputAmount.dividedBy(BN(10 ** outputDecimals)).toFixed(7); const outputValue = outputAmount.dividedBy(BN(10 ** outputDecimals)).toFixed(7)
const exchangeRate = BN(outputValue).dividedBy(BN(oldInputValue)); const exchangeRate = BN(outputValue).dividedBy(BN(oldInputValue))
const appendState = {}; const appendState = {}
if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) { if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) {
appendState.exchangeRate = exchangeRate; appendState.exchangeRate = exchangeRate
} }
if (outputValue !== oldOutputValue) { if (outputValue !== oldOutputValue) {
appendState.outputValue = outputValue; appendState.outputValue = outputValue
} }
this.setState(appendState); this.setState(appendState)
} else if (lastEditedField === OUTPUT) { } else if (lastEditedField === OUTPUT) {
if (!oldOutputValue) { if (!oldOutputValue) {
return this.setState({ return this.setState({
inputValue: '', inputValue: '',
exchangeRate: BN(0), exchangeRate: BN(0)
}); })
} }
const outputAmount = BN(oldOutputValue).multipliedBy(10 ** outputDecimals); const outputAmount = BN(oldOutputValue).multipliedBy(10 ** outputDecimals)
const inputAmount = calculateEtherTokenInput({ outputAmount, inputReserve, outputReserve }); const inputAmount = calculateEtherTokenInput({ outputAmount, inputReserve, outputReserve })
const inputValue = inputAmount.isNegative() const inputValue = inputAmount.isNegative() ? 'N/A' : inputAmount.dividedBy(BN(10 ** inputDecimals)).toFixed(7)
? 'N/A' const exchangeRate = BN(oldOutputValue).dividedBy(BN(inputValue))
: inputAmount.dividedBy(BN(10 ** inputDecimals)).toFixed(7);
const exchangeRate = BN(oldOutputValue).dividedBy(BN(inputValue));
const appendState = {}; const appendState = {}
if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) { if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) {
appendState.exchangeRate = exchangeRate; appendState.exchangeRate = exchangeRate
} }
if (inputValue !== oldInputValue) { if (inputValue !== oldInputValue) {
appendState.inputValue = inputValue; appendState.inputValue = inputValue
} }
this.setState(appendState); this.setState(appendState)
}
} }
};
updateInput = amount => { updateInput = amount => {
this.setState({ this.setState(
{
inputValue: amount, inputValue: amount,
lastEditedField: INPUT, lastEditedField: INPUT
}, this.recalcForm); },
}; this.recalcForm
)
}
updateOutput = amount => { updateOutput = amount => {
this.setState({ this.setState(
{
outputValue: amount, outputValue: amount,
lastEditedField: OUTPUT, lastEditedField: OUTPUT
}, this.recalcForm); },
}; this.recalcForm
)
}
onSwap = async () => { onSwap = async () => {
const { const {
...@@ -365,89 +377,97 @@ class Swap extends Component { ...@@ -365,89 +377,97 @@ class Swap extends Component {
account, account,
web3, web3,
selectors, selectors,
addPendingTx, addPendingTx
} = this.props; } = this.props
const { const { inputValue, outputValue, inputCurrency, outputCurrency, inputAmountB, lastEditedField } = this.state
inputValue, const ALLOWED_SLIPPAGE = 0.025
outputValue, const TOKEN_ALLOWED_SLIPPAGE = 0.04
inputCurrency,
outputCurrency, const type = getSwapType(inputCurrency, outputCurrency)
inputAmountB, const { decimals: inputDecimals } = selectors().getBalance(account, inputCurrency)
lastEditedField, const { decimals: outputDecimals } = selectors().getBalance(account, outputCurrency)
} = this.state; let deadline
const ALLOWED_SLIPPAGE = 0.025;
const TOKEN_ALLOWED_SLIPPAGE = 0.04;
const type = getSwapType(inputCurrency, outputCurrency);
const { decimals: inputDecimals } = selectors().getBalance(account, inputCurrency);
const { decimals: outputDecimals } = selectors().getBalance(account, outputCurrency);
let deadline;
try { try {
deadline = await retry(() => getBlockDeadline(web3, 600)); deadline = await retry(() => getBlockDeadline(web3, 600))
} catch(e) { } catch (e) {
// TODO: Handle error. // TODO: Handle error.
return; return
} }
if (lastEditedField === INPUT) { if (lastEditedField === INPUT) {
// swap input // swap input
ReactGA.event({ ReactGA.event({
category: type, category: type,
action: 'SwapInput', action: 'SwapInput'
}); })
switch(type) { switch (type) {
case 'ETH_TO_TOKEN': case 'ETH_TO_TOKEN':
// let exchange = new web3.eth.Contract(EXCHANGE_ABI, fromToken[outputCurrency]); // let exchange = new web3.eth.Contract(EXCHANGE_ABI, fromToken[outputCurrency]);
new web3.eth.Contract(EXCHANGE_ABI, fromToken[outputCurrency]) new web3.eth.Contract(EXCHANGE_ABI, fromToken[outputCurrency]).methods
.methods
.ethToTokenSwapInput( .ethToTokenSwapInput(
BN(outputValue).multipliedBy(10 ** outputDecimals).multipliedBy(1 - ALLOWED_SLIPPAGE).toFixed(0), BN(outputValue)
deadline, .multipliedBy(10 ** outputDecimals)
.multipliedBy(1 - ALLOWED_SLIPPAGE)
.toFixed(0),
deadline
) )
.send({ .send(
{
from: account, from: account,
value: BN(inputValue).multipliedBy(10 ** 18).toFixed(0), value: BN(inputValue)
}, (err, data) => { .multipliedBy(10 ** 18)
.toFixed(0)
},
(err, data) => {
if (!err) { if (!err) {
addPendingTx(data); addPendingTx(data)
this.reset(); this.reset()
}
} }
}); )
break; break
case 'TOKEN_TO_ETH': case 'TOKEN_TO_ETH':
new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency]) new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency]).methods
.methods
.tokenToEthSwapInput( .tokenToEthSwapInput(
BN(inputValue).multipliedBy(10 ** inputDecimals).toFixed(0), BN(inputValue)
BN(outputValue).multipliedBy(10 ** outputDecimals).multipliedBy(1 - ALLOWED_SLIPPAGE).toFixed(0), .multipliedBy(10 ** inputDecimals)
deadline, .toFixed(0),
BN(outputValue)
.multipliedBy(10 ** outputDecimals)
.multipliedBy(1 - ALLOWED_SLIPPAGE)
.toFixed(0),
deadline
) )
.send({ from: account }, (err, data) => { .send({ from: account }, (err, data) => {
if (!err) { if (!err) {
addPendingTx(data); addPendingTx(data)
this.reset(); this.reset()
} }
}); })
break; break
case 'TOKEN_TO_TOKEN': case 'TOKEN_TO_TOKEN':
new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency]) new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency]).methods
.methods
.tokenToTokenSwapInput( .tokenToTokenSwapInput(
BN(inputValue).multipliedBy(10 ** inputDecimals).toFixed(0), BN(inputValue)
BN(outputValue).multipliedBy(10 ** outputDecimals).multipliedBy(1 - TOKEN_ALLOWED_SLIPPAGE).toFixed(0), .multipliedBy(10 ** inputDecimals)
.toFixed(0),
BN(outputValue)
.multipliedBy(10 ** outputDecimals)
.multipliedBy(1 - TOKEN_ALLOWED_SLIPPAGE)
.toFixed(0),
'1', '1',
deadline, deadline,
outputCurrency, outputCurrency
) )
.send({ from: account }, (err, data) => { .send({ from: account }, (err, data) => {
if (!err) { if (!err) {
addPendingTx(data); addPendingTx(data)
this.reset(); this.reset()
} }
}); })
break; break
default: default:
break; break
} }
} }
...@@ -455,169 +475,187 @@ class Swap extends Component { ...@@ -455,169 +475,187 @@ class Swap extends Component {
// swap output // swap output
ReactGA.event({ ReactGA.event({
category: type, category: type,
action: 'SwapOutput', action: 'SwapOutput'
}); })
switch (type) { switch (type) {
case 'ETH_TO_TOKEN': case 'ETH_TO_TOKEN':
new web3.eth.Contract(EXCHANGE_ABI, fromToken[outputCurrency]) new web3.eth.Contract(EXCHANGE_ABI, fromToken[outputCurrency]).methods
.methods
.ethToTokenSwapOutput( .ethToTokenSwapOutput(
BN(outputValue).multipliedBy(10 ** outputDecimals).toFixed(0), BN(outputValue)
deadline, .multipliedBy(10 ** outputDecimals)
.toFixed(0),
deadline
) )
.send({ .send(
{
from: account, from: account,
value: BN(inputValue).multipliedBy(10 ** inputDecimals).multipliedBy(1 + ALLOWED_SLIPPAGE).toFixed(0), value: BN(inputValue)
}, (err, data) => { .multipliedBy(10 ** inputDecimals)
.multipliedBy(1 + ALLOWED_SLIPPAGE)
.toFixed(0)
},
(err, data) => {
if (!err) { if (!err) {
addPendingTx(data); addPendingTx(data)
this.reset(); this.reset()
} }
}); }
break; )
break
case 'TOKEN_TO_ETH': case 'TOKEN_TO_ETH':
new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency]) new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency]).methods
.methods
.tokenToEthSwapOutput( .tokenToEthSwapOutput(
BN(outputValue).multipliedBy(10 ** outputDecimals).toFixed(0), BN(outputValue)
BN(inputValue).multipliedBy(10 ** inputDecimals).multipliedBy(1 + ALLOWED_SLIPPAGE).toFixed(0), .multipliedBy(10 ** outputDecimals)
deadline, .toFixed(0),
BN(inputValue)
.multipliedBy(10 ** inputDecimals)
.multipliedBy(1 + ALLOWED_SLIPPAGE)
.toFixed(0),
deadline
) )
.send({ from: account }, (err, data) => { .send({ from: account }, (err, data) => {
if (!err) { if (!err) {
addPendingTx(data); addPendingTx(data)
this.reset(); this.reset()
} }
}); })
break; break
case 'TOKEN_TO_TOKEN': case 'TOKEN_TO_TOKEN':
if (!inputAmountB) { if (!inputAmountB) {
return; return
} }
new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency]) new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency]).methods
.methods
.tokenToTokenSwapOutput( .tokenToTokenSwapOutput(
BN(outputValue).multipliedBy(10 ** outputDecimals).toFixed(0), BN(outputValue)
BN(inputValue).multipliedBy(10 ** inputDecimals).multipliedBy(1 + TOKEN_ALLOWED_SLIPPAGE).toFixed(0), .multipliedBy(10 ** outputDecimals)
.toFixed(0),
BN(inputValue)
.multipliedBy(10 ** inputDecimals)
.multipliedBy(1 + TOKEN_ALLOWED_SLIPPAGE)
.toFixed(0),
inputAmountB.multipliedBy(1.2).toFixed(0), inputAmountB.multipliedBy(1.2).toFixed(0),
deadline, deadline,
outputCurrency, outputCurrency
) )
.send({ from: account }, (err, data) => { .send({ from: account }, (err, data) => {
if (!err) { if (!err) {
addPendingTx(data); addPendingTx(data)
this.reset(); this.reset()
} }
}); })
break; break
default: default:
break; break
}
} }
} }
};
renderSummary(inputError, outputError) { renderSummary(inputError, outputError) {
const { const { inputValue, inputCurrency, outputValue, outputCurrency } = this.state
inputValue, const t = this.props.t
inputCurrency,
outputValue,
outputCurrency,
} = this.state;
const t = this.props.t;
const inputIsZero = BN(inputValue).isZero(); const inputIsZero = BN(inputValue).isZero()
const outputIsZero = BN(outputValue).isZero(); const outputIsZero = BN(outputValue).isZero()
let contextualInfo = ''; let contextualInfo = ''
let isError = false; let isError = false
if (!inputCurrency || !outputCurrency) { if (!inputCurrency || !outputCurrency) {
contextualInfo = t("selectTokenCont"); contextualInfo = t('selectTokenCont')
} }
if (!inputValue || !outputValue) { if (!inputValue || !outputValue) {
contextualInfo = t("enterValueCont"); contextualInfo = t('enterValueCont')
} }
if (inputError || outputError) { if (inputError || outputError) {
contextualInfo = inputError || outputError; contextualInfo = inputError || outputError
isError = true; isError = true
} }
if (inputIsZero || outputIsZero) { if (inputIsZero || outputIsZero) {
contextualInfo = t("noLiquidity"); contextualInfo = t('noLiquidity')
} }
if (this.isUnapproved()) { if (this.isUnapproved()) {
contextualInfo = t("unlockTokenCont"); contextualInfo = t('unlockTokenCont')
} }
return ( return (
<ContextualInfo <ContextualInfo
openDetailsText={t("transactionDetails")} openDetailsText={t('transactionDetails')}
closeDetailsText={t("hideDetails")} closeDetailsText={t('hideDetails')}
contextualInfo={contextualInfo} contextualInfo={contextualInfo}
isError={isError} isError={isError}
renderTransactionDetails={this.renderTransactionDetails} renderTransactionDetails={this.renderTransactionDetails}
/> />
); )
} }
renderTransactionDetails = () => { renderTransactionDetails = () => {
const { const { inputValue, inputCurrency, outputValue, outputCurrency, lastEditedField } = this.state
inputValue, const { t, selectors, account } = this.props
inputCurrency,
outputValue,
outputCurrency,
lastEditedField,
} = this.state;
const { t, selectors, account } = this.props;
ReactGA.event({ ReactGA.event({
category: 'TransactionDetail', category: 'TransactionDetail',
action: 'Open', action: 'Open'
}); })
const ALLOWED_SLIPPAGE = 0.025; const ALLOWED_SLIPPAGE = 0.025
const TOKEN_ALLOWED_SLIPPAGE = 0.04; const TOKEN_ALLOWED_SLIPPAGE = 0.04
const type = getSwapType(inputCurrency, outputCurrency); const type = getSwapType(inputCurrency, outputCurrency)
const { label: inputLabel } = selectors().getBalance(account, inputCurrency); const { label: inputLabel } = selectors().getBalance(account, inputCurrency)
const { label: outputLabel } = selectors().getBalance(account, outputCurrency); const { label: outputLabel } = selectors().getBalance(account, outputCurrency)
// const label = lastEditedField === INPUT ? outputLabel : inputLabel; // const label = lastEditedField === INPUT ? outputLabel : inputLabel;
let minOutput; let minOutput
let maxInput; let maxInput
if (lastEditedField === INPUT) { if (lastEditedField === INPUT) {
switch(type) { switch (type) {
case 'ETH_TO_TOKEN': case 'ETH_TO_TOKEN':
minOutput = BN(outputValue).multipliedBy(1 - ALLOWED_SLIPPAGE).toFixed(7).trim(); minOutput = BN(outputValue)
break; .multipliedBy(1 - ALLOWED_SLIPPAGE)
.toFixed(7)
.trim()
break
case 'TOKEN_TO_ETH': case 'TOKEN_TO_ETH':
minOutput = BN(outputValue).multipliedBy(1 - ALLOWED_SLIPPAGE).toFixed(7); minOutput = BN(outputValue)
break; .multipliedBy(1 - ALLOWED_SLIPPAGE)
.toFixed(7)
break
case 'TOKEN_TO_TOKEN': case 'TOKEN_TO_TOKEN':
minOutput = BN(outputValue).multipliedBy(1 - TOKEN_ALLOWED_SLIPPAGE).toFixed(7); minOutput = BN(outputValue)
break; .multipliedBy(1 - TOKEN_ALLOWED_SLIPPAGE)
.toFixed(7)
break
default: default:
break; break
} }
} }
if (lastEditedField === OUTPUT) { if (lastEditedField === OUTPUT) {
switch (type) { switch (type) {
case 'ETH_TO_TOKEN': case 'ETH_TO_TOKEN':
maxInput = BN(inputValue).multipliedBy(1 + ALLOWED_SLIPPAGE).toFixed(7).trim(); maxInput = BN(inputValue)
break; .multipliedBy(1 + ALLOWED_SLIPPAGE)
.toFixed(7)
.trim()
break
case 'TOKEN_TO_ETH': case 'TOKEN_TO_ETH':
maxInput = BN(inputValue).multipliedBy(1 + ALLOWED_SLIPPAGE).toFixed(7); maxInput = BN(inputValue)
break; .multipliedBy(1 + ALLOWED_SLIPPAGE)
.toFixed(7)
break
case 'TOKEN_TO_TOKEN': case 'TOKEN_TO_TOKEN':
maxInput = BN(inputValue).multipliedBy(1 + TOKEN_ALLOWED_SLIPPAGE).toFixed(7); maxInput = BN(inputValue)
break; .multipliedBy(1 + TOKEN_ALLOWED_SLIPPAGE)
.toFixed(7)
break
default: default:
break; break
} }
} }
...@@ -625,81 +663,72 @@ class Swap extends Component { ...@@ -625,81 +663,72 @@ class Swap extends Component {
return ( return (
<div> <div>
<div> <div>
{t("youAreSelling")} {b(`${+inputValue} ${inputLabel}`)} {t("orTransFail")} {t('youAreSelling')} {b(`${+inputValue} ${inputLabel}`)} {t('orTransFail')}
</div> </div>
<div className="send__last-summary-text"> <div className="send__last-summary-text">
{t("youWillReceive")} {b(`${+minOutput} ${outputLabel}`)} {t("orTransFail")} {t('youWillReceive')} {b(`${+minOutput} ${outputLabel}`)} {t('orTransFail')}
</div> </div>
</div> </div>
); )
} else { } else {
return ( return (
<div> <div>
<div> <div>
{t("youAreBuying")} {b(`${+outputValue} ${outputLabel}`)}. {t('youAreBuying')} {b(`${+outputValue} ${outputLabel}`)}.
</div> </div>
<div className="send__last-summary-text"> <div className="send__last-summary-text">
{t("itWillCost")} {b(`${+maxInput} ${inputLabel}`)} {t("orTransFail")} {t('itWillCost')} {b(`${+maxInput} ${inputLabel}`)} {t('orTransFail')}
</div> </div>
</div> </div>
); )
} }
} }
renderExchangeRate() { renderExchangeRate() {
const { t, account, selectors } = this.props; const { t, account, selectors } = this.props
const { exchangeRate, inputCurrency, outputCurrency } = this.state; const { exchangeRate, inputCurrency, outputCurrency } = this.state
const { label: inputLabel } = selectors().getBalance(account, inputCurrency); const { label: inputLabel } = selectors().getBalance(account, inputCurrency)
const { label: outputLabel } = selectors().getBalance(account, outputCurrency); const { label: outputLabel } = selectors().getBalance(account, outputCurrency)
if (!exchangeRate || exchangeRate.isNaN() || !inputCurrency || !outputCurrency) { if (!exchangeRate || exchangeRate.isNaN() || !inputCurrency || !outputCurrency) {
return ( return (
<OversizedPanel hideBottom> <OversizedPanel hideBottom>
<div className="swap__exchange-rate-wrapper"> <div className="swap__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t("exchangeRate")}</span> <span className="swap__exchange-rate">{t('exchangeRate')}</span>
<span> - </span> <span> - </span>
</div> </div>
</OversizedPanel> </OversizedPanel>
); )
} }
return ( return (
<OversizedPanel hideBottom> <OversizedPanel hideBottom>
<div className="swap__exchange-rate-wrapper"> <div className="swap__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t("exchangeRate")}</span> <span className="swap__exchange-rate">{t('exchangeRate')}</span>
<span> <span>{`1 ${inputLabel} = ${exchangeRate.toFixed(7)} ${outputLabel}`}</span>
{`1 ${inputLabel} = ${exchangeRate.toFixed(7)} ${outputLabel}`}
</span>
</div> </div>
</OversizedPanel> </OversizedPanel>
); )
} }
renderBalance(currency, balance, decimals) { renderBalance(currency, balance, decimals) {
if (!currency || decimals === 0) { if (!currency || decimals === 0) {
return ''; return ''
} }
const balanceInput = balance.dividedBy(BN(10 ** decimals)).toFixed(4) const balanceInput = balance.dividedBy(BN(10 ** decimals)).toFixed(4)
return this.props.t("balance", { balanceInput }) return this.props.t('balance', { balanceInput })
} }
render() { render() {
const { t, selectors, account } = this.props; const { t, selectors, account } = this.props
const { const { lastEditedField, inputCurrency, outputCurrency, inputValue, outputValue } = this.state
lastEditedField, const estimatedText = `(${t('estimated')})`
inputCurrency,
outputCurrency,
inputValue,
outputValue,
} = this.state;
const estimatedText = `(${t("estimated")})`;
const { value: inputBalance, decimals: inputDecimals } = selectors().getBalance(account, inputCurrency); const { value: inputBalance, decimals: inputDecimals } = selectors().getBalance(account, inputCurrency)
const { value: outputBalance, decimals: outputDecimals } = selectors().getBalance(account, outputCurrency); const { value: outputBalance, decimals: outputDecimals } = selectors().getBalance(account, outputCurrency)
const { inputError, outputError, isValid } = this.validate();
const { inputError, outputError, isValid } = this.validate()
return ( return (
<div className="swap"> <div className="swap">
...@@ -708,17 +737,17 @@ class Swap extends Component { ...@@ -708,17 +737,17 @@ class Swap extends Component {
</MediaQuery> </MediaQuery>
<div <div
className={classnames('swap__content', { className={classnames('swap__content', {
'swap--inactive': !this.props.isConnected, 'swap--inactive': !this.props.isConnected
})} })}
> >
<NavigationTabs <NavigationTabs
className={classnames('header__navigation', { className={classnames('header__navigation', {
'header--inactive': !this.props.isConnected, 'header--inactive': !this.props.isConnected
})} })}
/> />
<CurrencyInputPanel <CurrencyInputPanel
title={t("input")} title={t('input')}
description={lastEditedField === OUTPUT ? estimatedText : ''} description={lastEditedField === OUTPUT ? estimatedText : ''}
extraText={this.renderBalance(inputCurrency, inputBalance, inputDecimals)} extraText={this.renderBalance(inputCurrency, inputBalance, inputDecimals)}
onCurrencySelected={inputCurrency => this.setState({ inputCurrency }, this.recalcForm)} onCurrencySelected={inputCurrency => this.setState({ inputCurrency }, this.recalcForm)}
...@@ -730,11 +759,16 @@ class Swap extends Component { ...@@ -730,11 +759,16 @@ class Swap extends Component {
/> />
<OversizedPanel> <OversizedPanel>
<div className="swap__down-arrow-background"> <div className="swap__down-arrow-background">
<img onClick={this.flipInputOutput} className="swap__down-arrow swap__down-arrow--clickable" alt='swap' src={isValid ? ArrowDownBlue : ArrowDownGrey} /> <img
onClick={this.flipInputOutput}
className="swap__down-arrow swap__down-arrow--clickable"
alt="swap"
src={isValid ? ArrowDownBlue : ArrowDownGrey}
/>
</div> </div>
</OversizedPanel> </OversizedPanel>
<CurrencyInputPanel <CurrencyInputPanel
title={t("output")} title={t('output')}
description={lastEditedField === INPUT ? estimatedText : ''} description={lastEditedField === INPUT ? estimatedText : ''}
extraText={this.renderBalance(outputCurrency, outputBalance, outputDecimals)} extraText={this.renderBalance(outputCurrency, outputBalance, outputDecimals)}
onCurrencySelected={outputCurrency => this.setState({ outputCurrency }, this.recalcForm)} onCurrencySelected={outputCurrency => this.setState({ outputCurrency }, this.recalcForm)}
...@@ -745,77 +779,85 @@ class Swap extends Component { ...@@ -745,77 +779,85 @@ class Swap extends Component {
errorMessage={outputError} errorMessage={outputError}
disableUnlock disableUnlock
/> />
{ this.renderExchangeRate() } {this.renderExchangeRate()}
{ this.renderSummary(inputError, outputError) } {this.renderSummary(inputError, outputError)}
<div className="swap__cta-container"> <div className="swap__cta-container">
<button <button
className={classnames('swap__cta-btn', { className={classnames('swap__cta-btn', {
'swap--inactive': !this.props.isConnected, 'swap--inactive': !this.props.isConnected
})} })}
disabled={!isValid} disabled={!isValid}
onClick={this.onSwap} onClick={this.onSwap}
> >
{t("swap")} {t('swap')}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
); )
} }
} }
export default connect( export default connect(
state => ({ state => ({
balances: state.web3connect.balances, balances: state.web3connect.balances,
isConnected: !!state.web3connect.account && state.web3connect.networkId === (process.env.REACT_APP_NETWORK_ID||1), isConnected: !!state.web3connect.account && state.web3connect.networkId === (process.env.REACT_APP_NETWORK_ID || 1),
account: state.web3connect.account, account: state.web3connect.account,
web3: state.web3connect.web3, web3: state.web3connect.web3,
exchangeAddresses: state.addresses.exchangeAddresses, exchangeAddresses: state.addresses.exchangeAddresses
}), }),
dispatch => ({ dispatch => ({
selectors: () => dispatch(selectors()), selectors: () => dispatch(selectors()),
addPendingTx: id => dispatch(addPendingTx(id)), addPendingTx: id => dispatch(addPendingTx(id))
}), })
)(withNamespaces()(Swap)); )(withNamespaces()(Swap))
const b = text => <span className="swap__highlight-text">{text}</span>; const b = text => <span className="swap__highlight-text">{text}</span>
function calculateEtherTokenOutput({ inputAmount: rawInput, inputReserve: rawReserveIn, outputReserve: rawReserveOut }) { function calculateEtherTokenOutput({
const inputAmount = BN(rawInput); inputAmount: rawInput,
const inputReserve = BN(rawReserveIn); inputReserve: rawReserveIn,
const outputReserve = BN(rawReserveOut); outputReserve: rawReserveOut
}) {
const inputAmount = BN(rawInput)
const inputReserve = BN(rawReserveIn)
const outputReserve = BN(rawReserveOut)
if (inputAmount.isLessThan(BN(10 ** 9))) { if (inputAmount.isLessThan(BN(10 ** 9))) {
console.warn(`inputAmount is only ${inputAmount.toFixed(0)}. Did you forget to multiply by 10 ** decimals?`); console.warn(`inputAmount is only ${inputAmount.toFixed(0)}. Did you forget to multiply by 10 ** decimals?`)
} }
const numerator = inputAmount.multipliedBy(outputReserve).multipliedBy(997); const numerator = inputAmount.multipliedBy(outputReserve).multipliedBy(997)
const denominator = inputReserve.multipliedBy(1000).plus(inputAmount.multipliedBy(997)); const denominator = inputReserve.multipliedBy(1000).plus(inputAmount.multipliedBy(997))
return numerator.dividedBy(denominator); return numerator.dividedBy(denominator)
} }
function calculateEtherTokenInput({ outputAmount: rawOutput, inputReserve: rawReserveIn, outputReserve: rawReserveOut }) { function calculateEtherTokenInput({
const outputAmount = BN(rawOutput); outputAmount: rawOutput,
const inputReserve = BN(rawReserveIn); inputReserve: rawReserveIn,
const outputReserve = BN(rawReserveOut); outputReserve: rawReserveOut
}) {
const outputAmount = BN(rawOutput)
const inputReserve = BN(rawReserveIn)
const outputReserve = BN(rawReserveOut)
if (outputAmount.isLessThan(BN(10 ** 9))) { if (outputAmount.isLessThan(BN(10 ** 9))) {
console.warn(`inputAmount is only ${outputAmount.toFixed(0)}. Did you forget to multiply by 10 ** decimals?`); console.warn(`inputAmount is only ${outputAmount.toFixed(0)}. Did you forget to multiply by 10 ** decimals?`)
} }
const numerator = outputAmount.multipliedBy(inputReserve).multipliedBy(1000); const numerator = outputAmount.multipliedBy(inputReserve).multipliedBy(1000)
const denominator = outputReserve.minus(outputAmount).multipliedBy(997); const denominator = outputReserve.minus(outputAmount).multipliedBy(997)
return (numerator.dividedBy(denominator)).plus(1); return numerator.dividedBy(denominator).plus(1)
} }
function getSwapType(inputCurrency, outputCurrency) { function getSwapType(inputCurrency, outputCurrency) {
if (!inputCurrency || !outputCurrency) { if (!inputCurrency || !outputCurrency) {
return; return
} }
if (inputCurrency === outputCurrency) { if (inputCurrency === outputCurrency) {
return; return
} }
if (inputCurrency !== 'ETH' && outputCurrency !== 'ETH') { if (inputCurrency !== 'ETH' && outputCurrency !== 'ETH') {
...@@ -823,12 +865,12 @@ function getSwapType(inputCurrency, outputCurrency) { ...@@ -823,12 +865,12 @@ function getSwapType(inputCurrency, outputCurrency) {
} }
if (inputCurrency === 'ETH') { if (inputCurrency === 'ETH') {
return 'ETH_TO_TOKEN'; return 'ETH_TO_TOKEN'
} }
if (outputCurrency === 'ETH') { if (outputCurrency === 'ETH') {
return 'TOKEN_TO_ETH'; return 'TOKEN_TO_ETH'
} }
return; return
} }
@import "../../variables.scss"; @import '../../variables.scss';
.swap { .swap {
@extend %col-nowrap; @extend %col-nowrap;
...@@ -6,22 +6,22 @@ ...@@ -6,22 +6,22 @@
background-color: $white; background-color: $white;
&--inactive { &--inactive {
opacity: .5; opacity: 0.5;
} }
&__content { &__content {
padding: 1rem .75rem; padding: 1rem 0.75rem;
flex: 1 1 auto; flex: 1 1 auto;
height: 0; height: 0;
overflow-y: auto; overflow-y: auto;
} }
&__down-arrow { &__down-arrow {
width: .625rem; width: 0.625rem;
height: .625rem; height: 0.625rem;
position: relative; position: relative;
z-index: 200; z-index: 200;
padding: .875rem; padding: 0.875rem;
&--clickable { &--clickable {
cursor: pointer; cursor: pointer;
...@@ -38,8 +38,8 @@ ...@@ -38,8 +38,8 @@
@extend %row-nowrap; @extend %row-nowrap;
align-items: center; align-items: center;
color: $dove-gray; color: $dove-gray;
font-size: .75rem; font-size: 0.75rem;
padding: .5rem 1rem; padding: 0.5rem 1rem;
} }
&__exchange-rate { &__exchange-rate {
......
...@@ -13,28 +13,26 @@ const isLocalhost = Boolean( ...@@ -13,28 +13,26 @@ const isLocalhost = Boolean(
// [::1] is the IPv6 localhost address. // [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' || window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4. // 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match( window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ )
)
);
export default function register() { export default function register() {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW. // The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location); const publicUrl = new URL(process.env.PUBLIC_URL, window.location)
if (publicUrl.origin !== window.location.origin) { if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin // Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to // from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return; return
} }
window.addEventListener('load', () => { window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
if (isLocalhost) { if (isLocalhost) {
// This is running on localhost. Lets check if a service worker still exists or not. // This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl); checkValidServiceWorker(swUrl)
// Add some additional logging to localhost, pointing developers to the // Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation. // service worker/PWA documentation.
...@@ -42,13 +40,13 @@ export default function register() { ...@@ -42,13 +40,13 @@ export default function register() {
console.log( console.log(
'This web app is being served cache-first by a service ' + 'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://goo.gl/SC7cgQ' 'worker. To learn more, visit https://goo.gl/SC7cgQ'
); )
}); })
} else { } else {
// Is not local host. Just register service worker // Is not local host. Just register service worker
registerValidSW(swUrl); registerValidSW(swUrl)
} }
}); })
} }
} }
...@@ -57,7 +55,7 @@ function registerValidSW(swUrl) { ...@@ -57,7 +55,7 @@ function registerValidSW(swUrl) {
.register(swUrl) .register(swUrl)
.then(registration => { .then(registration => {
registration.onupdatefound = () => { registration.onupdatefound = () => {
const installingWorker = registration.installing; const installingWorker = registration.installing
installingWorker.onstatechange = () => { installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') { if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) { if (navigator.serviceWorker.controller) {
...@@ -65,20 +63,20 @@ function registerValidSW(swUrl) { ...@@ -65,20 +63,20 @@ function registerValidSW(swUrl) {
// the fresh content will have been added to the cache. // the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is // It's the perfect time to display a "New content is
// available; please refresh." message in your web app. // available; please refresh." message in your web app.
console.log('New content is available; please refresh.'); console.log('New content is available; please refresh.')
} else { } else {
// At this point, everything has been precached. // At this point, everything has been precached.
// It's the perfect time to display a // It's the perfect time to display a
// "Content is cached for offline use." message. // "Content is cached for offline use." message.
console.log('Content is cached for offline use.'); console.log('Content is cached for offline use.')
}
}
} }
} }
};
};
}) })
.catch(error => { .catch(error => {
console.error('Error during service worker registration:', error); console.error('Error during service worker registration:', error)
}); })
} }
function checkValidServiceWorker(swUrl) { function checkValidServiceWorker(swUrl) {
...@@ -86,32 +84,27 @@ function checkValidServiceWorker(swUrl) { ...@@ -86,32 +84,27 @@ function checkValidServiceWorker(swUrl) {
fetch(swUrl) fetch(swUrl)
.then(response => { .then(response => {
// Ensure service worker exists, and that we really are getting a JS file. // Ensure service worker exists, and that we really are getting a JS file.
if ( if (response.status === 404 || response.headers.get('content-type').indexOf('javascript') === -1) {
response.status === 404 ||
response.headers.get('content-type').indexOf('javascript') === -1
) {
// No service worker found. Probably a different app. Reload the page. // No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => { navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => { registration.unregister().then(() => {
window.location.reload(); window.location.reload()
}); })
}); })
} else { } else {
// Service worker found. Proceed as normal. // Service worker found. Proceed as normal.
registerValidSW(swUrl); registerValidSW(swUrl)
} }
}) })
.catch(() => { .catch(() => {
console.log( console.log('No internet connection found. App is running in offline mode.')
'No internet connection found. App is running in offline mode.' })
);
});
} }
export function unregister() { export function unregister() {
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => { navigator.serviceWorker.ready.then(registration => {
registration.unregister(); registration.unregister()
}); })
} }
} }
import store from './store.dev'; import store from './store.dev'
export default store; export default store
\ No newline at end of file
export default {}; export default {}
import { applyMiddleware, compose, createStore } from 'redux'; import { applyMiddleware, compose, createStore } from 'redux'
import thunk from 'redux-thunk' import thunk from 'redux-thunk'
import initialState from './initial-state'; import initialState from './initial-state'
import reducer from '../ducks'; import reducer from '../ducks'
const middleware = [thunk]; const middleware = [thunk]
const enhancers = []; const enhancers = []
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const store = createStore( const store = createStore(reducer, initialState, composeEnhancers(applyMiddleware(...middleware), ...enhancers))
reducer,
initialState,
composeEnhancers(
applyMiddleware(...middleware),
...enhancers,
)
);
export default store; export default store
\ No newline at end of file
...@@ -2,26 +2,26 @@ $white: #fff; ...@@ -2,26 +2,26 @@ $white: #fff;
$black: #000; $black: #000;
// Gray // Gray
$concrete-gray: #FAFAFA; $concrete-gray: #fafafa;
$mercury-gray: #E1E1E1; $mercury-gray: #e1e1e1;
$silver-gray: #C4C4C4; $silver-gray: #c4c4c4;
$chalice-gray: #AEAEAE; $chalice-gray: #aeaeae;
$dove-gray: #737373; $dove-gray: #737373;
$mine-shaft-gray: #2B2B2B; $mine-shaft-gray: #2b2b2b;
// Blue // Blue
$zumthor-blue: #EBF4FF; $zumthor-blue: #ebf4ff;
$malibu-blue: #5CA2FF; $malibu-blue: #5ca2ff;
$royal-blue: #2F80ED; $royal-blue: #2f80ed;
// Purple // Purple
$wisteria-purple: #DC6BE5; $wisteria-purple: #dc6be5;
// Red // Red
$salmon-red: #FF6871; $salmon-red: #ff6871;
// Orange // Orange
$pizazz-orange: #FF8F05; $pizazz-orange: #ff8f05;
%col-nowrap { %col-nowrap {
display: flex; display: flex;
...@@ -57,7 +57,6 @@ $pizazz-orange: #FF8F05; ...@@ -57,7 +57,6 @@ $pizazz-orange: #FF8F05;
&:disabled { &:disabled {
background-color: $mercury-gray; background-color: $mercury-gray;
} }
} }
%borderless-input { %borderless-input {
...@@ -68,7 +67,6 @@ $pizazz-orange: #FF8F05; ...@@ -68,7 +67,6 @@ $pizazz-orange: #FF8F05;
flex: 1 1 auto; flex: 1 1 auto;
width: 0; width: 0;
&::placeholder { &::placeholder {
color: $mercury-gray; color: $mercury-gray;
} }
......
...@@ -9966,6 +9966,11 @@ preserve@^0.2.0: ...@@ -9966,6 +9966,11 @@ preserve@^0.2.0:
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
integrity sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks= integrity sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=
prettier@^1.17.0:
version "1.17.0"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.17.0.tgz#53b303676eed22cc14a9f0cec09b477b3026c008"
integrity sha512-sXe5lSt2WQlCbydGETgfm1YBShgOX4HxQkFPvbxkcwgDvGDeqVau8h+12+lmSVlP3rHPz0oavfddSZg/q+Szjw==
pretty-bytes@^4.0.2: pretty-bytes@^4.0.2:
version "4.0.2" version "4.0.2"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment