Commit 4e413915 authored by Noah Zinsmeister's avatar Noah Zinsmeister Committed by GitHub

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
parent f855706f
REACT_APP_NETWORK_ID="1"
REACT_APP_NETWORK_URL=""
REACT_APP_NETWORK_NAME="Main Ethereum Network"
\ No newline at end of file
......@@ -11,6 +11,7 @@
# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
......
......@@ -16,24 +16,26 @@ 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
```bash
yarn start:rinkeby
```
Rename `.env.example` to `.env` and fill in the appropriate variables.
###### Running locally on other testnet
### Run
```bash
REACT_APP_NETWORK_ID=2 REACT_APP_NETWORK='Ropsten Test Network' yarn start
yarn start
# or
yarn start:rinkeby
```
More robust support for other testnets is in the works!
## Contributions
**Please open all pull requests against the `beta` branch.** CI checks will run against all PRs. To ensure that your changes will pass, run `yarn check:all` before pushing. If this command fails, you can try to automatically fix problems with `yarn fix:all`, or do it manually.
# 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"
......
This diff is collapsed.
[{"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}]
\ No newline at end of file
[
{
"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;
......
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'
import './address-input-panel.scss'
class AddressInputPanel extends Component {
static propTypes = {
title: PropTypes.string,
onChange: PropTypes.func,
value: PropTypes.string,
errorMessage: PropTypes.string
}
// import QrCode from '../QrCode' // commented out pending further review
static defaultProps = {
onChange() {},
value: ''
}
import './address-input-panel.scss'
render() {
const { t, title, onChange, value, errorMessage } = this.props
export default function AddressInputPanel({ title, onChange = () => {}, value = '', errorMessage }) {
const { t } = useTranslation()
return (
<div className="currency-input-panel">
<div
className={c('currency-input-panel__container address-input-panel__recipient-row', {
className={classnames('currency-input-panel__container address-input-panel__recipient-row', {
'currency-input-panel__container--error': errorMessage
})}
>
......@@ -37,7 +25,7 @@ class AddressInputPanel extends Component {
<div className="currency-input-panel__input-row">
<input
type="text"
className={c('address-input-panel__input', {
className={classnames('address-input-panel__input', {
'address-input-panel__input--error': errorMessage
})}
placeholder="0x1234..."
......@@ -46,13 +34,12 @@ class AddressInputPanel extends Component {
/>
</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
This diff is collapsed.
......@@ -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;
}
}
......
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 = []
function BaseBlockingWarning({ title, description, children }) {
return (
<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>
)
}
const correctNetworkId = process.env.REACT_APP_NETWORK_ID || 1
const correctNetwork = process.env.REACT_APP_NETWORK || 'Main Ethereum Network'
function BlockingWarning() {
const { t } = useTranslation()
const wrongNetwork = networkId !== correctNetworkId
const correctNetwork = process.env.REACT_APP_NETWORK_NAME || 'Main Ethereum Network'
const context = useWeb3Context()
if (wrongNetwork && initialized) {
content = [
<div key="warning-title">{t('wrongNetwork')}</div>,
<div key="warning-desc" className="header__dialog__description">
{t('switchNetwork', { correctNetwork })}
</div>
]
if (context.error && context.error.code === Connector.errorCodes.UNSUPPORTED_NETWORK) {
return <BaseBlockingWarning title={t('wrongNetwork')} description={t('switchNetwork', { correctNetwork })} />
}
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>,
// 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()
? [
{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>
</BaseBlockingWarning>
)
}
return null
}
function Header(props) {
export default function Header() {
const context = useWeb3Context()
return (
<div className="header">
<BlockingWarning {...props} />
<div
className={classnames('header__top', {
'header--inactive': !props.isConnected
})}
>
<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 />
<Web3Status isConnected={!!(context.active && context.account)} />
</div>
</div>
)
}
Header.propTypes = {
currentAddress: PropTypes.string,
isConnected: PropTypes.bool.isRequired
}
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))
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])
useBodyKeyDown('ArrowRight', navigateRight)
useBodyKeyDown('ArrowLeft', navigateLeft)
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>
<>
<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">
......@@ -49,9 +69,8 @@ class NavigationTabs extends Component {
{t('betaWarning')}
</div>
)}
</div>
</>
)
}
}
export default withRouter(
......@@ -62,5 +81,5 @@ export default withRouter(
dispatch => ({
dismissBetaMessage: () => dispatch(dismissBetaMessage())
})
)(withTranslation()(NavigationTabs))
)(NavigationTabs)
)
......@@ -2,6 +2,7 @@
.beta-message {
@extend %row-nowrap;
cursor: pointer;
flex: 1 0 auto;
align-items: center;
position: relative;
......@@ -23,3 +24,35 @@
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;
}
}
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: ''
}
@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,
......
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,10 +283,10 @@ 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 => {
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())
......@@ -332,14 +295,13 @@ export const sync = () => async (dispatch, getState) => {
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,
......@@ -348,6 +310,7 @@ export const sync = () => async (dispatch, getState) => {
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)
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
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}>
<Web3Provider connectors={connectors} libraryName="ethers.js">
<App />
</Provider>
</Suspense>,
</Web3Provider>
</Provider>,
document.getElementById('root')
)
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
function App({ initialized, setAddresses, updateNetwork, updateAccount, initialize, startWatching }) {
const context = useWeb3Context()
componentWillUpdate() {
const { web3, setAddresses } = this.props
// 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')
}
})
}, [])
if (this.hasSetNetworkId || !web3 || !web3.eth || !web3.eth.net || !web3.eth.net.getId) {
return
// 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])
web3.eth.net.getId((err, networkId) => {
if (!err && !this.hasSetNetworkId) {
setAddresses(networkId)
this.hasSetNetworkId = true
// 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])
render() {
if (!this.props.initialized) {
return <noscript />
// 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>
{/* this is an intermediate state before infura is set */}
{initialized && (!context.error || context.error.code === InjectedConnector.errorCodes.UNLOCK_REQUIRED) && (
<div className="app__wrapper">
<div className="body">
<div className="body__content">
<BrowserRouter>
<NavigationTabs />
<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" />
<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>
</div>
</BrowserRouter>
</div>
</div>
</div>
)}
</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;
}
}
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}>
<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">
<AddLiquidityButton callOnClick={this.onAddLiquidity} isValid={isValid} />
</div>
</>
)
}
}
function AddLiquidityButton({ callOnClick, isValid }) {
const { t } = useTranslation()
const context = useWeb3Context()
const isActive = context.active && context.account
return (
<button
className={classnames('pool__cta-btn', {
'swap--inactive': !this.props.isConnected,
'pool__cta-btn--inactive': !isValid
'pool__cta-btn--inactive': !isActive
})}
disabled={!isValid}
onClick={this.onAddLiquidity}
onClick={callOnClick}
>
{t('addLiquidity')}
</button>
</div>
</div>
]
}
)
}
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,
......
This diff is collapsed.
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])
useBodyKeyDown('ArrowDown', navigateRight, isShowingModal)
useBodyKeyDown('ArrowUp', navigateLeft, isShowingModal)
return (
<Modal onClose={() => this.setState({ isShowingModal: false })}>
<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)
}}
>
<CSSTransitionGroup
transitionName="pool-modal"
transitionAppear={true}
......@@ -52,32 +78,26 @@ class ModeSelector extends Component {
transitionEnterTimeout={200}
>
<div className="pool-modal">
<div className="pool-modal__item" onClick={() => this.changeView(ADD)}>
{this.props.t('addLiquidity')}
</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>
{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>
</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>
)
}
}
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">
<RemoveLiquidityButton callOnClick={this.onRemoveLiquidity} isValid={isValid} />
</div>
</>
)
}
}
function RemoveLiquidityButton({ callOnClick, isValid }) {
const { t } = useTranslation()
const context = useWeb3Context()
const isActive = context.active && context.account
return (
<button
className={classnames('pool__cta-btn', {
'swap--inactive': !isConnected,
'pool__cta-btn--inactive': !isValid
'pool__cta-btn--inactive': !isActive
})}
disabled={!isValid}
onClick={this.onRemoveLiquidity}
onClick={callOnClick}
>
{t('removeLiquidity')}
</button>
</div>
</div>
]
}
)
}
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,
......
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>
<>
<ModeSelector />
<Switch>
<Route exact path="/add-liquidity" component={AddLiquidity} />
<Route exact path="/remove-liquidity" component={RemoveLiquidity} />
<Route exact path="/create-exchange/:tokenAddress?" component={CreateExchange} />
<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>
</div>
</>
)
}
}
export default Pool
......@@ -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,21 +752,7 @@ 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 : ''}
......@@ -812,26 +799,34 @@ class Send extends Component {
{this.renderExchangeRate()}
{this.renderSummary(inputError, outputError)}
<div className="swap__cta-container">
<SendButton callOnClick={this.onSend} isValid={isValid} />
</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': !this.props.isConnected
'swap--inactive': !isActive
})}
disabled={!isValid}
onClick={this.onSend}
onClick={callOnClick}
>
{t('send')}
</button>
</div>
</div>
</div>
)
}
}
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,21 +734,7 @@ 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 : ''}
......@@ -782,26 +771,33 @@ class Swap extends Component {
{this.renderExchangeRate()}
{this.renderSummary(inputError, outputError)}
<div className="swap__cta-container">
<SwapButton callOnClick={this.onSwap} isValid={isValid} />
</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': !this.props.isConnected
})}
className={classnames('swap__cta-btn', { 'swap--inactive': !isActive })}
disabled={!isValid}
onClick={this.onSwap}
onClick={callOnClick}
>
{t('swap')}
</button>
</div>
</div>
</div>
)
}
}
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 {
......
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 }))
}
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment