web3-react migration (#269)

* put suspense below redux

* don't mount qr

* properly format .json's

* remove useless Web3Connect component

* remove react-responsive header logic

* finalize initial web3-react migration

* add rudimentary network support

* address ci/cd issues

* fix syntax

* add infura support

rewrite create-exchange

closes #173

* remove CI flag, lazy loaded disabled for now

* roll back /pool

* fix currency input errors

fix valid state of buttons

* fix nav

* obscure env variables

* fix mobile header bug
This commit is contained in:
Noah Zinsmeister 2019-04-25 12:12:47 -04:00 committed by GitHub
parent f855706f0d
commit 4e413915f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 1826 additions and 1742 deletions

3
.env.example Normal file

@ -0,0 +1,3 @@
REACT_APP_NETWORK_ID="1"
REACT_APP_NETWORK_URL=""
REACT_APP_NETWORK_NAME="Main Ethereum Network"

1
.gitignore vendored

@ -11,6 +11,7 @@
# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local

@ -16,23 +16,25 @@ This an an open source interface for Uniswap - a protocol for decentralized exch
## To Start Development
###### Installing dependencies
### Install Dependencies
```bash
yarn
```
###### Running locally on Rinkeby
### Configure Environment
Rename `.env.example` to `.env` and fill in the appropriate variables.
### Run
```bash
yarn start
# or
yarn start:rinkeby
```
###### Running locally on other testnet
```bash
REACT_APP_NETWORK_ID=2 REACT_APP_NETWORK='Ropsten Test Network' yarn start
```
More robust support for other testnets is in the works!
## Contributions

@ -1,3 +1,4 @@
# support SPA setup
[[redirects]]
from = "/*"
to = "/index.html"

@ -20,7 +20,6 @@
"react-ga": "^2.5.7",
"react-i18next": "^10.7.0",
"react-redux": "^5.0.7",
"react-responsive": "^5.0.0",
"react-router-dom": "^5.0.0",
"react-scripts": "^2.1.8",
"react-transition-group": "1.x",
@ -29,13 +28,13 @@
"redux-thunk": "^2.2.0",
"ua-parser-js": "^0.7.18",
"web3": "1.0.0-beta.52",
"web3-react": "^4.0.0"
"web3-react": "^5.0.4"
},
"scripts": {
"start": "react-scripts start",
"start:rinkeby": "REACT_APP_NETWORK_ID=4 REACT_APP_NETWORK='Rinkeby Test Network' yarn start",
"build": "react-scripts build",
"build:rinkeby": "REACT_APP_NETWORK_ID=4 REACT_APP_NETWORK='Rinkeby Test Network' yarn build",
"start:rinkeby": "REACT_APP_NETWORK_ID=4 REACT_APP_NETWORK_NAME='Rinkeby Test Network' yarn start",
"build:rinkeby": "REACT_APP_NETWORK_ID=4 REACT_APP_NETWORK_NAME='Rinkeby Test Network' yarn build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"lint:base": "yarn eslint './src/**/*.{js,jsx}'",

@ -70,6 +70,7 @@
"invalidDecimals": "Invalid decimals",
"tokenAddress": "Token Address",
"label": "Label",
"symbol": "Symbol",
"decimals": "Decimals",
"enterTokenCont": "Enter a token address to continue"
}

