Commit 44d77ce9 authored by Noah Zinsmeister's avatar Noah Zinsmeister Committed by GitHub

Swap Migration (#277)

* migrate core logic

* finalize swap migration

* upgrade react-scripts and fix errors/warnings

* finalize swap, modularize currency input

* remove console.logs

* copy swap logic for send

* remove unnecessary variable

* tighten caching logic

* snappier ens integration
parent 9757b3e9
......@@ -22,6 +22,7 @@
"enterValueCont": "Enter a {{ missingCurrencyValue }} value to continue.",
"selectTokenCont": "Select a token to continue.",
"noLiquidity": "No liquidity.",
"insufficientLiquidity": "Insufficient liquidity.",
"unlockTokenCont": "Please unlock token to continue.",
"transactionDetails": "Transaction Details",
"hideDetails": "Hide Details",
......@@ -70,7 +71,9 @@
"invalidDecimals": "Invalid decimals",
"tokenAddress": "Token Address",
"label": "Label",
"name": "Name",
"symbol": "Symbol",
"decimals": "Decimals",
"enterTokenCont": "Enter a token address to continue"
"enterTokenCont": "Enter a token address to continue",
"priceChange": "This trade will cause the price to change by"
}
......@@ -3,7 +3,7 @@
"constant": true,
"inputs": [],
"name": "name",
"outputs": [{ "name": "", "type": "string" }],
"outputs": [{ "name": "", "type": "bytes32" }],
"payable": false,
"stateMutability": "view",
"type": "function"
......
import React from 'react'
import React, { useState, useEffect } from 'react'
import classnames from 'classnames'
import { useTranslation } from 'react-i18next'
import { useWeb3Context } from 'web3-react'
// import QrCode from '../QrCode' // commented out pending further review
import './address-input-panel.scss'
import { isAddress } from '../../utils'
export default function AddressInputPanel({ title, onChange = () => {}, value = '', errorMessage }) {
export default function AddressInputPanel({ title, onChange = () => {}, onError = () => {} }) {
const { t } = useTranslation()
const { library } = useWeb3Context()
const [input, setInput] = useState('')
const [data, setData] = useState({ address: undefined, name: undefined })
const [error, setError] = useState(false)
// keep stuff in sync
useEffect(() => {
onChange({ address: data.address, name: data.name })
}, [onChange, data.address, data.name])
useEffect(() => {
onError(error)
}, [onError, error])
useEffect(() => {
let stale = false
if (isAddress(input)) {
library.lookupAddress(input).then(name => {
if (!stale) {
// if an ENS name exists, set it as the destination
if (name) {
setInput(name)
} else {
setData({ address: input, name: '' })
setError(null)
}
}
})
} else {
try {
library.resolveName(input).then(address => {
if (!stale) {
// if the input name resolves to an address
if (address) {
setData({ address: address, name: input })
setError(null)
} else {
setError(true)
}
}
})
} catch {
setError(true)
}
}
return () => {
stale = true
setData({ address: undefined, name: undefined })
setError()
}
}, [input, library, onChange, onError])
return (
<div className="currency-input-panel">
<div
className={classnames('currency-input-panel__container address-input-panel__recipient-row', {
'currency-input-panel__container--error': errorMessage
'currency-input-panel__container--error': input !== '' && error
})}
>
<div className="address-input-panel__input-container">
......@@ -26,11 +82,11 @@ export default function AddressInputPanel({ title, onChange = () => {}, value =
<input
type="text"
className={classnames('address-input-panel__input', {
'address-input-panel__input--error': errorMessage
'address-input-panel__input--error': input !== '' && error
})}
placeholder="0x1234..."
onChange={e => onChange(e.target.value)}
value={value}
onChange={e => setInput(e.target.value)}
value={input}
/>
</div>
</div>
......
......@@ -45,7 +45,8 @@ class ContextualInfo extends Component {
)
}
return [
return (
<>
<div
key="open-details"
className="contextual-info__summary-wrapper contextual-info__open-details-container"
......@@ -66,9 +67,10 @@ class ContextualInfo extends Component {
<img src={DropupBlue} alt="dropup" />
</>
)}
</div>,
this.renderDetails()
]
</div>
{this.renderDetails()}
</>
)
}
}
......
......@@ -95,6 +95,10 @@
background-color: rgba($zumthor-blue, 0.8);
}
&:focus {
box-shadow: 0 0 0.5px 0.5px $malibu-blue;
}
&--selected {
background-color: $concrete-gray;
border-color: $mercury-gray;
......@@ -233,6 +237,8 @@
color: $white;
justify-content: center;
background-color: $malibu-blue;
text-decoration: none;
&:hover {
background-color: lighten($malibu-blue, 1);
}
......
import React, { useState, useRef, useEffect } from 'react'
import { connect } from 'react-redux'
import React, { useState, useRef, useEffect, useMemo } from 'react'
import { CSSTransitionGroup } from 'react-transition-group'
import classnames from 'classnames'
import { withRouter } from 'react-router-dom'
import { withTranslation, useTranslation } from 'react-i18next'
import { BigNumber as BN } from 'bignumber.js'
import { useWeb3Context } from 'web3-react'
import { Link } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { ethers } from 'ethers'
import escapeStringRegex from 'escape-string-regexp'
import { useSignerOrProvider } from '../../hooks'
import { getExchangeDetails, getTokenDetails, isAddress } from '../../utils'
import Fuse from '../../helpers/fuse'
import { useTokenContract } from '../../hooks'
import { isAddress, calculateGasMargin } from '../../utils'
import Modal from '../Modal'
import TokenLogo from '../TokenLogo'
import SearchIcon from '../../assets/images/magnifying-glass.svg'
import { selectors, addPendingTx } from '../../ducks/web3connect'
import { addApprovalTx } from '../../ducks/pending'
import { addExchange } from '../../ducks/addresses'
import ERC20_ABI from '../../abi/erc20'
import { useTokenDetails, useAllTokenDetails } from '../../contexts/Static'
import { useTransactionContext, usePendingApproval } from '../../contexts/Transaction'
import './currency-panel.scss'
const FUSE_OPTIONS = {
includeMatches: false,
threshold: 0.0,
tokenize: true,
location: 0,
distance: 100,
maxPatternLength: 45,
minMatchCharLength: 1,
keys: [{ name: 'address', weight: 0.8 }, { name: 'label', weight: 0.5 }]
}
const TOKEN_ADDRESS_TO_LABEL = { ETH: 'ETH' }
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
function CurrencyInputPanel({
tokenAddresses,
export default function CurrencyInputPanel({
filteredTokens = [],
onValueChange = () => {},
renderInput,
......@@ -43,210 +26,62 @@ function CurrencyInputPanel({
title,
description,
extraText,
extraTextClickHander,
errorMessage,
selectedTokens = [],
disableUnlock,
disableTokenSelect,
selectors,
account,
factoryAddress,
selectedTokenAddress = '',
exchangeAddresses: { fromToken },
addExchange,
history,
web3,
transactions,
pendingApprovals,
value,
addApprovalTx,
addPendingTx
showUnlock,
value
}) {
const { t } = useTranslation()
const context = useWeb3Context()
const signerOrProvider = useSignerOrProvider()
const inputRef = useRef()
const [isShowingModal, setIsShowingModal] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [loadingExchange, setLoadingExchange] = useState(false)
useEffect(() => {
if (inputRef.current && isShowingModal) {
inputRef.current.focus()
}
}, [inputRef.current, isShowingModal])
function createTokenList() {
let tokens = tokenAddresses.addresses
let tokenList = [{ value: 'ETH', label: 'ETH', address: 'ETH' }]
const tokenContract = useTokenContract(selectedTokenAddress)
const { exchangeAddress: selectedTokenExchangeAddress } = useTokenDetails(selectedTokenAddress)
for (let i = 0; i < tokens.length; i++) {
let entry = { value: '', label: '' }
entry.value = tokens[i][0]
entry.label = tokens[i][0]
entry.address = tokens[i][1]
tokenList.push(entry)
TOKEN_ADDRESS_TO_LABEL[tokens[i][1]] = tokens[i][0]
}
const pendingApproval = usePendingApproval(selectedTokenAddress)
return tokenList.filter(({ address }) => !filteredTokens.includes(address))
}
function onTokenSelect(address) {
setSearchQuery('')
setIsShowingModal(false)
onCurrencySelected(address)
}
function renderTokenList() {
const tokens = createTokenList()
if (loadingExchange) {
return (
<div className="token-modal__token-row token-modal__token-row--searching">
<div className="loader" />
<div>Searching for Exchange...</div>
</div>
)
}
if (isAddress(searchQuery)) {
const tokenAddress = searchQuery
const exchangeAddress = fromToken[tokenAddress]
const { addTransaction } = useTransactionContext()
const inputRef = useRef()
if (!exchangeAddress) {
setLoadingExchange(true)
const allTokens = useAllTokenDetails()
getExchangeDetails(context.networkId, tokenAddress, signerOrProvider).then(async ({ exchangeAddress }) => {
if (exchangeAddress !== ethers.constants.AddressZero) {
const { symbol } = await getTokenDetails(tokenAddress, signerOrProvider)
addExchange({
tokenAddress,
label: symbol,
exchangeAddress
})
}
setLoadingExchange(false)
})
}
}
if (disableTokenSelect) {
return
// manage focus on modal show
useEffect(() => {
if (inputRef.current && isShowingModal) {
inputRef.current.focus()
}
}, [isShowingModal])
let results
if (!searchQuery) {
results = tokens
function renderUnlockButton() {
if (disableUnlock || !showUnlock || selectedTokenAddress === 'ETH' || !selectedTokenAddress) {
return null
} else {
const fuse = new Fuse(tokens, FUSE_OPTIONS)
results = fuse.search(searchQuery)
}
if (!results.length && web3 && web3.utils && isAddress(searchQuery)) {
const { label } = selectors().getBalance(account, searchQuery)
return [
<div key="token-modal-no-exchange" className="token-modal__token-row token-modal__token-row--no-exchange">
<div>{t('noExchange')}</div>
</div>,
<div
key="token-modal-create-exchange"
className="token-modal__token-row token-modal__token-row--create-exchange"
onClick={() => {
setIsShowingModal(false)
history.push(`/create-exchange/${searchQuery}`)
}}
>
<div>{`Create exchange for ${label}`}</div>
</div>
]
}
if (!results.length) {
if (!pendingApproval) {
return (
<div className="token-modal__token-row token-modal__token-row--no-exchange">
<div>{t('noExchange')}</div>
</div>
<button
className="currency-input-panel__sub-currency-select"
onClick={async () => {
const estimatedGas = await tokenContract.estimate.approve(
selectedTokenExchangeAddress,
ethers.constants.MaxUint256
)
}
return results.map(({ label, address }) => {
const isSelected = selectedTokens.indexOf(address) > -1
return (
<div
key={label}
className={classnames('token-modal__token-row', {
'token-modal__token-row--selected': isSelected
})}
onClick={() => onTokenSelect(address)}
>
<TokenLogo className="token-modal__token-logo" address={address} />
<div className="token-modal__token-label">{label}</div>
</div>
)
tokenContract
.approve(selectedTokenExchangeAddress, ethers.constants.MaxUint256, {
gasLimit: calculateGasMargin(estimatedGas, GAS_MARGIN)
})
.then(response => {
addTransaction(response.hash, response)
})
}
function renderModal() {
if (!isShowingModal) {
return null
}
return (
<Modal
onClose={() => {
setIsShowingModal(false)
setSearchQuery('')
}}
>
<CSSTransitionGroup
transitionName="token-modal"
transitionAppear={true}
transitionLeave={true}
transitionAppearTimeout={200}
transitionLeaveTimeout={200}
transitionEnterTimeout={200}
>
<div className="token-modal">
<div className="token-modal__search-container">
<input
ref={inputRef}
type="text"
placeholder={t('searchOrPaste')}
className="token-modal__search-input"
onChange={e => {
setSearchQuery(e.target.value)
}}
/>
<img src={SearchIcon} className="token-modal__search-icon" alt="search" />
</div>
<div className="token-modal__token-list">{renderTokenList()}</div>
</div>
</CSSTransitionGroup>
</Modal>
)
}
function renderUnlockButton() {
if (disableUnlock || !selectedTokenAddress || selectedTokenAddress === 'ETH') {
return
}
const { value: allowance, decimals, label } = selectors().getApprovals(
selectedTokenAddress,
account,
fromToken[selectedTokenAddress]
{t('unlock')}
</button>
)
if (!label || (allowance.isGreaterThanOrEqualTo(BN((value || 0) * 10 ** decimals)) && !BN(allowance).isZero())) {
return
}
const approvalTxId = pendingApprovals[selectedTokenAddress]
if (approvalTxId && transactions.pending.includes(approvalTxId)) {
} else {
return (
<button className="currency-input-panel__sub-currency-select currency-input-panel__sub-currency-select--pending">
<div className="loader" />
......@@ -254,26 +89,7 @@ function CurrencyInputPanel({
</button>
)
}
return (
<button
className="currency-input-panel__sub-currency-select"
onClick={() => {
const contract = new web3.eth.Contract(ERC20_ABI, selectedTokenAddress)
const amount = BN(10 ** decimals)
.multipliedBy(10 ** 8)
.toFixed(0)
contract.methods.approve(fromToken[selectedTokenAddress], amount).send({ from: account }, (err, data) => {
if (!err && data) {
addPendingTx(data)
addApprovalTx({ tokenAddress: selectedTokenAddress, txId: data })
}
})
}}
>
{t('unlock')}
</button>
)
}
function _renderInput() {
......@@ -317,7 +133,7 @@ function CurrencyInputPanel({
{selectedTokenAddress ? (
<TokenLogo className="currency-input-panel__selected-token-logo" address={selectedTokenAddress} />
) : null}
{TOKEN_ADDRESS_TO_LABEL[selectedTokenAddress] || t('selectToken')}
{(allTokens[selectedTokenAddress] && allTokens[selectedTokenAddress].symbol) || t('selectToken')}
<span className="currency-input-panel__dropdown-icon" />
</button>
</div>
......@@ -340,35 +156,154 @@ function CurrencyInputPanel({
className={classnames('currency-input-panel__extra-text', {
'currency-input-panel__extra-text--error': errorMessage
})}
onClick={() => {
extraTextClickHander()
}}
>
{extraText}
</span>
</div>
{_renderInput()}
</div>
{renderModal()}
{!disableTokenSelect && isShowingModal && (
<CurrencySelectModal
onTokenSelect={onCurrencySelected}
onClose={() => {
setIsShowingModal(false)
}}
/>
)}
</div>
)
}
export default withRouter(
connect(
state => ({
factoryAddress: state.addresses.factoryAddress,
exchangeAddresses: state.addresses.exchangeAddresses,
tokenAddresses: state.addresses.tokenAddresses,
contracts: state.contracts,
account: state.web3connect.account,
approvals: state.web3connect.approvals,
transactions: state.web3connect.transactions,
web3: state.web3connect.web3,
pendingApprovals: state.pending.approvals
}),
dispatch => ({
selectors: () => dispatch(selectors()),
addExchange: opts => dispatch(addExchange(opts)),
addPendingTx: opts => dispatch(addPendingTx(opts)),
addApprovalTx: opts => dispatch(addApprovalTx(opts))
function CurrencySelectModal({ onClose, onTokenSelect }) {
const { t } = useTranslation()
const [searchQuery, setSearchQuery] = useState('')
const { exchangeAddress } = useTokenDetails(searchQuery)
const allTokens = useAllTokenDetails()
const tokenList = useMemo(() => {
return Object.keys(allTokens)
.sort((a, b) => {
const aSymbol = allTokens[a].symbol
const bSymbol = allTokens[b].symbol
if (aSymbol === 'ETH' || bSymbol === 'ETH') {
return aSymbol === bSymbol ? 0 : aSymbol === 'ETH' ? -1 : 1
} else {
return aSymbol < bSymbol ? -1 : aSymbol > bSymbol ? 1 : 0
}
})
.map(k => {
return {
name: allTokens[k].name,
symbol: allTokens[k].symbol,
address: k
}
})
}, [allTokens])
const filteredTokenList = useMemo(() => {
return tokenList.filter(tokenEntry => {
// check the regex for each field
const regexMatches = Object.keys(tokenEntry).map(tokenEntryKey => {
return !!tokenEntry[tokenEntryKey].match(new RegExp(escapeStringRegex(searchQuery), 'i'))
})
return regexMatches.some(m => m)
})
}, [tokenList, searchQuery])
function _onTokenSelect(address) {
setSearchQuery('')
onTokenSelect(address)
onClose()
}
function _onClose(address) {
setSearchQuery('')
onClose()
}
function renderTokenList() {
if (isAddress(searchQuery) && exchangeAddress === undefined) {
return (
<div className="token-modal__token-row token-modal__token-row--searching">
<div className="loader" />
<div>Searching for Exchange...</div>
</div>
)
}
if (isAddress(searchQuery) && exchangeAddress === ethers.constants.AddressZero) {
return (
<>
<div className="token-modal__token-row token-modal__token-row--no-exchange">
<div>{t('noExchange')}</div>
</div>
<Link
to={`/create-exchange/${searchQuery}`}
className="token-modal__token-row token-modal__token-row--create-exchange"
onClick={onClose}
>
<div>{t('createExchange')}</div>
</Link>
</>
)
}
if (!filteredTokenList.length) {
return (
<div className="token-modal__token-row token-modal__token-row--no-exchange">
<div>{t('noExchange')}</div>
</div>
)
}
return filteredTokenList.map(({ address, symbol }) => {
return (
<div key={address} className="token-modal__token-row" onClick={() => _onTokenSelect(address)}>
<TokenLogo className="token-modal__token-logo" address={address} />
<div className="token-modal__token-label">{symbol}</div>
</div>
)
})
)(withTranslation()(CurrencyInputPanel))
)
}
// manage focus on modal show
const inputRef = useRef()
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus()
}
}, [])
return (
<Modal onClose={_onClose}>
<CSSTransitionGroup
transitionName="token-modal"
transitionAppear={true}
transitionLeave={true}
transitionAppearTimeout={200}
transitionLeaveTimeout={200}
transitionEnterTimeout={200}
>
<div className="token-modal">
<div className="token-modal__search-container">
<input
ref={inputRef}
type="text"
placeholder={t('searchOrPaste')}
className="token-modal__search-input"
onChange={e => {
setSearchQuery(e.target.value)
}}
/>
<img src={SearchIcon} className="token-modal__search-icon" alt="search" />
</div>
<div className="token-modal__token-list">{renderTokenList()}</div>
</div>
</CSSTransitionGroup>
</Modal>
)
}
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import React, { useState } from 'react'
import { useWeb3Context } from 'web3-react'
import classnames from 'classnames'
import Jazzicon from 'jazzicon'
import { CSSTransitionGroup } from 'react-transition-group'
import { withTranslation } from 'react-i18next'
import { useTranslation } from 'react-i18next'
import { ethers } from 'ethers'
import Modal from '../Modal'
import { useAllTransactions } from '../../contexts/Transaction'
import './web3-status.scss'
......@@ -15,19 +15,47 @@ function getEtherscanLink(tx) {
return `https://etherscan.io/tx/${tx}`
}
class Web3Status extends Component {
state = {
isShowingModal: false
function getPendingText(pendingTransactions, pendingLabel) {
return (
<div className="web3-status__pending-container">
<div className="loader" />
<span key="text">
{pendingTransactions.length} {pendingLabel}
</span>
</div>
)
}
function getText(text, disconnectedText) {
if (!text || text.length < 42 || !ethers.utils.isHexString(text)) {
return disconnectedText
}
handleClick = () => {
if (this.props.pending.length && !this.state.isShowingModal) {
this.setState({ isShowingModal: true })
const address = ethers.utils.getAddress(text)
return `${address.substring(0, 6)}...${address.substring(38)}`
}
export default function Web3Status() {
const { t } = useTranslation()
const { active, account } = useWeb3Context()
const allTransactions = useAllTransactions()
const pending = Object.keys(allTransactions).filter(t => allTransactions[t].completed === false)
const confirmed = Object.keys(allTransactions).filter(t => allTransactions[t].completed === true)
const hasPendingTransactions = !!pending.length
const hasConfirmedTransactions = !!confirmed.length
const [isShowingModal, setIsShowingModal] = useState(false)
function handleClick() {
if (pending.length && !isShowingModal) {
setIsShowingModal(true)
}
}
renderPendingTransactions() {
return this.props.pending.map(transaction => {
function renderPendingTransactions() {
return pending.map(transaction => {
return (
<div
key={transaction}
......@@ -36,20 +64,20 @@ class Web3Status extends Component {
>
<div className="pending-modal__transaction-label">{transaction}</div>
<div className="pending-modal__pending-indicator">
<div className="loader" /> {this.props.t('pending')}
<div className="loader" /> {t('pending')}
</div>
</div>
)
})
}
renderModal() {
if (!this.state.isShowingModal) {
function renderModal() {
if (!isShowingModal) {
return null
}
return (
<Modal onClose={() => this.setState({ isShowingModal: false })}>
<Modal onClose={() => setIsShowingModal(false)}>
<CSSTransitionGroup
transitionName="token-modal"
transitionAppear={true}
......@@ -61,7 +89,7 @@ class Web3Status extends Component {
<div className="pending-modal">
<div className="pending-modal__transaction-list">
<div className="pending-modal__header">Transactions</div>
{this.renderPendingTransactions()}
{renderPendingTransactions()}
</div>
</div>
</CSSTransitionGroup>
......@@ -69,78 +97,30 @@ class Web3Status extends Component {
)
}
render() {
const { t, address, pending, confirmed } = this.props
const hasPendingTransactions = !!pending.length
const hasConfirmedTransactions = !!confirmed.length
return (
<div
className={classnames('web3-status', {
'web3-status__connected': this.props.isConnected,
'web3-status__connected': active,
'web3-status--pending': hasPendingTransactions,
'web3-status--confirmed': hasConfirmedTransactions
})}
onClick={this.handleClick}
onClick={handleClick}
>
<div className="web3-status__text">
{hasPendingTransactions ? getPendingText(pending, t('pending')) : getText(address, t('disconnected'))}
{hasPendingTransactions ? getPendingText(pending, t('pending')) : getText(account, t('disconnected'))}
</div>
<div
className="web3-status__identicon"
ref={el => {
if (!el) {
return
}
if (!address || address.length < 42 || !ethers.utils.isHexString(address)) {
if (!el || !account) {
return
}
} else {
el.innerHTML = ''
el.appendChild(Jazzicon(16, parseInt(address.slice(2), 16)))
el.appendChild(Jazzicon(16, parseInt(account.slice(2), 16)))
}
}}
/>
{this.renderModal()}
</div>
)
}
}
function getPendingText(pendingTransactions, pendingLabel) {
return (
<div className="web3-status__pending-container">
<div className="loader" />
<span key="text">
{pendingTransactions.length} {pendingLabel}
</span>
{renderModal()}
</div>
)
}
function getText(text, disconnectedText) {
if (!text || text.length < 42 || !ethers.utils.isHexString(text)) {
return disconnectedText
}
const address = ethers.utils.getAddress(text)
return `${address.substring(0, 6)}...${address.substring(38)}`
}
Web3Status.propTypes = {
isConnected: PropTypes.bool,
address: PropTypes.string
}
Web3Status.defaultProps = {
isConnected: false,
address: 'Disconnected'
}
export default connect(state => {
return {
address: state.web3connect.account,
pending: state.web3connect.transactions.pending,
confirmed: state.web3connect.transactions.confirmed
}
})(withTranslation()(Web3Status))
export const FACTORY_ADDRESSES = {
1: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95',
4: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36'
}
import React, { Component, createContext, useContext, useEffect } from 'react'
import { useWeb3Context } from 'web3-react'
import { useBlockEffect } from '../hooks'
const ApplicationContext = createContext()
export default class Provider extends Component {
constructor(props) {
super(props)
this.dismissBetaMessage = () => {
this.setState({ showBetaMessage: false })
}
this.updateBlockNumber = blockNumber => {
this.setState({ blockNumber })
}
this.state = {
showBetaMessage: true,
dismissBetaMessage: this.dismissBetaMessage,
blockNumber: undefined,
updateBlockNumber: this.updateBlockNumber
}
}
render() {
return <ApplicationContext.Provider value={this.state}>{this.props.children}</ApplicationContext.Provider>
}
}
export function useApplicationContext() {
return useContext(ApplicationContext)
}
export function Updater() {
const { library } = useWeb3Context()
const { updateBlockNumber } = useApplicationContext()
// fetch the block number once on load...
useEffect(() => {
if (library) {
let stale = false
library
.getBlockNumber()
.then(blockNumber => {
if (!stale) {
updateBlockNumber(blockNumber)
}
})
.catch(() => {
if (!stale) {
updateBlockNumber(null)
}
})
return () => {
stale = true
// this clears block number on network change because the library has changed
updateBlockNumber(undefined)
}
}
}, [library, updateBlockNumber])
// ...and every block...
useBlockEffect(updateBlockNumber)
return null
}
export function useBlockNumber() {
const { blockNumber } = useApplicationContext()
return blockNumber
}
import React, { Component, createContext, useContext, useEffect } from 'react'
import { useWeb3Context } from 'web3-react'
import merge from 'lodash.merge'
import { getEtherBalance, getTokenBalance, getTokenAllowance, isAddress } from '../utils'
import { useBlockNumber } from './Application'
import { useTokenDetails } from './Static'
// define constants
const BALANCE = 'balance'
const ALLOWANCE = 'allowance'
// node creation
function createAddressValueNode(name, value, blockNumber) {
return { [name]: value, blockNumber }
}
// tree creation
function createAddressBalanceTree(address, tokenAddress, value, blockNumber) {
return { [address]: { [tokenAddress]: createAddressValueNode(BALANCE, value, blockNumber) } }
}
function createAddressAllowanceTree(address, tokenAddress, spenderAddress, value, blockNumber) {
return {
[address]: { [tokenAddress]: { [spenderAddress]: createAddressValueNode(ALLOWANCE, value, blockNumber) } }
}
}
// create contexts
const AddressBalanceContext = createContext()
const AddressAllowanceContext = createContext()
// define providers
class AddressBalanceContextProvider extends Component {
constructor(props) {
super(props)
this.getValue = (address, tokenAddress) => {
return this.state[BALANCE][address] && this.state[BALANCE][address][tokenAddress]
? this.state[BALANCE][address][tokenAddress]
: createAddressValueNode(BALANCE)
}
this.updateValue = (address, tokenAddress, value, blockNumber) => {
this.setState(state => ({
[BALANCE]: merge(state[BALANCE], createAddressBalanceTree(address, tokenAddress, value, blockNumber))
}))
}
this.clearValues = () => {
this.setState({ [BALANCE]: {} })
}
this.state = {
[BALANCE]: {},
getValue: this.getValue,
updateValue: this.updateValue,
clearValues: this.clearValues
}
}
render() {
return <AddressBalanceContext.Provider value={this.state}>{this.props.children}</AddressBalanceContext.Provider>
}
}
class AddressAllowanceContextProvider extends Component {
constructor(props) {
super(props)
this.getValue = (address, tokenAddress, spenderAddress) => {
return this.state[ALLOWANCE][address] &&
this.state[ALLOWANCE][address][tokenAddress] &&
this.state[ALLOWANCE][address][tokenAddress][spenderAddress]
? this.state[ALLOWANCE][address][tokenAddress][spenderAddress]
: createAddressValueNode(ALLOWANCE)
}
this.updateValue = (address, tokenAddress, spenderAddress, value, blockNumber) => {
this.setState(state => ({
[ALLOWANCE]: merge(
state[ALLOWANCE],
createAddressAllowanceTree(address, tokenAddress, spenderAddress, value, blockNumber)
)
}))
}
this.clearValues = () => {
this.setState({ [ALLOWANCE]: {} })
}
this.state = {
[ALLOWANCE]: {},
getValue: this.getValue,
updateValue: this.updateValue,
clearValues: this.clearValues
}
}
render() {
return <AddressAllowanceContext.Provider value={this.state}>{this.props.children}</AddressAllowanceContext.Provider>
}
}
export default function Provider({ children }) {
return (
<AddressBalanceContextProvider>
<AddressAllowanceContextProvider>{children}</AddressAllowanceContextProvider>
</AddressBalanceContextProvider>
)
}
// define useContext wrappers
function useAddressBalanceContext() {
return useContext(AddressBalanceContext)
}
function useAddressAllowanceContext() {
return useContext(AddressAllowanceContext)
}
export function Updater() {
const { networkId } = useWeb3Context()
const { clearValues: clearValuesBalance } = useAddressBalanceContext()
const { clearValues: clearValuesAllowance } = useAddressAllowanceContext()
useEffect(() => {
return () => {
clearValuesBalance()
clearValuesAllowance()
}
}, [clearValuesBalance, clearValuesAllowance, networkId])
return null
}
// define custom hooks
export function useAddressBalance(address, tokenAddress) {
const { library } = useWeb3Context()
const globalBlockNumber = useBlockNumber()
const { getValue, updateValue } = useAddressBalanceContext()
const { [BALANCE]: balance, blockNumber: balanceUpdatedBlockNumber } = getValue(address, tokenAddress)
useEffect(() => {
// gate this entire effect by checking that the inputs are valid
if (isAddress(address) && (tokenAddress === 'ETH' || isAddress(tokenAddress))) {
// if they are, and the balance is undefined or stale, fetch it
if (balance === undefined || balanceUpdatedBlockNumber !== globalBlockNumber) {
let stale = false
;(tokenAddress === 'ETH' ? getEtherBalance(address, library) : getTokenBalance(tokenAddress, address, library))
.then(value => {
if (!stale) {
updateValue(address, tokenAddress, value, globalBlockNumber)
}
})
.catch(() => {
if (!stale) {
updateValue(address, tokenAddress, null, globalBlockNumber)
}
})
return () => {
stale = true
}
}
}
}, [address, tokenAddress, balance, balanceUpdatedBlockNumber, globalBlockNumber, library, updateValue])
return balance
}
export function useAddressAllowance(address, tokenAddress, spenderAddress) {
const { library } = useWeb3Context()
const globalBlockNumber = useBlockNumber()
const { getValue, updateValue } = useAddressAllowanceContext()
const { [ALLOWANCE]: allowance, blockNumber: allowanceUpdatedBlockNumber } = getValue(
address,
tokenAddress,
spenderAddress
)
useEffect(() => {
// gate this entire effect by checking that the inputs are valid
if (isAddress(address) && isAddress(tokenAddress) && isAddress(spenderAddress)) {
// if they are, and the balance is undefined or stale, fetch it
if (allowance === undefined || allowanceUpdatedBlockNumber !== globalBlockNumber) {
let stale = false
getTokenAllowance(address, tokenAddress, spenderAddress, library)
.then(value => {
if (!stale) {
updateValue(address, tokenAddress, spenderAddress, value, globalBlockNumber)
}
})
.catch(() => {
if (!stale) {
updateValue(address, tokenAddress, spenderAddress, null, globalBlockNumber)
}
})
return () => {
stale = true
}
}
}
}, [
address,
tokenAddress,
spenderAddress,
allowance,
allowanceUpdatedBlockNumber,
globalBlockNumber,
library,
updateValue
])
return allowance
}
export function useExchangeReserves(tokenAddress) {
const { exchangeAddress } = useTokenDetails(tokenAddress)
const reserveETH = useAddressBalance(exchangeAddress, 'ETH')
const reserveToken = useAddressBalance(exchangeAddress, tokenAddress)
return { reserveETH, reserveToken }
}
import React, { Component, createContext, useContext, useEffect } from 'react'
import { useWeb3Context } from 'web3-react'
import { ethers } from 'ethers'
import merge from 'lodash.merge'
import { isAddress, getTokenName, getTokenSymbol, getTokenDecimals, getTokenExchangeAddressFromFactory } from '../utils'
// define constants
const TOKEN_DETAIL = 'tokenDetail'
const NAME = 'name'
const SYMBOL = 'symbol'
const DECIMALS = 'decimals'
const EXCHANGE_ADDRESS = 'exchangeAddress'
const TOKEN_DETAILS_MAINNET = {
ETH: {
[NAME]: 'Ethereum',
[SYMBOL]: 'ETH',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: null
},
'0x960b236A07cf122663c4303350609A66A7B288C0': {
[NAME]: 'Aragon Network Token',
[SYMBOL]: 'ANT',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0x077d52B047735976dfdA76feF74d4d988AC25196'
},
'0x0D8775F648430679A709E98d2b0Cb6250d2887EF': {
[NAME]: 'Basic Attention Token',
[SYMBOL]: 'BAT',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0x2E642b8D59B45a1D8c5aEf716A84FF44ea665914'
},
'0x107c4504cd79C5d2696Ea0030a8dD4e92601B82e': {
[NAME]: 'Bloom Token',
[SYMBOL]: 'BLT',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0x0E6A53B13688018A3df8C69f99aFB19A3068D04f'
},
'0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C': {
[NAME]: 'Bancor Network Token',
[SYMBOL]: 'BNT',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0x87d80DBD37E551F58680B4217b23aF6a752DA83F'
},
'0x26E75307Fc0C021472fEb8F727839531F112f317': {
[NAME]: 'Crypto20',
[SYMBOL]: 'C20',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0xF7B5A4b934658025390ff69dB302BC7F2AC4a542'
},
'0x41e5560054824eA6B0732E656E3Ad64E20e94E45': {
[NAME]: 'Civic',
[SYMBOL]: 'CVC',
[DECIMALS]: 8,
[EXCHANGE_ADDRESS]: '0x1C6c712b1F4a7c263B1DBd8F97fb447c945d3b9a'
},
'0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359': {
[NAME]: 'Dai Stablecoin v1.0',
[SYMBOL]: 'DAI',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0x09cabEC1eAd1c0Ba254B09efb3EE13841712bE14'
},
'0x4f3AfEC4E5a3F2A6a1A411DEF7D7dFe50eE057bF': {
[NAME]: 'Digix Gold Token',
[SYMBOL]: 'DGX',
[DECIMALS]: 9,
[EXCHANGE_ADDRESS]: '0xb92dE8B30584392Af27726D5ce04Ef3c4e5c9924'
},
'0x4946Fcea7C692606e8908002e55A582af44AC121': {
[NAME]: 'FOAM Token',
[SYMBOL]: 'FOAM',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0xf79cb3BEA83BD502737586A6E8B133c378FD1fF2'
},
'0x419D0d8BdD9aF5e606Ae2232ed285Aff190E711b': {
[NAME]: 'FunFair',
[SYMBOL]: 'FUN',
[DECIMALS]: 8,
[EXCHANGE_ADDRESS]: '0x60a87cC7Fca7E53867facB79DA73181B1bB4238B'
},
'0x6810e776880C02933D47DB1b9fc05908e5386b96': {
[NAME]: 'Gnosis Token',
[SYMBOL]: 'GNO',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0xe8e45431b93215566BA923a7E611B7342Ea954DF'
},
'0x12B19D3e2ccc14Da04FAe33e63652ce469b3F2FD': {
[NAME]: 'GRID Token',
[SYMBOL]: 'GRID',
[DECIMALS]: 12,
[EXCHANGE_ADDRESS]: '0x4B17685b330307C751B47f33890c8398dF4Fe407'
},
'0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd': {
[NAME]: 'Gemini dollar',
[SYMBOL]: 'GUSD',
[DECIMALS]: 2,
[EXCHANGE_ADDRESS]: '0xD883264737Ed969d2696eE4B4cAF529c2Fc2A141'
},
'0x818Fc6C2Ec5986bc6E2CBf00939d90556aB12ce5': {
[NAME]: 'Kin',
[SYMBOL]: 'KIN',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0xb7520a5F8c832c573d6BD0Df955fC5c9b72400F7'
},
'0xdd974D5C2e2928deA5F71b9825b8b646686BD200': {
[NAME]: 'Kyber Network Crystal',
[SYMBOL]: 'KNC',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0x49c4f9bc14884f6210F28342ceD592A633801a8b'
},
'0x514910771AF9Ca656af840dff83E8264EcF986CA': {
[NAME]: 'ChainLink Token',
[SYMBOL]: 'LINK',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0xF173214C720f58E03e194085B1DB28B50aCDeeaD'
},
'0xA4e8C3Ec456107eA67d3075bF9e3DF3A75823DB0': {
[NAME]: 'LoomToken',
[SYMBOL]: 'LOOM',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0x417CB32bc991fBbDCaE230C7c4771CC0D69daA6b'
},
'0x58b6A8A3302369DAEc383334672404Ee733aB239': {
[NAME]: 'Livepeer Token',
[SYMBOL]: 'LPT',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0xc4a1C45D5546029Fd57128483aE65b56124BFA6A'
},
'0x0F5D2fB29fb7d3CFeE444a200298f468908cC942': {
[NAME]: 'Decentraland MANA',
[SYMBOL]: 'MANA',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0xC6581Ce3A005e2801c1e0903281BBd318eC5B5C2'
},
'0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2': {
[NAME]: 'Maker',
[SYMBOL]: 'MKR',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0x2C4Bd064b998838076fa341A83d007FC2FA50957'
},
'0xB62132e35a6c13ee1EE0f84dC5d40bad8d815206': {
[NAME]: 'Nexo',
[SYMBOL]: 'NEXO',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0x069C97DBA948175D10af4b2414969e0B88d44669'
},
'0x1776e1F26f98b1A5dF9cD347953a26dd3Cb46671': {
[NAME]: 'Numeraire',
[SYMBOL]: 'NMR',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0x2Bf5A5bA29E60682fC56B2Fcf9cE07Bef4F6196f'
},
'0x8E870D67F660D95d5be530380D0eC0bd388289E1': {
[NAME]: 'PAX',
[SYMBOL]: 'PAX',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0xC040d51b07Aea5d94a89Bc21E8078B77366Fc6C7'
},
'0x6758B7d441a9739b98552B373703d8d3d14f9e62': {
[NAME]: 'POA ERC20 on Foundation',
[SYMBOL]: 'POA20',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0xA2E6B3EF205FeAEe475937c4883b24E6eB717eeF'
},
'0x687BfC3E73f6af55F0CccA8450114D107E781a0e': {
[NAME]: 'QChi',
[SYMBOL]: 'QCH',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0x755899F0540c3548b99E68C59AdB0f15d2695188'
},
'0x255Aa6DF07540Cb5d3d297f0D0D4D84cb52bc8e6': {
[NAME]: 'Raiden Token',
[SYMBOL]: 'RDN',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0x7D03CeCb36820b4666F45E1b4cA2538724Db271C'
},
'0x408e41876cCCDC0F92210600ef50372656052a38': {
[NAME]: 'Republic Token',
[SYMBOL]: 'REN',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0x43892992B0b102459E895B88601Bb2C76736942c'
},
'0x1985365e9f78359a9B6AD760e32412f4a445E862': {
[NAME]: 'Reputation',
[SYMBOL]: 'REP',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0x48B04d2A05B6B604d8d5223Fd1984f191DED51af'
},
'0x168296bb09e24A88805CB9c33356536B980D3fC5': {
[NAME]: 'RHOC',
[SYMBOL]: 'RHOC',
[DECIMALS]: 8,
[EXCHANGE_ADDRESS]: '0x394e524b47A3AB3D3327f7fF6629dC378c1494a3'
},
'0x607F4C5BB672230e8672085532f7e901544a7375': {
[NAME]: 'iEx.ec Network Token',
[SYMBOL]: 'RLC',
[DECIMALS]: 9,
[EXCHANGE_ADDRESS]: '0xA825CAE02B310E9901b4776806CE25db520c8642'
},
'0x4156D3342D5c385a87D264F90653733592000581': {
[NAME]: 'Salt',
[SYMBOL]: 'SALT',
[DECIMALS]: 8,
[EXCHANGE_ADDRESS]: '0xC0C59cDe851bfcbdddD3377EC10ea54A18Efb937'
},
'0x744d70FDBE2Ba4CF95131626614a1763DF805B9E': {
[NAME]: 'Status Network Token',
[SYMBOL]: 'SNT',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0x1aEC8F11A7E78dC22477e91Ed924Fab46e3A88Fd'
},
'0x3772f9716Cf6D7a09edE3587738AA2af5577483a': {
[NAME]: 'Synthetix Network Token',
[SYMBOL]: 'SNX',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0x5d8888a212d033cff5F2e0AC24ad91A5495bAD62'
},
'0x42d6622deCe394b54999Fbd73D108123806f6a18': {
[NAME]: 'SPANK',
[SYMBOL]: 'SPANK',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0x4e395304655F0796bc3bc63709DB72173b9DdF98'
},
'0x0cbe2df57ca9191b64a7af3baa3f946fa7df2f25': {
[NAME]: 'Synth sUSD',
[SYMBOL]: 'sUSD',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0xA1ECDcca26150cF69090280eE2EE32347C238c7b'
},
'0xaAAf91D9b90dF800Df4F55c205fd6989c977E73a': {
[NAME]: 'Monolith TKN',
[SYMBOL]: 'TKN',
[DECIMALS]: 8,
[EXCHANGE_ADDRESS]: '0xb6cFBf322db47D39331E306005DC7E5e6549942B'
},
'0x8dd5fbCe2F6a956C3022bA3663759011Dd51e73E': {
[NAME]: 'TrueUSD',
[SYMBOL]: 'TUSD',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0x4F30E682D0541eAC91748bd38A648d759261b8f3'
},
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': {
[NAME]: 'USD//C',
[SYMBOL]: 'USDC',
[DECIMALS]: 6,
[EXCHANGE_ADDRESS]: '0x97deC872013f6B5fB443861090ad931542878126'
},
'0x8f3470A7388c05eE4e7AF3d01D8C722b0FF52374': {
[NAME]: 'Veritaseum',
[SYMBOL]: 'VERI',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0x17e5BF07D696eaf0d14caA4B44ff8A1E17B34de3'
},
'0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599': {
[NAME]: 'Wrapped BTC',
[SYMBOL]: 'WBTC',
[DECIMALS]: 8,
[EXCHANGE_ADDRESS]: '0x4d2f5cFbA55AE412221182D8475bC85799A5644b'
},
'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2': {
[NAME]: 'Wrapped Ether',
[SYMBOL]: 'WETH',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0xA2881A90Bf33F03E7a3f803765Cd2ED5c8928dFb'
},
'0xB4272071eCAdd69d933AdcD19cA99fe80664fc08': {
[NAME]: 'CryptoFranc',
[SYMBOL]: 'XCHF',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0x8dE0d002DC83478f479dC31F76cB0a8aa7CcEa17'
},
'0x05f4a42e251f2d52b8ed15E9FEdAacFcEF1FAD27': {
[NAME]: 'Zilliqa',
[SYMBOL]: 'ZIL',
[DECIMALS]: 12,
[EXCHANGE_ADDRESS]: '0x7dc095A5CF7D6208CC680fA9866F80a53911041a'
},
'0xE41d2489571d322189246DaFA5ebDe1F4699F498': {
[NAME]: '0x Protocol Token',
[SYMBOL]: 'ZRX',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0xaE76c84C9262Cdb9abc0C2c8888e62Db8E22A0bF'
}
}
const TOKEN_DETAILS = {
1: TOKEN_DETAILS_MAINNET
}
// node creation
function createTokenDetailNode(name, symbol, decimals, exchangeAddress) {
return { [NAME]: name, [SYMBOL]: symbol, [DECIMALS]: decimals, [EXCHANGE_ADDRESS]: exchangeAddress }
}
// tree creation
function createTokenDetailTree(tokenAddress, name, symbol, decimals, exchangeAddress) {
return { [tokenAddress]: createTokenDetailNode(name, symbol, decimals, exchangeAddress) }
}
// create contexts
const TokenDetailsContext = createContext()
// define providers
function getTokenDetails(tokenAddress, networkId, library) {
const namePromise = getTokenName(tokenAddress, library).catch(() => null)
const symbolPromise = getTokenSymbol(tokenAddress, library).catch(() => null)
const decimalsPromise = getTokenDecimals(tokenAddress, library).catch(() => null)
const exchangeAddressPromise = getTokenExchangeAddressFromFactory(tokenAddress, networkId, library).catch(() => null)
return Promise.all([namePromise, symbolPromise, decimalsPromise, exchangeAddressPromise])
}
export default class Provider extends Component {
constructor(props) {
super(props)
this.initializeValues = networkId => {
this.setState({ [TOKEN_DETAIL]: TOKEN_DETAILS[networkId] || {} })
}
this.getValue = tokenAddress => {
return this.state[TOKEN_DETAIL][tokenAddress] || createTokenDetailNode()
}
this.getValues = () => {
return this.state[TOKEN_DETAIL]
}
this.updateValue = (tokenAddress, name, symbol, decimals, exchangeAddress) => {
this.setState(state => ({
[TOKEN_DETAIL]: merge(
state[TOKEN_DETAIL],
createTokenDetailTree(tokenAddress, name, symbol, decimals, exchangeAddress, true)
)
}))
}
this.forceUpdateValue = (tokenAddress, networkId, library) => {
getTokenDetails(tokenAddress, networkId, library).then(([name, symbol, decimals, exchangeAddress]) => {
this.updateValue(tokenAddress, name, symbol, decimals, exchangeAddress)
})
}
this.state = {
[TOKEN_DETAIL]: {},
initializeValues: this.initializeValues,
getValue: this.getValue,
getValues: this.getValues,
updateValue: this.updateValue,
forceUpdateValue: this.forceUpdateValue
}
}
render() {
return <TokenDetailsContext.Provider value={this.state}>{this.props.children}</TokenDetailsContext.Provider>
}
}
// define useContext wrappers
export function useTokenDetailsContext() {
return useContext(TokenDetailsContext)
}
// define custom hooks
export function Updater() {
const { networkId } = useWeb3Context()
const { initializeValues } = useTokenDetailsContext()
useEffect(() => {
if (networkId) {
initializeValues(networkId)
}
return () => {
initializeValues()
}
}, [initializeValues, networkId])
return null
}
export function useTokenDetails(tokenAddress) {
const { networkId, library } = useWeb3Context()
const { getValue, updateValue } = useTokenDetailsContext()
const { [NAME]: name, [SYMBOL]: symbol, [DECIMALS]: decimals, [EXCHANGE_ADDRESS]: exchangeAddress } = getValue(
tokenAddress
)
useEffect(() => {
if (isAddress(tokenAddress)) {
getTokenDetails(tokenAddress, networkId, library).then(([name, symbol, decimals, exchangeAddress]) => {
updateValue(tokenAddress, name, symbol, decimals, exchangeAddress)
})
}
}, [tokenAddress, networkId, library, updateValue])
return { name, symbol, decimals, exchangeAddress }
}
export function useAllTokenDetails(requireExchange = true) {
const { getValues } = useTokenDetailsContext()
const allTokenDetails = getValues()
return requireExchange
? Object.keys(allTokenDetails)
.filter(
k =>
k === 'ETH' ||
(allTokenDetails[k][EXCHANGE_ADDRESS] &&
allTokenDetails[k][EXCHANGE_ADDRESS] !== ethers.constants.AddressZero)
)
.reduce((o, k) => {
o[k] = allTokenDetails[k]
return o
}, {})
: allTokenDetails
}
import React, { Component, createContext, useContext, useCallback, useEffect } from 'react'
import { useWeb3Context } from 'web3-react'
import { ethers } from 'ethers'
import merge from 'lodash.merge'
import { useBlockEffect } from '../hooks'
const TRANSACTION = 'transaction'
const RESPONSE = 'response'
const COMPLETED = 'completed'
const RECEIPT = 'receipt'
const TransactionContext = createContext()
function removeUndefinedValues(o) {
return Object.keys(o)
.filter(k => o[k] !== undefined)
.reduce((innerO, k) => {
innerO[k] = o[k]
return innerO
}, {})
}
function createTransactionNode(response, completed, receipt, noUndefinedValues) {
const node = { [RESPONSE]: response, [COMPLETED]: completed, [RECEIPT]: receipt }
return noUndefinedValues ? removeUndefinedValues(node) : node
}
// tree creation
function createTokenDetailTree(hash, response, completed, receipt, noUndefinedValues = false) {
return { [hash]: createTransactionNode(response, completed, receipt, noUndefinedValues) }
}
export default class Provider extends Component {
constructor(props) {
super(props)
this.getTransactions = () => {
return this.state[TRANSACTION]
}
this.addTransaction = (hash, response) => {
this.setState(state => ({
[TRANSACTION]: merge(state[TRANSACTION], createTokenDetailTree(hash, response, false))
}))
}
this.updateTransaction = (hash, receipt) => {
this.setState(state => ({
[TRANSACTION]: merge(state[TRANSACTION], createTokenDetailTree(hash, undefined, true, receipt, true))
}))
}
this.clearTransactions = () => {
this.setState({ [TRANSACTION]: {} })
}
this.state = {
[TRANSACTION]: {},
getTransactions: this.getTransactions,
addTransaction: this.addTransaction,
updateTransaction: this.updateTransaction,
clearTransactions: this.clearTransactions
}
}
render() {
return <TransactionContext.Provider value={this.state}>{this.props.children}</TransactionContext.Provider>
}
}
export function useTransactionContext() {
return useContext(TransactionContext)
}
export function Updater() {
const { library, networkId } = useWeb3Context()
const { getTransactions, updateTransaction, clearTransactions } = useTransactionContext()
useEffect(() => {
return () => {
clearTransactions()
}
}, [clearTransactions, networkId])
const updateTransactionHashes = useCallback(() => {
if (library) {
const transactions = getTransactions()
Object.keys(transactions)
.filter(k => !transactions[k][COMPLETED])
.forEach(hash => {
library.getTransactionReceipt(hash).then(receipt => {
if (receipt) {
updateTransaction(hash, receipt)
}
})
})
}
}, [library, getTransactions, updateTransaction])
useBlockEffect(updateTransactionHashes)
return null
}
export function useAllTransactions() {
const { getTransactions } = useTransactionContext()
return getTransactions()
}
export function usePendingApproval(tokenAddress) {
const allTransactions = useAllTransactions()
return (
Object.keys(allTransactions).filter(hash => {
const transaction = allTransactions[hash]
if (
transaction.completed ||
transaction.response.to !== tokenAddress ||
transaction.response.data.substring(0, 10) !== ethers.utils.id('approve(address,uint256)').substring(0, 10)
) {
return false
} else {
return true
}
}).length >= 1
)
}
import { BigNumber as BN } from 'bignumber.js'
import Web3 from 'web3'
import ERC20_ABI from '../abi/erc20'
import ERC20_WITH_BYTES_ABI from '../abi/erc20_symbol_bytes32'
import ERC20_WITH_BYTES_ABI from '../abi/erc20_bytes32'
export const INITIALIZE = 'web3connect/initialize'
export const INITIALIZE_WEB3 = 'web3connect/initializeWeb3'
......
export default function(matchmask = [], minMatchCharLength = 1) {
let matchedIndices = []
let start = -1
let end = -1
let i = 0
for (let len = matchmask.length; i < len; i += 1) {
let match = matchmask[i]
if (match && start === -1) {
start = i
} else if (!match && start !== -1) {
end = i - 1
if (end - start + 1 >= minMatchCharLength) {
matchedIndices.push([start, end])
}
start = -1
}
}
// (i-1 - start) + 1 => i - start
if (matchmask[i - 1] && i - start >= minMatchCharLength) {
matchedIndices.push([start, i - 1])
}
return matchedIndices
}
export default function(pattern) {
let mask = {}
let len = pattern.length
for (let i = 0; i < len; i += 1) {
mask[pattern.charAt(i)] = 0
}
for (let i = 0; i < len; i += 1) {
mask[pattern.charAt(i)] |= 1 << (len - i - 1)
}
return mask
}
// eslint-disable-next-line no-useless-escape
const SPECIAL_CHARS_REGEX = /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g
export default function(text, pattern, tokenSeparator = / +/g) {
let regex = new RegExp(pattern.replace(SPECIAL_CHARS_REGEX, '\\$&').replace(tokenSeparator, '|'))
let matches = text.match(regex)
let isMatch = !!matches
let matchedIndices = []
if (isMatch) {
for (let i = 0, matchesLen = matches.length; i < matchesLen; i += 1) {
let match = matches[i]
matchedIndices.push([text.indexOf(match), match.length - 1])
}
}
return {
// TODO: revisit this score
score: isMatch ? 0.5 : 1,
isMatch,
matchedIndices
}
}
export default function(pattern, { errors = 0, currentLocation = 0, expectedLocation = 0, distance = 100 }) {
const accuracy = errors / pattern.length
const proximity = Math.abs(expectedLocation - currentLocation)
if (!distance) {
// Dodge divide by zero error.
return proximity ? 1.0 : accuracy
}
return accuracy + proximity / distance
}
import bitapScore from './bitap_score'
import matchedIndices from './bitap_matched_indices'
export default function(
text,
pattern,
patternAlphabet,
{ location = 0, distance = 100, threshold = 0.6, findAllMatches = false, minMatchCharLength = 1 }
) {
const expectedLocation = location
// Set starting location at beginning text and initialize the alphabet.
const textLen = text.length
// Highest score beyond which we give up.
let currentThreshold = threshold
// Is there a nearby exact match? (speedup)
let bestLocation = text.indexOf(pattern, expectedLocation)
const patternLen = pattern.length
// a mask of the matches
const matchMask = []
for (let i = 0; i < textLen; i += 1) {
matchMask[i] = 0
}
if (bestLocation !== -1) {
let score = bitapScore(pattern, {
errors: 0,
currentLocation: bestLocation,
expectedLocation,
distance
})
currentThreshold = Math.min(score, currentThreshold)
// What about in the other direction? (speed up)
bestLocation = text.lastIndexOf(pattern, expectedLocation + patternLen)
if (bestLocation !== -1) {
let score = bitapScore(pattern, {
errors: 0,
currentLocation: bestLocation,
expectedLocation,
distance
})
currentThreshold = Math.min(score, currentThreshold)
}
}
// Reset the best location
bestLocation = -1
let lastBitArr = []
let finalScore = 1
let binMax = patternLen + textLen
const mask = 1 << (patternLen - 1)
for (let i = 0; i < patternLen; i += 1) {
// Scan for the best match; each iteration allows for one more error.
// Run a binary search to determine how far from the match location we can stray
// at this error level.
let binMin = 0
let binMid = binMax
while (binMin < binMid) {
const score = bitapScore(pattern, {
errors: i,
currentLocation: expectedLocation + binMid,
expectedLocation,
distance
})
if (score <= currentThreshold) {
binMin = binMid
} else {
binMax = binMid
}
binMid = Math.floor((binMax - binMin) / 2 + binMin)
}
// Use the result from this iteration as the maximum for the next.
binMax = binMid
let start = Math.max(1, expectedLocation - binMid + 1)
let finish = findAllMatches ? textLen : Math.min(expectedLocation + binMid, textLen) + patternLen
// Initialize the bit array
let bitArr = Array(finish + 2)
bitArr[finish + 1] = (1 << i) - 1
for (let j = finish; j >= start; j -= 1) {
let currentLocation = j - 1
let charMatch = patternAlphabet[text.charAt(currentLocation)]
if (charMatch) {
matchMask[currentLocation] = 1
}
// First pass: exact match
bitArr[j] = ((bitArr[j + 1] << 1) | 1) & charMatch
// Subsequent passes: fuzzy match
if (i !== 0) {
bitArr[j] |= ((lastBitArr[j + 1] | lastBitArr[j]) << 1) | 1 | lastBitArr[j + 1]
}
if (bitArr[j] & mask) {
finalScore = bitapScore(pattern, {
errors: i,
currentLocation,
expectedLocation,
distance
})
// This match will almost certainly be better than any existing match.
// But check anyway.
if (finalScore <= currentThreshold) {
// Indeed it is
currentThreshold = finalScore
bestLocation = currentLocation
// Already passed `loc`, downhill from here on in.
if (bestLocation <= expectedLocation) {
break
}
// When passing `bestLocation`, don't exceed our current distance from `expectedLocation`.
start = Math.max(1, 2 * expectedLocation - bestLocation)
}
}
}
// No hope for a (better) match at greater error levels.
const score = bitapScore(pattern, {
errors: i + 1,
currentLocation: expectedLocation,
expectedLocation,
distance
})
// console.log('score', score, finalScore)
if (score > currentThreshold) {
break
}
lastBitArr = bitArr
}
// console.log('FINAL SCORE', finalScore)
// Count exact matches (those with a score of 0) to be "almost" exact
return {
isMatch: bestLocation >= 0,
score: finalScore === 0 ? 0.001 : finalScore,
matchedIndices: matchedIndices(matchMask, minMatchCharLength)
}
}
import bitapRegexSearch from './bitap_regex_search'
import bitapSearch from './bitap_search'
import patternAlphabet from './bitap_pattern_alphabet'
class Bitap {
constructor(
pattern,
{
// Approximately where in the text is the pattern expected to be found?
location = 0,
// Determines how close the match must be to the fuzzy location (specified above).
// An exact letter match which is 'distance' characters away from the fuzzy location
// would score as a complete mismatch. A distance of '0' requires the match be at
// the exact location specified, a threshold of '1000' would require a perfect match
// to be within 800 characters of the fuzzy location to be found using a 0.8 threshold.
distance = 100,
// At what point does the match algorithm give up. A threshold of '0.0' requires a perfect match
// (of both letters and location), a threshold of '1.0' would match anything.
threshold = 0.6,
// Machine word size
maxPatternLength = 32,
// Indicates whether comparisons should be case sensitive.
isCaseSensitive = false,
// Regex used to separate words when searching. Only applicable when `tokenize` is `true`.
tokenSeparator = / +/g,
// When true, the algorithm continues searching to the end of the input even if a perfect
// match is found before the end of the same input.
findAllMatches = false,
// Minimum number of characters that must be matched before a result is considered a match
minMatchCharLength = 1
}
) {
this.options = {
location,
distance,
threshold,
maxPatternLength,
isCaseSensitive,
tokenSeparator,
findAllMatches,
minMatchCharLength
}
this.pattern = this.options.isCaseSensitive ? pattern : pattern.toLowerCase()
if (this.pattern.length <= maxPatternLength) {
this.patternAlphabet = patternAlphabet(this.pattern)
}
}
search(text) {
if (!this.options.isCaseSensitive) {
text = text.toLowerCase()
}
// Exact match
if (this.pattern === text) {
return {
isMatch: true,
score: 0,
matchedIndices: [[0, text.length - 1]]
}
}
// When pattern length is greater than the machine word length, just do a a regex comparison
const { maxPatternLength, tokenSeparator } = this.options
if (this.pattern.length > maxPatternLength) {
return bitapRegexSearch(text, this.pattern, tokenSeparator)
}
// Otherwise, use Bitap algorithm
const { location, distance, threshold, findAllMatches, minMatchCharLength } = this.options
return bitapSearch(text, this.pattern, this.patternAlphabet, {
location,
distance,
threshold,
findAllMatches,
minMatchCharLength
})
}
}
// let x = new Bitap("od mn war", {})
// let result = x.search("Old Man's War")
// console.log(result)
export default Bitap
const isArray = require('./is_array')
const deepValue = (obj, path, list) => {
if (!path) {
// If there's no path left, we've gotten to the object we care about.
list.push(obj)
} else {
const dotIndex = path.indexOf('.')
let firstSegment = path
let remaining = null
if (dotIndex !== -1) {
firstSegment = path.slice(0, dotIndex)
remaining = path.slice(dotIndex + 1)
}
const value = obj[firstSegment]
if (value !== null && value !== undefined) {
if (!remaining && (typeof value === 'string' || typeof value === 'number')) {
list.push(value.toString())
} else if (isArray(value)) {
// Search each item in the array.
for (let i = 0, len = value.length; i < len; i += 1) {
deepValue(value[i], remaining, list)
}
} else if (remaining) {
// An object. Recurse further.
deepValue(value, remaining, list)
}
}
}
return list
}
module.exports = (obj, path) => {
return deepValue(obj, path, [])
}
module.exports = obj => (!Array.isArray ? Object.prototype.toString.call(obj) === '[object Array]' : Array.isArray(obj))
import Bitap from './bitap'
const deepValue = require('./helpers/deep_value')
const isArray = require('./helpers/is_array')
class Fuse {
constructor(
list,
{
// Approximately where in the text is the pattern expected to be found?
location = 0,
// Determines how close the match must be to the fuzzy location (specified above).
// An exact letter match which is 'distance' characters away from the fuzzy location
// would score as a complete mismatch. A distance of '0' requires the match be at
// the exact location specified, a threshold of '1000' would require a perfect match
// to be within 800 characters of the fuzzy location to be found using a 0.8 threshold.
distance = 100,
// At what point does the match algorithm give up. A threshold of '0.0' requires a perfect match
// (of both letters and location), a threshold of '1.0' would match anything.
threshold = 0.6,
// Machine word size
maxPatternLength = 32,
// Indicates whether comparisons should be case sensitive.
caseSensitive = false,
// Regex used to separate words when searching. Only applicable when `tokenize` is `true`.
tokenSeparator = / +/g,
// When true, the algorithm continues searching to the end of the input even if a perfect
// match is found before the end of the same input.
findAllMatches = false,
// Minimum number of characters that must be matched before a result is considered a match
minMatchCharLength = 1,
// The name of the identifier property. If specified, the returned result will be a list
// of the items' dentifiers, otherwise it will be a list of the items.
id = null,
// List of properties that will be searched. This also supports nested properties.
keys = [],
// Whether to sort the result list, by score
shouldSort = true,
// The get function to use when fetching an object's properties.
// The default will search nested paths *ie foo.bar.baz*
getFn = deepValue,
// Default sort function
sortFn = (a, b) => a.score - b.score,
// When true, the search algorithm will search individual words **and** the full string,
// computing the final score as a function of both. Note that when `tokenize` is `true`,
// the `threshold`, `distance`, and `location` are inconsequential for individual tokens.
tokenize = false,
// When true, the result set will only include records that match all tokens. Will only work
// if `tokenize` is also true.
matchAllTokens = false,
includeMatches = false,
includeScore = false,
// Will print to the console. Useful for debugging.
verbose = false
}
) {
this.options = {
location,
distance,
threshold,
maxPatternLength,
isCaseSensitive: caseSensitive,
tokenSeparator,
findAllMatches,
minMatchCharLength,
id,
keys,
includeMatches,
includeScore,
shouldSort,
getFn,
sortFn,
verbose,
tokenize,
matchAllTokens
}
this.setCollection(list)
}
setCollection(list) {
this.list = list
return list
}
search(pattern) {
this._log(`---------\nSearch pattern: "${pattern}"`)
const { tokenSearchers, fullSearcher } = this._prepareSearchers(pattern)
let { weights, results } = this._search(tokenSearchers, fullSearcher)
this._computeScore(weights, results)
if (this.options.shouldSort) {
this._sort(results)
}
return this._format(results)
}
_prepareSearchers(pattern = '') {
const tokenSearchers = []
if (this.options.tokenize) {
// Tokenize on the separator
const tokens = pattern.split(this.options.tokenSeparator)
for (let i = 0, len = tokens.length; i < len; i += 1) {
tokenSearchers.push(new Bitap(tokens[i], this.options))
}
}
let fullSearcher = new Bitap(pattern, this.options)
return { tokenSearchers, fullSearcher }
}
_search(tokenSearchers = [], fullSearcher) {
const list = this.list
const resultMap = {}
const results = []
// Check the first item in the list, if it's a string, then we assume
// that every item in the list is also a string, and thus it's a flattened array.
if (typeof list[0] === 'string') {
// Iterate over every item
for (let i = 0, len = list.length; i < len; i += 1) {
this._analyze(
{
key: '',
value: list[i],
record: i,
index: i
},
{
resultMap,
results,
tokenSearchers,
fullSearcher
}
)
}
return { weights: null, results }
}
// Otherwise, the first item is an Object (hopefully), and thus the searching
// is done on the values of the keys of each item.
const weights = {}
for (let i = 0, len = list.length; i < len; i += 1) {
let item = list[i]
// Iterate over every key
for (let j = 0, keysLen = this.options.keys.length; j < keysLen; j += 1) {
let key = this.options.keys[j]
if (typeof key !== 'string') {
weights[key.name] = {
weight: 1 - key.weight || 1
}
if (key.weight <= 0 || key.weight > 1) {
throw new Error('Key weight has to be > 0 and <= 1')
}
key = key.name
} else {
weights[key] = {
weight: 1
}
}
this._analyze(
{
key,
value: this.options.getFn(item, key),
record: item,
index: i
},
{
resultMap,
results,
tokenSearchers,
fullSearcher
}
)
}
}
return { weights, results }
}
_analyze(
{ key, arrayIndex = -1, value, record, index },
{ tokenSearchers = [], fullSearcher = [], resultMap = {}, results = [] }
) {
// Check if the texvaluet can be searched
if (value === undefined || value === null) {
return
}
let exists = false
let averageScore = -1
let numTextMatches = 0
if (typeof value === 'string') {
this._log(`\nKey: ${key === '' ? '-' : key}`)
let mainSearchResult = fullSearcher.search(value)
this._log(`Full text: "${value}", score: ${mainSearchResult.score}`)
if (this.options.tokenize) {
let words = value.split(this.options.tokenSeparator)
let scores = []
for (let i = 0; i < tokenSearchers.length; i += 1) {
let tokenSearcher = tokenSearchers[i]
this._log(`\nPattern: "${tokenSearcher.pattern}"`)
// let tokenScores = []
let hasMatchInText = false
for (let j = 0; j < words.length; j += 1) {
let word = words[j]
let tokenSearchResult = tokenSearcher.search(word)
let obj = {}
if (tokenSearchResult.isMatch) {
obj[word] = tokenSearchResult.score
exists = true
hasMatchInText = true
scores.push(tokenSearchResult.score)
} else {
obj[word] = 1
if (!this.options.matchAllTokens) {
scores.push(1)
}
}
this._log(`Token: "${word}", score: ${obj[word]}`)
// tokenScores.push(obj)
}
if (hasMatchInText) {
numTextMatches += 1
}
}
averageScore = scores[0]
let scoresLen = scores.length
for (let i = 1; i < scoresLen; i += 1) {
averageScore += scores[i]
}
averageScore = averageScore / scoresLen
this._log('Token score average:', averageScore)
}
let finalScore = mainSearchResult.score
if (averageScore > -1) {
finalScore = (finalScore + averageScore) / 2
}
this._log('Score average:', finalScore)
let checkTextMatches =
this.options.tokenize && this.options.matchAllTokens ? numTextMatches >= tokenSearchers.length : true
this._log(`\nCheck Matches: ${checkTextMatches}`)
// If a match is found, add the item to <rawResults>, including its score
if ((exists || mainSearchResult.isMatch) && checkTextMatches) {
// Check if the item already exists in our results
let existingResult = resultMap[index]
if (existingResult) {
// Use the lowest score
// existingResult.score, bitapResult.score
existingResult.output.push({
key,
arrayIndex,
value,
score: finalScore,
matchedIndices: mainSearchResult.matchedIndices
})
} else {
// Add it to the raw result list
resultMap[index] = {
item: record,
output: [
{
key,
arrayIndex,
value,
score: finalScore,
matchedIndices: mainSearchResult.matchedIndices
}
]
}
results.push(resultMap[index])
}
}
} else if (isArray(value)) {
for (let i = 0, len = value.length; i < len; i += 1) {
this._analyze(
{
key,
arrayIndex: i,
value: value[i],
record,
index
},
{
resultMap,
results,
tokenSearchers,
fullSearcher
}
)
}
}
}
_computeScore(weights, results) {
this._log('\n\nComputing score:\n')
for (let i = 0, len = results.length; i < len; i += 1) {
const output = results[i].output
const scoreLen = output.length
let currScore = 1
let bestScore = 1
for (let j = 0; j < scoreLen; j += 1) {
let weight = weights ? weights[output[j].key].weight : 1
let score = weight === 1 ? output[j].score : output[j].score || 0.001
let nScore = score * weight
if (weight !== 1) {
bestScore = Math.min(bestScore, nScore)
} else {
output[j].nScore = nScore
currScore *= nScore
}
}
results[i].score = bestScore === 1 ? currScore : bestScore
this._log(results[i])
}
}
_sort(results) {
this._log('\n\nSorting....')
results.sort(this.options.sortFn)
}
_format(results) {
const finalOutput = []
if (this.options.verbose) {
this._log('\n\nOutput:\n\n', JSON.stringify(results))
}
let transformers = []
if (this.options.includeMatches) {
transformers.push((result, data) => {
const output = result.output
data.matches = []
for (let i = 0, len = output.length; i < len; i += 1) {
let item = output[i]
if (item.matchedIndices.length === 0) {
continue
}
let obj = {
indices: item.matchedIndices,
value: item.value
}
if (item.key) {
obj.key = item.key
}
if (item.hasOwnProperty('arrayIndex') && item.arrayIndex > -1) {
obj.arrayIndex = item.arrayIndex
}
data.matches.push(obj)
}
})
}
if (this.options.includeScore) {
transformers.push((result, data) => {
data.score = result.score
})
}
for (let i = 0, len = results.length; i < len; i += 1) {
const result = results[i]
if (this.options.id) {
result.item = this.options.getFn(result.item, this.options.id)[0]
}
if (!transformers.length) {
finalOutput.push(result.item)
continue
}
const data = {
item: result.item
}
for (let j = 0, len = transformers.length; j < len; j += 1) {
transformers[j](result, data)
}
finalOutput.push(data)
}
return finalOutput
}
_log() {
if (this.options.verbose) {
console.log(...arguments)
}
}
}
export default Fuse
import { useMemo, useEffect } from 'react'
import { useMemo, useCallback, useEffect } from 'react'
import { useWeb3Context } from 'web3-react'
import FACTORY_ABI from '../abi/factory'
import { getSignerOrProvider, getContract } from '../utils'
import ERC20_ABI from '../abi/erc20'
import { getContract, getFactoryContract, getExchangeContract } from '../utils'
const factoryAddresses = {
1: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95',
4: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36'
// modified from https://usehooks.com/useKeyPress/
export function useBodyKeyDown(targetKey, onKeyDown, suppressOnKeyDown = false) {
const downHandler = useCallback(
({ target: { tagName }, key }) => {
if (key === targetKey && tagName === 'BODY' && !suppressOnKeyDown) {
onKeyDown()
}
},
[targetKey, onKeyDown, suppressOnKeyDown]
)
useEffect(() => {
window.addEventListener('keydown', downHandler)
return () => {
window.removeEventListener('keydown', downHandler)
}
}, [downHandler])
}
export function useSignerOrProvider() {
const { library, account } = useWeb3Context()
export function useBlockEffect(functionToRun) {
const { library } = useWeb3Context()
return useMemo(() => getSignerOrProvider(library, account), [library, account])
useEffect(() => {
if (library) {
function wrappedEffect(blockNumber) {
functionToRun(blockNumber)
}
library.on('block', wrappedEffect)
return () => {
library.removeListener('block', wrappedEffect)
}
}
}, [library, functionToRun])
}
// returns null if the contract cannot be created for any reason
function useContract(contractAddress, ABI) {
const signerOrProvider = useSignerOrProvider()
// returns null on errors
export function useContract(address, ABI, withSignerIfPossible = true) {
const { library, account } = useWeb3Context()
return useMemo(() => {
try {
return getContract(contractAddress, ABI, signerOrProvider)
return getContract(address, ABI, library, withSignerIfPossible ? account : undefined)
} catch {
return null
}
}, [contractAddress, ABI, signerOrProvider])
}, [address, ABI, library, withSignerIfPossible, account])
}
export function useFactoryContract() {
const { networkId } = useWeb3Context()
// returns null on errors
export function useTokenContract(tokenAddress, withSignerIfPossible = true) {
const { library, account } = useWeb3Context()
return useContract(factoryAddresses[networkId], FACTORY_ABI)
return useMemo(() => {
try {
return getContract(tokenAddress, ERC20_ABI, library, withSignerIfPossible ? account : undefined)
} catch {
return null
}
}, [tokenAddress, library, withSignerIfPossible, account])
}
// 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()
}
// returns null on errors
export function useFactoryContract(withSignerIfPossible = true) {
const { networkId, library, account } = useWeb3Context()
return useMemo(() => {
try {
return getFactoryContract(networkId, library, withSignerIfPossible ? account : undefined)
} catch {
return null
}
}, [networkId, library, withSignerIfPossible, account])
}
useEffect(() => {
window.addEventListener('keydown', downHandler)
return () => {
window.removeEventListener('keydown', downHandler)
export function useExchangeContract(exchangeAddress, withSignerIfPossible = true) {
const { library, account } = useWeb3Context()
return useMemo(() => {
try {
return getExchangeContract(exchangeAddress, library, withSignerIfPossible ? account : undefined)
} catch {
return null
}
}, [targetKey, onKeyDown, suppressOnKeyDown])
}, [exchangeAddress, library, withSignerIfPossible, account])
}
......@@ -3,11 +3,16 @@ import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import ReactGA from 'react-ga'
import Web3Provider, { Connectors } from 'web3-react'
import ApplicationContextProvider, { Updater as ApplicationContextUpdater } from './contexts/Application'
import TransactionContextProvider, { Updater as TransactionUpdater } from './contexts/Transaction'
import StaticContextProvider, { Updater as StaticContextUpdater } from './contexts/Static'
import BlockContextProvider, { Updater as BlockContextUpdater } from './contexts/Block'
import './i18n'
import App from './pages/App'
import store from './store'
import './index.scss'
import './i18n'
if (process.env.NODE_ENV === 'production') {
ReactGA.initialize('UA-128182339-1')
......@@ -23,10 +28,36 @@ const Infura = new NetworkOnlyConnector({
})
const connectors = { Injected, Infura }
function ContextProviders({ children }) {
return (
<ApplicationContextProvider>
<TransactionContextProvider>
<StaticContextProvider>
<BlockContextProvider>{children}</BlockContextProvider>
</StaticContextProvider>
</TransactionContextProvider>
</ApplicationContextProvider>
)
}
function Updaters() {
return (
<>
<ApplicationContextUpdater />
<TransactionUpdater />
<StaticContextUpdater />
<BlockContextUpdater />
</>
)
}
ReactDOM.render(
<Provider store={store}>
<Web3Provider connectors={connectors} libraryName="ethers.js">
<ContextProviders>
<Updaters />
<App />
</ContextProviders>
</Web3Provider>
</Provider>,
document.getElementById('root')
......
......@@ -27,14 +27,14 @@ function App({ initialized, setAddresses, updateNetwork, updateAccount, initiali
context.setConnector('Infura')
}
})
}, [])
}, []) // eslint-disable-line react-hooks/exhaustive-deps
// 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])
}, [context, context.error, context.connectorName])
// initialize redux network
const [reduxNetworkInitialized, setReduxNetworkInitialized] = useState(false)
......@@ -44,7 +44,7 @@ function App({ initialized, setAddresses, updateNetwork, updateAccount, initiali
updateNetwork(context.library._web3Provider, context.networkId)
setReduxNetworkInitialized(true)
}
}, [context.active, context.networkId])
}, [context.active, context.networkId]) // eslint-disable-line react-hooks/exhaustive-deps
// initialize redux account
const [reduxAccountInitialized, setReduxAccountInitialized] = useState(false)
......@@ -53,14 +53,14 @@ function App({ initialized, setAddresses, updateNetwork, updateAccount, initiali
updateAccount(context.account)
setReduxAccountInitialized(true)
}
}, [context.active, context.account])
}, [context.active, context.account]) // eslint-disable-line react-hooks/exhaustive-deps
// initialize redux
useEffect(() => {
if (reduxNetworkInitialized && reduxAccountInitialized) {
initialize().then(startWatching)
}
}, [reduxNetworkInitialized, reduxAccountInitialized])
}, [reduxNetworkInitialized, reduxAccountInitialized]) // eslint-disable-line react-hooks/exhaustive-deps
// active state
if (initialized || context.error) {
......
import React, { useState, useEffect } from 'react'
import { connect } from 'react-redux'
import { withRouter } from 'react-router'
import { useWeb3Context } from 'web3-react'
import { ethers } from 'ethers'
import classnames from 'classnames'
import { withRouter } from 'react-router'
import { useTranslation } from 'react-i18next'
import ReactGA from 'react-ga'
import { useWeb3Context } from 'web3-react'
import { addPendingTx } from '../../ducks/web3connect'
import AddressInputPanel from '../../components/AddressInputPanel'
import OversizedPanel from '../../components/OversizedPanel'
import { addExchange } from '../../ducks/addresses'
import { useSignerOrProvider, useFactoryContract } from '../../hooks'
import { isAddress, getTokenDetails, getExchangeDetails, errorCodes } from '../../utils'
import { useTokenDetails } from '../../contexts/Static'
import { useTransactionContext } from '../../contexts/Transaction'
import { useFactoryContract } from '../../hooks'
import { isAddress } from '../../utils'
function CreateExchange({ history, location, addExchange, addPendingTx }) {
function CreateExchange({ history, location }) {
const { t } = useTranslation()
const context = useWeb3Context()
const signerOrProvider = useSignerOrProvider()
const { account } = useWeb3Context()
const factory = useFactoryContract()
const [tokenAddress, setTokenAddress] = useState(location.state && location.state.tokenAddress)
const [errorMessage, _setErrorMessage] = useState(context.account ? undefined : t('noWallet'))
const [tokenDetails, setTokenDetails] = useState()
// wrap _setErrorMessage to ensure an account is in context
function setErrorMessage(value) {
if (value) {
_setErrorMessage(value)
} else if (!context.account) {
_setErrorMessage(t('noWallet'))
} else {
_setErrorMessage()
}
}
const [tokenAddress, setTokenAddress] = useState((location.state && location.state.tokenAddress) || '')
const { name, symbol, decimals, exchangeAddress } = useTokenDetails(tokenAddress)
const { addTransaction } = useTransactionContext()
// clear state, if it exists
// clear location state, if it exists
useEffect(() => {
if (location.state) {
history.replace(location.pathname)
}
}, [])
}, []) // eslint-disable-line react-hooks/exhaustive-deps
// handle changes to tokenAddress
// validate everything
const [errorMessage, setErrorMessage] = useState(!account && t('noWallet'))
useEffect(() => {
let stale = false
// happy path
if (isAddress(tokenAddress)) {
const tokenDetailsPromise = getTokenDetails(tokenAddress, signerOrProvider)
const exchangeDetailsPromise = getExchangeDetails(context.networkId, tokenAddress, signerOrProvider)
Promise.all([tokenDetailsPromise, exchangeDetailsPromise])
.then(([tokenDetails, exchangeDetails]) => {
if (!stale) {
if (exchangeDetails.exchangeAddress !== ethers.constants.AddressZero) {
addExchange({
tokenAddress,
label: tokenDetails.symbol,
exchangeAddress: exchangeDetails.exchangeAddress
})
setErrorMessage(t('exchangeExists', { tokenAddress }))
}
setTokenDetails(tokenDetails)
}
})
.catch(error => {
if (!stale) {
if (error.code === errorCodes.TOKEN_DETAILS_DECIMALS) {
setErrorMessage(t('invalidDecimals'))
} else if (error.code === errorCodes.TOKEN_DETAILS_SYMBOL) {
setErrorMessage(t('invalidSymbol'))
} else {
if (tokenAddress && !isAddress(tokenAddress)) {
setErrorMessage(t('invalidTokenAddress'))
}
}
})
}
// is tokenAddress is empty, there's no error
else if (tokenAddress === undefined || tokenAddress === '') {
} else if (!tokenAddress || symbol === undefined || decimals === undefined || exchangeAddress === undefined) {
setErrorMessage()
}
// tokenAddress is not a proper address
else {
setErrorMessage(t('invalidTokenAddress'))
} else if (symbol === null) {
setErrorMessage(t('invalidSymbol'))
} else if (decimals === null) {
setErrorMessage(t('invalidDecimals'))
} else if (exchangeAddress !== ethers.constants.AddressZero) {
setErrorMessage(t('exchangeExists'))
} else if (!account) {
setErrorMessage(t('noWallet'))
} else {
setErrorMessage(null)
}
return () => {
stale = true
setErrorMessage()
setTokenDetails()
}
}, [tokenAddress, signerOrProvider, context.networkId])
}, [tokenAddress, symbol, decimals, exchangeAddress, account, t])
async function createExchange() {
const estimatedGasLimit = await factory.estimate.createExchange(tokenAddress)
factory.createExchange(tokenAddress, { gasLimit: estimatedGasLimit }).then(details => {
addPendingTx(details.hash)
setErrorMessage()
setTokenAddress()
factory.createExchange(tokenAddress, { gasLimit: estimatedGasLimit }).then(response => {
addTransaction(response.hash, response)
ReactGA.event({
category: 'Pool',
action: 'CreateExchange'
......@@ -107,25 +65,31 @@ function CreateExchange({ history, location, addExchange, addPendingTx }) {
})
}
const isValid = isAddress(tokenAddress) && !errorMessage && tokenDetails && tokenDetails.tokenAddress === tokenAddress
const isValid = errorMessage === null
return (
<>
<AddressInputPanel
title={t('tokenAddress')}
value={tokenAddress}
onChange={input => setTokenAddress(input)}
onChange={input => {
setTokenAddress(input)
}}
errorMessage={errorMessage === t('noWallet') ? '' : errorMessage}
/>
<OversizedPanel hideBottom>
<div className="pool__summary-panel">
<div className="pool__exchange-rate-wrapper">
<span className="pool__exchange-rate">{t('name')}</span>
<span>{name ? name : ' - '}</span>
</div>
<div className="pool__exchange-rate-wrapper">
<span className="pool__exchange-rate">{t('symbol')}</span>
<span>{tokenDetails ? tokenDetails.symbol : ' - '}</span>
<span>{symbol ? symbol : ' - '}</span>
</div>
<div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t('decimals')}</span>
<span>{tokenDetails ? tokenDetails.decimals : ' - '}</span>
<span>{decimals || decimals === 0 ? decimals : ' - '}</span>
</div>
</div>
</OversizedPanel>
......@@ -135,7 +99,7 @@ function CreateExchange({ history, location, addExchange, addPendingTx }) {
'create-exchange--error': !!errorMessage
})}
>
{!!errorMessage ? errorMessage : t('enterTokenCont')}
{errorMessage ? errorMessage : t('enterTokenCont')}
</div>
</div>
<div className="pool__cta-container">
......@@ -147,12 +111,4 @@ function CreateExchange({ history, location, addExchange, addPendingTx }) {
)
}
export default withRouter(
connect(
undefined,
dispatch => ({
addExchange: opts => dispatch(addExchange(opts)),
addPendingTx: id => dispatch(addPendingTx(id))
})
)(CreateExchange)
)
export default withRouter(CreateExchange)
import React, { Component } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import { BigNumber as BN } from 'bignumber.js'
import { withTranslation, useTranslation } from 'react-i18next'
import React, { useState, useReducer, useEffect } from 'react'
import ReactGA from 'react-ga'
import { useTranslation } from 'react-i18next'
import { useWeb3Context } from 'web3-react'
import { ethers } from 'ethers'
import { selectors, addPendingTx } from '../../ducks/web3connect'
import AddressInputPanel from '../../components/AddressInputPanel'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import ContextualInfo from '../../components/ContextualInfo'
import OversizedPanel from '../../components/OversizedPanel'
import AddressInputPanel from '../../components/AddressInputPanel'
import ArrowDownBlue from '../../assets/images/arrow-down-blue.svg'
import ArrowDownGrey from '../../assets/images/arrow-down-grey.svg'
import { getBlockDeadline } from '../../helpers/web3-utils'
import { retry } from '../../helpers/promise-utils'
import EXCHANGE_ABI from '../../abi/exchange'
import { useAddressBalance, useAddressAllowance, useExchangeReserves } from '../../contexts/Block'
import { useTokenDetails } from '../../contexts/Static'
import { useTransactionContext } from '../../contexts/Transaction'
import { amountFormatter, calculateGasMargin } from '../../utils'
import { useExchangeContract } from '../../hooks'
import './send.scss'
import { isAddress } from 'web3-utils'
const INPUT = 0
const OUTPUT = 1
class Send extends Component {
static propTypes = {
account: PropTypes.string,
selectors: PropTypes.func.isRequired,
web3: PropTypes.object.isRequired
}
const ETH_TO_TOKEN = 0
const TOKEN_TO_ETH = 1
const TOKEN_TO_TOKEN = 2
state = {
inputValue: '',
outputValue: '',
inputCurrency: 'ETH',
outputCurrency: '',
inputAmountB: '',
lastEditedField: '',
recipient: ''
}
// denominated in bips
const ALLOWED_SLIPPAGE = ethers.utils.bigNumberify(200)
const TOKEN_ALLOWED_SLIPPAGE = ethers.utils.bigNumberify(400)
componentWillMount() {
ReactGA.pageview(window.location.pathname + window.location.search)
}
// denominated in seconds
const DEADLINE_FROM_NOW = 60 * 15
shouldComponentUpdate(nextProps, nextState) {
return true
}
// denominated in bips
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
reset() {
this.setState({
inputValue: '',
outputValue: '',
inputAmountB: '',
lastEditedField: '',
recipient: ''
})
function calculateSlippageBounds(value, token = false) {
if (value) {
const offset = value.mul(token ? TOKEN_ALLOWED_SLIPPAGE : ALLOWED_SLIPPAGE).div(ethers.utils.bigNumberify(10000))
const minimum = value.sub(offset)
const maximum = value.add(offset)
return {
minimum: minimum.lt(ethers.constants.Zero) ? ethers.constants.Zero : minimum,
maximum: maximum.gt(ethers.constants.MaxUint256) ? ethers.constants.MaxUint256 : maximum
}
componentWillReceiveProps() {
this.recalcForm()
} else {
return {}
}
}
validate() {
const { selectors, account, web3 } = this.props
const { inputValue, outputValue, inputCurrency, outputCurrency, recipient } = this.state
let inputError = ''
let outputError = ''
let isValid = true
const validRecipientAddress = web3 && web3.utils.isAddress(recipient)
const inputIsZero = BN(inputValue).isZero()
const outputIsZero = BN(outputValue).isZero()
if (
!inputValue ||
inputIsZero ||
!outputValue ||
outputIsZero ||
!inputCurrency ||
!outputCurrency ||
!recipient ||
this.isUnapproved() ||
!validRecipientAddress
) {
isValid = false
function getSwapType(inputCurrency, outputCurrency) {
if (!inputCurrency || !outputCurrency) {
return null
} else if (inputCurrency === 'ETH') {
return ETH_TO_TOKEN
} else if (outputCurrency === 'ETH') {
return TOKEN_TO_ETH
} else {
return TOKEN_TO_TOKEN
}
}
const { value: inputBalance, decimals: inputDecimals } = selectors().getBalance(account, inputCurrency)
// this mocks the getInputPrice function, and calculates the required output
function calculateEtherTokenOutputFromInput(inputAmount, inputReserve, outputReserve) {
const inputAmountWithFee = inputAmount.mul(ethers.utils.bigNumberify(997))
const numerator = inputAmountWithFee.mul(outputReserve)
const denominator = inputReserve.mul(ethers.utils.bigNumberify(1000)).add(inputAmountWithFee)
return numerator.div(denominator)
}
if (inputBalance.isLessThan(BN(inputValue * 10 ** inputDecimals))) {
inputError = this.props.t('insufficientBalance')
}
// this mocks the getOutputPrice function, and calculates the required input
function calculateEtherTokenInputFromOutput(outputAmount, inputReserve, outputReserve) {
const numerator = inputReserve.mul(outputAmount).mul(ethers.utils.bigNumberify(1000))
const denominator = outputReserve.sub(outputAmount).mul(ethers.utils.bigNumberify(997))
return numerator.div(denominator).add(ethers.constants.One)
}
if (inputValue === 'N/A') {
inputError = this.props.t('inputNotValid')
}
const initialSwapState = {
independentValue: '', // this is a user input
dependentValue: '', // this is a calculated number
independentField: INPUT,
inputCurrency: 'ETH',
outputCurrency: ''
}
function swapStateReducer(state, action) {
switch (action.type) {
case 'FLIP_INDEPENDENT': {
const { independentField, inputCurrency, outputCurrency } = state
return {
inputError,
outputError,
isValid: isValid && !inputError && !outputError
}
}
flipInputOutput = () => {
const { state } = this
this.setState(
{
inputValue: state.outputValue,
outputValue: state.inputValue,
inputCurrency: state.outputCurrency,
outputCurrency: state.inputCurrency,
lastEditedField: state.lastEditedField === INPUT ? OUTPUT : INPUT
},
() => this.recalcForm()
)
...state,
dependentValue: '',
independentField: independentField === INPUT ? OUTPUT : INPUT,
inputCurrency: outputCurrency,
outputCurrency: inputCurrency
}
isUnapproved() {
const { account, exchangeAddresses, selectors } = this.props
const { inputCurrency, inputValue } = this.state
if (!inputCurrency || inputCurrency === 'ETH') {
return false
}
case 'SELECT_CURRENCY': {
const { inputCurrency, outputCurrency } = state
const { field, currency } = action.payload
const { value: allowance, label, decimals } = selectors().getApprovals(
inputCurrency,
account,
exchangeAddresses.fromToken[inputCurrency]
)
const newInputCurrency = field === INPUT ? currency : inputCurrency
const newOutputCurrency = field === OUTPUT ? currency : outputCurrency
if (label && allowance.isLessThan(BN(inputValue * 10 ** decimals || 0))) {
return true
if (newInputCurrency === newOutputCurrency) {
return {
...state,
inputCurrency: field === INPUT ? currency : '',
outputCurrency: field === OUTPUT ? currency : ''
}
return false
} else {
return {
...state,
inputCurrency: newInputCurrency,
outputCurrency: newOutputCurrency
}
recalcForm() {
const { inputCurrency, outputCurrency, lastEditedField } = this.state
if (!inputCurrency || !outputCurrency) {
return
}
const editedValue = lastEditedField === INPUT ? this.state.inputValue : this.state.outputValue
if (BN(editedValue).isZero()) {
return
}
if (inputCurrency === outputCurrency) {
this.setState({
inputValue: '',
outputValue: ''
})
return
case 'UPDATE_INDEPENDENT': {
const { field, value } = action.payload
return {
...state,
independentValue: value,
dependentValue: '',
independentField: field
}
if (inputCurrency !== 'ETH' && outputCurrency !== 'ETH') {
this.recalcTokenTokenForm()
return
}
this.recalcEthTokenForm()
case 'UPDATE_DEPENDENT': {
return {
...state,
dependentValue: action.payload
}
recalcTokenTokenForm = () => {
const {
exchangeAddresses: { fromToken },
selectors
} = this.props
const {
inputValue: oldInputValue,
outputValue: oldOutputValue,
inputCurrency,
outputCurrency,
lastEditedField,
exchangeRate: oldExchangeRate,
inputAmountB: oldInputAmountB
} = this.state
const exchangeAddressA = fromToken[inputCurrency]
const exchangeAddressB = fromToken[outputCurrency]
const { value: inputReserveA, decimals: inputDecimalsA } = selectors().getBalance(exchangeAddressA, inputCurrency)
const { value: outputReserveA } = selectors().getBalance(exchangeAddressA, 'ETH')
const { value: inputReserveB } = selectors().getBalance(exchangeAddressB, 'ETH')
const { value: outputReserveB, decimals: outputDecimalsB } = selectors().getBalance(
exchangeAddressB,
outputCurrency
)
if (lastEditedField === INPUT) {
if (!oldInputValue) {
return this.setState({
outputValue: '',
exchangeRate: BN(0)
})
}
const inputAmountA = BN(oldInputValue).multipliedBy(10 ** inputDecimalsA)
const outputAmountA = calculateEtherTokenOutput({
inputAmount: inputAmountA,
inputReserve: inputReserveA,
outputReserve: outputReserveA
})
// Redundant Variable for readability of the formala
// OutputAmount from the first send becomes InputAmount of the second send
const inputAmountB = outputAmountA
const outputAmountB = calculateEtherTokenOutput({
inputAmount: inputAmountB,
inputReserve: inputReserveB,
outputReserve: outputReserveB
})
const outputValue = outputAmountB.dividedBy(BN(10 ** outputDecimalsB)).toFixed(7)
const exchangeRate = BN(outputValue).dividedBy(BN(oldInputValue))
const appendState = {}
if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) {
appendState.exchangeRate = exchangeRate
default: {
return initialSwapState
}
if (outputValue !== oldOutputValue) {
appendState.outputValue = outputValue
}
}
this.setState(appendState)
function getExchangeRate(inputValue, inputDecimals, outputValue, outputDecimals, invert = false) {
try {
if (inputValue && inputDecimals && outputValue && outputDecimals) {
const factor = ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18))
if (invert) {
return inputValue
.mul(factor)
.div(outputValue)
.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(outputDecimals)))
.div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(inputDecimals)))
} else {
return outputValue
.mul(factor)
.div(inputValue)
.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(inputDecimals)))
.div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(outputDecimals)))
}
}
} catch {}
}
if (lastEditedField === OUTPUT) {
if (!oldOutputValue) {
return this.setState({
inputValue: '',
exchangeRate: BN(0)
})
function getMarketRate(
swapType,
inputReserveETH,
inputReserveToken,
inputDecimals,
outputReserveETH,
outputReserveToken,
outputDecimals,
invert = false
) {
if (swapType === ETH_TO_TOKEN) {
return getExchangeRate(outputReserveETH, inputDecimals, outputReserveToken, outputDecimals, invert)
} else if (swapType === TOKEN_TO_ETH) {
return getExchangeRate(inputReserveToken, inputDecimals, inputReserveETH, outputDecimals, invert)
} else if (swapType === TOKEN_TO_TOKEN) {
const factor = ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18))
const firstRate = getExchangeRate(inputReserveToken, inputDecimals, inputReserveETH, outputDecimals)
const secondRate = getExchangeRate(outputReserveETH, inputDecimals, outputReserveToken, outputDecimals)
try {
return !!(firstRate && secondRate) ? firstRate.mul(secondRate).div(factor) : undefined
} catch {}
}
}
const outputAmountB = BN(oldOutputValue).multipliedBy(10 ** outputDecimalsB)
const inputAmountB = calculateEtherTokenInput({
outputAmount: outputAmountB,
inputReserve: inputReserveB,
outputReserve: outputReserveB
})
export default function Swap() {
const { t } = useTranslation()
const { account } = useWeb3Context()
// Redundant Variable for readability of the formala
// InputAmount from the first send becomes OutputAmount of the second send
const outputAmountA = inputAmountB
const inputAmountA = calculateEtherTokenInput({
outputAmount: outputAmountA,
inputReserve: inputReserveA,
outputReserve: outputReserveA
})
const { addTransaction } = useTransactionContext()
// analytics
useEffect(() => {
ReactGA.pageview(window.location.pathname + window.location.search)
}, [])
const inputValue = inputAmountA.isNegative() ? 'N/A' : inputAmountA.dividedBy(BN(10 ** inputDecimalsA)).toFixed(7)
const exchangeRate = BN(oldOutputValue).dividedBy(BN(inputValue))
// core swap state
const [swapState, dispatchSwapState] = useReducer(swapStateReducer, initialSwapState)
const { independentValue, dependentValue, independentField, inputCurrency, outputCurrency } = swapState
const appendState = {}
const [recipient, setRecipient] = useState({ address: '', name: '' })
const [recipientError, setRecipientError] = useState()
if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) {
appendState.exchangeRate = exchangeRate
}
// get swap type from the currency types
const swapType = getSwapType(inputCurrency, outputCurrency)
if (inputValue !== oldInputValue) {
appendState.inputValue = inputValue
}
// get decimals and exchange addressfor each of the currency types
const { symbol: inputSymbol, decimals: inputDecimals, exchangeAddress: inputExchangeAddress } = useTokenDetails(
inputCurrency
)
const { symbol: outputSymbol, decimals: outputDecimals, exchangeAddress: outputExchangeAddress } = useTokenDetails(
outputCurrency
)
if (!inputAmountB.isEqualTo(BN(oldInputAmountB))) {
appendState.inputAmountB = inputAmountB
}
const inputExchangeContract = useExchangeContract(inputExchangeAddress)
const outputExchangeContract = useExchangeContract(outputExchangeAddress)
const contract = swapType === ETH_TO_TOKEN ? outputExchangeContract : inputExchangeContract
// get input allowance
const inputAllowance = useAddressAllowance(account, inputCurrency, inputExchangeAddress)
// fetch reserves for each of the currency types
const { reserveETH: inputReserveETH, reserveToken: inputReserveToken } = useExchangeReserves(inputCurrency)
const { reserveETH: outputReserveETH, reserveToken: outputReserveToken } = useExchangeReserves(outputCurrency)
// get balances for each of the currency types
const inputBalance = useAddressBalance(account, inputCurrency)
const outputBalance = useAddressBalance(account, outputCurrency)
const inputBalanceFormatted = !!(inputBalance && Number.isInteger(inputDecimals))
? amountFormatter(inputBalance, inputDecimals, Math.min(4, inputDecimals))
: ''
const outputBalanceFormatted = !!(outputBalance && Number.isInteger(outputDecimals))
? amountFormatter(outputBalance, outputDecimals, Math.min(4, outputDecimals))
: ''
// compute useful transforms of the data above
const independentDecimals = independentField === INPUT ? inputDecimals : outputDecimals
const dependentDecimals = independentField === OUTPUT ? inputDecimals : outputDecimals
// declare/get parsed and formatted versions of input/output values
const [independentValueParsed, setIndependentValueParsed] = useState()
const dependentValueFormatted = !!(dependentValue && dependentDecimals)
? amountFormatter(dependentValue, dependentDecimals, Math.min(4, dependentDecimals), false)
: ''
const inputValueParsed = independentField === INPUT ? independentValueParsed : dependentValue
const inputValueFormatted = independentField === INPUT ? independentValue : dependentValueFormatted
const outputValueParsed = independentField === OUTPUT ? independentValueParsed : dependentValue
const outputValueFormatted = independentField === OUTPUT ? independentValue : dependentValueFormatted
// validate + parse independent value
const [independentError, setIndependentError] = useState()
useEffect(() => {
if (independentValue && independentDecimals) {
try {
const parsedValue = ethers.utils.parseUnits(independentValue, independentDecimals)
this.setState(appendState)
if (parsedValue.lte(ethers.constants.Zero) || parsedValue.gte(ethers.constants.MaxUint256)) {
throw Error()
} else {
setIndependentValueParsed(parsedValue)
setIndependentError(null)
}
} catch {
setIndependentError(t('inputNotValid'))
}
recalcEthTokenForm = () => {
const {
exchangeAddresses: { fromToken },
selectors
} = this.props
const {
inputValue: oldInputValue,
outputValue: oldOutputValue,
inputCurrency,
outputCurrency,
lastEditedField,
exchangeRate: oldExchangeRate
} = this.state
const tokenAddress = [inputCurrency, outputCurrency].filter(currency => currency !== 'ETH')[0]
const exchangeAddress = fromToken[tokenAddress]
if (!exchangeAddress) {
return
return () => {
setIndependentValueParsed()
setIndependentError()
}
const { value: inputReserve, decimals: inputDecimals } = selectors().getBalance(exchangeAddress, inputCurrency)
const { value: outputReserve, decimals: outputDecimals } = selectors().getBalance(exchangeAddress, outputCurrency)
if (lastEditedField === INPUT) {
if (!oldInputValue) {
return this.setState({
outputValue: '',
exchangeRate: BN(0)
})
}
}, [independentValue, independentDecimals, t])
const inputAmount = BN(oldInputValue).multipliedBy(10 ** inputDecimals)
const outputAmount = calculateEtherTokenOutput({ inputAmount, inputReserve, outputReserve })
const outputValue = outputAmount.dividedBy(BN(10 ** outputDecimals)).toFixed(7)
const exchangeRate = BN(outputValue).dividedBy(BN(oldInputValue))
const appendState = {}
// calculate slippage from target rate
const { minimum: dependentValueMinumum, maximum: dependentValueMaximum } = calculateSlippageBounds(
dependentValue,
swapType === TOKEN_TO_TOKEN
)
if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) {
appendState.exchangeRate = exchangeRate
// validate input allowance + balance
const [inputError, setInputError] = useState()
const [showUnlock, setShowUnlock] = useState(false)
useEffect(() => {
const inputValueCalculation = independentField === INPUT ? independentValueParsed : dependentValueMaximum
if (inputBalance && (inputAllowance || inputCurrency === 'ETH') && inputValueCalculation) {
if (inputBalance.lt(inputValueCalculation)) {
setInputError(t('insufficientBalance'))
} else if (inputCurrency !== 'ETH' && inputAllowance.lt(inputValueCalculation)) {
setInputError(t('unlockTokenCont'))
setShowUnlock(true)
} else {
setInputError(null)
setShowUnlock(false)
}
if (outputValue !== oldOutputValue) {
appendState.outputValue = outputValue
return () => {
setInputError()
setShowUnlock(false)
}
this.setState(appendState)
} else if (lastEditedField === OUTPUT) {
if (!oldOutputValue) {
return this.setState({
inputValue: '',
exchangeRate: BN(0)
})
}
}, [independentField, independentValueParsed, dependentValueMaximum, inputBalance, inputCurrency, inputAllowance, t])
const outputAmount = BN(oldOutputValue).multipliedBy(10 ** outputDecimals)
const inputAmount = calculateEtherTokenInput({ outputAmount, inputReserve, outputReserve })
const inputValue = inputAmount.isNegative() ? 'N/A' : inputAmount.dividedBy(BN(10 ** inputDecimals)).toFixed(7)
const exchangeRate = BN(oldOutputValue).dividedBy(BN(inputValue))
// calculate dependent value
useEffect(() => {
const amount = independentValueParsed
const appendState = {}
if (swapType === ETH_TO_TOKEN) {
const reserveETH = outputReserveETH
const reserveToken = outputReserveToken
if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) {
appendState.exchangeRate = exchangeRate
}
if (amount && reserveETH && reserveToken) {
try {
const calculatedDependentValue =
independentField === INPUT
? calculateEtherTokenOutputFromInput(amount, reserveETH, reserveToken)
: calculateEtherTokenInputFromOutput(amount, reserveETH, reserveToken)
if (inputValue !== oldInputValue) {
appendState.inputValue = inputValue
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
throw Error()
}
this.setState(appendState)
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
} catch {
setIndependentError(t('insufficientLiquidity'))
}
return () => {
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' })
}
updateInput = amount => {
this.setState(
{
inputValue: amount,
lastEditedField: INPUT
},
this.recalcForm
)
}
updateOutput = amount => {
this.setState(
{
outputValue: amount,
lastEditedField: OUTPUT
},
this.recalcForm
)
}
} else if (swapType === TOKEN_TO_ETH) {
const reserveETH = inputReserveETH
const reserveToken = inputReserveToken
onSend = async () => {
const {
exchangeAddresses: { fromToken },
account,
web3,
selectors,
addPendingTx
} = this.props
const {
inputValue,
outputValue,
inputCurrency,
outputCurrency,
inputAmountB,
lastEditedField,
recipient
} = this.state
const ALLOWED_SLIPPAGE = 0.025
const TOKEN_ALLOWED_SLIPPAGE = 0.04
const type = getSendType(inputCurrency, outputCurrency)
const { decimals: inputDecimals } = selectors().getBalance(account, inputCurrency)
const { decimals: outputDecimals } = selectors().getBalance(account, outputCurrency)
let deadline
if (amount && reserveETH && reserveToken) {
try {
deadline = await retry(() => getBlockDeadline(web3, 600))
} catch (e) {
// TODO: Handle error.
return
}
const calculatedDependentValue =
independentField === INPUT
? calculateEtherTokenOutputFromInput(amount, reserveToken, reserveETH)
: calculateEtherTokenInputFromOutput(amount, reserveToken, reserveETH)
if (lastEditedField === INPUT) {
ReactGA.event({
category: type,
action: 'TransferInput'
})
// send input
switch (type) {
case 'ETH_TO_TOKEN':
new web3.eth.Contract(EXCHANGE_ABI, fromToken[outputCurrency]).methods
.ethToTokenTransferInput(
BN(outputValue)
.multipliedBy(10 ** outputDecimals)
.multipliedBy(1 - ALLOWED_SLIPPAGE)
.toFixed(0),
deadline,
recipient
)
.send(
{
from: account,
value: BN(inputValue)
.multipliedBy(10 ** 18)
.toFixed(0)
},
(err, data) => {
if (!err) {
addPendingTx(data)
this.reset()
}
}
)
break
case 'TOKEN_TO_ETH':
new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency]).methods
.tokenToEthTransferInput(
BN(inputValue)
.multipliedBy(10 ** inputDecimals)
.toFixed(0),
BN(outputValue)
.multipliedBy(10 ** outputDecimals)
.multipliedBy(1 - ALLOWED_SLIPPAGE)
.toFixed(0),
deadline,
recipient
)
.send({ from: account }, (err, data) => {
if (!err) {
addPendingTx(data)
this.reset()
}
})
break
case 'TOKEN_TO_TOKEN':
new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency]).methods
.tokenToTokenTransferInput(
BN(inputValue)
.multipliedBy(10 ** inputDecimals)
.toFixed(0),
BN(outputValue)
.multipliedBy(10 ** outputDecimals)
.multipliedBy(1 - TOKEN_ALLOWED_SLIPPAGE)
.toFixed(0),
'1',
deadline,
recipient,
outputCurrency
)
.send({ from: account }, (err, data) => {
if (!err) {
addPendingTx(data)
this.reset()
}
})
break
default:
break
}
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
throw Error()
}
if (lastEditedField === OUTPUT) {
// send output
ReactGA.event({
category: type,
action: 'TransferOutput'
})
switch (type) {
case 'ETH_TO_TOKEN':
new web3.eth.Contract(EXCHANGE_ABI, fromToken[outputCurrency]).methods
.ethToTokenTransferOutput(
BN(outputValue)
.multipliedBy(10 ** outputDecimals)
.toFixed(0),
deadline,
recipient
)
.send(
{
from: account,
value: BN(inputValue)
.multipliedBy(10 ** inputDecimals)
.multipliedBy(1 + ALLOWED_SLIPPAGE)
.toFixed(0)
},
(err, data) => {
if (!err) {
addPendingTx(data)
this.reset()
}
}
)
break
case 'TOKEN_TO_ETH':
new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency]).methods
.tokenToEthTransferOutput(
BN(outputValue)
.multipliedBy(10 ** outputDecimals)
.toFixed(0),
BN(inputValue)
.multipliedBy(10 ** inputDecimals)
.multipliedBy(1 + ALLOWED_SLIPPAGE)
.toFixed(0),
deadline,
recipient
)
.send({ from: account }, (err, data) => {
if (!err) {
addPendingTx(data)
this.reset()
}
})
break
case 'TOKEN_TO_TOKEN':
if (!inputAmountB) {
return
}
new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency]).methods
.tokenToTokenTransferOutput(
BN(outputValue)
.multipliedBy(10 ** outputDecimals)
.toFixed(0),
BN(inputValue)
.multipliedBy(10 ** inputDecimals)
.multipliedBy(1 + TOKEN_ALLOWED_SLIPPAGE)
.toFixed(0),
inputAmountB.multipliedBy(1.2).toFixed(0),
deadline,
recipient,
outputCurrency
)
.send({ from: account }, (err, data) => {
if (!err) {
addPendingTx(data)
this.reset()
}
})
break
default:
break
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
} catch {
setIndependentError(t('insufficientLiquidity'))
}
return () => {
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' })
}
}
} else if (swapType === TOKEN_TO_TOKEN) {
const reserveETHFirst = inputReserveETH
const reserveTokenFirst = inputReserveToken
renderSummary(inputError, outputError) {
const { inputValue, inputCurrency, outputValue, outputCurrency, recipient } = this.state
const { t, web3 } = this.props
const reserveETHSecond = outputReserveETH
const reserveTokenSecond = outputReserveToken
const { selectors, account } = this.props
const { label: inputLabel } = selectors().getBalance(account, inputCurrency)
const { label: outputLabel } = selectors().getBalance(account, outputCurrency)
const validRecipientAddress = web3 && web3.utils.isAddress(recipient)
const inputIsZero = BN(inputValue).isZero()
const outputIsZero = BN(outputValue).isZero()
if (amount && reserveETHFirst && reserveTokenFirst && reserveETHSecond && reserveTokenSecond) {
try {
const intermediateValue =
independentField === INPUT
? calculateEtherTokenOutputFromInput(amount, reserveTokenFirst, reserveETHFirst)
: calculateEtherTokenInputFromOutput(amount, reserveTokenFirst, reserveETHFirst)
if (intermediateValue.lte(ethers.constants.Zero)) {
throw Error()
}
const calculatedDependentValue =
independentField === INPUT
? calculateEtherTokenOutputFromInput(intermediateValue, reserveETHSecond, reserveTokenSecond)
: calculateEtherTokenInputFromOutput(intermediateValue, reserveETHSecond, reserveTokenSecond)
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
throw Error()
}
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
} catch {
setIndependentError(t('insufficientLiquidity'))
}
return () => {
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' })
}
}
}
}, [
independentValueParsed,
swapType,
outputReserveETH,
outputReserveToken,
inputReserveETH,
inputReserveToken,
independentField,
t
])
const [inverted, setInverted] = useState(false)
const exchangeRate = getExchangeRate(inputValueParsed, inputDecimals, outputValueParsed, outputDecimals)
const exchangeRateInverted = getExchangeRate(inputValueParsed, inputDecimals, outputValueParsed, outputDecimals, true)
const marketRate = getMarketRate(
swapType,
inputReserveETH,
inputReserveToken,
inputDecimals,
outputReserveETH,
outputReserveToken,
outputDecimals
)
let contextualInfo = ''
let isError = false
const percentSlippage =
exchangeRate &&
marketRate &&
amountFormatter(
exchangeRate
.sub(marketRate)
.abs()
.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)))
.div(marketRate)
.sub(ethers.utils.bigNumberify(3).mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(15)))),
16,
2
)
if (!account) {
contextualInfo = t('noWallet')
isError = true
} else if (inputError || outputError) {
contextualInfo = inputError || outputError
isError = true
} else if (!inputCurrency || !outputCurrency) {
contextualInfo = t('selectTokenCont')
} else if (inputCurrency === outputCurrency) {
contextualInfo = t('differentToken')
} else if (!inputValue || !outputValue) {
const missingCurrencyValue = !inputValue ? inputLabel : outputLabel
contextualInfo = t('enterValueCont', { missingCurrencyValue })
} else if (inputIsZero || outputIsZero) {
contextualInfo = t('noLiquidity')
} else if (this.isUnapproved()) {
contextualInfo = t('unlockTokenCont')
} else if (!recipient) {
contextualInfo = t('noRecipient')
} else if (!validRecipientAddress) {
contextualInfo = t('invalidRecipient')
}
const isValid = exchangeRate && inputError === null && independentError === null && recipientError === null
return (
<ContextualInfo
openDetailsText={t('transactionDetails')}
closeDetailsText={t('hideDetails')}
contextualInfo={contextualInfo}
isError={isError}
renderTransactionDetails={this.renderTransactionDetails}
/>
)
const estimatedText = `(${t('estimated')})`
function formatBalance(value) {
return `Balance: ${value}`
}
renderTransactionDetails = () => {
const { inputValue, inputCurrency, outputValue, outputCurrency, recipient, lastEditedField } = this.state
const { t, selectors, account } = this.props
function renderTransactionDetails() {
ReactGA.event({
category: 'TransactionDetail',
action: 'Open'
})
const ALLOWED_SLIPPAGE = 0.025
const TOKEN_ALLOWED_SLIPPAGE = 0.04
const type = getSendType(inputCurrency, outputCurrency)
const { label: inputLabel } = selectors().getBalance(account, inputCurrency)
const { label: outputLabel } = selectors().getBalance(account, outputCurrency)
// const label = lastEditedField === INPUT ? outputLabel : inputLabel;
let minOutput
let maxInput
if (lastEditedField === INPUT) {
switch (type) {
case 'ETH_TO_TOKEN':
minOutput = BN(outputValue)
.multipliedBy(1 - ALLOWED_SLIPPAGE)
.toFixed(7)
break
case 'TOKEN_TO_ETH':
minOutput = BN(outputValue)
.multipliedBy(1 - ALLOWED_SLIPPAGE)
.toFixed(7)
break
case 'TOKEN_TO_TOKEN':
minOutput = BN(outputValue)
.multipliedBy(1 - TOKEN_ALLOWED_SLIPPAGE)
.toFixed(7)
break
default:
break
}
}
if (lastEditedField === OUTPUT) {
switch (type) {
case 'ETH_TO_TOKEN':
maxInput = BN(inputValue)
.multipliedBy(1 + ALLOWED_SLIPPAGE)
.toFixed(7)
break
case 'TOKEN_TO_ETH':
maxInput = BN(inputValue)
.multipliedBy(1 + ALLOWED_SLIPPAGE)
.toFixed(7)
break
case 'TOKEN_TO_TOKEN':
maxInput = BN(inputValue)
.multipliedBy(1 + TOKEN_ALLOWED_SLIPPAGE)
.toFixed(7)
break
default:
break
}
}
const recipientText = b(`${recipient.slice(0, 6)}...${recipient.slice(-4)}`)
if (lastEditedField === INPUT) {
const b = text => <span className="swap__highlight-text">{text}</span>
if (independentField === INPUT) {
return (
<div>
<div>
{t('youAreSending')} {b(`${+inputValue} ${inputLabel}`)}.
{t('youAreSelling')}{' '}
{b(
`${amountFormatter(
independentValueParsed,
independentDecimals,
Math.min(4, independentDecimals)
)} ${inputSymbol}`
)}
.
</div>
<div className="send__last-summary-text">
{recipientText} {t('willReceive')} {b(`${+minOutput} ${outputLabel}`)} {t('orTransFail')}
{b(recipient.address)} {t('willReceive')}{' '}
{b(
`${amountFormatter(
dependentValueMinumum,
dependentDecimals,
Math.min(4, dependentDecimals)
)} ${outputSymbol}`
)}{' '}
{t('orTransFail')}
</div>
<div className="send__last-summary-text">
{t('priceChange')} {b(`${percentSlippage}%`)}.
</div>
</div>
)
......@@ -695,95 +476,191 @@ class Send extends Component {
return (
<div>
<div>
{t('youAreSending')} {b(`${+outputValue} ${outputLabel}`)} {t('to')} {recipientText}.
{/*You are selling between {b(`${+inputValue} ${inputLabel}`)} to {b(`${+maxInput} ${inputLabel}`)}.*/}
{t('youAreSending')}{' '}
{b(
`${amountFormatter(
independentValueParsed,
independentDecimals,
Math.min(4, independentDecimals)
)} ${outputSymbol}`
)}{' '}
{t('to')} {b(recipient.name || recipient.address)}.
</div>
<div className="send__last-summary-text">
{t('itWillCost')}{' '}
{b(
`${amountFormatter(
dependentValueMaximum,
dependentDecimals,
Math.min(4, dependentDecimals)
)} ${inputSymbol}`
)}{' '}
{t('orTransFail')}
</div>
<div className="send__last-summary-text">
{/*{b(`${recipient.slice(0, 6)}...${recipient.slice(-4)}`)} will receive {b(`${+outputValue} ${outputLabel}`)}.*/}
{t('itWillCost')} {b(`${+maxInput} ${inputLabel}`)} {t('orTransFail')}
{t('priceChange')} {b(`${percentSlippage}%`)}.
</div>
</div>
)
}
}
renderExchangeRate() {
const { t, account, selectors } = this.props
const { exchangeRate, inputCurrency, outputCurrency } = this.state
const { label: inputLabel } = selectors().getBalance(account, inputCurrency)
const { label: outputLabel } = selectors().getBalance(account, outputCurrency)
function renderSummary() {
let contextualInfo = ''
let isError = false
if (!exchangeRate || exchangeRate.isNaN() || !inputCurrency || !outputCurrency) {
return (
<OversizedPanel hideBottom>
<div className="swap__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t('exchangeRate')}</span>
<span> - </span>
</div>
</OversizedPanel>
)
if (inputError || independentError) {
contextualInfo = inputError || independentError
isError = true
} else if (!inputCurrency || !outputCurrency) {
contextualInfo = t('selectTokenCont')
} else if (!independentValue) {
contextualInfo = t('enterValueCont')
} else if (!recipient.address) {
contextualInfo = t('noRecipient')
} else if (!isAddress(recipient.address)) {
contextualInfo = t('invalidRecipient')
} else if (!account) {
contextualInfo = t('noWallet')
isError = true
}
return (
<OversizedPanel hideBottom>
<div className="swap__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t('exchangeRate')}</span>
<span>{`1 ${inputLabel} = ${exchangeRate.toFixed(7)} ${outputLabel}`}</span>
</div>
</OversizedPanel>
<ContextualInfo
openDetailsText={t('transactionDetails')}
closeDetailsText={t('hideDetails')}
contextualInfo={contextualInfo}
isError={isError}
renderTransactionDetails={renderTransactionDetails}
/>
)
}
renderBalance(currency, balance, decimals) {
if (!currency || decimals === 0) {
return ''
}
const balanceInput = balance.dividedBy(BN(10 ** decimals)).toFixed(4)
return this.props.t('balance', { balanceInput })
async function onSwap() {
const deadline = Math.ceil(Date.now() / 1000) + DEADLINE_FROM_NOW
let estimate, method, args, value
if (independentField === INPUT) {
ReactGA.event({
category: `${swapType}`,
action: 'TransferInput'
})
if (swapType === ETH_TO_TOKEN) {
estimate = contract.estimate.ethToTokenTransferInput
method = contract.ethToTokenTransferInput
args = [dependentValueMinumum, deadline, recipient.address]
value = independentValueParsed
} else if (swapType === TOKEN_TO_ETH) {
estimate = contract.estimate.tokenToEthTransferInput
method = contract.tokenToEthTransferInput
args = [independentValueParsed, dependentValueMinumum, deadline, recipient.address]
value = ethers.constants.Zero
} else if (swapType === TOKEN_TO_TOKEN) {
estimate = contract.estimate.tokenToTokenTransferInput
method = contract.tokenToTokenTransferInput
args = [
independentValueParsed,
dependentValueMinumum,
ethers.constants.One,
deadline,
recipient.address,
outputCurrency
]
value = ethers.constants.Zero
}
} else if (independentField === OUTPUT) {
ReactGA.event({
category: `${swapType}`,
action: 'TransferOutput'
})
render() {
const { t, selectors, account } = this.props
const { lastEditedField, inputCurrency, outputCurrency, inputValue, outputValue, recipient } = this.state
const estimatedText = `(${t('estimated')})`
if (swapType === ETH_TO_TOKEN) {
estimate = contract.estimate.ethToTokenTransferOutput
method = contract.ethToTokenTransferOutput
args = [independentValueParsed, deadline, recipient.address]
value = dependentValueMaximum
} else if (swapType === TOKEN_TO_ETH) {
estimate = contract.estimate.tokenToEthTransferOutput
method = contract.tokenToEthTransferOutput
args = [independentValueParsed, dependentValueMaximum, deadline, recipient.address]
value = ethers.constants.Zero
} else if (swapType === TOKEN_TO_TOKEN) {
estimate = contract.estimate.tokenToTokenTransferOutput
method = contract.tokenToTokenTransferOutput
args = [
independentValueParsed,
dependentValueMaximum,
ethers.constants.MaxUint256,
deadline,
recipient.address,
outputCurrency
]
value = ethers.constants.Zero
}
}
const { value: inputBalance, decimals: inputDecimals } = selectors().getBalance(account, inputCurrency)
const { value: outputBalance, decimals: outputDecimals } = selectors().getBalance(account, outputCurrency)
const { inputError, outputError, isValid } = this.validate()
const estimatedGasLimit = await estimate(...args, { value })
method(...args, { value, gasLimit: calculateGasMargin(estimatedGasLimit, GAS_MARGIN) }).then(response => {
addTransaction(response.hash, response)
})
}
return (
<>
<CurrencyInputPanel
title={t('input')}
description={lastEditedField === OUTPUT ? estimatedText : ''}
extraText={this.renderBalance(inputCurrency, inputBalance, inputDecimals)}
onCurrencySelected={inputCurrency => this.setState({ inputCurrency }, this.recalcForm)}
onValueChange={this.updateInput}
description={inputValueFormatted && independentField === OUTPUT ? estimatedText : ''}
extraText={inputBalanceFormatted && formatBalance(inputBalanceFormatted)}
extraTextClickHander={() => {
if (inputBalance && inputDecimals) {
const valueToSet = inputCurrency === 'ETH' ? inputBalance.sub(ethers.utils.parseEther('.1')) : inputBalance
if (valueToSet.gt(ethers.constants.Zero)) {
dispatchSwapState({
type: 'UPDATE_INDEPENDENT',
payload: { value: amountFormatter(valueToSet, inputDecimals, inputDecimals, false), field: INPUT }
})
}
}
}}
onCurrencySelected={inputCurrency => {
dispatchSwapState({ type: 'SELECT_CURRENCY', payload: { currency: inputCurrency, field: INPUT } })
}}
onValueChange={inputValue => {
dispatchSwapState({ type: 'UPDATE_INDEPENDENT', payload: { value: inputValue, field: INPUT } })
}}
showUnlock={showUnlock}
selectedTokens={[inputCurrency, outputCurrency]}
selectedTokenAddress={inputCurrency}
value={inputValue}
errorMessage={inputError}
value={inputValueFormatted}
errorMessage={inputError ? inputError : independentField === INPUT ? independentError : ''}
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
<img
onClick={this.flipInputOutput}
onClick={() => {
dispatchSwapState({ type: 'FLIP_INDEPENDENT' })
}}
className="swap__down-arrow swap__down-arrow--clickable"
alt="arrow"
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}
description={outputValueFormatted && independentField === INPUT ? estimatedText : ''}
extraText={outputBalanceFormatted && formatBalance(outputBalanceFormatted)}
onCurrencySelected={outputCurrency => {
dispatchSwapState({ type: 'SELECT_CURRENCY', payload: { currency: outputCurrency, field: OUTPUT } })
}}
onValueChange={outputValue => {
dispatchSwapState({ type: 'UPDATE_INDEPENDENT', payload: { value: outputValue, field: OUTPUT } })
}}
selectedTokens={[inputCurrency, outputCurrency]}
value={outputValue}
selectedTokenAddress={outputCurrency}
errorMessage={outputError}
value={outputValueFormatted}
errorMessage={independentField === OUTPUT ? independentError : ''}
disableUnlock
/>
<OversizedPanel>
......@@ -791,111 +668,36 @@ class Send extends Component {
<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)}
<AddressInputPanel onChange={setRecipient} onError={setRecipientError} />
<OversizedPanel hideBottom>
<div
className="swap__exchange-rate-wrapper"
onClick={() => {
setInverted(inverted => !inverted)
}}
>
<span className="swap__exchange-rate">{t('exchangeRate')}</span>
{inverted ? (
<span>
{exchangeRate
? `1 ${outputSymbol} = ${amountFormatter(exchangeRateInverted, 18, 4, false)} ${inputSymbol}`
: ' - '}
</span>
) : (
<span>
{exchangeRate
? `1 ${inputSymbol} = ${amountFormatter(exchangeRate, 18, 4, false)} ${outputSymbol}`
: ' - '}
</span>
)}
</div>
</OversizedPanel>
{renderSummary()}
<div className="swap__cta-container">
<SendButton callOnClick={this.onSend} isValid={isValid} />
<button className="swap__cta-btn" disabled={!isValid} onClick={onSwap}>
{t('swap')}
</button>
</div>
</>
)
}
}
function SendButton({ callOnClick, isValid }) {
const { t } = useTranslation()
const context = useWeb3Context()
const isActive = context.active && context.account
return (
<button
className={classnames('swap__cta-btn', {
'swap--inactive': !isActive
})}
disabled={!isValid}
onClick={callOnClick}
>
{t('send')}
</button>
)
}
export default connect(
state => ({
balances: state.web3connect.balances,
account: state.web3connect.account,
web3: state.web3connect.web3,
exchangeAddresses: state.addresses.exchangeAddresses
}),
dispatch => ({
selectors: () => dispatch(selectors()),
addPendingTx: id => dispatch(addPendingTx(id))
})
)(withTranslation()(Send))
const b = text => <span className="swap__highlight-text">{text}</span>
function calculateEtherTokenOutput({
inputAmount: rawInput,
inputReserve: rawReserveIn,
outputReserve: rawReserveOut
}) {
const inputAmount = BN(rawInput)
const inputReserve = BN(rawReserveIn)
const outputReserve = BN(rawReserveOut)
if (inputAmount.isLessThan(BN(10 ** 9))) {
console.warn(`inputAmount is only ${inputAmount.toFixed(0)}. Did you forget to multiply by 10 ** decimals?`)
}
const numerator = inputAmount.multipliedBy(outputReserve).multipliedBy(997)
const denominator = inputReserve.multipliedBy(1000).plus(inputAmount.multipliedBy(997))
return numerator.dividedBy(denominator)
}
function calculateEtherTokenInput({
outputAmount: rawOutput,
inputReserve: rawReserveIn,
outputReserve: rawReserveOut
}) {
const outputAmount = BN(rawOutput)
const inputReserve = BN(rawReserveIn)
const outputReserve = BN(rawReserveOut)
if (outputAmount.isLessThan(BN(10 ** 9))) {
console.warn(`inputAmount is only ${outputAmount.toFixed(0)}. Did you forget to multiply by 10 ** decimals?`)
}
const numerator = outputAmount.multipliedBy(inputReserve).multipliedBy(1000)
const denominator = outputReserve.minus(outputAmount).multipliedBy(997)
return numerator.dividedBy(denominator).plus(1)
}
function getSendType(inputCurrency, outputCurrency) {
if (!inputCurrency || !outputCurrency) {
return
}
if (inputCurrency === outputCurrency) {
return
}
if (inputCurrency !== 'ETH' && outputCurrency !== 'ETH') {
return 'TOKEN_TO_TOKEN'
}
if (inputCurrency === 'ETH') {
return 'ETH_TO_TOKEN'
}
if (outputCurrency === 'ETH') {
return 'TOKEN_TO_ETH'
}
return
}
import React, { Component } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import { BigNumber as BN } from 'bignumber.js'
import React, { useState, useReducer, useEffect } from 'react'
import ReactGA from 'react-ga'
import { withTranslation, useTranslation } from 'react-i18next'
import { useTranslation } from 'react-i18next'
import { useWeb3Context } from 'web3-react'
import { ethers } from 'ethers'
import { selectors, addPendingTx } from '../../ducks/web3connect'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import ContextualInfo from '../../components/ContextualInfo'
import OversizedPanel from '../../components/OversizedPanel'
import ArrowDownBlue from '../../assets/images/arrow-down-blue.svg'
import ArrowDownGrey from '../../assets/images/arrow-down-grey.svg'
import { getBlockDeadline } from '../../helpers/web3-utils'
import { retry } from '../../helpers/promise-utils'
import EXCHANGE_ABI from '../../abi/exchange'
import { useAddressBalance, useAddressAllowance, useExchangeReserves } from '../../contexts/Block'
import { useTokenDetails } from '../../contexts/Static'
import { useTransactionContext } from '../../contexts/Transaction'
import { amountFormatter, calculateGasMargin } from '../../utils'
import { useExchangeContract } from '../../hooks'
import './swap.scss'
const INPUT = 0
const OUTPUT = 1
class Swap extends Component {
static propTypes = {
account: PropTypes.string,
selectors: PropTypes.func.isRequired,
addPendingTx: PropTypes.func.isRequired,
web3: PropTypes.object.isRequired
}
const ETH_TO_TOKEN = 0
const TOKEN_TO_ETH = 1
const TOKEN_TO_TOKEN = 2
state = {
inputValue: '',
outputValue: '',
inputCurrency: 'ETH',
outputCurrency: '',
inputAmountB: '',
lastEditedField: ''
}
// denominated in bips
const ALLOWED_SLIPPAGE = ethers.utils.bigNumberify(200)
const TOKEN_ALLOWED_SLIPPAGE = ethers.utils.bigNumberify(400)
componentWillMount() {
ReactGA.pageview(window.location.pathname + window.location.search)
}
// denominated in seconds
const DEADLINE_FROM_NOW = 60 * 15
shouldComponentUpdate(nextProps, nextState) {
return true
}
// denominated in bips
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
reset() {
this.setState({
inputValue: '',
outputValue: '',
inputAmountB: '',
lastEditedField: ''
})
function calculateSlippageBounds(value, token = false) {
if (value) {
const offset = value.mul(token ? TOKEN_ALLOWED_SLIPPAGE : ALLOWED_SLIPPAGE).div(ethers.utils.bigNumberify(10000))
const minimum = value.sub(offset)
const maximum = value.add(offset)
return {
minimum: minimum.lt(ethers.constants.Zero) ? ethers.constants.Zero : minimum,
maximum: maximum.gt(ethers.constants.MaxUint256) ? ethers.constants.MaxUint256 : maximum
}
componentWillReceiveProps() {
this.recalcForm()
} else {
return {}
}
}
validate() {
const { selectors, account } = this.props
const { inputValue, outputValue, inputCurrency, outputCurrency } = this.state
let inputError = ''
let outputError = ''
let isValid = true
let isUnapproved = this.isUnapproved()
const inputIsZero = BN(inputValue).isZero()
const outputIsZero = BN(outputValue).isZero()
if (
!inputValue ||
inputIsZero ||
!outputValue ||
outputIsZero ||
!inputCurrency ||
!outputCurrency ||
isUnapproved
) {
isValid = false
function getSwapType(inputCurrency, outputCurrency) {
if (!inputCurrency || !outputCurrency) {
return null
} else if (inputCurrency === 'ETH') {
return ETH_TO_TOKEN
} else if (outputCurrency === 'ETH') {
return TOKEN_TO_ETH
} else {
return TOKEN_TO_TOKEN
}
}
const { value: inputBalance, decimals: inputDecimals } = selectors().getBalance(account, inputCurrency)
// this mocks the getInputPrice function, and calculates the required output
function calculateEtherTokenOutputFromInput(inputAmount, inputReserve, outputReserve) {
const inputAmountWithFee = inputAmount.mul(ethers.utils.bigNumberify(997))
const numerator = inputAmountWithFee.mul(outputReserve)
const denominator = inputReserve.mul(ethers.utils.bigNumberify(1000)).add(inputAmountWithFee)
return numerator.div(denominator)
}
if (inputBalance.isLessThan(BN(inputValue * 10 ** inputDecimals))) {
inputError = this.props.t('insufficientBalance')
}
// this mocks the getOutputPrice function, and calculates the required input
function calculateEtherTokenInputFromOutput(outputAmount, inputReserve, outputReserve) {
const numerator = inputReserve.mul(outputAmount).mul(ethers.utils.bigNumberify(1000))
const denominator = outputReserve.sub(outputAmount).mul(ethers.utils.bigNumberify(997))
return numerator.div(denominator).add(ethers.constants.One)
}
if (inputValue === 'N/A') {
inputError = this.props.t('inputNotValid')
}
const initialSwapState = {
independentValue: '', // this is a user input
dependentValue: '', // this is a calculated number
independentField: INPUT,
inputCurrency: 'ETH',
outputCurrency: ''
}
function swapStateReducer(state, action) {
switch (action.type) {
case 'FLIP_INDEPENDENT': {
const { independentField, inputCurrency, outputCurrency } = state
return {
inputError,
outputError,
isValid: isValid && !inputError && !outputError
}
}
flipInputOutput = () => {
const { state } = this
this.setState(
{
inputValue: state.outputValue,
outputValue: state.inputValue,
inputCurrency: state.outputCurrency,
outputCurrency: state.inputCurrency,
lastEditedField: state.lastEditedField === INPUT ? OUTPUT : INPUT
},
() => this.recalcForm()
)
...state,
dependentValue: '',
independentField: independentField === INPUT ? OUTPUT : INPUT,
inputCurrency: outputCurrency,
outputCurrency: inputCurrency
}
isUnapproved() {
const { account, exchangeAddresses, selectors } = this.props
const { inputCurrency, inputValue } = this.state
if (!inputCurrency || inputCurrency === 'ETH') {
return false
}
case 'SELECT_CURRENCY': {
const { inputCurrency, outputCurrency } = state
const { field, currency } = action.payload
const { value: allowance, label, decimals } = selectors().getApprovals(
inputCurrency,
account,
exchangeAddresses.fromToken[inputCurrency]
)
const newInputCurrency = field === INPUT ? currency : inputCurrency
const newOutputCurrency = field === OUTPUT ? currency : outputCurrency
if (label && allowance.isLessThan(BN(inputValue * 10 ** decimals || 0))) {
return true
if (newInputCurrency === newOutputCurrency) {
return {
...state,
inputCurrency: field === INPUT ? currency : '',
outputCurrency: field === OUTPUT ? currency : ''
}
return false
} else {
return {
...state,
inputCurrency: newInputCurrency,
outputCurrency: newOutputCurrency
}
recalcForm() {
const { inputCurrency, outputCurrency, lastEditedField } = this.state
if (!inputCurrency || !outputCurrency) {
return
}
const editedValue = lastEditedField === INPUT ? this.state.inputValue : this.state.outputValue
if (BN(editedValue).isZero()) {
return
}
if (inputCurrency === outputCurrency) {
this.setState({
inputValue: '',
outputValue: ''
})
return
case 'UPDATE_INDEPENDENT': {
const { field, value } = action.payload
return {
...state,
independentValue: value,
dependentValue: '',
independentField: field
}
if (inputCurrency !== 'ETH' && outputCurrency !== 'ETH') {
this.recalcTokenTokenForm()
return
}
this.recalcEthTokenForm()
case 'UPDATE_DEPENDENT': {
return {
...state,
dependentValue: action.payload
}
recalcTokenTokenForm = () => {
const {
exchangeAddresses: { fromToken },
selectors
} = this.props
const {
inputValue: oldInputValue,
outputValue: oldOutputValue,
inputCurrency,
outputCurrency,
lastEditedField,
exchangeRate: oldExchangeRate,
inputAmountB: oldInputAmountB
} = this.state
const exchangeAddressA = fromToken[inputCurrency]
const exchangeAddressB = fromToken[outputCurrency]
const { value: inputReserveA, decimals: inputDecimalsA } = selectors().getBalance(exchangeAddressA, inputCurrency)
const { value: outputReserveA } = selectors().getBalance(exchangeAddressA, 'ETH')
const { value: inputReserveB } = selectors().getBalance(exchangeAddressB, 'ETH')
const { value: outputReserveB, decimals: outputDecimalsB } = selectors().getBalance(
exchangeAddressB,
outputCurrency
)
if (lastEditedField === INPUT) {
if (!oldInputValue) {
return this.setState({
outputValue: '',
exchangeRate: BN(0)
})
}
const inputAmountA = BN(oldInputValue).multipliedBy(10 ** inputDecimalsA)
const outputAmountA = calculateEtherTokenOutput({
inputAmount: inputAmountA,
inputReserve: inputReserveA,
outputReserve: outputReserveA
})
// Redundant Variable for readability of the formala
// OutputAmount from the first swap becomes InputAmount of the second swap
const inputAmountB = outputAmountA
const outputAmountB = calculateEtherTokenOutput({
inputAmount: inputAmountB,
inputReserve: inputReserveB,
outputReserve: outputReserveB
})
const outputValue = outputAmountB.dividedBy(BN(10 ** outputDecimalsB)).toFixed(7)
const exchangeRate = BN(outputValue).dividedBy(BN(oldInputValue))
const appendState = {}
if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) {
appendState.exchangeRate = exchangeRate
default: {
return initialSwapState
}
if (outputValue !== oldOutputValue) {
appendState.outputValue = outputValue
}
}
this.setState(appendState)
function getExchangeRate(inputValue, inputDecimals, outputValue, outputDecimals, invert = false) {
try {
if (inputValue && inputDecimals && outputValue && outputDecimals) {
const factor = ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18))
if (invert) {
return inputValue
.mul(factor)
.div(outputValue)
.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(outputDecimals)))
.div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(inputDecimals)))
} else {
return outputValue
.mul(factor)
.div(inputValue)
.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(inputDecimals)))
.div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(outputDecimals)))
}
}
} catch {}
}
if (lastEditedField === OUTPUT) {
if (!oldOutputValue) {
return this.setState({
inputValue: '',
exchangeRate: BN(0)
})
function getMarketRate(
swapType,
inputReserveETH,
inputReserveToken,
inputDecimals,
outputReserveETH,
outputReserveToken,
outputDecimals,
invert = false
) {
if (swapType === ETH_TO_TOKEN) {
return getExchangeRate(outputReserveETH, inputDecimals, outputReserveToken, outputDecimals, invert)
} else if (swapType === TOKEN_TO_ETH) {
return getExchangeRate(inputReserveToken, inputDecimals, inputReserveETH, outputDecimals, invert)
} else if (swapType === TOKEN_TO_TOKEN) {
const factor = ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18))
const firstRate = getExchangeRate(inputReserveToken, inputDecimals, inputReserveETH, outputDecimals)
const secondRate = getExchangeRate(outputReserveETH, inputDecimals, outputReserveToken, outputDecimals)
try {
return !!(firstRate && secondRate) ? firstRate.mul(secondRate).div(factor) : undefined
} catch {}
}
}
const outputAmountB = BN(oldOutputValue).multipliedBy(10 ** outputDecimalsB)
const inputAmountB = calculateEtherTokenInput({
outputAmount: outputAmountB,
inputReserve: inputReserveB,
outputReserve: outputReserveB
})
export default function Swap() {
const { t } = useTranslation()
const { account } = useWeb3Context()
// Redundant Variable for readability of the formala
// InputAmount from the first swap becomes OutputAmount of the second swap
const outputAmountA = inputAmountB
const inputAmountA = calculateEtherTokenInput({
outputAmount: outputAmountA,
inputReserve: inputReserveA,
outputReserve: outputReserveA
})
const { addTransaction } = useTransactionContext()
const inputValue = inputAmountA.isNegative() ? 'N/A' : inputAmountA.dividedBy(BN(10 ** inputDecimalsA)).toFixed(7)
const exchangeRate = BN(oldOutputValue).dividedBy(BN(inputValue))
// analytics
useEffect(() => {
ReactGA.pageview(window.location.pathname + window.location.search)
}, [])
const appendState = {}
// core swap state
const [swapState, dispatchSwapState] = useReducer(swapStateReducer, initialSwapState)
const { independentValue, dependentValue, independentField, inputCurrency, outputCurrency } = swapState
if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) {
appendState.exchangeRate = exchangeRate
}
// get swap type from the currency types
const swapType = getSwapType(inputCurrency, outputCurrency)
if (inputValue !== oldInputValue) {
appendState.inputValue = inputValue
}
// get decimals and exchange addressfor each of the currency types
const { symbol: inputSymbol, decimals: inputDecimals, exchangeAddress: inputExchangeAddress } = useTokenDetails(
inputCurrency
)
const { symbol: outputSymbol, decimals: outputDecimals, exchangeAddress: outputExchangeAddress } = useTokenDetails(
outputCurrency
)
if (!inputAmountB.isEqualTo(BN(oldInputAmountB))) {
appendState.inputAmountB = inputAmountB
}
const inputExchangeContract = useExchangeContract(inputExchangeAddress)
const outputExchangeContract = useExchangeContract(outputExchangeAddress)
const contract = swapType === ETH_TO_TOKEN ? outputExchangeContract : inputExchangeContract
// get input allowance
const inputAllowance = useAddressAllowance(account, inputCurrency, inputExchangeAddress)
// fetch reserves for each of the currency types
const { reserveETH: inputReserveETH, reserveToken: inputReserveToken } = useExchangeReserves(inputCurrency)
const { reserveETH: outputReserveETH, reserveToken: outputReserveToken } = useExchangeReserves(outputCurrency)
// get balances for each of the currency types
const inputBalance = useAddressBalance(account, inputCurrency)
const outputBalance = useAddressBalance(account, outputCurrency)
const inputBalanceFormatted = !!(inputBalance && Number.isInteger(inputDecimals))
? amountFormatter(inputBalance, inputDecimals, Math.min(4, inputDecimals))
: ''
const outputBalanceFormatted = !!(outputBalance && Number.isInteger(outputDecimals))
? amountFormatter(outputBalance, outputDecimals, Math.min(4, outputDecimals))
: ''
// compute useful transforms of the data above
const independentDecimals = independentField === INPUT ? inputDecimals : outputDecimals
const dependentDecimals = independentField === OUTPUT ? inputDecimals : outputDecimals
// declare/get parsed and formatted versions of input/output values
const [independentValueParsed, setIndependentValueParsed] = useState()
const dependentValueFormatted = !!(dependentValue && dependentDecimals)
? amountFormatter(dependentValue, dependentDecimals, Math.min(4, dependentDecimals), false)
: ''
const inputValueParsed = independentField === INPUT ? independentValueParsed : dependentValue
const inputValueFormatted = independentField === INPUT ? independentValue : dependentValueFormatted
const outputValueParsed = independentField === OUTPUT ? independentValueParsed : dependentValue
const outputValueFormatted = independentField === OUTPUT ? independentValue : dependentValueFormatted
// validate + parse independent value
const [independentError, setIndependentError] = useState()
useEffect(() => {
if (independentValue && independentDecimals) {
try {
const parsedValue = ethers.utils.parseUnits(independentValue, independentDecimals)
this.setState(appendState)
if (parsedValue.lte(ethers.constants.Zero) || parsedValue.gte(ethers.constants.MaxUint256)) {
throw Error()
} else {
setIndependentValueParsed(parsedValue)
setIndependentError(null)
}
} catch {
setIndependentError(t('inputNotValid'))
}
recalcEthTokenForm = () => {
const {
exchangeAddresses: { fromToken },
selectors
} = this.props
const {
inputValue: oldInputValue,
outputValue: oldOutputValue,
inputCurrency,
outputCurrency,
lastEditedField,
exchangeRate: oldExchangeRate
} = this.state
const tokenAddress = [inputCurrency, outputCurrency].filter(currency => currency !== 'ETH')[0]
const exchangeAddress = fromToken[tokenAddress]
if (!exchangeAddress) {
return
return () => {
setIndependentValueParsed()
setIndependentError()
}
const { value: inputReserve, decimals: inputDecimals } = selectors().getBalance(exchangeAddress, inputCurrency)
const { value: outputReserve, decimals: outputDecimals } = selectors().getBalance(exchangeAddress, outputCurrency)
if (lastEditedField === INPUT) {
if (!oldInputValue) {
return this.setState({
outputValue: '',
exchangeRate: BN(0)
})
}
}, [independentValue, independentDecimals, t])
const inputAmount = BN(oldInputValue).multipliedBy(10 ** inputDecimals)
const outputAmount = calculateEtherTokenOutput({ inputAmount, inputReserve, outputReserve })
const outputValue = outputAmount.dividedBy(BN(10 ** outputDecimals)).toFixed(7)
const exchangeRate = BN(outputValue).dividedBy(BN(oldInputValue))
const appendState = {}
// calculate slippage from target rate
const { minimum: dependentValueMinumum, maximum: dependentValueMaximum } = calculateSlippageBounds(
dependentValue,
swapType === TOKEN_TO_TOKEN
)
if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) {
appendState.exchangeRate = exchangeRate
// validate input allowance + balance
const [inputError, setInputError] = useState()
const [showUnlock, setShowUnlock] = useState(false)
useEffect(() => {
const inputValueCalculation = independentField === INPUT ? independentValueParsed : dependentValueMaximum
if (inputBalance && (inputAllowance || inputCurrency === 'ETH') && inputValueCalculation) {
if (inputBalance.lt(inputValueCalculation)) {
setInputError(t('insufficientBalance'))
} else if (inputCurrency !== 'ETH' && inputAllowance.lt(inputValueCalculation)) {
setInputError(t('unlockTokenCont'))
setShowUnlock(true)
} else {
setInputError(null)
setShowUnlock(false)
}
if (outputValue !== oldOutputValue) {
appendState.outputValue = outputValue
return () => {
setInputError()
setShowUnlock(false)
}
this.setState(appendState)
} else if (lastEditedField === OUTPUT) {
if (!oldOutputValue) {
return this.setState({
inputValue: '',
exchangeRate: BN(0)
})
}
}, [independentField, independentValueParsed, dependentValueMaximum, inputBalance, inputCurrency, inputAllowance, t])
const outputAmount = BN(oldOutputValue).multipliedBy(10 ** outputDecimals)
const inputAmount = calculateEtherTokenInput({ outputAmount, inputReserve, outputReserve })
const inputValue = inputAmount.isNegative() ? 'N/A' : inputAmount.dividedBy(BN(10 ** inputDecimals)).toFixed(7)
const exchangeRate = BN(oldOutputValue).dividedBy(BN(inputValue))
// calculate dependent value
useEffect(() => {
const amount = independentValueParsed
const appendState = {}
if (swapType === ETH_TO_TOKEN) {
const reserveETH = outputReserveETH
const reserveToken = outputReserveToken
if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) {
appendState.exchangeRate = exchangeRate
}
if (amount && reserveETH && reserveToken) {
try {
const calculatedDependentValue =
independentField === INPUT
? calculateEtherTokenOutputFromInput(amount, reserveETH, reserveToken)
: calculateEtherTokenInputFromOutput(amount, reserveETH, reserveToken)
if (inputValue !== oldInputValue) {
appendState.inputValue = inputValue
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
throw Error()
}
this.setState(appendState)
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
} catch {
setIndependentError(t('insufficientLiquidity'))
}
return () => {
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' })
}
updateInput = amount => {
this.setState(
{
inputValue: amount,
lastEditedField: INPUT
},
this.recalcForm
)
}
} else if (swapType === TOKEN_TO_ETH) {
const reserveETH = inputReserveETH
const reserveToken = inputReserveToken
updateOutput = amount => {
this.setState(
{
outputValue: amount,
lastEditedField: OUTPUT
},
this.recalcForm
)
}
onSwap = async () => {
const {
exchangeAddresses: { fromToken },
account,
web3,
selectors,
addPendingTx
} = this.props
const { inputValue, outputValue, inputCurrency, outputCurrency, inputAmountB, lastEditedField } = this.state
const ALLOWED_SLIPPAGE = 0.025
const TOKEN_ALLOWED_SLIPPAGE = 0.04
const type = getSwapType(inputCurrency, outputCurrency)
const { decimals: inputDecimals } = selectors().getBalance(account, inputCurrency)
const { decimals: outputDecimals } = selectors().getBalance(account, outputCurrency)
let deadline
if (amount && reserveETH && reserveToken) {
try {
deadline = await retry(() => getBlockDeadline(web3, 600))
} catch (e) {
// TODO: Handle error.
return
}
const calculatedDependentValue =
independentField === INPUT
? calculateEtherTokenOutputFromInput(amount, reserveToken, reserveETH)
: calculateEtherTokenInputFromOutput(amount, reserveToken, reserveETH)
if (lastEditedField === INPUT) {
// swap input
ReactGA.event({
category: type,
action: 'SwapInput'
})
switch (type) {
case 'ETH_TO_TOKEN':
// let exchange = new web3.eth.Contract(EXCHANGE_ABI, fromToken[outputCurrency]);
new web3.eth.Contract(EXCHANGE_ABI, fromToken[outputCurrency]).methods
.ethToTokenSwapInput(
BN(outputValue)
.multipliedBy(10 ** outputDecimals)
.multipliedBy(1 - ALLOWED_SLIPPAGE)
.toFixed(0),
deadline
)
.send(
{
from: account,
value: BN(inputValue)
.multipliedBy(10 ** 18)
.toFixed(0)
},
(err, data) => {
if (!err) {
addPendingTx(data)
this.reset()
}
}
)
break
case 'TOKEN_TO_ETH':
new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency]).methods
.tokenToEthSwapInput(
BN(inputValue)
.multipliedBy(10 ** inputDecimals)
.toFixed(0),
BN(outputValue)
.multipliedBy(10 ** outputDecimals)
.multipliedBy(1 - ALLOWED_SLIPPAGE)
.toFixed(0),
deadline
)
.send({ from: account }, (err, data) => {
if (!err) {
addPendingTx(data)
this.reset()
}
})
break
case 'TOKEN_TO_TOKEN':
new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency]).methods
.tokenToTokenSwapInput(
BN(inputValue)
.multipliedBy(10 ** inputDecimals)
.toFixed(0),
BN(outputValue)
.multipliedBy(10 ** outputDecimals)
.multipliedBy(1 - TOKEN_ALLOWED_SLIPPAGE)
.toFixed(0),
'1',
deadline,
outputCurrency
)
.send({ from: account }, (err, data) => {
if (!err) {
addPendingTx(data)
this.reset()
}
})
break
default:
break
}
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
throw Error()
}
if (lastEditedField === OUTPUT) {
// swap output
ReactGA.event({
category: type,
action: 'SwapOutput'
})
switch (type) {
case 'ETH_TO_TOKEN':
new web3.eth.Contract(EXCHANGE_ABI, fromToken[outputCurrency]).methods
.ethToTokenSwapOutput(
BN(outputValue)
.multipliedBy(10 ** outputDecimals)
.toFixed(0),
deadline
)
.send(
{
from: account,
value: BN(inputValue)
.multipliedBy(10 ** inputDecimals)
.multipliedBy(1 + ALLOWED_SLIPPAGE)
.toFixed(0)
},
(err, data) => {
if (!err) {
addPendingTx(data)
this.reset()
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
} catch {
setIndependentError(t('insufficientLiquidity'))
}
}
)
break
case 'TOKEN_TO_ETH':
new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency]).methods
.tokenToEthSwapOutput(
BN(outputValue)
.multipliedBy(10 ** outputDecimals)
.toFixed(0),
BN(inputValue)
.multipliedBy(10 ** inputDecimals)
.multipliedBy(1 + ALLOWED_SLIPPAGE)
.toFixed(0),
deadline
)
.send({ from: account }, (err, data) => {
if (!err) {
addPendingTx(data)
this.reset()
}
})
break
case 'TOKEN_TO_TOKEN':
if (!inputAmountB) {
return
}
new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency]).methods
.tokenToTokenSwapOutput(
BN(outputValue)
.multipliedBy(10 ** outputDecimals)
.toFixed(0),
BN(inputValue)
.multipliedBy(10 ** inputDecimals)
.multipliedBy(1 + TOKEN_ALLOWED_SLIPPAGE)
.toFixed(0),
inputAmountB.multipliedBy(1.2).toFixed(0),
deadline,
outputCurrency
)
.send({ from: account }, (err, data) => {
if (!err) {
addPendingTx(data)
this.reset()
}
})
break
default:
break
return () => {
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' })
}
}
}
renderSummary(inputError, outputError) {
const { inputValue, inputCurrency, outputValue, outputCurrency } = this.state
const { t, account } = this.props
const inputIsZero = BN(inputValue).isZero()
const outputIsZero = BN(outputValue).isZero()
let contextualInfo = ''
let isError = false
if (!inputCurrency || !outputCurrency) {
contextualInfo = t('selectTokenCont')
}
if (!inputValue || !outputValue) {
contextualInfo = t('enterValueCont')
}
} else if (swapType === TOKEN_TO_TOKEN) {
const reserveETHFirst = inputReserveETH
const reserveTokenFirst = inputReserveToken
if (inputError || outputError) {
contextualInfo = inputError || outputError
isError = true
}
const reserveETHSecond = outputReserveETH
const reserveTokenSecond = outputReserveToken
if (inputIsZero || outputIsZero) {
contextualInfo = t('noLiquidity')
}
if (amount && reserveETHFirst && reserveTokenFirst && reserveETHSecond && reserveTokenSecond) {
try {
const intermediateValue =
independentField === INPUT
? calculateEtherTokenOutputFromInput(amount, reserveTokenFirst, reserveETHFirst)
: calculateEtherTokenInputFromOutput(amount, reserveTokenFirst, reserveETHFirst)
if (intermediateValue.lte(ethers.constants.Zero)) {
throw Error()
}
const calculatedDependentValue =
independentField === INPUT
? calculateEtherTokenOutputFromInput(intermediateValue, reserveETHSecond, reserveTokenSecond)
: calculateEtherTokenInputFromOutput(intermediateValue, reserveETHSecond, reserveTokenSecond)
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
throw Error()
}
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
} catch {
setIndependentError(t('insufficientLiquidity'))
}
return () => {
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' })
}
}
}
}, [
independentValueParsed,
swapType,
outputReserveETH,
outputReserveToken,
inputReserveETH,
inputReserveToken,
independentField,
t
])
const [inverted, setInverted] = useState(false)
const exchangeRate = getExchangeRate(inputValueParsed, inputDecimals, outputValueParsed, outputDecimals)
const exchangeRateInverted = getExchangeRate(inputValueParsed, inputDecimals, outputValueParsed, outputDecimals, true)
const marketRate = getMarketRate(
swapType,
inputReserveETH,
inputReserveToken,
inputDecimals,
outputReserveETH,
outputReserveToken,
outputDecimals
)
if (this.isUnapproved()) {
contextualInfo = t('unlockTokenCont')
}
const percentSlippage =
exchangeRate &&
marketRate &&
amountFormatter(
exchangeRate
.sub(marketRate)
.abs()
.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)))
.div(marketRate)
.sub(ethers.utils.bigNumberify(3).mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(15)))),
16,
2
)
if (!account) {
contextualInfo = t('noWallet')
isError = true
}
const isValid = exchangeRate && inputError === null && independentError === null
return (
<ContextualInfo
openDetailsText={t('transactionDetails')}
closeDetailsText={t('hideDetails')}
contextualInfo={contextualInfo}
isError={isError}
renderTransactionDetails={this.renderTransactionDetails}
/>
)
const estimatedText = `(${t('estimated')})`
function formatBalance(value) {
return `Balance: ${value}`
}
renderTransactionDetails = () => {
const { inputValue, inputCurrency, outputValue, outputCurrency, lastEditedField } = this.state
const { t, selectors, account } = this.props
function renderTransactionDetails() {
ReactGA.event({
category: 'TransactionDetail',
action: 'Open'
})
const ALLOWED_SLIPPAGE = 0.025
const TOKEN_ALLOWED_SLIPPAGE = 0.04
const type = getSwapType(inputCurrency, outputCurrency)
const { label: inputLabel } = selectors().getBalance(account, inputCurrency)
const { label: outputLabel } = selectors().getBalance(account, outputCurrency)
// const label = lastEditedField === INPUT ? outputLabel : inputLabel;
let minOutput
let maxInput
if (lastEditedField === INPUT) {
switch (type) {
case 'ETH_TO_TOKEN':
minOutput = BN(outputValue)
.multipliedBy(1 - ALLOWED_SLIPPAGE)
.toFixed(7)
.trim()
break
case 'TOKEN_TO_ETH':
minOutput = BN(outputValue)
.multipliedBy(1 - ALLOWED_SLIPPAGE)
.toFixed(7)
break
case 'TOKEN_TO_TOKEN':
minOutput = BN(outputValue)
.multipliedBy(1 - TOKEN_ALLOWED_SLIPPAGE)
.toFixed(7)
break
default:
break
}
}
if (lastEditedField === OUTPUT) {
switch (type) {
case 'ETH_TO_TOKEN':
maxInput = BN(inputValue)
.multipliedBy(1 + ALLOWED_SLIPPAGE)
.toFixed(7)
.trim()
break
case 'TOKEN_TO_ETH':
maxInput = BN(inputValue)
.multipliedBy(1 + ALLOWED_SLIPPAGE)
.toFixed(7)
break
case 'TOKEN_TO_TOKEN':
maxInput = BN(inputValue)
.multipliedBy(1 + TOKEN_ALLOWED_SLIPPAGE)
.toFixed(7)
break
default:
break
}
}
if (lastEditedField === INPUT) {
const b = text => <span className="swap__highlight-text">{text}</span>
if (independentField === INPUT) {
return (
<div>
<div>
{t('youAreSelling')} {b(`${+inputValue} ${inputLabel}`)} {t('orTransFail')}
{t('youAreSelling')}{' '}
{b(
`${amountFormatter(
independentValueParsed,
independentDecimals,
Math.min(4, independentDecimals)
)} ${inputSymbol}`
)}
.
</div>
<div className="send__last-summary-text">
{t('youWillReceive')}{' '}
{b(
`${amountFormatter(
dependentValueMinumum,
dependentDecimals,
Math.min(4, dependentDecimals)
)} ${outputSymbol}`
)}{' '}
{t('orTransFail')}
</div>
<div className="send__last-summary-text">
{t('youWillReceive')} {b(`${+minOutput} ${outputLabel}`)} {t('orTransFail')}
{t('priceChange')} {b(`${percentSlippage}%`)}.
</div>
</div>
)
......@@ -677,79 +471,153 @@ class Swap extends Component {
return (
<div>
<div>
{t('youAreBuying')} {b(`${+outputValue} ${outputLabel}`)}.
{t('youAreBuying')}{' '}
{b(
`${amountFormatter(
independentValueParsed,
independentDecimals,
Math.min(4, independentDecimals)
)} ${outputSymbol}`
)}
.
</div>
<div className="send__last-summary-text">
{t('itWillCost')} {b(`${+maxInput} ${inputLabel}`)} {t('orTransFail')}
{t('itWillCost')}{' '}
{b(
`${amountFormatter(
dependentValueMaximum,
dependentDecimals,
Math.min(4, dependentDecimals)
)} ${inputSymbol}`
)}{' '}
{t('orTransFail')}
</div>
<div className="send__last-summary-text">
{t('priceChange')} {b(`${percentSlippage}%`)}.
</div>
</div>
)
}
}
renderExchangeRate() {
const { t, account, selectors } = this.props
const { exchangeRate, inputCurrency, outputCurrency } = this.state
const { label: inputLabel } = selectors().getBalance(account, inputCurrency)
const { label: outputLabel } = selectors().getBalance(account, outputCurrency)
function renderSummary() {
let contextualInfo = ''
let isError = false
if (!exchangeRate || exchangeRate.isNaN() || !inputCurrency || !outputCurrency) {
return (
<OversizedPanel hideBottom>
<div className="swap__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t('exchangeRate')}</span>
<span> - </span>
</div>
</OversizedPanel>
)
if (inputError || independentError) {
contextualInfo = inputError || independentError
isError = true
} else if (!inputCurrency || !outputCurrency) {
contextualInfo = t('selectTokenCont')
} else if (!independentValue) {
contextualInfo = t('enterValueCont')
} else if (!account) {
contextualInfo = t('noWallet')
isError = true
}
return (
<OversizedPanel hideBottom>
<div className="swap__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t('exchangeRate')}</span>
<span>{`1 ${inputLabel} = ${exchangeRate.toFixed(7)} ${outputLabel}`}</span>
</div>
</OversizedPanel>
<ContextualInfo
openDetailsText={t('transactionDetails')}
closeDetailsText={t('hideDetails')}
contextualInfo={contextualInfo}
isError={isError}
renderTransactionDetails={renderTransactionDetails}
/>
)
}
renderBalance(currency, balance, decimals) {
if (!currency || decimals === 0) {
return ''
}
async function onSwap() {
const deadline = Math.ceil(Date.now() / 1000) + DEADLINE_FROM_NOW
const balanceInput = balance.dividedBy(BN(10 ** decimals)).toFixed(4)
return this.props.t('balance', { balanceInput })
}
render() {
const { t, selectors, account } = this.props
const { lastEditedField, inputCurrency, outputCurrency, inputValue, outputValue } = this.state
const estimatedText = `(${t('estimated')})`
let estimate, method, args, value
if (independentField === INPUT) {
ReactGA.event({
category: `${swapType}`,
action: 'SwapInput'
})
const { value: inputBalance, decimals: inputDecimals } = selectors().getBalance(account, inputCurrency)
const { value: outputBalance, decimals: outputDecimals } = selectors().getBalance(account, outputCurrency)
if (swapType === ETH_TO_TOKEN) {
estimate = contract.estimate.ethToTokenSwapInput
method = contract.ethToTokenSwapInput
args = [dependentValueMinumum, deadline]
value = independentValueParsed
} else if (swapType === TOKEN_TO_ETH) {
estimate = contract.estimate.tokenToEthSwapInput
method = contract.tokenToEthSwapInput
args = [independentValueParsed, dependentValueMinumum, deadline]
value = ethers.constants.Zero
} else if (swapType === TOKEN_TO_TOKEN) {
estimate = contract.estimate.tokenToTokenSwapInput
method = contract.tokenToTokenSwapInput
args = [independentValueParsed, dependentValueMinumum, ethers.constants.One, deadline, outputCurrency]
value = ethers.constants.Zero
}
} else if (independentField === OUTPUT) {
ReactGA.event({
category: `${swapType}`,
action: 'SwapOutput'
})
const { inputError, outputError, isValid } = this.validate()
if (swapType === ETH_TO_TOKEN) {
estimate = contract.estimate.ethToTokenSwapOutput
method = contract.ethToTokenSwapOutput
args = [independentValueParsed, deadline]
value = dependentValueMaximum
} else if (swapType === TOKEN_TO_ETH) {
estimate = contract.estimate.tokenToEthSwapOutput
method = contract.tokenToEthSwapOutput
args = [independentValueParsed, dependentValueMaximum, deadline]
value = ethers.constants.Zero
} else if (swapType === TOKEN_TO_TOKEN) {
estimate = contract.estimate.tokenToTokenSwapOutput
method = contract.tokenToTokenSwapOutput
args = [independentValueParsed, dependentValueMaximum, ethers.constants.MaxUint256, deadline, outputCurrency]
value = ethers.constants.Zero
}
}
const estimatedGasLimit = await estimate(...args, { value })
method(...args, { value, gasLimit: calculateGasMargin(estimatedGasLimit, GAS_MARGIN) }).then(response => {
addTransaction(response.hash, response)
})
}
return (
<>
<CurrencyInputPanel
title={t('input')}
description={lastEditedField === OUTPUT ? estimatedText : ''}
extraText={this.renderBalance(inputCurrency, inputBalance, inputDecimals)}
onCurrencySelected={inputCurrency => this.setState({ inputCurrency }, this.recalcForm)}
onValueChange={this.updateInput}
description={inputValueFormatted && independentField === OUTPUT ? estimatedText : ''}
extraText={inputBalanceFormatted && formatBalance(inputBalanceFormatted)}
extraTextClickHander={() => {
if (inputBalance && inputDecimals) {
const valueToSet = inputCurrency === 'ETH' ? inputBalance.sub(ethers.utils.parseEther('.1')) : inputBalance
if (valueToSet.gt(ethers.constants.Zero)) {
dispatchSwapState({
type: 'UPDATE_INDEPENDENT',
payload: { value: amountFormatter(valueToSet, inputDecimals, inputDecimals, false), field: INPUT }
})
}
}
}}
onCurrencySelected={inputCurrency => {
dispatchSwapState({ type: 'SELECT_CURRENCY', payload: { currency: inputCurrency, field: INPUT } })
}}
onValueChange={inputValue => {
dispatchSwapState({ type: 'UPDATE_INDEPENDENT', payload: { value: inputValue, field: INPUT } })
}}
showUnlock={showUnlock}
selectedTokens={[inputCurrency, outputCurrency]}
selectedTokenAddress={inputCurrency}
value={inputValue}
errorMessage={inputError}
value={inputValueFormatted}
errorMessage={inputError ? inputError : independentField === INPUT ? independentError : ''}
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
<img
onClick={this.flipInputOutput}
onClick={() => {
dispatchSwapState({ type: 'FLIP_INDEPENDENT' })
}}
className="swap__down-arrow swap__down-arrow--clickable"
alt="swap"
src={isValid ? ArrowDownBlue : ArrowDownGrey}
......@@ -758,115 +626,49 @@ class Swap extends Component {
</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}
description={outputValueFormatted && independentField === INPUT ? estimatedText : ''}
extraText={outputBalanceFormatted && formatBalance(outputBalanceFormatted)}
onCurrencySelected={outputCurrency => {
dispatchSwapState({ type: 'SELECT_CURRENCY', payload: { currency: outputCurrency, field: OUTPUT } })
}}
onValueChange={outputValue => {
dispatchSwapState({ type: 'UPDATE_INDEPENDENT', payload: { value: outputValue, field: OUTPUT } })
}}
selectedTokens={[inputCurrency, outputCurrency]}
value={outputValue}
selectedTokenAddress={outputCurrency}
errorMessage={outputError}
value={outputValueFormatted}
errorMessage={independentField === OUTPUT ? independentError : ''}
disableUnlock
/>
{this.renderExchangeRate()}
{this.renderSummary(inputError, outputError)}
<div className="swap__cta-container">
<SwapButton callOnClick={this.onSwap} isValid={isValid} />
</div>
</>
)
}
}
function SwapButton({ callOnClick, isValid }) {
const { t } = useTranslation()
const context = useWeb3Context()
const isActive = context.active && context.account
return (
<button
className={classnames('swap__cta-btn', { 'swap--inactive': !isActive })}
disabled={!isValid}
onClick={callOnClick}
<OversizedPanel hideBottom>
<div
className="swap__exchange-rate-wrapper"
onClick={() => {
setInverted(inverted => !inverted)
}}
>
<span className="swap__exchange-rate">{t('exchangeRate')}</span>
{inverted ? (
<span>
{exchangeRate
? `1 ${outputSymbol} = ${amountFormatter(exchangeRateInverted, 18, 4, false)} ${inputSymbol}`
: ' - '}
</span>
) : (
<span>
{exchangeRate
? `1 ${inputSymbol} = ${amountFormatter(exchangeRate, 18, 4, false)} ${outputSymbol}`
: ' - '}
</span>
)}
</div>
</OversizedPanel>
{renderSummary()}
<div className="swap__cta-container">
<button className="swap__cta-btn" disabled={!isValid} onClick={onSwap}>
{t('swap')}
</button>
</div>
</>
)
}
export default connect(
state => ({
balances: state.web3connect.balances,
account: state.web3connect.account,
web3: state.web3connect.web3,
exchangeAddresses: state.addresses.exchangeAddresses
}),
dispatch => ({
selectors: () => dispatch(selectors()),
addPendingTx: id => dispatch(addPendingTx(id))
})
)(withTranslation()(Swap))
const b = text => <span className="swap__highlight-text">{text}</span>
function calculateEtherTokenOutput({
inputAmount: rawInput,
inputReserve: rawReserveIn,
outputReserve: rawReserveOut
}) {
const inputAmount = BN(rawInput)
const inputReserve = BN(rawReserveIn)
const outputReserve = BN(rawReserveOut)
if (inputAmount.isLessThan(BN(10 ** 9))) {
console.warn(`inputAmount is only ${inputAmount.toFixed(0)}. Did you forget to multiply by 10 ** decimals?`)
}
const numerator = inputAmount.multipliedBy(outputReserve).multipliedBy(997)
const denominator = inputReserve.multipliedBy(1000).plus(inputAmount.multipliedBy(997))
return numerator.dividedBy(denominator)
}
function calculateEtherTokenInput({
outputAmount: rawOutput,
inputReserve: rawReserveIn,
outputReserve: rawReserveOut
}) {
const outputAmount = BN(rawOutput)
const inputReserve = BN(rawReserveIn)
const outputReserve = BN(rawReserveOut)
if (outputAmount.isLessThan(BN(10 ** 9))) {
console.warn(`inputAmount is only ${outputAmount.toFixed(0)}. Did you forget to multiply by 10 ** decimals?`)
}
const numerator = outputAmount.multipliedBy(inputReserve).multipliedBy(1000)
const denominator = outputReserve.minus(outputAmount).multipliedBy(997)
return numerator.dividedBy(denominator).plus(1)
}
function getSwapType(inputCurrency, outputCurrency) {
if (!inputCurrency || !outputCurrency) {
return
}
if (inputCurrency === outputCurrency) {
return
}
if (inputCurrency !== 'ETH' && outputCurrency !== 'ETH') {
return 'TOKEN_TO_TOKEN'
}
if (inputCurrency === 'ETH') {
return 'ETH_TO_TOKEN'
}
if (outputCurrency === 'ETH') {
return 'TOKEN_TO_ETH'
}
return
}
import { ethers } from 'ethers'
import FACTORY_ABI from '../abi/factory'
import EXCHANGE_ABI from '../abi/exchange'
import ERC20_ABI from '../abi/erc20'
import ERC20_WITH_BYTES_ABI from '../abi/erc20_symbol_bytes32'
import ERC20_WITH_BYTES_ABI from '../abi/erc20_bytes32'
import { FACTORY_ADDRESSES } from '../constants'
const factoryAddresses = {
1: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95',
4: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36'
}
export const errorCodes = ['TOKEN_DETAILS_DECIMALS', 'TOKEN_DETAILS_SYMBOL'].reduce(
export const ERROR_CODES = ['TOKEN_NAME', 'TOKEN_SYMBOL', 'TOKEN_DECIMALS'].reduce(
(accumulator, currentValue, currentIndex) => {
accumulator[currentValue] = currentIndex
return accumulator
......@@ -17,10 +14,6 @@ export const errorCodes = ['TOKEN_DETAILS_DECIMALS', 'TOKEN_DETAILS_SYMBOL'].red
{}
)
function getFactoryContract(networkId, signerOrProvider) {
return getContract(factoryAddresses[networkId], FACTORY_ABI, signerOrProvider)
}
export function isAddress(value) {
try {
ethers.utils.getAddress(value)
......@@ -30,42 +23,178 @@ export function isAddress(value) {
}
}
export function getSignerOrProvider(library, account) {
export function calculateGasMargin(value, margin) {
const offset = value.mul(margin).div(ethers.utils.bigNumberify(10000))
return value.add(offset)
}
// account is optional
export function getProviderOrSigner(library, account) {
return account ? library.getSigner(account) : library
}
export function getContract(contractAddress, ABI, signerOrProvider) {
return new ethers.Contract(contractAddress, ABI, signerOrProvider)
// account is optional
export function getContract(address, ABI, library, account) {
if (!isAddress(address) || address === ethers.constants.AddressZero) {
throw Error(`Invalid 'address' parameter '${address}'.`)
}
return new ethers.Contract(address, ABI, getProviderOrSigner(library, account))
}
// account is optional
export function getFactoryContract(networkId, library, account) {
return getContract(FACTORY_ADDRESSES[networkId], FACTORY_ABI, getProviderOrSigner(library, account))
}
export async function getTokenDetails(tokenAddress, signerOrProvider) {
const contract = getContract(tokenAddress, ERC20_ABI, signerOrProvider)
// account is optional
export function getExchangeContract(exchangeAddress, library, account) {
return getContract(exchangeAddress, EXCHANGE_ABI, getProviderOrSigner(library, account))
}
// get token name
export async function getTokenName(tokenAddress, library) {
if (!isAddress(tokenAddress)) {
throw Error(`Invalid 'tokenAddress' parameter '${tokenAddress}'.`)
}
const decimalsPromise = contract.decimals().catch(error => {
console.log(error)
error.code = errorCodes.TOKEN_DETAILS_DECIMALS
return getContract(tokenAddress, ERC20_ABI, library)
.name()
.catch(() =>
getContract(tokenAddress, ERC20_WITH_BYTES_ABI, library)
.name()
.then(bytes32 => ethers.utils.parseBytes32String(bytes32))
)
.catch(error => {
error.code = ERROR_CODES.TOKEN_SYMBOL
throw error
})
const symbolPromise = contract
}
// get token symbol
export async function getTokenSymbol(tokenAddress, library) {
if (!isAddress(tokenAddress)) {
throw Error(`Invalid 'tokenAddress' parameter '${tokenAddress}'.`)
}
return getContract(tokenAddress, ERC20_ABI, library)
.symbol()
.catch(() => {
const contractBytes32 = getContract(tokenAddress, ERC20_WITH_BYTES_ABI, signerOrProvider)
const contractBytes32 = getContract(tokenAddress, ERC20_WITH_BYTES_ABI, library)
return contractBytes32.symbol().then(bytes32 => ethers.utils.parseBytes32String(bytes32))
})
.catch(error => {
error.code = errorCodes.TOKEN_DETAILS_SYMBOL
error.code = ERROR_CODES.TOKEN_SYMBOL
throw error
})
}
// get token decimals
export async function getTokenDecimals(tokenAddress, library) {
if (!isAddress(tokenAddress)) {
throw Error(`Invalid 'tokenAddress' parameter '${tokenAddress}'.`)
}
return getContract(tokenAddress, ERC20_ABI, library)
.decimals()
.catch(error => {
error.code = ERROR_CODES.TOKEN_DECIMALS
throw error
})
}
return Promise.all([decimalsPromise, symbolPromise]).then(([decimals, symbol]) => ({
decimals,
symbol,
tokenAddress
}))
// get the exchange address for a token from the factory
export async function getTokenExchangeAddressFromFactory(tokenAddress, networkId, library) {
return getFactoryContract(networkId, library).getExchange(tokenAddress)
}
export async function getExchangeDetails(networkId, tokenAddress, signerOrProvider) {
const factoryContract = getFactoryContract(networkId, signerOrProvider)
// get the ether balance of an address
export async function getEtherBalance(address, library) {
if (!isAddress(address)) {
throw Error(`Invalid 'address' parameter '${address}'`)
}
return library.getBalance(address)
}
// get the token balance of an address
export async function getTokenBalance(tokenAddress, address, library) {
if (!isAddress(tokenAddress) || !isAddress(address)) {
throw Error(`Invalid 'tokenAddress' or 'address' parameter '${tokenAddress}' or '${address}'.`)
}
return getContract(tokenAddress, ERC20_ABI, library).balanceOf(address)
}
// get the token allowance
export async function getTokenAllowance(address, tokenAddress, spenderAddress, library) {
if (!isAddress(address) || !isAddress(tokenAddress) || !isAddress(spenderAddress)) {
throw Error(
"Invalid 'address' or 'tokenAddress' or 'spenderAddress' parameter" +
`'${address}' or '${tokenAddress}' or '${spenderAddress}'.`
)
}
return getContract(tokenAddress, ERC20_ABI, library).allowance(address, spenderAddress)
}
// amount must be a BigNumber, {base,display}Decimals must be Numbers
export function amountFormatter(amount, baseDecimals = 18, displayDecimals = 3, useLessThan = true) {
if (baseDecimals > 18 || displayDecimals > 18 || displayDecimals > baseDecimals) {
throw Error(`Invalid combination of baseDecimals '${baseDecimals}' and displayDecimals '${displayDecimals}.`)
}
return factoryContract.getExchange(tokenAddress).then(exchangeAddress => ({ exchangeAddress, tokenAddress }))
// if balance is falsy, return undefined
if (!amount) {
return undefined
}
// if amount is 0, return
else if (amount.isZero()) {
return '0'
}
// amount > 0
else {
// amount of 'wei' in 1 'ether'
const baseAmount = ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(baseDecimals))
const minimumDisplayAmount = baseAmount.div(
ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(displayDecimals))
)
// if balance is less than the minimum display amount
if (amount.lt(minimumDisplayAmount)) {
return useLessThan
? `<${ethers.utils.formatUnits(minimumDisplayAmount, baseDecimals)}`
: `${ethers.utils.formatUnits(amount, baseDecimals)}`
}
// if the balance is greater than the minimum display amount
else {
const stringAmount = ethers.utils.formatUnits(amount, baseDecimals)
// if there isn't a decimal portion
if (!stringAmount.match(/\./)) {
return stringAmount
}
// if there is a decimal portion
else {
const [wholeComponent, decimalComponent] = stringAmount.split('.')
const roundUpAmount = minimumDisplayAmount.div(ethers.constants.Two)
const roundedDecimalComponent = ethers.utils
.bigNumberify(decimalComponent.padEnd(baseDecimals, '0'))
.add(roundUpAmount)
.toString()
.padStart(baseDecimals, '0')
.substring(0, displayDecimals)
// decimals are too small to show
if (roundedDecimalComponent === '0'.repeat(displayDecimals)) {
return wholeComponent
}
// decimals are not too small to show
else {
return `${wholeComponent}.${roundedDecimalComponent.toString().replace(/0*$/, '')}`
}
}
}
}
}
......@@ -56,6 +56,7 @@ $pizazz-orange: #ff8f05;
&:disabled {
background-color: $mercury-gray;
cursor: auto;
}
}
......
This source diff could not be displayed because it is too large. You can view the blob instead.
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