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

Swap Migration (#277)

* migrate core logic

* finalize swap migration

* upgrade react-scripts and fix errors/warnings

* finalize swap, modularize currency input

* remove console.logs

* copy swap logic for send

* remove unnecessary variable

* tighten caching logic

* snappier ens integration
parent 9757b3e9
......@@ -22,6 +22,7 @@
"enterValueCont": "Enter a {{ missingCurrencyValue }} value to continue.",
"selectTokenCont": "Select a token to continue.",
"noLiquidity": "No liquidity.",
"insufficientLiquidity": "Insufficient liquidity.",
"unlockTokenCont": "Please unlock token to continue.",
"transactionDetails": "Transaction Details",
"hideDetails": "Hide Details",
......@@ -70,7 +71,9 @@
"invalidDecimals": "Invalid decimals",
"tokenAddress": "Token Address",
"label": "Label",
"name": "Name",
"symbol": "Symbol",
"decimals": "Decimals",
"enterTokenCont": "Enter a token address to continue"
"enterTokenCont": "Enter a token address to continue",
"priceChange": "This trade will cause the price to change by"
}
......@@ -3,7 +3,7 @@
"constant": true,
"inputs": [],
"name": "name",
"outputs": [{ "name": "", "type": "string" }],
"outputs": [{ "name": "", "type": "bytes32" }],
"payable": false,
"stateMutability": "view",
"type": "function"
......
import React from 'react'
import React, { useState, useEffect } from 'react'
import classnames from 'classnames'
import { useTranslation } from 'react-i18next'
import { useWeb3Context } from 'web3-react'
// import QrCode from '../QrCode' // commented out pending further review
import './address-input-panel.scss'
import { isAddress } from '../../utils'
export default function AddressInputPanel({ title, onChange = () => {}, value = '', errorMessage }) {
export default function AddressInputPanel({ title, onChange = () => {}, onError = () => {} }) {
const { t } = useTranslation()
const { library } = useWeb3Context()
const [input, setInput] = useState('')
const [data, setData] = useState({ address: undefined, name: undefined })
const [error, setError] = useState(false)
// keep stuff in sync
useEffect(() => {
onChange({ address: data.address, name: data.name })
}, [onChange, data.address, data.name])
useEffect(() => {
onError(error)
}, [onError, error])
useEffect(() => {
let stale = false
if (isAddress(input)) {
library.lookupAddress(input).then(name => {
if (!stale) {
// if an ENS name exists, set it as the destination
if (name) {
setInput(name)
} else {
setData({ address: input, name: '' })
setError(null)
}
}
})
} else {
try {
library.resolveName(input).then(address => {
if (!stale) {
// if the input name resolves to an address
if (address) {
setData({ address: address, name: input })
setError(null)
} else {
setError(true)
}
}
})
} catch {
setError(true)
}
}
return () => {
stale = true
setData({ address: undefined, name: undefined })
setError()
}
}, [input, library, onChange, onError])
return (
<div className="currency-input-panel">
<div
className={classnames('currency-input-panel__container address-input-panel__recipient-row', {
'currency-input-panel__container--error': errorMessage
'currency-input-panel__container--error': input !== '' && error
})}
>
<div className="address-input-panel__input-container">
......@@ -26,11 +82,11 @@ export default function AddressInputPanel({ title, onChange = () => {}, value =
<input
type="text"
className={classnames('address-input-panel__input', {
'address-input-panel__input--error': errorMessage
'address-input-panel__input--error': input !== '' && error
})}
placeholder="0x1234..."
onChange={e => onChange(e.target.value)}
value={value}
onChange={e => setInput(e.target.value)}
value={input}
/>
</div>
</div>
......
......@@ -45,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()}
</>
)
}
}
......
......@@ -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);
}
......
This diff is collapsed.
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import React, { useState } from 'react'
import { useWeb3Context } from 'web3-react'
import classnames from 'classnames'
import Jazzicon from 'jazzicon'
import { CSSTransitionGroup } from 'react-transition-group'
import { withTranslation } from 'react-i18next'
import { useTranslation } from 'react-i18next'
import { ethers } from 'ethers'
import Modal from '../Modal'
import { useAllTransactions } from '../../contexts/Transaction'
import './web3-status.scss'
......@@ -15,19 +15,47 @@ function getEtherscanLink(tx) {
return `https://etherscan.io/tx/${tx}`
}
class Web3Status extends Component {
state = {
isShowingModal: false
function getPendingText(pendingTransactions, pendingLabel) {
return (
<div className="web3-status__pending-container">
<div className="loader" />
<span key="text">
{pendingTransactions.length} {pendingLabel}
</span>
</div>
)
}
function getText(text, disconnectedText) {
if (!text || text.length < 42 || !ethers.utils.isHexString(text)) {
return disconnectedText
}
handleClick = () => {
if (this.props.pending.length && !this.state.isShowingModal) {
this.setState({ isShowingModal: true })
const address = ethers.utils.getAddress(text)
return `${address.substring(0, 6)}...${address.substring(38)}`
}
export default function Web3Status() {
const { t } = useTranslation()
const { active, account } = useWeb3Context()
const allTransactions = useAllTransactions()
const pending = Object.keys(allTransactions).filter(t => allTransactions[t].completed === false)
const confirmed = Object.keys(allTransactions).filter(t => allTransactions[t].completed === true)
const hasPendingTransactions = !!pending.length
const hasConfirmedTransactions = !!confirmed.length
const [isShowingModal, setIsShowingModal] = useState(false)
function handleClick() {
if (pending.length && !isShowingModal) {
setIsShowingModal(true)
}
}
renderPendingTransactions() {
return this.props.pending.map(transaction => {
function renderPendingTransactions() {
return pending.map(transaction => {
return (
<div
key={transaction}
......@@ -36,20 +64,20 @@ class Web3Status extends Component {
>
<div className="pending-modal__transaction-label">{transaction}</div>
<div className="pending-modal__pending-indicator">
<div className="loader" /> {this.props.t('pending')}
<div className="loader" /> {t('pending')}
</div>
</div>
)
})
}
renderModal() {
if (!this.state.isShowingModal) {
function renderModal() {
if (!isShowingModal) {
return null
}
return (
<Modal onClose={() => this.setState({ isShowingModal: false })}>
<Modal onClose={() => setIsShowingModal(false)}>
<CSSTransitionGroup
transitionName="token-modal"
transitionAppear={true}
......@@ -61,7 +89,7 @@ class Web3Status extends Component {
<div className="pending-modal">
<div className="pending-modal__transaction-list">
<div className="pending-modal__header">Transactions</div>
{this.renderPendingTransactions()}
{renderPendingTransactions()}
</div>
</div>
</CSSTransitionGroup>
......@@ -69,78 +97,30 @@ class Web3Status extends Component {
)
}
render() {
const { t, address, pending, confirmed } = this.props
const hasPendingTransactions = !!pending.length
const hasConfirmedTransactions = !!confirmed.length
return (
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), 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))
export const FACTORY_ADDRESSES = {
1: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95',
4: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36'
}
import React, { Component, createContext, useContext, useEffect } from 'react'
import { useWeb3Context } from 'web3-react'
import { useBlockEffect } from '../hooks'
const ApplicationContext = createContext()
export default class Provider extends Component {
constructor(props) {
super(props)
this.dismissBetaMessage = () => {
this.setState({ showBetaMessage: false })
}
this.updateBlockNumber = blockNumber => {
this.setState({ blockNumber })
}
this.state = {
showBetaMessage: true,
dismissBetaMessage: this.dismissBetaMessage,
blockNumber: undefined,
updateBlockNumber: this.updateBlockNumber
}
}
render() {
return <ApplicationContext.Provider value={this.state}>{this.props.children}</ApplicationContext.Provider>
}
}
export function useApplicationContext() {
return useContext(ApplicationContext)
}
export function Updater() {
const { library } = useWeb3Context()
const { updateBlockNumber } = useApplicationContext()
// fetch the block number once on load...
useEffect(() => {
if (library) {
let stale = false
library
.getBlockNumber()
.then(blockNumber => {
if (!stale) {
updateBlockNumber(blockNumber)
}
})
.catch(() => {
if (!stale) {
updateBlockNumber(null)
}
})
return () => {
stale = true
// this clears block number on network change because the library has changed
updateBlockNumber(undefined)
}
}
}, [library, updateBlockNumber])
// ...and every block...
useBlockEffect(updateBlockNumber)
return null
}
export function useBlockNumber() {
const { blockNumber } = useApplicationContext()
return blockNumber
}
import React, { Component, createContext, useContext, useEffect } from 'react'
import { useWeb3Context } from 'web3-react'
import merge from 'lodash.merge'
import { getEtherBalance, getTokenBalance, getTokenAllowance, isAddress } from '../utils'
import { useBlockNumber } from './Application'
import { useTokenDetails } from './Static'
// define constants
const BALANCE = 'balance'
const ALLOWANCE = 'allowance'
// node creation
function createAddressValueNode(name, value, blockNumber) {
return { [name]: value, blockNumber }
}
// tree creation
function createAddressBalanceTree(address, tokenAddress, value, blockNumber) {
return { [address]: { [tokenAddress]: createAddressValueNode(BALANCE, value, blockNumber) } }
}
function createAddressAllowanceTree(address, tokenAddress, spenderAddress, value, blockNumber) {
return {
[address]: { [tokenAddress]: { [spenderAddress]: createAddressValueNode(ALLOWANCE, value, blockNumber) } }
}
}
// create contexts
const AddressBalanceContext = createContext()
const AddressAllowanceContext = createContext()
// define providers
class AddressBalanceContextProvider extends Component {
constructor(props) {
super(props)
this.getValue = (address, tokenAddress) => {
return this.state[BALANCE][address] && this.state[BALANCE][address][tokenAddress]
? this.state[BALANCE][address][tokenAddress]
: createAddressValueNode(BALANCE)
}
this.updateValue = (address, tokenAddress, value, blockNumber) => {
this.setState(state => ({
[BALANCE]: merge(state[BALANCE], createAddressBalanceTree(address, tokenAddress, value, blockNumber))
}))
}
this.clearValues = () => {
this.setState({ [BALANCE]: {} })
}
this.state = {
[BALANCE]: {},
getValue: this.getValue,
updateValue: this.updateValue,
clearValues: this.clearValues
}
}
render() {
return <AddressBalanceContext.Provider value={this.state}>{this.props.children}</AddressBalanceContext.Provider>
}
}
class AddressAllowanceContextProvider extends Component {
constructor(props) {
super(props)
this.getValue = (address, tokenAddress, spenderAddress) => {
return this.state[ALLOWANCE][address] &&
this.state[ALLOWANCE][address][tokenAddress] &&
this.state[ALLOWANCE][address][tokenAddress][spenderAddress]
? this.state[ALLOWANCE][address][tokenAddress][spenderAddress]
: createAddressValueNode(ALLOWANCE)
}
this.updateValue = (address, tokenAddress, spenderAddress, value, blockNumber) => {
this.setState(state => ({
[ALLOWANCE]: merge(
state[ALLOWANCE],
createAddressAllowanceTree(address, tokenAddress, spenderAddress, value, blockNumber)
)
}))
}
this.clearValues = () => {
this.setState({ [ALLOWANCE]: {} })
}
this.state = {
[ALLOWANCE]: {},
getValue: this.getValue,
updateValue: this.updateValue,
clearValues: this.clearValues
}
}
render() {
return <AddressAllowanceContext.Provider value={this.state}>{this.props.children}</AddressAllowanceContext.Provider>
}
}
export default function Provider({ children }) {
return (
<AddressBalanceContextProvider>
<AddressAllowanceContextProvider>{children}</AddressAllowanceContextProvider>
</AddressBalanceContextProvider>
)
}
// define useContext wrappers
function useAddressBalanceContext() {
return useContext(AddressBalanceContext)
}
function useAddressAllowanceContext() {
return useContext(AddressAllowanceContext)
}
export function Updater() {
const { networkId } = useWeb3Context()
const { clearValues: clearValuesBalance } = useAddressBalanceContext()
const { clearValues: clearValuesAllowance } = useAddressAllowanceContext()
useEffect(() => {
return () => {
clearValuesBalance()
clearValuesAllowance()
}
}, [clearValuesBalance, clearValuesAllowance, networkId])
return null
}
// define custom hooks
export function useAddressBalance(address, tokenAddress) {
const { library } = useWeb3Context()
const globalBlockNumber = useBlockNumber()
const { getValue, updateValue } = useAddressBalanceContext()
const { [BALANCE]: balance, blockNumber: balanceUpdatedBlockNumber } = getValue(address, tokenAddress)
useEffect(() => {
// gate this entire effect by checking that the inputs are valid
if (isAddress(address) && (tokenAddress === 'ETH' || isAddress(tokenAddress))) {
// if they are, and the balance is undefined or stale, fetch it
if (balance === undefined || balanceUpdatedBlockNumber !== globalBlockNumber) {
let stale = false
;(tokenAddress === 'ETH' ? getEtherBalance(address, library) : getTokenBalance(tokenAddress, address, library))
.then(value => {
if (!stale) {
updateValue(address, tokenAddress, value, globalBlockNumber)
}
})
.catch(() => {
if (!stale) {
updateValue(address, tokenAddress, null, globalBlockNumber)
}
})
return () => {
stale = true
}
}
}
}, [address, tokenAddress, balance, balanceUpdatedBlockNumber, globalBlockNumber, library, updateValue])
return balance
}
export function useAddressAllowance(address, tokenAddress, spenderAddress) {
const { library } = useWeb3Context()
const globalBlockNumber = useBlockNumber()
const { getValue, updateValue } = useAddressAllowanceContext()
const { [ALLOWANCE]: allowance, blockNumber: allowanceUpdatedBlockNumber } = getValue(
address,
tokenAddress,
spenderAddress
)
useEffect(() => {
// gate this entire effect by checking that the inputs are valid
if (isAddress(address) && isAddress(tokenAddress) && isAddress(spenderAddress)) {
// if they are, and the balance is undefined or stale, fetch it
if (allowance === undefined || allowanceUpdatedBlockNumber !== globalBlockNumber) {
let stale = false
getTokenAllowance(address, tokenAddress, spenderAddress, library)
.then(value => {
if (!stale) {
updateValue(address, tokenAddress, spenderAddress, value, globalBlockNumber)
}
})
.catch(() => {
if (!stale) {
updateValue(address, tokenAddress, spenderAddress, null, globalBlockNumber)
}
})
return () => {
stale = true
}
}
}
}, [
address,
tokenAddress,
spenderAddress,
allowance,
allowanceUpdatedBlockNumber,
globalBlockNumber,
library,
updateValue
])
return allowance
}
export function useExchangeReserves(tokenAddress) {
const { exchangeAddress } = useTokenDetails(tokenAddress)
const reserveETH = useAddressBalance(exchangeAddress, 'ETH')
const reserveToken = useAddressBalance(exchangeAddress, tokenAddress)
return { reserveETH, reserveToken }
}
This diff is collapsed.
import React, { Component, createContext, useContext, useCallback, useEffect } from 'react'
import { useWeb3Context } from 'web3-react'
import { ethers } from 'ethers'
import merge from 'lodash.merge'
import { useBlockEffect } from '../hooks'
const TRANSACTION = 'transaction'
const RESPONSE = 'response'
const COMPLETED = 'completed'
const RECEIPT = 'receipt'
const TransactionContext = createContext()
function removeUndefinedValues(o) {
return Object.keys(o)
.filter(k => o[k] !== undefined)
.reduce((innerO, k) => {
innerO[k] = o[k]
return innerO
}, {})
}
function createTransactionNode(response, completed, receipt, noUndefinedValues) {
const node = { [RESPONSE]: response, [COMPLETED]: completed, [RECEIPT]: receipt }
return noUndefinedValues ? removeUndefinedValues(node) : node
}
// tree creation
function createTokenDetailTree(hash, response, completed, receipt, noUndefinedValues = false) {
return { [hash]: createTransactionNode(response, completed, receipt, noUndefinedValues) }
}
export default class Provider extends Component {
constructor(props) {
super(props)
this.getTransactions = () => {
return this.state[TRANSACTION]
}
this.addTransaction = (hash, response) => {
this.setState(state => ({
[TRANSACTION]: merge(state[TRANSACTION], createTokenDetailTree(hash, response, false))
}))
}
this.updateTransaction = (hash, receipt) => {
this.setState(state => ({
[TRANSACTION]: merge(state[TRANSACTION], createTokenDetailTree(hash, undefined, true, receipt, true))
}))
}
this.clearTransactions = () => {
this.setState({ [TRANSACTION]: {} })
}
this.state = {
[TRANSACTION]: {},
getTransactions: this.getTransactions,
addTransaction: this.addTransaction,
updateTransaction: this.updateTransaction,
clearTransactions: this.clearTransactions
}
}
render() {
return <TransactionContext.Provider value={this.state}>{this.props.children}</TransactionContext.Provider>
}
}
export function useTransactionContext() {
return useContext(TransactionContext)
}
export function Updater() {
const { library, networkId } = useWeb3Context()
const { getTransactions, updateTransaction, clearTransactions } = useTransactionContext()
useEffect(() => {
return () => {
clearTransactions()
}
}, [clearTransactions, networkId])
const updateTransactionHashes = useCallback(() => {
if (library) {
const transactions = getTransactions()
Object.keys(transactions)
.filter(k => !transactions[k][COMPLETED])
.forEach(hash => {
library.getTransactionReceipt(hash).then(receipt => {
if (receipt) {
updateTransaction(hash, receipt)
}
})
})
}
}, [library, getTransactions, updateTransaction])
useBlockEffect(updateTransactionHashes)
return null
}
export function useAllTransactions() {
const { getTransactions } = useTransactionContext()
return getTransactions()
}
export function usePendingApproval(tokenAddress) {
const allTransactions = useAllTransactions()
return (
Object.keys(allTransactions).filter(hash => {
const transaction = allTransactions[hash]
if (
transaction.completed ||
transaction.response.to !== tokenAddress ||
transaction.response.data.substring(0, 10) !== ethers.utils.id('approve(address,uint256)').substring(0, 10)
) {
return false
} else {
return true
}
}).length >= 1
)
}
import { BigNumber as BN } from 'bignumber.js'
import Web3 from 'web3'
import ERC20_ABI from '../abi/erc20'
import ERC20_WITH_BYTES_ABI from '../abi/erc20_symbol_bytes32'
import ERC20_WITH_BYTES_ABI from '../abi/erc20_bytes32'
export const INITIALIZE = 'web3connect/initialize'
export const INITIALIZE_WEB3 = 'web3connect/initializeWeb3'
......
export default function(matchmask = [], minMatchCharLength = 1) {
let matchedIndices = []
let start = -1
let end = -1
let i = 0
for (let len = matchmask.length; i < len; i += 1) {
let match = matchmask[i]
if (match && start === -1) {
start = i
} else if (!match && start !== -1) {
end = i - 1
if (end - start + 1 >= minMatchCharLength) {
matchedIndices.push([start, end])
}
start = -1
}
}
// (i-1 - start) + 1 => i - start
if (matchmask[i - 1] && i - start >= minMatchCharLength) {
matchedIndices.push([start, i - 1])
}
return matchedIndices
}
export default function(pattern) {
let mask = {}
let len = pattern.length
for (let i = 0; i < len; i += 1) {
mask[pattern.charAt(i)] = 0
}
for (let i = 0; i < len; i += 1) {
mask[pattern.charAt(i)] |= 1 << (len - i - 1)
}
return mask
}
// eslint-disable-next-line no-useless-escape
const SPECIAL_CHARS_REGEX = /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g
export default function(text, pattern, tokenSeparator = / +/g) {
let regex = new RegExp(pattern.replace(SPECIAL_CHARS_REGEX, '\\$&').replace(tokenSeparator, '|'))
let matches = text.match(regex)
let isMatch = !!matches
let matchedIndices = []
if (isMatch) {
for (let i = 0, matchesLen = matches.length; i < matchesLen; i += 1) {
let match = matches[i]
matchedIndices.push([text.indexOf(match), match.length - 1])
}
}
return {
// TODO: revisit this score
score: isMatch ? 0.5 : 1,
isMatch,
matchedIndices
}
}
export default function(pattern, { errors = 0, currentLocation = 0, expectedLocation = 0, distance = 100 }) {
const accuracy = errors / pattern.length
const proximity = Math.abs(expectedLocation - currentLocation)
if (!distance) {
// Dodge divide by zero error.
return proximity ? 1.0 : accuracy
}
return accuracy + proximity / distance
}
import bitapScore from './bitap_score'
import matchedIndices from './bitap_matched_indices'
export default function(
text,
pattern,
patternAlphabet,
{ location = 0, distance = 100, threshold = 0.6, findAllMatches = false, minMatchCharLength = 1 }
) {
const expectedLocation = location
// Set starting location at beginning text and initialize the alphabet.
const textLen = text.length
// Highest score beyond which we give up.
let currentThreshold = threshold
// Is there a nearby exact match? (speedup)
let bestLocation = text.indexOf(pattern, expectedLocation)
const patternLen = pattern.length
// a mask of the matches
const matchMask = []
for (let i = 0; i < textLen; i += 1) {
matchMask[i] = 0
}
if (bestLocation !== -1) {
let score = bitapScore(pattern, {
errors: 0,
currentLocation: bestLocation,
expectedLocation,
distance
})
currentThreshold = Math.min(score, currentThreshold)
// What about in the other direction? (speed up)
bestLocation = text.lastIndexOf(pattern, expectedLocation + patternLen)
if (bestLocation !== -1) {
let score = bitapScore(pattern, {
errors: 0,
currentLocation: bestLocation,
expectedLocation,
distance
})
currentThreshold = Math.min(score, currentThreshold)
}
}
// Reset the best location
bestLocation = -1
let lastBitArr = []
let finalScore = 1
let binMax = patternLen + textLen
const mask = 1 << (patternLen - 1)
for (let i = 0; i < patternLen; i += 1) {
// Scan for the best match; each iteration allows for one more error.
// Run a binary search to determine how far from the match location we can stray
// at this error level.
let binMin = 0
let binMid = binMax
while (binMin < binMid) {
const score = bitapScore(pattern, {
errors: i,
currentLocation: expectedLocation + binMid,
expectedLocation,
distance
})
if (score <= currentThreshold) {
binMin = binMid
} else {
binMax = binMid
}
binMid = Math.floor((binMax - binMin) / 2 + binMin)
}
// Use the result from this iteration as the maximum for the next.
binMax = binMid
let start = Math.max(1, expectedLocation - binMid + 1)
let finish = findAllMatches ? textLen : Math.min(expectedLocation + binMid, textLen) + patternLen
// Initialize the bit array
let bitArr = Array(finish + 2)
bitArr[finish + 1] = (1 << i) - 1
for (let j = finish; j >= start; j -= 1) {
let currentLocation = j - 1
let charMatch = patternAlphabet[text.charAt(currentLocation)]
if (charMatch) {
matchMask[currentLocation] = 1
}
// First pass: exact match
bitArr[j] = ((bitArr[j + 1] << 1) | 1) & charMatch
// Subsequent passes: fuzzy match
if (i !== 0) {
bitArr[j] |= ((lastBitArr[j + 1] | lastBitArr[j]) << 1) | 1 | lastBitArr[j + 1]
}
if (bitArr[j] & mask) {
finalScore = bitapScore(pattern, {
errors: i,
currentLocation,
expectedLocation,
distance
})
// This match will almost certainly be better than any existing match.
// But check anyway.
if (finalScore <= currentThreshold) {
// Indeed it is
currentThreshold = finalScore
bestLocation = currentLocation
// Already passed `loc`, downhill from here on in.
if (bestLocation <= expectedLocation) {
break
}
// When passing `bestLocation`, don't exceed our current distance from `expectedLocation`.
start = Math.max(1, 2 * expectedLocation - bestLocation)
}
}
}
// No hope for a (better) match at greater error levels.
const score = bitapScore(pattern, {
errors: i + 1,
currentLocation: expectedLocation,
expectedLocation,
distance
})
// console.log('score', score, finalScore)
if (score > currentThreshold) {
break
}
lastBitArr = bitArr
}
// console.log('FINAL SCORE', finalScore)
// Count exact matches (those with a score of 0) to be "almost" exact
return {
isMatch: bestLocation >= 0,
score: finalScore === 0 ? 0.001 : finalScore,
matchedIndices: matchedIndices(matchMask, minMatchCharLength)
}
}
import bitapRegexSearch from './bitap_regex_search'
import bitapSearch from './bitap_search'
import patternAlphabet from './bitap_pattern_alphabet'
class Bitap {
constructor(
pattern,
{
// Approximately where in the text is the pattern expected to be found?
location = 0,
// Determines how close the match must be to the fuzzy location (specified above).
// An exact letter match which is 'distance' characters away from the fuzzy location
// would score as a complete mismatch. A distance of '0' requires the match be at
// the exact location specified, a threshold of '1000' would require a perfect match
// to be within 800 characters of the fuzzy location to be found using a 0.8 threshold.
distance = 100,
// At what point does the match algorithm give up. A threshold of '0.0' requires a perfect match
// (of both letters and location), a threshold of '1.0' would match anything.
threshold = 0.6,
// Machine word size
maxPatternLength = 32,
// Indicates whether comparisons should be case sensitive.
isCaseSensitive = false,
// Regex used to separate words when searching. Only applicable when `tokenize` is `true`.
tokenSeparator = / +/g,
// When true, the algorithm continues searching to the end of the input even if a perfect
// match is found before the end of the same input.
findAllMatches = false,
// Minimum number of characters that must be matched before a result is considered a match
minMatchCharLength = 1
}
) {
this.options = {
location,
distance,
threshold,
maxPatternLength,
isCaseSensitive,
tokenSeparator,
findAllMatches,
minMatchCharLength
}
this.pattern = this.options.isCaseSensitive ? pattern : pattern.toLowerCase()
if (this.pattern.length <= maxPatternLength) {
this.patternAlphabet = patternAlphabet(this.pattern)
}
}
search(text) {
if (!this.options.isCaseSensitive) {
text = text.toLowerCase()
}
// Exact match
if (this.pattern === text) {
return {
isMatch: true,
score: 0,
matchedIndices: [[0, text.length - 1]]
}
}
// When pattern length is greater than the machine word length, just do a a regex comparison
const { maxPatternLength, tokenSeparator } = this.options
if (this.pattern.length > maxPatternLength) {
return bitapRegexSearch(text, this.pattern, tokenSeparator)
}
// Otherwise, use Bitap algorithm
const { location, distance, threshold, findAllMatches, minMatchCharLength } = this.options
return bitapSearch(text, this.pattern, this.patternAlphabet, {
location,
distance,
threshold,
findAllMatches,
minMatchCharLength
})
}
}
// let x = new Bitap("od mn war", {})
// let result = x.search("Old Man's War")
// console.log(result)
export default Bitap
const isArray = require('./is_array')
const deepValue = (obj, path, list) => {
if (!path) {
// If there's no path left, we've gotten to the object we care about.
list.push(obj)
} else {
const dotIndex = path.indexOf('.')
let firstSegment = path
let remaining = null
if (dotIndex !== -1) {
firstSegment = path.slice(0, dotIndex)
remaining = path.slice(dotIndex + 1)
}
const value = obj[firstSegment]
if (value !== null && value !== undefined) {
if (!remaining && (typeof value === 'string' || typeof value === 'number')) {
list.push(value.toString())
} else if (isArray(value)) {
// Search each item in the array.
for (let i = 0, len = value.length; i < len; i += 1) {
deepValue(value[i], remaining, list)
}
} else if (remaining) {
// An object. Recurse further.
deepValue(value, remaining, list)
}
}
}
return list
}
module.exports = (obj, path) => {
return deepValue(obj, path, [])
}
module.exports = obj => (!Array.isArray ? Object.prototype.toString.call(obj) === '[object Array]' : Array.isArray(obj))
This diff is collapsed.
import { useMemo, useEffect } from 'react'
import { useMemo, useCallback, useEffect } from 'react'
import { useWeb3Context } from 'web3-react'
import FACTORY_ABI from '../abi/factory'
import { getSignerOrProvider, getContract } from '../utils'
import ERC20_ABI from '../abi/erc20'
import { getContract, getFactoryContract, getExchangeContract } from '../utils'
const factoryAddresses = {
1: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95',
4: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36'
// modified from https://usehooks.com/useKeyPress/
export function useBodyKeyDown(targetKey, onKeyDown, suppressOnKeyDown = false) {
const downHandler = useCallback(
({ target: { tagName }, key }) => {
if (key === targetKey && tagName === 'BODY' && !suppressOnKeyDown) {
onKeyDown()
}
},
[targetKey, onKeyDown, suppressOnKeyDown]
)
useEffect(() => {
window.addEventListener('keydown', downHandler)
return () => {
window.removeEventListener('keydown', downHandler)
}
}, [downHandler])
}
export function useSignerOrProvider() {
const { library, account } = useWeb3Context()
export function useBlockEffect(functionToRun) {
const { library } = useWeb3Context()
return useMemo(() => getSignerOrProvider(library, account), [library, account])
useEffect(() => {
if (library) {
function wrappedEffect(blockNumber) {
functionToRun(blockNumber)
}
library.on('block', wrappedEffect)
return () => {
library.removeListener('block', wrappedEffect)
}
}
}, [library, functionToRun])
}
// returns null if the contract cannot be created for any reason
function useContract(contractAddress, ABI) {
const signerOrProvider = useSignerOrProvider()
// returns null on errors
export function useContract(address, ABI, withSignerIfPossible = true) {
const { library, account } = useWeb3Context()
return useMemo(() => {
try {
return getContract(contractAddress, ABI, signerOrProvider)
return getContract(address, ABI, library, withSignerIfPossible ? account : undefined)
} catch {
return null
}
}, [contractAddress, ABI, signerOrProvider])
}, [address, ABI, library, withSignerIfPossible, account])
}
export function useFactoryContract() {
const { networkId } = useWeb3Context()
// returns null on errors
export function useTokenContract(tokenAddress, withSignerIfPossible = true) {
const { library, account } = useWeb3Context()
return useContract(factoryAddresses[networkId], FACTORY_ABI)
return useMemo(() => {
try {
return getContract(tokenAddress, ERC20_ABI, library, withSignerIfPossible ? account : undefined)
} catch {
return null
}
}, [tokenAddress, library, withSignerIfPossible, account])
}
// modified from https://usehooks.com/useKeyPress/
export function useBodyKeyDown(targetKey, onKeyDown, suppressOnKeyDown = false) {
function downHandler({ target: { tagName }, key }) {
if (key === targetKey && tagName === 'BODY' && !suppressOnKeyDown) {
onKeyDown()
// returns null on errors
export function useFactoryContract(withSignerIfPossible = true) {
const { networkId, library, account } = useWeb3Context()
return useMemo(() => {
try {
return getFactoryContract(networkId, library, withSignerIfPossible ? account : undefined)
} catch {
return null
}
}
}, [networkId, library, withSignerIfPossible, account])
}
useEffect(() => {
window.addEventListener('keydown', downHandler)
return () => {
window.removeEventListener('keydown', downHandler)
export function useExchangeContract(exchangeAddress, withSignerIfPossible = true) {
const { library, account } = useWeb3Context()
return useMemo(() => {
try {
return getExchangeContract(exchangeAddress, library, withSignerIfPossible ? account : undefined)
} catch {
return null
}
}, [targetKey, onKeyDown, suppressOnKeyDown])
}, [exchangeAddress, library, withSignerIfPossible, account])
}
......@@ -3,11 +3,16 @@ import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import ReactGA from 'react-ga'
import Web3Provider, { Connectors } from 'web3-react'
import ApplicationContextProvider, { Updater as ApplicationContextUpdater } from './contexts/Application'
import TransactionContextProvider, { Updater as TransactionUpdater } from './contexts/Transaction'
import StaticContextProvider, { Updater as StaticContextUpdater } from './contexts/Static'
import BlockContextProvider, { Updater as BlockContextUpdater } from './contexts/Block'
import './i18n'
import App from './pages/App'
import store from './store'
import './index.scss'
import './i18n'
if (process.env.NODE_ENV === 'production') {
ReactGA.initialize('UA-128182339-1')
......@@ -23,10 +28,36 @@ const Infura = new NetworkOnlyConnector({
})
const connectors = { Injected, Infura }
function ContextProviders({ children }) {
return (
<ApplicationContextProvider>
<TransactionContextProvider>
<StaticContextProvider>
<BlockContextProvider>{children}</BlockContextProvider>
</StaticContextProvider>
</TransactionContextProvider>
</ApplicationContextProvider>
)
}
function Updaters() {
return (
<>
<ApplicationContextUpdater />
<TransactionUpdater />
<StaticContextUpdater />
<BlockContextUpdater />
</>
)
}
ReactDOM.render(
<Provider store={store}>
<Web3Provider connectors={connectors} libraryName="ethers.js">
<App />
<ContextProviders>
<Updaters />
<App />
</ContextProviders>
</Web3Provider>
</Provider>,
document.getElementById('root')
......
......@@ -27,14 +27,14 @@ function App({ initialized, setAddresses, updateNetwork, updateAccount, initiali
context.setConnector('Infura')
}
})
}, [])
}, []) // eslint-disable-line react-hooks/exhaustive-deps
// if the metamask user logs out, set the infura provider
useEffect(() => {
if (context.error && context.error.code === InjectedConnector.errorCodes.UNLOCK_REQUIRED) {
context.setConnector('Infura')
}
}, [context.error, context.connectorName])
}, [context, context.error, context.connectorName])
// initialize redux network
const [reduxNetworkInitialized, setReduxNetworkInitialized] = useState(false)
......@@ -44,7 +44,7 @@ function App({ initialized, setAddresses, updateNetwork, updateAccount, initiali
updateNetwork(context.library._web3Provider, context.networkId)
setReduxNetworkInitialized(true)
}
}, [context.active, context.networkId])
}, [context.active, context.networkId]) // eslint-disable-line react-hooks/exhaustive-deps
// initialize redux account
const [reduxAccountInitialized, setReduxAccountInitialized] = useState(false)
......@@ -53,14 +53,14 @@ function App({ initialized, setAddresses, updateNetwork, updateAccount, initiali
updateAccount(context.account)
setReduxAccountInitialized(true)
}
}, [context.active, context.account])
}, [context.active, context.account]) // eslint-disable-line react-hooks/exhaustive-deps
// initialize redux
useEffect(() => {
if (reduxNetworkInitialized && reduxAccountInitialized) {
initialize().then(startWatching)
}
}, [reduxNetworkInitialized, reduxAccountInitialized])
}, [reduxNetworkInitialized, reduxAccountInitialized]) // eslint-disable-line react-hooks/exhaustive-deps
// active state
if (initialized || context.error) {
......
import React, { useState, useEffect } from 'react'
import { connect } from 'react-redux'
import { withRouter } from 'react-router'
import { useWeb3Context } from 'web3-react'
import { ethers } from 'ethers'
import classnames from 'classnames'
import { withRouter } from 'react-router'
import { useTranslation } from 'react-i18next'
import ReactGA from 'react-ga'
import { useWeb3Context } from 'web3-react'
import { addPendingTx } from '../../ducks/web3connect'
import AddressInputPanel from '../../components/AddressInputPanel'
import OversizedPanel from '../../components/OversizedPanel'
import { addExchange } from '../../ducks/addresses'
import { useSignerOrProvider, useFactoryContract } from '../../hooks'
import { isAddress, getTokenDetails, getExchangeDetails, errorCodes } from '../../utils'
import { useTokenDetails } from '../../contexts/Static'
import { useTransactionContext } from '../../contexts/Transaction'
import { useFactoryContract } from '../../hooks'
import { isAddress } from '../../utils'
function CreateExchange({ history, location, addExchange, addPendingTx }) {
function CreateExchange({ history, location }) {
const { t } = useTranslation()
const context = useWeb3Context()
const signerOrProvider = useSignerOrProvider()
const { account } = useWeb3Context()
const factory = useFactoryContract()
const [tokenAddress, setTokenAddress] = useState(location.state && location.state.tokenAddress)
const [errorMessage, _setErrorMessage] = useState(context.account ? undefined : t('noWallet'))
const [tokenDetails, setTokenDetails] = useState()
// wrap _setErrorMessage to ensure an account is in context
function setErrorMessage(value) {
if (value) {
_setErrorMessage(value)
} else if (!context.account) {
_setErrorMessage(t('noWallet'))
} else {
_setErrorMessage()
}
}
const [tokenAddress, setTokenAddress] = useState((location.state && location.state.tokenAddress) || '')
const { name, symbol, decimals, exchangeAddress } = useTokenDetails(tokenAddress)
const { addTransaction } = useTransactionContext()
// clear state, if it exists
// clear location state, if it exists
useEffect(() => {
if (location.state) {
history.replace(location.pathname)
}
}, [])
}, []) // eslint-disable-line react-hooks/exhaustive-deps
// handle changes to tokenAddress
// validate everything
const [errorMessage, setErrorMessage] = useState(!account && t('noWallet'))
useEffect(() => {
let stale = false
// happy path
if (isAddress(tokenAddress)) {
const tokenDetailsPromise = getTokenDetails(tokenAddress, signerOrProvider)
const exchangeDetailsPromise = getExchangeDetails(context.networkId, tokenAddress, signerOrProvider)
Promise.all([tokenDetailsPromise, exchangeDetailsPromise])
.then(([tokenDetails, exchangeDetails]) => {
if (!stale) {
if (exchangeDetails.exchangeAddress !== ethers.constants.AddressZero) {
addExchange({
tokenAddress,
label: tokenDetails.symbol,
exchangeAddress: exchangeDetails.exchangeAddress
})
setErrorMessage(t('exchangeExists', { tokenAddress }))
}
setTokenDetails(tokenDetails)
}
})
.catch(error => {
if (!stale) {
if (error.code === errorCodes.TOKEN_DETAILS_DECIMALS) {
setErrorMessage(t('invalidDecimals'))
} else if (error.code === errorCodes.TOKEN_DETAILS_SYMBOL) {
setErrorMessage(t('invalidSymbol'))
} else {
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 (tokenAddress && !isAddress(tokenAddress)) {
setErrorMessage(t('invalidTokenAddress'))
} else if (!tokenAddress || 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, symbol, decimals, exchangeAddress, account, t])
async function createExchange() {
const estimatedGasLimit = await factory.estimate.createExchange(tokenAddress)
factory.createExchange(tokenAddress, { gasLimit: estimatedGasLimit }).then(details => {
addPendingTx(details.hash)
setErrorMessage()
setTokenAddress()
factory.createExchange(tokenAddress, { gasLimit: estimatedGasLimit }).then(response => {
addTransaction(response.hash, response)
ReactGA.event({
category: 'Pool',
action: 'CreateExchange'
......@@ -107,25 +65,31 @@ function CreateExchange({ history, location, addExchange, addPendingTx }) {
})
}
const isValid = isAddress(tokenAddress) && !errorMessage && tokenDetails && tokenDetails.tokenAddress === tokenAddress
const isValid = errorMessage === null
return (
<>
<AddressInputPanel
title={t('tokenAddress')}
value={tokenAddress}
onChange={input => setTokenAddress(input)}
onChange={input => {
setTokenAddress(input)
}}
errorMessage={errorMessage === t('noWallet') ? '' : errorMessage}
/>
<OversizedPanel hideBottom>
<div className="pool__summary-panel">
<div className="pool__exchange-rate-wrapper">
<span className="pool__exchange-rate">{t('name')}</span>
<span>{name ? name : ' - '}</span>
</div>
<div className="pool__exchange-rate-wrapper">
<span className="pool__exchange-rate">{t('symbol')}</span>
<span>{tokenDetails ? tokenDetails.symbol : ' - '}</span>
<span>{symbol ? symbol : ' - '}</span>
</div>
<div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t('decimals')}</span>
<span>{tokenDetails ? tokenDetails.decimals : ' - '}</span>
<span>{decimals || decimals === 0 ? decimals : ' - '}</span>
</div>
</div>
</OversizedPanel>
......@@ -135,7 +99,7 @@ function CreateExchange({ history, location, addExchange, addPendingTx }) {
'create-exchange--error': !!errorMessage
})}
>
{!!errorMessage ? errorMessage : t('enterTokenCont')}
{errorMessage ? errorMessage : t('enterTokenCont')}
</div>
</div>
<div className="pool__cta-container">
......@@ -147,12 +111,4 @@ function CreateExchange({ history, location, addExchange, addPendingTx }) {
)
}
export default withRouter(
connect(
undefined,
dispatch => ({
addExchange: opts => dispatch(addExchange(opts)),
addPendingTx: id => dispatch(addPendingTx(id))
})
)(CreateExchange)
)
export default withRouter(CreateExchange)
This diff is collapsed.
This diff is collapsed.
import { ethers } from 'ethers'
import FACTORY_ABI from '../abi/factory'
import EXCHANGE_ABI from '../abi/exchange'
import ERC20_ABI from '../abi/erc20'
import ERC20_WITH_BYTES_ABI from '../abi/erc20_symbol_bytes32'
import ERC20_WITH_BYTES_ABI from '../abi/erc20_bytes32'
import { FACTORY_ADDRESSES } from '../constants'
const factoryAddresses = {
1: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95',
4: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36'
}
export const errorCodes = ['TOKEN_DETAILS_DECIMALS', 'TOKEN_DETAILS_SYMBOL'].reduce(
export const ERROR_CODES = ['TOKEN_NAME', 'TOKEN_SYMBOL', 'TOKEN_DECIMALS'].reduce(
(accumulator, currentValue, currentIndex) => {
accumulator[currentValue] = currentIndex
return accumulator
......@@ -17,10 +14,6 @@ export const errorCodes = ['TOKEN_DETAILS_DECIMALS', 'TOKEN_DETAILS_SYMBOL'].red
{}
)
function getFactoryContract(networkId, signerOrProvider) {
return getContract(factoryAddresses[networkId], FACTORY_ABI, signerOrProvider)
}
export function isAddress(value) {
try {
ethers.utils.getAddress(value)
......@@ -30,42 +23,178 @@ export function isAddress(value) {
}
}
export function getSignerOrProvider(library, account) {
export function calculateGasMargin(value, margin) {
const offset = value.mul(margin).div(ethers.utils.bigNumberify(10000))
return value.add(offset)
}
// account is optional
export function getProviderOrSigner(library, account) {
return account ? library.getSigner(account) : library
}
export function getContract(contractAddress, ABI, signerOrProvider) {
return new ethers.Contract(contractAddress, ABI, signerOrProvider)
// account is optional
export function getContract(address, ABI, library, account) {
if (!isAddress(address) || address === ethers.constants.AddressZero) {
throw Error(`Invalid 'address' parameter '${address}'.`)
}
return new ethers.Contract(address, ABI, getProviderOrSigner(library, account))
}
// account is optional
export function getFactoryContract(networkId, library, account) {
return getContract(FACTORY_ADDRESSES[networkId], FACTORY_ABI, getProviderOrSigner(library, account))
}
// account is optional
export function getExchangeContract(exchangeAddress, library, account) {
return getContract(exchangeAddress, EXCHANGE_ABI, getProviderOrSigner(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}'.`)
}
return getContract(tokenAddress, ERC20_ABI, library)
.name()
.catch(() =>
getContract(tokenAddress, ERC20_WITH_BYTES_ABI, library)
.name()
.then(bytes32 => ethers.utils.parseBytes32String(bytes32))
)
.catch(error => {
error.code = ERROR_CODES.TOKEN_SYMBOL
throw error
})
}
const decimalsPromise = contract.decimals().catch(error => {
console.log(error)
error.code = errorCodes.TOKEN_DETAILS_DECIMALS
throw error
})
const symbolPromise = contract
// get token symbol
export async function getTokenSymbol(tokenAddress, library) {
if (!isAddress(tokenAddress)) {
throw Error(`Invalid 'tokenAddress' parameter '${tokenAddress}'.`)
}
return getContract(tokenAddress, ERC20_ABI, library)
.symbol()
.catch(() => {
const contractBytes32 = getContract(tokenAddress, ERC20_WITH_BYTES_ABI, signerOrProvider)
const contractBytes32 = getContract(tokenAddress, ERC20_WITH_BYTES_ABI, library)
return contractBytes32.symbol().then(bytes32 => ethers.utils.parseBytes32String(bytes32))
})
.catch(error => {
error.code = errorCodes.TOKEN_DETAILS_SYMBOL
error.code = ERROR_CODES.TOKEN_SYMBOL
throw error
})
}
// get token decimals
export async function getTokenDecimals(tokenAddress, library) {
if (!isAddress(tokenAddress)) {
throw Error(`Invalid 'tokenAddress' parameter '${tokenAddress}'.`)
}
return getContract(tokenAddress, ERC20_ABI, library)
.decimals()
.catch(error => {
error.code = ERROR_CODES.TOKEN_DECIMALS
throw error
})
}
// 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)
}
// 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 Promise.all([decimalsPromise, symbolPromise]).then(([decimals, symbol]) => ({
decimals,
symbol,
tokenAddress
}))
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 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))
)
return factoryContract.getExchange(tokenAddress).then(exchangeAddress => ({ exchangeAddress, tokenAddress }))
// 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 diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment