Commit 1ec6709f authored by Noah Zinsmeister's avatar Noah Zinsmeister Committed by GitHub

Merge pull request #295 from NoahZinsmeister/production

deploy beta to prod
parents 4e413915 25692792
{
"extends": "react-app"
}
---
name: Token Request
about: Request a token addition
title: ''
labels: token request
assignees: ''
---
**Please provide the following information for your token.**
Token Address:
Token Name (from contract):
Token Decimals (from contract):
Token Symbol (from contract):
Uniswap Exchange Address of Token:
Link to the official homepage of the token:
branches:
only:
- production
- beta
except:
- master
language: node_js
node_js:
- '10'
......
......@@ -5,29 +5,25 @@
"homepage": ".",
"private": true,
"dependencies": {
"bignumber.js": "^7.2.1",
"@reach/tooltip": "^0.2.0",
"classnames": "^2.2.6",
"escape-string-regexp": "^2.0.0",
"ethers": "^4.0.27",
"fuse": "^0.4.0",
"i18next": "^15.0.9",
"i18next-browser-languagedetector": "^3.0.1",
"i18next-xhr-backend": "^2.0.1",
"jazzicon": "^1.5.0",
"node-sass": "^4.11.0",
"prop-types": "^15.7.2",
"react": "^16.8.6",
"react-aria-modal": "^4.0.0",
"react-dom": "^16.8.6",
"react-ga": "^2.5.7",
"react-i18next": "^10.7.0",
"react-redux": "^5.0.7",
"react-router-dom": "^5.0.0",
"react-scripts": "^2.1.8",
"react-scripts": "^3.0.1",
"react-transition-group": "1.x",
"redux": "^3.7.2",
"redux-subscriber": "^1.1.0",
"redux-thunk": "^2.2.0",
"styled-components": "^4.2.0",
"ua-parser-js": "^0.7.18",
"web3": "1.0.0-beta.52",
"web3-react": "^5.0.4"
},
"scripts": {
......@@ -46,14 +42,23 @@
"check:format": "yarn format:base --check",
"check:all": "yarn check:lint && yarn check:format"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"license": "GPL-3.0-or-later",
"devDependencies": {
"prettier": "^1.17.0"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
}
{
"noWallet": "Keine Ethereum wallet gefunden",
"noWallet": "Keine Ethereum-Wallet gefunden",
"wrongNetwork": "Du bist auf dem falschen Netzwerk.",
"switchNetwork": "Bitte wechsle zum {{ correctNetwork }}",
"installWeb3MobileBrowser": "Bitte besuche uns mit einem web3-fähigen mobilen Browser wie z.B. Trust Wallet oder Coinbase Wallet.",
"installMetamask": "Bitte besuch uns erneut nachdem du Metamask oder Brave installiert hast.",
"installMetamask": "Bitte besuch uns erneut, nachdem du Metamask oder Brave installiert hast.",
"disconnected": "Nicht verbunden",
"swap": "Tauschen",
"send": "Senden",
......@@ -58,7 +58,7 @@
"totalSupplyIs0": "Die gesamte Anzahl Liquiditätstokens ist aktuell 0.",
"tokenWorth": "Zum gegenwärtigen Wechselkurs ist jeder Pool Token so viel Wert",
"firstLiquidity": "Du bist die erste Person die Liquidität bereitstellt!",
"initialExchangeRate": "The initial exchange rate will be set based on your deposits. Please make sure that your ETH and {{ label }} deposits have the same fiat value.",
"initialExchangeRate": "Der initiale Wechselkurs wird auf deiner Überweisung basieren. Stelle sicher, dass deine ETH und {{ label }} denselben Fiatwert haben.",
"removeLiquidity": "Liquidität entfernen",
"poolTokens": "Pool Tokens",
"enterLabelCont": "{{ label }} Wert eingeben um fortzufahren.",
......
......@@ -22,9 +22,11 @@
"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",
"slippageWarning": "Slippage Warning",
"youAreSelling": "You are selling",
"orTransFail": "or the transaction will fail.",
"youWillReceive": "You will receive at least",
......@@ -46,7 +48,7 @@
"noZero": "Amount cannot be zero.",
"mustBeETH": "One of the input must be ETH.",
"enterCurrencyOrLabelCont": "Enter a {{ inputCurrency }} or {{ label }} value to continue.",
"youAreAdding": "You are adding between",
"youAreAdding": "You are adding",
"and": "and",
"intoPool": "into the liquidity pool.",
"outPool": "from the liquidity pool.",
......@@ -70,7 +72,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 { isAddress } from '../../utils'
import { useDebounce } from '../../hooks'
// import QrCode from '../QrCode' // commented out pending further review
import './address-input-panel.scss'
export default function AddressInputPanel({ title, onChange = () => {}, value = '', errorMessage }) {
export default function AddressInputPanel({ title, initialInput = '', onChange = () => {}, onError = () => {} }) {
const { t } = useTranslation()
const { library } = useWeb3Context()
const [input, setInput] = useState(initialInput)
const debouncedInput = useDebounce(input, 150)
const [data, setData] = useState({ address: undefined, name: undefined })
const [error, setError] = useState(false)
// keep data and errors in sync
useEffect(() => {
onChange({ address: data.address, name: data.name })
}, [onChange, data.address, data.name])
useEffect(() => {
onError(error)
}, [onError, error])
// run parser on debounced input
useEffect(() => {
let stale = false
if (isAddress(debouncedInput)) {
library.lookupAddress(debouncedInput).then(name => {
if (!stale) {
// if an ENS name exists, set it as the destination
if (name) {
setInput(name)
} else {
setData({ address: debouncedInput, name: '' })
setError(null)
}
}
})
} else {
if (debouncedInput !== '') {
try {
library.resolveName(debouncedInput).then(address => {
if (!stale) {
// if the debounced input name resolves to an address
if (address) {
setData({ address: address, name: debouncedInput })
setError(null)
} else {
setError(true)
}
}
})
} catch {
setError(true)
}
}
}
return () => {
stale = true
}
}, [debouncedInput, library, onChange, onError])
function onInput(event) {
if (data.address !== undefined || data.name !== undefined) {
setData({ address: undefined, name: undefined })
}
if (error !== undefined) {
setError()
}
const input = event.target.value
const checksummedInput = isAddress(input)
setInput(checksummedInput || input)
}
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">
......@@ -25,12 +97,16 @@ export default function AddressInputPanel({ title, onChange = () => {}, value =
<div className="currency-input-panel__input-row">
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
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={onInput}
value={input}
/>
</div>
</div>
......
......@@ -45,30 +45,32 @@ class ContextualInfo extends Component {
)
}
return [
<div
key="open-details"
className="contextual-info__summary-wrapper contextual-info__open-details-container"
onClick={() =>
this.setState(prevState => {
return { showDetails: !prevState.showDetails }
})
}
>
{!this.state.showDetails ? (
<>
<span>{openDetailsText}</span>
<img src={DropdownBlue} alt="dropdown" />
</>
) : (
<>
<span>{closeDetailsText}</span>
<img src={DropupBlue} alt="dropup" />
</>
)}
</div>,
this.renderDetails()
]
return (
<>
<div
key="open-details"
className="contextual-info__summary-wrapper contextual-info__open-details-container"
onClick={() =>
this.setState(prevState => {
return { showDetails: !prevState.showDetails }
})
}
>
{!this.state.showDetails ? (
<>
<span>{openDetailsText}</span>
<img src={DropdownBlue} alt="dropdown" />
</>
) : (
<>
<span>{closeDetailsText}</span>
<img src={DropupBlue} alt="dropup" />
</>
)}
</div>
{this.renderDetails()}
</>
)
}
}
......
@import '../../variables.scss';
.contextual-info {
&__summary-wrapper {
color: $dove-gray;
font-size: 0.75rem;
text-align: center;
margin-top: 1rem;
padding-top: 1rem;
}
&--error {
color: $salmon-red;
}
&__details {
background-color: $concrete-gray;
padding: 1.5rem;
border-radius: 1rem;
font-size: 0.75rem;
margin-top: 1rem;
}
&__open-details-container {
cursor: pointer;
@extend %row-nowrap;
align-items: center;
justify-content: center;
font-size: 0.75rem;
color: $royal-blue;
span {
margin-right: 12px;
}
img {
height: 0.75rem;
width: 0.75rem;
}
}
}
import React, { useState } from 'react'
import styled from 'styled-components'
import c from 'classnames'
import { ReactComponent as Dropup } from '../../assets/images/dropup-blue.svg'
import { ReactComponent as Dropdown } from '../../assets/images/dropdown-blue.svg'
import './contextual-info.scss'
const WrappedDropup = ({ isError, ...rest }) => <Dropup {...rest} />
const ColoredDropup = styled(WrappedDropup)`
path {
stroke: ${props => props.isError && props.theme.salmonRed};
}
`
const WrappedDropdown = ({ isError, ...rest }) => <Dropdown {...rest} />
const ColoredDropdown = styled(WrappedDropdown)`
path {
stroke: ${props => props.isError && props.theme.salmonRed};
}
`
export default function ContextualInfo({
openDetailsText = 'Transaction Details',
closeDetailsText = 'Hide Details',
contextualInfo = '',
allowExpand = false,
renderTransactionDetails = () => {},
isError = false
}) {
const [showDetails, setShowDetails] = useState(false)
return !allowExpand ? (
<div className={c({ 'contextual-info--error': isError }, 'contextual-info__summary-wrapper')}>
<div>{contextualInfo}</div>
</div>
) : (
<>
<div
key="open-details"
className="contextual-info__summary-wrapper contextual-info__open-details-container"
onClick={() => setShowDetails(s => !s)}
>
<>
<span className={c({ 'contextual-info--error': isError })}>
{contextualInfo ? contextualInfo : showDetails ? closeDetailsText : openDetailsText}
</span>
{showDetails ? <ColoredDropup isError={isError} /> : <ColoredDropdown isError={isError} />}
</>
</div>
{showDetails && <div className="contextual-info__details">{renderTransactionDetails()}</div>}
</>
)
}
......@@ -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 { useTransactionAdder, usePendingApproval } from '../../contexts/Transactions'
import { useTokenDetails, useAllTokenDetails } from '../../contexts/Tokens'
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,237 +26,70 @@ 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]
}
return tokenList.filter(({ address }) => !filteredTokens.includes(address))
}
function onTokenSelect(address) {
setSearchQuery('')
setIsShowingModal(false)
onCurrencySelected(address)
}
function renderTokenList() {
const tokens = createTokenList()
const pendingApproval = usePendingApproval(selectedTokenAddress)
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]
if (!exchangeAddress) {
setLoadingExchange(true)
getExchangeDetails(context.networkId, tokenAddress, signerOrProvider).then(async ({ exchangeAddress }) => {
if (exchangeAddress !== ethers.constants.AddressZero) {
const { symbol } = await getTokenDetails(tokenAddress, signerOrProvider)
addExchange({
tokenAddress,
label: symbol,
exchangeAddress
})
}
setLoadingExchange(false)
})
}
}
if (disableTokenSelect) {
return
}
let results
if (!searchQuery) {
results = tokens
} 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) {
return (
<div className="token-modal__token-row token-modal__token-row--no-exchange">
<div>{t('noExchange')}</div>
</div>
)
}
return results.map(({ label, address }) => {
const isSelected = selectedTokens.indexOf(address) > -1
const addTransaction = useTransactionAdder()
const inputRef = useRef()
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>
)
})
}
const allTokens = useAllTokenDetails()
function renderModal() {
if (!isShowingModal) {
return null
// manage focus on modal show
useEffect(() => {
if (inputRef.current && isShowingModal) {
inputRef.current.focus()
}
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>
)
}
}, [isShowingModal])
function renderUnlockButton() {
if (disableUnlock || !selectedTokenAddress || selectedTokenAddress === 'ETH') {
return
}
const { value: allowance, decimals, label } = selectors().getApprovals(
selectedTokenAddress,
account,
fromToken[selectedTokenAddress]
)
if (!label || (allowance.isGreaterThanOrEqualTo(BN((value || 0) * 10 ** decimals)) && !BN(allowance).isZero())) {
return
}
const approvalTxId = pendingApprovals[selectedTokenAddress]
if (approvalTxId && transactions.pending.includes(approvalTxId)) {
return (
<button className="currency-input-panel__sub-currency-select currency-input-panel__sub-currency-select--pending">
<div className="loader" />
{t('pending')}
</button>
)
if (disableUnlock || !showUnlock || selectedTokenAddress === 'ETH' || !selectedTokenAddress) {
return null
} else {
if (!pendingApproval) {
return (
<button
className="currency-input-panel__sub-currency-select"
onClick={async () => {
const estimatedGas = await tokenContract.estimate.approve(
selectedTokenExchangeAddress,
ethers.constants.MaxUint256
)
tokenContract
.approve(selectedTokenExchangeAddress, ethers.constants.MaxUint256, {
gasLimit: calculateGasMargin(estimatedGas, GAS_MARGIN)
})
.then(response => {
addTransaction(response)
})
}}
>
{t('unlock')}
</button>
)
} else {
return (
<button className="currency-input-panel__sub-currency-select currency-input-panel__sub-currency-select--pending">
<div className="loader" />
{t('pending')}
</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,161 @@ 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] &&
!!tokenEntry[tokenEntryKey].match(new RegExp(escapeStringRegex(searchQuery), 'i'))
)
})
return regexMatches.some(m => m)
})
)(withTranslation()(CurrencyInputPanel))
)
}, [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>
)
})
}
// manage focus on modal show
const inputRef = useRef()
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus()
}
}, [])
function onInput(event) {
const input = event.target.value
const checksummedInput = isAddress(input)
setSearchQuery(checksummedInput || input)
}
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={onInput}
/>
<img src={SearchIcon} className="token-modal__search-icon" alt="search" />
</div>
<div className="token-modal__token-list">{renderTokenList()}</div>
</div>
</CSSTransitionGroup>
</Modal>
)
}
import React, { useCallback } from 'react'
import { withRouter, NavLink } from 'react-router-dom'
import { connect } from 'react-redux'
import { useTranslation } from 'react-i18next'
import { dismissBetaMessage } from '../../ducks/app'
import { useBodyKeyDown } from '../../hooks'
import './navigation-tabs.scss'
import { useBetaMessageManager } from '../../contexts/Application'
const tabOrder = [
{
......@@ -26,9 +25,11 @@ const tabOrder = [
}
]
function NavigationTabs({ location: { pathname }, history, dismissBetaMessage, showBetaMessage }) {
function NavigationTabs({ location: { pathname }, history }) {
const { t } = useTranslation()
const [showBetaMessage, dismissBetaMessage] = useBetaMessageManager()
const navigate = useCallback(
direction => {
const tabIndex = tabOrder.findIndex(({ regex }) => pathname.match(regex))
......@@ -73,13 +74,4 @@ function NavigationTabs({ location: { pathname }, history, dismissBetaMessage, s
)
}
export default withRouter(
connect(
state => ({
showBetaMessage: state.app.showBetaMessage
}),
dispatch => ({
dismissBetaMessage: () => dispatch(dismissBetaMessage())
})
)(NavigationTabs)
)
export default withRouter(NavigationTabs)
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/Transactions'
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(hash => !allTransactions[hash].receipt)
const confirmed = Object.keys(allTransactions).filter(hash => allTransactions[hash].receipt)
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 (
return (
<div
className={classnames('web3-status', {
'web3-status__connected': active,
'web3-status--pending': hasPendingTransactions,
'web3-status--confirmed': hasConfirmedTransactions
})}
onClick={handleClick}
>
<div className="web3-status__text">
{hasPendingTransactions ? getPendingText(pending, t('pending')) : getText(account, t('disconnected'))}
</div>
<div
className={classnames('web3-status', {
'web3-status__connected': this.props.isConnected,
'web3-status--pending': hasPendingTransactions,
'web3-status--confirmed': hasConfirmedTransactions
})}
onClick={this.handleClick}
>
<div className="web3-status__text">
{hasPendingTransactions ? getPendingText(pending, t('pending')) : getText(address, t('disconnected'))}
</div>
<div
className="web3-status__identicon"
ref={el => {
if (!el) {
return
}
if (!address || address.length < 42 || !ethers.utils.isHexString(address)) {
return
}
className="web3-status__identicon"
ref={el => {
if (!el || !account) {
return
} else {
el.innerHTML = ''
el.appendChild(Jazzicon(16, parseInt(address.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>
el.appendChild(Jazzicon(16, parseInt(account.slice(2, 10), 16)))
}
}}
/>
{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))
// string literals for actions
// set global web3 object
export const INITIALIZE_GLOBAL_WEB3 = 'INITIALIZE_GLOBAL_WEB3'
// web3 actions, all set from action creator to reducer to app
export const SET_WEB3_CONNECTION_STATUS = 'WEB3_CONNECTION_STATUS'
export const CHECK_WEB3_CONNECTION = 'CHECK_WEB3_CONNECTION'
export const SET_CURRENT_MASK_ADDRESS = 'SET_CURRENT_MASK_ADDRESS'
export const SET_INTERACTION_STATE = 'SET_INTERACTION_STATE'
export const SET_NETWORK_MESSAGE = 'SET_NETWORK_MESSAGE'
export const SET_BLOCK_TIMESTAMP = 'SET_BLOCK_TIMESTAMP'
export const SET_EXCHANGE_TYPE = 'SET_EXCHANGE_TYPE'
// actions to toggle divs
export const TOGGLE_ABOUT = 'TOGGLE_ABOUT'
export const TOGGLE_INVEST = 'TOGGLE_INVEST'
// CONTRACT actions in actions, action creator, reducer
export const FACTORY_CONTRACT_READY = 'FACTORY_CONTRACT_READY'
export const EXCHANGE_CONTRACT_READY = 'EXCHANGE_CONTRACT_READY'
export const TOKEN_CONTRACT_READY = 'TOKEN_CONTRACT_READY'
// actions for the exchange
export const SET_INPUT_BALANCE = 'SET_INPUT_BALANCE'
export const SET_OUTPUT_BALANCE = 'SET_OUTPUT_BALANCE'
export const SET_INPUT_TOKEN = 'SET_INPUT_TOKEN'
export const SET_OUTPUT_TOKEN = 'SET_OUTPUT_TOKEN'
export const SET_ETH_POOL_1 = 'SET_ETH_POOL_1'
export const SET_ETH_POOL_2 = 'SET_ETH_POOL_2'
export const SET_TOKEN_POOL_1 = 'SET_TOKEN_POOL_1'
export const SET_TOKEN_POOL_2 = 'SET_TOKEN_POOL_2'
export const SET_ALLOWANCE_APPROVAL_STATE = 'SET_ALLOWANCE_APPROVAL_STATE'
export const SET_EXCHANGE_INPUT_VALUE = 'SET_EXCHANGE_INPUT_VALUE'
export const SET_EXCHANGE_OUTPUT_VALUE = 'SET_EXCHANGE_OUTPUT_VALUE'
export const SET_EXCHANGE_RATE = 'SET_EXCHANGE_RATE'
export const SET_EXCHANGE_FEE = 'SET_EXCHANGE_FEE'
export const SET_INVEST_TOKEN = 'SET_INVEST_TOKEN'
export const SET_INVEST_ETH_POOL = 'SET_INVEST_ETH'
export const SET_INVEST_TOKEN_POOL = 'SET_INVEST_TOKENS'
export const SET_INVEST_TOKEN_ALLOWANCE = 'SET_INVEST_TOKEN_ALLOWANCE'
export const SET_INVEST_SHARES = 'SET_INVEST_SHARES'
export const SET_USER_SHARES = 'SET_USER_SHARES'
export const SET_INVEST_TOKEN_BALANCE = 'SET_INVEST_TOKEN_BALANCE'
export const SET_INVEST_ETH_BALANCE = 'SET_INVEST_ETH_BALANCE'
export const SET_INVEST_SHARES_INPUT = 'SET_INVEST_SHARES_INPUT'
export const SET_INVEST_ETH_REQUIRED = 'SET_INVEST_ETH_REQUIRED'
export const SET_INVEST_TOKENS_REQUIRED = 'SET_INVEST_TOKENS_REQUIRED'
export const SET_INVEST_CHECKED = 'SET_INVEST_CHECKED'
export const INSUFFICIENT_BALANCE = 'Insufficient balance'
export const FACTORY_ADDRESSES = {
1: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95',
4: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36'
}
import React, { createContext, useContext, useReducer, useCallback, useMemo, useEffect } from 'react'
import { useWeb3Context } from 'web3-react'
import { safeAccess, isAddress, getTokenAllowance } from '../utils'
import { useBlockNumber } from './Application'
const UPDATE = 'UPDATE'
const AllowancesContext = createContext()
function useAllowancesContext() {
return useContext(AllowancesContext)
}
function reducer(state, { type, payload }) {
switch (type) {
case UPDATE: {
const { networkId, address, tokenAddress, spenderAddress, value, blockNumber } = payload
return {
...state,
[networkId]: {
...(safeAccess(state, [networkId]) || {}),
[address]: {
...(safeAccess(state, [networkId, address]) || {}),
[tokenAddress]: {
...(safeAccess(state, [networkId, address, tokenAddress]) || {}),
[spenderAddress]: {
value,
blockNumber
}
}
}
}
}
}
default: {
throw Error(`Unexpected action type in AllowancesContext reducer: '${type}'.`)
}
}
}
export default function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, {})
const update = useCallback((networkId, address, tokenAddress, spenderAddress, value, blockNumber) => {
dispatch({ type: UPDATE, payload: { networkId, address, tokenAddress, spenderAddress, value, blockNumber } })
}, [])
const contextValue = useMemo(() => [state, { update }], [state, update])
return <AllowancesContext.Provider value={contextValue}>{children}</AllowancesContext.Provider>
}
export function useAddressAllowance(address, tokenAddress, spenderAddress) {
const { networkId, library } = useWeb3Context()
const globalBlockNumber = useBlockNumber()
const [state, { update }] = useAllowancesContext()
const { value, blockNumber } = safeAccess(state, [networkId, address, tokenAddress, spenderAddress]) || {}
useEffect(() => {
if (
isAddress(address) &&
isAddress(tokenAddress) &&
isAddress(spenderAddress) &&
(value === undefined || blockNumber !== globalBlockNumber) &&
(networkId || networkId === 0) &&
library
) {
let stale = false
getTokenAllowance(address, tokenAddress, spenderAddress, library)
.then(value => {
if (!stale) {
update(networkId, address, tokenAddress, spenderAddress, value, globalBlockNumber)
}
})
.catch(() => {
if (!stale) {
update(networkId, address, tokenAddress, spenderAddress, null, globalBlockNumber)
}
})
return () => {
stale = true
}
}
}, [address, tokenAddress, spenderAddress, value, blockNumber, globalBlockNumber, networkId, library, update])
return value
}
import React, { createContext, useContext, useReducer, useCallback, useMemo, useEffect } from 'react'
import { useWeb3Context } from 'web3-react'
import { safeAccess } from '../utils'
const SHOW_BETA_MESSAGE = 'SHOW_BETA_MESSAGE'
const BLOCK_NUMBERS = 'BLOCK_NUMBERS'
const DISMISS_BETA_MESSAGE = 'DISMISS_BETA_MESSAGE'
const UPDATE_BLOCK_NUMBER = 'UPDATE_BLOCK_NUMBER'
const ApplicationContext = createContext()
function useApplicationContext() {
return useContext(ApplicationContext)
}
function reducer(state, { type, payload }) {
switch (type) {
case DISMISS_BETA_MESSAGE: {
return {
...state,
[SHOW_BETA_MESSAGE]: false
}
}
case UPDATE_BLOCK_NUMBER: {
const { networkId, blockNumber } = payload
return {
...state,
[BLOCK_NUMBERS]: {
...(safeAccess(state, [BLOCK_NUMBERS]) || {}),
[networkId]: blockNumber
}
}
}
default: {
throw Error(`Unexpected action type in ApplicationContext reducer: '${type}'.`)
}
}
}
export default function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, {
[SHOW_BETA_MESSAGE]: true,
[BLOCK_NUMBERS]: {}
})
const dismissBetaMessage = useCallback(() => {
dispatch({ type: DISMISS_BETA_MESSAGE })
}, [])
const updateBlockNumber = useCallback((networkId, blockNumber) => {
dispatch({ type: UPDATE_BLOCK_NUMBER, payload: { networkId, blockNumber } })
}, [])
const contextValue = useMemo(() => [state, { dismissBetaMessage, updateBlockNumber }], [
state,
dismissBetaMessage,
updateBlockNumber
])
return <ApplicationContext.Provider value={contextValue}>{children}</ApplicationContext.Provider>
}
export function Updater() {
const { networkId, library } = useWeb3Context()
const [, { updateBlockNumber }] = useApplicationContext()
useEffect(() => {
if ((networkId || networkId === 0) && library) {
let stale = false
function update() {
library
.getBlockNumber()
.then(blockNumber => {
if (!stale) {
updateBlockNumber(networkId, blockNumber)
}
})
.catch(() => {
if (!stale) {
updateBlockNumber(networkId, null)
}
})
}
update()
library.on('block', update)
return () => {
stale = true
library.removeListener('block', update)
}
}
}, [networkId, library, updateBlockNumber])
return null
}
export function useBetaMessageManager() {
const [state, { dismissBetaMessage }] = useApplicationContext()
return [safeAccess(state, [SHOW_BETA_MESSAGE]), dismissBetaMessage]
}
export function useBlockNumber() {
const { networkId } = useWeb3Context()
const [state] = useApplicationContext()
return safeAccess(state, [BLOCK_NUMBERS, networkId])
}
import React, { createContext, useContext, useReducer, useCallback, useMemo, useEffect } from 'react'
import { useWeb3Context } from 'web3-react'
import { safeAccess, isAddress, getEtherBalance, getTokenBalance } from '../utils'
import { useBlockNumber } from './Application'
import { useTokenDetails } from './Tokens'
const UPDATE = 'UPDATE'
const BalancesContext = createContext()
function useBalancesContext() {
return useContext(BalancesContext)
}
function reducer(state, { type, payload }) {
switch (type) {
case UPDATE: {
const { networkId, address, tokenAddress, value, blockNumber } = payload
return {
...state,
[networkId]: {
...(safeAccess(state, [networkId]) || {}),
[address]: {
...(safeAccess(state, [networkId, address]) || {}),
[tokenAddress]: {
value,
blockNumber
}
}
}
}
}
default: {
throw Error(`Unexpected action type in BalancesContext reducer: '${type}'.`)
}
}
}
export default function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, {})
const update = useCallback((networkId, address, tokenAddress, value, blockNumber) => {
dispatch({ type: UPDATE, payload: { networkId, address, tokenAddress, value, blockNumber } })
}, [])
const contextValue = useMemo(() => [state, { update }], [state, update])
return <BalancesContext.Provider value={contextValue}>{children}</BalancesContext.Provider>
}
export function useAddressBalance(address, tokenAddress) {
const { networkId, library } = useWeb3Context()
const globalBlockNumber = useBlockNumber()
const [state, { update }] = useBalancesContext()
const { value, blockNumber } = safeAccess(state, [networkId, address, tokenAddress]) || {}
useEffect(() => {
if (
isAddress(address) &&
(tokenAddress === 'ETH' || isAddress(tokenAddress)) &&
(value === undefined || blockNumber !== globalBlockNumber) &&
(networkId || networkId === 0) &&
library
) {
let stale = false
;(tokenAddress === 'ETH' ? getEtherBalance(address, library) : getTokenBalance(tokenAddress, address, library))
.then(value => {
if (!stale) {
update(networkId, address, tokenAddress, value, globalBlockNumber)
}
})
.catch(e => {
if (!stale) {
update(networkId, address, tokenAddress, null, globalBlockNumber)
}
})
return () => {
stale = true
}
}
}, [address, tokenAddress, value, blockNumber, globalBlockNumber, networkId, library, update])
return value
}
export function useExchangeReserves(tokenAddress) {
const { exchangeAddress } = useTokenDetails(tokenAddress)
const reserveETH = useAddressBalance(exchangeAddress, 'ETH')
const reserveToken = useAddressBalance(exchangeAddress, tokenAddress)
return { reserveETH, reserveToken }
}
import React, { createContext, useContext, useReducer, useCallback, useMemo, useEffect } from 'react'
import { useWeb3Context } from 'web3-react'
import { ethers } from 'ethers'
import {
isAddress,
getTokenName,
getTokenSymbol,
getTokenDecimals,
getTokenExchangeAddressFromFactory,
safeAccess
} from '../utils'
const NAME = 'name'
const SYMBOL = 'symbol'
const DECIMALS = 'decimals'
const EXCHANGE_ADDRESS = 'exchangeAddress'
const UPDATE = 'UPDATE'
const INITIAL_TOKENS_CONTEXT = {
1: {
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'
},
'0x6c6EE5e31d828De241282B9606C8e98Ea48526E2': {
[NAME]: 'HoloToken',
[SYMBOL]: 'HOT',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0xd4777E164c6C683E10593E08760B803D58529a8E'
},
'0xD29F0b5b3F50b07Fe9a9511F7d86F4f4bAc3f8c4': {
[NAME]: 'Liquidity.Network Token',
[SYMBOL]: 'LQD',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0xe3406e7D0155E0a83236eC25D34Cd3D903036669'
},
'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 TokensContext = createContext()
function useTokensContext() {
return useContext(TokensContext)
}
function reducer(state, { type, payload }) {
switch (type) {
case UPDATE: {
const { networkId, tokenAddress, name, symbol, decimals, exchangeAddress } = payload
return {
...state,
[networkId]: {
...(safeAccess(state, [networkId]) || {}),
[tokenAddress]: {
[NAME]: name,
[SYMBOL]: symbol,
[DECIMALS]: decimals,
[EXCHANGE_ADDRESS]: exchangeAddress
}
}
}
}
default: {
throw Error(`Unexpected action type in TokensContext reducer: '${type}'.`)
}
}
}
export default function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, INITIAL_TOKENS_CONTEXT)
const update = useCallback((networkId, tokenAddress, name, symbol, decimals, exchangeAddress) => {
dispatch({ type: UPDATE, payload: { networkId, tokenAddress, name, symbol, decimals, exchangeAddress } })
}, [])
const contextValue = useMemo(() => [state, { update }], [state, update])
return <TokensContext.Provider value={contextValue}>{children}</TokensContext.Provider>
}
export function useTokenDetails(tokenAddress) {
const { networkId, library } = useWeb3Context()
const [state, { update }] = useTokensContext()
const { [NAME]: name, [SYMBOL]: symbol, [DECIMALS]: decimals, [EXCHANGE_ADDRESS]: exchangeAddress } =
safeAccess(state, [networkId, tokenAddress]) || {}
useEffect(() => {
if (
isAddress(tokenAddress) &&
(name === undefined || symbol === undefined || decimals === undefined || exchangeAddress === undefined) &&
(networkId || networkId === 0) &&
library
) {
let stale = false
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
)
Promise.all([namePromise, symbolPromise, decimalsPromise, exchangeAddressPromise]).then(
([resolvedName, resolvedSymbol, resolvedDecimals, resolvedExchangeAddress]) => {
if (!stale) {
update(networkId, tokenAddress, resolvedName, resolvedSymbol, resolvedDecimals, resolvedExchangeAddress)
}
}
)
return () => {
stale = true
}
}
}, [tokenAddress, name, symbol, decimals, exchangeAddress, networkId, library, update])
return { name, symbol, decimals, exchangeAddress }
}
export function useAllTokenDetails(requireExchange = true) {
const { networkId } = useWeb3Context()
const [state] = useTokensContext()
const tokenDetails = safeAccess(state, [networkId]) || {}
return requireExchange
? Object.keys(tokenDetails)
.filter(
tokenAddress =>
tokenAddress === 'ETH' ||
(safeAccess(tokenDetails, [tokenAddress, EXCHANGE_ADDRESS]) &&
safeAccess(tokenDetails, [tokenAddress, EXCHANGE_ADDRESS]) !== ethers.constants.AddressZero)
)
.reduce((accumulator, tokenAddress) => {
accumulator[tokenAddress] = tokenDetails[tokenAddress]
return accumulator
}, {})
: tokenDetails
}
import React, { createContext, useContext, useReducer, useCallback, useMemo, useEffect } from 'react'
import { useWeb3Context } from 'web3-react'
import { ethers } from 'ethers'
import { safeAccess } from '../utils'
import { useBlockNumber } from './Application'
const RESPONSE = 'response'
const BLOCK_NUMBER_CHECKED = 'BLOCK_NUMBER_CHECKED'
const RECEIPT = 'receipt'
const ADD = 'ADD'
const CHECK = 'CHECK'
const FINALIZE = 'FINALIZE'
const TransactionsContext = createContext()
export function useTransactionsContext() {
return useContext(TransactionsContext)
}
function reducer(state, { type, payload }) {
switch (type) {
case ADD: {
const { networkId, hash, response } = payload
if (safeAccess(state, [networkId, hash]) !== null) {
throw Error('Attempted to add existing transaction.')
}
return {
...state,
[networkId]: {
...(safeAccess(state, [networkId]) || {}),
[hash]: {
[RESPONSE]: response
}
}
}
}
case CHECK: {
const { networkId, hash, blockNumber } = payload
if (safeAccess(state, [networkId, hash]) === null) {
throw Error('Attempted to check non-existent transaction.')
}
return {
...state,
[networkId]: {
...(safeAccess(state, [networkId]) || {}),
[hash]: {
...(safeAccess(state, [networkId, hash]) || {}),
[BLOCK_NUMBER_CHECKED]: blockNumber
}
}
}
}
case FINALIZE: {
const { networkId, hash, receipt } = payload
if (safeAccess(state, [networkId, hash]) === null) {
throw Error('Attempted to finalize non-existent transaction.')
}
return {
...state,
[networkId]: {
...(safeAccess(state, [networkId]) || {}),
[hash]: {
...(safeAccess(state, [networkId, hash]) || {}),
[RECEIPT]: receipt
}
}
}
}
default: {
throw Error(`Unexpected action type in TransactionsContext reducer: '${type}'.`)
}
}
}
export default function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, {})
const add = useCallback((networkId, hash, response) => {
dispatch({ type: ADD, payload: { networkId, hash, response } })
}, [])
const check = useCallback((networkId, hash, blockNumber) => {
dispatch({ type: CHECK, payload: { networkId, hash, blockNumber } })
}, [])
const finalize = useCallback((networkId, hash, receipt) => {
dispatch({ type: FINALIZE, payload: { networkId, hash, receipt } })
}, [])
const contextValue = useMemo(() => [state, { add, check, finalize }], [state, add, check, finalize])
return <TransactionsContext.Provider value={contextValue}>{children}</TransactionsContext.Provider>
}
export function Updater() {
const { networkId, library } = useWeb3Context()
const globalBlockNumber = useBlockNumber()
const [state, { check, finalize }] = useTransactionsContext()
const allTransactions = safeAccess(state, [networkId]) || {}
useEffect(() => {
if ((networkId || networkId === 0) && library) {
let stale = false
Object.keys(allTransactions)
.filter(
hash => !allTransactions[hash][RECEIPT] && allTransactions[hash][BLOCK_NUMBER_CHECKED] !== globalBlockNumber
)
.forEach(hash => {
library
.getTransactionReceipt(hash)
.then(receipt => {
if (!stale) {
if (!receipt) {
check(networkId, hash, globalBlockNumber)
} else {
finalize(networkId, hash, receipt)
}
}
})
.catch(() => {
check(networkId, hash, globalBlockNumber)
})
})
return () => {
stale = true
}
}
}, [networkId, library, allTransactions, globalBlockNumber, check, finalize])
return null
}
export function useTransactionAdder() {
const { networkId } = useWeb3Context()
const [, { add }] = useTransactionsContext()
return useCallback(
response => {
if (!(networkId || networkId === 0)) {
throw Error(`Invalid networkId '${networkId}`)
}
const hash = safeAccess(response, ['hash'])
if (!hash) {
throw Error('No transaction hash found.')
}
add(networkId, hash, response)
},
[networkId, add]
)
}
export function useAllTransactions() {
const { networkId } = useWeb3Context()
const [state] = useTransactionsContext()
return safeAccess(state, [networkId]) || {}
}
export function usePendingApproval(tokenAddress) {
const allTransactions = useAllTransactions()
return (
Object.keys(allTransactions).filter(hash => {
if (allTransactions[hash][RECEIPT]) {
return false
} else if (!allTransactions[hash][RESPONSE]) {
return false
} else if (allTransactions[hash][RESPONSE].to !== tokenAddress) {
return false
} else if (
allTransactions[hash][RESPONSE].data.substring(0, 10) !==
ethers.utils.id('approve(address,uint256)').substring(0, 10)
) {
return false
} else {
return true
}
}).length >= 1
)
}
const RINKEBY = {
factoryAddress: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36',
exchangeAddresses: {
addresses: [
['BAT', '0x9B913956036a3462330B0642B20D3879ce68b450'],
['DAI', '0x77dB9C915809e7BE439D2AB21032B1b8B58F6891'],
['MKR', '0x93bB63aFe1E0180d0eF100D774B473034fd60C36'],
['OMG', '0x26C226EBb6104676E593F8A070aD6f25cDa60F8D']
// ['ZRX','0xaBD44a1D1b9Fb0F39fE1D1ee6b1e2a14916D067D'],
],
fromToken: {
'0xDA5B056Cfb861282B4b59d29c9B395bcC238D29B': '0x9B913956036a3462330B0642B20D3879ce68b450',
'0x2448eE2641d78CC42D7AD76498917359D961A783': '0x77dB9C915809e7BE439D2AB21032B1b8B58F6891',
'0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85': '0x93bB63aFe1E0180d0eF100D774B473034fd60C36',
'0x879884c3C46A24f56089f3bBbe4d5e38dB5788C0': '0x26C226EBb6104676E593F8A070aD6f25cDa60F8D'
// '0xF22e3F33768354c9805d046af3C0926f27741B43': '0xaBD44a1D1b9Fb0F39fE1D1ee6b1e2a14916D067D',
}
},
tokenAddresses: {
addresses: [
['BAT', '0xDA5B056Cfb861282B4b59d29c9B395bcC238D29B'],
['DAI', '0x2448eE2641d78CC42D7AD76498917359D961A783'],
['MKR', '0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85'],
['OMG', '0x879884c3C46A24f56089f3bBbe4d5e38dB5788C0']
// ['ZRX','0xF22e3F33768354c9805d046af3C0926f27741B43'],
]
}
}
const MAIN = {
factoryAddress: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95',
exchangeAddresses: {
addresses: [
['ANT', '0x077d52B047735976dfdA76feF74d4d988AC25196'],
['BAT', '0x2E642b8D59B45a1D8c5aEf716A84FF44ea665914'],
['BLT', '0x0E6A53B13688018A3df8C69f99aFB19A3068D04f'],
['BNT', '0x87d80DBD37E551F58680B4217b23aF6a752DA83F'],
['C20', '0xf7b5a4b934658025390ff69db302bc7f2ac4a542'],
['CVC', '0x1C6c712b1F4a7c263B1DBd8F97fb447c945d3b9a'],
['DAI', '0x09cabEC1eAd1c0Ba254B09efb3EE13841712bE14'],
['DGX', '0xb92dE8B30584392Af27726D5ce04Ef3c4e5c9924'],
['FOAM', '0xf79cb3BEA83BD502737586A6E8B133c378FD1fF2'],
['FUN', '0x60a87cC7Fca7E53867facB79DA73181B1bB4238B'],
['GNO', '0xe8e45431b93215566BA923a7E611B7342Ea954DF'],
['GRID', '0x4B17685b330307C751B47f33890c8398dF4Fe407'],
['GUSD', '0xD883264737Ed969d2696eE4B4cAF529c2Fc2A141'],
['KIN', '0xb7520a5F8c832c573d6BD0Df955fC5c9b72400F7'],
['KNC', '0x49c4f9bc14884f6210F28342ceD592A633801a8b'],
['LINK', '0xF173214C720f58E03e194085B1DB28B50aCDeeaD'],
['LOOM', '0x417CB32bc991fBbDCaE230C7c4771CC0D69daA6b'],
['LPT', '0xc4a1C45D5546029Fd57128483aE65b56124BFA6A'],
['MANA', '0xC6581Ce3A005e2801c1e0903281BBd318eC5B5C2'],
['MKR', '0x2C4Bd064b998838076fa341A83d007FC2FA50957'],
['NEXO', '0x069C97DBA948175D10af4b2414969e0B88d44669'],
['NMR', '0x2Bf5A5bA29E60682fC56B2Fcf9cE07Bef4F6196f'],
['PAX', '0xC040d51b07Aea5d94a89Bc21E8078B77366Fc6C7'],
['QCH', '0x755899F0540c3548b99E68C59AdB0f15d2695188'],
['RDN', '0x7D03CeCb36820b4666F45E1b4cA2538724Db271C'],
['REN', '0x43892992B0b102459E895B88601Bb2C76736942c'],
['REP', '0x48B04d2A05B6B604d8d5223Fd1984f191DED51af'],
['RLC', '0xA825CAE02B310E9901b4776806CE25db520c8642'],
['RHOC', '0x394e524b47A3AB3D3327f7fF6629dC378c1494a3'],
['SALT', '0xC0C59cDe851bfcbdddD3377EC10ea54A18Efb937'],
['SNT', '0x1aEC8F11A7E78dC22477e91Ed924Fab46e3A88Fd'],
['SNX', '0x5d8888a212d033cff5f2e0ac24ad91a5495bad62'],
['SPANK', '0x4e395304655F0796bc3bc63709DB72173b9DdF98'],
['SUSD', '0xa1ecdcca26150cf69090280ee2ee32347c238c7b'],
['TKN', '0xb6cFBf322db47D39331E306005DC7E5e6549942B'],
['TUSD', '0x4F30E682D0541eAC91748bd38A648d759261b8f3'],
['USDC', '0x97deC872013f6B5fB443861090ad931542878126'],
['VERI', '0x17e5BF07D696eaf0d14caA4B44ff8A1E17B34de3'],
['WBTC', '0x4d2f5cFbA55AE412221182D8475bC85799A5644b'],
['WETH', '0xA2881A90Bf33F03E7a3f803765Cd2ED5c8928dFb'],
['XCHF', '0x8dE0d002DC83478f479dC31F76cB0a8aa7CcEa17'],
['ZIL', '0x7dc095A5CF7D6208CC680fA9866F80a53911041a'],
['ZRX', '0xaE76c84C9262Cdb9abc0C2c8888e62Db8E22A0bF']
],
fromToken: {
'0x960b236A07cf122663c4303350609A66A7B288C0': '0x077d52B047735976dfdA76feF74d4d988AC25196',
'0x0D8775F648430679A709E98d2b0Cb6250d2887EF': '0x2E642b8D59B45a1D8c5aEf716A84FF44ea665914',
'0x107c4504cd79C5d2696Ea0030a8dD4e92601B82e': '0x0E6A53B13688018A3df8C69f99aFB19A3068D04f',
'0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C': '0x87d80DBD37E551F58680B4217b23aF6a752DA83F',
'0x26E75307Fc0C021472fEb8F727839531F112f317': '0xf7b5a4b934658025390ff69db302bc7f2ac4a542',
'0x41e5560054824eA6B0732E656E3Ad64E20e94E45': '0x1C6c712b1F4a7c263B1DBd8F97fb447c945d3b9a',
'0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359': '0x09cabEC1eAd1c0Ba254B09efb3EE13841712bE14',
'0x4f3AfEC4E5a3F2A6a1A411DEF7D7dFe50eE057bF': '0xb92dE8B30584392Af27726D5ce04Ef3c4e5c9924',
'0x4946Fcea7C692606e8908002e55A582af44AC121': '0xf79cb3BEA83BD502737586A6E8B133c378FD1fF2',
'0x419D0d8BdD9aF5e606Ae2232ed285Aff190E711b': '0x60a87cC7Fca7E53867facB79DA73181B1bB4238B',
'0x6810e776880C02933D47DB1b9fc05908e5386b96': '0xe8e45431b93215566BA923a7E611B7342Ea954DF',
'0x12B19D3e2ccc14Da04FAe33e63652ce469b3F2FD': '0x4B17685b330307C751B47f33890c8398dF4Fe407',
'0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd': '0xD883264737Ed969d2696eE4B4cAF529c2Fc2A141',
'0x818Fc6C2Ec5986bc6E2CBf00939d90556aB12ce5': '0xb7520a5F8c832c573d6BD0Df955fC5c9b72400F7',
'0xdd974D5C2e2928deA5F71b9825b8b646686BD200': '0x49c4f9bc14884f6210F28342ceD592A633801a8b',
'0x514910771AF9Ca656af840dff83E8264EcF986CA': '0xF173214C720f58E03e194085B1DB28B50aCDeeaD',
'0xA4e8C3Ec456107eA67d3075bF9e3DF3A75823DB0': '0x417CB32bc991fBbDCaE230C7c4771CC0D69daA6b',
'0x58b6A8A3302369DAEc383334672404Ee733aB239': '0xc4a1C45D5546029Fd57128483aE65b56124BFA6A',
'0x0F5D2fB29fb7d3CFeE444a200298f468908cC942': '0xC6581Ce3A005e2801c1e0903281BBd318eC5B5C2',
'0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2': '0x2C4Bd064b998838076fa341A83d007FC2FA50957',
'0xB62132e35a6c13ee1EE0f84dC5d40bad8d815206': '0x069C97DBA948175D10af4b2414969e0B88d44669',
'0x1776e1F26f98b1A5dF9cD347953a26dd3Cb46671': '0x2Bf5A5bA29E60682fC56B2Fcf9cE07Bef4F6196f',
'0x8E870D67F660D95d5be530380D0eC0bd388289E1': '0xC040d51b07Aea5d94a89Bc21E8078B77366Fc6C7',
'0x687BfC3E73f6af55F0CccA8450114D107E781a0e': '0x755899F0540c3548b99E68C59AdB0f15d2695188',
'0x255Aa6DF07540Cb5d3d297f0D0D4D84cb52bc8e6': '0x7D03CeCb36820b4666F45E1b4cA2538724Db271C',
'0x408e41876cCCDC0F92210600ef50372656052a38': '0x43892992B0b102459E895B88601Bb2C76736942c',
'0x1985365e9f78359a9B6AD760e32412f4a445E862': '0x48B04d2A05B6B604d8d5223Fd1984f191DED51af',
'0x607F4C5BB672230e8672085532f7e901544a7375': '0xA825CAE02B310E9901b4776806CE25db520c8642',
'0x168296bb09e24A88805CB9c33356536B980D3fC5': '0x394e524b47A3AB3D3327f7fF6629dC378c1494a3',
'0x4156D3342D5c385a87D264F90653733592000581': '0xC0C59cDe851bfcbdddD3377EC10ea54A18Efb937',
'0x744d70FDBE2Ba4CF95131626614a1763DF805B9E': '0x1aEC8F11A7E78dC22477e91Ed924Fab46e3A88Fd',
'0x42d6622deCe394b54999Fbd73D108123806f6a18': '0x4e395304655F0796bc3bc63709DB72173b9DdF98',
'0xaAAf91D9b90dF800Df4F55c205fd6989c977E73a': '0xb6cFBf322db47D39331E306005DC7E5e6549942B',
'0x8dd5fbCe2F6a956C3022bA3663759011Dd51e73E': '0x4F30E682D0541eAC91748bd38A648d759261b8f3',
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': '0x97deC872013f6B5fB443861090ad931542878126',
'0x8f3470A7388c05eE4e7AF3d01D8C722b0FF52374': '0x17e5BF07D696eaf0d14caA4B44ff8A1E17B34de3',
'0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599': '0x4d2f5cFbA55AE412221182D8475bC85799A5644b',
'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2': '0xA2881A90Bf33F03E7a3f803765Cd2ED5c8928dFb',
'0xB4272071eCAdd69d933AdcD19cA99fe80664fc08': '0x8dE0d002DC83478f479dC31F76cB0a8aa7CcEa17',
'0x05f4a42e251f2d52b8ed15E9FEdAacFcEF1FAD27': '0x7dc095A5CF7D6208CC680fA9866F80a53911041a',
'0xE41d2489571d322189246DaFA5ebDe1F4699F498': '0xaE76c84C9262Cdb9abc0C2c8888e62Db8E22A0bF',
'0x3772f9716Cf6D7a09edE3587738AA2af5577483a': '0x5d8888a212d033cff5f2e0ac24ad91a5495bad62',
'0x0cbe2df57ca9191b64a7af3baa3f946fa7df2f25': '0xa1ecdcca26150cf69090280ee2ee32347c238c7b'
}
},
tokenAddresses: {
addresses: [
['ANT', '0x960b236A07cf122663c4303350609A66A7B288C0'],
['BAT', '0x0D8775F648430679A709E98d2b0Cb6250d2887EF'],
['BLT', '0x107c4504cd79C5d2696Ea0030a8dD4e92601B82e'],
['BNT', '0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C'],
['C20', '0x26E75307Fc0C021472fEb8F727839531F112f317'],
['CVC', '0x41e5560054824eA6B0732E656E3Ad64E20e94E45'],
['DAI', '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359'],
['DGX', '0x4f3AfEC4E5a3F2A6a1A411DEF7D7dFe50eE057bF'],
['FOAM', '0x4946Fcea7C692606e8908002e55A582af44AC121'],
['FUN', '0x419D0d8BdD9aF5e606Ae2232ed285Aff190E711b'],
['GNO', '0x6810e776880C02933D47DB1b9fc05908e5386b96'],
['GRID', '0x12B19D3e2ccc14Da04FAe33e63652ce469b3F2FD'],
['GUSD', '0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd'],
['KIN', '0x818Fc6C2Ec5986bc6E2CBf00939d90556aB12ce5'],
['KNC', '0xdd974D5C2e2928deA5F71b9825b8b646686BD200'],
['LINK', '0x514910771AF9Ca656af840dff83E8264EcF986CA'],
['LOOM', '0xA4e8C3Ec456107eA67d3075bF9e3DF3A75823DB0'],
['LPT', '0x58b6A8A3302369DAEc383334672404Ee733aB239'],
['MANA', '0x0F5D2fB29fb7d3CFeE444a200298f468908cC942'],
['MKR', '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2'],
['NEXO', '0xB62132e35a6c13ee1EE0f84dC5d40bad8d815206'],
['NMR', '0x1776e1F26f98b1A5dF9cD347953a26dd3Cb46671'],
['PAX', '0x8E870D67F660D95d5be530380D0eC0bd388289E1'],
['QCH', '0x687BfC3E73f6af55F0CccA8450114D107E781a0e'],
['RDN', '0x255Aa6DF07540Cb5d3d297f0D0D4D84cb52bc8e6'],
['REN', '0x408e41876cCCDC0F92210600ef50372656052a38'],
['REP', '0x1985365e9f78359a9B6AD760e32412f4a445E862'],
['RLC', '0x607F4C5BB672230e8672085532f7e901544a7375'],
['RHOC', '0x168296bb09e24A88805CB9c33356536B980D3fC5'],
['SALT', '0x4156D3342D5c385a87D264F90653733592000581'],
['SNT', '0x744d70FDBE2Ba4CF95131626614a1763DF805B9E'],
['SNX', '0x3772f9716Cf6D7a09edE3587738AA2af5577483a'],
['SPANK', '0x42d6622deCe394b54999Fbd73D108123806f6a18'],
['SUSD', '0x0cbe2df57ca9191b64a7af3baa3f946fa7df2f25'],
['TKN', '0xaAAf91D9b90dF800Df4F55c205fd6989c977E73a'],
['TUSD', '0x8dd5fbCe2F6a956C3022bA3663759011Dd51e73E'],
['USDC', '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'],
['VERI', '0x8f3470A7388c05eE4e7AF3d01D8C722b0FF52374'],
['WBTC', '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599'],
['WETH', '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'],
['XCHF', '0xB4272071eCAdd69d933AdcD19cA99fe80664fc08'],
['ZIL', '0x05f4a42e251f2d52b8ed15E9FEdAacFcEF1FAD27'],
['ZRX', '0xE41d2489571d322189246DaFA5ebDe1F4699F498']
]
}
}
const SET_ADDRESSES = 'app/addresses/setAddresses'
const ADD_EXCHANGE = 'app/addresses/addExchange'
const initialState = RINKEBY
export const addExchange = ({ label, exchangeAddress, tokenAddress }) => (dispatch, getState) => {
const {
addresses: { tokenAddresses, exchangeAddresses }
} = getState()
if (tokenAddresses.addresses.filter(([symbol]) => symbol === label).length) {
return
}
if (exchangeAddresses.fromToken[tokenAddresses]) {
return
}
dispatch({
type: ADD_EXCHANGE,
payload: {
label,
exchangeAddress,
tokenAddress
}
})
}
export const setAddresses = networkId => {
switch (networkId) {
// Main Net
case 1:
case '1':
return {
type: SET_ADDRESSES,
payload: MAIN
}
// Rinkeby
case 4:
case '4':
return {
type: SET_ADDRESSES,
payload: RINKEBY
}
default:
return {
type: SET_ADDRESSES,
payload: RINKEBY
}
}
}
export default (state = initialState, { type, payload }) => {
switch (type) {
case SET_ADDRESSES:
return payload
case ADD_EXCHANGE:
return handleAddExchange(state, { payload })
default:
return state
}
}
function handleAddExchange(state, { payload }) {
const { label, tokenAddress, exchangeAddress } = payload
if (!label || !tokenAddress || !exchangeAddress) {
return state
}
return {
...state,
exchangeAddresses: {
...state.exchangeAddresses,
addresses: [...state.exchangeAddresses.addresses, [label, exchangeAddress]],
fromToken: {
...state.exchangeAddresses.fromToken,
[tokenAddress]: exchangeAddress
}
},
tokenAddresses: {
...state.tokenAddresses,
addresses: [...state.tokenAddresses.addresses, [label, tokenAddress]]
}
}
}
const DISMISS_BETA_MESSAGE = 'app/app/dismissBetaMessage'
const initialState = {
showBetaMessage: true
}
export const dismissBetaMessage = () => ({ type: DISMISS_BETA_MESSAGE })
export default function appReducer(state = initialState, { type, payload }) {
switch (type) {
case DISMISS_BETA_MESSAGE:
return { ...state, showBetaMessage: false }
default:
return state
}
}
import { combineReducers } from 'redux'
import addresses from './addresses'
import app from './app'
import pending from './pending'
import web3connect from './web3connect'
export default combineReducers({
app,
addresses,
pending,
web3connect
})
const ADD_APPROVAL_TX = 'app/send/addApprovalTx'
const getInitialState = () => {
return {
approvals: {}
}
}
export const addApprovalTx = ({ tokenAddress, txId }) => ({
type: ADD_APPROVAL_TX,
payload: { tokenAddress, txId }
})
export default function sendReducer(state = getInitialState(), { type, payload }) {
switch (type) {
case ADD_APPROVAL_TX:
return {
approvals: {
...state.approvals,
[payload.tokenAddress]: payload.txId
}
}
default:
return state
}
}
import { BigNumber as BN } from 'bignumber.js'
import Web3 from 'web3'
import ERC20_ABI from '../abi/erc20'
import ERC20_WITH_BYTES_ABI from '../abi/erc20_symbol_bytes32'
export const INITIALIZE = 'web3connect/initialize'
export const INITIALIZE_WEB3 = 'web3connect/initializeWeb3'
export const UPDATE_ACCOUNT = 'web3connect/updateAccount'
export const WATCH_ETH_BALANCE = 'web3connect/watchEthBalance'
export const WATCH_TOKEN_BALANCE = 'web3connect/watchTokenBalance'
export const UPDATE_ETH_BALANCE = 'web3connect/updateEthBalance'
export const UPDATE_TOKEN_BALANCE = 'web3connect/updateTokenBalance'
export const WATCH_APPROVALS = 'web3connect/watchApprovals'
export const UPDATE_APPROVALS = 'web3connect/updateApprovals'
export const ADD_CONTRACT = 'web3connect/addContract'
export const UPDATE_NETWORK_ID = 'web3connect/updateNetworkId'
export const ADD_PENDING_TX = 'web3connect/addPendingTx'
export const REMOVE_PENDING_TX = 'web3connect/removePendingTx'
export const ADD_CONFIRMED_TX = 'web3connect/addConfirmedTx'
const initialState = {
web3: null,
networkId: 0,
initialized: false,
account: null,
balances: {
ethereum: {}
},
approvals: {
'0x0': {
TOKEN_OWNER: {
SPENDER: {}
}
}
},
transactions: {
pending: [],
confirmed: []
},
watched: {
balances: {
ethereum: []
},
approvals: {}
},
contracts: {}
}
// selectors
export const selectors = () => (dispatch, getState) => {
const state = getState().web3connect
const getTokenBalance = (tokenAddress, address) => {
const tokenBalances = state.balances[tokenAddress] || {}
const balance = tokenBalances[address]
if (!balance) {
dispatch(watchBalance({ balanceOf: address, tokenAddress }))
return Balance(0)
}
return balance
}
const getBalance = (address, tokenAddress) => {
if (!tokenAddress || tokenAddress === 'ETH') {
const balance = state.balances.ethereum[address]
if (!balance) {
dispatch(watchBalance({ balanceOf: address }))
return Balance(0, 'ETH')
}
return balance
} else if (tokenAddress) {
return getTokenBalance(tokenAddress, address)
}
return Balance(NaN)
}
const getApprovals = (tokenAddress, tokenOwner, spender) => {
const token = state.approvals[tokenAddress] || {}
const owner = token[tokenOwner] || {}
if (!owner[spender]) {
dispatch(watchApprovals({ tokenAddress, tokenOwner, spender }))
return Balance(0)
}
return owner[spender]
}
return {
getBalance,
getTokenBalance,
getApprovals
}
}
const Balance = (value, label = '', decimals = 0) => ({
value: BN(value),
label: label.toUpperCase(),
decimals: +decimals
})
export const initialize = () => async dispatch => {
await dispatch({ type: INITIALIZE })
}
export const updateNetwork = (passedProvider, networkId) => async dispatch => {
const web3 = new Web3(passedProvider)
const dispatches = [
dispatch({ type: INITIALIZE_WEB3, payload: web3 }),
dispatch({ type: UPDATE_NETWORK_ID, payload: networkId })
]
await Promise.all(dispatches)
}
export const updateAccount = account => async dispatch => {
if (account !== null) {
const dispatches = [
dispatch({ type: UPDATE_ACCOUNT, payload: account }),
dispatch(watchBalance({ balanceOf: account }))
]
await Promise.all(dispatches)
}
}
export const watchBalance = ({ balanceOf, tokenAddress }) => (dispatch, getState) => {
if (!balanceOf) {
return
}
const { web3connect } = getState()
const { watched } = web3connect
if (!tokenAddress) {
if (watched.balances.ethereum.includes(balanceOf)) {
return
}
dispatch({
type: WATCH_ETH_BALANCE,
payload: balanceOf
})
setTimeout(() => dispatch(sync()), 0)
} else if (tokenAddress) {
if (watched.balances[tokenAddress] && watched.balances[tokenAddress].includes(balanceOf)) {
return
}
dispatch({
type: WATCH_TOKEN_BALANCE,
payload: {
tokenAddress,
balanceOf
}
})
setTimeout(() => dispatch(sync()), 0)
}
}
export const watchApprovals = ({ tokenAddress, tokenOwner, spender }) => (dispatch, getState) => {
const {
web3connect: { watched }
} = getState()
const token = watched.approvals[tokenAddress] || {}
const owner = token[tokenOwner] || []
if (owner.includes(spender)) {
return
}
return dispatch({
type: WATCH_APPROVALS,
payload: {
tokenAddress,
tokenOwner,
spender
}
})
}
export const addPendingTx = txId => ({
type: ADD_PENDING_TX,
payload: txId
})
export const updateApprovals = ({ tokenAddress, tokenOwner, spender, balance }) => ({
type: UPDATE_APPROVALS,
payload: {
tokenAddress,
tokenOwner,
spender,
balance
}
})
export const sync = () => async (dispatch, getState) => {
const { getBalance, getApprovals } = dispatch(selectors())
const {
web3,
watched,
contracts,
transactions: { pending }
} = getState().web3connect
// Sync Ethereum Balances
watched.balances.ethereum.forEach(async address => {
const balance = await web3.eth.getBalance(address)
const { value } = getBalance(address)
if (value.isEqualTo(BN(balance))) {
return
}
dispatch({
type: UPDATE_ETH_BALANCE,
payload: {
balance: Balance(balance, 'ETH', 18),
balanceOf: address
}
})
})
// Sync Token Balances
Object.keys(watched.balances).forEach(tokenAddress => {
if (tokenAddress === 'ethereum') {
return
}
const contract = contracts[tokenAddress] || new web3.eth.Contract(ERC20_ABI, tokenAddress)
if (!contracts[tokenAddress]) {
dispatch({
type: ADD_CONTRACT,
payload: {
address: tokenAddress,
contract: contract
}
})
}
const watchlist = watched.balances[tokenAddress] || []
watchlist.forEach(async address => {
const tokenBalance = getBalance(address, tokenAddress)
const balance = await contract.methods.balanceOf(address).call()
const decimals = tokenBalance.decimals || (await contract.methods.decimals().call())
let symbol = tokenBalance.symbol
try {
symbol =
symbol ||
(await contract.methods
.symbol()
.call()
.catch())
} catch (e) {
try {
const contractBytes32 = new web3.eth.Contract(ERC20_WITH_BYTES_ABI, tokenAddress)
symbol =
symbol ||
web3.utils.hexToString(
await contractBytes32.methods
.symbol()
.call()
.catch()
)
} catch (err) {}
}
if (tokenBalance.value.isEqualTo(BN(balance)) && tokenBalance.label && tokenBalance.decimals) {
return
}
dispatch({
type: UPDATE_TOKEN_BALANCE,
payload: {
tokenAddress,
balanceOf: address,
balance: Balance(balance, symbol, decimals)
}
})
})
})
// Update Approvals
Object.entries(watched.approvals).forEach(([tokenAddress, token]) => {
const contract = contracts[tokenAddress] || new web3.eth.Contract(ERC20_ABI, tokenAddress)
Object.entries(token).forEach(([tokenOwnerAddress, tokenOwner]) => {
tokenOwner.forEach(async spenderAddress => {
if (tokenOwnerAddress !== null && tokenOwnerAddress !== 'null') {
const approvalBalance = getApprovals(tokenAddress, tokenOwnerAddress, spenderAddress)
const balance = await contract.methods.allowance(tokenOwnerAddress, spenderAddress).call()
const decimals = approvalBalance.decimals || (await contract.methods.decimals().call())
let symbol = approvalBalance.label
try {
symbol = symbol || (await contract.methods.symbol().call())
} catch (e) {
try {
const contractBytes32 = new web3.eth.Contract(ERC20_WITH_BYTES_ABI, tokenAddress)
symbol = symbol || web3.utils.hexToString(await contractBytes32.methods.symbol().call())
} catch (err) {}
}
if (approvalBalance.label && approvalBalance.value.isEqualTo(BN(balance))) {
return
}
dispatch(
updateApprovals({
tokenAddress,
tokenOwner: tokenOwnerAddress,
spender: spenderAddress,
balance: Balance(balance, symbol, decimals)
})
)
}
})
})
})
pending.forEach(async txId => {
try {
const data = (await web3.eth.getTransactionReceipt(txId)) || {}
// If data is an empty obj, then it's still pending.
if (!('status' in data)) {
return
}
dispatch({
type: REMOVE_PENDING_TX,
payload: txId
})
if (data.status) {
dispatch({
type: ADD_CONFIRMED_TX,
payload: txId
})
} else {
// TODO: dispatch ADD_REJECTED_TX
}
} catch (err) {
dispatch({
type: REMOVE_PENDING_TX,
payload: txId
})
// TODO: dispatch ADD_REJECTED_TX
}
})
}
export const startWatching = () => async dispatch => {
await dispatch(sync())
setTimeout(() => dispatch(startWatching()), 5000)
}
export default function web3connectReducer(state = initialState, { type, payload }) {
switch (type) {
case INITIALIZE_WEB3:
return {
...state,
web3: payload
}
case INITIALIZE:
return {
...state,
initialized: true
}
case UPDATE_NETWORK_ID:
return { ...state, networkId: payload }
case UPDATE_ACCOUNT:
return {
...state,
account: payload
}
case WATCH_ETH_BALANCE:
return {
...state,
watched: {
...state.watched,
balances: {
...state.watched.balances,
ethereum: [...state.watched.balances.ethereum, payload]
}
}
}
case WATCH_TOKEN_BALANCE:
const { watched } = state
const { balances } = watched
const watchlist = balances[payload.tokenAddress] || []
return {
...state,
watched: {
...watched,
balances: {
...balances,
[payload.tokenAddress]: [...watchlist, payload.balanceOf]
}
}
}
case UPDATE_ETH_BALANCE:
return {
...state,
balances: {
...state.balances,
ethereum: {
...state.balances.ethereum,
[payload.balanceOf]: payload.balance
}
}
}
case UPDATE_TOKEN_BALANCE:
const tokenBalances = state.balances[payload.tokenAddress] || {}
return {
...state,
balances: {
...state.balances,
[payload.tokenAddress]: {
...tokenBalances,
[payload.balanceOf]: payload.balance
}
}
}
case ADD_CONTRACT:
return {
...state,
contracts: {
...state.contracts,
[payload.address]: payload.contract
}
}
case WATCH_APPROVALS:
const token = state.watched.approvals[payload.tokenAddress] || {}
const tokenOwner = token[payload.tokenOwner] || []
return {
...state,
watched: {
...state.watched,
approvals: {
...state.watched.approvals,
[payload.tokenAddress]: {
...token,
[payload.tokenOwner]: [...tokenOwner, payload.spender]
}
}
}
}
case UPDATE_APPROVALS:
const erc20 = state.approvals[payload.tokenAddress] || {}
const erc20Owner = erc20[payload.tokenOwner] || {}
return {
...state,
approvals: {
...state.approvals,
[payload.tokenAddress]: {
...erc20,
[payload.tokenOwner]: {
...erc20Owner,
[payload.spender]: payload.balance
}
}
}
}
case ADD_PENDING_TX:
return {
...state,
transactions: {
...state.transactions,
pending: [...state.transactions.pending, payload]
}
}
case REMOVE_PENDING_TX:
return {
...state,
transactions: {
...state.transactions,
pending: state.transactions.pending.filter(id => id !== payload)
}
}
case ADD_CONFIRMED_TX:
if (state.transactions.confirmed.includes(payload)) {
return state
}
return {
...state,
transactions: {
...state.transactions,
confirmed: [...state.transactions.confirmed, payload]
}
}
default:
return state
}
}
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
export function retry(func, retryCount = 5) {
return new Promise((resolve, reject) => {
func().then(
(...args) => {
resolve(...args)
},
() => {
if (retryCount === 0) {
return reject()
}
setTimeout(() => retry(func, retryCount - 1).then(resolve, reject), 50)
}
)
})
}
export default function promisify(web3, methodName, ...args) {
return new Promise((resolve, reject) => {
if (!web3) {
reject(new Error('No Web3 object'))
return
}
const method = web3.eth[methodName]
if (!method) {
reject(new Error(`Cannot find web3.eth.${methodName}`))
return
}
method(...args, (error, data) => {
if (error) {
reject(error)
return
}
resolve(data)
})
})
}
import promisify from './web3-promisfy'
export function getBlockDeadline(web3, deadline) {
return new Promise(async (resolve, reject) => {
const blockNumber = await promisify(web3, 'getBlockNumber')
if (!blockNumber && blockNumber !== 0) {
return reject()
}
const block = await promisify(web3, 'getBlock', blockNumber)
if (!block) {
return reject()
}
resolve(block.timestamp + deadline)
})
}
import { useMemo, useEffect } from 'react'
import { useState, 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/useDebounce/
export function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
// Update debounced value after delay
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
// Cancel the timeout if value changes (also on delay change or unmount)
// This is how we prevent debounced value from updating if value is changed ...
// .. within the delay period. Timeout gets cleared and restarted.
return () => {
clearTimeout(handler)
}
}, [value, delay])
return debouncedValue
}
export function useSignerOrProvider() {
const { library, account } = useWeb3Context()
// 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]
)
return useMemo(() => getSignerOrProvider(library, account), [library, account])
useEffect(() => {
window.addEventListener('keydown', downHandler)
return () => {
window.removeEventListener('keydown', downHandler)
}
}, [downHandler])
}
// 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])
}
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import ReactGA from 'react-ga'
import Web3Provider, { Connectors } from 'web3-react'
import './i18n'
import ThemeProvider, { GlobalStyle } from './theme'
import ApplicationContextProvider, { Updater as ApplicationContextUpdater } from './contexts/Application'
import TransactionContextProvider, { Updater as TransactionContextUpdater } from './contexts/Transactions'
import TokensContextProvider from './contexts/Tokens'
import BalancesContextProvider from './contexts/Balances'
import AllowancesContextProvider from './contexts/Allowances'
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,11 +28,40 @@ const Infura = new NetworkOnlyConnector({
})
const connectors = { Injected, Infura }
function ContextProviders({ children }) {
return (
<ApplicationContextProvider>
<TransactionContextProvider>
<TokensContextProvider>
<BalancesContextProvider>
<AllowancesContextProvider>{children}</AllowancesContextProvider>
</BalancesContextProvider>
</TokensContextProvider>
</TransactionContextProvider>
</ApplicationContextProvider>
)
}
function Updaters() {
return (
<>
<ApplicationContextUpdater />
<TransactionContextUpdater />
</>
)
}
ReactDOM.render(
<Provider store={store}>
<Web3Provider connectors={connectors} libraryName="ethers.js">
<App />
</Web3Provider>
</Provider>,
<ThemeProvider>
<>
<GlobalStyle />
<Web3Provider connectors={connectors} libraryName="ethers.js">
<ContextProviders>
<Updaters />
<App />
</ContextProviders>
</Web3Provider>
</>
</ThemeProvider>,
document.getElementById('root')
)
@import url('https://rsms.me/inter/inter.css');
@import './variables.scss';
html,
body {
margin: 0;
padding: 0;
font-family: 'Inter UI', sans-serif;
font-size: 16px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
}
#root {
position: relative;
display: flex;
flex-flow: column nowrap;
height: 100vh;
width: 100vw;
overflow-x: hidden;
overflow-y: auto;
background-color: $white;
z-index: 100;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
@media only screen and (min-width: 768px) {
justify-content: center;
align-items: center;
}
}
#modal-root {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 200;
}
.loader {
border: 1px solid transparent; /* Light grey */
border-top: 1px solid $royal-blue; /* Blue */
border-radius: 50%;
width: 0.75rem;
height: 0.75rem;
margin-right: 0.25rem;
animation: spin 1s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
import React, { useState, useEffect } from 'react'
import { connect } from 'react-redux'
import React, { useEffect } from 'react'
import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom'
import { useWeb3Context, Connectors } from 'web3-react'
import NavigationTabs from '../components/NavigationTabs'
import { updateNetwork, updateAccount, initialize, startWatching } from '../ducks/web3connect'
import { setAddresses } from '../ducks/addresses'
import Header from '../components/Header'
import Swap from './Swap'
import Send from './Send'
......@@ -15,60 +12,34 @@ import './App.scss'
const { Connector, InjectedConnector } = Connectors
function App({ initialized, setAddresses, updateNetwork, updateAccount, initialize, startWatching }) {
const context = useWeb3Context()
export default function App() {
const { setConnector, setError, error, active, connectorName } = useWeb3Context()
// start web3-react on page-load
useEffect(() => {
context.setConnector('Injected', { suppressAndThrowErrors: true }).catch(error => {
setConnector('Injected', { suppressAndThrowErrors: true }).catch(error => {
if (error.code === Connector.errorCodes.UNSUPPORTED_NETWORK) {
context.setError(error, { connectorName: 'Injected' })
setError(error, { connectorName: 'Injected' })
} else {
context.setConnector('Infura')
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')
if (error && error.code === InjectedConnector.errorCodes.UNLOCK_REQUIRED) {
setConnector('Infura')
}
}, [context.error, context.connectorName])
// initialize redux network
const [reduxNetworkInitialized, setReduxNetworkInitialized] = useState(false)
useEffect(() => {
if (context.active) {
setAddresses(context.networkId)
updateNetwork(context.library._web3Provider, context.networkId)
setReduxNetworkInitialized(true)
}
}, [context.active, context.networkId])
// initialize redux account
const [reduxAccountInitialized, setReduxAccountInitialized] = useState(false)
useEffect(() => {
if (context.active) {
updateAccount(context.account)
setReduxAccountInitialized(true)
}
}, [context.active, context.account])
// initialize redux
useEffect(() => {
if (reduxNetworkInitialized && reduxAccountInitialized) {
initialize().then(startWatching)
}
}, [reduxNetworkInitialized, reduxAccountInitialized])
}, [error, connectorName, setConnector])
// active state
if (initialized || context.error) {
if (active || error) {
return (
<div id="app-container">
<Header />
{/* this is an intermediate state before infura is set */}
{initialized && (!context.error || context.error.code === InjectedConnector.errorCodes.UNLOCK_REQUIRED) && (
{(!error || error.code === InjectedConnector.errorCodes.UNLOCK_REQUIRED) && (
<div className="app__wrapper">
<div className="body">
<div className="body__content">
......@@ -100,16 +71,3 @@ function App({ initialized, setAddresses, updateNetwork, updateAccount, initiali
// loading state
return null
}
export default connect(
state => ({
initialized: state.web3connect.initialized
}),
dispatch => ({
setAddresses: networkId => dispatch(setAddresses(networkId)),
updateNetwork: (passedProvider, networkId) => dispatch(updateNetwork(passedProvider, networkId)),
updateAccount: account => dispatch(updateAccount(account)),
initialize: () => dispatch(initialize()),
startWatching: () => dispatch(startWatching())
})
)(App)
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import Web3Provider, { Connectors } from 'web3-react'
import App from './App'
import store from '../store'
// TODO, fix this hacky workaround
const { NetworkOnlyConnector } = Connectors
......@@ -16,11 +14,9 @@ export const connectors = { Injected }
it('renders without crashing', () => {
const div = document.createElement('div')
ReactDOM.render(
<Provider store={store}>
<Web3Provider connectors={connectors} libraryName="ethers.js">
<App />
</Web3Provider>
</Provider>,
<Web3Provider connectors={connectors} libraryName="ethers.js">
<App />
</Web3Provider>,
div
)
ReactDOM.unmountComponentAtNode(div)
......
import React, { Component } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import React, { useReducer, useState, useCallback, useEffect, useMemo } from 'react'
import classnames from 'classnames'
import { withTranslation, useTranslation } from 'react-i18next'
import { useTranslation } from 'react-i18next'
import { useWeb3Context } from 'web3-react'
import { ethers } from 'ethers'
import ReactGA from 'react-ga'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import OversizedPanel from '../../components/OversizedPanel'
import ContextualInfo from '../../components/ContextualInfo'
import { selectors, addPendingTx } from '../../ducks/web3connect'
import PlusBlue from '../../assets/images/plus-blue.svg'
import PlusGrey from '../../assets/images/plus-grey.svg'
import { getBlockDeadline } from '../../helpers/web3-utils'
import { retry } from '../../helpers/promise-utils'
import { BigNumber as BN } from 'bignumber.js'
import EXCHANGE_ABI from '../../abi/exchange'
import { useExchangeContract } from '../../hooks'
import { amountFormatter, calculateGasMargin } from '../../utils'
import { useTransactionAdder } from '../../contexts/Transactions'
import { useTokenDetails } from '../../contexts/Tokens'
import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances'
import { useAddressAllowance } from '../../contexts/Allowances'
import './pool.scss'
const INPUT = 0
const OUTPUT = 1
class AddLiquidity extends Component {
static propTypes = {
account: PropTypes.string,
selectors: PropTypes.func.isRequired,
balances: PropTypes.object.isRequired,
exchangeAddresses: PropTypes.shape({
fromToken: PropTypes.object.isRequired
}).isRequired
}
state = {
inputValue: '',
outputValue: '',
inputCurrency: 'ETH',
outputCurrency: '',
lastEditedField: '',
totalSupply: BN(0)
}
reset = () => {
this.setState({
inputValue: '',
outputValue: '',
lastEditedField: ''
})
}
shouldComponentUpdate(nextProps, nextState) {
const { t, account, exchangeAddresses, balances, web3 } = this.props
const { inputValue, outputValue, inputCurrency, outputCurrency, lastEditedField } = this.state
// denominated in bips
const ALLOWED_SLIPPAGE = ethers.utils.bigNumberify(200)
return (
t !== nextProps.t ||
account !== nextProps.account ||
exchangeAddresses !== nextProps.exchangeAddresses ||
web3 !== nextProps.web3 ||
balances !== nextProps.balances ||
inputValue !== nextState.inputValue ||
outputValue !== nextState.outputValue ||
inputCurrency !== nextState.inputCurrency ||
outputCurrency !== nextState.outputCurrency ||
lastEditedField !== nextState.lastEditedField
)
}
// denominated in seconds
const DEADLINE_FROM_NOW = 60 * 15
componentWillReceiveProps() {
this.recalcForm()
}
// denominated in bips
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
recalcForm = async () => {
const { outputCurrency, inputValue, outputValue, lastEditedField, totalSupply: oldTotalSupply } = this.state
const {
exchangeAddresses: { fromToken },
web3
} = this.props
const exchangeAddress = fromToken[outputCurrency]
const exchangeRate = this.getExchangeRate()
const append = {}
if (!outputCurrency || this.isNewExchange() || !web3) {
return
function calculateSlippageBounds(value) {
if (value) {
const offset = value.mul(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
}
} else {
return {}
}
}
const exchange = new web3.eth.Contract(EXCHANGE_ABI, exchangeAddress)
const totalSupply = await exchange.methods.totalSupply().call()
if (!oldTotalSupply.isEqualTo(BN(totalSupply))) {
append.totalSupply = BN(totalSupply)
}
const initialAddLiquidityState = {
inputValue: '',
outputValue: '',
lastEditedField: INPUT,
outputCurrency: ''
}
if (lastEditedField === INPUT) {
const newOutputValue = exchangeRate.multipliedBy(inputValue).toFixed(7)
if (newOutputValue !== outputValue) {
append.outputValue = newOutputValue
function addLiquidityStateReducer(state, action) {
switch (action.type) {
case 'SELECT_CURRENCY': {
return {
...state,
outputCurrency: action.payload
}
}
if (lastEditedField === OUTPUT) {
const newInputValue = BN(outputValue)
.dividedBy(exchangeRate)
.toFixed(7)
if (newInputValue !== inputValue) {
append.inputValue = newInputValue
case 'UPDATE_VALUE': {
const { inputValue, outputValue } = state
const { field, value } = action.payload
return {
...state,
inputValue: field === INPUT ? value : inputValue,
outputValue: field === OUTPUT ? value : outputValue,
lastEditedField: field
}
}
this.setState(append)
}
getBalance(currency) {
const { t, selectors, account } = this.props
if (!currency) {
return ''
}
const { value, decimals } = selectors().getBalance(account, currency)
if (!decimals) {
return ''
}
const balanceInput = value.dividedBy(10 ** decimals).toFixed(4)
return t('balance', { balanceInput })
}
isUnapproved() {
const { account, exchangeAddresses, selectors } = this.props
const { outputCurrency, outputValue } = this.state
if (!outputCurrency) {
return false
}
const { value: allowance, label, decimals } = selectors().getApprovals(
outputCurrency,
account,
exchangeAddresses.fromToken[outputCurrency]
)
if (label && allowance.isLessThan(BN(outputValue * 10 ** decimals || 0))) {
return true
}
return false
}
onAddLiquidity = async () => {
const {
account,
web3,
exchangeAddresses: { fromToken },
selectors
} = this.props
const { inputValue, outputValue, outputCurrency } = this.state
const exchange = new web3.eth.Contract(EXCHANGE_ABI, fromToken[outputCurrency])
const ethAmount = BN(inputValue).multipliedBy(10 ** 18)
const { decimals } = selectors().getTokenBalance(outputCurrency, fromToken[outputCurrency])
const tokenAmount = BN(outputValue).multipliedBy(10 ** decimals)
const { value: ethReserve } = selectors().getBalance(fromToken[outputCurrency])
const totalLiquidity = await exchange.methods.totalSupply().call()
const liquidityMinted = BN(totalLiquidity).multipliedBy(ethAmount.dividedBy(ethReserve))
let deadline
try {
deadline = await retry(() => getBlockDeadline(web3, 600))
} catch (e) {
// TODO: Handle error.
return
}
const MAX_LIQUIDITY_SLIPPAGE = 0.025
const minLiquidity = this.isNewExchange() ? BN(0) : liquidityMinted.multipliedBy(1 - MAX_LIQUIDITY_SLIPPAGE)
const maxTokens = this.isNewExchange() ? tokenAmount : tokenAmount.multipliedBy(1 + MAX_LIQUIDITY_SLIPPAGE)
try {
exchange.methods.addLiquidity(minLiquidity.toFixed(0), maxTokens.toFixed(0), deadline).send(
{
from: account,
value: ethAmount.toFixed(0)
},
(err, data) => {
this.reset()
this.props.addPendingTx(data)
if (data) {
ReactGA.event({
category: 'Pool',
action: 'AddLiquidity'
})
}
}
)
} catch (err) {
console.error(err)
}
}
onInputChange = value => {
const { inputCurrency, outputCurrency } = this.state
const exchangeRate = this.getExchangeRate()
let outputValue
if (inputCurrency === 'ETH' && outputCurrency && outputCurrency !== 'ETH') {
outputValue = exchangeRate.multipliedBy(value).toFixed(7)
}
if (outputCurrency === 'ETH' && inputCurrency && inputCurrency !== 'ETH') {
outputValue = BN(value)
.dividedBy(exchangeRate)
.toFixed(7)
}
const append = {
inputValue: value,
lastEditedField: INPUT
}
if (!this.isNewExchange()) {
append.outputValue = outputValue
}
this.setState(append)
}
onOutputChange = value => {
const { inputCurrency, outputCurrency } = this.state
const exchangeRate = this.getExchangeRate()
let inputValue
if (inputCurrency === 'ETH' && outputCurrency && outputCurrency !== 'ETH') {
inputValue = BN(value)
.dividedBy(exchangeRate)
.toFixed(7)
}
if (outputCurrency === 'ETH' && inputCurrency && inputCurrency !== 'ETH') {
inputValue = exchangeRate.multipliedBy(value).toFixed(7)
}
const append = {
outputValue: value,
lastEditedField: INPUT
}
if (!this.isNewExchange()) {
append.inputValue = inputValue
case 'UPDATE_DEPENDENT_VALUE': {
const { inputValue, outputValue } = state
const { field, value } = action.payload
return {
...state,
inputValue: field === INPUT ? value : inputValue,
outputValue: field === OUTPUT ? value : outputValue
}
}
this.setState(append)
}
isNewExchange() {
const {
selectors,
exchangeAddresses: { fromToken }
} = this.props
const { inputCurrency, outputCurrency } = this.state
const eth = [inputCurrency, outputCurrency].filter(currency => currency === 'ETH')[0]
const token = [inputCurrency, outputCurrency].filter(currency => currency !== 'ETH')[0]
if (!eth || !token) {
return false
default: {
return initialAddLiquidityState
}
const { value: tokenValue, decimals } = selectors().getBalance(fromToken[token], token)
const { value: ethValue } = selectors().getBalance(fromToken[token], eth)
return tokenValue.isZero() && ethValue.isZero() && decimals !== 0
}
}
getExchangeRate() {
const {
selectors,
exchangeAddresses: { fromToken }
} = this.props
const { inputCurrency, outputCurrency } = this.state
const eth = [inputCurrency, outputCurrency].filter(currency => currency === 'ETH')[0]
const token = [inputCurrency, outputCurrency].filter(currency => currency !== 'ETH')[0]
if (!eth || !token) {
return
}
const { value: tokenValue, decimals } = selectors().getBalance(fromToken[token], token)
const { value: ethValue } = selectors().getBalance(fromToken[token], eth)
return tokenValue.multipliedBy(10 ** (18 - decimals)).dividedBy(ethValue)
}
validate() {
const { t, selectors, account } = this.props
const { inputValue, outputValue, inputCurrency, outputCurrency } = this.state
let inputError
let outputError
let isValid = true
const inputIsZero = BN(inputValue).isZero()
const outputIsZero = BN(outputValue).isZero()
function getExchangeRate(inputValue, inputDecimals, outputValue, outputDecimals, invert = false) {
try {
if (
!inputValue ||
inputIsZero ||
!outputValue ||
outputIsZero ||
!inputCurrency ||
!outputCurrency ||
this.isUnapproved()
inputValue &&
(inputDecimals || inputDecimals === 0) &&
outputValue &&
(outputDecimals || outputDecimals === 0)
) {
isValid = false
}
const { value: ethValue } = selectors().getBalance(account, inputCurrency)
const { value: tokenValue, decimals } = selectors().getBalance(account, outputCurrency)
if (ethValue.isLessThan(BN(inputValue * 10 ** 18))) {
inputError = t('insufficientBalance')
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 (tokenValue.isLessThan(BN(outputValue * 10 ** decimals))) {
outputError = t('insufficientBalance')
}
function getMarketRate(reserveETH, reserveToken, decimals, invert = false) {
return getExchangeRate(reserveETH, 18, reserveToken, decimals, invert)
}
return {
inputError,
outputError,
isValid: isValid && !inputError && !outputError
export default function AddLiquidity() {
const { t } = useTranslation()
const { library, active, account } = useWeb3Context()
const [addLiquidityState, dispatchAddLiquidityState] = useReducer(addLiquidityStateReducer, initialAddLiquidityState)
const { inputValue, outputValue, lastEditedField, outputCurrency } = addLiquidityState
const inputCurrency = 'ETH'
const [inputValueParsed, setInputValueParsed] = useState()
const [outputValueParsed, setOutputValueParsed] = useState()
const [inputError, setInputError] = useState()
const [outputError, setOutputError] = useState()
const { symbol, decimals, exchangeAddress } = useTokenDetails(outputCurrency)
const exchangeContract = useExchangeContract(exchangeAddress)
const [totalPoolTokens, setTotalPoolTokens] = useState()
const fetchPoolTokens = useCallback(() => {
if (exchangeContract) {
exchangeContract.totalSupply().then(totalSupply => {
setTotalPoolTokens(totalSupply)
})
}
}
renderInfo() {
const t = this.props.t
const blank = (
<div className="pool__summary-panel">
<div className="pool__exchange-rate-wrapper">
<span className="pool__exchange-rate">{t('exchangeRate')}</span>
<span> - </span>
</div>
<div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t('currentPoolSize')}</span>
<span> - </span>
</div>
<div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t('yourPoolShare')}</span>
<span> - </span>
</div>
</div>
)
}, [exchangeContract])
useEffect(() => {
fetchPoolTokens()
library.on('block', fetchPoolTokens)
const {
selectors,
exchangeAddresses: { fromToken },
account
} = this.props
const { getBalance } = selectors()
const { inputCurrency, outputCurrency, inputValue, outputValue, totalSupply } = this.state
const eth = [inputCurrency, outputCurrency].filter(currency => currency === 'ETH')[0]
const token = [inputCurrency, outputCurrency].filter(currency => currency !== 'ETH')[0]
const exchangeAddress = fromToken[token]
if (!eth || !token || !exchangeAddress) {
return blank
return () => {
library.removeListener('block', fetchPoolTokens)
}
}, [fetchPoolTokens, library])
const poolTokenBalance = useAddressBalance(account, exchangeAddress)
const exchangeETHBalance = useAddressBalance(exchangeAddress, 'ETH')
const exchangeTokenBalance = useAddressBalance(exchangeAddress, outputCurrency)
const { reserveETH, reserveToken } = useExchangeReserves(outputCurrency)
const isNewExchange = !!(reserveETH && reserveToken && reserveETH.isZero() && reserveToken.isZero())
// 18 decimals
const poolTokenPercentage =
poolTokenBalance && totalPoolTokens && isNewExchange === false && !totalPoolTokens.isZero()
? poolTokenBalance.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18))).div(totalPoolTokens)
: undefined
const ethShare =
exchangeETHBalance && poolTokenPercentage
? exchangeETHBalance
.mul(poolTokenPercentage)
.div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)))
: undefined
const tokenShare =
exchangeTokenBalance && poolTokenPercentage
? exchangeTokenBalance
.mul(poolTokenPercentage)
.div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)))
: undefined
const liquidityMinted = isNewExchange
? inputValueParsed
: totalPoolTokens && inputValueParsed && exchangeETHBalance && !exchangeETHBalance.isZero()
? totalPoolTokens.mul(inputValueParsed).div(exchangeETHBalance)
: undefined
// user balances
const inputBalance = useAddressBalance(account, inputCurrency)
const outputBalance = useAddressBalance(account, outputCurrency)
const ethPerLiquidityToken =
exchangeETHBalance && totalPoolTokens && isNewExchange === false && !totalPoolTokens.isZero()
? exchangeETHBalance.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18))).div(totalPoolTokens)
: undefined
const tokenPerLiquidityToken =
exchangeTokenBalance && totalPoolTokens && isNewExchange === false && !totalPoolTokens.isZero()
? exchangeTokenBalance.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18))).div(totalPoolTokens)
: undefined
const outputValueMax = outputValueParsed && calculateSlippageBounds(outputValueParsed).maximum
const liquidityTokensMin = liquidityMinted && calculateSlippageBounds(liquidityMinted).minimum
const marketRate = useMemo(() => {
return getMarketRate(reserveETH, reserveToken, decimals)
}, [reserveETH, reserveToken, decimals])
const marketRateInverted = useMemo(() => {
return getMarketRate(reserveETH, reserveToken, decimals, true)
}, [reserveETH, reserveToken, decimals])
function renderTransactionDetails() {
ReactGA.event({
category: 'TransactionDetail',
action: 'Open'
})
const { value: tokenValue, decimals, label } = getBalance(exchangeAddress, token)
const { value: ethValue } = getBalance(exchangeAddress)
const { value: liquidityBalance } = getBalance(account, exchangeAddress)
const ownership = liquidityBalance.dividedBy(totalSupply)
const ethPer = ethValue.dividedBy(totalSupply)
const tokenPer = tokenValue.dividedBy(totalSupply)
const ownedEth = ethPer.multipliedBy(liquidityBalance).dividedBy(10 ** 18)
const ownedToken = tokenPer.multipliedBy(liquidityBalance).dividedBy(10 ** decimals)
if (!label || !decimals) {
return blank
}
const b = text => <span className="swap__highlight-text">{text}</span>
if (this.isNewExchange()) {
const rate = BN(outputValue).dividedBy(inputValue)
const rateText = rate.isNaN() ? '---' : rate.toFixed(4)
if (isNewExchange) {
return (
<div className="pool__summary-panel">
<div className="pool__exchange-rate-wrapper">
<span className="pool__exchange-rate">{t('exchangeRate')}</span>
<span>{`1 ETH = ${rateText} ${label}`}</span>
<div>
<div className="pool__summary-item">
{t('youAreAdding')} {b(`${inputValue} ETH`)} {t('and')} {b(`${outputValue} ${symbol}`)} {t('intoPool')}
</div>
<div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t('currentPoolSize')}</span>
<span>{` ${ethValue.dividedBy(10 ** 18).toFixed(2)} ${eth} + ${tokenValue
.dividedBy(10 ** decimals)
.toFixed(2)} ${label}`}</span>
<div className="pool__summary-item">
{t('youAreSettingExRate')}{' '}
{b(
`1 ETH = ${amountFormatter(
getMarketRate(inputValueParsed, outputValueParsed, decimals),
18,
4,
false
)} ${symbol}`
)}
.
</div>
<div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">
{t('yourPoolShare')} ({ownership.multipliedBy(100).toFixed(2)}%)
</span>
<span>{`${ownedEth.toFixed(2)} ETH + ${ownedToken.toFixed(2)} ${label}`}</span>
<div className="pool__summary-item">
{t('youWillMint')} {b(`${inputValue}`)} {t('liquidityTokens')}
</div>
<div className="pool__summary-item">{t('totalSupplyIs0')}</div>
</div>
)
} else {
return (
<>
<div className="pool__summary-modal__item">
{t('youAreAdding')} {b(`${amountFormatter(inputValueParsed, 18, 4)} ETH`)} {t('and')} {'at most'}{' '}
{b(`${amountFormatter(outputValueMax, 18, 4)} ${symbol}`)} {t('intoPool')}
</div>
<div className="pool__summary-modal__item">
{t('youWillMint')} {b(amountFormatter(liquidityMinted, 18, 4))} {t('liquidityTokens')}
</div>
<div className="pool__summary-modal__item">
{t('totalSupplyIs')} {b(amountFormatter(totalPoolTokens, 18, 4))}
</div>
<div className="pool__summary-modal__item">
{t('tokenWorth')} {b(amountFormatter(ethPerLiquidityToken, 18, 4))} ETH {t('and')}{' '}
{b(amountFormatter(tokenPerLiquidityToken, decimals, Math.min(decimals, 4)))} {symbol}
</div>
</>
)
}
if (tokenValue.dividedBy(ethValue).isNaN()) {
return blank
}
return (
<div className="pool__summary-panel">
<div className="pool__exchange-rate-wrapper">
<span className="pool__exchange-rate">{t('exchangeRate')}</span>
<span>{`1 ETH = ${tokenValue
.multipliedBy(10 ** (18 - decimals))
.dividedBy(ethValue)
.toFixed(4)} ${label}`}</span>
</div>
<div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t('currentPoolSize')}</span>
<span>{` ${ethValue.dividedBy(10 ** 18).toFixed(2)} ${eth} + ${tokenValue
.dividedBy(10 ** decimals)
.toFixed(2)} ${label}`}</span>
</div>
<div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">
{t('yourPoolShare')} ({ownership.multipliedBy(100).toFixed(2)}%)
</span>
<span>{`${ownedEth.toFixed(2)} ETH + ${ownedToken.toFixed(2)} ${label}`}</span>
</div>
</div>
)
}
renderSummary(inputError, outputError) {
const {
t,
account,
selectors,
exchangeAddresses: { fromToken }
} = this.props
const { inputValue, outputValue, inputCurrency, outputCurrency } = this.state
const inputIsZero = BN(inputValue).isZero()
const outputIsZero = BN(outputValue).isZero()
function renderSummary() {
let contextualInfo = ''
let isError = false
const { label } = selectors().getTokenBalance(outputCurrency, fromToken[outputCurrency])
if (!account) {
contextualInfo = t('noWallet')
isError = true
} else if (inputError || outputError) {
if (inputError || outputError) {
contextualInfo = inputError || outputError
isError = true
} else if (!inputCurrency || !outputCurrency) {
contextualInfo = t('selectTokenCont')
} else if (inputCurrency === outputCurrency) {
contextualInfo = t('differentToken')
} else if (![inputCurrency, outputCurrency].includes('ETH')) {
contextualInfo = t('mustBeETH')
} else if (inputIsZero || outputIsZero) {
contextualInfo = t('noZero')
} else if (this.isUnapproved()) {
contextualInfo = t('unlockTokenCont')
} else if (!inputValue || !outputValue) {
contextualInfo = t('enterCurrencyOrLabelCont', { inputCurrency, label })
} else if (!inputValue) {
contextualInfo = t('enterValueCont')
} else if (!account) {
contextualInfo = t('noWallet')
isError = true
}
return (
<ContextualInfo
key="context-info"
openDetailsText={t('transactionDetails')}
closeDetailsText={t('hideDetails')}
contextualInfo={contextualInfo}
isError={isError}
renderTransactionDetails={this.renderTransactionDetails}
renderTransactionDetails={renderTransactionDetails}
/>
)
}
renderTransactionDetails = () => {
const {
t,
selectors,
exchangeAddresses: { fromToken },
account
} = this.props
const { inputValue, outputValue, outputCurrency, totalSupply } = this.state
const addTransaction = useTransactionAdder()
const isActive = active && account
const isValid = inputError === null || outputError === null
async function onAddLiquidity() {
ReactGA.event({
category: 'TransactionDetail',
action: 'Open'
category: 'Pool',
action: 'AddLiquidity'
})
const { value: tokenReserve, label } = selectors().getTokenBalance(outputCurrency, fromToken[outputCurrency])
const { value: ethReserve } = selectors().getBalance(fromToken[outputCurrency])
const { decimals: poolTokenDecimals } = selectors().getBalance(account, fromToken[outputCurrency])
const deadline = Math.ceil(Date.now() / 1000) + DEADLINE_FROM_NOW
const estimatedGasLimit = await exchangeContract.estimate.addLiquidity(
isNewExchange ? ethers.constants.Zero : liquidityTokensMin,
isNewExchange ? outputValueParsed : outputValueMax,
deadline,
{
value: inputValueParsed
}
)
if (this.isNewExchange()) {
return (
<>
<div className="pool__summary-item">
{t('youAreAdding')} {b(`${inputValue} ETH`)} {t('and')} {b(`${outputValue} ${label}`)} {t('intoPool')}
</div>
<div className="pool__summary-item">
{t('youAreSettingExRate')}{' '}
{b(
`1 ETH = ${BN(outputValue)
.dividedBy(inputValue)
.toFixed(4)} ${label}`
)}
.
</div>
<div className="pool__summary-item">
{t('youWillMint')} {b(`${inputValue}`)} {t('liquidityTokens')}
</div>
<div className="pool__summary-item">{t('totalSupplyIs0')}</div>
</>
exchangeContract
.addLiquidity(
isNewExchange ? ethers.constants.Zero : liquidityTokensMin,
isNewExchange ? outputValueParsed : outputValueMax,
deadline,
{
value: inputValueParsed,
gasLimit: calculateGasMargin(estimatedGasLimit, GAS_MARGIN)
}
)
.then(response => {
addTransaction(response)
})
}
function formatBalance(value) {
return `Balance: ${value}`
}
useEffect(() => {
if (isNewExchange) {
if (inputValue) {
const parsedInputValue = ethers.utils.parseUnits(inputValue, 18)
setInputValueParsed(parsedInputValue)
}
if (outputValue) {
const parsedOutputValue = ethers.utils.parseUnits(outputValue, decimals)
setOutputValueParsed(parsedOutputValue)
}
}
}, [decimals, inputValue, isNewExchange, outputValue])
const SLIPPAGE = 0.025
const minOutput = BN(outputValue).multipliedBy(1 - SLIPPAGE)
const maxOutput = BN(outputValue).multipliedBy(1 + SLIPPAGE)
// const minPercentage = minOutput.dividedBy(minOutput.plus(tokenReserve)).multipliedBy(100);
// const maxPercentage = maxOutput.dividedBy(maxOutput.plus(tokenReserve)).multipliedBy(100);
const liquidityMinted = BN(inputValue).multipliedBy(totalSupply.dividedBy(ethReserve))
const adjTotalSupply = totalSupply.dividedBy(10 ** poolTokenDecimals)
// parse input value
useEffect(() => {
if (isNewExchange === false && inputValue && marketRate && lastEditedField === INPUT) {
try {
const parsedValue = ethers.utils.parseUnits(inputValue, 18)
return (
<>
<div className="pool__summary-modal__item">
{t('youAreAdding')} {b(`${+BN(inputValue).toFixed(7)} ETH`)} {t('and')}{' '}
{b(`${+minOutput.toFixed(7)} - ${+maxOutput.toFixed(7)} ${label}`)} {t('intoPool')}
</div>
<div className="pool__summary-modal__item">
{t('youWillMint')} {b(+liquidityMinted.toFixed(7))} {t('liquidityTokens')}
</div>
<div className="pool__summary-modal__item">
{t('totalSupplyIs')} {b(+adjTotalSupply.toFixed(7))}
</div>
<div className="pool__summary-modal__item">
{t('tokenWorth')} {b(+ethReserve.dividedBy(totalSupply).toFixed(7))} ETH {t('and')}{' '}
{b(+tokenReserve.dividedBy(totalSupply).toFixed(7))} {label}
</div>
</>
)
}
if (parsedValue.lte(ethers.constants.Zero) || parsedValue.gte(ethers.constants.MaxUint256)) {
throw Error()
}
render() {
const {
t,
exchangeAddresses: { fromToken },
selectors
} = this.props
setInputValueParsed(parsedValue)
const currencyAmount = marketRate
.mul(parsedValue)
.div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)))
setOutputValueParsed(currencyAmount)
dispatchAddLiquidityState({
type: 'UPDATE_DEPENDENT_VALUE',
payload: { field: OUTPUT, value: amountFormatter(currencyAmount, 18, 4, false) }
})
return () => {
setOutputError()
setInputValueParsed()
setOutputValueParsed()
dispatchAddLiquidityState({
type: 'UPDATE_DEPENDENT_VALUE',
payload: { field: OUTPUT, value: '' }
})
}
} catch {
setOutputError(t('inputNotValid'))
}
}
}, [inputValue, isNewExchange, lastEditedField, marketRate, t])
const { inputValue, outputValue, inputCurrency, outputCurrency } = this.state
// parse output value
useEffect(() => {
if (isNewExchange === false && outputValue && marketRateInverted && lastEditedField === OUTPUT) {
try {
const parsedValue = ethers.utils.parseUnits(outputValue, 18)
const { inputError, outputError, isValid } = this.validate()
const { label } = selectors().getTokenBalance(outputCurrency, fromToken[outputCurrency])
if (parsedValue.lte(ethers.constants.Zero) || parsedValue.gte(ethers.constants.MaxUint256)) {
throw Error()
}
return (
<>
{this.isNewExchange() ? (
<div className="pool__new-exchange-warning">
<div className="pool__new-exchange-warning-text">
<span role="img" aria-label="liquidity">
🚰
</span>{' '}
{t('firstLiquidity')}
</div>
<div className="pool__new-exchange-warning-text">{t('initialExchangeRate', { label })}</div>
</div>
) : null}
<CurrencyInputPanel
title={t('deposit')}
extraText={this.getBalance(inputCurrency)}
onValueChange={this.onInputChange}
selectedTokenAddress="ETH"
value={inputValue}
errorMessage={inputError}
disableTokenSelect
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
<img className="swap__down-arrow" src={isValid ? PlusBlue : PlusGrey} alt="plus" />
</div>
</OversizedPanel>
<CurrencyInputPanel
title={t('deposit')}
description={this.isNewExchange() ? `(${t('estimated')})` : ''}
extraText={this.getBalance(outputCurrency)}
selectedTokenAddress={outputCurrency}
onCurrencySelected={currency => {
this.setState(
{
outputCurrency: currency
},
this.recalcForm
)
}}
onValueChange={this.onOutputChange}
value={outputValue}
errorMessage={outputError}
filteredTokens={['ETH']}
/>
<OversizedPanel hideBottom>{this.renderInfo()}</OversizedPanel>
{this.renderSummary(inputError, outputError)}
<div className="pool__cta-container">
<AddLiquidityButton callOnClick={this.onAddLiquidity} isValid={isValid} />
</div>
</>
)
}
}
setOutputValueParsed(parsedValue)
const currencyAmount = marketRateInverted
.mul(parsedValue)
.div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)))
setInputValueParsed(currencyAmount)
dispatchAddLiquidityState({
type: 'UPDATE_DEPENDENT_VALUE',
payload: { field: INPUT, value: amountFormatter(currencyAmount, 18, 4, false) }
})
return () => {
setInputError()
setOutputValueParsed()
setInputValueParsed()
dispatchAddLiquidityState({
type: 'UPDATE_DEPENDENT_VALUE',
payload: { field: INPUT, value: '' }
})
}
} catch {
setInputError(t('inputNotValid'))
}
}
}, [outputValue, isNewExchange, lastEditedField, marketRateInverted, t])
// input validation
useEffect(() => {
if (inputValueParsed && inputBalance) {
if (inputValueParsed.gt(inputBalance)) {
setInputError(t('insufficientBalance'))
} else {
setInputError(null)
}
}
function AddLiquidityButton({ callOnClick, isValid }) {
const { t } = useTranslation()
const context = useWeb3Context()
if (outputValueParsed && outputBalance) {
if (outputValueParsed.gt(outputBalance)) {
setOutputError(t('insufficientBalance'))
} else {
setOutputError(null)
}
}
}, [inputValueParsed, inputBalance, outputValueParsed, outputBalance, t])
const allowance = useAddressAllowance(account, outputCurrency, exchangeAddress)
const [showUnlock, setShowUnlock] = useState(false)
useEffect(() => {
if (outputValueParsed && allowance) {
if (allowance.lt(outputValueParsed)) {
setOutputError(t('unlockTokenCont'))
setShowUnlock(true)
}
return () => {
setOutputError()
setShowUnlock(false)
}
}
}, [outputValueParsed, allowance, t])
const isActive = context.active && context.account
return (
<button
className={classnames('pool__cta-btn', {
'pool__cta-btn--inactive': !isActive
})}
disabled={!isValid}
onClick={callOnClick}
>
{t('addLiquidity')}
</button>
<>
{isNewExchange ? (
<div className="pool__new-exchange-warning">
<div className="pool__new-exchange-warning-text">
<span role="img" aria-label="first-liquidity">
🚰
</span>{' '}
{t('firstLiquidity')}
</div>
<div className="pool__new-exchange-warning-text">{t('initialExchangeRate', { symbol })}</div>
</div>
) : null}
<CurrencyInputPanel
title={t('deposit')}
extraText={inputBalance && formatBalance(amountFormatter(inputBalance, 18, 4))}
onValueChange={inputValue => {
dispatchAddLiquidityState({ type: 'UPDATE_VALUE', payload: { value: inputValue, field: INPUT } })
}}
selectedTokenAddress="ETH"
value={inputValue}
errorMessage={inputError}
disableTokenSelect
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
<img className="swap__down-arrow" src={isActive ? PlusBlue : PlusGrey} alt="plus" />
</div>
</OversizedPanel>
<CurrencyInputPanel
title={t('deposit')}
description={isNewExchange ? '' : outputValue ? `(${t('estimated')})` : ''}
extraText={outputBalance && formatBalance(amountFormatter(outputBalance, decimals, Math.min(decimals, 4)))}
selectedTokenAddress={outputCurrency}
onCurrencySelected={outputCurrency => {
dispatchAddLiquidityState({ type: 'SELECT_CURRENCY', payload: outputCurrency })
}}
onValueChange={outputValue => {
dispatchAddLiquidityState({ type: 'UPDATE_VALUE', payload: { value: outputValue, field: OUTPUT } })
}}
value={outputValue}
showUnlock={showUnlock}
errorMessage={outputError}
/>
<OversizedPanel hideBottom>
<div className="pool__summary-panel">
<div className="pool__exchange-rate-wrapper">
<span className="pool__exchange-rate">{t('exchangeRate')}</span>
<span>{marketRate ? `1 ETH = ${amountFormatter(marketRate, 18, 4)} ${symbol}` : ' - '}</span>
</div>
<div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t('currentPoolSize')}</span>
<span>
{exchangeETHBalance && exchangeTokenBalance
? `${amountFormatter(exchangeETHBalance, 18, 4)} ETH + ${amountFormatter(
exchangeTokenBalance,
decimals,
Math.min(4, decimals)
)} ${symbol}`
: ' - '}
</span>
</div>
<div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">
{t('yourPoolShare')} ({exchangeETHBalance && amountFormatter(poolTokenPercentage, 16, 2)}%)
</span>
<span>
{ethShare && tokenShare
? `${amountFormatter(ethShare, 18, 4)} ETH + ${amountFormatter(
tokenShare,
decimals,
Math.min(4, decimals)
)} ${symbol}`
: ' - '}
</span>
</div>
</div>
</OversizedPanel>
{renderSummary()}
<div className="pool__cta-container">
<button
className={classnames('pool__cta-btn', {
'pool__cta-btn--inactive': !isActive
})}
disabled={!isValid}
onClick={onAddLiquidity}
>
{t('addLiquidity')}
</button>
</div>
</>
)
}
export default connect(
state => ({
account: state.web3connect.account,
balances: state.web3connect.balances,
web3: state.web3connect.web3,
exchangeAddresses: state.addresses.exchangeAddresses
}),
dispatch => ({
selectors: () => dispatch(selectors()),
addPendingTx: id => dispatch(addPendingTx(id))
})
)(withTranslation()(AddLiquidity))
function b(text) {
return <span className="swap__highlight-text">{text}</span>
}
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 { useFactoryContract } from '../../hooks'
import { useTokenDetails } from '../../contexts/Tokens'
import { useTransactionAdder } from '../../contexts/Transactions'
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()
const [tokenAddress, setTokenAddress] = useState({
address: '',
name: ''
})
const [tokenAddressError, setTokenAddressError] = 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 { name, symbol, decimals, exchangeAddress } = useTokenDetails(tokenAddress.address)
const addTransaction = useTransactionAdder()
// 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 {
setErrorMessage(t('invalidTokenAddress'))
}
}
})
}
// is tokenAddress is empty, there's no error
else if (tokenAddress === undefined || tokenAddress === '') {
setErrorMessage()
}
// tokenAddress is not a proper address
else {
if (tokenAddressError) {
setErrorMessage(t('invalidTokenAddress'))
} else if (symbol === undefined || decimals === undefined || exchangeAddress === undefined) {
setErrorMessage()
} 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.address, symbol, decimals, exchangeAddress, account, t, tokenAddressError])
async function createExchange() {
const estimatedGasLimit = await factory.estimate.createExchange(tokenAddress)
const estimatedGasLimit = await factory.estimate.createExchange(tokenAddress.address)
factory.createExchange(tokenAddress, { gasLimit: estimatedGasLimit }).then(details => {
addPendingTx(details.hash)
setErrorMessage()
setTokenAddress()
factory.createExchange(tokenAddress.address, { gasLimit: estimatedGasLimit }).then(response => {
ReactGA.event({
category: 'Pool',
action: 'CreateExchange'
})
addTransaction(response)
})
}
const isValid = isAddress(tokenAddress) && !errorMessage && tokenDetails && tokenDetails.tokenAddress === tokenAddress
const isValid = errorMessage === null
return (
<>
<AddressInputPanel
title={t('tokenAddress')}
value={tokenAddress}
onChange={input => setTokenAddress(input)}
errorMessage={errorMessage === t('noWallet') ? '' : errorMessage}
initialInput={(location.state && location.state.tokenAddress) || ''}
onChange={setTokenAddress}
onError={setTokenAddressError}
/>
<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 +102,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 +114,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 PropTypes from 'prop-types'
import React, { useState, useEffect, useCallback } from 'react'
import classnames from 'classnames'
import { connect } from 'react-redux'
import { BigNumber as BN } from 'bignumber.js'
import { withTranslation, useTranslation } from 'react-i18next'
import { useTranslation } from 'react-i18next'
import ReactGA from 'react-ga'
import { useWeb3Context } from 'web3-react'
import { ethers } from 'ethers'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import { selectors, addPendingTx } from '../../ducks/web3connect'
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'
class RemoveLiquidity extends Component {
static propTypes = {
account: PropTypes.string,
balances: PropTypes.object,
web3: PropTypes.object,
exchangeAddresses: PropTypes.shape({
fromToken: PropTypes.object.isRequired
}).isRequired
}
state = {
tokenAddress: '',
value: '',
totalSupply: BN(0)
}
reset() {
this.setState({
value: ''
})
}
validate() {
const { tokenAddress, value } = this.state
const {
t,
account,
selectors,
exchangeAddresses: { fromToken },
web3
} = this.props
const exchangeAddress = fromToken[tokenAddress]
if (!web3 || !exchangeAddress || !account || !value) {
return {
isValid: false
import { useExchangeContract } from '../../hooks'
import { useTransactionAdder } from '../../contexts/Transactions'
import { useTokenDetails } from '../../contexts/Tokens'
import { useAddressBalance } from '../../contexts/Balances'
import { calculateGasMargin, amountFormatter } from '../../utils'
// denominated in bips
const ALLOWED_SLIPPAGE = ethers.utils.bigNumberify(200)
// denominated in seconds
const DEADLINE_FROM_NOW = 60 * 15
// denominated in bips
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
function getExchangeRate(inputValue, inputDecimals, outputValue, outputDecimals, invert = false) {
try {
if (
inputValue &&
(inputDecimals || inputDecimals === 0) &&
outputValue &&
(outputDecimals || outputDecimals === 0)
) {
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 {}
}
const { getBalance } = selectors()
const { value: liquidityBalance, decimals: liquidityDecimals } = getBalance(account, exchangeAddress)
if (liquidityBalance.isLessThan(BN(value).multipliedBy(10 ** liquidityDecimals))) {
return { isValid: false, errorMessage: t('insufficientBalance') }
}
function getMarketRate(reserveETH, reserveToken, decimals, invert = false) {
return getExchangeRate(reserveETH, 18, reserveToken, decimals, invert)
}
function calculateSlippageBounds(value) {
if (value) {
const offset = value.mul(ALLOWED_SLIPPAGE).div(ethers.utils.bigNumberify(10000))
const minimum = value.sub(offset)
const maximum = value.add(offset)
return {
isValid: true
minimum: minimum.lt(ethers.constants.Zero) ? ethers.constants.Zero : minimum,
maximum: maximum.gt(ethers.constants.MaxUint256) ? ethers.constants.MaxUint256 : maximum
}
} else {
return {}
}
}
onTokenSelect = async tokenAddress => {
const {
exchangeAddresses: { fromToken },
web3
} = this.props
const exchangeAddress = fromToken[tokenAddress]
this.setState({ tokenAddress })
if (!web3 || !exchangeAddress) {
return
}
const exchange = new web3.eth.Contract(EXCHANGE_ABI, exchangeAddress)
const totalSupply = await exchange.methods.totalSupply().call()
this.setState({
totalSupply: BN(totalSupply)
})
}
export default function RemoveLiquidity() {
const { library, account, active } = useWeb3Context()
const { t } = useTranslation()
onInputChange = value => {
this.setState({ value })
}
const addTransaction = useTransactionAdder()
onRemoveLiquidity = async () => {
const { tokenAddress, value: input, totalSupply } = this.state
const {
exchangeAddresses: { fromToken },
web3,
selectors,
account
} = this.props
const exchangeAddress = fromToken[tokenAddress]
const { getBalance } = selectors()
if (!web3 || !exchangeAddress) {
return
}
const exchange = new web3.eth.Contract(EXCHANGE_ABI, exchangeAddress)
const SLIPPAGE = 0.02
const { decimals } = getBalance(account, exchangeAddress)
const { value: ethReserve } = getBalance(exchangeAddress)
const { value: tokenReserve } = getBalance(exchangeAddress, tokenAddress)
const amount = BN(input).multipliedBy(10 ** decimals)
const ownership = amount.dividedBy(totalSupply)
const ethWithdrawn = ethReserve.multipliedBy(ownership)
const tokenWithdrawn = tokenReserve.multipliedBy(ownership)
let deadline
const [outputCurrency, setOutputCurrency] = useState('')
const [value, setValue] = useState('')
const [inputError, setInputError] = useState()
const [valueParsed, setValueParsed] = useState()
// parse value
useEffect(() => {
try {
deadline = await retry(() => getBlockDeadline(web3, 600))
} catch (e) {
// TODO: Handle error.
return
const parsedValue = ethers.utils.parseUnits(value, 18)
setValueParsed(parsedValue)
} catch {
if (value !== '') {
setInputError(t('inputNotValid'))
}
}
exchange.methods
.removeLiquidity(
amount.toFixed(0),
ethWithdrawn.multipliedBy(1 - SLIPPAGE).toFixed(0),
tokenWithdrawn.multipliedBy(1 - SLIPPAGE).toFixed(0),
deadline
)
.send({ from: account }, (err, data) => {
if (data) {
this.reset()
this.props.addPendingTx(data)
ReactGA.event({
category: 'Pool',
action: 'RemoveLiquidity'
})
}
})
}
getBalance = () => {
const {
exchangeAddresses: { fromToken },
account,
web3,
selectors
} = this.props
const { tokenAddress } = this.state
if (!web3) {
return ''
return () => {
setInputError()
setValueParsed()
}
const exchangeAddress = fromToken[tokenAddress]
if (!exchangeAddress) {
return ''
}, [t, value])
const { symbol, decimals, exchangeAddress } = useTokenDetails(outputCurrency)
const [totalPoolTokens, setTotalPoolTokens] = useState()
const poolTokenBalance = useAddressBalance(account, exchangeAddress)
const exchangeETHBalance = useAddressBalance(exchangeAddress, 'ETH')
const exchangeTokenBalance = useAddressBalance(exchangeAddress, outputCurrency)
// input validation
useEffect(() => {
if (valueParsed && poolTokenBalance) {
if (valueParsed.gt(poolTokenBalance)) {
setInputError(t('insufficientBalance'))
} else {
setInputError(null)
}
}
const { value, decimals } = selectors().getBalance(account, exchangeAddress)
if (!decimals) {
return ''
}, [poolTokenBalance, t, valueParsed])
const exchange = useExchangeContract(exchangeAddress)
const ownershipPercentage =
poolTokenBalance && totalPoolTokens && !totalPoolTokens.isZero()
? poolTokenBalance.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18))).div(totalPoolTokens)
: undefined
const ownershipPercentageFormatted = ownershipPercentage && amountFormatter(ownershipPercentage, 16, 4)
const ETHOwnShare =
exchangeETHBalance &&
ownershipPercentage &&
exchangeETHBalance.mul(ownershipPercentage).div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)))
const TokenOwnShare =
exchangeTokenBalance &&
ownershipPercentage &&
exchangeTokenBalance.mul(ownershipPercentage).div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)))
const ETHPer =
exchangeETHBalance && totalPoolTokens && !totalPoolTokens.isZero()
? exchangeETHBalance.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18))).div(totalPoolTokens)
: undefined
const tokenPer =
exchangeTokenBalance && totalPoolTokens && !totalPoolTokens.isZero()
? exchangeTokenBalance.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18))).div(totalPoolTokens)
: undefined
const ethWithdrawn =
ETHPer &&
valueParsed &&
ETHPer.mul(valueParsed).div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)))
const tokenWithdrawn =
tokenPer &&
valueParsed &&
tokenPer.mul(valueParsed).div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)))
const ethWithdrawnMin = ethWithdrawn ? calculateSlippageBounds(ethWithdrawn).minimum : undefined
const tokenWithdrawnMin = tokenWithdrawn ? calculateSlippageBounds(tokenWithdrawn).minimum : undefined
const fetchPoolTokens = useCallback(() => {
if (exchange) {
exchange.totalSupply().then(totalSupply => {
setTotalPoolTokens(totalSupply)
})
}
}, [exchange])
useEffect(() => {
fetchPoolTokens()
library.on('block', fetchPoolTokens)
return `Balance: ${value.dividedBy(10 ** decimals).toFixed(7)}`
}
return () => {
library.removeListener('block', fetchPoolTokens)
}
}, [fetchPoolTokens, library])
renderSummary(errorMessage) {
const {
t,
account,
selectors,
exchangeAddresses: { fromToken }
} = this.props
const { value: input, tokenAddress } = this.state
const inputIsZero = BN(input).isZero()
let contextualInfo = ''
let isError = false
async function onRemoveLiquidity() {
ReactGA.event({
category: 'Pool',
action: 'RemoveLiquidity'
})
if (!account) {
contextualInfo = t('noWallet')
isError = true
} else if (errorMessage) {
contextualInfo = errorMessage
isError = true
} else if (!tokenAddress) {
contextualInfo = t('selectTokenCont')
} else if (inputIsZero) {
contextualInfo = t('noZero')
} else if (!input) {
const { label } = selectors().getTokenBalance(tokenAddress, fromToken[tokenAddress])
contextualInfo = t('enterLabelCont', { label })
}
const deadline = Math.ceil(Date.now() / 1000) + DEADLINE_FROM_NOW
return (
<ContextualInfo
key="context-info"
openDetailsText={t('transactionDetails')}
closeDetailsText={t('hideDetails')}
contextualInfo={contextualInfo}
isError={isError}
renderTransactionDetails={this.renderTransactionDetails}
/>
const estimatedGasLimit = await exchange.estimate.removeLiquidity(
valueParsed,
ethWithdrawnMin,
tokenWithdrawnMin,
deadline
)
exchange
.removeLiquidity(valueParsed, ethWithdrawnMin, tokenWithdrawnMin, deadline, {
gasLimit: calculateGasMargin(estimatedGasLimit, GAS_MARGIN)
})
.then(response => {
addTransaction(response)
})
}
renderTransactionDetails = () => {
const { tokenAddress, value: input, totalSupply } = this.state
const {
t,
exchangeAddresses: { fromToken },
selectors,
account
} = this.props
const exchangeAddress = fromToken[tokenAddress]
const { getBalance } = selectors()
if (!exchangeAddress) {
return null
}
const b = text => <span className="swap__highlight-text">{text}</span>
function renderTransactionDetails() {
ReactGA.event({
category: 'TransactionDetail',
action: 'Open'
})
const SLIPPAGE = 0.025
const { decimals } = getBalance(account, exchangeAddress)
const { value: ethReserve } = getBalance(exchangeAddress)
const { value: tokenReserve, label } = getBalance(exchangeAddress, tokenAddress)
const ethPer = ethReserve.dividedBy(totalSupply)
const tokenPer = tokenReserve.dividedBy(totalSupply)
const ethWithdrawn = ethPer.multipliedBy(input)
const tokenWithdrawn = tokenPer.multipliedBy(input)
const minTokenWithdrawn = tokenWithdrawn.multipliedBy(1 - SLIPPAGE).toFixed(7)
const maxTokenWithdrawn = tokenWithdrawn.multipliedBy(1 + SLIPPAGE).toFixed(7)
const adjTotalSupply = totalSupply.dividedBy(10 ** decimals).minus(input)
return (
<div>
<div className="pool__summary-modal__item">
{t('youAreRemoving')} {b(`${+BN(ethWithdrawn).toFixed(7)} ETH`)} {t('and')}{' '}
{b(`${+minTokenWithdrawn} - ${+maxTokenWithdrawn} ${label}`)} {t('outPool')}
{t('youAreRemoving')} {b(`${amountFormatter(ethWithdrawnMin, 18, 4)} ETH`)} {t('and')}{' '}
{b(`${amountFormatter(tokenWithdrawnMin, decimals, Math.min(decimals, 4))} ${symbol}`)} {t('outPool')}
</div>
<div className="pool__summary-modal__item">
{t('youWillRemove')} {b(+input)} {t('liquidityTokens')}
{t('youWillRemove')} {b(amountFormatter(valueParsed, 18, 4))} {t('liquidityTokens')}
</div>
<div className="pool__summary-modal__item">
{t('totalSupplyIs')} {b(+adjTotalSupply.toFixed(7))}
{t('totalSupplyIs')} {b(amountFormatter(totalPoolTokens, 18, 4))}
</div>
<div className="pool__summary-modal__item">
{t('tokenWorth')} {b(+ethReserve.dividedBy(totalSupply).toFixed(7))} ETH {t('and')}{' '}
{b(+tokenReserve.dividedBy(totalSupply).toFixed(7))} {label}
{t('tokenWorth')} {b(amountFormatter(ETHPer, 18, 4))} ETH {t('and')}{' '}
{b(amountFormatter(tokenPer, decimals, Math.min(4, decimals)))} {symbol}
</div>
</div>
)
}
renderOutput() {
const {
t,
exchangeAddresses: { fromToken },
account,
web3,
selectors
} = this.props
const { getBalance } = selectors()
const { tokenAddress, totalSupply, value: input } = this.state
const blank = [
<CurrencyInputPanel
key="remove-liquidity-input"
title={t('output')}
description={`(${t('estimated')})`}
renderInput={() => <div className="remove-liquidity__output" />}
disableTokenSelect
disableUnlock
/>,
<OversizedPanel key="remove-liquidity-input-under" hideBottom>
<div className="pool__summary-panel">
<div className="pool__exchange-rate-wrapper">
<span className="pool__exchange-rate">{t('exchangeRate')}</span>
<span> - </span>
</div>
<div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t('currentPoolSize')}</span>
<span> - </span>
</div>
<div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t('yourPoolShare')}</span>
<span> - </span>
</div>
</div>
</OversizedPanel>
]
function renderSummary() {
let contextualInfo = ''
let isError = false
const exchangeAddress = fromToken[tokenAddress]
if (!exchangeAddress || !web3) {
return blank
if (inputError) {
contextualInfo = inputError
isError = true
} else if (!outputCurrency || outputCurrency === 'ETH') {
contextualInfo = t('selectTokenCont')
} else if (!valueParsed) {
contextualInfo = t('enterValueCont')
} else if (!account) {
contextualInfo = t('noWallet')
isError = true
}
const { value: liquidityBalance } = getBalance(account, exchangeAddress)
const { value: ethReserve } = getBalance(exchangeAddress)
const { value: tokenReserve, decimals: tokenDecimals, label } = getBalance(exchangeAddress, tokenAddress)
return (
<ContextualInfo
key="context-info"
openDetailsText={t('transactionDetails')}
closeDetailsText={t('hideDetails')}
contextualInfo={contextualInfo}
isError={isError}
renderTransactionDetails={renderTransactionDetails}
/>
)
}
if (!tokenDecimals) {
return blank
}
function formatBalance(value) {
return `Balance: ${value}`
}
const ownership = liquidityBalance.dividedBy(totalSupply)
const ethPer = ethReserve.dividedBy(totalSupply)
const tokenPer = tokenReserve.multipliedBy(10 ** (18 - tokenDecimals)).dividedBy(totalSupply)
const exchangeRate = tokenReserve.multipliedBy(10 ** (18 - tokenDecimals)).div(ethReserve)
const isActive = active && account
const isValid = inputError === null
const ownedEth = ethPer.multipliedBy(liquidityBalance).dividedBy(10 ** 18)
const ownedToken = tokenPer.multipliedBy(liquidityBalance).dividedBy(10 ** tokenDecimals)
const marketRate = getMarketRate(exchangeETHBalance, exchangeTokenBalance, decimals)
return [
return (
<>
<CurrencyInputPanel
title={t('poolTokens')}
extraText={poolTokenBalance && formatBalance(amountFormatter(poolTokenBalance, 18, 4))}
extraTextClickHander={() => {
if (poolTokenBalance) {
const valueToSet = poolTokenBalance
if (valueToSet.gt(ethers.constants.Zero)) {
setValue(amountFormatter(valueToSet, 18, 18, false))
}
}
}}
onCurrencySelected={setOutputCurrency}
onValueChange={setValue}
value={value}
errorMessage={inputError}
selectedTokenAddress={outputCurrency}
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
<img className="swap__down-arrow" src={isValid ? ArrowDownBlue : ArrowDownGrey} alt="arrow" />
</div>
</OversizedPanel>
<CurrencyInputPanel
title={t('output')}
description={`(${t('estimated')})`}
description={ethWithdrawn && tokenWithdrawn ? `(${t('estimated')})` : ''}
key="remove-liquidity-input"
renderInput={() =>
input ? (
ethWithdrawn && tokenWithdrawn ? (
<div className="remove-liquidity__output">
<div className="remove-liquidity__output-text">{`${ethPer.multipliedBy(input).toFixed(3)} ETH`}</div>
<div className="remove-liquidity__output-text">{`${amountFormatter(
ethWithdrawn,
18,
4,
false
)} ETH`}</div>
<div className="remove-liquidity__output-plus"> + </div>
<div className="remove-liquidity__output-text">
{`${tokenPer.multipliedBy(input).toFixed(3)} ${label}`}
</div>
<div className="remove-liquidity__output-text">{`${amountFormatter(
tokenWithdrawn,
decimals,
Math.min(4, decimals)
)} ${symbol}`}</div>
</div>
) : (
<div className="remove-liquidity__output" />
......@@ -345,93 +308,55 @@ class RemoveLiquidity extends Component {
}
disableTokenSelect
disableUnlock
/>,
/>
<OversizedPanel key="remove-liquidity-input-under" hideBottom>
<div className="pool__summary-panel">
<div className="pool__exchange-rate-wrapper">
<span className="pool__exchange-rate">{t('exchangeRate')}</span>
<span>{`1 ETH = ${exchangeRate.toFixed(4)} ${label}`}</span>
{marketRate ? <span>{`1 ETH = ${amountFormatter(marketRate, 18, 4)} ${symbol}`}</span> : ' - '}
</div>
<div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t('currentPoolSize')}</span>
<span>{`${ethReserve.dividedBy(10 ** 18).toFixed(2)} ETH + ${tokenReserve
.dividedBy(10 ** tokenDecimals)
.toFixed(2)} ${label}`}</span>
{exchangeETHBalance && exchangeTokenBalance && decimals ? (
<span>{`${amountFormatter(exchangeETHBalance, 18, 4)} ETH + ${amountFormatter(
exchangeTokenBalance,
decimals,
Math.min(decimals, 4)
)} ${symbol}`}</span>
) : (
' - '
)}
</div>
<div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">
{t('yourPoolShare')} ({ownership.multipliedBy(100).toFixed(2)}%)
{t('yourPoolShare')} ({ownershipPercentageFormatted && ownershipPercentageFormatted}%)
</span>
<span>{`${ownedEth.toFixed(2)} ETH + ${ownedToken.toFixed(2)} ${label}`}</span>
{ETHOwnShare && TokenOwnShare ? (
<span>
{`${amountFormatter(ETHOwnShare, 18, 4)} ETH + ${amountFormatter(
TokenOwnShare,
decimals,
Math.min(decimals, 4)
)} ${symbol}`}
</span>
) : (
' - '
)}
</div>
</div>
</OversizedPanel>
]
}
render() {
const { t } = this.props
const { tokenAddress, value } = this.state
const { isValid, errorMessage } = this.validate()
return (
<>
<CurrencyInputPanel
title={t('poolTokens')}
extraText={this.getBalance(tokenAddress)}
onValueChange={this.onInputChange}
value={value}
errorMessage={errorMessage}
selectedTokenAddress={tokenAddress}
onCurrencySelected={this.onTokenSelect}
filteredTokens={['ETH']}
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
<img className="swap__down-arrow" src={isValid ? ArrowDownBlue : ArrowDownGrey} alt="arrow" />
</div>
</OversizedPanel>
{this.renderOutput()}
{this.renderSummary(errorMessage)}
<div className="pool__cta-container">
<RemoveLiquidityButton callOnClick={this.onRemoveLiquidity} isValid={isValid} />
</div>
</>
)
}
}
function RemoveLiquidityButton({ callOnClick, isValid }) {
const { t } = useTranslation()
const context = useWeb3Context()
const isActive = context.active && context.account
return (
<button
className={classnames('pool__cta-btn', {
'pool__cta-btn--inactive': !isActive
})}
disabled={!isValid}
onClick={callOnClick}
>
{t('removeLiquidity')}
</button>
{renderSummary()}
<div className="pool__cta-container">
<button
className={classnames('pool__cta-btn', {
'pool__cta-btn--inactive': !isActive
})}
disabled={!isValid}
onClick={onRemoveLiquidity}
>
{t('removeLiquidity')}
</button>
</div>
</>
)
}
export default connect(
state => ({
web3: state.web3connect.web3,
balances: state.web3connect.balances,
account: state.web3connect.account,
exchangeAddresses: state.addresses.exchangeAddresses
}),
dispatch => ({
selectors: () => dispatch(selectors()),
addPendingTx: id => dispatch(addPendingTx(id))
})
)(withTranslation()(RemoveLiquidity))
function b(text) {
return <span className="swap__highlight-text">{text}</span>
}
......@@ -69,6 +69,7 @@
}
&__new-exchange-warning {
margin-top: 1rem;
padding: 1rem;
margin-bottom: 2rem;
border: 1px solid rgba($pizazz-orange, 0.4);
......
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 NewContextualInfo from '../../components/ContextualInfoNew'
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 { isAddress, amountFormatter, calculateGasMargin } from '../../utils'
import { useExchangeContract } from '../../hooks'
import { useTokenDetails } from '../../contexts/Tokens'
import { useTransactionAdder } from '../../contexts/Transactions'
import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances'
import { useAddressAllowance } from '../../contexts/Allowances'
import './send.scss'
const INPUT = 0
const OUTPUT = 1
class Send extends Component {
static propTypes = {
account: PropTypes.string,
selectors: PropTypes.func.isRequired,
web3: PropTypes.object.isRequired
}
state = {
inputValue: '',
outputValue: '',
inputCurrency: 'ETH',
outputCurrency: '',
inputAmountB: '',
lastEditedField: '',
recipient: ''
}
componentWillMount() {
ReactGA.pageview(window.location.pathname + window.location.search)
}
shouldComponentUpdate(nextProps, nextState) {
return true
}
reset() {
this.setState({
inputValue: '',
outputValue: '',
inputAmountB: '',
lastEditedField: '',
recipient: ''
})
}
componentWillReceiveProps() {
this.recalcForm()
}
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
}
const ETH_TO_TOKEN = 0
const TOKEN_TO_ETH = 1
const TOKEN_TO_TOKEN = 2
const { value: inputBalance, decimals: inputDecimals } = selectors().getBalance(account, inputCurrency)
// denominated in bips
const ALLOWED_SLIPPAGE = ethers.utils.bigNumberify(200)
const TOKEN_ALLOWED_SLIPPAGE = ethers.utils.bigNumberify(400)
if (inputBalance.isLessThan(BN(inputValue * 10 ** inputDecimals))) {
inputError = this.props.t('insufficientBalance')
}
// denominated in seconds
const DEADLINE_FROM_NOW = 60 * 15
if (inputValue === 'N/A') {
inputError = this.props.t('inputNotValid')
}
// denominated in bips
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
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 {
inputError,
outputError,
isValid: isValid && !inputError && !outputError
minimum: minimum.lt(ethers.constants.Zero) ? ethers.constants.Zero : minimum,
maximum: maximum.gt(ethers.constants.MaxUint256) ? ethers.constants.MaxUint256 : maximum
}
} else {
return {}
}
}
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()
)
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
}
}
isUnapproved() {
const { account, exchangeAddresses, selectors } = this.props
const { inputCurrency, inputValue } = this.state
// 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 (!inputCurrency || inputCurrency === 'ETH') {
return false
}
// 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)
}
const { value: allowance, label, decimals } = selectors().getApprovals(
inputCurrency,
account,
exchangeAddresses.fromToken[inputCurrency]
)
const initialSwapState = {
independentValue: '', // this is a user input
dependentValue: '', // this is a calculated number
independentField: INPUT,
inputCurrency: 'ETH',
outputCurrency: ''
}
if (label && allowance.isLessThan(BN(inputValue * 10 ** decimals || 0))) {
return true
function swapStateReducer(state, action) {
switch (action.type) {
case 'FLIP_INDEPENDENT': {
const { independentField, inputCurrency, outputCurrency } = state
return {
...state,
dependentValue: '',
independentField: independentField === INPUT ? OUTPUT : INPUT,
inputCurrency: outputCurrency,
outputCurrency: inputCurrency
}
}
return false
}
recalcForm() {
const { inputCurrency, outputCurrency, lastEditedField } = this.state
if (!inputCurrency || !outputCurrency) {
return
case 'SELECT_CURRENCY': {
const { inputCurrency, outputCurrency } = state
const { field, currency } = action.payload
const newInputCurrency = field === INPUT ? currency : inputCurrency
const newOutputCurrency = field === OUTPUT ? currency : outputCurrency
if (newInputCurrency === newOutputCurrency) {
return {
...state,
inputCurrency: field === INPUT ? currency : '',
outputCurrency: field === OUTPUT ? currency : ''
}
} else {
return {
...state,
inputCurrency: newInputCurrency,
outputCurrency: newOutputCurrency
}
}
}
const editedValue = lastEditedField === INPUT ? this.state.inputValue : this.state.outputValue
if (BN(editedValue).isZero()) {
return
case 'UPDATE_INDEPENDENT': {
const { field, value } = action.payload
return {
...state,
independentValue: value,
dependentValue: '',
independentField: field
}
}
if (inputCurrency === outputCurrency) {
this.setState({
inputValue: '',
outputValue: ''
})
return
case 'UPDATE_DEPENDENT': {
return {
...state,
dependentValue: action.payload
}
}
if (inputCurrency !== 'ETH' && outputCurrency !== 'ETH') {
this.recalcTokenTokenForm()
return
default: {
return initialSwapState
}
this.recalcEthTokenForm()
}
}
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
function getExchangeRate(inputValue, inputDecimals, outputValue, outputDecimals, invert = false) {
try {
if (
inputValue &&
(inputDecimals || inputDecimals === 0) &&
outputValue &&
(outputDecimals || outputDecimals === 0)
) {
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 (outputValue !== oldOutputValue) {
appendState.outputValue = outputValue
}
function getMarketRate(
swapType,
inputReserveETH,
inputReserveToken,
inputDecimals,
outputReserveETH,
outputReserveToken,
outputDecimals,
invert = false
) {
if (swapType === ETH_TO_TOKEN) {
return getExchangeRate(outputReserveETH, 18, outputReserveToken, outputDecimals, invert)
} else if (swapType === TOKEN_TO_ETH) {
return getExchangeRate(inputReserveToken, inputDecimals, inputReserveETH, 18, invert)
} else if (swapType === TOKEN_TO_TOKEN) {
const factor = ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18))
const firstRate = getExchangeRate(inputReserveToken, inputDecimals, inputReserveETH, 18)
const secondRate = getExchangeRate(outputReserveETH, 18, outputReserveToken, outputDecimals)
try {
return !!(firstRate && secondRate) ? firstRate.mul(secondRate).div(factor) : undefined
} catch {}
}
}
this.setState(appendState)
}
export default function Swap() {
const { t } = useTranslation()
const { account } = useWeb3Context()
if (lastEditedField === OUTPUT) {
if (!oldOutputValue) {
return this.setState({
inputValue: '',
exchangeRate: BN(0)
})
}
const addTransaction = useTransactionAdder()
const outputAmountB = BN(oldOutputValue).multipliedBy(10 ** outputDecimalsB)
const inputAmountB = calculateEtherTokenInput({
outputAmount: outputAmountB,
inputReserve: inputReserveB,
outputReserve: outputReserveB
})
// analytics
useEffect(() => {
ReactGA.pageview(window.location.pathname + window.location.search)
}, [])
// 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
})
// core swap state
const [swapState, dispatchSwapState] = useReducer(swapStateReducer, initialSwapState)
const { independentValue, dependentValue, independentField, inputCurrency, outputCurrency } = swapState
const inputValue = inputAmountA.isNegative() ? 'N/A' : inputAmountA.dividedBy(BN(10 ** inputDecimalsA)).toFixed(7)
const exchangeRate = BN(oldOutputValue).dividedBy(BN(inputValue))
const [recipient, setRecipient] = useState({ address: '', name: '' })
const [recipientError, setRecipientError] = useState()
const appendState = {}
// get swap type from the currency types
const swapType = getSwapType(inputCurrency, outputCurrency)
if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) {
appendState.exchangeRate = exchangeRate
}
// 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 (inputValue !== oldInputValue) {
appendState.inputValue = inputValue
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)
if (parsedValue.lte(ethers.constants.Zero) || parsedValue.gte(ethers.constants.MaxUint256)) {
throw Error()
} else {
setIndependentValueParsed(parsedValue)
setIndependentError(null)
}
} catch {
setIndependentError(t('inputNotValid'))
}
if (!inputAmountB.isEqualTo(BN(oldInputAmountB))) {
appendState.inputAmountB = inputAmountB
return () => {
setIndependentValueParsed()
setIndependentError()
}
this.setState(appendState)
}
}
}, [independentValue, independentDecimals, t])
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
}
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)
})
}
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)
}
}
}, [independentField, independentValueParsed, dependentValueMaximum, inputBalance, inputCurrency, inputAllowance, t])
this.setState(appendState)
} else if (lastEditedField === OUTPUT) {
if (!oldOutputValue) {
return this.setState({
inputValue: '',
exchangeRate: BN(0)
})
}
// calculate dependent value
useEffect(() => {
const amount = independentValueParsed
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))
if (swapType === ETH_TO_TOKEN) {
const reserveETH = outputReserveETH
const reserveToken = outputReserveToken
const appendState = {}
if (amount && reserveETH && reserveToken) {
try {
const calculatedDependentValue =
independentField === INPUT
? calculateEtherTokenOutputFromInput(amount, reserveETH, reserveToken)
: calculateEtherTokenInputFromOutput(amount, reserveETH, reserveToken)
if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) {
appendState.exchangeRate = exchangeRate
}
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
throw Error()
}
if (inputValue !== oldInputValue) {
appendState.inputValue = inputValue
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
} catch {
setIndependentError(t('insufficientLiquidity'))
}
return () => {
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' })
}
}
} else if (swapType === TOKEN_TO_ETH) {
const reserveETH = inputReserveETH
const reserveToken = inputReserveToken
if (amount && reserveETH && reserveToken) {
try {
const calculatedDependentValue =
independentField === INPUT
? calculateEtherTokenOutputFromInput(amount, reserveToken, reserveETH)
: calculateEtherTokenInputFromOutput(amount, reserveToken, reserveETH)
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
throw Error()
}
this.setState(appendState)
}
}
updateInput = amount => {
this.setState(
{
inputValue: amount,
lastEditedField: INPUT
},
this.recalcForm
)
}
updateOutput = amount => {
this.setState(
{
outputValue: amount,
lastEditedField: OUTPUT
},
this.recalcForm
)
}
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
try {
deadline = await retry(() => getBlockDeadline(web3, 600))
} catch (e) {
// TODO: Handle error.
return
}
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
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
} catch {
setIndependentError(t('insufficientLiquidity'))
}
return () => {
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' })
}
}
}
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
} else if (swapType === TOKEN_TO_TOKEN) {
const reserveETHFirst = inputReserveETH
const reserveTokenFirst = inputReserveToken
const reserveETHSecond = outputReserveETH
const reserveTokenSecond = outputReserveToken
if (amount && reserveETHFirst && reserveTokenFirst && reserveETHSecond && reserveTokenSecond) {
try {
if (independentField === INPUT) {
const intermediateValue = calculateEtherTokenOutputFromInput(amount, reserveTokenFirst, reserveETHFirst)
if (intermediateValue.lte(ethers.constants.Zero)) {
throw Error()
}
const calculatedDependentValue = calculateEtherTokenOutputFromInput(
intermediateValue,
reserveETHSecond,
reserveTokenSecond
)
.send(
{
from: account,
value: BN(inputValue)
.multipliedBy(10 ** inputDecimals)
.multipliedBy(1 + ALLOWED_SLIPPAGE)
.toFixed(0)
},
(err, data) => {
if (!err) {
addPendingTx(data)
this.reset()
}
}
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
throw Error()
}
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
} else {
const intermediateValue = calculateEtherTokenInputFromOutput(amount, reserveETHSecond, reserveTokenSecond)
if (intermediateValue.lte(ethers.constants.Zero)) {
throw Error()
}
// console.log('hi!', amountFormatter(intermediateValue, ))
const calculatedDependentValue = calculateEtherTokenInputFromOutput(
intermediateValue,
reserveTokenFirst,
reserveETHFirst
)
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
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
throw Error()
}
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
}
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
} catch {
setIndependentError(t('insufficientLiquidity'))
}
return () => {
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' })
}
}
}
}
renderSummary(inputError, outputError) {
const { inputValue, inputCurrency, outputValue, outputCurrency, recipient } = this.state
const { t, web3 } = this.props
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()
let contextualInfo = ''
let isError = false
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')
}
}, [
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
)
return (
<ContextualInfo
openDetailsText={t('transactionDetails')}
closeDetailsText={t('hideDetails')}
contextualInfo={contextualInfo}
isError={isError}
renderTransactionDetails={this.renderTransactionDetails}
/>
)
const percentSlippage =
exchangeRate && marketRate
? 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))))
: undefined
const percentSlippageFormatted = percentSlippage && amountFormatter(percentSlippage, 16, 2)
const slippageWarning = percentSlippage && percentSlippage.gte(ethers.utils.parseEther('.1')) // 10%
const isValid = exchangeRate && inputError === null && independentError === null && recipientError === null
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
}
}
const b = text => <span className="swap__highlight-text">{text}</span>
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) {
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(`${percentSlippageFormatted}%`)}.
</div>
</div>
)
......@@ -695,207 +491,229 @@ 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.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(`${percentSlippageFormatted}%`)}.
</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>
<NewContextualInfo
openDetailsText={t('transactionDetails')}
closeDetailsText={t('hideDetails')}
contextualInfo={contextualInfo ? contextualInfo : slippageWarning ? t('slippageWarning') : ''}
allowExpand={!!(inputCurrency && outputCurrency && inputValueParsed && outputValueParsed && recipient.address)}
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
render() {
const { t, selectors, account } = this.props
const { lastEditedField, inputCurrency, outputCurrency, inputValue, outputValue, recipient } = this.state
const estimatedText = `(${t('estimated')})`
let estimate, method, args, value
if (independentField === INPUT) {
ReactGA.event({
category: `${swapType}`,
action: 'TransferInput'
})
const { value: inputBalance, decimals: inputDecimals } = selectors().getBalance(account, inputCurrency)
const { value: outputBalance, decimals: outputDecimals } = selectors().getBalance(account, outputCurrency)
const { inputError, outputError, isValid } = this.validate()
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'
})
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}
selectedTokens={[inputCurrency, outputCurrency]}
selectedTokenAddress={inputCurrency}
value={inputValue}
errorMessage={inputError}
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
<img
onClick={this.flipInputOutput}
className="swap__down-arrow swap__down-arrow--clickable"
alt="arrow"
src={isValid ? ArrowDownBlue : ArrowDownGrey}
/>
</div>
</OversizedPanel>
<CurrencyInputPanel
title={t('output')}
description={lastEditedField === INPUT ? estimatedText : ''}
extraText={this.renderBalance(outputCurrency, outputBalance, outputDecimals)}
onCurrencySelected={outputCurrency => this.setState({ outputCurrency }, this.recalcForm)}
onValueChange={this.updateOutput}
selectedTokens={[inputCurrency, outputCurrency]}
value={outputValue}
selectedTokenAddress={outputCurrency}
errorMessage={outputError}
disableUnlock
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
<img className="swap__down-arrow" src={isValid ? ArrowDownBlue : ArrowDownGrey} alt="arrow" />
</div>
</OversizedPanel>
<AddressInputPanel
t={this.props.t}
value={recipient}
onChange={address => this.setState({ recipient: address })}
/>
{this.renderExchangeRate()}
{this.renderSummary(inputError, outputError)}
<div className="swap__cta-container">
<SendButton callOnClick={this.onSend} isValid={isValid} />
</div>
</>
)
}
}
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
}
}
function SendButton({ callOnClick, isValid }) {
const { t } = useTranslation()
const context = useWeb3Context()
const estimatedGasLimit = await estimate(...args, { value })
method(...args, { value, gasLimit: calculateGasMargin(estimatedGasLimit, GAS_MARGIN) }).then(response => {
addTransaction(response)
})
}
const isActive = context.active && context.account
return (
<button
className={classnames('swap__cta-btn', {
'swap--inactive': !isActive
})}
disabled={!isValid}
onClick={callOnClick}
>
{t('send')}
</button>
<>
<CurrencyInputPanel
title={t('input')}
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={inputValueFormatted}
errorMessage={inputError ? inputError : independentField === INPUT ? independentError : ''}
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
<img
onClick={() => {
dispatchSwapState({ type: 'FLIP_INDEPENDENT' })
}}
className="swap__down-arrow swap__down-arrow--clickable"
alt="swap"
src={isValid ? ArrowDownBlue : ArrowDownGrey}
/>
</div>
</OversizedPanel>
<CurrencyInputPanel
title={t('output')}
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]}
selectedTokenAddress={outputCurrency}
value={outputValueFormatted}
errorMessage={independentField === OUTPUT ? independentError : ''}
disableUnlock
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
<img className="swap__down-arrow" src={isValid ? ArrowDownBlue : ArrowDownGrey} alt="arrow" />
</div>
</OversizedPanel>
<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">
<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()(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 NewContextualInfo from '../../components/ContextualInfoNew'
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 { amountFormatter, calculateGasMargin } from '../../utils'
import { useExchangeContract } from '../../hooks'
import { useTokenDetails } from '../../contexts/Tokens'
import { useTransactionAdder } from '../../contexts/Transactions'
import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances'
import { useAddressAllowance } from '../../contexts/Allowances'
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
}
state = {
inputValue: '',
outputValue: '',
inputCurrency: 'ETH',
outputCurrency: '',
inputAmountB: '',
lastEditedField: ''
}
componentWillMount() {
ReactGA.pageview(window.location.pathname + window.location.search)
}
shouldComponentUpdate(nextProps, nextState) {
return true
}
reset() {
this.setState({
inputValue: '',
outputValue: '',
inputAmountB: '',
lastEditedField: ''
})
}
componentWillReceiveProps() {
this.recalcForm()
}
validate() {
const { selectors, account } = this.props
const { inputValue, outputValue, inputCurrency, outputCurrency } = this.state
const ETH_TO_TOKEN = 0
const TOKEN_TO_ETH = 1
const TOKEN_TO_TOKEN = 2
let inputError = ''
let outputError = ''
let isValid = true
let isUnapproved = this.isUnapproved()
const inputIsZero = BN(inputValue).isZero()
const outputIsZero = BN(outputValue).isZero()
// denominated in bips
const ALLOWED_SLIPPAGE = ethers.utils.bigNumberify(200)
const TOKEN_ALLOWED_SLIPPAGE = ethers.utils.bigNumberify(400)
if (
!inputValue ||
inputIsZero ||
!outputValue ||
outputIsZero ||
!inputCurrency ||
!outputCurrency ||
isUnapproved
) {
isValid = false
}
const { value: inputBalance, decimals: inputDecimals } = selectors().getBalance(account, inputCurrency)
// denominated in seconds
const DEADLINE_FROM_NOW = 60 * 15
if (inputBalance.isLessThan(BN(inputValue * 10 ** inputDecimals))) {
inputError = this.props.t('insufficientBalance')
}
if (inputValue === 'N/A') {
inputError = this.props.t('inputNotValid')
}
// denominated in bips
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
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 {
inputError,
outputError,
isValid: isValid && !inputError && !outputError
minimum: minimum.lt(ethers.constants.Zero) ? ethers.constants.Zero : minimum,
maximum: maximum.gt(ethers.constants.MaxUint256) ? ethers.constants.MaxUint256 : maximum
}
} else {
return {}
}
}
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()
)
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
}
}
isUnapproved() {
const { account, exchangeAddresses, selectors } = this.props
const { inputCurrency, inputValue } = this.state
// 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 (!inputCurrency || inputCurrency === 'ETH') {
return false
}
// 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)
}
const { value: allowance, label, decimals } = selectors().getApprovals(
inputCurrency,
account,
exchangeAddresses.fromToken[inputCurrency]
)
const initialSwapState = {
independentValue: '', // this is a user input
dependentValue: '', // this is a calculated number
independentField: INPUT,
inputCurrency: 'ETH',
outputCurrency: ''
}
if (label && allowance.isLessThan(BN(inputValue * 10 ** decimals || 0))) {
return true
function swapStateReducer(state, action) {
switch (action.type) {
case 'FLIP_INDEPENDENT': {
const { independentField, inputCurrency, outputCurrency } = state
return {
...state,
dependentValue: '',
independentField: independentField === INPUT ? OUTPUT : INPUT,
inputCurrency: outputCurrency,
outputCurrency: inputCurrency
}
}
return false
}
recalcForm() {
const { inputCurrency, outputCurrency, lastEditedField } = this.state
if (!inputCurrency || !outputCurrency) {
return
case 'SELECT_CURRENCY': {
const { inputCurrency, outputCurrency } = state
const { field, currency } = action.payload
const newInputCurrency = field === INPUT ? currency : inputCurrency
const newOutputCurrency = field === OUTPUT ? currency : outputCurrency
if (newInputCurrency === newOutputCurrency) {
return {
...state,
inputCurrency: field === INPUT ? currency : '',
outputCurrency: field === OUTPUT ? currency : ''
}
} else {
return {
...state,
inputCurrency: newInputCurrency,
outputCurrency: newOutputCurrency
}
}
}
const editedValue = lastEditedField === INPUT ? this.state.inputValue : this.state.outputValue
if (BN(editedValue).isZero()) {
return
case 'UPDATE_INDEPENDENT': {
const { field, value } = action.payload
const { dependentValue, independentValue } = state
return {
...state,
independentValue: value,
dependentValue: value === independentValue ? dependentValue : '',
independentField: field
}
}
if (inputCurrency === outputCurrency) {
this.setState({
inputValue: '',
outputValue: ''
})
return
case 'UPDATE_DEPENDENT': {
return {
...state,
dependentValue: action.payload
}
}
if (inputCurrency !== 'ETH' && outputCurrency !== 'ETH') {
this.recalcTokenTokenForm()
return
default: {
return initialSwapState
}
this.recalcEthTokenForm()
}
}
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
}
if (outputValue !== oldOutputValue) {
appendState.outputValue = outputValue
function getExchangeRate(inputValue, inputDecimals, outputValue, outputDecimals, invert = false) {
try {
if (
inputValue &&
(inputDecimals || inputDecimals === 0) &&
outputValue &&
(outputDecimals || outputDecimals === 0)
) {
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)))
}
this.setState(appendState)
}
} 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, 18, outputReserveToken, outputDecimals, invert)
} else if (swapType === TOKEN_TO_ETH) {
return getExchangeRate(inputReserveToken, inputDecimals, inputReserveETH, 18, invert)
} else if (swapType === TOKEN_TO_TOKEN) {
const factor = ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18))
const firstRate = getExchangeRate(inputReserveToken, inputDecimals, inputReserveETH, 18)
const secondRate = getExchangeRate(outputReserveETH, 18, 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 = useTransactionAdder()
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)
if (parsedValue.lte(ethers.constants.Zero) || parsedValue.gte(ethers.constants.MaxUint256)) {
throw Error()
} else {
setIndependentValueParsed(parsedValue)
setIndependentError(null)
}
} catch {
setIndependentError(t('inputNotValid'))
}
this.setState(appendState)
}
}
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
}
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)
})
return () => {
setIndependentValueParsed()
setIndependentError()
}
}
}, [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)
}
}
}, [independentField, independentValueParsed, dependentValueMaximum, inputBalance, inputCurrency, inputAllowance, t])
this.setState(appendState)
} else if (lastEditedField === OUTPUT) {
if (!oldOutputValue) {
return this.setState({
inputValue: '',
exchangeRate: BN(0)
})
}
// calculate dependent value
useEffect(() => {
const amount = independentValueParsed
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))
if (swapType === ETH_TO_TOKEN) {
const reserveETH = outputReserveETH
const reserveToken = outputReserveToken
const appendState = {}
if (amount && reserveETH && reserveToken) {
try {
const calculatedDependentValue =
independentField === INPUT
? calculateEtherTokenOutputFromInput(amount, reserveETH, reserveToken)
: calculateEtherTokenInputFromOutput(amount, reserveETH, reserveToken)
if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) {
appendState.exchangeRate = exchangeRate
}
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
throw Error()
}
if (inputValue !== oldInputValue) {
appendState.inputValue = inputValue
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
} catch {
setIndependentError(t('insufficientLiquidity'))
}
return () => {
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' })
}
}
} else if (swapType === TOKEN_TO_ETH) {
const reserveETH = inputReserveETH
const reserveToken = inputReserveToken
if (amount && reserveETH && reserveToken) {
try {
const calculatedDependentValue =
independentField === INPUT
? calculateEtherTokenOutputFromInput(amount, reserveToken, reserveETH)
: calculateEtherTokenInputFromOutput(amount, reserveToken, reserveETH)
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
throw Error()
}
this.setState(appendState)
}
}
updateInput = amount => {
this.setState(
{
inputValue: amount,
lastEditedField: INPUT
},
this.recalcForm
)
}
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
try {
deadline = await retry(() => getBlockDeadline(web3, 600))
} catch (e) {
// TODO: Handle error.
return
}
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
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
} catch {
setIndependentError(t('insufficientLiquidity'))
}
return () => {
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' })
}
}
}
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()
}
}
} else if (swapType === TOKEN_TO_TOKEN) {
const reserveETHFirst = inputReserveETH
const reserveTokenFirst = inputReserveToken
const reserveETHSecond = outputReserveETH
const reserveTokenSecond = outputReserveToken
if (amount && reserveETHFirst && reserveTokenFirst && reserveETHSecond && reserveTokenSecond) {
try {
if (independentField === INPUT) {
const intermediateValue = calculateEtherTokenOutputFromInput(amount, reserveTokenFirst, reserveETHFirst)
if (intermediateValue.lte(ethers.constants.Zero)) {
throw Error()
}
const calculatedDependentValue = calculateEtherTokenOutputFromInput(
intermediateValue,
reserveETHSecond,
reserveTokenSecond
)
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
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
throw Error()
}
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
} else {
const intermediateValue = calculateEtherTokenInputFromOutput(amount, reserveETHSecond, reserveTokenSecond)
if (intermediateValue.lte(ethers.constants.Zero)) {
throw Error()
}
const calculatedDependentValue = calculateEtherTokenInputFromOutput(
intermediateValue,
reserveTokenFirst,
reserveETHFirst
)
.send({ from: account }, (err, data) => {
if (!err) {
addPendingTx(data)
this.reset()
}
})
break
case 'TOKEN_TO_TOKEN':
if (!inputAmountB) {
return
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
throw Error()
}
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
}
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
} catch {
setIndependentError(t('insufficientLiquidity'))
}
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')
}
if (inputError || outputError) {
contextualInfo = inputError || outputError
isError = true
}
if (inputIsZero || outputIsZero) {
contextualInfo = t('noLiquidity')
}
if (this.isUnapproved()) {
contextualInfo = t('unlockTokenCont')
}
if (!account) {
contextualInfo = t('noWallet')
isError = true
}
}, [
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
)
return (
<ContextualInfo
openDetailsText={t('transactionDetails')}
closeDetailsText={t('hideDetails')}
contextualInfo={contextualInfo}
isError={isError}
renderTransactionDetails={this.renderTransactionDetails}
/>
)
const percentSlippage =
exchangeRate && marketRate
? 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))))
: undefined
const percentSlippageFormatted = percentSlippage && amountFormatter(percentSlippage, 16, 2)
const slippageWarning = percentSlippage && percentSlippage.gte(ethers.utils.parseEther('.1')) // 10%
const isValid = exchangeRate && inputError === null && independentError === null
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
}
}
const b = text => <span className="swap__highlight-text">{text}</span>
if (lastEditedField === INPUT) {
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(`${percentSlippageFormatted}%`)}.
</div>
</div>
)
......@@ -677,196 +486,205 @@ 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(`${percentSlippageFormatted}%`)}.
</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>
<NewContextualInfo
openDetailsText={t('transactionDetails')}
closeDetailsText={t('hideDetails')}
contextualInfo={contextualInfo ? contextualInfo : slippageWarning ? t('slippageWarning') : ''}
allowExpand={!!(inputCurrency && outputCurrency && inputValueParsed && outputValueParsed)}
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
}
}
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}
selectedTokens={[inputCurrency, outputCurrency]}
selectedTokenAddress={inputCurrency}
value={inputValue}
errorMessage={inputError}
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
<img
onClick={this.flipInputOutput}
className="swap__down-arrow swap__down-arrow--clickable"
alt="swap"
src={isValid ? ArrowDownBlue : ArrowDownGrey}
/>
</div>
</OversizedPanel>
<CurrencyInputPanel
title={t('output')}
description={lastEditedField === INPUT ? estimatedText : ''}
extraText={this.renderBalance(outputCurrency, outputBalance, outputDecimals)}
onCurrencySelected={outputCurrency => this.setState({ outputCurrency }, this.recalcForm)}
onValueChange={this.updateOutput}
selectedTokens={[inputCurrency, outputCurrency]}
value={outputValue}
selectedTokenAddress={outputCurrency}
errorMessage={outputError}
disableUnlock
/>
{this.renderExchangeRate()}
{this.renderSummary(inputError, outputError)}
<div className="swap__cta-container">
<SwapButton callOnClick={this.onSwap} isValid={isValid} />
</div>
</>
)
const estimatedGasLimit = await estimate(...args, { value })
method(...args, { value, gasLimit: calculateGasMargin(estimatedGasLimit, GAS_MARGIN) }).then(response => {
addTransaction(response)
})
}
}
function SwapButton({ callOnClick, isValid }) {
const { t } = useTranslation()
const context = useWeb3Context()
const isActive = context.active && context.account
return (
<button
className={classnames('swap__cta-btn', { 'swap--inactive': !isActive })}
disabled={!isValid}
onClick={callOnClick}
>
{t('swap')}
</button>
<>
<CurrencyInputPanel
title={t('input')}
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={inputValueFormatted}
errorMessage={inputError ? inputError : independentField === INPUT ? independentError : ''}
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
<img
onClick={() => {
dispatchSwapState({ type: 'FLIP_INDEPENDENT' })
}}
className="swap__down-arrow swap__down-arrow--clickable"
alt="swap"
src={isValid ? ArrowDownBlue : ArrowDownGrey}
/>
</div>
</OversizedPanel>
<CurrencyInputPanel
title={t('output')}
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]}
selectedTokenAddress={outputCurrency}
value={outputValueFormatted}
errorMessage={independentField === OUTPUT ? independentError : ''}
disableUnlock
/>
<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 store from './store.dev'
export default store
import { applyMiddleware, compose, createStore } from 'redux'
import thunk from 'redux-thunk'
import initialState from './initial-state'
import reducer from '../ducks'
const middleware = [thunk]
const enhancers = []
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const store = createStore(reducer, initialState, composeEnhancers(applyMiddleware(...middleware), ...enhancers))
export default store
import React from 'react'
import { ThemeProvider as StyledComponentsThemeProvider, createGlobalStyle } from 'styled-components'
const theme = {
uniswapPink: '#DC6BE5',
royalBlue: '#2f80ed',
salmonRed: '#ff6871',
white: '#FFF',
black: '#000'
}
export default function ThemeProvider({ children }) {
return <StyledComponentsThemeProvider theme={theme}>{children}</StyledComponentsThemeProvider>
}
export const GlobalStyle = createGlobalStyle`
@import url('https://rsms.me/inter/inter.css');
html,
body {
padding: 0;
margin: 0;
font-family: Inter, sans-serif;
font-variant: none;
font-size: 16px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
}
#root {
position: relative;
display: flex;
flex-flow: column nowrap;
height: 100vh;
width: 100vw;
overflow-x: hidden;
overflow-y: auto;
background-color: ${props => props.theme.white};
z-index: 100;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
@media only screen and (min-width: 768px) {
justify-content: center;
align-items: center;
}
}
#modal-root {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 200;
}
.loader {
border: 1px solid transparent;
border-top: 1px solid ${props => props.theme.royalBlue};
border-radius: 50%;
width: 0.75rem;
height: 0.75rem;
margin-right: 0.25rem;
animation: spin 1s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
`
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,55 +14,195 @@ export const errorCodes = ['TOKEN_DETAILS_DECIMALS', 'TOKEN_DETAILS_SYMBOL'].red
{}
)
function getFactoryContract(networkId, signerOrProvider) {
return getContract(factoryAddresses[networkId], FACTORY_ABI, signerOrProvider)
export function safeAccess(object, path) {
return object
? path.reduce(
(accumulator, currentValue) => (accumulator && accumulator[currentValue] ? accumulator[currentValue] : null),
object
)
: null
}
export function isAddress(value) {
try {
ethers.utils.getAddress(value)
return true
return ethers.utils.getAddress(value.toLowerCase())
} catch {
return false
}
}
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, library, account)
}
// account is optional
export function getExchangeContract(exchangeAddress, library, account) {
return getContract(exchangeAddress, EXCHANGE_ABI, library, account)
}
export async function getTokenDetails(tokenAddress, signerOrProvider) {
const contract = getContract(tokenAddress, ERC20_ABI, signerOrProvider)
// 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
throw error
})
const symbolPromise = contract
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
})
}
// 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
})
}
// get the exchange address for a token from the factory
export async function getTokenExchangeAddressFromFactory(tokenAddress, networkId, library) {
return getFactoryContract(networkId, library).getExchange(tokenAddress)
}
// 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)
}
return Promise.all([decimalsPromise, symbolPromise]).then(([decimals, symbol]) => ({
decimals,
symbol,
tokenAddress
}))
// 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)
}
export async function getExchangeDetails(networkId, tokenAddress, signerOrProvider) {
const factoryContract = getFactoryContract(networkId, signerOrProvider)
// 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 factoryContract.getExchange(tokenAddress).then(exchangeAddress => ({ exchangeAddress, tokenAddress }))
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}.`)
}
// 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