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: branches:
only: except:
- production - master
- beta
language: node_js language: node_js
node_js: node_js:
- '10' - '10'
......
...@@ -5,29 +5,25 @@ ...@@ -5,29 +5,25 @@
"homepage": ".", "homepage": ".",
"private": true, "private": true,
"dependencies": { "dependencies": {
"bignumber.js": "^7.2.1", "@reach/tooltip": "^0.2.0",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"escape-string-regexp": "^2.0.0",
"ethers": "^4.0.27", "ethers": "^4.0.27",
"fuse": "^0.4.0",
"i18next": "^15.0.9", "i18next": "^15.0.9",
"i18next-browser-languagedetector": "^3.0.1", "i18next-browser-languagedetector": "^3.0.1",
"i18next-xhr-backend": "^2.0.1", "i18next-xhr-backend": "^2.0.1",
"jazzicon": "^1.5.0", "jazzicon": "^1.5.0",
"node-sass": "^4.11.0", "node-sass": "^4.11.0",
"prop-types": "^15.7.2",
"react": "^16.8.6", "react": "^16.8.6",
"react-aria-modal": "^4.0.0",
"react-dom": "^16.8.6", "react-dom": "^16.8.6",
"react-ga": "^2.5.7", "react-ga": "^2.5.7",
"react-i18next": "^10.7.0", "react-i18next": "^10.7.0",
"react-redux": "^5.0.7",
"react-router-dom": "^5.0.0", "react-router-dom": "^5.0.0",
"react-scripts": "^2.1.8", "react-scripts": "^3.0.1",
"react-transition-group": "1.x", "react-transition-group": "1.x",
"redux": "^3.7.2", "styled-components": "^4.2.0",
"redux-subscriber": "^1.1.0",
"redux-thunk": "^2.2.0",
"ua-parser-js": "^0.7.18", "ua-parser-js": "^0.7.18",
"web3": "1.0.0-beta.52",
"web3-react": "^5.0.4" "web3-react": "^5.0.4"
}, },
"scripts": { "scripts": {
...@@ -46,14 +42,23 @@ ...@@ -46,14 +42,23 @@
"check:format": "yarn format:base --check", "check:format": "yarn format:base --check",
"check:all": "yarn check:lint && yarn check:format" "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", "license": "GPL-3.0-or-later",
"devDependencies": { "devDependencies": {
"prettier": "^1.17.0" "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.", "wrongNetwork": "Du bist auf dem falschen Netzwerk.",
"switchNetwork": "Bitte wechsle zum {{ correctNetwork }}", "switchNetwork": "Bitte wechsle zum {{ correctNetwork }}",
"installWeb3MobileBrowser": "Bitte besuche uns mit einem web3-fähigen mobilen Browser wie z.B. Trust Wallet oder Coinbase Wallet.", "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", "disconnected": "Nicht verbunden",
"swap": "Tauschen", "swap": "Tauschen",
"send": "Senden", "send": "Senden",
...@@ -58,7 +58,7 @@ ...@@ -58,7 +58,7 @@
"totalSupplyIs0": "Die gesamte Anzahl Liquiditätstokens ist aktuell 0.", "totalSupplyIs0": "Die gesamte Anzahl Liquiditätstokens ist aktuell 0.",
"tokenWorth": "Zum gegenwärtigen Wechselkurs ist jeder Pool Token so viel Wert", "tokenWorth": "Zum gegenwärtigen Wechselkurs ist jeder Pool Token so viel Wert",
"firstLiquidity": "Du bist die erste Person die Liquidität bereitstellt!", "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", "removeLiquidity": "Liquidität entfernen",
"poolTokens": "Pool Tokens", "poolTokens": "Pool Tokens",
"enterLabelCont": "{{ label }} Wert eingeben um fortzufahren.", "enterLabelCont": "{{ label }} Wert eingeben um fortzufahren.",
......
...@@ -22,9 +22,11 @@ ...@@ -22,9 +22,11 @@
"enterValueCont": "Enter a {{ missingCurrencyValue }} value to continue.", "enterValueCont": "Enter a {{ missingCurrencyValue }} value to continue.",
"selectTokenCont": "Select a token to continue.", "selectTokenCont": "Select a token to continue.",
"noLiquidity": "No liquidity.", "noLiquidity": "No liquidity.",
"insufficientLiquidity": "Insufficient liquidity.",
"unlockTokenCont": "Please unlock token to continue.", "unlockTokenCont": "Please unlock token to continue.",
"transactionDetails": "Transaction Details", "transactionDetails": "Transaction Details",
"hideDetails": "Hide Details", "hideDetails": "Hide Details",
"slippageWarning": "Slippage Warning",
"youAreSelling": "You are selling", "youAreSelling": "You are selling",
"orTransFail": "or the transaction will fail.", "orTransFail": "or the transaction will fail.",
"youWillReceive": "You will receive at least", "youWillReceive": "You will receive at least",
...@@ -46,7 +48,7 @@ ...@@ -46,7 +48,7 @@
"noZero": "Amount cannot be zero.", "noZero": "Amount cannot be zero.",
"mustBeETH": "One of the input must be ETH.", "mustBeETH": "One of the input must be ETH.",
"enterCurrencyOrLabelCont": "Enter a {{ inputCurrency }} or {{ label }} value to continue.", "enterCurrencyOrLabelCont": "Enter a {{ inputCurrency }} or {{ label }} value to continue.",
"youAreAdding": "You are adding between", "youAreAdding": "You are adding",
"and": "and", "and": "and",
"intoPool": "into the liquidity pool.", "intoPool": "into the liquidity pool.",
"outPool": "from the liquidity pool.", "outPool": "from the liquidity pool.",
...@@ -70,7 +72,9 @@ ...@@ -70,7 +72,9 @@
"invalidDecimals": "Invalid decimals", "invalidDecimals": "Invalid decimals",
"tokenAddress": "Token Address", "tokenAddress": "Token Address",
"label": "Label", "label": "Label",
"name": "Name",
"symbol": "Symbol", "symbol": "Symbol",
"decimals": "Decimals", "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 @@ ...@@ -3,7 +3,7 @@
"constant": true, "constant": true,
"inputs": [], "inputs": [],
"name": "name", "name": "name",
"outputs": [{ "name": "", "type": "string" }], "outputs": [{ "name": "", "type": "bytes32" }],
"payable": false, "payable": false,
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
......
import React from 'react' import React, { useState, useEffect } from 'react'
import classnames from 'classnames' import classnames from 'classnames'
import { useTranslation } from 'react-i18next' 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 QrCode from '../QrCode' // commented out pending further review
import './address-input-panel.scss' 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 { 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 ( return (
<div className="currency-input-panel"> <div className="currency-input-panel">
<div <div
className={classnames('currency-input-panel__container address-input-panel__recipient-row', { className={classnames('currency-input-panel__container address-input-panel__recipient-row', {
'currency-input-panel__container--error': errorMessage 'currency-input-panel__container--error': input !== '' && error
})} })}
> >
<div className="address-input-panel__input-container"> <div className="address-input-panel__input-container">
...@@ -25,12 +97,16 @@ export default function AddressInputPanel({ title, onChange = () => {}, value = ...@@ -25,12 +97,16 @@ export default function AddressInputPanel({ title, onChange = () => {}, value =
<div className="currency-input-panel__input-row"> <div className="currency-input-panel__input-row">
<input <input
type="text" type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
className={classnames('address-input-panel__input', { className={classnames('address-input-panel__input', {
'address-input-panel__input--error': errorMessage 'address-input-panel__input--error': input !== '' && error
})} })}
placeholder="0x1234..." placeholder="0x1234..."
onChange={e => onChange(e.target.value)} onChange={onInput}
value={value} value={input}
/> />
</div> </div>
</div> </div>
......
...@@ -45,30 +45,32 @@ class ContextualInfo extends Component { ...@@ -45,30 +45,32 @@ class ContextualInfo extends Component {
) )
} }
return [ return (
<div <>
key="open-details" <div
className="contextual-info__summary-wrapper contextual-info__open-details-container" key="open-details"
onClick={() => className="contextual-info__summary-wrapper contextual-info__open-details-container"
this.setState(prevState => { onClick={() =>
return { showDetails: !prevState.showDetails } this.setState(prevState => {
}) return { showDetails: !prevState.showDetails }
} })
> }
{!this.state.showDetails ? ( >
<> {!this.state.showDetails ? (
<span>{openDetailsText}</span> <>
<img src={DropdownBlue} alt="dropdown" /> <span>{openDetailsText}</span>
</> <img src={DropdownBlue} alt="dropdown" />
) : ( </>
<> ) : (
<span>{closeDetailsText}</span> <>
<img src={DropupBlue} alt="dropup" /> <span>{closeDetailsText}</span>
</> <img src={DropupBlue} alt="dropup" />
)} </>
</div>, )}
this.renderDetails() </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 @@ ...@@ -95,6 +95,10 @@
background-color: rgba($zumthor-blue, 0.8); background-color: rgba($zumthor-blue, 0.8);
} }
&:focus {
box-shadow: 0 0 0.5px 0.5px $malibu-blue;
}
&--selected { &--selected {
background-color: $concrete-gray; background-color: $concrete-gray;
border-color: $mercury-gray; border-color: $mercury-gray;
...@@ -233,6 +237,8 @@ ...@@ -233,6 +237,8 @@
color: $white; color: $white;
justify-content: center; justify-content: center;
background-color: $malibu-blue; background-color: $malibu-blue;
text-decoration: none;
&:hover { &:hover {
background-color: lighten($malibu-blue, 1); background-color: lighten($malibu-blue, 1);
} }
......
This diff is collapsed.
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { withRouter, NavLink } from 'react-router-dom' import { withRouter, NavLink } from 'react-router-dom'
import { connect } from 'react-redux'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { dismissBetaMessage } from '../../ducks/app'
import { useBodyKeyDown } from '../../hooks' import { useBodyKeyDown } from '../../hooks'
import './navigation-tabs.scss' import './navigation-tabs.scss'
import { useBetaMessageManager } from '../../contexts/Application'
const tabOrder = [ const tabOrder = [
{ {
...@@ -26,9 +25,11 @@ const tabOrder = [ ...@@ -26,9 +25,11 @@ const tabOrder = [
} }
] ]
function NavigationTabs({ location: { pathname }, history, dismissBetaMessage, showBetaMessage }) { function NavigationTabs({ location: { pathname }, history }) {
const { t } = useTranslation() const { t } = useTranslation()
const [showBetaMessage, dismissBetaMessage] = useBetaMessageManager()
const navigate = useCallback( const navigate = useCallback(
direction => { direction => {
const tabIndex = tabOrder.findIndex(({ regex }) => pathname.match(regex)) const tabIndex = tabOrder.findIndex(({ regex }) => pathname.match(regex))
...@@ -73,13 +74,4 @@ function NavigationTabs({ location: { pathname }, history, dismissBetaMessage, s ...@@ -73,13 +74,4 @@ function NavigationTabs({ location: { pathname }, history, dismissBetaMessage, s
) )
} }
export default withRouter( export default withRouter(NavigationTabs)
connect(
state => ({
showBetaMessage: state.app.showBetaMessage
}),
dispatch => ({
dismissBetaMessage: () => dispatch(dismissBetaMessage())
})
)(NavigationTabs)
)
import React, { Component } from 'react' import React, { useState } from 'react'
import PropTypes from 'prop-types' import { useWeb3Context } from 'web3-react'
import { connect } from 'react-redux'
import classnames from 'classnames' import classnames from 'classnames'
import Jazzicon from 'jazzicon' import Jazzicon from 'jazzicon'
import { CSSTransitionGroup } from 'react-transition-group' import { CSSTransitionGroup } from 'react-transition-group'
import { withTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ethers } from 'ethers' import { ethers } from 'ethers'
import Modal from '../Modal' import Modal from '../Modal'
import { useAllTransactions } from '../../contexts/Transactions'
import './web3-status.scss' import './web3-status.scss'
...@@ -15,19 +15,47 @@ function getEtherscanLink(tx) { ...@@ -15,19 +15,47 @@ function getEtherscanLink(tx) {
return `https://etherscan.io/tx/${tx}` return `https://etherscan.io/tx/${tx}`
} }
class Web3Status extends Component { function getPendingText(pendingTransactions, pendingLabel) {
state = { return (
isShowingModal: false <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 = () => { const address = ethers.utils.getAddress(text)
if (this.props.pending.length && !this.state.isShowingModal) { return `${address.substring(0, 6)}...${address.substring(38)}`
this.setState({ isShowingModal: true }) }
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() { function renderPendingTransactions() {
return this.props.pending.map(transaction => { return pending.map(transaction => {
return ( return (
<div <div
key={transaction} key={transaction}
...@@ -36,20 +64,20 @@ class Web3Status extends Component { ...@@ -36,20 +64,20 @@ class Web3Status extends Component {
> >
<div className="pending-modal__transaction-label">{transaction}</div> <div className="pending-modal__transaction-label">{transaction}</div>
<div className="pending-modal__pending-indicator"> <div className="pending-modal__pending-indicator">
<div className="loader" /> {this.props.t('pending')} <div className="loader" /> {t('pending')}
</div> </div>
</div> </div>
) )
}) })
} }
renderModal() { function renderModal() {
if (!this.state.isShowingModal) { if (!isShowingModal) {
return null return null
} }
return ( return (
<Modal onClose={() => this.setState({ isShowingModal: false })}> <Modal onClose={() => setIsShowingModal(false)}>
<CSSTransitionGroup <CSSTransitionGroup
transitionName="token-modal" transitionName="token-modal"
transitionAppear={true} transitionAppear={true}
...@@ -61,7 +89,7 @@ class Web3Status extends Component { ...@@ -61,7 +89,7 @@ class Web3Status extends Component {
<div className="pending-modal"> <div className="pending-modal">
<div className="pending-modal__transaction-list"> <div className="pending-modal__transaction-list">
<div className="pending-modal__header">Transactions</div> <div className="pending-modal__header">Transactions</div>
{this.renderPendingTransactions()} {renderPendingTransactions()}
</div> </div>
</div> </div>
</CSSTransitionGroup> </CSSTransitionGroup>
...@@ -69,78 +97,30 @@ class Web3Status extends Component { ...@@ -69,78 +97,30 @@ class Web3Status extends Component {
) )
} }
render() { return (
const { t, address, pending, confirmed } = this.props <div
const hasPendingTransactions = !!pending.length className={classnames('web3-status', {
const hasConfirmedTransactions = !!confirmed.length 'web3-status__connected': active,
'web3-status--pending': hasPendingTransactions,
return ( 'web3-status--confirmed': hasConfirmedTransactions
})}
onClick={handleClick}
>
<div className="web3-status__text">
{hasPendingTransactions ? getPendingText(pending, t('pending')) : getText(account, t('disconnected'))}
</div>
<div <div
className={classnames('web3-status', { className="web3-status__identicon"
'web3-status__connected': this.props.isConnected, ref={el => {
'web3-status--pending': hasPendingTransactions, if (!el || !account) {
'web3-status--confirmed': hasConfirmedTransactions return
})} } else {
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
}
el.innerHTML = '' el.innerHTML = ''
el.appendChild(Jazzicon(16, parseInt(address.slice(2), 16))) el.appendChild(Jazzicon(16, parseInt(account.slice(2, 10), 16)))
}} }
/> }}
{this.renderModal()} />
</div> {renderModal()}
)
}
}
function getPendingText(pendingTransactions, pendingLabel) {
return (
<div className="web3-status__pending-container">
<div className="loader" />
<span key="text">
{pendingTransactions.length} {pendingLabel}
</span>
</div> </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 }
}
This diff is collapsed.
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
)
}
This diff is collapsed.
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
}
}
This diff is collapsed.
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.
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 { useWeb3Context } from 'web3-react'
import FACTORY_ABI from '../abi/factory' import ERC20_ABI from '../abi/erc20'
import { getSignerOrProvider, getContract } from '../utils' import { getContract, getFactoryContract, getExchangeContract } from '../utils'
const factoryAddresses = { // modified from https://usehooks.com/useDebounce/
1: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95', export function useDebounce(value, delay) {
4: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36' 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() { // modified from https://usehooks.com/useKeyPress/
const { library, account } = useWeb3Context() 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 // returns null on errors
function useContract(contractAddress, ABI) { export function useContract(address, ABI, withSignerIfPossible = true) {
const signerOrProvider = useSignerOrProvider() const { library, account } = useWeb3Context()
return useMemo(() => { return useMemo(() => {
try { try {
return getContract(contractAddress, ABI, signerOrProvider) return getContract(address, ABI, library, withSignerIfPossible ? account : undefined)
} catch { } catch {
return null return null
} }
}, [contractAddress, ABI, signerOrProvider]) }, [address, ABI, library, withSignerIfPossible, account])
} }
export function useFactoryContract() { // returns null on errors
const { networkId } = useWeb3Context() 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/ // returns null on errors
export function useBodyKeyDown(targetKey, onKeyDown, suppressOnKeyDown = false) { export function useFactoryContract(withSignerIfPossible = true) {
function downHandler({ target: { tagName }, key }) { const { networkId, library, account } = useWeb3Context()
if (key === targetKey && tagName === 'BODY' && !suppressOnKeyDown) {
onKeyDown() return useMemo(() => {
try {
return getFactoryContract(networkId, library, withSignerIfPossible ? account : undefined)
} catch {
return null
} }
} }, [networkId, library, withSignerIfPossible, account])
}
useEffect(() => { export function useExchangeContract(exchangeAddress, withSignerIfPossible = true) {
window.addEventListener('keydown', downHandler) const { library, account } = useWeb3Context()
return () => {
window.removeEventListener('keydown', downHandler) 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 React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import ReactGA from 'react-ga' import ReactGA from 'react-ga'
import Web3Provider, { Connectors } from 'web3-react' 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 App from './pages/App'
import store from './store'
import './index.scss' import './i18n'
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
ReactGA.initialize('UA-128182339-1') ReactGA.initialize('UA-128182339-1')
...@@ -23,11 +28,40 @@ const Infura = new NetworkOnlyConnector({ ...@@ -23,11 +28,40 @@ const Infura = new NetworkOnlyConnector({
}) })
const connectors = { Injected, Infura } 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( ReactDOM.render(
<Provider store={store}> <ThemeProvider>
<Web3Provider connectors={connectors} libraryName="ethers.js"> <>
<App /> <GlobalStyle />
</Web3Provider> <Web3Provider connectors={connectors} libraryName="ethers.js">
</Provider>, <ContextProviders>
<Updaters />
<App />
</ContextProviders>
</Web3Provider>
</>
</ThemeProvider>,
document.getElementById('root') 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 React, { useEffect } from 'react'
import { connect } from 'react-redux'
import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom' import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom'
import { useWeb3Context, Connectors } from 'web3-react' import { useWeb3Context, Connectors } from 'web3-react'
import NavigationTabs from '../components/NavigationTabs' import NavigationTabs from '../components/NavigationTabs'
import { updateNetwork, updateAccount, initialize, startWatching } from '../ducks/web3connect'
import { setAddresses } from '../ducks/addresses'
import Header from '../components/Header' import Header from '../components/Header'
import Swap from './Swap' import Swap from './Swap'
import Send from './Send' import Send from './Send'
...@@ -15,60 +12,34 @@ import './App.scss' ...@@ -15,60 +12,34 @@ import './App.scss'
const { Connector, InjectedConnector } = Connectors const { Connector, InjectedConnector } = Connectors
function App({ initialized, setAddresses, updateNetwork, updateAccount, initialize, startWatching }) { export default function App() {
const context = useWeb3Context() const { setConnector, setError, error, active, connectorName } = useWeb3Context()
// start web3-react on page-load // start web3-react on page-load
useEffect(() => { useEffect(() => {
context.setConnector('Injected', { suppressAndThrowErrors: true }).catch(error => { setConnector('Injected', { suppressAndThrowErrors: true }).catch(error => {
if (error.code === Connector.errorCodes.UNSUPPORTED_NETWORK) { if (error.code === Connector.errorCodes.UNSUPPORTED_NETWORK) {
context.setError(error, { connectorName: 'Injected' }) setError(error, { connectorName: 'Injected' })
} else { } else {
context.setConnector('Infura') setConnector('Infura')
} }
}) })
}, []) }, []) // eslint-disable-line react-hooks/exhaustive-deps
// if the metamask user logs out, set the infura provider // if the metamask user logs out, set the infura provider
useEffect(() => { useEffect(() => {
if (context.error && context.error.code === InjectedConnector.errorCodes.UNLOCK_REQUIRED) { if (error && error.code === InjectedConnector.errorCodes.UNLOCK_REQUIRED) {
context.setConnector('Infura') setConnector('Infura')
} }
}, [context.error, context.connectorName]) }, [error, connectorName, setConnector])
// 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])
// active state // active state
if (initialized || context.error) { if (active || error) {
return ( return (
<div id="app-container"> <div id="app-container">
<Header /> <Header />
{/* this is an intermediate state before infura is set */} {/* 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="app__wrapper">
<div className="body"> <div className="body">
<div className="body__content"> <div className="body__content">
...@@ -100,16 +71,3 @@ function App({ initialized, setAddresses, updateNetwork, updateAccount, initiali ...@@ -100,16 +71,3 @@ function App({ initialized, setAddresses, updateNetwork, updateAccount, initiali
// loading state // loading state
return null 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 React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import Web3Provider, { Connectors } from 'web3-react' import Web3Provider, { Connectors } from 'web3-react'
import App from './App' import App from './App'
import store from '../store'
// TODO, fix this hacky workaround // TODO, fix this hacky workaround
const { NetworkOnlyConnector } = Connectors const { NetworkOnlyConnector } = Connectors
...@@ -16,11 +14,9 @@ export const connectors = { Injected } ...@@ -16,11 +14,9 @@ export const connectors = { Injected }
it('renders without crashing', () => { it('renders without crashing', () => {
const div = document.createElement('div') const div = document.createElement('div')
ReactDOM.render( ReactDOM.render(
<Provider store={store}> <Web3Provider connectors={connectors} libraryName="ethers.js">
<Web3Provider connectors={connectors} libraryName="ethers.js"> <App />
<App /> </Web3Provider>,
</Web3Provider>
</Provider>,
div div
) )
ReactDOM.unmountComponentAtNode(div) ReactDOM.unmountComponentAtNode(div)
......
This diff is collapsed.
import React, { useState, useEffect } from 'react' 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 { ethers } from 'ethers'
import classnames from 'classnames' import classnames from 'classnames'
import { withRouter } from 'react-router'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import ReactGA from 'react-ga' import ReactGA from 'react-ga'
import { useWeb3Context } from 'web3-react'
import { addPendingTx } from '../../ducks/web3connect'
import AddressInputPanel from '../../components/AddressInputPanel' import AddressInputPanel from '../../components/AddressInputPanel'
import OversizedPanel from '../../components/OversizedPanel' import OversizedPanel from '../../components/OversizedPanel'
import { addExchange } from '../../ducks/addresses' import { useFactoryContract } from '../../hooks'
import { useSignerOrProvider, useFactoryContract } from '../../hooks' import { useTokenDetails } from '../../contexts/Tokens'
import { isAddress, getTokenDetails, getExchangeDetails, errorCodes } from '../../utils' import { useTransactionAdder } from '../../contexts/Transactions'
function CreateExchange({ history, location, addExchange, addPendingTx }) { function CreateExchange({ history, location }) {
const { t } = useTranslation() const { t } = useTranslation()
const context = useWeb3Context() const { account } = useWeb3Context()
const signerOrProvider = useSignerOrProvider()
const factory = useFactoryContract() const factory = useFactoryContract()
const [tokenAddress, setTokenAddress] = useState(location.state && location.state.tokenAddress) const [tokenAddress, setTokenAddress] = useState({
const [errorMessage, _setErrorMessage] = useState(context.account ? undefined : t('noWallet')) address: '',
const [tokenDetails, setTokenDetails] = useState() name: ''
})
const [tokenAddressError, setTokenAddressError] = useState()
// wrap _setErrorMessage to ensure an account is in context const { name, symbol, decimals, exchangeAddress } = useTokenDetails(tokenAddress.address)
function setErrorMessage(value) { const addTransaction = useTransactionAdder()
if (value) {
_setErrorMessage(value)
} else if (!context.account) {
_setErrorMessage(t('noWallet'))
} else {
_setErrorMessage()
}
}
// clear state, if it exists // clear location state, if it exists
useEffect(() => { useEffect(() => {
if (location.state) { if (location.state) {
history.replace(location.pathname) 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(() => { useEffect(() => {
let stale = false if (tokenAddressError) {
// 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 {
setErrorMessage(t('invalidTokenAddress')) 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 () => { return () => {
stale = true
setErrorMessage() setErrorMessage()
setTokenDetails()
} }
}, [tokenAddress, signerOrProvider, context.networkId]) }, [tokenAddress.address, symbol, decimals, exchangeAddress, account, t, tokenAddressError])
async function createExchange() { 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 => { factory.createExchange(tokenAddress.address, { gasLimit: estimatedGasLimit }).then(response => {
addPendingTx(details.hash)
setErrorMessage()
setTokenAddress()
ReactGA.event({ ReactGA.event({
category: 'Pool', category: 'Pool',
action: 'CreateExchange' action: 'CreateExchange'
}) })
addTransaction(response)
}) })
} }
const isValid = isAddress(tokenAddress) && !errorMessage && tokenDetails && tokenDetails.tokenAddress === tokenAddress const isValid = errorMessage === null
return ( return (
<> <>
<AddressInputPanel <AddressInputPanel
title={t('tokenAddress')} title={t('tokenAddress')}
value={tokenAddress} initialInput={(location.state && location.state.tokenAddress) || ''}
onChange={input => setTokenAddress(input)} onChange={setTokenAddress}
errorMessage={errorMessage === t('noWallet') ? '' : errorMessage} onError={setTokenAddressError}
/> />
<OversizedPanel hideBottom> <OversizedPanel hideBottom>
<div className="pool__summary-panel"> <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"> <div className="pool__exchange-rate-wrapper">
<span className="pool__exchange-rate">{t('symbol')}</span> <span className="pool__exchange-rate">{t('symbol')}</span>
<span>{tokenDetails ? tokenDetails.symbol : ' - '}</span> <span>{symbol ? symbol : ' - '}</span>
</div> </div>
<div className="pool__exchange-rate-wrapper"> <div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">{t('decimals')}</span> <span className="swap__exchange-rate">{t('decimals')}</span>
<span>{tokenDetails ? tokenDetails.decimals : ' - '}</span> <span>{decimals || decimals === 0 ? decimals : ' - '}</span>
</div> </div>
</div> </div>
</OversizedPanel> </OversizedPanel>
...@@ -135,7 +102,7 @@ function CreateExchange({ history, location, addExchange, addPendingTx }) { ...@@ -135,7 +102,7 @@ function CreateExchange({ history, location, addExchange, addPendingTx }) {
'create-exchange--error': !!errorMessage 'create-exchange--error': !!errorMessage
})} })}
> >
{!!errorMessage ? errorMessage : t('enterTokenCont')} {errorMessage ? errorMessage : t('enterTokenCont')}
</div> </div>
</div> </div>
<div className="pool__cta-container"> <div className="pool__cta-container">
...@@ -147,12 +114,4 @@ function CreateExchange({ history, location, addExchange, addPendingTx }) { ...@@ -147,12 +114,4 @@ function CreateExchange({ history, location, addExchange, addPendingTx }) {
) )
} }
export default withRouter( export default withRouter(CreateExchange)
connect(
undefined,
dispatch => ({
addExchange: opts => dispatch(addExchange(opts)),
addPendingTx: id => dispatch(addPendingTx(id))
})
)(CreateExchange)
)
This diff is collapsed.
...@@ -69,6 +69,7 @@ ...@@ -69,6 +69,7 @@
} }
&__new-exchange-warning { &__new-exchange-warning {
margin-top: 1rem;
padding: 1rem; padding: 1rem;
margin-bottom: 2rem; margin-bottom: 2rem;
border: 1px solid rgba($pizazz-orange, 0.4); border: 1px solid rgba($pizazz-orange, 0.4);
......
This diff is collapsed.
This diff is collapsed.
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 { ethers } from 'ethers'
import FACTORY_ABI from '../abi/factory' import FACTORY_ABI from '../abi/factory'
import EXCHANGE_ABI from '../abi/exchange'
import ERC20_ABI from '../abi/erc20' import ERC20_ABI from '../abi/erc20'
import ERC20_WITH_BYTES_ABI from '../abi/erc20_symbol_bytes32' import ERC20_WITH_BYTES_ABI from '../abi/erc20_bytes32'
import { FACTORY_ADDRESSES } from '../constants'
const factoryAddresses = { export const ERROR_CODES = ['TOKEN_NAME', 'TOKEN_SYMBOL', 'TOKEN_DECIMALS'].reduce(
1: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95',
4: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36'
}
export const errorCodes = ['TOKEN_DETAILS_DECIMALS', 'TOKEN_DETAILS_SYMBOL'].reduce(
(accumulator, currentValue, currentIndex) => { (accumulator, currentValue, currentIndex) => {
accumulator[currentValue] = currentIndex accumulator[currentValue] = currentIndex
return accumulator return accumulator
...@@ -17,55 +14,195 @@ export const errorCodes = ['TOKEN_DETAILS_DECIMALS', 'TOKEN_DETAILS_SYMBOL'].red ...@@ -17,55 +14,195 @@ export const errorCodes = ['TOKEN_DETAILS_DECIMALS', 'TOKEN_DETAILS_SYMBOL'].red
{} {}
) )
function getFactoryContract(networkId, signerOrProvider) { export function safeAccess(object, path) {
return getContract(factoryAddresses[networkId], FACTORY_ABI, signerOrProvider) return object
? path.reduce(
(accumulator, currentValue) => (accumulator && accumulator[currentValue] ? accumulator[currentValue] : null),
object
)
: null
} }
export function isAddress(value) { export function isAddress(value) {
try { try {
ethers.utils.getAddress(value) return ethers.utils.getAddress(value.toLowerCase())
return true
} catch { } catch {
return false 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 return account ? library.getSigner(account) : library
} }
export function getContract(contractAddress, ABI, signerOrProvider) { // account is optional
return new ethers.Contract(contractAddress, ABI, signerOrProvider) 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) { // get token name
const contract = getContract(tokenAddress, ERC20_ABI, signerOrProvider) export async function getTokenName(tokenAddress, library) {
if (!isAddress(tokenAddress)) {
throw Error(`Invalid 'tokenAddress' parameter '${tokenAddress}'.`)
}
const decimalsPromise = contract.decimals().catch(error => { return getContract(tokenAddress, ERC20_ABI, library)
console.log(error) .name()
error.code = errorCodes.TOKEN_DETAILS_DECIMALS .catch(() =>
throw error getContract(tokenAddress, ERC20_WITH_BYTES_ABI, library)
}) .name()
const symbolPromise = contract .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() .symbol()
.catch(() => { .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)) return contractBytes32.symbol().then(bytes32 => ethers.utils.parseBytes32String(bytes32))
}) })
.catch(error => { .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 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]) => ({ // get the token balance of an address
decimals, export async function getTokenBalance(tokenAddress, address, library) {
symbol, if (!isAddress(tokenAddress) || !isAddress(address)) {
tokenAddress 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) { // get the token allowance
const factoryContract = getFactoryContract(networkId, signerOrProvider) 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; ...@@ -56,6 +56,7 @@ $pizazz-orange: #ff8f05;
&:disabled { &:disabled {
background-color: $mercury-gray; 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