@ -3,35 +3,16 @@
"constant": true,
"inputs": [],
"name": "name",
"outputs": [
{
"name": "",
"type": "string"
}
],
"outputs": [{ "name": "", "type": "string" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_spender",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"inputs": [{ "name": "_spender", "type": "address" }, { "name": "_value", "type": "uint256" }],
"name": "approve",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
@ -40,12 +21,7 @@
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
@ -53,26 +29,12 @@
{
"constant": false,
"inputs": [
{
"name": "_from",
"type": "address"
},
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
{ "name": "_from", "type": "address" },
{ "name": "_to", "type": "address" },
{ "name": "_value", "type": "uint256" }
],
"name": "transferFrom",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
@ -81,31 +43,16 @@
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [
{
"name": "",
"type": "uint8"
}
],
"outputs": [{ "name": "", "type": "uint8" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_owner",
"type": "address"
}
],
"inputs": [{ "name": "_owner", "type": "address" }],
"name": "balanceOf",
"outputs": [
{
"name": "balance",
"type": "uint256"
}
],
"outputs": [{ "name": "balance", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
@ -114,85 +61,36 @@
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [
{
"name": "",
"type": "string"
}
],
"outputs": [{ "name": "", "type": "string" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"inputs": [{ "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" }],
"name": "transfer",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_owner",
"type": "address"
},
{
"name": "_spender",
"type": "address"
}
],
"inputs": [{ "name": "_owner", "type": "address" }, { "name": "_spender", "type": "address" }],
"name": "allowance",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"payable": true,
"stateMutability": "payable",
"type": "fallback"
},
{ "payable": true, "stateMutability": "payable", "type": "fallback" },
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "owner",
"type": "address"
},
{
"indexed": true,
"name": "spender",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
{ "indexed": true, "name": "owner", "type": "address" },
{ "indexed": true, "name": "spender", "type": "address" },
{ "indexed": false, "name": "value", "type": "uint256" }
],
"name": "Approval",
"type": "event"
@ -200,21 +98,9 @@
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "from",
"type": "address"
},
{
"indexed": true,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
{ "indexed": true, "name": "from", "type": "address" },
{ "indexed": true, "name": "to", "type": "address" },
{ "indexed": false, "name": "value", "type": "uint256" }
],
"name": "Transfer",
"type": "event"

@ -3,35 +3,16 @@
"constant": true,
"inputs": [],
"name": "name",
"outputs": [
{
"name": "",
"type": "string"
}
],
"outputs": [{ "name": "", "type": "string" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_spender",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"inputs": [{ "name": "_spender", "type": "address" }, { "name": "_value", "type": "uint256" }],
"name": "approve",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
@ -40,12 +21,7 @@
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
@ -53,26 +29,12 @@
{
"constant": false,
"inputs": [
{
"name": "_from",
"type": "address"
},
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
{ "name": "_from", "type": "address" },
{ "name": "_to", "type": "address" },
{ "name": "_value", "type": "uint256" }
],
"name": "transferFrom",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
@ -81,31 +43,16 @@
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [
{
"name": "",
"type": "uint8"
}
],
"outputs": [{ "name": "", "type": "uint8" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_owner",
"type": "address"
}
],
"inputs": [{ "name": "_owner", "type": "address" }],
"name": "balanceOf",
"outputs": [
{
"name": "balance",
"type": "uint256"
}
],
"outputs": [{ "name": "balance", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
@ -114,85 +61,36 @@
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [
{
"name": "",
"type": "bytes32"
}
],
"outputs": [{ "name": "", "type": "bytes32" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"inputs": [{ "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" }],
"name": "transfer",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_owner",
"type": "address"
},
{
"name": "_spender",
"type": "address"
}
],
"inputs": [{ "name": "_owner", "type": "address" }, { "name": "_spender", "type": "address" }],
"name": "allowance",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"payable": true,
"stateMutability": "payable",
"type": "fallback"
},
{ "payable": true, "stateMutability": "payable", "type": "fallback" },
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "owner",
"type": "address"
},
{
"indexed": true,
"name": "spender",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
{ "indexed": true, "name": "owner", "type": "address" },
{ "indexed": true, "name": "spender", "type": "address" },
{ "indexed": false, "name": "value", "type": "uint256" }
],
"name": "Approval",
"type": "event"
@ -200,21 +98,9 @@
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "from",
"type": "address"
},
{
"indexed": true,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
{ "indexed": true, "name": "from", "type": "address" },
{ "indexed": true, "name": "to", "type": "address" },
{ "indexed": false, "name": "value", "type": "uint256" }
],
"name": "Transfer",
"type": "event"

File diff suppressed because one or more lines are too long

@ -1 +1,74 @@
[{"name": "NewExchange", "inputs": [{"type": "address", "name": "token", "indexed": true}, {"type": "address", "name": "exchange", "indexed": true}], "anonymous": false, "type": "event"}, {"name": "initializeFactory", "outputs": [], "inputs": [{"type": "address", "name": "template"}], "constant": false, "payable": false, "type": "function", "gas": 35725}, {"name": "createExchange", "outputs": [{"type": "address", "name": "out"}], "inputs": [{"type": "address", "name": "token"}], "constant": false, "payable": false, "type": "function", "gas": 187911}, {"name": "getExchange", "outputs": [{"type": "address", "name": "out"}], "inputs": [{"type": "address", "name": "token"}], "constant": true, "payable": false, "type": "function", "gas": 715}, {"name": "getToken", "outputs": [{"type": "address", "name": "out"}], "inputs": [{"type": "address", "name": "exchange"}], "constant": true, "payable": false, "type": "function", "gas": 745}, {"name": "getTokenWithId", "outputs": [{"type": "address", "name": "out"}], "inputs": [{"type": "uint256", "name": "token_id"}], "constant": true, "payable": false, "type": "function", "gas": 736}, {"name": "exchangeTemplate", "outputs": [{"type": "address", "name": "out"}], "inputs": [], "constant": true, "payable": false, "type": "function", "gas": 633}, {"name": "tokenCount", "outputs": [{"type": "uint256", "name": "out"}], "inputs": [], "constant": true, "payable": false, "type": "function", "gas": 663}]
[
{
"name": "NewExchange",
"inputs": [
{ "type": "address", "name": "token", "indexed": true },
{ "type": "address", "name": "exchange", "indexed": true }
],
"anonymous": false,
"type": "event"
},
{
"name": "initializeFactory",
"outputs": [],
"inputs": [{ "type": "address", "name": "template" }],
"constant": false,
"payable": false,
"type": "function",
"gas": 35725
},
{
"name": "createExchange",
"outputs": [{ "type": "address", "name": "out" }],
"inputs": [{ "type": "address", "name": "token" }],
"constant": false,
"payable": false,
"type": "function",
"gas": 187911
},
{
"name": "getExchange",
"outputs": [{ "type": "address", "name": "out" }],
"inputs": [{ "type": "address", "name": "token" }],
"constant": true,
"payable": false,
"type": "function",
"gas": 715
},
{
"name": "getToken",
"outputs": [{ "type": "address", "name": "out" }],
"inputs": [{ "type": "address", "name": "exchange" }],
"constant": true,
"payable": false,
"type": "function",
"gas": 745
},
{
"name": "getTokenWithId",
"outputs": [{ "type": "address", "name": "out" }],
"inputs": [{ "type": "uint256", "name": "token_id" }],
"constant": true,
"payable": false,
"type": "function",
"gas": 736
},
{
"name": "exchangeTemplate",
"outputs": [{ "type": "address", "name": "out" }],
"inputs": [],
"constant": true,
"payable": false,
"type": "function",
"gas": 633
},
{
"name": "tokenCount",
"outputs": [{ "type": "uint256", "name": "out" }],
"inputs": [],
"constant": true,
"payable": false,
"type": "function",
"gas": 663
}
]

@ -33,7 +33,6 @@
}
&__qr-container {
display: none;
padding: 10px;
background: $concrete-gray;
border: 1px solid $mercury-gray;

@ -1,58 +1,45 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import c from 'classnames'
import React from 'react'
import classnames from 'classnames'
import { useTranslation } from 'react-i18next'
// import QrCode from '../QrCode' // commented out pending further review
import QrCode from '../QrCode'
import './address-input-panel.scss'
class AddressInputPanel extends Component {
static propTypes = {
title: PropTypes.string,
onChange: PropTypes.func,
value: PropTypes.string,
errorMessage: PropTypes.string
}
export default function AddressInputPanel({ title, onChange = () => {}, value = '', errorMessage }) {
const { t } = useTranslation()
static defaultProps = {
onChange() {},
value: ''
}
render() {
const { t, title, onChange, value, errorMessage } = this.props
return (
<div className="currency-input-panel">
<div
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="currency-input-panel__label-row">
<div className="currency-input-panel__label-container">
<span className="currency-input-panel__label">{title || t('recipientAddress')}</span>
</div>
</div>
<div className="currency-input-panel__input-row">
<input
type="text"
className={c('address-input-panel__input', {
'address-input-panel__input--error': errorMessage
})}
placeholder="0x1234..."
onChange={e => onChange(e.target.value)}
value={value}
/>
return (
<div className="currency-input-panel">
<div
className={classnames('currency-input-panel__container address-input-panel__recipient-row', {
'currency-input-panel__container--error': errorMessage
})}
>
<div className="address-input-panel__input-container">
<div className="currency-input-panel__label-row">
<div className="currency-input-panel__label-container">
<span className="currency-input-panel__label">{title || t('recipientAddress')}</span>
</div>
</div>
<div className="address-input-panel__qr-container">
<QrCode onValueReceived={value => onChange(value)} />
<div className="currency-input-panel__input-row">
<input
type="text"
className={classnames('address-input-panel__input', {
'address-input-panel__input--error': errorMessage
})}
placeholder="0x1234..."
onChange={e => onChange(e.target.value)}
value={value}
/>
</div>
</div>
{/* commented out pending further review
<div className="address-input-panel__qr-container">
<QrCode onValueReceived={value => onChange(value)} />
</div>
*/}
</div>
)
}
</div>
)
}
export default AddressInputPanel

@ -1,10 +1,15 @@
import React, { Component } from 'react'
import React, { useState, useRef, useEffect } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { CSSTransitionGroup } from 'react-transition-group'
import classnames from 'classnames'
import { withRouter } from 'react-router-dom'
import { withTranslation } from 'react-i18next'
import { withTranslation, useTranslation } from 'react-i18next'
import { BigNumber as BN } from 'bignumber.js'
import { useWeb3Context } from 'web3-react'
import { ethers } from 'ethers'
import { useSignerOrProvider } from '../../hooks'
import { getExchangeDetails, getTokenDetails, isAddress } from '../../utils'
import Fuse from '../../helpers/fuse'
import Modal from '../Modal'
import TokenLogo from '../TokenLogo'
@ -12,13 +17,10 @@ import SearchIcon from '../../assets/images/magnifying-glass.svg'
import { selectors, addPendingTx } from '../../ducks/web3connect'
import { addApprovalTx } from '../../ducks/pending'
import { addExchange } from '../../ducks/addresses'
import { BigNumber as BN } from 'bignumber.js'
import ERC20_ABI from '../../abi/erc20'
import './currency-panel.scss'
import ERC20_ABI from '../../abi/erc20'
import FACTORY_ABI from '../../abi/factory'
const FUSE_OPTIONS = {
includeMatches: false,
threshold: 0.0,
@ -32,50 +34,51 @@ const FUSE_OPTIONS = {
const TOKEN_ADDRESS_TO_LABEL = { ETH: 'ETH' }
class CurrencyInputPanel extends Component {
static propTypes = {
title: PropTypes.string,
description: PropTypes.string,
extraText: PropTypes.string,
value: PropTypes.string,
onCurrencySelected: PropTypes.func,
onValueChange: PropTypes.func,
tokenAddresses: PropTypes.shape({
addresses: PropTypes.array.isRequired
}).isRequired,
exchangeAddresses: PropTypes.shape({
fromToken: PropTypes.object.isRequired
}).isRequired,
factoryAddress: PropTypes.string,
selectedTokens: PropTypes.array.isRequired,
errorMessage: PropTypes.string,
account: PropTypes.string,
selectedTokenAddress: PropTypes.string,
disableTokenSelect: PropTypes.bool,
selectors: PropTypes.func.isRequired,
addExchange: PropTypes.func.isRequired,
filteredTokens: PropTypes.arrayOf(PropTypes.string),
disableUnlock: PropTypes.bool,
renderInput: PropTypes.func
}
function CurrencyInputPanel({
tokenAddresses,
filteredTokens = [],
onValueChange = () => {},
renderInput,
onCurrencySelected = () => {},
title,
description,
extraText,
errorMessage,
selectedTokens = [],
disableUnlock,
disableTokenSelect,
selectors,
account,
factoryAddress,
selectedTokenAddress = '',
exchangeAddresses: { fromToken },
addExchange,
history,
web3,
transactions,
pendingApprovals,
value,
addApprovalTx,
addPendingTx
}) {
const { t } = useTranslation()
const context = useWeb3Context()
const signerOrProvider = useSignerOrProvider()
static defaultProps = {
selectedTokens: [],
filteredTokens: [],
onCurrencySelected() {},
onValueChange() {},
selectedTokenAddress: ''
}
const inputRef = useRef()
state = {
isShowingModal: false,
searchQuery: '',
loadingExchange: false
}
const [isShowingModal, setIsShowingModal] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [loadingExchange, setLoadingExchange] = useState(false)
createTokenList = () => {
const { filteredTokens } = this.props
let tokens = this.props.tokenAddresses.addresses
useEffect(() => {
if (inputRef.current && isShowingModal) {
inputRef.current.focus()
}
}, [inputRef.current, isShowingModal])
function createTokenList() {
let tokens = tokenAddresses.addresses
let tokenList = [{ value: 'ETH', label: 'ETH', address: 'ETH' }]
for (let i = 0; i < tokens.length; i++) {
@ -90,30 +93,15 @@ class CurrencyInputPanel extends Component {
return tokenList.filter(({ address }) => !filteredTokens.includes(address))
}
onTokenSelect = address => {
this.setState({
searchQuery: '',
isShowingModal: false
})
function onTokenSelect(address) {
setSearchQuery('')
setIsShowingModal(false)
this.props.onCurrencySelected(address)
onCurrencySelected(address)
}
renderTokenList() {
const tokens = this.createTokenList()
const { loadingExchange, searchQuery } = this.state
const {
t,
selectedTokens,
disableTokenSelect,
web3,
selectors,
account,
factoryAddress,
exchangeAddresses: { fromToken },
addExchange,
history
} = this.props
function renderTokenList() {
const tokens = createTokenList()
if (loadingExchange) {
return (
@ -124,21 +112,24 @@ class CurrencyInputPanel extends Component {
)
}
if (web3 && web3.utils && web3.utils.isAddress(searchQuery)) {
if (isAddress(searchQuery)) {
const tokenAddress = searchQuery
const { label } = selectors().getBalance(account, tokenAddress)
const factory = new web3.eth.Contract(FACTORY_ABI, factoryAddress)
const exchangeAddress = fromToken[tokenAddress]
if (!exchangeAddress) {
this.setState({ loadingExchange: true })
factory.methods.getExchange(tokenAddress).call((err, data) => {
if (!err && data !== '0x0000000000000000000000000000000000000000') {
addExchange({ label, tokenAddress, exchangeAddress: data })
setLoadingExchange(true)
getExchangeDetails(context.networkId, tokenAddress, signerOrProvider).then(async ({ exchangeAddress }) => {
if (exchangeAddress !== ethers.constants.AddressZero) {
const { symbol } = await getTokenDetails(tokenAddress, signerOrProvider)
addExchange({
tokenAddress,
label: symbol,
exchangeAddress
})
}
this.setState({ loadingExchange: false })
setLoadingExchange(false)
})
return
}
}
@ -152,10 +143,10 @@ class CurrencyInputPanel extends Component {
results = tokens
} else {
const fuse = new Fuse(tokens, FUSE_OPTIONS)
results = fuse.search(this.state.searchQuery)
results = fuse.search(searchQuery)
}
if (!results.length && web3 && web3.utils && web3.utils.isAddress(searchQuery)) {
if (!results.length && web3 && web3.utils && isAddress(searchQuery)) {
const { label } = selectors().getBalance(account, searchQuery)
return [
<div key="token-modal-no-exchange" className="token-modal__token-row token-modal__token-row--no-exchange">
@ -165,7 +156,7 @@ class CurrencyInputPanel extends Component {
key="token-modal-create-exchange"
className="token-modal__token-row token-modal__token-row--create-exchange"
onClick={() => {
this.setState({ isShowingModal: false })
setIsShowingModal(false)
history.push(`/create-exchange/${searchQuery}`)
}}
>
@ -191,7 +182,7 @@ class CurrencyInputPanel extends Component {
className={classnames('token-modal__token-row', {
'token-modal__token-row--selected': isSelected
})}
onClick={() => this.onTokenSelect(address)}
onClick={() => onTokenSelect(address)}
>
<TokenLogo className="token-modal__token-logo" address={address} />
<div className="token-modal__token-label">{label}</div>
@ -200,13 +191,18 @@ class CurrencyInputPanel extends Component {
})
}
renderModal() {
if (!this.state.isShowingModal) {
function renderModal() {
if (!isShowingModal) {
return null
}
return (
<Modal onClose={() => this.setState({ isShowingModal: false, searchQuery: '' })}>
<Modal
onClose={() => {
setIsShowingModal(false)
setSearchQuery('')
}}
>
<CSSTransitionGroup
transitionName="token-modal"
transitionAppear={true}
@ -218,38 +214,24 @@ class CurrencyInputPanel extends Component {
<div className="token-modal">
<div className="token-modal__search-container">
<input
ref={inputRef}
type="text"
placeholder={this.props.t('searchOrPaste')}
placeholder={t('searchOrPaste')}
className="token-modal__search-input"
onChange={e => {
this.setState({ searchQuery: e.target.value })
setSearchQuery(e.target.value)
}}
/>
<img src={SearchIcon} className="token-modal__search-icon" alt="search" />
</div>
<div className="token-modal__token-list">{this.renderTokenList()}</div>
<div className="token-modal__token-list">{renderTokenList()}</div>
</div>
</CSSTransitionGroup>
</Modal>
)
}
renderUnlockButton() {
const {
t,
selectors,
selectedTokenAddress,
account,
exchangeAddresses: { fromToken },
web3,
disableUnlock,
transactions,
pendingApprovals,
value,
addApprovalTx,
addPendingTx
} = this.props
function renderUnlockButton() {
if (disableUnlock || !selectedTokenAddress || selectedTokenAddress === 'ETH') {
return
}
@ -294,9 +276,7 @@ class CurrencyInputPanel extends Component {
)
}
renderInput() {
const { t, errorMessage, value, onValueChange, selectedTokenAddress, disableTokenSelect, renderInput } = this.props
function _renderInput() {
if (typeof renderInput === 'function') {
return renderInput()
}
@ -322,7 +302,7 @@ class CurrencyInputPanel extends Component {
}}
value={value}
/>
{this.renderUnlockButton()}
{renderUnlockButton()}
<button
className={classnames('currency-input-panel__currency-select', {
'currency-input-panel__currency-select--selected': selectedTokenAddress,
@ -330,7 +310,7 @@ class CurrencyInputPanel extends Component {
})}
onClick={() => {
if (!disableTokenSelect) {
this.setState({ isShowingModal: true })
setIsShowingModal(true)
}
}}
>
@ -344,35 +324,31 @@ class CurrencyInputPanel extends Component {
)
}
render() {
const { title, description, extraText, errorMessage } = this.props
return (
<div className="currency-input-panel">
<div
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-container">
<span className="currency-input-panel__label">{title}</span>
<span className="currency-input-panel__label-description">{description}</span>
</div>
<span
className={classnames('currency-input-panel__extra-text', {
'currency-input-panel__extra-text--error': errorMessage
})}
>
{extraText}
</span>
return (
<div className="currency-input-panel">
<div
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-container">
<span className="currency-input-panel__label">{title}</span>
<span className="currency-input-panel__label-description">{description}</span>
</div>
{this.renderInput()}
<span
className={classnames('currency-input-panel__extra-text', {
'currency-input-panel__extra-text--error': errorMessage
})}
>
{extraText}
</span>
</div>
{this.renderModal()}
{_renderInput()}
</div>
)
}
{renderModal()}
</div>
)
}
export default withRouter(

@ -2,6 +2,11 @@
.header {
@extend %col-nowrap;
display: block;
&__no-decoration {
text-decoration: none;
}
&__top {
@extend %row-nowrap;
@ -27,10 +32,6 @@
margin-bottom: 1rem;
}
&--inactive {
opacity: 0.5;
}
&__dialog {
@extend %col-nowrap;
border-radius: 0.875rem;
@ -47,7 +48,7 @@
}
&--disconnected {
display: inherit;
display: block;
}
}

@ -1,9 +1,9 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import React from 'react'
import { useTranslation } from 'react-i18next'
import classnames from 'classnames'
import { useWeb3Context, Connectors } from 'web3-react'
import UAParser from 'ua-parser-js'
import { withTranslation } from 'react-i18next'
import Logo from '../Logo'
import CoinbaseWalletLogo from '../../assets/images/coinbase-wallet-logo.png'
import TrustLogo from '../../assets/images/trust-wallet-logo.svg'
@ -13,6 +13,8 @@ import Web3Status from '../Web3Status'
import './header.scss'
const { Connector, InjectedConnector } = Connectors
const links = {
coinbaseWallet: {
android: 'https://play.google.com/store/apps/details?id=org.toshi',
@ -33,8 +35,6 @@ const links = {
}
}
const ua = new UAParser(window.navigator.userAgent)
function getTrustLink() {
const os = ua.getOS()
@ -73,99 +73,102 @@ function getMetamaskLink() {
return links.metamask.chrome
}
const ua = new UAParser(window.navigator.userAgent)
function isMobile() {
return ua.getDevice().type === 'mobile'
}
class BlockingWarning extends Component {
render() {
const { t, isConnected, initialized, networkId } = this.props
let content = []
const correctNetworkId = process.env.REACT_APP_NETWORK_ID || 1
const correctNetwork = process.env.REACT_APP_NETWORK || 'Main Ethereum Network'
const wrongNetwork = networkId !== correctNetworkId
if (wrongNetwork && initialized) {
content = [
<div key="warning-title">{t('wrongNetwork')}</div>,
<div key="warning-desc" className="header__dialog__description">
{t('switchNetwork', { correctNetwork })}
</div>
]
}
if (!isConnected && initialized) {
content = [
<div key="warning-title">{t('noWallet')}</div>,
<div key="warning-desc" className="header__dialog__description">
{isMobile() ? t('installWeb3MobileBrowser') : t('installMetamask')}
</div>,
<div key="warning-logos" className="header__download">
{isMobile()
? [
<img
alt="coinbase"
src={CoinbaseWalletLogo}
key="coinbase-wallet"
onClick={() => window.open(getCoinbaseWalletLink(), '_blank')}
/>,
<img alt="trust" src={TrustLogo} key="trust" onClick={() => window.open(getTrustLink(), '_blank')} />
]
: [
<img
alt="metamask"
src={MetamaskLogo}
key="metamask"
onClick={() => window.open(getMetamaskLink(), '_blank')}
/>,
<img alt="brave" src={BraveLogo} key="brave" onClick={() => window.open(getBraveLink(), '_blank')} />
]}
</div>
]
}
return (
<div
className={classnames('header__dialog', {
'header__dialog--disconnected': (!isConnected || wrongNetwork) && initialized
})}
>
{content}
</div>
)
}
}
function Header(props) {
function BaseBlockingWarning({ title, description, children }) {
return (
<div className="header">
<BlockingWarning {...props} />
<div
className={classnames('header__top', {
'header--inactive': !props.isConnected
})}
>
<Logo />
<div className="header__center-group">
<span className="header__title">Uniswap</span>
</div>
<Web3Status isConnected />
<div
className={classnames('header__dialog', {
'header__dialog--disconnected': true
})}
>
<div key="warning-title">{title}</div>
<div key="warning-desc" className="header__dialog__description">
{description}
</div>
{children}
</div>
)
}
Header.propTypes = {
currentAddress: PropTypes.string,
isConnected: PropTypes.bool.isRequired
function BlockingWarning() {
const { t } = useTranslation()
const correctNetwork = process.env.REACT_APP_NETWORK_NAME || 'Main Ethereum Network'
const context = useWeb3Context()
if (context.error && context.error.code === Connector.errorCodes.UNSUPPORTED_NETWORK) {
return <BaseBlockingWarning title={t('wrongNetwork')} description={t('switchNetwork', { correctNetwork })} />
}
// this is an intermediate state before infura is set
if (context.error && context.error.code === InjectedConnector.errorCodes.UNLOCK_REQUIRED) {
return null
}
if (context.error) {
console.error(context.error)
return <BaseBlockingWarning title={t('disconnected')} />
}
if (!context.account) {
return (
<BaseBlockingWarning
title={t('noWallet')}
description={isMobile() ? t('installWeb3MobileBrowser') : t('installMetamask')}
>
<div key="warning-logos" className="header__download">
{isMobile() ? (
<>
<img
alt="coinbase"
src={CoinbaseWalletLogo}
key="coinbase-wallet"
onClick={() => window.open(getCoinbaseWalletLink(), '_blank')}
/>
<img alt="trust" src={TrustLogo} key="trust" onClick={() => window.open(getTrustLink(), '_blank')} />
</>
) : (
<>
<img
alt="metamask"
src={MetamaskLogo}
key="metamask"
onClick={() => window.open(getMetamaskLink(), '_blank')}
/>
<img alt="brave" src={BraveLogo} key="brave" onClick={() => window.open(getBraveLink(), '_blank')} />
</>
)}
</div>
</BaseBlockingWarning>
)
}
return null
}
export default connect(state => ({
currentAddress: state.web3connect.account,
initialized: state.web3connect.initialized,
isConnected: !!state.web3connect.account,
web3: state.web3connect.web3,
networkId: state.web3connect.networkId
}))(withTranslation()(Header))
export default function Header() {
const context = useWeb3Context()
return (
<div className="header">
<BlockingWarning />
<div className={classnames('header__top')}>
<a className="header__no-decoration" href="https://uniswap.io" target="_blank" rel="noopener noreferrer">
<Logo />
</a>
<div className="header__center-group">
<a className="header__no-decoration" href="https://uniswap.io" target="_blank" rel="noopener noreferrer">
<span className="header__title">Uniswap</span>
</a>
</div>
<Web3Status isConnected={!!(context.active && context.account)} />
</div>
</div>
)
}

@ -1,25 +0,0 @@
@import '../../variables';
.beta-message {
@extend %row-nowrap;
flex: 1 0 auto;
align-items: center;
position: relative;
padding: 0.5rem 1rem;
margin-bottom: 1rem;
border: 1px solid rgba($wisteria-purple, 0.4);
background-color: rgba($wisteria-purple, 0.1);
border-radius: 2rem;
font-size: 0.75rem;
line-height: 1rem;
text-align: center;
color: $wisteria-purple;
&:after {
content: '';
top: 0.5rem;
right: 1rem;
position: absolute;
color: $wisteria-purple;
}
}

@ -1,57 +1,76 @@
import React, { Component } from 'react'
import { withRouter } from 'react-router-dom'
import React, { useCallback } from 'react'
import { withRouter, NavLink } from 'react-router-dom'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { withTranslation } from 'react-i18next'
import { useTranslation } from 'react-i18next'
import { dismissBetaMessage } from '../../ducks/app'
import { Tab, Tabs } from '../Tab'
import { useBodyKeyDown } from '../../hooks'
import './beta-message.scss'
import './navigation-tabs.scss'
class NavigationTabs extends Component {
static propTypes = {
history: PropTypes.shape({
push: PropTypes.func.isRequired
}),
className: PropTypes.string,
dismissBetaMessage: PropTypes.func.isRequired,
showBetaMessage: PropTypes.bool.isRequired
const tabOrder = [
{
path: '/swap',
textKey: 'swap',
regex: /\/swap/
},
{
path: '/send',
textKey: 'send',
regex: /\/send/
},
{
path: 'add-liquidity',
textKey: 'pool',
regex: /\/add-liquidity|\/remove-liquidity|\/create-exchange.*/
}
]
constructor(props) {
super(props)
this.state = {
selectedPath: this.props.location.pathname,
className: '',
showWarning: true
}
}
function NavigationTabs({ location: { pathname }, history, dismissBetaMessage, showBetaMessage }) {
const { t } = useTranslation()
renderTab(name, path, regex) {
const { push } = this.props.history
return <Tab text={name} onClick={() => push(path)} isSelected={regex.test(this.props.location.pathname)} />
}
const navigate = useCallback(
direction => {
const tabIndex = tabOrder.findIndex(({ regex }) => pathname.match(regex))
history.push(tabOrder[(tabIndex + tabOrder.length + direction) % tabOrder.length].path)
},
[pathname, history]
)
const navigateRight = useCallback(() => {
navigate(1)
}, [navigate])
const navigateLeft = useCallback(() => {
navigate(-1)
}, [navigate])
render() {
const { t, showBetaMessage, className, dismissBetaMessage } = this.props
return (
<div>
<Tabs className={className}>
{this.renderTab(t('swap'), '/swap', /swap/)}
{this.renderTab(t('send'), '/send', /send/)}
{this.renderTab(t('pool'), '/add-liquidity', /add-liquidity|remove-liquidity|create-exchange/)}
</Tabs>
{showBetaMessage && (
<div className="beta-message" onClick={dismissBetaMessage}>
<span role="img" aria-label="warning">
💀
</span>{' '}
{t('betaWarning')}
</div>
)}
useBodyKeyDown('ArrowRight', navigateRight)
useBodyKeyDown('ArrowLeft', navigateLeft)
return (
<>
<div className="tabs">
{tabOrder.map(({ path, textKey, regex }) => (
<NavLink
key={path}
to={path}
className="tab"
activeClassName="tab--selected"
isActive={(_, { pathname }) => pathname.match(regex)}
>
{t(textKey)}
</NavLink>
))}
</div>
)
}
{showBetaMessage && (
<div className="beta-message" onClick={dismissBetaMessage}>
<span role="img" aria-label="warning">
💀
</span>{' '}
{t('betaWarning')}
</div>
)}
</>
)
}
export default withRouter(
@ -62,5 +81,5 @@ export default withRouter(
dispatch => ({
dismissBetaMessage: () => dispatch(dismissBetaMessage())
})
)(withTranslation()(NavigationTabs))
)(NavigationTabs)
)

@ -0,0 +1,58 @@
@import '../../variables';
.beta-message {
@extend %row-nowrap;
cursor: pointer;
flex: 1 0 auto;
align-items: center;
position: relative;
padding: 0.5rem 1rem;
margin-bottom: 1rem;
border: 1px solid rgba($wisteria-purple, 0.4);
background-color: rgba($wisteria-purple, 0.1);
border-radius: 2rem;
font-size: 0.75rem;
line-height: 1rem;
text-align: center;
color: $wisteria-purple;
&:after {
content: '';
top: 0.5rem;
right: 1rem;
position: absolute;
color: $wisteria-purple;
}
}
.tabs {
@extend %row-nowrap;
align-items: center;
height: 2.5rem;
background-color: $concrete-gray;
border-radius: 3rem;
box-shadow: 0 0 0 0.5px darken($concrete-gray, 5);
margin-bottom: 1rem;
}
.tab {
@extend %row-nowrap;
align-items: center;
justify-content: center;
height: 2.5rem;
flex: 1 0 auto;
border-radius: 3rem;
cursor: pointer;
text-decoration: none;
color: $dove-gray;
font-size: 1rem;
&--selected {
background-color: $white;
border-radius: 3rem;
box-shadow: 0 0 0.5px 0.5px $mercury-gray;
font-weight: 500;
color: $royal-blue;
}
}

@ -1,33 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import './tab.scss'
export const Tabs = props => {
return <div className={classnames('tabs', props.className)}>{props.children}</div>
}
export const Tab = props => {
return (
<div
className={classnames('tab', {
'tab--selected': props.isSelected
})}
onClick={props.onClick}
>
{props.text ? <span>{props.text}</span> : null}
</div>
)
}
Tab.propTypes = {
className: PropTypes.string,
text: PropTypes.string,
isSelected: PropTypes.bool,
onClick: PropTypes.func
}
Tab.defaultProps = {
className: ''
}

@ -1,45 +0,0 @@
@import '../../variables.scss';
.tabs {
@extend %row-nowrap;
align-items: center;
height: 2.5rem;
background-color: $concrete-gray;
border-radius: 3rem;
box-shadow: 0 0 0 0.5px darken($concrete-gray, 5);
.tab:first-child {
//margin-left: -1px;
}
.tab:last-child {
//margin-right: -1px;
}
}
.tab {
@extend %row-nowrap;
align-items: center;
justify-content: center;
height: 2.5rem;
flex: 1 0 auto;
border-radius: 3rem;
transition: 300ms ease-in-out;
cursor: pointer;
span {
color: $dove-gray;
font-size: 1rem;
}
&--selected {
background-color: $white;
border-radius: 3rem;
box-shadow: 0 0 0.5px 0.5px $mercury-gray;
font-weight: 500;
span {
color: $royal-blue;
}
}
}

@ -6,9 +6,11 @@ import Jazzicon from 'jazzicon'
import { CSSTransitionGroup } from 'react-transition-group'
import { withTranslation } from 'react-i18next'
import { ethers } from 'ethers'
import './web3-status.scss'
import Modal from '../Modal'
import './web3-status.scss'
function getEtherscanLink(tx) {
return `https://etherscan.io/tx/${tx}`
}
@ -138,7 +140,6 @@ Web3Status.defaultProps = {
export default connect(state => {
return {
address: state.web3connect.account,
isConnected: !!(state.web3connect.web3 && state.web3connect.account),
pending: state.web3connect.transactions.pending,
confirmed: state.web3connect.transactions.confirmed
}

@ -8,8 +8,6 @@ export const SET_WEB3_CONNECTION_STATUS = 'WEB3_CONNECTION_STATUS'
export const CHECK_WEB3_CONNECTION = 'CHECK_WEB3_CONNECTION'
export const SET_CURRENT_MASK_ADDRESS = 'SET_CURRENT_MASK_ADDRESS'
export const METAMASK_LOCKED = 'METAMASK_LOCKED'
export const METAMASK_UNLOCKED = 'METAMASK_UNLOCKED'
export const SET_INTERACTION_STATE = 'SET_INTERACTION_STATE'
export const SET_NETWORK_MESSAGE = 'SET_NETWORK_MESSAGE'

@ -210,6 +210,10 @@ export const setAddresses = networkId => {
// Rinkeby
case 4:
case '4':
return {
type: SET_ADDRESSES,
payload: RINKEBY
}
default:
return {
type: SET_ADDRESSES,

@ -1,13 +1,11 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { BigNumber as BN } from 'bignumber.js'
import Web3 from 'web3'
import ERC20_ABI from '../abi/erc20'
import ERC20_WITH_BYTES_ABI from '../abi/erc20_symbol_bytes32'
export const INITIALIZE = 'we3connect/initialize'
export const UPDATE_ACCOUNT = 'we3connect/updateAccount'
export const INITIALIZE = 'web3connect/initialize'
export const INITIALIZE_WEB3 = 'web3connect/initializeWeb3'
export const UPDATE_ACCOUNT = 'web3connect/updateAccount'
export const WATCH_ETH_BALANCE = 'web3connect/watchEthBalance'
export const WATCH_TOKEN_BALANCE = 'web3connect/watchTokenBalance'
export const UPDATE_ETH_BALANCE = 'web3connect/updateEthBalance'
@ -24,7 +22,7 @@ const initialState = {
web3: null,
networkId: 0,
initialized: false,
account: '',
account: null,
balances: {
ethereum: {}
},
@ -63,10 +61,6 @@ export const selectors = () => (dispatch, getState) => {
}
const getBalance = (address, tokenAddress) => {
if (process.env.NODE_ENV !== 'production' && !tokenAddress) {
console.warn('No token address found - return ETH balance')
}
if (!tokenAddress || tokenAddress === 'ETH') {
const balance = state.balances.ethereum[address]
if (!balance) {
@ -106,46 +100,30 @@ const Balance = (value, label = '', decimals = 0) => ({
decimals: +decimals
})
export const initialize = () => (dispatch, getState) => {
const { web3connect } = getState()
export const initialize = () => async dispatch => {
await dispatch({ type: INITIALIZE })
}
return new Promise(async (resolve, reject) => {
if (web3connect.web3) {
resolve(web3connect.web3)
return
}
export const updateNetwork = (passedProvider, networkId) => async dispatch => {
const web3 = new Web3(passedProvider)
if (typeof window.ethereum !== 'undefined') {
try {
const web3 = new Web3(window.ethereum)
await window.ethereum.enable()
dispatch({
type: INITIALIZE,
payload: web3
})
resolve(web3)
return
} catch (error) {
console.error('User denied access.')
dispatch({ type: INITIALIZE })
reject()
return
}
}
const dispatches = [
dispatch({ type: INITIALIZE_WEB3, payload: web3 }),
dispatch({ type: UPDATE_NETWORK_ID, payload: networkId })
]
if (typeof window.web3 !== 'undefined') {
const web3 = new Web3(window.web3.currentProvider)
dispatch({
type: INITIALIZE,
payload: web3
})
resolve(web3)
return
}
await Promise.all(dispatches)
}
dispatch({ type: INITIALIZE })
reject()
})
export const updateAccount = account => async dispatch => {
if (account !== null) {
const dispatches = [
dispatch({ type: UPDATE_ACCOUNT, payload: account }),
dispatch(watchBalance({ balanceOf: account }))
]
await Promise.all(dispatches)
}
}
export const watchBalance = ({ balanceOf, tokenAddress }) => (dispatch, getState) => {
@ -216,29 +194,14 @@ export const updateApprovals = ({ tokenAddress, tokenOwner, spender, balance })
export const sync = () => async (dispatch, getState) => {
const { getBalance, getApprovals } = dispatch(selectors())
const web3 = await dispatch(initialize())
const {
account,
web3,
watched,
contracts,
networkId,
transactions: { pending }
} = getState().web3connect
// Sync Account
const accounts = await web3.eth.getAccounts()
if (account !== accounts[0]) {
dispatch({ type: UPDATE_ACCOUNT, payload: accounts[0] })
dispatch(watchBalance({ balanceOf: accounts[0] }))
}
if (!networkId) {
dispatch({
type: UPDATE_NETWORK_ID,
payload: await web3.eth.net.getId()
})
}
// Sync Ethereum Balances
watched.balances.ethereum.forEach(async address => {
const balance = await web3.eth.getBalance(address)
@ -264,7 +227,6 @@ export const sync = () => async (dispatch, getState) => {
}
const contract = contracts[tokenAddress] || new web3.eth.Contract(ERC20_ABI, tokenAddress)
const contractBytes32 = contracts[tokenAddress] || new web3.eth.Contract(ERC20_WITH_BYTES_ABI, tokenAddress)
if (!contracts[tokenAddress]) {
dispatch({
@ -291,6 +253,7 @@ export const sync = () => async (dispatch, getState) => {
.catch())
} catch (e) {
try {
const contractBytes32 = new web3.eth.Contract(ERC20_WITH_BYTES_ABI, tokenAddress)
symbol =
symbol ||
web3.utils.hexToString(
@ -320,34 +283,34 @@ export const sync = () => async (dispatch, getState) => {
// Update Approvals
Object.entries(watched.approvals).forEach(([tokenAddress, token]) => {
const contract = contracts[tokenAddress] || new web3.eth.Contract(ERC20_ABI, tokenAddress)
const contractBytes32 = contracts[tokenAddress] || new web3.eth.Contract(ERC20_WITH_BYTES_ABI, tokenAddress)
Object.entries(token).forEach(([tokenOwnerAddress, tokenOwner]) => {
tokenOwner.forEach(async spenderAddress => {
const approvalBalance = getApprovals(tokenAddress, tokenOwnerAddress, spenderAddress)
const balance = await contract.methods.allowance(tokenOwnerAddress, spenderAddress).call()
const decimals = approvalBalance.decimals || (await contract.methods.decimals().call())
let symbol = approvalBalance.label
try {
symbol = symbol || (await contract.methods.symbol().call())
} catch (e) {
if (tokenOwnerAddress !== null && tokenOwnerAddress !== 'null') {
const approvalBalance = getApprovals(tokenAddress, tokenOwnerAddress, spenderAddress)
const balance = await contract.methods.allowance(tokenOwnerAddress, spenderAddress).call()
const decimals = approvalBalance.decimals || (await contract.methods.decimals().call())
let symbol = approvalBalance.label
try {
symbol = symbol || web3.utils.hexToString(await contractBytes32.methods.symbol().call())
} catch (err) {}
symbol = symbol || (await contract.methods.symbol().call())
} catch (e) {
try {
const contractBytes32 = new web3.eth.Contract(ERC20_WITH_BYTES_ABI, tokenAddress)
symbol = symbol || web3.utils.hexToString(await contractBytes32.methods.symbol().call())
} catch (err) {}
}
if (approvalBalance.label && approvalBalance.value.isEqualTo(BN(balance))) {
return
}
dispatch(
updateApprovals({
tokenAddress,
tokenOwner: tokenOwnerAddress,
spender: spenderAddress,
balance: Balance(balance, symbol, decimals)
})
)
}
if (approvalBalance.label && approvalBalance.value.isEqualTo(BN(balance))) {
return
}
dispatch(
updateApprovals({
tokenAddress,
tokenOwner: tokenOwnerAddress,
spender: spenderAddress,
balance: Balance(balance, symbol, decimals)
})
)
})
})
})
@ -384,22 +347,25 @@ export const sync = () => async (dispatch, getState) => {
})
}
export const startWatching = () => async (dispatch, getState) => {
const { account } = getState().web3connect
const timeout = !account ? 1000 : 5000
dispatch(sync())
setTimeout(() => dispatch(startWatching()), timeout)
export const startWatching = () => async dispatch => {
await dispatch(sync())
setTimeout(() => dispatch(startWatching()), 5000)
}
export default function web3connectReducer(state = initialState, { type, payload }) {
switch (type) {
case INITIALIZE_WEB3:
return {
...state,
web3: payload
}
case INITIALIZE:
return {
...state,
web3: payload,
initialized: true
}
case UPDATE_NETWORK_ID:
return { ...state, networkId: payload }
case UPDATE_ACCOUNT:
return {
...state,
@ -496,8 +462,6 @@ export default function web3connectReducer(state = initialState, { type, payload
}
}
}
case UPDATE_NETWORK_ID:
return { ...state, networkId: payload }
case ADD_PENDING_TX:
return {
...state,
@ -530,32 +494,3 @@ export default function web3connectReducer(state = initialState, { type, payload
return state
}
}
// Connect Component
export class _Web3Connect extends Component {
static propTypes = {
initialize: PropTypes.func.isRequired
}
static defaultProps = {
initialize() {}
}
componentWillMount() {
this.props.initialize().then(this.props.startWatching())
}
render() {
return <noscript />
}
}
export const Web3Connect = connect(
({ web3connect }) => ({
web3: web3connect.web3
}),
dispatch => ({
initialize: () => dispatch(initialize()),
startWatching: () => dispatch(startWatching())
})
)(_Web3Connect)

51
src/hooks/index.js Normal file

@ -0,0 +1,51 @@
import { useMemo, useEffect } from 'react'
import { useWeb3Context } from 'web3-react'
import FACTORY_ABI from '../abi/factory'
import { getSignerOrProvider, getContract } from '../utils'
const factoryAddresses = {
1: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95',
4: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36'
}
export function useSignerOrProvider() {
const { library, account } = useWeb3Context()
return useMemo(() => getSignerOrProvider(library, account), [library, account])
}
// returns null if the contract cannot be created for any reason
function useContract(contractAddress, ABI) {
const signerOrProvider = useSignerOrProvider()
return useMemo(() => {
try {
return getContract(contractAddress, ABI, signerOrProvider)
} catch {
return null
}
}, [contractAddress, ABI, signerOrProvider])
}
export function useFactoryContract() {
const { networkId } = useWeb3Context()
return useContract(factoryAddresses[networkId], FACTORY_ABI)
}
// modified from https://usehooks.com/useKeyPress/
export function useBodyKeyDown(targetKey, onKeyDown, suppressOnKeyDown = false) {
function downHandler({ target: { tagName }, key }) {
if (key === targetKey && tagName === 'BODY' && !suppressOnKeyDown) {
onKeyDown()
}
}
useEffect(() => {
window.addEventListener('keydown', downHandler)
return () => {
window.removeEventListener('keydown', downHandler)
}
}, [targetKey, onKeyDown, suppressOnKeyDown])
}

@ -14,13 +14,15 @@ i18next
// https://www.i18next.com/overview/configuration-options
.init({
backend: {
loadPath: './locales/{{lng}}.json'
loadPath: '/locales/{{lng}}.json'
},
react: {
useSuspense: false
},
lng: 'en',
fallbackLng: 'en',
keySeparator: false,
interpolation: {
escapeValue: false // not needed for react as it escapes by default
}
interpolation: { escapeValue: false }
})
export default i18next

@ -1,7 +1,8 @@
import React, { Suspense } from 'react'
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import ReactGA from 'react-ga'
import Web3Provider, { Connectors } from 'web3-react'
import './i18n'
import App from './pages/App'
@ -13,15 +14,20 @@ if (process.env.NODE_ENV === 'production') {
} else {
ReactGA.initialize('test', { testMode: true })
}
ReactGA.pageview(window.location.pathname + window.location.search)
const { InjectedConnector, NetworkOnlyConnector } = Connectors
const Injected = new InjectedConnector({ supportedNetworks: [Number(process.env.REACT_APP_NETWORK_ID) || 1] })
const Infura = new NetworkOnlyConnector({
providerURL: process.env.REACT_APP_NETWORK_URL || ''
})
const connectors = { Injected, Infura }
ReactDOM.render(
// catch the suspense in case translations are not yet loaded
<Suspense fallback={null}>
<Provider store={store}>
<Provider store={store}>
<Web3Provider connectors={connectors} libraryName="ethers.js">
<App />
</Provider>
</Suspense>,
</Web3Provider>
</Provider>,
document.getElementById('root')
)

@ -1,8 +1,10 @@
import React, { Component } from 'react'
import React, { useState, useEffect } from 'react'
import { connect } from 'react-redux'
import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom'
import MediaQuery from 'react-responsive'
import { Web3Connect, startWatching, initialize } from '../ducks/web3connect'
import { useWeb3Context, Connectors } from 'web3-react'
import NavigationTabs from '../components/NavigationTabs'
import { updateNetwork, updateAccount, initialize, startWatching } from '../ducks/web3connect'
import { setAddresses } from '../ducks/addresses'
import Header from '../components/Header'
import Swap from './Swap'
@ -11,63 +13,102 @@ import Pool from './Pool'
import './App.scss'
class App extends Component {
componentWillMount() {
const { initialize, startWatching } = this.props
initialize().then(startWatching)
}
const { Connector, InjectedConnector } = Connectors
componentWillUpdate() {
const { web3, setAddresses } = this.props
function App({ initialized, setAddresses, updateNetwork, updateAccount, initialize, startWatching }) {
const context = useWeb3Context()
if (this.hasSetNetworkId || !web3 || !web3.eth || !web3.eth.net || !web3.eth.net.getId) {
return
}
web3.eth.net.getId((err, networkId) => {
if (!err && !this.hasSetNetworkId) {
setAddresses(networkId)
this.hasSetNetworkId = true
// start web3-react on page-load
useEffect(() => {
context.setConnector('Injected', { suppressAndThrowErrors: true }).catch(error => {
if (error.code === Connector.errorCodes.UNSUPPORTED_NETWORK) {
context.setError(error, { connectorName: 'Injected' })
} else {
context.setConnector('Infura')
}
})
}
}, [])
render() {
if (!this.props.initialized) {
return <noscript />
// if the metamask user logs out, set the infura provider
useEffect(() => {
if (context.error && context.error.code === InjectedConnector.errorCodes.UNLOCK_REQUIRED) {
context.setConnector('Infura')
}
}, [context.error, context.connectorName])
// initialize redux network
const [reduxNetworkInitialized, setReduxNetworkInitialized] = useState(false)
useEffect(() => {
if (context.active) {
setAddresses(context.networkId)
updateNetwork(context.library._web3Provider, context.networkId)
setReduxNetworkInitialized(true)
}
}, [context.active, context.networkId])
// initialize redux account
const [reduxAccountInitialized, setReduxAccountInitialized] = useState(false)
useEffect(() => {
if (context.active) {
updateAccount(context.account)
setReduxAccountInitialized(true)
}
}, [context.active, context.account])
// initialize redux
useEffect(() => {
if (reduxNetworkInitialized && reduxAccountInitialized) {
initialize().then(startWatching)
}
}, [reduxNetworkInitialized, reduxAccountInitialized])
// active state
if (initialized || context.error) {
return (
<div id="app-container">
<MediaQuery query="(min-width: 768px)">
<Header />
</MediaQuery>
<Web3Connect />
<BrowserRouter>
<Header />
{/* this is an intermediate state before infura is set */}
{initialized && (!context.error || context.error.code === InjectedConnector.errorCodes.UNLOCK_REQUIRED) && (
<div className="app__wrapper">
<Switch>
<Route exact path="/swap" component={Swap} />
<Route exact path="/send" component={Send} />
<Route exact path="/add-liquidity" component={Pool} />
<Route exact path="/remove-liquidity" component={Pool} />
<Route exact path="/create-exchange/:tokenAddress?" component={Pool} />
<Redirect exact from="/" to="/swap" />
</Switch>
<div className="body">
<div className="body__content">
<BrowserRouter>
<NavigationTabs />
<Switch>
<Route exact strict path="/swap" component={Swap} />
<Route exact strict path="/send" component={Send} />
<Route
path={[
'/add-liquidity',
'/remove-liquidity',
'/create-exchange',
'/create-exchange/:tokenAddress?'
]}
component={Pool}
/>
<Redirect to="/swap" />
</Switch>
</BrowserRouter>
</div>
</div>
</div>
</BrowserRouter>
)}
</div>
)
}
// loading state
return null
}
export default connect(
state => ({
account: state.web3connect.account,
initialized: state.web3connect.initialized,
web3: state.web3connect.web3
initialized: state.web3connect.initialized
}),
dispatch => ({
setAddresses: networkId => dispatch(setAddresses(networkId)),
updateNetwork: (passedProvider, networkId) => dispatch(updateNetwork(passedProvider, networkId)),
updateAccount: account => dispatch(updateAccount(account)),
initialize: () => dispatch(initialize()),
startWatching: () => dispatch(startWatching())
})

@ -7,6 +7,7 @@
margin: auto;
max-width: 560px;
width: 100%;
overflow-y: auto;
& > div {
position: absolute;
@ -24,3 +25,16 @@
@extend %col-nowrap;
}
.body {
@extend %col-nowrap;
height: 100%;
background-color: $white;
&__content {
padding: 1rem 0.75rem;
flex: 1 1 auto;
height: 0;
overflow-y: auto;
}
}

@ -1,15 +1,25 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import Web3Provider, { Connectors } from 'web3-react'
import App from './App'
import store from '../store'
// TODO, fix this hacky workaround
const { NetworkOnlyConnector } = Connectors
const Injected = new NetworkOnlyConnector({
providerURL: process.env.REACT_APP_NETWORK_URL
})
export const connectors = { Injected }
it('renders without crashing', () => {
const div = document.createElement('div')
ReactDOM.render(
<Provider store={store}>
<App />
<Web3Provider connectors={connectors} libraryName="ethers.js">
<App />
</Web3Provider>
</Provider>,
div
)

@ -2,29 +2,29 @@ import React, { Component } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import { withTranslation } from 'react-i18next'
import { withTranslation, useTranslation } from 'react-i18next'
import { useWeb3Context } from 'web3-react'
import ReactGA from 'react-ga'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import OversizedPanel from '../../components/OversizedPanel'
import ContextualInfo from '../../components/ContextualInfo'
import NavigationTabs from '../../components/NavigationTabs'
import { selectors, addPendingTx } from '../../ducks/web3connect'
import PlusBlue from '../../assets/images/plus-blue.svg'
import PlusGrey from '../../assets/images/plus-grey.svg'
import { getBlockDeadline } from '../../helpers/web3-utils'
import { retry } from '../../helpers/promise-utils'
import ModeSelector from './ModeSelector'
import { BigNumber as BN } from 'bignumber.js'
import EXCHANGE_ABI from '../../abi/exchange'
import './pool.scss'
import ReactGA from 'react-ga'
const INPUT = 0
const OUTPUT = 1
class AddLiquidity extends Component {
static propTypes = {
isConnected: PropTypes.bool.isRequired,
account: PropTypes.string.isRequired,
account: PropTypes.string,
selectors: PropTypes.func.isRequired,
balances: PropTypes.object.isRequired,
exchangeAddresses: PropTypes.shape({
@ -50,11 +50,10 @@ class AddLiquidity extends Component {
}
shouldComponentUpdate(nextProps, nextState) {
const { t, isConnected, account, exchangeAddresses, balances, web3 } = this.props
const { t, account, exchangeAddresses, balances, web3 } = this.props
const { inputValue, outputValue, inputCurrency, outputCurrency, lastEditedField } = this.state
return (
isConnected !== nextProps.isConnected ||
t !== nextProps.t ||
account !== nextProps.account ||
exchangeAddresses !== nextProps.exchangeAddresses ||
@ -434,6 +433,7 @@ class AddLiquidity extends Component {
renderSummary(inputError, outputError) {
const {
t,
account,
selectors,
exchangeAddresses: { fromToken }
} = this.props
@ -444,7 +444,11 @@ class AddLiquidity extends Component {
let contextualInfo = ''
let isError = false
const { label } = selectors().getTokenBalance(outputCurrency, fromToken[outputCurrency])
if (inputError || outputError) {
if (!account) {
contextualInfo = t('noWallet')
isError = true
} else if (inputError || outputError) {
contextualInfo = inputError || outputError
isError = true
} else if (!inputCurrency || !outputCurrency) {
@ -493,7 +497,7 @@ class AddLiquidity extends Component {
if (this.isNewExchange()) {
return (
<div>
<>
<div className="pool__summary-item">
{t('youAreAdding')} {b(`${inputValue} ETH`)} {t('and')} {b(`${outputValue} ${label}`)} {t('intoPool')}
</div>
@ -510,7 +514,7 @@ class AddLiquidity extends Component {
{t('youWillMint')} {b(`${inputValue}`)} {t('liquidityTokens')}
</div>
<div className="pool__summary-item">{t('totalSupplyIs0')}</div>
</div>
</>
)
}
@ -523,7 +527,7 @@ class AddLiquidity extends Component {
const adjTotalSupply = totalSupply.dividedBy(10 ** poolTokenDecimals)
return (
<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')}
@ -538,14 +542,13 @@ class AddLiquidity extends Component {
{t('tokenWorth')} {b(+ethReserve.dividedBy(totalSupply).toFixed(7))} ETH {t('and')}{' '}
{b(+tokenReserve.dividedBy(totalSupply).toFixed(7))} {label}
</div>
</div>
</>
)
}
render() {
const {
t,
isConnected,
exchangeAddresses: { fromToken },
selectors
} = this.props
@ -555,19 +558,8 @@ class AddLiquidity extends Component {
const { inputError, outputError, isValid } = this.validate()
const { label } = selectors().getTokenBalance(outputCurrency, fromToken[outputCurrency])
return [
<div
key="content"
className={classnames('swap__content', {
'swap--inactive': !isConnected
})}
>
<NavigationTabs
className={classnames('header__navigation', {
'header--inactive': !isConnected
})}
/>
return (
<>
{this.isNewExchange() ? (
<div className="pool__new-exchange-warning">
<div className="pool__new-exchange-warning-text">
@ -579,7 +571,7 @@ class AddLiquidity extends Component {
<div className="pool__new-exchange-warning-text">{t('initialExchangeRate', { label })}</div>
</div>
) : null}
<ModeSelector title={t('addLiquidity')} />
<CurrencyInputPanel
title={t('deposit')}
extraText={this.getBalance(inputCurrency)}
@ -615,26 +607,33 @@ class AddLiquidity extends Component {
<OversizedPanel hideBottom>{this.renderInfo()}</OversizedPanel>
{this.renderSummary(inputError, outputError)}
<div className="pool__cta-container">
<button
className={classnames('pool__cta-btn', {
'swap--inactive': !this.props.isConnected,
'pool__cta-btn--inactive': !isValid
})}
disabled={!isValid}
onClick={this.onAddLiquidity}
>
{t('addLiquidity')}
</button>
<AddLiquidityButton callOnClick={this.onAddLiquidity} isValid={isValid} />
</div>
</div>
]
</>
)
}
}
function AddLiquidityButton({ callOnClick, isValid }) {
const { t } = useTranslation()
const context = useWeb3Context()
const isActive = context.active && context.account
return (
<button
className={classnames('pool__cta-btn', {
'pool__cta-btn--inactive': !isActive
})}
disabled={!isValid}
onClick={callOnClick}
>
{t('addLiquidity')}
</button>
)
}
export default connect(
state => ({
isConnected:
Boolean(state.web3connect.account) && state.web3connect.networkId === (process.env.REACT_APP_NETWORK_ID || 1),
account: state.web3connect.account,
balances: state.web3connect.balances,
web3: state.web3connect.web3,

@ -1,243 +1,158 @@
import React, { Component } from 'react'
import React, { useState, useEffect } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { withRouter } from 'react-router-dom'
import { withTranslation } from 'react-i18next'
import { selectors, addPendingTx } from '../../ducks/web3connect'
import { ethers } from 'ethers'
import classnames from 'classnames'
import NavigationTabs from '../../components/NavigationTabs'
import ModeSelector from './ModeSelector'
import { withRouter } from 'react-router'
import { useTranslation } from 'react-i18next'
import ReactGA from 'react-ga'
import { useWeb3Context } from 'web3-react'
import { addPendingTx } from '../../ducks/web3connect'
import AddressInputPanel from '../../components/AddressInputPanel'
import OversizedPanel from '../../components/OversizedPanel'
import FACTORY_ABI from '../../abi/factory'
import { addExchange } from '../../ducks/addresses'
import ReactGA from 'react-ga'
import { useSignerOrProvider, useFactoryContract } from '../../hooks'
import { isAddress, getTokenDetails, getExchangeDetails, errorCodes } from '../../utils'
class CreateExchange extends Component {
static propTypes = {
web3: PropTypes.object,
selectors: PropTypes.func.isRequired,
addExchange: PropTypes.func.isRequired,
account: PropTypes.string,
isConnected: PropTypes.bool.isRequired,
factoryAddress: PropTypes.string.isRequired,
exchangeAddresses: PropTypes.shape({
fromToken: PropTypes.object.isRequired
}).isRequired
}
function CreateExchange({ history, location, addExchange, addPendingTx }) {
const { t } = useTranslation()
const context = useWeb3Context()
const signerOrProvider = useSignerOrProvider()
const factory = useFactoryContract()
constructor(props) {
super(props)
const {
match: {
params: { tokenAddress }
}
} = this.props
const [tokenAddress, setTokenAddress] = useState(location.state && location.state.tokenAddress)
const [errorMessage, _setErrorMessage] = useState(context.account ? undefined : t('noWallet'))
const [tokenDetails, setTokenDetails] = useState()
this.state = {
tokenAddress,
label: '',
decimals: 0
}
}
validate() {
const { tokenAddress } = this.state
const {
t,
web3,
account,
selectors,
factoryAddress,
exchangeAddresses: { fromToken },
addExchange
} = this.props
let isValid = true
let errorMessage = ''
if (!tokenAddress) {
return {
isValid: false
}
}
if (web3 && web3.utils && !web3.utils.isAddress(tokenAddress)) {
return {
isValid: false,
errorMessage: t('invalidTokenAddress')
}
}
const { label, decimals } = selectors().getBalance(account, tokenAddress)
const factory = new web3.eth.Contract(FACTORY_ABI, factoryAddress)
const exchangeAddress = fromToken[tokenAddress]
if (!exchangeAddress) {
factory.methods.getExchange(tokenAddress).call((err, data) => {
if (!err && data !== '0x0000000000000000000000000000000000000000') {
addExchange({ label, tokenAddress, exchangeAddress: data })
}
})
// wrap _setErrorMessage to ensure an account is in context
function setErrorMessage(value) {
if (value) {
_setErrorMessage(value)
} else if (!context.account) {
_setErrorMessage(t('noWallet'))
} else {
errorMessage = t('exchangeExists', { label })
}
if (!label) {
errorMessage = t('invalidSymbol')
}
if (!decimals) {
errorMessage = t('invalidDecimals')
}
return {
isValid: isValid && !errorMessage,
errorMessage
_setErrorMessage()
}
}
onChange = tokenAddress => {
const { selectors, account, web3 } = this.props
if (web3 && web3.utils && web3.utils.isAddress(tokenAddress)) {
const { label, decimals } = selectors().getBalance(account, tokenAddress)
this.setState({
label,
decimals,
tokenAddress
})
} else {
this.setState({
label: '',
decimals: 0,
tokenAddress
})
// clear state, if it exists
useEffect(() => {
if (location.state) {
history.replace(location.pathname)
}
}
}, [])
onCreateExchange = () => {
const { tokenAddress } = this.state
const { account, web3, factoryAddress } = this.props
// handle changes to tokenAddress
useEffect(() => {
let stale = false
if (web3 && web3.utils && !web3.utils.isAddress(tokenAddress)) {
return
// happy path
if (isAddress(tokenAddress)) {
const tokenDetailsPromise = getTokenDetails(tokenAddress, signerOrProvider)
const exchangeDetailsPromise = getExchangeDetails(context.networkId, tokenAddress, signerOrProvider)
Promise.all([tokenDetailsPromise, exchangeDetailsPromise])
.then(([tokenDetails, exchangeDetails]) => {
if (!stale) {
if (exchangeDetails.exchangeAddress !== ethers.constants.AddressZero) {
addExchange({
tokenAddress,
label: tokenDetails.symbol,
exchangeAddress: exchangeDetails.exchangeAddress
})
setErrorMessage(t('exchangeExists', { tokenAddress }))
}
setTokenDetails(tokenDetails)
}
})
.catch(error => {
if (!stale) {
if (error.code === errorCodes.TOKEN_DETAILS_DECIMALS) {
setErrorMessage(t('invalidDecimals'))
} else if (error.code === errorCodes.TOKEN_DETAILS_SYMBOL) {
setErrorMessage(t('invalidSymbol'))
} else {
setErrorMessage(t('invalidTokenAddress'))
}
}
})
}
// is tokenAddress is empty, there's no error
else if (tokenAddress === undefined || tokenAddress === '') {
setErrorMessage()
}
// tokenAddress is not a proper address
else {
setErrorMessage(t('invalidTokenAddress'))
}
const factory = new web3.eth.Contract(FACTORY_ABI, factoryAddress)
factory.methods.createExchange(tokenAddress).send({ from: account }, (err, data) => {
if (!err) {
this.setState({
label: '',
decimals: 0,
tokenAddress: ''
})
this.props.addPendingTx(data)
ReactGA.event({
category: 'Pool',
action: 'CreateExchange'
})
}
return () => {
stale = true
setErrorMessage()
setTokenDetails()
}
}, [tokenAddress, signerOrProvider, context.networkId])
async function createExchange() {
const estimatedGasLimit = await factory.estimate.createExchange(tokenAddress)
factory.createExchange(tokenAddress, { gasLimit: estimatedGasLimit }).then(details => {
addPendingTx(details.hash)
setErrorMessage()
setTokenAddress()
ReactGA.event({
category: 'Pool',
action: 'CreateExchange'
})
})
}
renderSummary() {
const { tokenAddress } = this.state
const { errorMessage } = this.validate()
const isValid = isAddress(tokenAddress) && !errorMessage && tokenDetails && tokenDetails.tokenAddress === tokenAddress
if (!tokenAddress) {
return (
<div className="create-exchange__summary-panel">
<div className="create-exchange__summary-text">{this.props.t('enterTokenCont')}</div>
</div>
)
}
if (errorMessage) {
return (
<div className="create-exchange__summary-panel">
<div className="create-exchange__summary-text create-exchange--error">{errorMessage}</div>
</div>
)
}
return null
}
render() {
const { tokenAddress } = this.state
const { t, isConnected, account, selectors, web3 } = this.props
const { isValid, errorMessage } = this.validate()
let label, decimals
if (web3 && web3.utils && web3.utils.isAddress(tokenAddress)) {
const { label: _label, decimals: _decimals } = selectors().getBalance(account, tokenAddress)
label = _label
decimals = _decimals
}
return (
<div
key="content"
className={classnames('swap__content', {
'swap--inactive': !isConnected
})}
>
<NavigationTabs
className={classnames('header__navigation', {
'header--inactive': !isConnected
})}
/>
<ModeSelector title={t('createExchange')} />
<AddressInputPanel
title={t('tokenAddress')}
value={tokenAddress}
onChange={this.onChange}
errorMessage={errorMessage}
/>
<OversizedPanel hideBottom>
<div className="pool__summary-panel">
<div className="pool__exchange-rate-wrapper">
<span className="pool__exchange-rate">{t('label')}</span>
<span>{label || ' - '}</span>
</div>
<div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t('decimals')}</span>
<span>{decimals || ' - '}</span>
</div>
return (
<>
<AddressInputPanel
title={t('tokenAddress')}
value={tokenAddress}
onChange={input => setTokenAddress(input)}
errorMessage={errorMessage === t('noWallet') ? '' : errorMessage}
/>
<OversizedPanel hideBottom>
<div className="pool__summary-panel">
<div className="pool__exchange-rate-wrapper">
<span className="pool__exchange-rate">{t('symbol')}</span>
<span>{tokenDetails ? tokenDetails.symbol : ' - '}</span>
</div>
</OversizedPanel>
{this.renderSummary()}
<div className="pool__cta-container">
<button
className={classnames('pool__cta-btn', {
'swap--inactive': !isConnected
})}
disabled={!isValid}
onClick={this.onCreateExchange}
>
{t('createExchange')}
</button>
<div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t('decimals')}</span>
<span>{tokenDetails ? tokenDetails.decimals : ' - '}</span>
</div>
</div>
</OversizedPanel>
<div className="create-exchange__summary-panel">
<div
className={classnames('create-exchange__summary-text', {
'create-exchange--error': !!errorMessage
})}
>
{!!errorMessage ? errorMessage : t('enterTokenCont')}
</div>
</div>
)
}
<div className="pool__cta-container">
<button className="pool__cta-btn" disabled={!isValid} onClick={createExchange}>
{t('createExchange')}
</button>
</div>
</>
)
}
export default withRouter(
connect(
state => ({
isConnected:
Boolean(state.web3connect.account) && state.web3connect.networkId === (process.env.REACT_APP_NETWORK_ID || 1),
account: state.web3connect.account,
balances: state.web3connect.balances,
web3: state.web3connect.web3,
exchangeAddresses: state.addresses.exchangeAddresses,
factoryAddress: state.addresses.factoryAddress
}),
undefined,
dispatch => ({
selectors: () => dispatch(selectors()),
addExchange: opts => dispatch(addExchange(opts)),
addPendingTx: id => dispatch(addPendingTx(id))
})
)(withTranslation()(CreateExchange))
)(CreateExchange)
)

@ -1,83 +1,103 @@
import React, { Component } from 'react'
import { withRouter } from 'react-router-dom'
import { withTranslation } from 'react-i18next'
import React, { useState, useCallback } from 'react'
import { withRouter, NavLink } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { CSSTransitionGroup } from 'react-transition-group'
import OversizedPanel from '../../components/OversizedPanel'
import Dropdown from '../../assets/images/dropdown-blue.svg'
import Modal from '../../components/Modal'
import { CSSTransitionGroup } from 'react-transition-group'
import { useBodyKeyDown } from '../../hooks'
const ADD = 'Add Liquidity'
const REMOVE = 'Remove Liquidity'
const CREATE = 'Create Exchange'
import './pool.scss'
class ModeSelector extends Component {
state = {
isShowingModal: false,
selected: ADD
const poolTabOrder = [
{
path: '/add-liquidity',
textKey: 'addLiquidity',
regex: /\/add-liquidity/
},
{
path: '/remove-liquidity',
textKey: 'removeLiquidity',
regex: /\/remove-liquidity/
},
{
path: '/create-exchange',
textKey: 'createExchange',
regex: /\/create-exchange.*/
}
]
changeView(view) {
const { history } = this.props
function ModeSelector({ location: { pathname }, history }) {
const { t } = useTranslation()
this.setState({
isShowingModal: false,
selected: view
})
const [isShowingModal, setIsShowingModal] = useState(false)
switch (view) {
case ADD:
return history.push('/add-liquidity')
case REMOVE:
return history.push('/remove-liquidity')
case CREATE:
return history.push('/create-exchange')
default:
return
}
}
const activeTabKey = poolTabOrder[poolTabOrder.findIndex(({ regex }) => pathname.match(regex))].textKey
renderModal() {
if (!this.state.isShowingModal) {
return
}
const navigate = useCallback(
direction => {
const tabIndex = poolTabOrder.findIndex(({ regex }) => pathname.match(regex))
history.push(poolTabOrder[(tabIndex + poolTabOrder.length + direction) % poolTabOrder.length].path)
},
[pathname, history]
)
const navigateRight = useCallback(() => {
navigate(1)
}, [navigate])
const navigateLeft = useCallback(() => {
navigate(-1)
}, [navigate])
return (
<Modal onClose={() => this.setState({ isShowingModal: false })}>
<CSSTransitionGroup
transitionName="pool-modal"
transitionAppear={true}
transitionLeave={true}
transitionAppearTimeout={200}
transitionLeaveTimeout={200}
transitionEnterTimeout={200}
useBodyKeyDown('ArrowDown', navigateRight, isShowingModal)
useBodyKeyDown('ArrowUp', navigateLeft, isShowingModal)
return (
<OversizedPanel hideTop>
<div
className="pool__liquidity-container"
onClick={() => {
setIsShowingModal(true)
}}
>
<span className="pool__liquidity-label">{t(activeTabKey)}</span>
<img src={Dropdown} alt="dropdown" />
</div>
{isShowingModal && (
<Modal
onClose={() => {
setIsShowingModal(false)
}}
>
<div className="pool-modal">
<div className="pool-modal__item" onClick={() => this.changeView(ADD)}>
{this.props.t('addLiquidity')}
<CSSTransitionGroup
transitionName="pool-modal"
transitionAppear={true}
transitionLeave={true}
transitionAppearTimeout={200}
transitionLeaveTimeout={200}
transitionEnterTimeout={200}
>
<div className="pool-modal">
{poolTabOrder.map(({ path, textKey, regex }) => (
<NavLink
key={path}
to={path}
className="pool-modal__item"
activeClassName="pool-modal__item--selected"
isActive={(_, { pathname }) => pathname.match(regex)}
onClick={() => {
setIsShowingModal(false)
}}
>
{t(textKey)}
</NavLink>
))}
</div>
<div className="pool-modal__item" onClick={() => this.changeView(REMOVE)}>
{this.props.t('removeLiquidity')}
</div>
<div className="pool-modal__item" onClick={() => this.changeView(CREATE)}>
{this.props.t('createExchange')}
</div>
</div>
</CSSTransitionGroup>
</Modal>
)
}
render() {
return (
<OversizedPanel hideTop>
<div className="pool__liquidity-container" onClick={() => this.setState({ isShowingModal: true })}>
<span className="pool__liquidity-label">{this.props.title}</span>
<img src={Dropdown} alt="dropdown" />
</div>
{this.renderModal()}
</OversizedPanel>
)
}
</CSSTransitionGroup>
</Modal>
)}
</OversizedPanel>
)
}
export default withRouter(withTranslation()(ModeSelector))
export default withRouter(ModeSelector)

@ -3,9 +3,10 @@ import PropTypes from 'prop-types'
import classnames from 'classnames'
import { connect } from 'react-redux'
import { BigNumber as BN } from 'bignumber.js'
import { withTranslation } from 'react-i18next'
import NavigationTabs from '../../components/NavigationTabs'
import ModeSelector from './ModeSelector'
import { withTranslation, useTranslation } from 'react-i18next'
import ReactGA from 'react-ga'
import { useWeb3Context } from 'web3-react'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import { selectors, addPendingTx } from '../../ducks/web3connect'
import ContextualInfo from '../../components/ContextualInfo'
@ -15,7 +16,6 @@ import ArrowDownGrey from '../../assets/images/arrow-down-grey.svg'
import { getBlockDeadline } from '../../helpers/web3-utils'
import { retry } from '../../helpers/promise-utils'
import EXCHANGE_ABI from '../../abi/exchange'
import ReactGA from 'react-ga'
class RemoveLiquidity extends Component {
static propTypes = {
@ -173,6 +173,7 @@ class RemoveLiquidity extends Component {
renderSummary(errorMessage) {
const {
t,
account,
selectors,
exchangeAddresses: { fromToken }
} = this.props
@ -181,7 +182,10 @@ class RemoveLiquidity extends Component {
let contextualInfo = ''
let isError = false
if (errorMessage) {
if (!account) {
contextualInfo = t('noWallet')
isError = true
} else if (errorMessage) {
contextualInfo = errorMessage
isError = true
} else if (!tokenAddress) {
@ -366,23 +370,12 @@ class RemoveLiquidity extends Component {
}
render() {
const { t, isConnected } = this.props
const { t } = this.props
const { tokenAddress, value } = this.state
const { isValid, errorMessage } = this.validate()
return [
<div
key="content"
className={classnames('swap__content', {
'swap--inactive': !isConnected
})}
>
<NavigationTabs
className={classnames('header__navigation', {
'header--inactive': !isConnected
})}
/>
<ModeSelector title={t('removeLiquidity')} />
return (
<>
<CurrencyInputPanel
title={t('poolTokens')}
extraText={this.getBalance(tokenAddress)}
@ -401,26 +394,33 @@ class RemoveLiquidity extends Component {
{this.renderOutput()}
{this.renderSummary(errorMessage)}
<div className="pool__cta-container">
<button
className={classnames('pool__cta-btn', {
'swap--inactive': !isConnected,
'pool__cta-btn--inactive': !isValid
})}
disabled={!isValid}
onClick={this.onRemoveLiquidity}
>
{t('removeLiquidity')}
</button>
<RemoveLiquidityButton callOnClick={this.onRemoveLiquidity} isValid={isValid} />
</div>
</div>
]
</>
)
}
}
function RemoveLiquidityButton({ callOnClick, isValid }) {
const { t } = useTranslation()
const context = useWeb3Context()
const isActive = context.active && context.account
return (
<button
className={classnames('pool__cta-btn', {
'pool__cta-btn--inactive': !isActive
})}
disabled={!isValid}
onClick={callOnClick}
>
{t('removeLiquidity')}
</button>
)
}
export default connect(
state => ({
isConnected:
Boolean(state.web3connect.account) && state.web3connect.networkId === (process.env.REACT_APP_NETWORK_ID || 1),
web3: state.web3connect.web3,
balances: state.web3connect.balances,
account: state.web3connect.account,

@ -1,31 +1,36 @@
import React, { Component } from 'react'
import Header from '../../components/Header'
import React, { useEffect } from 'react'
import ReactGA from 'react-ga'
import { Switch, Route, Redirect } from 'react-router-dom'
import ModeSelector from './ModeSelector'
import AddLiquidity from './AddLiquidity'
import CreateExchange from './CreateExchange'
import RemoveLiquidity from './RemoveLiquidity'
import { Switch, Route } from 'react-router-dom'
import './pool.scss'
import MediaQuery from 'react-responsive'
import ReactGA from 'react-ga'
class Pool extends Component {
componentWillMount() {
export default function Pool() {
useEffect(() => {
ReactGA.pageview(window.location.pathname + window.location.search)
}
render() {
return (
<div className="pool">
<MediaQuery query="(max-width: 768px)">
<Header />
</MediaQuery>
<Switch>
<Route exact path="/add-liquidity" component={AddLiquidity} />
<Route exact path="/remove-liquidity" component={RemoveLiquidity} />
<Route exact path="/create-exchange/:tokenAddress?" component={CreateExchange} />
</Switch>
</div>
)
}
}
}, [])
export default Pool
return (
<>
<ModeSelector />
<Switch>
<Route exact strict path="/add-liquidity" component={AddLiquidity} />
<Route exact strict path="/remove-liquidity" component={RemoveLiquidity} />
<Route exact strict path="/create-exchange" component={CreateExchange} />
<Route
path="/create-exchange/:tokenAddress"
render={({ match }) => {
return (
<Redirect to={{ pathname: '/create-exchange', state: { tokenAddress: match.params.tokenAddress } }} />
)
}}
/>
<Redirect to="/add-liquidity" />
</Switch>
</>
)
}

@ -5,6 +5,13 @@
height: 100%;
background-color: $white;
&__content {
padding: 1rem 0.75rem;
flex: 1 1 auto;
height: 0;
overflow-y: auto;
}
&__liquidity-container {
@extend %row-nowrap;
align-items: center;
@ -114,20 +121,22 @@
}
&__item {
@extend %row-nowrap;
padding: 1rem;
margin-left: 1rem;
margin-right: 1rem;
font-size: 1rem;
cursor: pointer;
text-decoration: none;
color: $dove-gray;
font-size: 1rem;
&:hover {
background-color: $concrete-gray;
.token-modal__token-label {
color: $black;
}
}
&:active {
background-color: darken($concrete-gray, 1);
&--selected {
background-color: $white;
border-radius: 3rem;
box-shadow: 0 0 0.5px 0.5px $mercury-gray;
font-weight: 500;
color: $royal-blue;
}
}
}

@ -3,10 +3,11 @@ import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import { BigNumber as BN } from 'bignumber.js'
import { withTranslation } from 'react-i18next'
import { withTranslation, useTranslation } from 'react-i18next'
import ReactGA from 'react-ga'
import { useWeb3Context } from 'web3-react'
import { selectors, addPendingTx } from '../../ducks/web3connect'
import Header from '../../components/Header'
import NavigationTabs from '../../components/NavigationTabs'
import AddressInputPanel from '../../components/AddressInputPanel'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import ContextualInfo from '../../components/ContextualInfo'
@ -18,8 +19,6 @@ import { retry } from '../../helpers/promise-utils'
import EXCHANGE_ABI from '../../abi/exchange'
import './send.scss'
import MediaQuery from 'react-responsive'
import ReactGA from 'react-ga'
const INPUT = 0
const OUTPUT = 1
@ -27,7 +26,6 @@ const OUTPUT = 1
class Send extends Component {
static propTypes = {
account: PropTypes.string,
isConnected: PropTypes.bool.isRequired,
selectors: PropTypes.func.isRequired,
web3: PropTypes.object.isRequired
}
@ -583,7 +581,10 @@ class Send extends Component {
let contextualInfo = ''
let isError = false
if (inputError || outputError) {
if (!account) {
contextualInfo = t('noWallet')
isError = true
} else if (inputError || outputError) {
contextualInfo = inputError || outputError
isError = true
} else if (!inputCurrency || !outputCurrency) {
@ -751,87 +752,81 @@ class Send extends Component {
const { inputError, outputError, isValid } = this.validate()
return (
<div className="send">
<MediaQuery query="(max-width: 767px)">
<Header />
</MediaQuery>
<div
className={classnames('swap__content', {
'swap--inactive': !this.props.isConnected
})}
>
<NavigationTabs
className={classnames('header__navigation', {
'header--inactive': !this.props.isConnected
})}
/>
<CurrencyInputPanel
title={t('input')}
description={lastEditedField === OUTPUT ? estimatedText : ''}
extraText={this.renderBalance(inputCurrency, inputBalance, inputDecimals)}
onCurrencySelected={inputCurrency => this.setState({ inputCurrency }, this.recalcForm)}
onValueChange={this.updateInput}
selectedTokens={[inputCurrency, outputCurrency]}
selectedTokenAddress={inputCurrency}
value={inputValue}
errorMessage={inputError}
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
<img
onClick={this.flipInputOutput}
className="swap__down-arrow swap__down-arrow--clickable"
alt="arrow"
src={isValid ? ArrowDownBlue : ArrowDownGrey}
/>
</div>
</OversizedPanel>
<CurrencyInputPanel
title={t('output')}
description={lastEditedField === INPUT ? estimatedText : ''}
extraText={this.renderBalance(outputCurrency, outputBalance, outputDecimals)}
onCurrencySelected={outputCurrency => this.setState({ outputCurrency }, this.recalcForm)}
onValueChange={this.updateOutput}
selectedTokens={[inputCurrency, outputCurrency]}
value={outputValue}
selectedTokenAddress={outputCurrency}
errorMessage={outputError}
disableUnlock
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
<img className="swap__down-arrow" src={isValid ? ArrowDownBlue : ArrowDownGrey} alt="arrow" />
</div>
</OversizedPanel>
<AddressInputPanel
t={this.props.t}
value={recipient}
onChange={address => this.setState({ recipient: address })}
/>
{this.renderExchangeRate()}
{this.renderSummary(inputError, outputError)}
<div className="swap__cta-container">
<button
className={classnames('swap__cta-btn', {
'swap--inactive': !this.props.isConnected
})}
disabled={!isValid}
onClick={this.onSend}
>
{t('send')}
</button>
<>
<CurrencyInputPanel
title={t('input')}
description={lastEditedField === OUTPUT ? estimatedText : ''}
extraText={this.renderBalance(inputCurrency, inputBalance, inputDecimals)}
onCurrencySelected={inputCurrency => this.setState({ inputCurrency }, this.recalcForm)}
onValueChange={this.updateInput}
selectedTokens={[inputCurrency, outputCurrency]}
selectedTokenAddress={inputCurrency}
value={inputValue}
errorMessage={inputError}
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
<img
onClick={this.flipInputOutput}
className="swap__down-arrow swap__down-arrow--clickable"
alt="arrow"
src={isValid ? ArrowDownBlue : ArrowDownGrey}
/>
</div>
</OversizedPanel>
<CurrencyInputPanel
title={t('output')}
description={lastEditedField === INPUT ? estimatedText : ''}
extraText={this.renderBalance(outputCurrency, outputBalance, outputDecimals)}
onCurrencySelected={outputCurrency => this.setState({ outputCurrency }, this.recalcForm)}
onValueChange={this.updateOutput}
selectedTokens={[inputCurrency, outputCurrency]}
value={outputValue}
selectedTokenAddress={outputCurrency}
errorMessage={outputError}
disableUnlock
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
<img className="swap__down-arrow" src={isValid ? ArrowDownBlue : ArrowDownGrey} alt="arrow" />
</div>
</OversizedPanel>
<AddressInputPanel
t={this.props.t}
value={recipient}
onChange={address => this.setState({ recipient: address })}
/>
{this.renderExchangeRate()}
{this.renderSummary(inputError, outputError)}
<div className="swap__cta-container">
<SendButton callOnClick={this.onSend} isValid={isValid} />
</div>
</div>
</>
)
}
}
function SendButton({ callOnClick, isValid }) {
const { t } = useTranslation()
const context = useWeb3Context()
const isActive = context.active && context.account
return (
<button
className={classnames('swap__cta-btn', {
'swap--inactive': !isActive
})}
disabled={!isValid}
onClick={callOnClick}
>
{t('send')}
</button>
)
}
export default connect(
state => ({
balances: state.web3connect.balances,
isConnected: !!state.web3connect.account && state.web3connect.networkId === (process.env.REACT_APP_NETWORK_ID || 1),
account: state.web3connect.account,
web3: state.web3connect.web3,
exchangeAddresses: state.addresses.exchangeAddresses

@ -3,12 +3,11 @@ import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import { BigNumber as BN } from 'bignumber.js'
import MediaQuery from 'react-responsive'
import ReactGA from 'react-ga'
import { withTranslation } from 'react-i18next'
import { withTranslation, useTranslation } from 'react-i18next'
import { useWeb3Context } from 'web3-react'
import { selectors, addPendingTx } from '../../ducks/web3connect'
import Header from '../../components/Header'
import NavigationTabs from '../../components/NavigationTabs'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import ContextualInfo from '../../components/ContextualInfo'
import OversizedPanel from '../../components/OversizedPanel'
@ -26,7 +25,6 @@ const OUTPUT = 1
class Swap extends Component {
static propTypes = {
account: PropTypes.string,
isConnected: PropTypes.bool.isRequired,
selectors: PropTypes.func.isRequired,
addPendingTx: PropTypes.func.isRequired,
web3: PropTypes.object.isRequired
@ -554,7 +552,7 @@ class Swap extends Component {
renderSummary(inputError, outputError) {
const { inputValue, inputCurrency, outputValue, outputCurrency } = this.state
const t = this.props.t
const { t, account } = this.props
const inputIsZero = BN(inputValue).isZero()
const outputIsZero = BN(outputValue).isZero()
@ -582,6 +580,11 @@ class Swap extends Component {
contextualInfo = t('unlockTokenCont')
}
if (!account) {
contextualInfo = t('noWallet')
isError = true
}
return (
<ContextualInfo
openDetailsText={t('transactionDetails')}
@ -731,77 +734,70 @@ class Swap extends Component {
const { inputError, outputError, isValid } = this.validate()
return (
<div className="swap">
<MediaQuery query="(max-width: 767px)">
<Header />
</MediaQuery>
<div
className={classnames('swap__content', {
'swap--inactive': !this.props.isConnected
})}
>
<NavigationTabs
className={classnames('header__navigation', {
'header--inactive': !this.props.isConnected
})}
/>
<CurrencyInputPanel
title={t('input')}
description={lastEditedField === OUTPUT ? estimatedText : ''}
extraText={this.renderBalance(inputCurrency, inputBalance, inputDecimals)}
onCurrencySelected={inputCurrency => this.setState({ inputCurrency }, this.recalcForm)}
onValueChange={this.updateInput}
selectedTokens={[inputCurrency, outputCurrency]}
selectedTokenAddress={inputCurrency}
value={inputValue}
errorMessage={inputError}
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
<img
onClick={this.flipInputOutput}
className="swap__down-arrow swap__down-arrow--clickable"
alt="swap"
src={isValid ? ArrowDownBlue : ArrowDownGrey}
/>
</div>
</OversizedPanel>
<CurrencyInputPanel
title={t('output')}
description={lastEditedField === INPUT ? estimatedText : ''}
extraText={this.renderBalance(outputCurrency, outputBalance, outputDecimals)}
onCurrencySelected={outputCurrency => this.setState({ outputCurrency }, this.recalcForm)}
onValueChange={this.updateOutput}
selectedTokens={[inputCurrency, outputCurrency]}
value={outputValue}
selectedTokenAddress={outputCurrency}
errorMessage={outputError}
disableUnlock
/>
{this.renderExchangeRate()}
{this.renderSummary(inputError, outputError)}
<div className="swap__cta-container">
<button
className={classnames('swap__cta-btn', {
'swap--inactive': !this.props.isConnected
})}
disabled={!isValid}
onClick={this.onSwap}
>
{t('swap')}
</button>
<>
<CurrencyInputPanel
title={t('input')}
description={lastEditedField === OUTPUT ? estimatedText : ''}
extraText={this.renderBalance(inputCurrency, inputBalance, inputDecimals)}
onCurrencySelected={inputCurrency => this.setState({ inputCurrency }, this.recalcForm)}
onValueChange={this.updateInput}
selectedTokens={[inputCurrency, outputCurrency]}
selectedTokenAddress={inputCurrency}
value={inputValue}
errorMessage={inputError}
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
<img
onClick={this.flipInputOutput}
className="swap__down-arrow swap__down-arrow--clickable"
alt="swap"
src={isValid ? ArrowDownBlue : ArrowDownGrey}
/>
</div>
</OversizedPanel>
<CurrencyInputPanel
title={t('output')}
description={lastEditedField === INPUT ? estimatedText : ''}
extraText={this.renderBalance(outputCurrency, outputBalance, outputDecimals)}
onCurrencySelected={outputCurrency => this.setState({ outputCurrency }, this.recalcForm)}
onValueChange={this.updateOutput}
selectedTokens={[inputCurrency, outputCurrency]}
value={outputValue}
selectedTokenAddress={outputCurrency}
errorMessage={outputError}
disableUnlock
/>
{this.renderExchangeRate()}
{this.renderSummary(inputError, outputError)}
<div className="swap__cta-container">
<SwapButton callOnClick={this.onSwap} isValid={isValid} />
</div>
</div>
</>
)
}
}
function SwapButton({ callOnClick, isValid }) {
const { t } = useTranslation()
const context = useWeb3Context()
const isActive = context.active && context.account
return (
<button
className={classnames('swap__cta-btn', { 'swap--inactive': !isActive })}
disabled={!isValid}
onClick={callOnClick}
>
{t('swap')}
</button>
)
}
export default connect(
state => ({
balances: state.web3connect.balances,
isConnected: !!state.web3connect.account && state.web3connect.networkId === (process.env.REACT_APP_NETWORK_ID || 1),
account: state.web3connect.account,
web3: state.web3connect.web3,
exchangeAddresses: state.addresses.exchangeAddresses

@ -7,6 +7,7 @@
&--inactive {
opacity: 0.5;
cursor: default;
}
&__content {

71
src/utils/index.js Normal file

@ -0,0 +1,71 @@
import { ethers } from 'ethers'
import FACTORY_ABI from '../abi/factory'
import ERC20_ABI from '../abi/erc20'
import ERC20_WITH_BYTES_ABI from '../abi/erc20_symbol_bytes32'
const factoryAddresses = {
1: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95',
4: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36'
}
export const errorCodes = ['TOKEN_DETAILS_DECIMALS', 'TOKEN_DETAILS_SYMBOL'].reduce(
(accumulator, currentValue, currentIndex) => {
accumulator[currentValue] = currentIndex
return accumulator
},
{}
)
function getFactoryContract(networkId, signerOrProvider) {
return getContract(factoryAddresses[networkId], FACTORY_ABI, signerOrProvider)
}
export function isAddress(value) {
try {
ethers.utils.getAddress(value)
return true
} catch {
return false
}
}
export function getSignerOrProvider(library, account) {
return account ? library.getSigner(account) : library
}
export function getContract(contractAddress, ABI, signerOrProvider) {
return new ethers.Contract(contractAddress, ABI, signerOrProvider)
}
export async function getTokenDetails(tokenAddress, signerOrProvider) {
const contract = getContract(tokenAddress, ERC20_ABI, signerOrProvider)
const decimalsPromise = contract.decimals().catch(error => {
console.log(error)
error.code = errorCodes.TOKEN_DETAILS_DECIMALS
throw error
})
const symbolPromise = contract
.symbol()
.catch(() => {
const contractBytes32 = getContract(tokenAddress, ERC20_WITH_BYTES_ABI, signerOrProvider)
return contractBytes32.symbol().then(bytes32 => ethers.utils.parseBytes32String(bytes32))
})
.catch(error => {
error.code = errorCodes.TOKEN_DETAILS_SYMBOL
throw error
})
return Promise.all([decimalsPromise, symbolPromise]).then(([decimals, symbol]) => ({
decimals,
symbol,
tokenAddress
}))
}
export async function getExchangeDetails(networkId, tokenAddress, signerOrProvider) {
const factoryContract = getFactoryContract(networkId, signerOrProvider)
return factoryContract.getExchange(tokenAddress).then(exchangeAddress => ({ exchangeAddress, tokenAddress }))
}

385
yarn.lock

@ -871,7 +871,7 @@
js-levenshtein "^1.1.3"
semver "^5.3.0"
"@babel/preset-env@^7.1.6", "@babel/preset-env@^7.3.4":
"@babel/preset-env@^7.1.6":
version "7.4.3"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.4.3.tgz#e71e16e123dc0fbf65a52cbcbcefd072fbd02880"
integrity sha512-FYbZdV12yHdJU5Z70cEg0f6lvtpZ8jFSDakTm7WXeJbLXh4R0ztGEu/SW7G1nJ2ZvKwDhz8YrbA84eYyprmGqw==
@ -951,13 +951,6 @@
dependencies:
regenerator-runtime "^0.12.0"
"@babel/runtime@7.3.4":
version "7.3.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83"
integrity sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g==
dependencies:
regenerator-runtime "^0.12.0"
"@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1":
version "7.4.3"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.3.tgz#79888e452034223ad9609187a0ad1fe0d2ad4bdc"
@ -1064,43 +1057,6 @@
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
"@portis/web3-provider-engine@1.0.9":
version "1.0.9"
resolved "https://registry.yarnpkg.com/@portis/web3-provider-engine/-/web3-provider-engine-1.0.9.tgz#2d2f6e4d3701f0003308a1071bd8b9de4cca6ba5"
integrity sha512-0N9uhKKGUBWKU96KlbOkQSz6WYXI46QP5oubxffFGdxKUBiRU2IPrRkQA2d0zoERLMgtT4OlGvog1pxLpjzLfg==
dependencies:
async "^2.5.0"
backoff "^2.5.0"
clone "^2.0.0"
cross-fetch "^2.1.0"
eth-block-tracker "^4.2.0"
eth-json-rpc-filters "^4.0.2"
eth-json-rpc-infura "^3.1.0"
eth-json-rpc-middleware "github:radotzki/eth-json-rpc-middleware#patch-1"
eth-sig-util "^1.4.2"
ethereumjs-block "^1.2.2"
ethereumjs-tx "^1.2.0"
ethereumjs-util "^5.1.5"
ethereumjs-vm "^2.3.4"
json-rpc-error "^2.0.0"
json-stable-stringify "^1.0.1"
promise-to-callback "^1.0.0"
readable-stream "^2.2.9"
request "^2.85.0"
semaphore "^1.0.3"
ws "^5.1.1"
xhr "^2.2.0"
xtend "^4.0.1"
"@portis/web3@^2.0.0-beta.23":
version "2.0.0-beta.23"
resolved "https://registry.yarnpkg.com/@portis/web3/-/web3-2.0.0-beta.23.tgz#873ef9c47405cbc7cffd0bbb58d5a887095a3410"
integrity sha512-rf1MkUvG9rtRdC/jNENit74X02ZtSClcC6/2dnyiNZHlufBAF+G5KUXVgrKZRDQSL/lGUCJL7dZYssltqDhXMg==
dependencies:
"@portis/web3-provider-engine" "1.0.9"
ethereumjs-util "5.2.0"
penpal "3.0.7"
"@svgr/babel-plugin-add-jsx-attribute@^4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz#dadcb6218503532d6884b210e7f3c502caaa44b1"
@ -1296,55 +1252,6 @@
dependencies:
"@types/ethereum-protocol" "*"
"@walletconnect/browser@^1.0.0-beta.11":
version "1.0.0-beta.11"
resolved "https://registry.yarnpkg.com/@walletconnect/browser/-/browser-1.0.0-beta.11.tgz#ca826e5c5253e59b492cb406b8eecc45bb33279e"
integrity sha512-JTpUvMOBF6G1D46AJIOJRhsRvDZ9GpOJIcOg5jXv95oExtCRqjlVhoEx/i4KiYrIudaFg4+gzI3uhL829aCqjA==
dependencies:
"@walletconnect/core" "^1.0.0-beta.11"
"@walletconnect/types" "^1.0.0-beta.10"
"@walletconnect/utils" "^1.0.0-beta.11"
"@walletconnect/core@^1.0.0-beta.11":
version "1.0.0-beta.11"
resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-1.0.0-beta.11.tgz#d83b17488cf0f3fff6818009d705081963ad9fbd"
integrity sha512-Lh51x4J9yXTS87C0YtNigVRo+3XggStSTH0VlJrwW7l7XFkBJwDCzYwwKyM5h7w91WGlrn6tSjnJl6nE4V2W/w==
dependencies:
"@walletconnect/types" "^1.0.0-beta.10"
"@walletconnect/utils" "^1.0.0-beta.11"
"@walletconnect/qrcode-modal@^1.0.0-beta.11":
version "1.0.0-beta.11"
resolved "https://registry.yarnpkg.com/@walletconnect/qrcode-modal/-/qrcode-modal-1.0.0-beta.11.tgz#0dd14b76a1897a95a55153c73697e1ae46860e3b"
integrity sha512-BbEv05Bp9n7pWoau1iwiyEggTujuFVKm+vC83aNPcsYNQ0uyxyRfhZkUhnIRg6ry1dsIqeyazBS/Q8/J8Q8PkQ==
dependencies:
"@walletconnect/types" "^1.0.0-beta.10"
qr-image "^3.2.0"
"@walletconnect/types@^1.0.0-beta.10":
version "1.0.0-beta.11"
resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-1.0.0-beta.11.tgz#e9e751268c73945255484adcbddaabda4a81dab4"
integrity sha512-LgO/R5WGUIwbn9Euzwbx2tWC/XtlFVtRR3ulSldsl5wUA+y04XlAcHsirp2b3ebuaaOJGcAnfFdwK3Qa7k5K5g==
"@walletconnect/utils@^1.0.0-beta.11":
version "1.0.0-beta.11"
resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-1.0.0-beta.11.tgz#25fadd17973086df8f10d625161c0c3cb1f61bc1"
integrity sha512-El+OVX4T+5xZcfCYunHECkPBcX6AKiuhwxzQ1ElQ9jnujEr6Z1+KP1DptLnAGMfvNKZ/dbJlqh8Y1K0wYV0hOQ==
dependencies:
"@walletconnect/types" "^1.0.0-beta.10"
lodash.isnumber "^3.0.3"
"@walletconnect/web3-subprovider@^1.0.0-beta.10":
version "1.0.0-beta.11"
resolved "https://registry.yarnpkg.com/@walletconnect/web3-subprovider/-/web3-subprovider-1.0.0-beta.11.tgz#7d459e70cf0d8e6bca4cc4997d40c693228446e7"
integrity sha512-0mRe45WkNKdZAidkIJBe3T91U7YpDLwPlno5B5SzwJZIVgrfLT2eU94shVHEUFsfYSP4J7d+FWiGpe3PqLJdEw==
dependencies:
"@walletconnect/browser" "^1.0.0-beta.11"
"@walletconnect/qrcode-modal" "^1.0.0-beta.11"
"@walletconnect/types" "^1.0.0-beta.10"
"@walletconnect/utils" "^1.0.0-beta.11"
web3-provider-engine "github:walletconnect/web3-provider-engine"
"@webassemblyjs/ast@1.7.11":
version "1.7.11"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.7.11.tgz#b988582cafbb2b095e8b556526f30c90d057cace"
@ -1922,11 +1829,6 @@ autoprefixer@^9.4.2:
postcss "^7.0.14"
postcss-value-parser "^3.3.1"
await-semaphore@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/await-semaphore/-/await-semaphore-0.1.3.tgz#2b88018cc8c28e06167ae1cdff02504f1f9688d3"
integrity sha512-d1W2aNSYcz/sxYO4pMGX9vq65qOTu0P800epMud+6cYYX0QcT7zyqcxec3VWzpgvdXo57UWmVbZpLMjX2m1I7Q==
aws-sign2@~0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
@ -2578,11 +2480,6 @@ babel-types@^6.0.0, babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.24.
lodash "^4.17.4"
to-fast-properties "^1.0.3"
babelify@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/babelify/-/babelify-10.0.0.tgz#fe73b1a22583f06680d8d072e25a1e0d1d1d7fb5"
integrity sha512-X40FaxyH7t3X+JFAKvb1H9wooWKLRCi8pg3m8poqtdZaIng+bjzp9RvKQCvRjF9isHiPkXspbbXT/zwXLtwgwg==
babelify@^7.3.0:
version "7.3.0"
resolved "https://registry.yarnpkg.com/babelify/-/babelify-7.3.0.tgz#aa56aede7067fd7bd549666ee16dc285087e88e5"
@ -2959,7 +2856,7 @@ browserslist@^3.2.6:
caniuse-lite "^1.0.30000844"
electron-to-chromium "^1.3.47"
browserslist@^4.0.0, browserslist@^4.3.4, browserslist@^4.3.5, browserslist@^4.5.2, browserslist@^4.5.4:
browserslist@^4.0.0, browserslist@^4.3.4, browserslist@^4.5.2, browserslist@^4.5.4:
version "4.5.4"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.5.4.tgz#166c4ecef3b51737a42436ea8002aeea466ea2c7"
integrity sha512-rAjx494LMjqKnMPhFkuLmLp8JWEX0o8ADTGeAbOqaF+XCvYLreZrG5uVjnPBlAQ8REZK4pzXGvp0bWgrFtKaag==
@ -2968,6 +2865,15 @@ browserslist@^4.0.0, browserslist@^4.3.4, browserslist@^4.3.5, browserslist@^4.5
electron-to-chromium "^1.3.122"
node-releases "^1.1.13"
browserslist@^4.3.5:
version "4.5.5"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.5.5.tgz#fe1a352330d2490d5735574c149a85bc18ef9b82"
integrity sha512-0QFO1r/2c792Ohkit5XI8Cm8pDtZxgNl2H6HU4mHrpYz7314pEYcsAVVatM0l/YmxPnEzh9VygXouj4gkFUTKA==
dependencies:
caniuse-lite "^1.0.30000960"
electron-to-chromium "^1.3.124"
node-releases "^1.1.14"
bs58@=4.0.1, bs58@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a"
@ -2996,11 +2902,6 @@ bser@^2.0.0:
dependencies:
node-int64 "^0.4.0"
btoa@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73"
integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==
buffer-alloc-unsafe@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
@ -3209,11 +3110,16 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30000918, caniuse-lite@^1.0.30000929, caniuse-lite@^1.0.30000955, caniuse-lite@^1.0.30000957:
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30000955, caniuse-lite@^1.0.30000957:
version "1.0.30000959"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000959.tgz#215d3455866da874179c6170202f0cc64f961cfd"
integrity sha512-6BvqmS0VLmY4sJCz6AbIJRQfcns8McDxi424y+3kmtisJeA9/5qslP+K8sqremDau7UU4WSsqdRP032JrqZY8Q==
caniuse-lite@^1.0.30000918, caniuse-lite@^1.0.30000929, caniuse-lite@^1.0.30000960:
version "1.0.30000960"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000960.tgz#ec48297037e5607f582f246ae7b12bee66a78999"
integrity sha512-7nK5qs17icQaX6V3/RYrJkOsZyRNnroA4+ZwxaKJzIKy+crIy0Mz5CBlLySd2SNV+4nbUZeqeNfiaEieUBu3aA==
capture-exit@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-1.2.0.tgz#1c5fcc489fd0ab00d4f1ac7ae1072e3173fbab6f"
@ -3407,7 +3313,7 @@ clone-deep@^2.0.1:
kind-of "^6.0.0"
shallow-clone "^1.0.0"
clone@2.1.2, clone@^2.0.0, clone@^2.1.1:
clone@2.1.2, clone@^2.0.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
@ -3884,11 +3790,6 @@ css-loader@1.0.0:
postcss-value-parser "^3.3.0"
source-list-map "^2.0.0"
css-mediaquery@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/css-mediaquery/-/css-mediaquery-0.1.2.tgz#6a2c37344928618631c54bd33cedd301da18bea0"
integrity sha1-aiw3NEkoYYYxxUvTPO3TAdoYvqA=
css-prefers-color-scheme@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-3.1.1.tgz#6f830a2714199d4f0d0d0bb8a27916ed65cff1f4"
@ -4548,7 +4449,7 @@ ee-first@1.1.1:
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
electron-to-chromium@^1.3.103, electron-to-chromium@^1.3.122, electron-to-chromium@^1.3.47:
electron-to-chromium@^1.3.103, electron-to-chromium@^1.3.122, electron-to-chromium@^1.3.124, electron-to-chromium@^1.3.47:
version "1.3.124"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.124.tgz#861fc0148748a11b3e5ccebdf8b795ff513fa11f"
integrity sha512-glecGr/kFdfeXUHOHAWvGcXrxNU+1wSO/t5B23tT1dtlvYB26GY8aHzZSWD7HqhqC800Lr+w/hQul6C5AF542w==
@ -4929,15 +4830,6 @@ eth-block-tracker@^3.0.0:
pify "^2.3.0"
tape "^4.6.3"
eth-block-tracker@^4.2.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/eth-block-tracker/-/eth-block-tracker-4.3.0.tgz#9a54bb8241510367003d1f183adfac05abfe0e87"
integrity sha512-QbBsNaGFbpozqzUnjhaR6Skej0485tApPoKMWAW6qpR6J3kY2lgQv47vQvv0MjayxgWXnYH48sjTm2gNRS0NXA==
dependencies:
eth-query "^2.1.0"
pify "^3.0.0"
safe-event-emitter "^1.0.1"
eth-ens-namehash@2.0.8:
version "2.0.8"
resolved "https://registry.yarnpkg.com/eth-ens-namehash/-/eth-ens-namehash-2.0.8.tgz#229ac46eca86d52e0c991e7cb2aef83ff0f68bcf"
@ -4946,18 +4838,6 @@ eth-ens-namehash@2.0.8:
idna-uts46-hx "^2.3.1"
js-sha3 "^0.5.7"
eth-json-rpc-filters@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/eth-json-rpc-filters/-/eth-json-rpc-filters-4.0.2.tgz#881ea459f3edd6b4f1889914f4d0e7d2deeb2032"
integrity sha512-936WGvom7gGMwVqqIdWyQZeXCk5i6yjAfd3oDcgfKG/jCNQnkB28qplviX2cEik/2onWvg8mpgy2D/eutv9xnw==
dependencies:
await-semaphore "^0.1.3"
eth-json-rpc-middleware "^4.0.0"
eth-query "^2.1.2"
json-rpc-engine "^5.0.0"
lodash.flatmap "^4.5.0"
safe-event-emitter "^1.0.1"
eth-json-rpc-infura@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/eth-json-rpc-infura/-/eth-json-rpc-infura-3.2.0.tgz#62c3f516b51351038c32a548704467cec113ca8f"
@ -4988,25 +4868,6 @@ eth-json-rpc-middleware@^1.5.0:
promise-to-callback "^1.0.0"
tape "^4.6.3"
eth-json-rpc-middleware@^4.0.0, eth-json-rpc-middleware@^4.1.1, "eth-json-rpc-middleware@github:radotzki/eth-json-rpc-middleware#patch-1":
version "4.1.2"
resolved "https://codeload.github.com/radotzki/eth-json-rpc-middleware/tar.gz/8acc43df4a8c0c7f6d212af9163d379211dad4c7"
dependencies:
btoa "^1.2.1"
clone "^2.1.1"
eth-query "^2.1.2"
eth-sig-util "^1.4.2"
ethereumjs-block "^1.6.0"
ethereumjs-tx "^1.3.3"
ethereumjs-util "^5.1.2"
ethereumjs-vm "2.2.2"
fetch-ponyfill "^4.0.0"
json-rpc-engine "^5.0.0"
json-rpc-error "^2.0.0"
json-stable-stringify "^1.0.1"
pify "^3.0.0"
safe-event-emitter "^1.0.1"
eth-lib@0.1.27, eth-lib@^0.1.26:
version "0.1.27"
resolved "https://registry.yarnpkg.com/eth-lib/-/eth-lib-0.1.27.tgz#f0b0fd144f865d2d6bf8257a40004f2e75ca1dd6"
@ -5095,21 +4956,16 @@ eth-tx-summary@^3.1.2:
ethereumjs-vm "^2.6.0"
through2 "^2.0.3"
ethereum-common@0.0.18, ethereum-common@^0.0.18:
version "0.0.18"
resolved "https://registry.yarnpkg.com/ethereum-common/-/ethereum-common-0.0.18.tgz#2fdc3576f232903358976eb39da783213ff9523f"
integrity sha1-L9w1dvIykDNYl26znaeDIT/5Uj8=
ethereum-common@0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/ethereum-common/-/ethereum-common-0.1.0.tgz#874dd0fae5e962a56c50ebf28efa6fe39492b0e7"
integrity sha1-h03Q+uXpYqVsUOvyjvpv45SSsOc=
ethereum-common@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/ethereum-common/-/ethereum-common-0.2.0.tgz#13bf966131cce1eeade62a1b434249bb4cb120ca"
integrity sha512-XOnAR/3rntJgbCdGhqdaLIxDLWKLmsZOGhHdBKadEr6gEnJLH52k93Ou+TUdFaPN3hJc3isBZBal3U/XZ15abA==
ethereum-common@^0.0.18:
version "0.0.18"
resolved "https://registry.yarnpkg.com/ethereum-common/-/ethereum-common-0.0.18.tgz#2fdc3576f232903358976eb39da783213ff9523f"
integrity sha1-L9w1dvIykDNYl26znaeDIT/5Uj8=
ethereum-types@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ethereum-types/-/ethereum-types-2.1.2.tgz#7398873e0eede1dd0956a134e1037032f6ed3c14"
@ -5164,17 +5020,6 @@ ethereumjs-block@^1.2.2, ethereumjs-block@^1.4.1, ethereumjs-block@^1.6.0:
ethereumjs-util "^5.0.0"
merkle-patricia-tree "^2.1.2"
ethereumjs-block@~1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/ethereumjs-block/-/ethereumjs-block-1.6.0.tgz#cded4962deaca1eef17372b4d290e84b35c84372"
integrity sha1-ze1JYt6soe7xc3K00pDoSzXIQ3I=
dependencies:
async "^2.0.1"
ethereum-common "0.0.18"
ethereumjs-tx "^1.2.2"
ethereumjs-util "^5.0.0"
merkle-patricia-tree "^2.1.2"
ethereumjs-block@~2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/ethereumjs-block/-/ethereumjs-block-2.2.0.tgz#8c6c3ab4a5eff0a16d9785fbeedbe643f4dbcbef"
@ -5204,17 +5049,6 @@ ethereumjs-tx@1.3.7, ethereumjs-tx@^1.1.1, ethereumjs-tx@^1.2.0, ethereumjs-tx@^
ethereum-common "^0.0.18"
ethereumjs-util "^5.0.0"
ethereumjs-util@4.5.0, ethereumjs-util@^4.3.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-4.5.0.tgz#3e9428b317eebda3d7260d854fddda954b1f1bc6"
integrity sha1-PpQosxfuvaPXJg2FT93alUsfG8Y=
dependencies:
bn.js "^4.8.0"
create-hash "^1.1.2"
keccakjs "^0.2.0"
rlp "^2.0.0"
secp256k1 "^3.0.1"
ethereumjs-util@5.2.0, ethereumjs-util@^5.0.0, ethereumjs-util@^5.0.1, ethereumjs-util@^5.1.1, ethereumjs-util@^5.1.2, ethereumjs-util@^5.1.3, ethereumjs-util@^5.1.5, ethereumjs-util@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz#3e0c0d1741471acf1036052d048623dee54ad642"
@ -5228,6 +5062,17 @@ ethereumjs-util@5.2.0, ethereumjs-util@^5.0.0, ethereumjs-util@^5.0.1, ethereumj
safe-buffer "^5.1.1"
secp256k1 "^3.0.1"
ethereumjs-util@^4.3.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-4.5.0.tgz#3e9428b317eebda3d7260d854fddda954b1f1bc6"
integrity sha1-PpQosxfuvaPXJg2FT93alUsfG8Y=
dependencies:
bn.js "^4.8.0"
create-hash "^1.1.2"
keccakjs "^0.2.0"
rlp "^2.0.0"
secp256k1 "^3.0.1"
ethereumjs-util@^6.0.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-6.1.0.tgz#e9c51e5549e8ebd757a339cc00f5380507e799c8"
@ -5241,22 +5086,6 @@ ethereumjs-util@^6.0.0:
safe-buffer "^5.1.1"
secp256k1 "^3.0.1"
ethereumjs-vm@2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/ethereumjs-vm/-/ethereumjs-vm-2.2.2.tgz#786317d2c63e547d936c1a468607510e70b4c262"
integrity sha512-sAus9UxYjUnA42G91Q1/hR7ff35IJRpcLrUfbaIH7V4cl8qKsNs3wqf3dHvtj3wRqy12ke2Wd0tYdARyGKdD6g==
dependencies:
async "^2.1.2"
async-eventemitter "^0.2.2"
ethereum-common "0.1.0"
ethereumjs-account "^2.0.3"
ethereumjs-block "~1.6.0"
ethereumjs-util "4.5.0"
fake-merkle-patricia-tree "^1.0.1"
functional-red-black-tree "^1.0.1"
merkle-patricia-tree "^2.1.2"
safe-buffer "^5.1.1"
ethereumjs-vm@2.6.0, ethereumjs-vm@^2.1.0, ethereumjs-vm@^2.3.4, ethereumjs-vm@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/ethereumjs-vm/-/ethereumjs-vm-2.6.0.tgz#76243ed8de031b408793ac33907fb3407fe400c6"
@ -5861,13 +5690,6 @@ form-data@~2.3.2:
combined-stream "^1.0.6"
mime-types "^2.1.12"
fortmatic@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/fortmatic/-/fortmatic-0.7.0.tgz#978fa01e56a0fb1e5fb167c8fb366e8f100b57cd"
integrity sha512-ZLHy7g3z9jKkfba7Ow8H7ZR0IbKAG7wUy062BKBMGrDFqtM2ii7n4eMRYNWcqvsZXZ3E26CxXEqIFI81ibTdLw==
dependencies:
"@babel/runtime" "7.3.4"
forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
@ -5964,7 +5786,15 @@ fsevents@1.2.4:
nan "^2.9.2"
node-pre-gyp "^0.10.0"
fsevents@^1.2.3, fsevents@^1.2.7:
fsevents@^1.2.3:
version "1.2.8"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.8.tgz#57ea5320f762cd4696e5e8e87120eccc8b11cacf"
integrity sha512-tPvHgPGB7m40CZ68xqFGkKuzN+RnpGmSV+hgeKxhRpbxdqKXUFJGC3yonBOLzQBcJyGpdZFDfCsdOC2KFsXzeA==
dependencies:
nan "^2.12.1"
node-pre-gyp "^0.12.0"
fsevents@^1.2.7:
version "1.2.7"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.7.tgz#4851b664a3783e52003b3c66eb0eee1074933aa4"
integrity sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==
@ -6650,11 +6480,6 @@ https-browserify@^1.0.0:
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
hyphenate-style-name@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz#097bb7fa0b8f1a9cf0bd5c734cf95899981a9b48"
integrity sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ==
i18next-browser-languagedetector@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-3.0.1.tgz#a47c43176e8412c91e808afb7c6eb5367649aa8e"
@ -7877,17 +7702,6 @@ json-rpc-engine@^3.4.0, json-rpc-engine@^3.6.0:
promise-to-callback "^1.0.0"
safe-event-emitter "^1.0.1"
json-rpc-engine@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/json-rpc-engine/-/json-rpc-engine-5.0.0.tgz#f048a5402f602604595f820229b71e8431206490"
integrity sha512-bxzuHwoP/U7xBIcqxP9QaOMgW7GDbBNU2TLNWfndRfHxwPLKkGMBfgtns0oDvgjMPJLCgyFZjrnVuSHW5GSC7A==
dependencies:
"@babel/preset-env" "^7.3.4"
async "^2.0.1"
babelify "^10.0.0"
promise-to-callback "^1.0.0"
safe-event-emitter "^1.0.1"
json-rpc-error@2.0.0, json-rpc-error@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/json-rpc-error/-/json-rpc-error-2.0.0.tgz#a7af9c202838b5e905c7250e547f1aff77258a02"
@ -8298,16 +8112,6 @@ lodash.clonedeep@^4.3.2:
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
lodash.flatmap@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.flatmap/-/lodash.flatmap-4.5.0.tgz#ef8cbf408f6e48268663345305c6acc0b778702e"
integrity sha1-74y/QI9uSCaGYzRTBcaswLd4cC4=
lodash.isnumber@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc"
integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=
lodash.memoize@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
@ -8486,13 +8290,6 @@ map-visit@^1.0.0:
dependencies:
object-visit "^1.0.0"
matchmediaquery@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/matchmediaquery/-/matchmediaquery-0.3.0.tgz#6f672bcdbc44de16825c6917fbcdcfb9b82607b1"
integrity sha512-u0dlv+VENJ+3YepvwSPBieuvnA6DWfaYa/ctwysAR13y4XLJNyt7bEVKzNj/Nvjo+50d88Pj+xL9xaSo6JmX/w==
dependencies:
css-mediaquery "^0.1.2"
math-random@^1.0.1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c"
@ -9103,13 +8900,36 @@ node-pre-gyp@^0.11.0:
semver "^5.3.0"
tar "^4"
node-releases@^1.1.13, node-releases@^1.1.3:
node-pre-gyp@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz#39ba4bb1439da030295f899e3b520b7785766149"
integrity sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==
dependencies:
detect-libc "^1.0.2"
mkdirp "^0.5.1"
needle "^2.2.1"
nopt "^4.0.1"
npm-packlist "^1.1.6"
npmlog "^4.0.2"
rc "^1.2.7"
rimraf "^2.6.1"
semver "^5.3.0"
tar "^4"
node-releases@^1.1.13:
version "1.1.14"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.14.tgz#f1f41c83cac82caebd6739e6313d56b3b09c9189"
integrity sha512-d58EpVZRhQE60kWiWUaaPlK9dyC4zg3ZoMcHcky2d4hDksyQj0rUozwInOl0C66mBsqo01Tuns8AvxnL5S7PKg==
dependencies:
semver "^5.3.0"
node-releases@^1.1.14, node-releases@^1.1.3:
version "1.1.15"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.15.tgz#9e76a73b0eca3bf7801addaa0e6ce90c795f2b9a"
integrity sha512-cKV097BQaZr8LTSRUa2+oc/aX5L8UkZtPQrMSTgiJEeaW7ymTDCoRaGCoaTqk0lqnalcoSHu4wjSl0Cmj2+bMw==
dependencies:
semver "^5.3.0"
node-sass@^4.11.0:
version "4.11.0"
resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.11.0.tgz#183faec398e9cbe93ba43362e2768ca988a6369a"
@ -9742,11 +9562,6 @@ pend@~1.2.0:
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA=
penpal@3.0.7:
version "3.0.7"
resolved "https://registry.yarnpkg.com/penpal/-/penpal-3.0.7.tgz#d252711ed93b30f1d867eb82342785b3a95f5f75"
integrity sha512-WSXiq5HnEvzvY05SHhaXcsviUmCvh4Ze8AiIZzvmdzaaYAAx4rx8c6Xq6+MaVDG/Nfve3VmGD8HyRP3CkPvPbQ==
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
@ -10756,11 +10571,6 @@ q@^1.1.2:
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
qr-image@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/qr-image/-/qr-image-3.2.0.tgz#9fa8295beae50c4a149cf9f909a1db464a8672e8"
integrity sha1-n6gpW+rlDEoUnPn5CaHbRkqGcug=
qs@6.5.2, qs@~6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
@ -10951,15 +10761,6 @@ react-redux@^5.0.7:
react-is "^16.6.0"
react-lifecycles-compat "^3.0.0"
react-responsive@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/react-responsive/-/react-responsive-5.0.0.tgz#b6e559b2ce8c4d82b0d47a7a78a74645b977e4af"
integrity sha512-oEimZ0FTCC3/pjGDEBHOz06nWbBNDIbMGOdRYp6K9SBUmrqgNAX77hTiqvmRQeLyI97zz4F4kiaFRxFspDxE+w==
dependencies:
hyphenate-style-name "^1.0.0"
matchmediaquery "^0.3.0"
prop-types "^15.6.1"
react-router-dom@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.0.0.tgz#542a9b86af269a37f0b87218c4c25ea8dcf0c073"
@ -12821,15 +12622,6 @@ tr46@^1.0.1:
dependencies:
punycode "^2.1.0"
trezor-connect@^7.0.2:
version "7.0.2"
resolved "https://registry.yarnpkg.com/trezor-connect/-/trezor-connect-7.0.2.tgz#8b1b0d1b3b6dc6564bc3d22fe8f54ba826f2a242"
integrity sha512-KAFOqxEHHaFvrG8NGLFlM/QxHcwIa3gwfXcgTjCYM0g0zRpwIQBwe35AKsjAQO5yiTJQGa0Cu5MZufGJRGYjjw==
dependencies:
"@babel/runtime" "^7.3.1"
events "^3.0.0"
whatwg-fetch "^3.0.0"
trim-newlines@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
@ -13753,33 +13545,6 @@ web3-provider-engine@14.1.0:
xhr "^2.2.0"
xtend "^4.0.1"
"web3-provider-engine@github:walletconnect/web3-provider-engine":
version "15.0.0"
resolved "https://codeload.github.com/walletconnect/web3-provider-engine/tar.gz/48080be33018e8f24e4ba80ccb76787a33ed3e9b"
dependencies:
async "^2.5.0"
backoff "^2.5.0"
clone "^2.0.0"
cross-fetch "^2.1.0"
eth-block-tracker "^4.2.0"
eth-json-rpc-filters "^4.0.2"
eth-json-rpc-infura "^3.1.0"
eth-json-rpc-middleware "^4.1.1"
eth-sig-util "^1.4.2"
ethereumjs-block "^1.2.2"
ethereumjs-tx "^1.2.0"
ethereumjs-util "^5.1.5"
ethereumjs-vm "^2.3.4"
json-rpc-error "^2.0.0"
json-stable-stringify "^1.0.1"
promise-to-callback "^1.0.0"
readable-stream "^2.2.9"
request "^2.85.0"
semaphore "^1.0.3"
ws "^5.1.1"
xhr "^2.2.0"
xtend "^4.0.1"
web3-providers-http@1.0.0-beta.35:
version "1.0.0-beta.35"
resolved "https://registry.yarnpkg.com/web3-providers-http/-/web3-providers-http-1.0.0-beta.35.tgz#92059d9d6de6e9f82f4fae30b743efd841afc1e1"
@ -13819,16 +13584,12 @@ web3-providers@1.0.0-beta.52:
websocket "^1.0.28"
xhr2-cookies "1.1.0"
web3-react@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/web3-react/-/web3-react-4.0.0.tgz#d364c96f0c3ac615129490efe3ad8526e415b022"
integrity sha512-lcUXlv1m+eO1nesMFH7faNpVpTTiWzB+9J3TNPjibndtlqRw91EzDLO4uS1LK053aYUIK1dWikANjyX6GM6f9w==
web3-react@^5.0.4:
version "5.0.4"
resolved "https://registry.yarnpkg.com/web3-react/-/web3-react-5.0.4.tgz#6006ee51c17255707f73d927f16eb48857957034"
integrity sha512-K9Bz+CojWOt3Fiya9SRkYmDsTPjT4vuEs5JQCfEyWH9wWVrXJgGMQxglq5RNfRWBhqgN0PmDMLeYiERQcNt/6g==
dependencies:
"@0x/subproviders" "^4.0.5"
"@portis/web3" "^2.0.0-beta.23"
"@walletconnect/web3-subprovider" "^1.0.0-beta.10"
fortmatic "^0.7.0"
trezor-connect "^7.0.2"
web3-shh@1.0.0-beta.35:
version "1.0.0-beta.35"
@ -14085,7 +13846,7 @@ whatwg-fetch@2.0.4:
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f"
integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==
whatwg-fetch@3.0.0, whatwg-fetch@>=0.10.0, whatwg-fetch@^3.0.0:
whatwg-fetch@3.0.0, whatwg-fetch@>=0.10.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb"
integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==