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 @@ ...@@ -11,6 +11,7 @@
# misc # misc
.DS_Store .DS_Store
.env
.env.local .env.local
.env.development.local .env.development.local
.env.test.local .env.test.local
......
...@@ -16,24 +16,26 @@ This an an open source interface for Uniswap - a protocol for decentralized exch ...@@ -16,24 +16,26 @@ This an an open source interface for Uniswap - a protocol for decentralized exch
## To Start Development ## To Start Development
###### Installing dependencies ### Install Dependencies
```bash ```bash
yarn yarn
``` ```
###### Running locally on Rinkeby ### Configure Environment
```bash Rename `.env.example` to `.env` and fill in the appropriate variables.
yarn start:rinkeby
```
###### Running locally on other testnet ### Run
```bash ```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 ## 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. **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]] [[redirects]]
from = "/*" from = "/*"
to = "/index.html" to = "/index.html"
......
...@@ -20,7 +20,6 @@ ...@@ -20,7 +20,6 @@
"react-ga": "^2.5.7", "react-ga": "^2.5.7",
"react-i18next": "^10.7.0", "react-i18next": "^10.7.0",
"react-redux": "^5.0.7", "react-redux": "^5.0.7",
"react-responsive": "^5.0.0",
"react-router-dom": "^5.0.0", "react-router-dom": "^5.0.0",
"react-scripts": "^2.1.8", "react-scripts": "^2.1.8",
"react-transition-group": "1.x", "react-transition-group": "1.x",
...@@ -29,13 +28,13 @@ ...@@ -29,13 +28,13 @@
"redux-thunk": "^2.2.0", "redux-thunk": "^2.2.0",
"ua-parser-js": "^0.7.18", "ua-parser-js": "^0.7.18",
"web3": "1.0.0-beta.52", "web3": "1.0.0-beta.52",
"web3-react": "^4.0.0" "web3-react": "^5.0.4"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
"start:rinkeby": "REACT_APP_NETWORK_ID=4 REACT_APP_NETWORK='Rinkeby Test Network' yarn start",
"build": "react-scripts build", "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", "test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"lint:base": "yarn eslint './src/**/*.{js,jsx}'", "lint:base": "yarn eslint './src/**/*.{js,jsx}'",
......
...@@ -70,6 +70,7 @@ ...@@ -70,6 +70,7 @@
"invalidDecimals": "Invalid decimals", "invalidDecimals": "Invalid decimals",
"tokenAddress": "Token Address", "tokenAddress": "Token Address",
"label": "Label", "label": "Label",
"symbol": "Symbol",
"decimals": "Decimals", "decimals": "Decimals",
"enterTokenCont": "Enter a token address to continue" "enterTokenCont": "Enter a token address to continue"
} }
...@@ -3,35 +3,16 @@ ...@@ -3,35 +3,16 @@
"constant": true, "constant": true,
"inputs": [], "inputs": [],
"name": "name", "name": "name",
"outputs": [ "outputs": [{ "name": "", "type": "string" }],
{
"name": "",
"type": "string"
}
],
"payable": false, "payable": false,
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{ {
"constant": false, "constant": false,
"inputs": [ "inputs": [{ "name": "_spender", "type": "address" }, { "name": "_value", "type": "uint256" }],
{
"name": "_spender",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "approve", "name": "approve",
"outputs": [ "outputs": [{ "name": "", "type": "bool" }],
{
"name": "",
"type": "bool"
}
],
"payable": false, "payable": false,
"stateMutability": "nonpayable", "stateMutability": "nonpayable",
"type": "function" "type": "function"
...@@ -40,12 +21,7 @@ ...@@ -40,12 +21,7 @@
"constant": true, "constant": true,
"inputs": [], "inputs": [],
"name": "totalSupply", "name": "totalSupply",
"outputs": [ "outputs": [{ "name": "", "type": "uint256" }],
{
"name": "",
"type": "uint256"
}
],
"payable": false, "payable": false,
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
...@@ -53,26 +29,12 @@ ...@@ -53,26 +29,12 @@
{ {
"constant": false, "constant": false,
"inputs": [ "inputs": [
{ { "name": "_from", "type": "address" },
"name": "_from", { "name": "_to", "type": "address" },
"type": "address" { "name": "_value", "type": "uint256" }
},
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
], ],
"name": "transferFrom", "name": "transferFrom",
"outputs": [ "outputs": [{ "name": "", "type": "bool" }],
{
"name": "",
"type": "bool"
}
],
"payable": false, "payable": false,
"stateMutability": "nonpayable", "stateMutability": "nonpayable",
"type": "function" "type": "function"
...@@ -81,31 +43,16 @@ ...@@ -81,31 +43,16 @@
"constant": true, "constant": true,
"inputs": [], "inputs": [],
"name": "decimals", "name": "decimals",
"outputs": [ "outputs": [{ "name": "", "type": "uint8" }],
{
"name": "",
"type": "uint8"
}
],
"payable": false, "payable": false,
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{ {
"constant": true, "constant": true,
"inputs": [ "inputs": [{ "name": "_owner", "type": "address" }],
{
"name": "_owner",
"type": "address"
}
],
"name": "balanceOf", "name": "balanceOf",
"outputs": [ "outputs": [{ "name": "balance", "type": "uint256" }],
{
"name": "balance",
"type": "uint256"
}
],
"payable": false, "payable": false,
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
...@@ -114,85 +61,36 @@ ...@@ -114,85 +61,36 @@
"constant": true, "constant": true,
"inputs": [], "inputs": [],
"name": "symbol", "name": "symbol",
"outputs": [ "outputs": [{ "name": "", "type": "string" }],
{
"name": "",
"type": "string"
}
],
"payable": false, "payable": false,
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{ {
"constant": false, "constant": false,
"inputs": [ "inputs": [{ "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" }],
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "transfer", "name": "transfer",
"outputs": [ "outputs": [{ "name": "", "type": "bool" }],
{
"name": "",
"type": "bool"
}
],
"payable": false, "payable": false,
"stateMutability": "nonpayable", "stateMutability": "nonpayable",
"type": "function" "type": "function"
}, },
{ {
"constant": true, "constant": true,
"inputs": [ "inputs": [{ "name": "_owner", "type": "address" }, { "name": "_spender", "type": "address" }],
{
"name": "_owner",
"type": "address"
},
{
"name": "_spender",
"type": "address"
}
],
"name": "allowance", "name": "allowance",
"outputs": [ "outputs": [{ "name": "", "type": "uint256" }],
{
"name": "",
"type": "uint256"
}
],
"payable": false, "payable": false,
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{ { "payable": true, "stateMutability": "payable", "type": "fallback" },
"payable": true,
"stateMutability": "payable",
"type": "fallback"
},
{ {
"anonymous": false, "anonymous": false,
"inputs": [ "inputs": [
{ { "indexed": true, "name": "owner", "type": "address" },
"indexed": true, { "indexed": true, "name": "spender", "type": "address" },
"name": "owner", { "indexed": false, "name": "value", "type": "uint256" }
"type": "address"
},
{
"indexed": true,
"name": "spender",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
], ],
"name": "Approval", "name": "Approval",
"type": "event" "type": "event"
...@@ -200,21 +98,9 @@ ...@@ -200,21 +98,9 @@
{ {
"anonymous": false, "anonymous": false,
"inputs": [ "inputs": [
{ { "indexed": true, "name": "from", "type": "address" },
"indexed": true, { "indexed": true, "name": "to", "type": "address" },
"name": "from", { "indexed": false, "name": "value", "type": "uint256" }
"type": "address"
},
{
"indexed": true,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
], ],
"name": "Transfer", "name": "Transfer",
"type": "event" "type": "event"
......
...@@ -3,35 +3,16 @@ ...@@ -3,35 +3,16 @@
"constant": true, "constant": true,
"inputs": [], "inputs": [],
"name": "name", "name": "name",
"outputs": [ "outputs": [{ "name": "", "type": "string" }],
{
"name": "",
"type": "string"
}
],
"payable": false, "payable": false,
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{ {
"constant": false, "constant": false,
"inputs": [ "inputs": [{ "name": "_spender", "type": "address" }, { "name": "_value", "type": "uint256" }],
{
"name": "_spender",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "approve", "name": "approve",
"outputs": [ "outputs": [{ "name": "", "type": "bool" }],
{
"name": "",
"type": "bool"
}
],
"payable": false, "payable": false,
"stateMutability": "nonpayable", "stateMutability": "nonpayable",
"type": "function" "type": "function"
...@@ -40,12 +21,7 @@ ...@@ -40,12 +21,7 @@
"constant": true, "constant": true,
"inputs": [], "inputs": [],
"name": "totalSupply", "name": "totalSupply",
"outputs": [ "outputs": [{ "name": "", "type": "uint256" }],
{
"name": "",
"type": "uint256"
}
],
"payable": false, "payable": false,
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
...@@ -53,26 +29,12 @@ ...@@ -53,26 +29,12 @@
{ {
"constant": false, "constant": false,
"inputs": [ "inputs": [
{ { "name": "_from", "type": "address" },
"name": "_from", { "name": "_to", "type": "address" },
"type": "address" { "name": "_value", "type": "uint256" }
},
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
], ],
"name": "transferFrom", "name": "transferFrom",
"outputs": [ "outputs": [{ "name": "", "type": "bool" }],
{
"name": "",
"type": "bool"
}
],
"payable": false, "payable": false,
"stateMutability": "nonpayable", "stateMutability": "nonpayable",
"type": "function" "type": "function"
...@@ -81,31 +43,16 @@ ...@@ -81,31 +43,16 @@
"constant": true, "constant": true,
"inputs": [], "inputs": [],
"name": "decimals", "name": "decimals",
"outputs": [ "outputs": [{ "name": "", "type": "uint8" }],
{
"name": "",
"type": "uint8"
}
],
"payable": false, "payable": false,
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{ {
"constant": true, "constant": true,
"inputs": [ "inputs": [{ "name": "_owner", "type": "address" }],
{
"name": "_owner",
"type": "address"
}
],
"name": "balanceOf", "name": "balanceOf",
"outputs": [ "outputs": [{ "name": "balance", "type": "uint256" }],
{
"name": "balance",
"type": "uint256"
}
],
"payable": false, "payable": false,
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
...@@ -114,85 +61,36 @@ ...@@ -114,85 +61,36 @@
"constant": true, "constant": true,
"inputs": [], "inputs": [],
"name": "symbol", "name": "symbol",
"outputs": [ "outputs": [{ "name": "", "type": "bytes32" }],
{
"name": "",
"type": "bytes32"
}
],
"payable": false, "payable": false,
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{ {
"constant": false, "constant": false,
"inputs": [ "inputs": [{ "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" }],
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "transfer", "name": "transfer",
"outputs": [ "outputs": [{ "name": "", "type": "bool" }],
{
"name": "",
"type": "bool"
}
],
"payable": false, "payable": false,
"stateMutability": "nonpayable", "stateMutability": "nonpayable",
"type": "function" "type": "function"
}, },
{ {
"constant": true, "constant": true,
"inputs": [ "inputs": [{ "name": "_owner", "type": "address" }, { "name": "_spender", "type": "address" }],
{
"name": "_owner",
"type": "address"
},
{
"name": "_spender",
"type": "address"
}
],
"name": "allowance", "name": "allowance",
"outputs": [ "outputs": [{ "name": "", "type": "uint256" }],
{
"name": "",
"type": "uint256"
}
],
"payable": false, "payable": false,
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{ { "payable": true, "stateMutability": "payable", "type": "fallback" },
"payable": true,
"stateMutability": "payable",
"type": "fallback"
},
{ {
"anonymous": false, "anonymous": false,
"inputs": [ "inputs": [
{ { "indexed": true, "name": "owner", "type": "address" },
"indexed": true, { "indexed": true, "name": "spender", "type": "address" },
"name": "owner", { "indexed": false, "name": "value", "type": "uint256" }
"type": "address"
},
{
"indexed": true,
"name": "spender",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
], ],
"name": "Approval", "name": "Approval",
"type": "event" "type": "event"
...@@ -200,21 +98,9 @@ ...@@ -200,21 +98,9 @@
{ {
"anonymous": false, "anonymous": false,
"inputs": [ "inputs": [
{ { "indexed": true, "name": "from", "type": "address" },
"indexed": true, { "indexed": true, "name": "to", "type": "address" },
"name": "from", { "indexed": false, "name": "value", "type": "uint256" }
"type": "address"
},
{
"indexed": true,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
], ],
"name": "Transfer", "name": "Transfer",
"type": "event" "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 @@ ...@@ -33,7 +33,6 @@
} }
&__qr-container { &__qr-container {
display: none;
padding: 10px; padding: 10px;
background: $concrete-gray; background: $concrete-gray;
border: 1px solid $mercury-gray; border: 1px solid $mercury-gray;
......
import React, { Component } from 'react' import React from 'react'
import PropTypes from 'prop-types' import classnames from 'classnames'
import c from 'classnames' import { useTranslation } from 'react-i18next'
import QrCode from '../QrCode' // import QrCode from '../QrCode' // commented out pending further review
import './address-input-panel.scss'
class AddressInputPanel extends Component {
static propTypes = {
title: PropTypes.string,
onChange: PropTypes.func,
value: PropTypes.string,
errorMessage: PropTypes.string
}
static defaultProps = { import './address-input-panel.scss'
onChange() {},
value: ''
}
render() { export default function AddressInputPanel({ title, onChange = () => {}, value = '', errorMessage }) {
const { t, title, onChange, value, errorMessage } = this.props const { t } = useTranslation()
return ( return (
<div className="currency-input-panel"> <div className="currency-input-panel">
<div <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 'currency-input-panel__container--error': errorMessage
})} })}
> >
<div className="address-input-panel__input-container"> <div className="address-input-panel__input-container">
<div className="currency-input-panel__label-row"> <div className="currency-input-panel__label-row">
<div className="currency-input-panel__label-container"> <div className="currency-input-panel__label-container">
<span className="currency-input-panel__label">{title || t('recipientAddress')}</span> <span className="currency-input-panel__label">{title || t('recipientAddress')}</span>
</div>
</div>
<div 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}
/>
</div> </div>
</div> </div>
<div className="address-input-panel__qr-container"> <div className="currency-input-panel__input-row">
<QrCode onValueReceived={value => onChange(value)} /> <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>
</div> </div>
{/* commented out pending further review
<div className="address-input-panel__qr-container">
<QrCode onValueReceived={value => onChange(value)} />
</div>
*/}
</div> </div>
) </div>
} )
} }
export default AddressInputPanel
This diff is collapsed.
...@@ -2,6 +2,11 @@ ...@@ -2,6 +2,11 @@
.header { .header {
@extend %col-nowrap; @extend %col-nowrap;
display: block;
&__no-decoration {
text-decoration: none;
}
&__top { &__top {
@extend %row-nowrap; @extend %row-nowrap;
...@@ -27,10 +32,6 @@ ...@@ -27,10 +32,6 @@
margin-bottom: 1rem; margin-bottom: 1rem;
} }
&--inactive {
opacity: 0.5;
}
&__dialog { &__dialog {
@extend %col-nowrap; @extend %col-nowrap;
border-radius: 0.875rem; border-radius: 0.875rem;
...@@ -47,7 +48,7 @@ ...@@ -47,7 +48,7 @@
} }
&--disconnected { &--disconnected {
display: inherit; display: block;
} }
} }
......
import React, { Component } from 'react' import React from 'react'
import PropTypes from 'prop-types' import { useTranslation } from 'react-i18next'
import { connect } from 'react-redux'
import classnames from 'classnames' import classnames from 'classnames'
import { useWeb3Context, Connectors } from 'web3-react'
import UAParser from 'ua-parser-js' import UAParser from 'ua-parser-js'
import { withTranslation } from 'react-i18next'
import Logo from '../Logo' import Logo from '../Logo'
import CoinbaseWalletLogo from '../../assets/images/coinbase-wallet-logo.png' import CoinbaseWalletLogo from '../../assets/images/coinbase-wallet-logo.png'
import TrustLogo from '../../assets/images/trust-wallet-logo.svg' import TrustLogo from '../../assets/images/trust-wallet-logo.svg'
...@@ -13,6 +13,8 @@ import Web3Status from '../Web3Status' ...@@ -13,6 +13,8 @@ import Web3Status from '../Web3Status'
import './header.scss' import './header.scss'
const { Connector, InjectedConnector } = Connectors
const links = { const links = {
coinbaseWallet: { coinbaseWallet: {
android: 'https://play.google.com/store/apps/details?id=org.toshi', android: 'https://play.google.com/store/apps/details?id=org.toshi',
...@@ -33,8 +35,6 @@ const links = { ...@@ -33,8 +35,6 @@ const links = {
} }
} }
const ua = new UAParser(window.navigator.userAgent)
function getTrustLink() { function getTrustLink() {
const os = ua.getOS() const os = ua.getOS()
...@@ -73,99 +73,102 @@ function getMetamaskLink() { ...@@ -73,99 +73,102 @@ function getMetamaskLink() {
return links.metamask.chrome return links.metamask.chrome
} }
const ua = new UAParser(window.navigator.userAgent)
function isMobile() { function isMobile() {
return ua.getDevice().type === 'mobile' return ua.getDevice().type === 'mobile'
} }
class BlockingWarning extends Component { function BaseBlockingWarning({ title, description, children }) {
render() { return (
const { t, isConnected, initialized, networkId } = this.props <div
let content = [] 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 function BlockingWarning() {
const correctNetwork = process.env.REACT_APP_NETWORK || 'Main Ethereum Network' const { t } = useTranslation()
const wrongNetwork = networkId !== correctNetworkId const correctNetwork = process.env.REACT_APP_NETWORK_NAME || 'Main Ethereum Network'
const context = useWeb3Context()
if (wrongNetwork && initialized) { if (context.error && context.error.code === Connector.errorCodes.UNSUPPORTED_NETWORK) {
content = [ return <BaseBlockingWarning title={t('wrongNetwork')} description={t('switchNetwork', { correctNetwork })} />
<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>
]
}
// 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 ( return (
<div <BaseBlockingWarning
className={classnames('header__dialog', { title={t('noWallet')}
'header__dialog--disconnected': (!isConnected || wrongNetwork) && initialized description={isMobile() ? t('installWeb3MobileBrowser') : t('installMetamask')}
})}
> >
{content} <div key="warning-logos" className="header__download">
</div> {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
} }
function Header(props) { export default function Header() {
const context = useWeb3Context()
return ( return (
<div className="header"> <div className="header">
<BlockingWarning {...props} /> <BlockingWarning />
<div <div className={classnames('header__top')}>
className={classnames('header__top', { <a className="header__no-decoration" href="https://uniswap.io" target="_blank" rel="noopener noreferrer">
'header--inactive': !props.isConnected <Logo />
})} </a>
>
<Logo />
<div className="header__center-group"> <div className="header__center-group">
<span className="header__title">Uniswap</span> <a className="header__no-decoration" href="https://uniswap.io" target="_blank" rel="noopener noreferrer">
<span className="header__title">Uniswap</span>
</a>
</div> </div>
<Web3Status isConnected />
<Web3Status isConnected={!!(context.active && context.account)} />
</div> </div>
</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 React, { useCallback } from 'react'
import { withRouter } from 'react-router-dom' import { withRouter, NavLink } from 'react-router-dom'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import PropTypes from 'prop-types' import { useTranslation } from 'react-i18next'
import { withTranslation } from 'react-i18next'
import { dismissBetaMessage } from '../../ducks/app' 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 { const tabOrder = [
static propTypes = { {
history: PropTypes.shape({ path: '/swap',
push: PropTypes.func.isRequired textKey: 'swap',
}), regex: /\/swap/
className: PropTypes.string, },
dismissBetaMessage: PropTypes.func.isRequired, {
showBetaMessage: PropTypes.bool.isRequired path: '/send',
textKey: 'send',
regex: /\/send/
},
{
path: 'add-liquidity',
textKey: 'pool',
regex: /\/add-liquidity|\/remove-liquidity|\/create-exchange.*/
} }
]
constructor(props) { function NavigationTabs({ location: { pathname }, history, dismissBetaMessage, showBetaMessage }) {
super(props) const { t } = useTranslation()
this.state = {
selectedPath: this.props.location.pathname,
className: '',
showWarning: true
}
}
renderTab(name, path, regex) { const navigate = useCallback(
const { push } = this.props.history direction => {
return <Tab text={name} onClick={() => push(path)} isSelected={regex.test(this.props.location.pathname)} /> 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() { return (
const { t, showBetaMessage, className, dismissBetaMessage } = this.props <>
return ( <div className="tabs">
<div> {tabOrder.map(({ path, textKey, regex }) => (
<Tabs className={className}> <NavLink
{this.renderTab(t('swap'), '/swap', /swap/)} key={path}
{this.renderTab(t('send'), '/send', /send/)} to={path}
{this.renderTab(t('pool'), '/add-liquidity', /add-liquidity|remove-liquidity|create-exchange/)} className="tab"
</Tabs> activeClassName="tab--selected"
{showBetaMessage && ( isActive={(_, { pathname }) => pathname.match(regex)}
<div className="beta-message" onClick={dismissBetaMessage}> >
<span role="img" aria-label="warning"> {t(textKey)}
💀 </NavLink>
</span>{' '} ))}
{t('betaWarning')}
</div>
)}
</div> </div>
) {showBetaMessage && (
} <div className="beta-message" onClick={dismissBetaMessage}>
<span role="img" aria-label="warning">
💀
</span>{' '}
{t('betaWarning')}
</div>
)}
</>
)
} }
export default withRouter( export default withRouter(
...@@ -62,5 +81,5 @@ export default withRouter( ...@@ -62,5 +81,5 @@ export default withRouter(
dispatch => ({ dispatch => ({
dismissBetaMessage: () => dispatch(dismissBetaMessage()) dismissBetaMessage: () => dispatch(dismissBetaMessage())
}) })
)(withTranslation()(NavigationTabs)) )(NavigationTabs)
) )
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
.beta-message { .beta-message {
@extend %row-nowrap; @extend %row-nowrap;
cursor: pointer;
flex: 1 0 auto; flex: 1 0 auto;
align-items: center; align-items: center;
position: relative; position: relative;
...@@ -23,3 +24,35 @@ ...@@ -23,3 +24,35 @@
color: $wisteria-purple; 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' ...@@ -6,9 +6,11 @@ import Jazzicon from 'jazzicon'
import { CSSTransitionGroup } from 'react-transition-group' import { CSSTransitionGroup } from 'react-transition-group'
import { withTranslation } from 'react-i18next' import { withTranslation } from 'react-i18next'
import { ethers } from 'ethers' import { ethers } from 'ethers'
import './web3-status.scss'
import Modal from '../Modal' import Modal from '../Modal'
import './web3-status.scss'
function getEtherscanLink(tx) { function getEtherscanLink(tx) {
return `https://etherscan.io/tx/${tx}` return `https://etherscan.io/tx/${tx}`
} }
...@@ -138,7 +140,6 @@ Web3Status.defaultProps = { ...@@ -138,7 +140,6 @@ Web3Status.defaultProps = {
export default connect(state => { export default connect(state => {
return { return {
address: state.web3connect.account, address: state.web3connect.account,
isConnected: !!(state.web3connect.web3 && state.web3connect.account),
pending: state.web3connect.transactions.pending, pending: state.web3connect.transactions.pending,
confirmed: state.web3connect.transactions.confirmed confirmed: state.web3connect.transactions.confirmed
} }
......
...@@ -8,8 +8,6 @@ export const SET_WEB3_CONNECTION_STATUS = 'WEB3_CONNECTION_STATUS' ...@@ -8,8 +8,6 @@ export const SET_WEB3_CONNECTION_STATUS = 'WEB3_CONNECTION_STATUS'
export const CHECK_WEB3_CONNECTION = 'CHECK_WEB3_CONNECTION' export const CHECK_WEB3_CONNECTION = 'CHECK_WEB3_CONNECTION'
export const SET_CURRENT_MASK_ADDRESS = 'SET_CURRENT_MASK_ADDRESS' export const SET_CURRENT_MASK_ADDRESS = 'SET_CURRENT_MASK_ADDRESS'
export const METAMASK_LOCKED = 'METAMASK_LOCKED'
export const METAMASK_UNLOCKED = 'METAMASK_UNLOCKED'
export const SET_INTERACTION_STATE = 'SET_INTERACTION_STATE' export const SET_INTERACTION_STATE = 'SET_INTERACTION_STATE'
export const SET_NETWORK_MESSAGE = 'SET_NETWORK_MESSAGE' export const SET_NETWORK_MESSAGE = 'SET_NETWORK_MESSAGE'
......
...@@ -210,6 +210,10 @@ export const setAddresses = networkId => { ...@@ -210,6 +210,10 @@ export const setAddresses = networkId => {
// Rinkeby // Rinkeby
case 4: case 4:
case '4': case '4':
return {
type: SET_ADDRESSES,
payload: RINKEBY
}
default: default:
return { return {
type: SET_ADDRESSES, 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 { BigNumber as BN } from 'bignumber.js'
import Web3 from 'web3' import Web3 from 'web3'
import ERC20_ABI from '../abi/erc20' import ERC20_ABI from '../abi/erc20'
import ERC20_WITH_BYTES_ABI from '../abi/erc20_symbol_bytes32' import ERC20_WITH_BYTES_ABI from '../abi/erc20_symbol_bytes32'
export const INITIALIZE = 'we3connect/initialize' export const INITIALIZE = 'web3connect/initialize'
export const UPDATE_ACCOUNT = 'we3connect/updateAccount' export const INITIALIZE_WEB3 = 'web3connect/initializeWeb3'
export const UPDATE_ACCOUNT = 'web3connect/updateAccount'
export const WATCH_ETH_BALANCE = 'web3connect/watchEthBalance' export const WATCH_ETH_BALANCE = 'web3connect/watchEthBalance'
export const WATCH_TOKEN_BALANCE = 'web3connect/watchTokenBalance' export const WATCH_TOKEN_BALANCE = 'web3connect/watchTokenBalance'
export const UPDATE_ETH_BALANCE = 'web3connect/updateEthBalance' export const UPDATE_ETH_BALANCE = 'web3connect/updateEthBalance'
...@@ -24,7 +22,7 @@ const initialState = { ...@@ -24,7 +22,7 @@ const initialState = {
web3: null, web3: null,
networkId: 0, networkId: 0,
initialized: false, initialized: false,
account: '', account: null,
balances: { balances: {
ethereum: {} ethereum: {}
}, },
...@@ -63,10 +61,6 @@ export const selectors = () => (dispatch, getState) => { ...@@ -63,10 +61,6 @@ export const selectors = () => (dispatch, getState) => {
} }
const getBalance = (address, tokenAddress) => { const getBalance = (address, tokenAddress) => {
if (process.env.NODE_ENV !== 'production' && !tokenAddress) {
console.warn('No token address found - return ETH balance')
}
if (!tokenAddress || tokenAddress === 'ETH') { if (!tokenAddress || tokenAddress === 'ETH') {
const balance = state.balances.ethereum[address] const balance = state.balances.ethereum[address]
if (!balance) { if (!balance) {
...@@ -106,46 +100,30 @@ const Balance = (value, label = '', decimals = 0) => ({ ...@@ -106,46 +100,30 @@ const Balance = (value, label = '', decimals = 0) => ({
decimals: +decimals decimals: +decimals
}) })
export const initialize = () => (dispatch, getState) => { export const initialize = () => async dispatch => {
const { web3connect } = getState() await dispatch({ type: INITIALIZE })
}
return new Promise(async (resolve, reject) => { export const updateNetwork = (passedProvider, networkId) => async dispatch => {
if (web3connect.web3) { const web3 = new Web3(passedProvider)
resolve(web3connect.web3)
return
}
if (typeof window.ethereum !== 'undefined') { const dispatches = [
try { dispatch({ type: INITIALIZE_WEB3, payload: web3 }),
const web3 = new Web3(window.ethereum) dispatch({ type: UPDATE_NETWORK_ID, payload: networkId })
await window.ethereum.enable() ]
dispatch({
type: INITIALIZE,
payload: web3
})
resolve(web3)
return
} catch (error) {
console.error('User denied access.')
dispatch({ type: INITIALIZE })
reject()
return
}
}
if (typeof window.web3 !== 'undefined') { await Promise.all(dispatches)
const web3 = new Web3(window.web3.currentProvider) }
dispatch({
type: INITIALIZE,
payload: web3
})
resolve(web3)
return
}
dispatch({ type: INITIALIZE }) export const updateAccount = account => async dispatch => {
reject() 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) => { export const watchBalance = ({ balanceOf, tokenAddress }) => (dispatch, getState) => {
...@@ -216,29 +194,14 @@ export const updateApprovals = ({ tokenAddress, tokenOwner, spender, balance }) ...@@ -216,29 +194,14 @@ export const updateApprovals = ({ tokenAddress, tokenOwner, spender, balance })
export const sync = () => async (dispatch, getState) => { export const sync = () => async (dispatch, getState) => {
const { getBalance, getApprovals } = dispatch(selectors()) const { getBalance, getApprovals } = dispatch(selectors())
const web3 = await dispatch(initialize())
const { const {
account, web3,
watched, watched,
contracts, contracts,
networkId,
transactions: { pending } transactions: { pending }
} = getState().web3connect } = 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 // Sync Ethereum Balances
watched.balances.ethereum.forEach(async address => { watched.balances.ethereum.forEach(async address => {
const balance = await web3.eth.getBalance(address) const balance = await web3.eth.getBalance(address)
...@@ -264,7 +227,6 @@ export const sync = () => async (dispatch, getState) => { ...@@ -264,7 +227,6 @@ export const sync = () => async (dispatch, getState) => {
} }
const contract = contracts[tokenAddress] || new web3.eth.Contract(ERC20_ABI, tokenAddress) const contract = contracts[tokenAddress] || new web3.eth.Contract(ERC20_ABI, tokenAddress)
const contractBytes32 = contracts[tokenAddress] || new web3.eth.Contract(ERC20_WITH_BYTES_ABI, tokenAddress)
if (!contracts[tokenAddress]) { if (!contracts[tokenAddress]) {
dispatch({ dispatch({
...@@ -291,6 +253,7 @@ export const sync = () => async (dispatch, getState) => { ...@@ -291,6 +253,7 @@ export const sync = () => async (dispatch, getState) => {
.catch()) .catch())
} catch (e) { } catch (e) {
try { try {
const contractBytes32 = new web3.eth.Contract(ERC20_WITH_BYTES_ABI, tokenAddress)
symbol = symbol =
symbol || symbol ||
web3.utils.hexToString( web3.utils.hexToString(
...@@ -320,34 +283,34 @@ export const sync = () => async (dispatch, getState) => { ...@@ -320,34 +283,34 @@ export const sync = () => async (dispatch, getState) => {
// Update Approvals // Update Approvals
Object.entries(watched.approvals).forEach(([tokenAddress, token]) => { Object.entries(watched.approvals).forEach(([tokenAddress, token]) => {
const contract = contracts[tokenAddress] || new web3.eth.Contract(ERC20_ABI, tokenAddress) const contract = contracts[tokenAddress] || new web3.eth.Contract(ERC20_ABI, tokenAddress)
const contractBytes32 = contracts[tokenAddress] || new web3.eth.Contract(ERC20_WITH_BYTES_ABI, tokenAddress)
Object.entries(token).forEach(([tokenOwnerAddress, tokenOwner]) => { Object.entries(token).forEach(([tokenOwnerAddress, tokenOwner]) => {
tokenOwner.forEach(async spenderAddress => { tokenOwner.forEach(async spenderAddress => {
const approvalBalance = getApprovals(tokenAddress, tokenOwnerAddress, spenderAddress) if (tokenOwnerAddress !== null && tokenOwnerAddress !== 'null') {
const balance = await contract.methods.allowance(tokenOwnerAddress, spenderAddress).call() const approvalBalance = getApprovals(tokenAddress, tokenOwnerAddress, spenderAddress)
const decimals = approvalBalance.decimals || (await contract.methods.decimals().call()) const balance = await contract.methods.allowance(tokenOwnerAddress, spenderAddress).call()
let symbol = approvalBalance.label const decimals = approvalBalance.decimals || (await contract.methods.decimals().call())
try { let symbol = approvalBalance.label
symbol = symbol || (await contract.methods.symbol().call())
} catch (e) {
try { try {
symbol = symbol || web3.utils.hexToString(await contractBytes32.methods.symbol().call()) symbol = symbol || (await contract.methods.symbol().call())
} catch (err) {} } catch (e) {
} try {
const contractBytes32 = new web3.eth.Contract(ERC20_WITH_BYTES_ABI, tokenAddress)
if (approvalBalance.label && approvalBalance.value.isEqualTo(BN(balance))) { symbol = symbol || web3.utils.hexToString(await contractBytes32.methods.symbol().call())
return } catch (err) {}
}
if (approvalBalance.label && approvalBalance.value.isEqualTo(BN(balance))) {
return
}
dispatch(
updateApprovals({
tokenAddress,
tokenOwner: tokenOwnerAddress,
spender: spenderAddress,
balance: Balance(balance, symbol, decimals)
})
)
} }
dispatch(
updateApprovals({
tokenAddress,
tokenOwner: tokenOwnerAddress,
spender: spenderAddress,
balance: Balance(balance, symbol, decimals)
})
)
}) })
}) })
}) })
...@@ -384,22 +347,25 @@ export const sync = () => async (dispatch, getState) => { ...@@ -384,22 +347,25 @@ export const sync = () => async (dispatch, getState) => {
}) })
} }
export const startWatching = () => async (dispatch, getState) => { export const startWatching = () => async dispatch => {
const { account } = getState().web3connect await dispatch(sync())
const timeout = !account ? 1000 : 5000 setTimeout(() => dispatch(startWatching()), 5000)
dispatch(sync())
setTimeout(() => dispatch(startWatching()), timeout)
} }
export default function web3connectReducer(state = initialState, { type, payload }) { export default function web3connectReducer(state = initialState, { type, payload }) {
switch (type) { switch (type) {
case INITIALIZE_WEB3:
return {
...state,
web3: payload
}
case INITIALIZE: case INITIALIZE:
return { return {
...state, ...state,
web3: payload,
initialized: true initialized: true
} }
case UPDATE_NETWORK_ID:
return { ...state, networkId: payload }
case UPDATE_ACCOUNT: case UPDATE_ACCOUNT:
return { return {
...state, ...state,
...@@ -496,8 +462,6 @@ export default function web3connectReducer(state = initialState, { type, payload ...@@ -496,8 +462,6 @@ export default function web3connectReducer(state = initialState, { type, payload
} }
} }
} }
case UPDATE_NETWORK_ID:
return { ...state, networkId: payload }
case ADD_PENDING_TX: case ADD_PENDING_TX:
return { return {
...state, ...state,
...@@ -530,32 +494,3 @@ export default function web3connectReducer(state = initialState, { type, payload ...@@ -530,32 +494,3 @@ export default function web3connectReducer(state = initialState, { type, payload
return state 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 ...@@ -14,13 +14,15 @@ i18next
// https://www.i18next.com/overview/configuration-options // https://www.i18next.com/overview/configuration-options
.init({ .init({
backend: { backend: {
loadPath: './locales/{{lng}}.json' loadPath: '/locales/{{lng}}.json'
}, },
react: {
useSuspense: false
},
lng: 'en',
fallbackLng: 'en', fallbackLng: 'en',
keySeparator: false, keySeparator: false,
interpolation: { interpolation: { escapeValue: false }
escapeValue: false // not needed for react as it escapes by default
}
}) })
export default i18next export default i18next
import React, { Suspense } from 'react' import React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import ReactGA from 'react-ga' import ReactGA from 'react-ga'
import Web3Provider, { Connectors } from 'web3-react'
import './i18n' import './i18n'
import App from './pages/App' import App from './pages/App'
...@@ -13,15 +14,20 @@ if (process.env.NODE_ENV === 'production') { ...@@ -13,15 +14,20 @@ if (process.env.NODE_ENV === 'production') {
} else { } else {
ReactGA.initialize('test', { testMode: true }) ReactGA.initialize('test', { testMode: true })
} }
ReactGA.pageview(window.location.pathname + window.location.search) 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( ReactDOM.render(
// catch the suspense in case translations are not yet loaded <Provider store={store}>
<Suspense fallback={null}> <Web3Provider connectors={connectors} libraryName="ethers.js">
<Provider store={store}>
<App /> <App />
</Provider> </Web3Provider>
</Suspense>, </Provider>,
document.getElementById('root') document.getElementById('root')
) )
import React, { Component } from 'react' import React, { useState, useEffect } from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom' import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom'
import MediaQuery from 'react-responsive' import { useWeb3Context, Connectors } from 'web3-react'
import { Web3Connect, startWatching, initialize } from '../ducks/web3connect'
import NavigationTabs from '../components/NavigationTabs'
import { updateNetwork, updateAccount, initialize, startWatching } from '../ducks/web3connect'
import { setAddresses } from '../ducks/addresses' import { setAddresses } from '../ducks/addresses'
import Header from '../components/Header' import Header from '../components/Header'
import Swap from './Swap' import Swap from './Swap'
...@@ -11,63 +13,102 @@ import Pool from './Pool' ...@@ -11,63 +13,102 @@ import Pool from './Pool'
import './App.scss' import './App.scss'
class App extends Component { const { Connector, InjectedConnector } = Connectors
componentWillMount() {
const { initialize, startWatching } = this.props
initialize().then(startWatching)
}
componentWillUpdate() {
const { web3, setAddresses } = this.props
if (this.hasSetNetworkId || !web3 || !web3.eth || !web3.eth.net || !web3.eth.net.getId) { function App({ initialized, setAddresses, updateNetwork, updateAccount, initialize, startWatching }) {
return const context = useWeb3Context()
}
web3.eth.net.getId((err, networkId) => { // start web3-react on page-load
if (!err && !this.hasSetNetworkId) { useEffect(() => {
setAddresses(networkId) context.setConnector('Injected', { suppressAndThrowErrors: true }).catch(error => {
this.hasSetNetworkId = true if (error.code === Connector.errorCodes.UNSUPPORTED_NETWORK) {
context.setError(error, { connectorName: 'Injected' })
} else {
context.setConnector('Infura')
} }
}) })
} }, [])
// 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])
render() { // initialize redux account
if (!this.props.initialized) { const [reduxAccountInitialized, setReduxAccountInitialized] = useState(false)
return <noscript /> 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 ( return (
<div id="app-container"> <div id="app-container">
<MediaQuery query="(min-width: 768px)"> <Header />
<Header /> {/* this is an intermediate state before infura is set */}
</MediaQuery> {initialized && (!context.error || context.error.code === InjectedConnector.errorCodes.UNLOCK_REQUIRED) && (
<Web3Connect />
<BrowserRouter>
<div className="app__wrapper"> <div className="app__wrapper">
<Switch> <div className="body">
<Route exact path="/swap" component={Swap} /> <div className="body__content">
<Route exact path="/send" component={Send} /> <BrowserRouter>
<Route exact path="/add-liquidity" component={Pool} /> <NavigationTabs />
<Route exact path="/remove-liquidity" component={Pool} /> <Switch>
<Route exact path="/create-exchange/:tokenAddress?" component={Pool} /> <Route exact strict path="/swap" component={Swap} />
<Redirect exact from="/" to="/swap" /> <Route exact strict path="/send" component={Send} />
</Switch> <Route
path={[
'/add-liquidity',
'/remove-liquidity',
'/create-exchange',
'/create-exchange/:tokenAddress?'
]}
component={Pool}
/>
<Redirect to="/swap" />
</Switch>
</BrowserRouter>
</div>
</div>
</div> </div>
</BrowserRouter> )}
</div> </div>
) )
} }
// loading state
return null
} }
export default connect( export default connect(
state => ({ state => ({
account: state.web3connect.account, initialized: state.web3connect.initialized
initialized: state.web3connect.initialized,
web3: state.web3connect.web3
}), }),
dispatch => ({ dispatch => ({
setAddresses: networkId => dispatch(setAddresses(networkId)), setAddresses: networkId => dispatch(setAddresses(networkId)),
updateNetwork: (passedProvider, networkId) => dispatch(updateNetwork(passedProvider, networkId)),
updateAccount: account => dispatch(updateAccount(account)),
initialize: () => dispatch(initialize()), initialize: () => dispatch(initialize()),
startWatching: () => dispatch(startWatching()) startWatching: () => dispatch(startWatching())
}) })
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
margin: auto; margin: auto;
max-width: 560px; max-width: 560px;
width: 100%; width: 100%;
overflow-y: auto;
& > div { & > div {
position: absolute; position: absolute;
...@@ -24,3 +25,16 @@ ...@@ -24,3 +25,16 @@
@extend %col-nowrap; @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 React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import Web3Provider, { Connectors } from 'web3-react'
import App from './App' import App from './App'
import store from '../store' 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', () => { it('renders without crashing', () => {
const div = document.createElement('div') const div = document.createElement('div')
ReactDOM.render( ReactDOM.render(
<Provider store={store}> <Provider store={store}>
<App /> <Web3Provider connectors={connectors} libraryName="ethers.js">
<App />
</Web3Provider>
</Provider>, </Provider>,
div div
) )
......
...@@ -2,29 +2,29 @@ import React, { Component } from 'react' ...@@ -2,29 +2,29 @@ import React, { Component } from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import classnames from 'classnames' import classnames from 'classnames'
import { 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 CurrencyInputPanel from '../../components/CurrencyInputPanel'
import OversizedPanel from '../../components/OversizedPanel' import OversizedPanel from '../../components/OversizedPanel'
import ContextualInfo from '../../components/ContextualInfo' import ContextualInfo from '../../components/ContextualInfo'
import NavigationTabs from '../../components/NavigationTabs'
import { selectors, addPendingTx } from '../../ducks/web3connect' import { selectors, addPendingTx } from '../../ducks/web3connect'
import PlusBlue from '../../assets/images/plus-blue.svg' import PlusBlue from '../../assets/images/plus-blue.svg'
import PlusGrey from '../../assets/images/plus-grey.svg' import PlusGrey from '../../assets/images/plus-grey.svg'
import { getBlockDeadline } from '../../helpers/web3-utils' import { getBlockDeadline } from '../../helpers/web3-utils'
import { retry } from '../../helpers/promise-utils' import { retry } from '../../helpers/promise-utils'
import ModeSelector from './ModeSelector'
import { BigNumber as BN } from 'bignumber.js' import { BigNumber as BN } from 'bignumber.js'
import EXCHANGE_ABI from '../../abi/exchange' import EXCHANGE_ABI from '../../abi/exchange'
import './pool.scss' import './pool.scss'
import ReactGA from 'react-ga'
const INPUT = 0 const INPUT = 0
const OUTPUT = 1 const OUTPUT = 1
class AddLiquidity extends Component { class AddLiquidity extends Component {
static propTypes = { static propTypes = {
isConnected: PropTypes.bool.isRequired, account: PropTypes.string,
account: PropTypes.string.isRequired,
selectors: PropTypes.func.isRequired, selectors: PropTypes.func.isRequired,
balances: PropTypes.object.isRequired, balances: PropTypes.object.isRequired,
exchangeAddresses: PropTypes.shape({ exchangeAddresses: PropTypes.shape({
...@@ -50,11 +50,10 @@ class AddLiquidity extends Component { ...@@ -50,11 +50,10 @@ class AddLiquidity extends Component {
} }
shouldComponentUpdate(nextProps, nextState) { 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 const { inputValue, outputValue, inputCurrency, outputCurrency, lastEditedField } = this.state
return ( return (
isConnected !== nextProps.isConnected ||
t !== nextProps.t || t !== nextProps.t ||
account !== nextProps.account || account !== nextProps.account ||
exchangeAddresses !== nextProps.exchangeAddresses || exchangeAddresses !== nextProps.exchangeAddresses ||
...@@ -434,6 +433,7 @@ class AddLiquidity extends Component { ...@@ -434,6 +433,7 @@ class AddLiquidity extends Component {
renderSummary(inputError, outputError) { renderSummary(inputError, outputError) {
const { const {
t, t,
account,
selectors, selectors,
exchangeAddresses: { fromToken } exchangeAddresses: { fromToken }
} = this.props } = this.props
...@@ -444,7 +444,11 @@ class AddLiquidity extends Component { ...@@ -444,7 +444,11 @@ class AddLiquidity extends Component {
let contextualInfo = '' let contextualInfo = ''
let isError = false let isError = false
const { label } = selectors().getTokenBalance(outputCurrency, fromToken[outputCurrency]) const { label } = selectors().getTokenBalance(outputCurrency, fromToken[outputCurrency])
if (inputError || outputError) {
if (!account) {
contextualInfo = t('noWallet')
isError = true
} else if (inputError || outputError) {
contextualInfo = inputError || outputError contextualInfo = inputError || outputError
isError = true isError = true
} else if (!inputCurrency || !outputCurrency) { } else if (!inputCurrency || !outputCurrency) {
...@@ -493,7 +497,7 @@ class AddLiquidity extends Component { ...@@ -493,7 +497,7 @@ class AddLiquidity extends Component {
if (this.isNewExchange()) { if (this.isNewExchange()) {
return ( return (
<div> <>
<div className="pool__summary-item"> <div className="pool__summary-item">
{t('youAreAdding')} {b(`${inputValue} ETH`)} {t('and')} {b(`${outputValue} ${label}`)} {t('intoPool')} {t('youAreAdding')} {b(`${inputValue} ETH`)} {t('and')} {b(`${outputValue} ${label}`)} {t('intoPool')}
</div> </div>
...@@ -510,7 +514,7 @@ class AddLiquidity extends Component { ...@@ -510,7 +514,7 @@ class AddLiquidity extends Component {
{t('youWillMint')} {b(`${inputValue}`)} {t('liquidityTokens')} {t('youWillMint')} {b(`${inputValue}`)} {t('liquidityTokens')}
</div> </div>
<div className="pool__summary-item">{t('totalSupplyIs0')}</div> <div className="pool__summary-item">{t('totalSupplyIs0')}</div>
</div> </>
) )
} }
...@@ -523,7 +527,7 @@ class AddLiquidity extends Component { ...@@ -523,7 +527,7 @@ class AddLiquidity extends Component {
const adjTotalSupply = totalSupply.dividedBy(10 ** poolTokenDecimals) const adjTotalSupply = totalSupply.dividedBy(10 ** poolTokenDecimals)
return ( return (
<div> <>
<div className="pool__summary-modal__item"> <div className="pool__summary-modal__item">
{t('youAreAdding')} {b(`${+BN(inputValue).toFixed(7)} ETH`)} {t('and')}{' '} {t('youAreAdding')} {b(`${+BN(inputValue).toFixed(7)} ETH`)} {t('and')}{' '}
{b(`${+minOutput.toFixed(7)} - ${+maxOutput.toFixed(7)} ${label}`)} {t('intoPool')} {b(`${+minOutput.toFixed(7)} - ${+maxOutput.toFixed(7)} ${label}`)} {t('intoPool')}
...@@ -538,14 +542,13 @@ class AddLiquidity extends Component { ...@@ -538,14 +542,13 @@ class AddLiquidity extends Component {
{t('tokenWorth')} {b(+ethReserve.dividedBy(totalSupply).toFixed(7))} ETH {t('and')}{' '} {t('tokenWorth')} {b(+ethReserve.dividedBy(totalSupply).toFixed(7))} ETH {t('and')}{' '}
{b(+tokenReserve.dividedBy(totalSupply).toFixed(7))} {label} {b(+tokenReserve.dividedBy(totalSupply).toFixed(7))} {label}
</div> </div>
</div> </>
) )
} }
render() { render() {
const { const {
t, t,
isConnected,
exchangeAddresses: { fromToken }, exchangeAddresses: { fromToken },
selectors selectors
} = this.props } = this.props
...@@ -555,19 +558,8 @@ class AddLiquidity extends Component { ...@@ -555,19 +558,8 @@ class AddLiquidity extends Component {
const { inputError, outputError, isValid } = this.validate() const { inputError, outputError, isValid } = this.validate()
const { label } = selectors().getTokenBalance(outputCurrency, fromToken[outputCurrency]) const { label } = selectors().getTokenBalance(outputCurrency, fromToken[outputCurrency])
return [ return (
<div <>
key="content"
className={classnames('swap__content', {
'swap--inactive': !isConnected
})}
>
<NavigationTabs
className={classnames('header__navigation', {
'header--inactive': !isConnected
})}
/>
{this.isNewExchange() ? ( {this.isNewExchange() ? (
<div className="pool__new-exchange-warning"> <div className="pool__new-exchange-warning">
<div className="pool__new-exchange-warning-text"> <div className="pool__new-exchange-warning-text">
...@@ -579,7 +571,7 @@ class AddLiquidity extends Component { ...@@ -579,7 +571,7 @@ class AddLiquidity extends Component {
<div className="pool__new-exchange-warning-text">{t('initialExchangeRate', { label })}</div> <div className="pool__new-exchange-warning-text">{t('initialExchangeRate', { label })}</div>
</div> </div>
) : null} ) : null}
<ModeSelector title={t('addLiquidity')} />
<CurrencyInputPanel <CurrencyInputPanel
title={t('deposit')} title={t('deposit')}
extraText={this.getBalance(inputCurrency)} extraText={this.getBalance(inputCurrency)}
...@@ -615,26 +607,33 @@ class AddLiquidity extends Component { ...@@ -615,26 +607,33 @@ class AddLiquidity extends Component {
<OversizedPanel hideBottom>{this.renderInfo()}</OversizedPanel> <OversizedPanel hideBottom>{this.renderInfo()}</OversizedPanel>
{this.renderSummary(inputError, outputError)} {this.renderSummary(inputError, outputError)}
<div className="pool__cta-container"> <div className="pool__cta-container">
<button <AddLiquidityButton callOnClick={this.onAddLiquidity} isValid={isValid} />
className={classnames('pool__cta-btn', {
'swap--inactive': !this.props.isConnected,
'pool__cta-btn--inactive': !isValid
})}
disabled={!isValid}
onClick={this.onAddLiquidity}
>
{t('addLiquidity')}
</button>
</div> </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( export default connect(
state => ({ state => ({
isConnected:
Boolean(state.web3connect.account) && state.web3connect.networkId === (process.env.REACT_APP_NETWORK_ID || 1),
account: state.web3connect.account, account: state.web3connect.account,
balances: state.web3connect.balances, balances: state.web3connect.balances,
web3: state.web3connect.web3, web3: state.web3connect.web3,
......
This diff is collapsed.
import React, { Component } from 'react' import React, { useState, useCallback } from 'react'
import { withRouter } from 'react-router-dom' import { withRouter, NavLink } from 'react-router-dom'
import { withTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { CSSTransitionGroup } from 'react-transition-group'
import OversizedPanel from '../../components/OversizedPanel' import OversizedPanel from '../../components/OversizedPanel'
import Dropdown from '../../assets/images/dropdown-blue.svg' import Dropdown from '../../assets/images/dropdown-blue.svg'
import Modal from '../../components/Modal' import Modal from '../../components/Modal'
import { CSSTransitionGroup } from 'react-transition-group' import { useBodyKeyDown } from '../../hooks'
const ADD = 'Add Liquidity' import './pool.scss'
const REMOVE = 'Remove Liquidity'
const CREATE = 'Create Exchange'
class ModeSelector extends Component { const poolTabOrder = [
state = { {
isShowingModal: false, path: '/add-liquidity',
selected: ADD textKey: 'addLiquidity',
regex: /\/add-liquidity/
},
{
path: '/remove-liquidity',
textKey: 'removeLiquidity',
regex: /\/remove-liquidity/
},
{
path: '/create-exchange',
textKey: 'createExchange',
regex: /\/create-exchange.*/
} }
]
changeView(view) { function ModeSelector({ location: { pathname }, history }) {
const { history } = this.props const { t } = useTranslation()
this.setState({ const [isShowingModal, setIsShowingModal] = useState(false)
isShowingModal: false,
selected: view
})
switch (view) { const activeTabKey = poolTabOrder[poolTabOrder.findIndex(({ regex }) => pathname.match(regex))].textKey
case ADD:
return history.push('/add-liquidity') const navigate = useCallback(
case REMOVE: direction => {
return history.push('/remove-liquidity') const tabIndex = poolTabOrder.findIndex(({ regex }) => pathname.match(regex))
case CREATE: history.push(poolTabOrder[(tabIndex + poolTabOrder.length + direction) % poolTabOrder.length].path)
return history.push('/create-exchange') },
default: [pathname, history]
return )
} const navigateRight = useCallback(() => {
} navigate(1)
}, [navigate])
const navigateLeft = useCallback(() => {
navigate(-1)
}, [navigate])
renderModal() { useBodyKeyDown('ArrowDown', navigateRight, isShowingModal)
if (!this.state.isShowingModal) { useBodyKeyDown('ArrowUp', navigateLeft, isShowingModal)
return
}
return ( return (
<Modal onClose={() => this.setState({ isShowingModal: false })}> <OversizedPanel hideTop>
<CSSTransitionGroup <div
transitionName="pool-modal" className="pool__liquidity-container"
transitionAppear={true} onClick={() => {
transitionLeave={true} setIsShowingModal(true)
transitionAppearTimeout={200} }}
transitionLeaveTimeout={200} >
transitionEnterTimeout={200} <span className="pool__liquidity-label">{t(activeTabKey)}</span>
<img src={Dropdown} alt="dropdown" />
</div>
{isShowingModal && (
<Modal
onClose={() => {
setIsShowingModal(false)
}}
> >
<div className="pool-modal"> <CSSTransitionGroup
<div className="pool-modal__item" onClick={() => this.changeView(ADD)}> transitionName="pool-modal"
{this.props.t('addLiquidity')} 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>
<div className="pool-modal__item" onClick={() => this.changeView(REMOVE)}> </CSSTransitionGroup>
{this.props.t('removeLiquidity')} </Modal>
</div> )}
<div className="pool-modal__item" onClick={() => this.changeView(CREATE)}> </OversizedPanel>
{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>
)
}
} }
export default withRouter(withTranslation()(ModeSelector)) export default withRouter(ModeSelector)
...@@ -3,9 +3,10 @@ import PropTypes from 'prop-types' ...@@ -3,9 +3,10 @@ import PropTypes from 'prop-types'
import classnames from 'classnames' import classnames from 'classnames'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { BigNumber as BN } from 'bignumber.js' import { BigNumber as BN } from 'bignumber.js'
import { withTranslation } from 'react-i18next' import { withTranslation, useTranslation } from 'react-i18next'
import NavigationTabs from '../../components/NavigationTabs' import ReactGA from 'react-ga'
import ModeSelector from './ModeSelector' import { useWeb3Context } from 'web3-react'
import CurrencyInputPanel from '../../components/CurrencyInputPanel' import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import { selectors, addPendingTx } from '../../ducks/web3connect' import { selectors, addPendingTx } from '../../ducks/web3connect'
import ContextualInfo from '../../components/ContextualInfo' import ContextualInfo from '../../components/ContextualInfo'
...@@ -15,7 +16,6 @@ import ArrowDownGrey from '../../assets/images/arrow-down-grey.svg' ...@@ -15,7 +16,6 @@ import ArrowDownGrey from '../../assets/images/arrow-down-grey.svg'
import { getBlockDeadline } from '../../helpers/web3-utils' import { getBlockDeadline } from '../../helpers/web3-utils'
import { retry } from '../../helpers/promise-utils' import { retry } from '../../helpers/promise-utils'
import EXCHANGE_ABI from '../../abi/exchange' import EXCHANGE_ABI from '../../abi/exchange'
import ReactGA from 'react-ga'
class RemoveLiquidity extends Component { class RemoveLiquidity extends Component {
static propTypes = { static propTypes = {
...@@ -173,6 +173,7 @@ class RemoveLiquidity extends Component { ...@@ -173,6 +173,7 @@ class RemoveLiquidity extends Component {
renderSummary(errorMessage) { renderSummary(errorMessage) {
const { const {
t, t,
account,
selectors, selectors,
exchangeAddresses: { fromToken } exchangeAddresses: { fromToken }
} = this.props } = this.props
...@@ -181,7 +182,10 @@ class RemoveLiquidity extends Component { ...@@ -181,7 +182,10 @@ class RemoveLiquidity extends Component {
let contextualInfo = '' let contextualInfo = ''
let isError = false let isError = false
if (errorMessage) { if (!account) {
contextualInfo = t('noWallet')
isError = true
} else if (errorMessage) {
contextualInfo = errorMessage contextualInfo = errorMessage
isError = true isError = true
} else if (!tokenAddress) { } else if (!tokenAddress) {
...@@ -366,23 +370,12 @@ class RemoveLiquidity extends Component { ...@@ -366,23 +370,12 @@ class RemoveLiquidity extends Component {
} }
render() { render() {
const { t, isConnected } = this.props const { t } = this.props
const { tokenAddress, value } = this.state const { tokenAddress, value } = this.state
const { isValid, errorMessage } = this.validate() const { isValid, errorMessage } = this.validate()
return [ return (
<div <>
key="content"
className={classnames('swap__content', {
'swap--inactive': !isConnected
})}
>
<NavigationTabs
className={classnames('header__navigation', {
'header--inactive': !isConnected
})}
/>
<ModeSelector title={t('removeLiquidity')} />
<CurrencyInputPanel <CurrencyInputPanel
title={t('poolTokens')} title={t('poolTokens')}
extraText={this.getBalance(tokenAddress)} extraText={this.getBalance(tokenAddress)}
...@@ -401,26 +394,33 @@ class RemoveLiquidity extends Component { ...@@ -401,26 +394,33 @@ class RemoveLiquidity extends Component {
{this.renderOutput()} {this.renderOutput()}
{this.renderSummary(errorMessage)} {this.renderSummary(errorMessage)}
<div className="pool__cta-container"> <div className="pool__cta-container">
<button <RemoveLiquidityButton callOnClick={this.onRemoveLiquidity} isValid={isValid} />
className={classnames('pool__cta-btn', {
'swap--inactive': !isConnected,
'pool__cta-btn--inactive': !isValid
})}
disabled={!isValid}
onClick={this.onRemoveLiquidity}
>
{t('removeLiquidity')}
</button>
</div> </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( export default connect(
state => ({ state => ({
isConnected:
Boolean(state.web3connect.account) && state.web3connect.networkId === (process.env.REACT_APP_NETWORK_ID || 1),
web3: state.web3connect.web3, web3: state.web3connect.web3,
balances: state.web3connect.balances, balances: state.web3connect.balances,
account: state.web3connect.account, account: state.web3connect.account,
......
import React, { Component } from 'react' import React, { useEffect } from 'react'
import Header from '../../components/Header' import ReactGA from 'react-ga'
import { Switch, Route, Redirect } from 'react-router-dom'
import ModeSelector from './ModeSelector'
import AddLiquidity from './AddLiquidity' import AddLiquidity from './AddLiquidity'
import CreateExchange from './CreateExchange' import CreateExchange from './CreateExchange'
import RemoveLiquidity from './RemoveLiquidity' import RemoveLiquidity from './RemoveLiquidity'
import { Switch, Route } from 'react-router-dom'
import './pool.scss' import './pool.scss'
import MediaQuery from 'react-responsive'
import ReactGA from 'react-ga'
class Pool extends Component { export default function Pool() {
componentWillMount() { useEffect(() => {
ReactGA.pageview(window.location.pathname + window.location.search) 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 @@ ...@@ -5,6 +5,13 @@
height: 100%; height: 100%;
background-color: $white; background-color: $white;
&__content {
padding: 1rem 0.75rem;
flex: 1 1 auto;
height: 0;
overflow-y: auto;
}
&__liquidity-container { &__liquidity-container {
@extend %row-nowrap; @extend %row-nowrap;
align-items: center; align-items: center;
...@@ -114,20 +121,22 @@ ...@@ -114,20 +121,22 @@
} }
&__item { &__item {
@extend %row-nowrap;
padding: 1rem; padding: 1rem;
margin-left: 1rem;
margin-right: 1rem;
font-size: 1rem; font-size: 1rem;
cursor: pointer; cursor: pointer;
text-decoration: none;
color: $dove-gray;
font-size: 1rem;
&:hover { &--selected {
background-color: $concrete-gray; background-color: $white;
border-radius: 3rem;
.token-modal__token-label { box-shadow: 0 0 0.5px 0.5px $mercury-gray;
color: $black; font-weight: 500;
} color: $royal-blue;
}
&:active {
background-color: darken($concrete-gray, 1);
} }
} }
} }
......
...@@ -3,10 +3,11 @@ import { connect } from 'react-redux' ...@@ -3,10 +3,11 @@ import { connect } from 'react-redux'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import classnames from 'classnames' import classnames from 'classnames'
import { BigNumber as BN } from 'bignumber.js' import { BigNumber as BN } from 'bignumber.js'
import { 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 { selectors, addPendingTx } from '../../ducks/web3connect'
import Header from '../../components/Header'
import NavigationTabs from '../../components/NavigationTabs'
import AddressInputPanel from '../../components/AddressInputPanel' import AddressInputPanel from '../../components/AddressInputPanel'
import CurrencyInputPanel from '../../components/CurrencyInputPanel' import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import ContextualInfo from '../../components/ContextualInfo' import ContextualInfo from '../../components/ContextualInfo'
...@@ -18,8 +19,6 @@ import { retry } from '../../helpers/promise-utils' ...@@ -18,8 +19,6 @@ import { retry } from '../../helpers/promise-utils'
import EXCHANGE_ABI from '../../abi/exchange' import EXCHANGE_ABI from '../../abi/exchange'
import './send.scss' import './send.scss'
import MediaQuery from 'react-responsive'
import ReactGA from 'react-ga'
const INPUT = 0 const INPUT = 0
const OUTPUT = 1 const OUTPUT = 1
...@@ -27,7 +26,6 @@ const OUTPUT = 1 ...@@ -27,7 +26,6 @@ const OUTPUT = 1
class Send extends Component { class Send extends Component {
static propTypes = { static propTypes = {
account: PropTypes.string, account: PropTypes.string,
isConnected: PropTypes.bool.isRequired,
selectors: PropTypes.func.isRequired, selectors: PropTypes.func.isRequired,
web3: PropTypes.object.isRequired web3: PropTypes.object.isRequired
} }
...@@ -583,7 +581,10 @@ class Send extends Component { ...@@ -583,7 +581,10 @@ class Send extends Component {
let contextualInfo = '' let contextualInfo = ''
let isError = false let isError = false
if (inputError || outputError) { if (!account) {
contextualInfo = t('noWallet')
isError = true
} else if (inputError || outputError) {
contextualInfo = inputError || outputError contextualInfo = inputError || outputError
isError = true isError = true
} else if (!inputCurrency || !outputCurrency) { } else if (!inputCurrency || !outputCurrency) {
...@@ -751,87 +752,81 @@ class Send extends Component { ...@@ -751,87 +752,81 @@ class Send extends Component {
const { inputError, outputError, isValid } = this.validate() const { inputError, outputError, isValid } = this.validate()
return ( return (
<div className="send"> <>
<MediaQuery query="(max-width: 767px)"> <CurrencyInputPanel
<Header /> title={t('input')}
</MediaQuery> description={lastEditedField === OUTPUT ? estimatedText : ''}
<div extraText={this.renderBalance(inputCurrency, inputBalance, inputDecimals)}
className={classnames('swap__content', { onCurrencySelected={inputCurrency => this.setState({ inputCurrency }, this.recalcForm)}
'swap--inactive': !this.props.isConnected onValueChange={this.updateInput}
})} selectedTokens={[inputCurrency, outputCurrency]}
> selectedTokenAddress={inputCurrency}
<NavigationTabs value={inputValue}
className={classnames('header__navigation', { errorMessage={inputError}
'header--inactive': !this.props.isConnected />
})} <OversizedPanel>
/> <div className="swap__down-arrow-background">
<img
<CurrencyInputPanel onClick={this.flipInputOutput}
title={t('input')} className="swap__down-arrow swap__down-arrow--clickable"
description={lastEditedField === OUTPUT ? estimatedText : ''} alt="arrow"
extraText={this.renderBalance(inputCurrency, inputBalance, inputDecimals)} src={isValid ? ArrowDownBlue : ArrowDownGrey}
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>
</div> </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>
</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( export default connect(
state => ({ state => ({
balances: state.web3connect.balances, balances: state.web3connect.balances,
isConnected: !!state.web3connect.account && state.web3connect.networkId === (process.env.REACT_APP_NETWORK_ID || 1),
account: state.web3connect.account, account: state.web3connect.account,
web3: state.web3connect.web3, web3: state.web3connect.web3,
exchangeAddresses: state.addresses.exchangeAddresses exchangeAddresses: state.addresses.exchangeAddresses
......
...@@ -3,12 +3,11 @@ import { connect } from 'react-redux' ...@@ -3,12 +3,11 @@ import { connect } from 'react-redux'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import classnames from 'classnames' import classnames from 'classnames'
import { BigNumber as BN } from 'bignumber.js' import { BigNumber as BN } from 'bignumber.js'
import MediaQuery from 'react-responsive'
import ReactGA from 'react-ga' 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 { selectors, addPendingTx } from '../../ducks/web3connect'
import Header from '../../components/Header'
import NavigationTabs from '../../components/NavigationTabs'
import CurrencyInputPanel from '../../components/CurrencyInputPanel' import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import ContextualInfo from '../../components/ContextualInfo' import ContextualInfo from '../../components/ContextualInfo'
import OversizedPanel from '../../components/OversizedPanel' import OversizedPanel from '../../components/OversizedPanel'
...@@ -26,7 +25,6 @@ const OUTPUT = 1 ...@@ -26,7 +25,6 @@ const OUTPUT = 1
class Swap extends Component { class Swap extends Component {
static propTypes = { static propTypes = {
account: PropTypes.string, account: PropTypes.string,
isConnected: PropTypes.bool.isRequired,
selectors: PropTypes.func.isRequired, selectors: PropTypes.func.isRequired,
addPendingTx: PropTypes.func.isRequired, addPendingTx: PropTypes.func.isRequired,
web3: PropTypes.object.isRequired web3: PropTypes.object.isRequired
...@@ -554,7 +552,7 @@ class Swap extends Component { ...@@ -554,7 +552,7 @@ class Swap extends Component {
renderSummary(inputError, outputError) { renderSummary(inputError, outputError) {
const { inputValue, inputCurrency, outputValue, outputCurrency } = this.state const { inputValue, inputCurrency, outputValue, outputCurrency } = this.state
const t = this.props.t const { t, account } = this.props
const inputIsZero = BN(inputValue).isZero() const inputIsZero = BN(inputValue).isZero()
const outputIsZero = BN(outputValue).isZero() const outputIsZero = BN(outputValue).isZero()
...@@ -582,6 +580,11 @@ class Swap extends Component { ...@@ -582,6 +580,11 @@ class Swap extends Component {
contextualInfo = t('unlockTokenCont') contextualInfo = t('unlockTokenCont')
} }
if (!account) {
contextualInfo = t('noWallet')
isError = true
}
return ( return (
<ContextualInfo <ContextualInfo
openDetailsText={t('transactionDetails')} openDetailsText={t('transactionDetails')}
...@@ -731,77 +734,70 @@ class Swap extends Component { ...@@ -731,77 +734,70 @@ class Swap extends Component {
const { inputError, outputError, isValid } = this.validate() const { inputError, outputError, isValid } = this.validate()
return ( return (
<div className="swap"> <>
<MediaQuery query="(max-width: 767px)"> <CurrencyInputPanel
<Header /> title={t('input')}
</MediaQuery> description={lastEditedField === OUTPUT ? estimatedText : ''}
<div extraText={this.renderBalance(inputCurrency, inputBalance, inputDecimals)}
className={classnames('swap__content', { onCurrencySelected={inputCurrency => this.setState({ inputCurrency }, this.recalcForm)}
'swap--inactive': !this.props.isConnected onValueChange={this.updateInput}
})} selectedTokens={[inputCurrency, outputCurrency]}
> selectedTokenAddress={inputCurrency}
<NavigationTabs value={inputValue}
className={classnames('header__navigation', { errorMessage={inputError}
'header--inactive': !this.props.isConnected />
})} <OversizedPanel>
/> <div className="swap__down-arrow-background">
<img
<CurrencyInputPanel onClick={this.flipInputOutput}
title={t('input')} className="swap__down-arrow swap__down-arrow--clickable"
description={lastEditedField === OUTPUT ? estimatedText : ''} alt="swap"
extraText={this.renderBalance(inputCurrency, inputBalance, inputDecimals)} src={isValid ? ArrowDownBlue : ArrowDownGrey}
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>
</div> </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>
</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( export default connect(
state => ({ state => ({
balances: state.web3connect.balances, balances: state.web3connect.balances,
isConnected: !!state.web3connect.account && state.web3connect.networkId === (process.env.REACT_APP_NETWORK_ID || 1),
account: state.web3connect.account, account: state.web3connect.account,
web3: state.web3connect.web3, web3: state.web3connect.web3,
exchangeAddresses: state.addresses.exchangeAddresses exchangeAddresses: state.addresses.exchangeAddresses
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
&--inactive { &--inactive {
opacity: 0.5; opacity: 0.5;
cursor: default;
} }
&__content { &__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