Commit cb7dbe79 authored by ian-jh's avatar ian-jh

abort rebase fix conflicts

parents ab3faaa2 4e834c4c
**PLEASE DO NOT SUBMIT TOKEN ADDITIONS AS PULL REQUESTS**
All token requests should be made via an issue.
...@@ -25,3 +25,5 @@ yarn-error.log* ...@@ -25,3 +25,5 @@ yarn-error.log*
notes.txt notes.txt
.idea/ .idea/
.vscode/
\ No newline at end of file
...@@ -18,7 +18,8 @@ ...@@ -18,7 +18,8 @@
"unlock": "Unlock", "unlock": "Unlock",
"pending": "Pending", "pending": "Pending",
"selectToken": "Select a token", "selectToken": "Select a token",
"searchOrPaste": "Search Token or Paste Address", "searchOrPaste": "Search Token Name, Symbol, or Address",
"searchOrPasteMobile": "Name, Symbol, or Address",
"noExchange": "No Exchange Found", "noExchange": "No Exchange Found",
"exchangeRate": "Exchange Rate", "exchangeRate": "Exchange Rate",
"unknownError": "Oops! An unknown error occurred. Please refresh the page, or visit from another browser or device.", "unknownError": "Oops! An unknown error occurred. Please refresh the page, or visit from another browser or device.",
...@@ -36,6 +37,7 @@ ...@@ -36,6 +37,7 @@
"youWillReceive": "You will receive at least", "youWillReceive": "You will receive at least",
"youAreBuying": "You are buying", "youAreBuying": "You are buying",
"itWillCost": "It will cost at most", "itWillCost": "It will cost at most",
"forAtMost": "for at most",
"insufficientBalance": "Insufficient Balance", "insufficientBalance": "Insufficient Balance",
"inputNotValid": "Not a valid input value", "inputNotValid": "Not a valid input value",
"differentToken": "Must be different token.", "differentToken": "Must be different token.",
......
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 9.27455 20.9097 6.80375 19.1414 5" stroke="#AEAEAE" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
\ No newline at end of file
import React from 'react'
const SVGArrowDown = props => (
<svg width="1em" height="1em" viewBox="0 0 9 10" fill="currentColor" {...props}>
<path
d="M5.298 0H4.24v7.911h-.075L1.256 4.932l-.717.735L4.769 10 9 5.667l-.718-.735-2.908 2.979h-.076V0z"
fill="currentColor"
/>
</svg>
)
export default SVGArrowDown
...@@ -2,17 +2,17 @@ import React, { useState, useEffect } from 'react' ...@@ -2,17 +2,17 @@ import React, { useState, useEffect } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useWeb3Context } from 'web3-react' import { useWeb3Context } from 'web3-react'
import { lighten } from 'polished' import { transparentize } from 'polished'
import { isAddress } from '../../utils' import { isAddress } from '../../utils'
import { useDebounce } from '../../hooks' import { useDebounce } from '../../hooks'
const InputPanel = styled.div` const InputPanel = styled.div`
${({ theme }) => theme.flexColumnNoWrap} ${({ theme }) => theme.flexColumnNoWrap}
box-shadow: 0 4px 8px 0 ${({ theme }) => lighten(0.9, theme.royalBlue)}; box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.95, theme.royalBlue)};
position: relative; position: relative;
border-radius: 1.25rem; border-radius: 1.25rem;
background-color: ${({ theme }) => theme.white}; background-color: ${({ theme }) => theme.inputBackground};
z-index: 1; z-index: 1;
` `
...@@ -21,9 +21,10 @@ const ContainerRow = styled.div` ...@@ -21,9 +21,10 @@ const ContainerRow = styled.div`
justify-content: center; justify-content: center;
align-items: center; align-items: center;
border-radius: 1.25rem; border-radius: 1.25rem;
box-shadow: 0 0 0 1px ${({ error, theme }) => (error ? theme.salmonRed : theme.mercuryGray)}; border: 1px solid ${({ error, theme }) => (error ? theme.salmonRed : theme.mercuryGray)};
background-color: ${({ theme }) => theme.white};
transition: box-shadow 200ms ease-in-out; background-color: ${({ theme }) => theme.inputBackground};
transition: box-shadow 125ms ease-in-out;
` `
const InputContainer = styled.div` const InputContainer = styled.div`
...@@ -59,13 +60,15 @@ const Input = styled.input` ...@@ -59,13 +60,15 @@ const Input = styled.input`
border: none; border: none;
flex: 1 1 auto; flex: 1 1 auto;
width: 0; width: 0;
background-color: ${({ theme }) => theme.inputBackground};
color: ${({ error, theme }) => (error ? theme.salmonRed : theme.royalBlue)}; color: ${({ error, theme }) => (error ? theme.salmonRed : theme.royalBlue)};
transition: color 200ms ease-in-out; transition: color 125ms ease-in-out;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
::placeholder { ::placeholder {
color: ${({ theme }) => theme.chaliceGray}; color: ${({ theme }) => theme.placeholderGray};
} }
` `
...@@ -93,6 +96,7 @@ export default function AddressInputPanel({ title, initialInput = '', onChange = ...@@ -93,6 +96,7 @@ export default function AddressInputPanel({ title, initialInput = '', onChange =
let stale = false let stale = false
if (isAddress(debouncedInput)) { if (isAddress(debouncedInput)) {
try {
library library
.lookupAddress(debouncedInput) .lookupAddress(debouncedInput)
.then(name => { .then(name => {
...@@ -107,11 +111,18 @@ export default function AddressInputPanel({ title, initialInput = '', onChange = ...@@ -107,11 +111,18 @@ export default function AddressInputPanel({ title, initialInput = '', onChange =
} }
}) })
.catch(() => { .catch(() => {
if (!stale) {
setData({ address: debouncedInput, name: '' }) setData({ address: debouncedInput, name: '' })
setError(null) setError(null)
}
}) })
} catch {
setData({ address: debouncedInput, name: '' })
setError(null)
}
} else { } else {
if (debouncedInput !== '') { if (debouncedInput !== '') {
try {
library library
.resolveName(debouncedInput) .resolveName(debouncedInput)
.then(address => { .then(address => {
...@@ -126,8 +137,13 @@ export default function AddressInputPanel({ title, initialInput = '', onChange = ...@@ -126,8 +137,13 @@ export default function AddressInputPanel({ title, initialInput = '', onChange =
} }
}) })
.catch(() => { .catch(() => {
if (!stale) {
setError(true) setError(true)
}
}) })
} catch {
setError(true)
}
} }
} }
......
...@@ -2,8 +2,8 @@ import React, { Component } from 'react' ...@@ -2,8 +2,8 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import styled from 'styled-components' import styled from 'styled-components'
import DropdownBlue from '../../assets/images/dropdown-blue.svg' import { ReactComponent as Dropup } from '../../assets/images/dropup-blue.svg'
import DropupBlue from '../../assets/images/dropup-blue.svg' import { ReactComponent as Dropdown } from '../../assets/images/dropdown-blue.svg'
const SummaryWrapper = styled.div` const SummaryWrapper = styled.div`
color: ${({ error, theme }) => (error ? theme.salmonRed : theme.doveGray)}; color: ${({ error, theme }) => (error ? theme.salmonRed : theme.doveGray)};
...@@ -42,6 +42,20 @@ const SummaryWrapperContainer = styled.div` ...@@ -42,6 +42,20 @@ const SummaryWrapperContainer = styled.div`
} }
` `
const WrappedDropup = ({ isError, highSlippageWarning, ...rest }) => <Dropup {...rest} />
const ColoredDropup = styled(WrappedDropup)`
path {
stroke: ${({ theme }) => theme.royalBlue};
}
`
const WrappedDropdown = ({ isError, highSlippageWarning, ...rest }) => <Dropdown {...rest} />
const ColoredDropdown = styled(WrappedDropdown)`
path {
stroke: ${({ theme }) => theme.royalBlue};
}
`
class ContextualInfo extends Component { class ContextualInfo extends Component {
static propTypes = { static propTypes = {
openDetailsText: PropTypes.string, openDetailsText: PropTypes.string,
...@@ -89,12 +103,12 @@ class ContextualInfo extends Component { ...@@ -89,12 +103,12 @@ class ContextualInfo extends Component {
{!this.state.showDetails ? ( {!this.state.showDetails ? (
<> <>
<span>{openDetailsText}</span> <span>{openDetailsText}</span>
<img src={DropdownBlue} alt="dropdown" /> <ColoredDropup />
</> </>
) : ( ) : (
<> <>
<span>{closeDetailsText}</span> <span>{closeDetailsText}</span>
<img src={DropupBlue} alt="dropup" /> <ColoredDropdown />
</> </>
)} )}
</SummaryWrapperContainer> </SummaryWrapperContainer>
......
...@@ -32,8 +32,7 @@ const SummaryWrapperContainer = styled.div` ...@@ -32,8 +32,7 @@ const SummaryWrapperContainer = styled.div`
const Details = styled.div` const Details = styled.div`
background-color: ${({ theme }) => theme.concreteGray}; background-color: ${({ theme }) => theme.concreteGray};
padding: 1.5rem; /* padding: 1.25rem 1.25rem 1rem 1.25rem; */
padding-bottom: 1rem;
border-radius: 1rem; border-radius: 1rem;
font-size: 0.75rem; font-size: 0.75rem;
margin: 1rem 0.5rem 0 0.5rem; margin: 1rem 0.5rem 0 0.5rem;
...@@ -62,7 +61,7 @@ const ErrorSpan = styled.span` ...@@ -62,7 +61,7 @@ const ErrorSpan = styled.span`
const WrappedDropup = ({ isError, highSlippageWarning, ...rest }) => <Dropup {...rest} /> const WrappedDropup = ({ isError, highSlippageWarning, ...rest }) => <Dropup {...rest} />
const ColoredDropup = styled(WrappedDropup)` const ColoredDropup = styled(WrappedDropup)`
path { path {
stroke: ${({ isError, theme }) => isError && theme.salmonRed}; stroke: ${({ isError, theme }) => (isError ? theme.salmonRed : theme.royalBlue)};
${({ highSlippageWarning, theme }) => ${({ highSlippageWarning, theme }) =>
highSlippageWarning && highSlippageWarning &&
...@@ -75,7 +74,7 @@ const ColoredDropup = styled(WrappedDropup)` ...@@ -75,7 +74,7 @@ const ColoredDropup = styled(WrappedDropup)`
const WrappedDropdown = ({ isError, highSlippageWarning, ...rest }) => <Dropdown {...rest} /> const WrappedDropdown = ({ isError, highSlippageWarning, ...rest }) => <Dropdown {...rest} />
const ColoredDropdown = styled(WrappedDropdown)` const ColoredDropdown = styled(WrappedDropdown)`
path { path {
stroke: ${({ isError, theme }) => isError && theme.salmonRed}; stroke: ${({ isError, theme }) => (isError ? theme.salmonRed : theme.royalBlue)};
${({ highSlippageWarning, theme }) => ${({ highSlippageWarning, theme }) =>
highSlippageWarning && highSlippageWarning &&
......
...@@ -2,22 +2,28 @@ import React, { useState, useRef, useMemo } from 'react' ...@@ -2,22 +2,28 @@ import React, { useState, useRef, useMemo } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ethers } from 'ethers' import { ethers } from 'ethers'
import { BigNumber } from '@uniswap/sdk'
import styled from 'styled-components' import styled from 'styled-components'
import escapeStringRegex from 'escape-string-regexp' import escapeStringRegex from 'escape-string-regexp'
import { lighten, darken } from 'polished' import { darken } from 'polished'
import Tooltip from '@reach/tooltip' import Tooltip from '@reach/tooltip'
import '@reach/tooltip/styles.css' import '@reach/tooltip/styles.css'
import { isMobile } from 'react-device-detect' import { isMobile } from 'react-device-detect'
import { BorderlessInput } from '../../theme' import { BorderlessInput } from '../../theme'
import { useTokenContract } from '../../hooks' import { useTokenContract } from '../../hooks'
import { isAddress, calculateGasMargin } from '../../utils' import { isAddress, calculateGasMargin, formatToUsd, formatTokenBalance, formatEthBalance } from '../../utils'
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg' import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
import Modal from '../Modal' import Modal from '../Modal'
import TokenLogo from '../TokenLogo' import TokenLogo from '../TokenLogo'
import SearchIcon from '../../assets/images/magnifying-glass.svg' import SearchIcon from '../../assets/images/magnifying-glass.svg'
import { useTransactionAdder, usePendingApproval } from '../../contexts/Transactions' import { useTransactionAdder, usePendingApproval } from '../../contexts/Transactions'
import { useTokenDetails, useAllTokenDetails } from '../../contexts/Tokens' import { useTokenDetails, useAllTokenDetails } from '../../contexts/Tokens'
import close from '../../assets/images/x.svg'
import { transparentize } from 'polished'
import { Spinner } from '../../theme'
import Circle from '../../assets/images/circle-grey.svg'
import { useUSDPrice } from '../../contexts/Application'
const GAS_MARGIN = ethers.utils.bigNumberify(1000) const GAS_MARGIN = ethers.utils.bigNumberify(1000)
...@@ -34,29 +40,33 @@ const SubCurrencySelect = styled.button` ...@@ -34,29 +40,33 @@ const SubCurrencySelect = styled.button`
outline: none; outline: none;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
` `
const InputRow = styled.div` const InputRow = styled.div`
${({ theme }) => theme.flexRowNoWrap} ${({ theme }) => theme.flexRowNoWrap}
align-items: center; align-items: center;
padding: 0.25rem 0.85rem 0.75rem; padding: 0.25rem 0.85rem 0.75rem;
` `
const Input = styled(BorderlessInput)` const Input = styled(BorderlessInput)`
font-size: 1.5rem; font-size: 1.5rem;
color: ${({ error, theme }) => error && theme.salmonRed}; color: ${({ error, theme }) => error && theme.salmonRed};
background-color: ${({ theme }) => theme.inputBackground};
` `
const StyledBorderlessInput = styled(BorderlessInput)` const StyledBorderlessInput = styled(BorderlessInput)`
min-height: 1.75rem; min-height: 2.5rem;
flex-shrink: 0; flex-shrink: 0;
text-align: left;
padding-left: 1.6rem;
background-color: ${({ theme }) => theme.concreteGray};
` `
const CurrencySelect = styled.button` const CurrencySelect = styled.button`
align-items: center; align-items: center;
font-size: 1rem; font-size: 1rem;
color: ${({ selected, theme }) => (selected ? theme.black : theme.royalBlue)}; color: ${({ selected, theme }) => (selected ? theme.textColor : theme.royalBlue)};
height: 2rem; height: 2rem;
border: 1px solid ${({ selected, theme }) => (selected ? theme.mercuryGray : theme.royalBlue)}; border: 1px solid ${({ selected, theme }) => (selected ? theme.mercuryGray : theme.royalBlue)};
border-radius: 2.5rem; border-radius: 2.5rem;
...@@ -90,27 +100,28 @@ const StyledDropDown = styled(DropDown)` ...@@ -90,27 +100,28 @@ const StyledDropDown = styled(DropDown)`
height: 35%; height: 35%;
path { path {
stroke: ${({ selected, theme }) => (selected ? theme.black : theme.royalBlue)}; stroke: ${({ selected, theme }) => (selected ? theme.textColor : theme.royalBlue)};
} }
` `
const InputPanel = styled.div` const InputPanel = styled.div`
${({ theme }) => theme.flexColumnNoWrap} ${({ theme }) => theme.flexColumnNoWrap}
box-shadow: 0 4px 8px 0 ${({ theme }) => lighten(0.9, theme.royalBlue)}; box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.95, theme.royalBlue)};
position: relative; position: relative;
border-radius: 1.25rem; border-radius: 1.25rem;
background-color: ${({ theme }) => theme.white}; background-color: ${({ theme }) => theme.inputBackground};
z-index: 1; z-index: 1;
` `
const Container = styled.div` const Container = styled.div`
border-radius: 1.25rem; border-radius: 1.25rem;
box-shadow: 0 0 0 1px ${({ error, theme }) => (error ? theme.salmonRed : theme.mercuryGray)}; border: 1px solid ${({ error, theme }) => (error ? theme.salmonRed : theme.mercuryGray)};
background-color: ${({ theme }) => theme.white};
transition: box-shadow 200ms ease-in-out; background-color: ${({ theme }) => theme.inputBackground};
transition: box-shadow 150ms ease-out;
:focus-within { :focus-within {
box-shadow: 0 0 1px 1px ${({ theme }) => theme.malibuBlue}; border: 1px solid ${({ theme }) => theme.malibuBlue};
} }
` `
...@@ -145,14 +156,30 @@ const ErrorSpan = styled.span` ...@@ -145,14 +156,30 @@ const ErrorSpan = styled.span`
const TokenModal = styled.div` const TokenModal = styled.div`
${({ theme }) => theme.flexColumnNoWrap} ${({ theme }) => theme.flexColumnNoWrap}
background-color: ${({ theme }) => theme.white};
width: 100%; width: 100%;
` `
const ModalHeader = styled.div`
position: relative;
display: flex;
flex-direction: row;
align-items: center;
padding: 0 2rem;
height: 60px;
`
const CloseIcon = styled.div`
position: absolute;
right: 1.4rem;
&:hover {
cursor: pointer;
}
`
const SearchContainer = styled.div` const SearchContainer = styled.div`
${({ theme }) => theme.flexRowNoWrap} ${({ theme }) => theme.flexRowNoWrap}
padding: 1rem; padding: 0.5rem 2rem;
border-bottom: 1px solid ${({ theme }) => theme.mercuryGray}; background-color: ${({ theme }) => theme.concreteGray};
` `
const TokenModalInfo = styled.div` const TokenModalInfo = styled.div`
...@@ -174,9 +201,8 @@ const TokenList = styled.div` ...@@ -174,9 +201,8 @@ const TokenList = styled.div`
const TokenModalRow = styled.div` const TokenModalRow = styled.div`
${({ theme }) => theme.flexRowNoWrap} ${({ theme }) => theme.flexRowNoWrap}
align-items: center; align-items: center;
padding: 1rem 1.5rem;
margin: 0.25rem 0.5rem;
justify-content: space-between; justify-content: space-between;
padding: 0.8rem 2rem;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
...@@ -185,16 +211,55 @@ const TokenModalRow = styled.div` ...@@ -185,16 +211,55 @@ const TokenModalRow = styled.div`
} }
:hover { :hover {
background-color: ${({ theme }) => theme.concreteGray}; background-color: ${({ theme }) => theme.tokenRowHover};
} }
${({ theme }) => theme.mediaWidth.upToMedium`padding: 0.8rem 1rem;`}
`
const TokenRowLeft = styled.div`
${({ theme }) => theme.flexRowNoWrap}
align-items : center;
`
const TokenSymbolGroup = styled.div`
${({ theme }) => theme.flexColumnNoWrap};
margin-left: 1rem;
`
const TokenFullName = styled.div`
color: ${({ theme }) => theme.chaliceGray};
`
const TokenRowBalance = styled.div`
font-size: 1rem;
line-height: 20px;
`
const TokenRowUsd = styled.div`
font-size: 1rem;
line-height: 1.5rem;
color: ${({ theme }) => theme.chaliceGray};
`
const TokenRowRight = styled.div`
${({ theme }) => theme.flexColumnNoWrap};
align-items: flex-end;
` `
const StyledTokenName = styled.span` const StyledTokenName = styled.span`
margin: 0 0.25rem 0 0.25rem; margin: 0 0.25rem 0 0.25rem;
` `
const SpinnerWrapper = styled(Spinner)`
margin: 0 0.25rem 0 0.25rem;
color: ${({ theme }) => theme.chaliceGray};
opacity: 0.6;
`
export default function CurrencyInputPanel({ export default function CurrencyInputPanel({
onValueChange = () => {}, onValueChange = () => {},
allBalances,
renderInput, renderInput,
onCurrencySelected = () => {}, onCurrencySelected = () => {},
title, title,
...@@ -233,7 +298,6 @@ export default function CurrencyInputPanel({ ...@@ -233,7 +298,6 @@ export default function CurrencyInputPanel({
selectedTokenExchangeAddress, selectedTokenExchangeAddress,
ethers.constants.MaxUint256 ethers.constants.MaxUint256
) )
tokenContract tokenContract
.approve(selectedTokenExchangeAddress, ethers.constants.MaxUint256, { .approve(selectedTokenExchangeAddress, ethers.constants.MaxUint256, {
gasLimit: calculateGasMargin(estimatedGas, GAS_MARGIN) gasLimit: calculateGasMargin(estimatedGas, GAS_MARGIN)
...@@ -334,48 +398,106 @@ export default function CurrencyInputPanel({ ...@@ -334,48 +398,106 @@ export default function CurrencyInputPanel({
{!disableTokenSelect && ( {!disableTokenSelect && (
<CurrencySelectModal <CurrencySelectModal
isOpen={modalIsOpen} isOpen={modalIsOpen}
// isOpen={true}
onDismiss={() => { onDismiss={() => {
setModalIsOpen(false) setModalIsOpen(false)
}} }}
onTokenSelect={onCurrencySelected} onTokenSelect={onCurrencySelected}
allBalances={allBalances}
/> />
)} )}
</InputPanel> </InputPanel>
) )
} }
function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect }) { function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect, allBalances }) {
const { t } = useTranslation() const { t } = useTranslation()
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const { exchangeAddress } = useTokenDetails(searchQuery) const { exchangeAddress } = useTokenDetails(searchQuery)
const allTokens = useAllTokenDetails() const allTokens = useAllTokenDetails()
// BigNumber.js instance
const ethPrice = useUSDPrice()
const _usdAmounts = Object.keys(allTokens).map(k => {
if (
ethPrice &&
allBalances &&
allBalances[k] &&
allBalances[k].ethRate &&
!allBalances[k].ethRate.isNaN() &&
allBalances[k].balance
) {
const USDRate = ethPrice.times(allBalances[k].ethRate)
const balanceBigNumber = new BigNumber(allBalances[k].balance.toString())
const usdBalance = balanceBigNumber.times(USDRate).div(new BigNumber(10).pow(allTokens[k].decimals))
return usdBalance
} else {
return null
}
})
const usdAmounts =
_usdAmounts &&
Object.keys(allTokens).reduce(
(accumulator, currentValue, i) => Object.assign({ [currentValue]: _usdAmounts[i] }, accumulator),
{}
)
const tokenList = useMemo(() => { const tokenList = useMemo(() => {
return Object.keys(allTokens) return Object.keys(allTokens)
.sort((a, b) => { .sort((a, b) => {
const aSymbol = allTokens[a].symbol.toLowerCase() const aSymbol = allTokens[a].symbol.toLowerCase()
const bSymbol = allTokens[b].symbol.toLowerCase() const bSymbol = allTokens[b].symbol.toLowerCase()
if (aSymbol === 'ETH'.toLowerCase() || bSymbol === 'ETH'.toLowerCase()) { if (aSymbol === 'ETH'.toLowerCase() || bSymbol === 'ETH'.toLowerCase()) {
return aSymbol === bSymbol ? 0 : aSymbol === 'ETH'.toLowerCase() ? -1 : 1 return aSymbol === bSymbol ? 0 : aSymbol === 'ETH'.toLowerCase() ? -1 : 1
} else {
return aSymbol < bSymbol ? -1 : aSymbol > bSymbol ? 1 : 0
} }
if (usdAmounts[a] && !usdAmounts[b]) {
return -1
} else if (usdAmounts[b] && !usdAmounts[a]) {
return 1
}
// check for balance - sort by value
if (usdAmounts[a] && usdAmounts[b]) {
const aUSD = usdAmounts[a]
const bUSD = usdAmounts[b]
return aUSD.gt(bUSD) ? -1 : aUSD.lt(bUSD) ? 1 : 0
}
return aSymbol < bSymbol ? -1 : aSymbol > bSymbol ? 1 : 0
}) })
.map(k => { .map(k => {
let balance
let usdBalance
// only update if we have data
if (k === 'ETH' && allBalances && allBalances[k]) {
balance = formatEthBalance(allBalances[k].balance)
usdBalance = usdAmounts[k]
} else if (allBalances && allBalances[k]) {
balance = formatTokenBalance(allBalances[k].balance, allTokens[k].decimals)
usdBalance = usdAmounts[k]
}
return { return {
name: allTokens[k].name, name: allTokens[k].name,
symbol: allTokens[k].symbol, symbol: allTokens[k].symbol,
address: k address: k,
balance: balance,
usdBalance: usdBalance
} }
}) })
}, [allTokens]) }, [allBalances, allTokens, usdAmounts])
const filteredTokenList = useMemo(() => { const filteredTokenList = useMemo(() => {
return tokenList.filter(tokenEntry => { return tokenList.filter(tokenEntry => {
// check the regex for each field // check the regex for each field
const regexMatches = Object.keys(tokenEntry).map(tokenEntryKey => { const regexMatches = Object.keys(tokenEntry).map(tokenEntryKey => {
return ( return (
tokenEntry[tokenEntryKey] && typeof tokenEntry[tokenEntryKey] === 'string' &&
!!tokenEntry[tokenEntryKey].match(new RegExp(escapeStringRegex(searchQuery), 'i')) !!tokenEntry[tokenEntryKey].match(new RegExp(escapeStringRegex(searchQuery), 'i'))
) )
}) })
...@@ -394,7 +516,6 @@ function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect }) { ...@@ -394,7 +516,6 @@ function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect }) {
if (isAddress(searchQuery) && exchangeAddress === undefined) { if (isAddress(searchQuery) && exchangeAddress === undefined) {
return <TokenModalInfo>Searching for Exchange...</TokenModalInfo> return <TokenModalInfo>Searching for Exchange...</TokenModalInfo>
} }
if (isAddress(searchQuery) && exchangeAddress === ethers.constants.AddressZero) { if (isAddress(searchQuery) && exchangeAddress === ethers.constants.AddressZero) {
return ( return (
<> <>
...@@ -405,16 +526,30 @@ function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect }) { ...@@ -405,16 +526,30 @@ function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect }) {
</> </>
) )
} }
if (!filteredTokenList.length) { if (!filteredTokenList.length) {
return <TokenModalInfo>{t('noExchange')}</TokenModalInfo> return <TokenModalInfo>{t('noExchange')}</TokenModalInfo>
} }
return filteredTokenList.map(({ address, symbol }) => { return filteredTokenList.map(({ address, symbol, name, balance, usdBalance }) => {
return ( return (
<TokenModalRow key={address} onClick={() => _onTokenSelect(address)}> <TokenModalRow key={address} onClick={() => _onTokenSelect(address)}>
<TokenLogo address={address} /> <TokenRowLeft>
<TokenLogo address={address} size={'2rem'} />
<TokenSymbolGroup>
<span id="symbol">{symbol}</span> <span id="symbol">{symbol}</span>
<TokenFullName>{name}</TokenFullName>
</TokenSymbolGroup>
</TokenRowLeft>
<TokenRowRight>
{balance ? (
<TokenRowBalance>{balance && (balance > 0 || balance === '<0.0001') ? balance : '-'}</TokenRowBalance>
) : (
<SpinnerWrapper src={Circle} alt="loader" />
)}
<TokenRowUsd>
{usdBalance ? (usdBalance.lt(0.01) ? '<$0.01' : '$' + formatToUsd(usdBalance)) : ''}
</TokenRowUsd>
</TokenRowRight>
</TokenModalRow> </TokenModalRow>
) )
}) })
...@@ -429,12 +564,33 @@ function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect }) { ...@@ -429,12 +564,33 @@ function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect }) {
setSearchQuery(checksummedInput || input) setSearchQuery(checksummedInput || input)
} }
function clearInputAndDismiss() {
setSearchQuery('')
onDismiss()
}
return ( return (
<Modal isOpen={isOpen} onDismiss={onDismiss} minHeight={50} initialFocusRef={isMobile ? undefined : inputRef}> <Modal
isOpen={isOpen}
onDismiss={clearInputAndDismiss}
minHeight={60}
initialFocusRef={isMobile ? undefined : inputRef}
>
<TokenModal> <TokenModal>
<ModalHeader>
<p>Select Token</p>
<CloseIcon onClick={clearInputAndDismiss}>
<img src={close} alt={'close icon'} />
</CloseIcon>
</ModalHeader>
<SearchContainer> <SearchContainer>
<StyledBorderlessInput ref={inputRef} type="text" placeholder={t('searchOrPaste')} onChange={onInput} />
<img src={SearchIcon} alt="search" /> <img src={SearchIcon} alt="search" />
<StyledBorderlessInput
ref={inputRef}
type="text"
placeholder={isMobile ? t('searchOrPasteMobile') : t('searchOrPaste')}
onChange={onInput}
/>
</SearchContainer> </SearchContainer>
<TokenList>{renderTokenList()}</TokenList> <TokenList>{renderTokenList()}</TokenList>
</TokenModal> </TokenModal>
......
import React, { useState, useReducer, useEffect } from 'react' import React, { useState, useReducer, useEffect } from 'react'
import ReactGA from 'react-ga' import ReactGA from 'react-ga'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useWeb3Context } from 'web3-react' import { useWeb3Context } from 'web3-react'
import { ethers } from 'ethers' import { ethers } from 'ethers'
import styled from 'styled-components' import styled from 'styled-components'
...@@ -10,13 +12,13 @@ import CurrencyInputPanel from '../CurrencyInputPanel' ...@@ -10,13 +12,13 @@ import CurrencyInputPanel from '../CurrencyInputPanel'
import AddressInputPanel from '../AddressInputPanel' import AddressInputPanel from '../AddressInputPanel'
import OversizedPanel from '../OversizedPanel' import OversizedPanel from '../OversizedPanel'
import TransactionDetails from '../TransactionDetails' import TransactionDetails from '../TransactionDetails'
import ArrowDownBlue from '../../assets/images/arrow-down-blue.svg' import ArrowDown from '../../assets/svg/SVGArrowDown'
import ArrowDownGrey from '../../assets/images/arrow-down-grey.svg'
import { amountFormatter, calculateGasMargin } from '../../utils' import { amountFormatter, calculateGasMargin } from '../../utils'
import { useExchangeContract } from '../../hooks' import { useExchangeContract } from '../../hooks'
import { useTokenDetails } from '../../contexts/Tokens' import { useTokenDetails } from '../../contexts/Tokens'
import { useTransactionAdder } from '../../contexts/Transactions' import { useTransactionAdder } from '../../contexts/Transactions'
import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances' import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances'
import { useFetchAllBalances } from '../../contexts/AllBalances'
import { useAddressAllowance } from '../../contexts/Allowances' import { useAddressAllowance } from '../../contexts/Allowances'
const INPUT = 0 const INPUT = 0
...@@ -27,23 +29,24 @@ const TOKEN_TO_ETH = 1 ...@@ -27,23 +29,24 @@ const TOKEN_TO_ETH = 1
const TOKEN_TO_TOKEN = 2 const TOKEN_TO_TOKEN = 2
// denominated in bips // denominated in bips
const ALLOWED_SLIPPAGE_DEFAULT = 150 const ALLOWED_SLIPPAGE_DEFAULT = 100
const TOKEN_ALLOWED_SLIPPAGE_DEFAULT = 200 const TOKEN_ALLOWED_SLIPPAGE_DEFAULT = 100
// denominated in seconds // 15 minutes, denominated in seconds
const DEADLINE_FROM_NOW = 60 * 15 const DEADLINE_FROM_NOW = 60 * 15
// denominated in bips // % above the calculated gas cost that we actually send, denominated in bips
const GAS_MARGIN = ethers.utils.bigNumberify(1000) const GAS_MARGIN = ethers.utils.bigNumberify(1000)
const DownArrowBackground = styled.div` const DownArrowBackground = styled.div`
${({ theme }) => theme.flexRowNoWrap} ${({ theme }) => theme.flexRowNoWrap}
justify-content: center; justify-content: center;
align-items: center; align-items: center;
` `
const DownArrow = styled.img` const WrappedArrowDown = ({ clickable, active, ...rest }) => <ArrowDown {...rest} />
const DownArrow = styled(WrappedArrowDown)`
color: ${({ theme, active }) => (active ? theme.royalBlue : theme.chaliceGray)};
width: 0.625rem; width: 0.625rem;
height: 0.625rem; height: 0.625rem;
position: relative; position: relative;
...@@ -62,7 +65,7 @@ const ExchangeRateWrapper = styled.div` ...@@ -62,7 +65,7 @@ const ExchangeRateWrapper = styled.div`
const ExchangeRate = styled.span` const ExchangeRate = styled.span`
flex: 1 1 auto; flex: 1 1 auto;
width: 0; width: 0;
color: ${({ theme }) => theme.chaliceGray}; color: ${({ theme }) => theme.doveGray};
` `
const Flex = styled.div` const Flex = styled.div`
...@@ -241,16 +244,17 @@ export default function ExchangePage({ initialCurrency, sending }) { ...@@ -241,16 +244,17 @@ export default function ExchangePage({ initialCurrency, sending }) {
const [rawSlippage, setRawSlippage] = useState(ALLOWED_SLIPPAGE_DEFAULT) const [rawSlippage, setRawSlippage] = useState(ALLOWED_SLIPPAGE_DEFAULT)
const [rawTokenSlippage, setRawTokenSlippage] = useState(TOKEN_ALLOWED_SLIPPAGE_DEFAULT) const [rawTokenSlippage, setRawTokenSlippage] = useState(TOKEN_ALLOWED_SLIPPAGE_DEFAULT)
let allowedSlippageBig = ethers.utils.bigNumberify(rawSlippage) const allowedSlippageBig = ethers.utils.bigNumberify(rawSlippage)
let tokenAllowedSlippageBig = ethers.utils.bigNumberify(rawTokenSlippage) const tokenAllowedSlippageBig = ethers.utils.bigNumberify(rawTokenSlippage)
// analytics // analytics
useEffect(() => { useEffect(() => {
ReactGA.pageview(window.location.pathname + window.location.search) ReactGA.pageview(window.location.pathname + window.location.search)
}, []) }, [])
// core swap state- // core swap state
const [swapState, dispatchSwapState] = useReducer(swapStateReducer, initialCurrency, getInitialSwapState) const [swapState, dispatchSwapState] = useReducer(swapStateReducer, initialCurrency, getInitialSwapState)
const { independentValue, dependentValue, independentField, inputCurrency, outputCurrency } = swapState const { independentValue, dependentValue, independentField, inputCurrency, outputCurrency } = swapState
const [recipient, setRecipient] = useState({ address: '', name: '' }) const [recipient, setRecipient] = useState({ address: '', name: '' })
...@@ -259,7 +263,7 @@ export default function ExchangePage({ initialCurrency, sending }) { ...@@ -259,7 +263,7 @@ export default function ExchangePage({ initialCurrency, sending }) {
// get swap type from the currency types // get swap type from the currency types
const swapType = getSwapType(inputCurrency, outputCurrency) const swapType = getSwapType(inputCurrency, outputCurrency)
// get decimals and exchange addressfor each of the currency types // get decimals and exchange address for each of the currency types
const { symbol: inputSymbol, decimals: inputDecimals, exchangeAddress: inputExchangeAddress } = useTokenDetails( const { symbol: inputSymbol, decimals: inputDecimals, exchangeAddress: inputExchangeAddress } = useTokenDetails(
inputCurrency inputCurrency
) )
...@@ -583,10 +587,13 @@ export default function ExchangePage({ initialCurrency, sending }) { ...@@ -583,10 +587,13 @@ export default function ExchangePage({ initialCurrency, sending }) {
const [customSlippageError, setcustomSlippageError] = useState('') const [customSlippageError, setcustomSlippageError] = useState('')
const allBalances = useFetchAllBalances()
return ( return (
<> <>
<CurrencyInputPanel <CurrencyInputPanel
title={t('input')} title={t('input')}
allBalances={allBalances}
description={inputValueFormatted && independentField === OUTPUT ? estimatedText : ''} description={inputValueFormatted && independentField === OUTPUT ? estimatedText : ''}
extraText={inputBalanceFormatted && formatBalance(inputBalanceFormatted)} extraText={inputBalanceFormatted && formatBalance(inputBalanceFormatted)}
extraTextClickHander={() => { extraTextClickHander={() => {
...@@ -620,12 +627,13 @@ export default function ExchangePage({ initialCurrency, sending }) { ...@@ -620,12 +627,13 @@ export default function ExchangePage({ initialCurrency, sending }) {
}} }}
clickable clickable
alt="swap" alt="swap"
src={isValid ? ArrowDownBlue : ArrowDownGrey} active={isValid}
/> />
</DownArrowBackground> </DownArrowBackground>
</OversizedPanel> </OversizedPanel>
<CurrencyInputPanel <CurrencyInputPanel
title={t('output')} title={t('output')}
allBalances={allBalances}
description={outputValueFormatted && independentField === INPUT ? estimatedText : ''} description={outputValueFormatted && independentField === INPUT ? estimatedText : ''}
extraText={outputBalanceFormatted && formatBalance(outputBalanceFormatted)} extraText={outputBalanceFormatted && formatBalance(outputBalanceFormatted)}
onCurrencySelected={outputCurrency => { onCurrencySelected={outputCurrency => {
...@@ -644,7 +652,7 @@ export default function ExchangePage({ initialCurrency, sending }) { ...@@ -644,7 +652,7 @@ export default function ExchangePage({ initialCurrency, sending }) {
<> <>
<OversizedPanel> <OversizedPanel>
<DownArrowBackground> <DownArrowBackground>
<DownArrow src={isValid ? ArrowDownBlue : ArrowDownGrey} alt="arrow" /> <DownArrow active={isValid} alt="arrow" />
</DownArrowBackground> </DownArrowBackground>
</OversizedPanel> </OversizedPanel>
<AddressInputPanel onChange={setRecipient} onError={setRecipientError} /> <AddressInputPanel onChange={setRecipient} onError={setRecipientError} />
...@@ -662,13 +670,13 @@ export default function ExchangePage({ initialCurrency, sending }) { ...@@ -662,13 +670,13 @@ export default function ExchangePage({ initialCurrency, sending }) {
{inverted ? ( {inverted ? (
<span> <span>
{exchangeRate {exchangeRate
? `1 ${outputSymbol} = ${amountFormatter(exchangeRateInverted, 18, 4, false)} ${inputSymbol}` ? `1 ${inputSymbol} = ${amountFormatter(exchangeRate, 18, 4, false)} ${outputSymbol}`
: ' - '} : ' - '}
</span> </span>
) : ( ) : (
<span> <span>
{exchangeRate {exchangeRate
? `1 ${inputSymbol} = ${amountFormatter(exchangeRate, 18, 4, false)} ${outputSymbol}` ? `1 ${outputSymbol} = ${amountFormatter(exchangeRateInverted, 18, 4, false)} ${inputSymbol}`
: ' - '} : ' - '}
</span> </span>
)} )}
......
import React from 'react'
import styled from 'styled-components'
import { darken, transparentize } from 'polished'
import Toggle from 'react-switch'
import { Link } from '../../theme'
import { useDarkModeManager } from '../../contexts/LocalStorage'
const FooterFrame = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
`
const FooterElement = styled.div`
margin: 1.25rem;
display: flex;
min-width: 0;
display: flex;
align-items: center;
`
const Title = styled.div`
display: flex;
align-items: center;
color: ${({ theme }) => theme.uniswapPink};
:hover {
cursor: pointer;
}
#link {
text-decoration-color: ${({ theme }) => theme.uniswapPink};
}
#title {
display: inline;
font-size: 0.825rem;
margin-right: 12px;
font-weight: 400;
color: ${({ theme }) => theme.uniswapPink};
:hover {
color: ${({ theme }) => darken(0.2, theme.uniswapPink)};
}
}
`
const StyledToggle = styled(Toggle)`
margin-right: 24px;
.react-switch-bg[style] {
background-color: ${({ theme }) => theme.inputBackground} !important;
border: 1px solid ${({ theme }) => theme.concreteGray} !important;
}
.react-switch-handle[style] {
background-color: ${({ theme }) => theme.inputBackground};
box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.93, theme.royalBlue)};
border: 1px solid ${({ theme }) => theme.mercuryGray};
border-color: ${({ theme }) => theme.mercuryGray} !important;
}
`
const EmojiToggle = styled.span`
display: flex;
justify-content: center;
align-items: center;
height: 100%;
font-family: Arial sans-serif;
`
export default function Footer() {
const [isDark, toggleDarkMode] = useDarkModeManager()
return (
<FooterFrame>
<FooterElement>
<Title>
<Link id="link" href="https://uniswap.io/">
<h1 id="title">About</h1>
</Link>
<Link id="link" href="https://docs.uniswap.io/">
<h1 id="title">Docs</h1>
</Link>
<Link id="link" href="https://github.com/Uniswap">
<h1 id="title">Code</h1>
</Link>
</Title>
</FooterElement>
<StyledToggle
checked={!isDark}
uncheckedIcon={
<EmojiToggle role="img" aria-label="moon">
{/* eslint-disable-line jsx-a11y/accessible-emoji */}
🌙️
</EmojiToggle>
}
checkedIcon={
<EmojiToggle role="img" aria-label="sun">
{/* eslint-disable-line jsx-a11y/accessible-emoji */}
{'☀️'}
</EmojiToggle>
}
onChange={() => toggleDarkMode()}
/>
</FooterFrame>
)
}
...@@ -5,23 +5,40 @@ import { Link } from '../../theme' ...@@ -5,23 +5,40 @@ import { Link } from '../../theme'
import Web3Status from '../Web3Status' import Web3Status from '../Web3Status'
import { darken } from 'polished' import { darken } from 'polished'
const HeaderFrame = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
`
const HeaderElement = styled.div` const HeaderElement = styled.div`
margin: 1.25rem; margin: 1.25rem;
display: flex; display: flex;
min-width: 0; min-width: 0;
display: flex;
align-items: center;
`
const Nod = styled.span`
transform: rotate(0deg);
transition: transform 150ms ease-out;
:hover {
transform: rotate(-10deg);
}
` `
const Title = styled.div` const Title = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
#image { :hover {
font-size: 1.5rem; cursor: pointer;
margin-right: 1rem;
} }
#link { #link {
text-decoration-color: ${({ theme }) => theme.wisteriaPurple}; text-decoration-color: ${({ theme }) => theme.UniswapPink};
} }
#title { #title {
...@@ -37,22 +54,24 @@ const Title = styled.div` ...@@ -37,22 +54,24 @@ const Title = styled.div`
export default function Header() { export default function Header() {
return ( return (
<> <HeaderFrame>
<HeaderElement> <HeaderElement>
<Title> <Title>
<span id="image" role="img" aria-label="Unicorn Emoji"> <Nod>
🦄 <Link id="link" href="https://uniswap.io">
<span role="img" aria-label="unicorn">
🦄{' '}
</span> </span>
</Link>
</Nod>
<Link id="link" href="https://uniswap.io"> <Link id="link" href="https://uniswap.io">
<h1 id="title">Uniswap</h1> <h1 id="title">Uniswap</h1>
</Link> </Link>
</Title> </Title>
</HeaderElement> </HeaderElement>
<HeaderElement> <HeaderElement>
<Web3Status /> <Web3Status />
</HeaderElement> </HeaderElement>
</> </HeaderFrame>
) )
} }
...@@ -5,8 +5,9 @@ import { DialogOverlay, DialogContent } from '@reach/dialog' ...@@ -5,8 +5,9 @@ import { DialogOverlay, DialogContent } from '@reach/dialog'
import '@reach/dialog/styles.css' import '@reach/dialog/styles.css'
const AnimatedDialogOverlay = animated(DialogOverlay) const AnimatedDialogOverlay = animated(DialogOverlay)
const StyledDialogOverlay = styled(AnimatedDialogOverlay).attrs({ const WrappedDialogOverlay = ({ suppressClassNameWarning, ...rest }) => <AnimatedDialogOverlay {...rest} />
suppressclassnamewarning: 'true' const StyledDialogOverlay = styled(WrappedDialogOverlay).attrs({
suppressClassNameWarning: true
})` })`
&[data-reach-dialog-overlay] { &[data-reach-dialog-overlay] {
z-index: 2; z-index: 2;
...@@ -20,12 +21,14 @@ const FilteredDialogContent = ({ minHeight, ...rest }) => <DialogContent {...res ...@@ -20,12 +21,14 @@ const FilteredDialogContent = ({ minHeight, ...rest }) => <DialogContent {...res
const StyledDialogContent = styled(FilteredDialogContent)` const StyledDialogContent = styled(FilteredDialogContent)`
&[data-reach-dialog-content] { &[data-reach-dialog-content] {
margin: 0 0 2rem 0; margin: 0 0 2rem 0;
${({ theme }) => theme.mediaWidth.upToMedium`margin: 0;`} border: 1px solid ${({ theme }) => theme.concreteGray};
padding: 0; background-color: ${({ theme }) => theme.inputBackground};
${({ theme }) => theme.mediaWidth.upToMedium`margin: 0;`};
padding: 0px;
width: 50vw; width: 50vw;
max-width: 650px; max-width: 650px;
${({ theme }) => theme.mediaWidth.upToMedium`width: 65vw;`} ${({ theme }) => theme.mediaWidth.upToMedium`width: 65vw;`}
${({ theme }) => theme.mediaWidth.upToSmall`width: 80vw;`} ${({ theme }) => theme.mediaWidth.upToSmall`width: 85vw;`}
max-height: 50vh; max-height: 50vh;
${({ minHeight }) => ${({ minHeight }) =>
minHeight && minHeight &&
...@@ -36,7 +39,7 @@ const StyledDialogContent = styled(FilteredDialogContent)` ...@@ -36,7 +39,7 @@ const StyledDialogContent = styled(FilteredDialogContent)`
${({ theme }) => theme.mediaWidth.upToSmall`max-height: 80vh;`} ${({ theme }) => theme.mediaWidth.upToSmall`max-height: 80vh;`}
display: flex; display: flex;
overflow: hidden; overflow: hidden;
border-radius: 1.5rem; border-radius: 10px;
} }
` `
...@@ -50,7 +53,7 @@ const HiddenCloseButton = styled.button` ...@@ -50,7 +53,7 @@ const HiddenCloseButton = styled.button`
export default function Modal({ isOpen, onDismiss, minHeight = false, initialFocusRef, children }) { export default function Modal({ isOpen, onDismiss, minHeight = false, initialFocusRef, children }) {
const transitions = useTransition(isOpen, null, { const transitions = useTransition(isOpen, null, {
config: { duration: 125 }, config: { duration: 150 },
from: { opacity: 0 }, from: { opacity: 0 },
enter: { opacity: 1 }, enter: { opacity: 1 },
leave: { opacity: 0 } leave: { opacity: 0 }
......
...@@ -60,7 +60,7 @@ const Tabs = styled.div` ...@@ -60,7 +60,7 @@ const Tabs = styled.div`
height: 2.5rem; height: 2.5rem;
background-color: ${({ theme }) => theme.concreteGray}; background-color: ${({ theme }) => theme.concreteGray};
border-radius: 3rem; border-radius: 3rem;
box-shadow: 0 0 0 1px ${({ theme }) => darken(0.05, theme.concreteGray)}; /* border: 1px solid ${({ theme }) => theme.mercuryGray}; */
margin-bottom: 1rem; margin-bottom: 1rem;
` `
...@@ -73,6 +73,7 @@ const StyledNavLink = styled(NavLink).attrs({ ...@@ -73,6 +73,7 @@ const StyledNavLink = styled(NavLink).attrs({
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 2.5rem; height: 2.5rem;
border: 1px solid ${({ theme }) => transparentize(1, theme.mercuryGray)};
flex: 1 0 auto; flex: 1 0 auto;
border-radius: 3rem; border-radius: 3rem;
outline: none; outline: none;
...@@ -80,21 +81,24 @@ const StyledNavLink = styled(NavLink).attrs({ ...@@ -80,21 +81,24 @@ const StyledNavLink = styled(NavLink).attrs({
text-decoration: none; text-decoration: none;
color: ${({ theme }) => theme.doveGray}; color: ${({ theme }) => theme.doveGray};
font-size: 1rem; font-size: 1rem;
box-sizing: border-box;
&.${activeClassName} { &.${activeClassName} {
background-color: ${({ theme }) => theme.white}; background-color: ${({ theme }) => theme.inputBackground};
border-radius: 3rem; border-radius: 3rem;
box-shadow: 0 0 1px 1px ${({ theme }) => theme.mercuryGray}; border: 1px solid ${({ theme }) => theme.mercuryGray};
box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.95, theme.royalBlue)};
box-sizing: border-box;
font-weight: 500; font-weight: 500;
color: ${({ theme }) => theme.royalBlue}; color: ${({ theme }) => theme.royalBlue};
:hover { :hover {
box-shadow: 0 0 1px 1px ${({ theme }) => darken(0.1, theme.mercuryGray)}; border: 1px solid ${({ theme }) => darken(0.1, theme.mercuryGray)};
background-color: ${({ theme }) => darken(0.01, theme.inputBackground)};
} }
} }
:hover, :hover,
:focus { :focus {
font-weight: 500;
color: ${({ theme }) => darken(0.1, theme.royalBlue)}; color: ${({ theme }) => darken(0.1, theme.royalBlue)};
} }
` `
......
...@@ -3,18 +3,20 @@ import styled from 'styled-components' ...@@ -3,18 +3,20 @@ import styled from 'styled-components'
import { ReactComponent as EthereumLogo } from '../../assets/images/ethereum-logo.svg' import { ReactComponent as EthereumLogo } from '../../assets/images/ethereum-logo.svg'
const TOKEN_ICON_API = 'https://raw.githubusercontent.com/TrustWallet/tokens/master/tokens' const TOKEN_ICON_API = address =>
`https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${address}/logo.png`
const BAD_IMAGES = {} const BAD_IMAGES = {}
const Image = styled.img` const Image = styled.img`
width: ${({ size }) => size}; width: ${({ size }) => size};
height: ${({ size }) => size}; height: ${({ size }) => size};
background-color: white;
border-radius: 1rem; border-radius: 1rem;
` `
const Emoji = styled.span` const Emoji = styled.span`
width: ${({ size }) => size}; width: ${({ size }) => size};
font-size: ${({ size }) => size}; height: ${({ size }) => size};
` `
const StyledEthereumLogo = styled(EthereumLogo)` const StyledEthereumLogo = styled(EthereumLogo)`
...@@ -29,10 +31,10 @@ export default function TokenLogo({ address, size = '1rem', ...rest }) { ...@@ -29,10 +31,10 @@ export default function TokenLogo({ address, size = '1rem', ...rest }) {
if (address === 'ETH') { if (address === 'ETH') {
return <StyledEthereumLogo size={size} /> return <StyledEthereumLogo size={size} />
} else if (!error && !BAD_IMAGES[address]) { } else if (!error && !BAD_IMAGES[address]) {
path = `${TOKEN_ICON_API}/${address.toLowerCase()}.png` path = TOKEN_ICON_API(address.toLowerCase())
} else { } else {
return ( return (
<Emoji {...rest}> <Emoji {...rest} size={size}>
<span role="img" aria-label="Thinking"> <span role="img" aria-label="Thinking">
🤔 🤔
</span> </span>
......
import React, { useState } from 'react' import React, { useState, useEffect, useRef } from 'react'
import ReactGA from 'react-ga' import ReactGA from 'react-ga'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled, { css, keyframes } from 'styled-components' import styled, { css, keyframes } from 'styled-components'
import { darken, lighten } from 'polished' import { darken, lighten } from 'polished'
import { amountFormatter } from '../../utils' import { isAddress, amountFormatter } from '../../utils'
import { useDebounce } from '../../hooks' import { useDebounce } from '../../hooks'
import question from '../../assets/images/question.svg' import question from '../../assets/images/question.svg'
import NewContextualInfo from '../../components/ContextualInfoNew' import NewContextualInfo from '../../components/ContextualInfoNew'
const WARNING_TYPE = Object.freeze({
none: 'none',
emptyInput: 'emptyInput',
invalidEntryBound: 'invalidEntryBound',
riskyEntryHigh: 'riskyEntryHigh',
riskyEntryLow: 'riskyEntryLow'
})
const Flex = styled.div` const Flex = styled.div`
display: flex; display: flex;
justify-content: center; justify-content: center;
button {
max-width: 20rem;
}
` `
const SlippageRow = styled(Flex)` const FlexBetween = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
height: 100%;
`
const WrappedSlippageRow = ({ wrap, ...rest }) => <Flex {...rest} />
const SlippageRow = styled(WrappedSlippageRow)`
position: relative; position: relative;
width: 100%; flex-wrap: ${({ wrap }) => wrap && 'wrap'};
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
font-size: 0.8rem; width: 100%;
padding: 0; padding: 0;
height: 24px; padding-top: ${({ wrap }) => wrap && '0.25rem'};
margin-bottom: 14px;
` `
const QuestionWrapper = styled.div` const QuestionWrapper = styled.button`
display: flex;
align-items: center;
justify-content: center;
margin: 0;
padding: 0;
margin-left: 0.4rem; margin-left: 0.4rem;
margin-top: 0.2rem; padding: 0.2rem;
border: none;
background: none;
outline: none;
cursor: default;
border-radius: 36px;
&:hover { :hover,
cursor: pointer; :focus {
opacity: 0.7;
}
`
const HelpCircleStyled = styled.img`
height: 18px;
width: 18px;
`
const fadeIn = keyframes`
from {
opacity : 0;
}
to {
opacity : 1;
} }
` `
...@@ -44,18 +82,45 @@ const Popup = styled(Flex)` ...@@ -44,18 +82,45 @@ const Popup = styled(Flex)`
width: 228px; width: 228px;
left: -78px; left: -78px;
top: -124px; top: -124px;
flex-direction: column; flex-direction: column;
aligm-items: center; align-items: center;
padding: 0.6rem 1rem;
line-height: 150%;
background: ${({ theme }) => theme.inputBackground};
border: 1px solid ${({ theme }) => theme.mercuryGray};
padding: 1rem; border-radius: 8px;
line-height: 183.52%; line-height: 183.52%;
background: #404040; background: #404040;
border-radius: 8px; border-radius: 8px;
color: white; color: ${({ theme }) => theme.textColor};
font-style: italic; font-style: italic;
${({ theme }) => theme.mediaWidth.upToSmall`
left: -20px;
`}
`
const FancyButton = styled.button`
color: ${({ theme }) => theme.textColor};
align-items: center;
min-width: 55px;
height: 2rem;
border-radius: 36px;
font-size: 12px;
border: 1px solid ${({ theme }) => theme.mercuryGray};
outline: none;
background: ${({ theme }) => theme.inputBackground};
:hover {
cursor: inherit;
border: 1px solid ${({ theme }) => theme.chaliceGray};
}
:focus {
border: 1px solid ${({ theme }) => theme.royalBlue};
}
` `
const Option = styled(FancyButton)` const Option = styled(FancyButton)`
...@@ -98,55 +163,60 @@ const Option = styled(FancyButton)` ...@@ -98,55 +163,60 @@ const Option = styled(FancyButton)`
`} `}
` `
const OptionLarge = styled(Option)`
width: 120px;
`
const Input = styled.input` const Input = styled.input`
width: 123.27px; background: ${({ theme }) => theme.inputBackground};
background: #ffffff; flex-grow: 1;
height: 2rem; font-size: 12px;
outline: none; outline: none;
margin-left: 20px;
border: 1px solid #f2f2f2;
box-sizing: border-box; box-sizing: border-box;
border-radius: 36px;
color: #aeaeae;
&:focus {
}
text-align: left;
padding-left: 0.9rem;
&::-webkit-outer-spin-button, &::-webkit-outer-spin-button,
&::-webkit-inner-spin-button { &::-webkit-inner-spin-button {
-webkit-appearance: none; -webkit-appearance: none;
} }
cursor: inherit;
color: ${({ theme }) => theme.doveGray};
text-align: left;
${({ active }) => ${({ active }) =>
active && active &&
` css`
border: 1px solid #2f80ed; color: initial;
cursor: initial;
text-align: right;
`}
${({ placeholder }) =>
placeholder !== 'Custom' &&
css`
text-align: right; text-align: right;
padding-right 1.5rem; color: ${({ theme }) => theme.textColor};
padding-left 0rem;
color : inherit;
`} `}
${({ warning }) => ${({ color }) =>
warning === 'red' && color === 'red' &&
` css`
color : #FF6871; color: ${({ theme }) => theme.salmonRed};
border: 1px solid #FF6871;
`} `}
` `
const BottomError = styled.div` const BottomError = styled.div`
margin-top: 1rem; ${({ show }) =>
color: #aeaeae; show &&
css`
padding-top: 12px;
`}
color: ${({ theme }) => theme.doveGray};
${({ color }) => ${({ color }) =>
color === 'red' && color === 'red' &&
` css`
color : #FF6871; color: ${({ theme }) => theme.salmonRed};
`} `}
` `
...@@ -166,8 +236,18 @@ const OptionCustom = styled(FancyButton)` ...@@ -166,8 +236,18 @@ const OptionCustom = styled(FancyButton)`
} }
`} `}
const OptionLarge = styled(Option)` ${({ color }) =>
width: 120px; color === 'red' &&
css`
border: 1px solid ${({ theme }) => theme.salmonRed};
`}
input {
width: 100%;
height: 100%;
border: 0px;
border-radius: 2rem;
}
` `
const Bold = styled.span` const Bold = styled.span`
...@@ -179,39 +259,33 @@ const LastSummaryText = styled.div` ...@@ -179,39 +259,33 @@ const LastSummaryText = styled.div`
` `
const SlippageSelector = styled.div` const SlippageSelector = styled.div`
margin-top: 1rem; background-color: ${({ theme }) => darken(0.04, theme.concreteGray)};
` padding: 1rem 1.25rem 1rem 1.25rem;
border-radius: 12px;
const InputGroup = styled.div`
position: relative;
` `
const Percent = styled.div` const Percent = styled.div`
right: 14px;
top: 9px;
position: absolute;
color: inherit; color: inherit;
font-size: 0, 8rem; font-size: 0, 8rem;
flex-grow: 0;
${({ color }) => ${({ color, theme }) =>
(color === 'faded' && (color === 'faded' &&
` css`
color : #AEAEAE color: ${theme.doveGray};
`) || `) ||
(color === 'red' && (color === 'red' &&
` css`
color : #FF6871 color: ${theme.salmonRed};
`)} `)};
` `
const Faded = styled.span` const Faded = styled.span`
opacity: 0.7; opacity: 0.7;
` `
const ErrorEmoji = styled.span` const TransactionInfo = styled.div`
left: 30px; padding: 1.25rem 1.25rem 1rem 1.25rem;
top: 4px;
position: absolute;
` `
const ValueWrapper = styled.span` const ValueWrapper = styled.span`
...@@ -219,12 +293,28 @@ const ValueWrapper = styled.span` ...@@ -219,12 +293,28 @@ const ValueWrapper = styled.span`
background-color: ${({ theme }) => darken(0.04, theme.concreteGray)}; background-color: ${({ theme }) => darken(0.04, theme.concreteGray)};
border-radius: 12px; border-radius: 12px;
font-variant: tabular-nums; font-variant: tabular-nums;
vertical
` `
export default function TransactionDetails(props) { export default function TransactionDetails(props) {
const { t } = useTranslation() const { t } = useTranslation()
const [activeIndex, setActiveIndex] = useState(3)
const [warningType, setWarningType] = useState(WARNING_TYPE.none)
const inputRef = useRef()
const [showPopup, setPopup] = useState(false)
const [userInput, setUserInput] = useState('')
const debouncedInput = useDebounce(userInput, 150)
useEffect(() => {
if (activeIndex === 4) {
checkBounds(debouncedInput)
}
})
function renderSummary() { function renderSummary() {
let contextualInfo = '' let contextualInfo = ''
let isError = false let isError = false
...@@ -274,23 +364,17 @@ export default function TransactionDetails(props) { ...@@ -274,23 +364,17 @@ export default function TransactionDetails(props) {
) )
} }
const [activeIndex, setActiveIndex] = useState(3)
const [placeHolder, setplaceHolder] = useState('Custom')
const [warningType, setWarningType] = useState('none')
const [showPopup, setPopup] = useState(false)
const dropDownContent = () => { const dropDownContent = () => {
return ( return (
<> <>
{renderTransactionDetails()} {renderTransactionDetails()}
<Break />
<SlippageSelector> <SlippageSelector>
<SlippageRow> <SlippageRow>
Limit addtional price slippage Limit additional price slippage
<QuestionWrapper <QuestionWrapper
onClick={() => {
setPopup(!showPopup)
}}
onMouseEnter={() => { onMouseEnter={() => {
setPopup(true) setPopup(true)
}} }}
...@@ -298,105 +382,114 @@ export default function TransactionDetails(props) { ...@@ -298,105 +382,114 @@ export default function TransactionDetails(props) {
setPopup(false) setPopup(false)
}} }}
> >
<img src={questionMark} alt="question mark" /> <HelpCircleStyled src={question} alt="popup" />
</QuestionWrapper> </QuestionWrapper>
{showPopup ? ( {showPopup ? (
<Popup> <Popup>
Lowering this limit decreases your risk of frontrunning. This makes it more likely that your transaction Lowering this limit decreases your risk of frontrunning. However, this makes it more likely that your
will fail due to normal price movements. transaction will fail due to normal price movements.
</Popup> </Popup>
) : ( ) : (
'' ''
)} )}
</SlippageRow> </SlippageRow>
<SlippageRow> <SlippageRow wrap>
<Option <Option
onClick={() => { onClick={() => {
updateSlippage(0.1) setFromFixed(1, 0.1)
setWarningType('none')
setActiveIndex(1)
props.setcustomSlippageError('valid')
setplaceHolder('Custom')
}} }}
active={activeIndex === 1 ? true : false} active={activeIndex === 1}
> >
0.1% 0.1%
</Option> </Option>
<Option <Option
onClick={() => { onClick={() => {
updateSlippage(1) setFromFixed(2, 0.5)
setWarningType('none')
setActiveIndex(2)
props.setcustomSlippageError('valid')
setplaceHolder('Custom')
}} }}
active={activeIndex === 2 ? true : false} active={activeIndex === 2}
> >
1% 0.5%
</Option> </Option>
<OptionLarge <OptionLarge
onClick={() => { onClick={() => {
updateSlippage(2) setFromFixed(3, 1)
setWarningType('none')
setActiveIndex(3)
props.setcustomSlippageError('valid')
setplaceHolder('Custom')
}} }}
active={activeIndex === 3 ? true : false} active={activeIndex === 3}
> >
2% 1% <Faded>(suggested)</Faded>
<Faded>(suggested)</Faded>
</OptionLarge> </OptionLarge>
<InputGroup> <OptionCustom
{warningType !== 'none' ? <ErrorEmoji>⚠️</ErrorEmoji> : ''} active={activeIndex === 4}
color={
warningType === WARNING_TYPE.emptyInput
? ''
: warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
? 'red'
: ''
}
onClick={() => {
setFromCustom()
}}
>
<FlexBetween>
{!(warningType === WARNING_TYPE.none || warningType === WARNING_TYPE.emptyInput) && (
<span role="img" aria-label="warning">
⚠️
</span>
)}
<Input <Input
placeholder={placeHolder} tabIndex={-1}
value={userInput || ''} ref={inputRef}
active={activeIndex === 4}
placeholder={
activeIndex === 4
? !!userInput
? ''
: '0'
: activeIndex !== 4 && userInput !== ''
? userInput
: 'Custom'
}
value={activeIndex === 4 ? userInput : ''}
onChange={parseInput} onChange={parseInput}
onClick={e => { color={
setActiveIndex(4) warningType === WARNING_TYPE.emptyInput
setplaceHolder('')
parseInput(e)
}}
active={activeIndex === 4 ? true : false}
warning={
warningType === 'emptyInput'
? '' ? ''
: warningType !== 'none' && warningType !== 'riskyEntryLow' : warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
? 'red' ? 'red'
: '' : ''
} }
/> />
<Percent <Percent
color={ color={
warningType === 'emptyInput' activeIndex !== 4
? 'faded' ? 'faded'
: warningType !== 'none' && warningType !== 'riskyEntryLow' : warningType === WARNING_TYPE.riskyEntryHigh || warningType === WARNING_TYPE.invalidEntryBound
? 'red' ? 'red'
: activeIndex !== 4
? 'faded'
: '' : ''
} }
> >
% %
</Percent> </Percent>
</InputGroup> </FlexBetween>
</OptionCustom>
</SlippageRow> </SlippageRow>
<SlippageRow> <SlippageRow>
<BottomError <BottomError
show={activeIndex === 4}
color={ color={
warningType === 'emptyInput' warningType === WARNING_TYPE.emptyInput
? '' ? ''
: warningType !== 'none' && warningType !== 'riskyEntryLow' : warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
? 'red' ? 'red'
: '' : ''
} }
> >
{warningType === 'emptyInput' ? 'Enter a slippage percentage.' : ''} {activeIndex === 4 && warningType.toString() === 'none' && 'Custom slippage value'}
{warningType === 'invalidEntry' ? 'Please input a valid percentage.' : ''} {warningType === WARNING_TYPE.emptyInput && 'Enter a slippage percentage'}
{warningType === 'invalidEntryBound' ? 'Pleae select value less than 50%' : ''} {warningType === WARNING_TYPE.invalidEntryBound && 'Please select a value no greater than 50%'}
{warningType === 'riskyEntryHigh' ? 'Your transaction may be frontrun.' : ''} {warningType === WARNING_TYPE.riskyEntryHigh && 'Your transaction may be frontrun'}
{warningType === 'riskyEntryLow' ? 'Your transaction may fail.' : ''} {warningType === WARNING_TYPE.riskyEntryLow && 'Your transaction may fail'}
</BottomError> </BottomError>
</SlippageRow> </SlippageRow>
</SlippageSelector> </SlippageSelector>
...@@ -404,52 +497,65 @@ export default function TransactionDetails(props) { ...@@ -404,52 +497,65 @@ export default function TransactionDetails(props) {
) )
} }
const [userInput, setUserInput] = useState() const setFromCustom = () => {
setActiveIndex(4)
const parseInput = e => { inputRef.current.focus()
let input = e.target.value // if there's a value, evaluate the bounds
if (input === '') { checkBounds(debouncedInput)
setUserInput(input)
props.setcustomSlippageError('invalid')
return setWarningType('emptyInput')
}
//check for decimal
let isValid = /^[+]?\d*\.?\d{1,2}$/.test(input) || /^[+]?\d*\.$/.test(input)
let decimalLimit = /^\d+\.?\d{0,2}$/.test(input) || input === ''
if (decimalLimit) {
setUserInput(input)
} else {
return
}
if (isValid) {
checkAcceptablePercentValue(input)
} else {
setWarningType('invalidEntry')
} }
// used for slippage presets
const setFromFixed = (index, slippage) => {
// update slippage in parent, reset errors and input state
updateSlippage(slippage)
setWarningType(WARNING_TYPE.none)
setActiveIndex(index)
props.setcustomSlippageError('valid`')
} }
const checkAcceptablePercentValue = input => { const checkBounds = slippageValue => {
setTimeout(function() { setWarningType(WARNING_TYPE.none)
setWarningType('none')
props.setcustomSlippageError('valid') props.setcustomSlippageError('valid')
if (input < 0 || input > 50) {
if (slippageValue === '' || slippageValue === '.') {
props.setcustomSlippageError('invalid') props.setcustomSlippageError('invalid')
return setWarningType('invalidEntryBound') return setWarningType(WARNING_TYPE.emptyInput)
} }
if (input >= 0 && input < 0.1) {
// check bounds and set errors
if (Number(slippageValue) < 0 || Number(slippageValue) > 50) {
props.setcustomSlippageError('invalid')
return setWarningType(WARNING_TYPE.invalidEntryBound)
}
if (Number(slippageValue) >= 0 && Number(slippageValue) < 0.1) {
props.setcustomSlippageError('valid') props.setcustomSlippageError('valid')
setWarningType('riskyEntryLow') setWarningType(WARNING_TYPE.riskyEntryLow)
} }
if (input >= 5) { if (Number(slippageValue) > 5) {
props.setcustomSlippageError('warning') props.setcustomSlippageError('warning')
setWarningType('riskyEntryHigh') setWarningType(WARNING_TYPE.riskyEntryHigh)
}
//update the actual slippage value in parent
updateSlippage(Number(slippageValue))
}
// check that the theyve entered number and correct decimal
const parseInput = e => {
let input = e.target.value
// restrict to 2 decimal places
let acceptableValues = [/^$/, /^\d{1,2}$/, /^\d{0,2}\.\d{0,2}$/]
// if its within accepted decimal limit, update the input state
if (acceptableValues.some(a => a.test(input))) {
setUserInput(input)
} }
updateSlippage(input)
}, 300)
} }
const updateSlippage = newSlippage => { const updateSlippage = newSlippage => {
let numParsed = parseFloat((newSlippage * 100).toFixed(2)) // round to 2 decimals to prevent ethers error
let numParsed = parseInt(newSlippage * 100)
// set both slippage values in parents
props.setRawSlippage(numParsed) props.setRawSlippage(numParsed)
props.setRawTokenSlippage(numParsed) props.setRawTokenSlippage(numParsed)
} }
...@@ -464,9 +570,10 @@ export default function TransactionDetails(props) { ...@@ -464,9 +570,10 @@ export default function TransactionDetails(props) {
if (props.independentField === props.INPUT) { if (props.independentField === props.INPUT) {
return props.sending ? ( return props.sending ? (
<div> <TransactionInfo>
<div> <div>
{t('youAreSelling')}{' '} {t('youAreSelling')}{' '}
<ValueWrapper>
{b( {b(
`${amountFormatter( `${amountFormatter(
props.independentValueParsed, props.independentValueParsed,
...@@ -474,24 +581,26 @@ export default function TransactionDetails(props) { ...@@ -474,24 +581,26 @@ export default function TransactionDetails(props) {
Math.min(4, props.independentDecimals) Math.min(4, props.independentDecimals)
)} ${props.inputSymbol}` )} ${props.inputSymbol}`
)} )}
. </ValueWrapper>
</div> </div>
<LastSummaryText> <LastSummaryText>
{b(props.recipientAddress)} {t('willReceive')}{' '} {b(props.recipientAddress)} {t('willReceive')}{' '}
<ValueWrapper>
{b( {b(
`${amountFormatter( `${amountFormatter(
props.dependentValueMinumum, props.dependentValueMinumum,
props.dependentDecimals, props.dependentDecimals,
Math.min(4, props.dependentDecimals) Math.min(4, props.dependentDecimals)
)} ${props.outputSymbol}` )} ${props.outputSymbol}`
)}{' '} )}
</ValueWrapper>{' '}
</LastSummaryText> </LastSummaryText>
<LastSummaryText> <LastSummaryText>
{t('priceChange')} {b(`${props.percentSlippageFormatted}%`)}. {t('priceChange')} <ValueWrapper>{b(`${props.percentSlippageFormatted}%`)}</ValueWrapper>
</LastSummaryText> </LastSummaryText>
</div> </TransactionInfo>
) : ( ) : (
<div> <TransactionInfo>
<div> <div>
{t('youAreSelling')}{' '} {t('youAreSelling')}{' '}
<ValueWrapper> <ValueWrapper>
...@@ -503,7 +612,7 @@ export default function TransactionDetails(props) { ...@@ -503,7 +612,7 @@ export default function TransactionDetails(props) {
)} ${props.inputSymbol}` )} ${props.inputSymbol}`
)} )}
</ValueWrapper>{' '} </ValueWrapper>{' '}
{t('forAtLeast')}{' '} {t('forAtLeast')}
<ValueWrapper> <ValueWrapper>
{b( {b(
`${amountFormatter( `${amountFormatter(
...@@ -513,44 +622,43 @@ export default function TransactionDetails(props) { ...@@ -513,44 +622,43 @@ export default function TransactionDetails(props) {
)} ${props.outputSymbol}` )} ${props.outputSymbol}`
)} )}
</ValueWrapper> </ValueWrapper>
.
</div> </div>
<LastSummaryText> <LastSummaryText>
{t('priceChange')} <ValueWrapper>{b(`${props.percentSlippageFormatted}%`)}</ValueWrapper> {t('priceChange')} <ValueWrapper>{b(`${props.percentSlippageFormatted}%`)}</ValueWrapper>
</LastSummaryText> </LastSummaryText>
</div> </TransactionInfo>
) )
} else { } else {
return props.sending ? ( return props.sending ? (
<div> <TransactionInfo>
<div> <div>
{t('youAreSending')}{' '} {t('youAreSending')}{' '}
<ValueWrapper>
{b( {b(
`${amountFormatter( `${amountFormatter(
props.independentValueParsed, props.independentValueParsed,
props.independentDecimals, props.independentDecimals,
Math.min(4, props.independentDecimals) Math.min(4, props.independentDecimals)
)} ${props.outputSymbol}` )} ${props.outputSymbol}`
)}{' '} )}
{t('to')} {b(props.recipientAddress)}. </ValueWrapper>{' '}
</div> {t('to')} {b(props.recipientAddress)} {t('forAtMost')}{' '}
<LastSummaryText> <ValueWrapper>
{t('itWillCost')}{' '}
{b( {b(
`${amountFormatter( `${amountFormatter(
props.dependentValueMaximum, props.independentValueParsed,
props.dependentDecimals, props.independentDecimals,
Math.min(4, props.dependentDecimals) Math.min(4, props.independentDecimals)
)} ${props.inputSymbol}` )} ${props.outputSymbol}`
)}{' '} )}
</LastSummaryText> </ValueWrapper>{' '}
</div>
<LastSummaryText> <LastSummaryText>
{t('priceChange')} {b(`${props.percentSlippageFormatted}%`)}. {t('priceChange')} <ValueWrapper>{b(`${props.percentSlippageFormatted}%`)}</ValueWrapper>
</LastSummaryText> </LastSummaryText>
</div> </TransactionInfo>
) : ( ) : (
<div> <TransactionInfo>
<div>
{t('youAreBuying')}{' '} {t('youAreBuying')}{' '}
<ValueWrapper> <ValueWrapper>
{b( {b(
...@@ -560,23 +668,21 @@ export default function TransactionDetails(props) { ...@@ -560,23 +668,21 @@ export default function TransactionDetails(props) {
Math.min(4, props.independentDecimals) Math.min(4, props.independentDecimals)
)} ${props.outputSymbol}` )} ${props.outputSymbol}`
)} )}
</ValueWrapper> </ValueWrapper>{' '}
. {t('forAtMost')}{' '}
</div> <ValueWrapper>
<LastSummaryText>
{t('itWillCost')}{' '}
{b( {b(
`${amountFormatter( `${amountFormatter(
props.dependentValueMaximum, props.dependentValueMaximum,
props.dependentDecimals, props.dependentDecimals,
Math.min(4, props.dependentDecimals) Math.min(4, props.dependentDecimals)
)} ${props.inputSymbol}` )} ${props.inputSymbol}`
)}{' '} )}
</LastSummaryText> </ValueWrapper>{' '}
<LastSummaryText> <LastSummaryText>
{t('priceChange')} <ValueWrapper>{b(`${props.percentSlippageFormatted}%`)}</ValueWrapper> {t('priceChange')} <ValueWrapper>{b(`${props.percentSlippageFormatted}%`)}</ValueWrapper>
</LastSummaryText> </LastSummaryText>
</div> </TransactionInfo>
) )
} }
} }
......
...@@ -32,8 +32,8 @@ const Web3StatusGeneric = styled.button` ...@@ -32,8 +32,8 @@ const Web3StatusGeneric = styled.button`
` `
const Web3StatusError = styled(Web3StatusGeneric)` const Web3StatusError = styled(Web3StatusGeneric)`
background-color: ${({ theme }) => theme.salmonRed}; background-color: ${({ theme }) => theme.salmonRed};
color: ${({ theme }) => theme.white};
border: 1px solid ${({ theme }) => theme.salmonRed}; border: 1px solid ${({ theme }) => theme.salmonRed};
color: ${({ theme }) => theme.white};
font-weight: 500; font-weight: 500;
:hover, :hover,
:focus { :focus {
...@@ -43,9 +43,10 @@ const Web3StatusError = styled(Web3StatusGeneric)` ...@@ -43,9 +43,10 @@ const Web3StatusError = styled(Web3StatusGeneric)`
const Web3StatusConnect = styled(Web3StatusGeneric)` const Web3StatusConnect = styled(Web3StatusGeneric)`
background-color: ${({ theme }) => theme.royalBlue}; background-color: ${({ theme }) => theme.royalBlue};
color: ${({ theme }) => theme.white};
border: 1px solid ${({ theme }) => theme.royalBlue}; border: 1px solid ${({ theme }) => theme.royalBlue};
color: ${({ theme }) => theme.white};
font-weight: 500; font-weight: 500;
:hover, :hover,
:focus { :focus {
background-color: ${({ theme }) => darken(0.1, theme.royalBlue)}; background-color: ${({ theme }) => darken(0.1, theme.royalBlue)};
...@@ -53,14 +54,18 @@ const Web3StatusConnect = styled(Web3StatusGeneric)` ...@@ -53,14 +54,18 @@ const Web3StatusConnect = styled(Web3StatusGeneric)`
` `
const Web3StatusConnected = styled(Web3StatusGeneric)` const Web3StatusConnected = styled(Web3StatusGeneric)`
background-color: ${({ pending, theme }) => (pending ? theme.zumthorBlue : theme.white)}; background-color: ${({ pending, theme }) => (pending ? theme.zumthorBlue : theme.inputBackground)};
color: ${({ pending, theme }) => (pending ? theme.royalBlue : theme.doveGray)};
border: 1px solid ${({ pending, theme }) => (pending ? theme.royalBlue : theme.mercuryGray)}; border: 1px solid ${({ pending, theme }) => (pending ? theme.royalBlue : theme.mercuryGray)};
color: ${({ pending, theme }) => (pending ? theme.royalBlue : theme.doveGray)};
font-weight: 400; font-weight: 400;
:hover { :hover {
> P {
color: ${({ theme }) => theme.uniswapPink};
}
background-color: ${({ pending, theme }) => background-color: ${({ pending, theme }) =>
pending ? transparentize(0.9, theme.royalBlue) : transparentize(0.9, theme.mercuryGray)}; pending ? transparentize(0.9, theme.royalBlue) : transparentize(0.9, theme.mercuryGray)};
}
:focus { :focus {
border: 1px solid border: 1px solid
${({ pending, theme }) => (pending ? darken(0.1, theme.royalBlue) : darken(0.1, theme.mercuryGray))}; ${({ pending, theme }) => (pending ? darken(0.1, theme.royalBlue) : darken(0.1, theme.mercuryGray))};
...@@ -72,7 +77,6 @@ const Text = styled.p` ...@@ -72,7 +77,6 @@ const Text = styled.p`
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
margin: 0 0.5rem 0 0.25rem; margin: 0 0.5rem 0 0.25rem;
font-size: 0.83rem; font-size: 0.83rem;
` `
......
import React, { createContext, useContext, useReducer, useMemo, useCallback } from 'react'
import { ethers } from 'ethers'
import { getTokenReserves, getMarketDetails, BigNumber } from '@uniswap/sdk'
import { useWeb3Context } from 'web3-react'
import { safeAccess, isAddress, getEtherBalance, getTokenBalance } from '../utils'
import { useAllTokenDetails } from './Tokens'
const ZERO = ethers.utils.bigNumberify(0)
const ONE = new BigNumber(1)
const UPDATE = 'UPDATE'
const AllBalancesContext = createContext()
function useAllBalancesContext() {
return useContext(AllBalancesContext)
}
function reducer(state, { type, payload }) {
switch (type) {
case UPDATE: {
const { allBalanceData, networkId, address } = payload
return {
...state,
[networkId]: {
...(safeAccess(state, [networkId]) || {}),
[address]: {
...(safeAccess(state, [networkId, address]) || {}),
allBalanceData
}
}
}
}
default: {
throw Error(`Unexpected action type in AllBalancesContext reducer: '${type}'.`)
}
}
}
export default function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, {})
const update = useCallback((allBalanceData, networkId, address) => {
dispatch({ type: UPDATE, payload: { allBalanceData, networkId, address } })
}, [])
return (
<AllBalancesContext.Provider value={useMemo(() => [state, { update }], [state, update])}>
{children}
</AllBalancesContext.Provider>
)
}
export function useFetchAllBalances() {
const { account, networkId, library } = useWeb3Context()
const allTokens = useAllTokenDetails()
const [state, { update }] = useAllBalancesContext()
const { allBalanceData } = safeAccess(state, [networkId, account]) || {}
const getData = async () => {
if (!!library && !!account) {
const newBalances = {}
await Promise.all(
Object.keys(allTokens).map(async k => {
let balance = null
let ethRate = null
if (isAddress(k) || k === 'ETH') {
if (k === 'ETH') {
balance = await getEtherBalance(account, library).catch(() => null)
ethRate = ONE
} else {
balance = await getTokenBalance(k, account, library).catch(() => null)
// only get values for tokens with positive balances
if (!!balance && balance.gt(ZERO)) {
const tokenReserves = await getTokenReserves(k, library).catch(() => null)
if (!!tokenReserves) {
const marketDetails = getMarketDetails(tokenReserves)
if (marketDetails.marketRate && marketDetails.marketRate.rate) {
ethRate = marketDetails.marketRate.rate
}
}
}
}
return (newBalances[k] = { balance, ethRate })
}
})
)
update(newBalances, networkId, account)
}
}
useMemo(getData, [account, state])
return allBalanceData
}
import React, { createContext, useContext, useReducer, useMemo, useCallback, useEffect } from 'react' import React, { createContext, useContext, useReducer, useMemo, useCallback, useEffect } from 'react'
import { useWeb3Context } from 'web3-react' import { useWeb3Context } from 'web3-react'
import { safeAccess } from '../utils' import { safeAccess } from '../utils'
import { getUSDPrice } from '../utils/price'
const BLOCK_NUMBERS = 'BLOCK_NUMBERS' const BLOCK_NUMBER = 'BLOCK_NUMBER'
const USD_PRICE = 'USD_PRICE'
const UPDATE_BLOCK_NUMBER = 'UPDATE_BLOCK_NUMBER' const UPDATE_BLOCK_NUMBER = 'UPDATE_BLOCK_NUMBER'
const UPDATE_USD_PRICE = 'UPDATE_USD_PRICE'
const ApplicationContext = createContext() const ApplicationContext = createContext()
...@@ -18,12 +22,22 @@ function reducer(state, { type, payload }) { ...@@ -18,12 +22,22 @@ function reducer(state, { type, payload }) {
const { networkId, blockNumber } = payload const { networkId, blockNumber } = payload
return { return {
...state, ...state,
[BLOCK_NUMBERS]: { [BLOCK_NUMBER]: {
...(safeAccess(state, [BLOCK_NUMBERS]) || {}), ...(safeAccess(state, [BLOCK_NUMBER]) || {}),
[networkId]: blockNumber [networkId]: blockNumber
} }
} }
} }
case UPDATE_USD_PRICE: {
const { networkId, USDPrice } = payload
return {
...state,
[USD_PRICE]: {
...(safeAccess(state, [USD_PRICE]) || {}),
[networkId]: USDPrice
}
}
}
default: { default: {
throw Error(`Unexpected action type in ApplicationContext reducer: '${type}'.`) throw Error(`Unexpected action type in ApplicationContext reducer: '${type}'.`)
} }
...@@ -32,15 +46,22 @@ function reducer(state, { type, payload }) { ...@@ -32,15 +46,22 @@ function reducer(state, { type, payload }) {
export default function Provider({ children }) { export default function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, { const [state, dispatch] = useReducer(reducer, {
[BLOCK_NUMBERS]: {} [BLOCK_NUMBER]: {},
[USD_PRICE]: {}
}) })
const updateBlockNumber = useCallback((networkId, blockNumber) => { const updateBlockNumber = useCallback((networkId, blockNumber) => {
dispatch({ type: UPDATE_BLOCK_NUMBER, payload: { networkId, blockNumber } }) dispatch({ type: UPDATE_BLOCK_NUMBER, payload: { networkId, blockNumber } })
}, []) }, [])
const updateUSDPrice = useCallback((networkId, USDPrice) => {
dispatch({ type: UPDATE_USD_PRICE, payload: { networkId, USDPrice } })
}, [])
return ( return (
<ApplicationContext.Provider value={useMemo(() => [state, { updateBlockNumber }], [state, updateBlockNumber])}> <ApplicationContext.Provider
value={useMemo(() => [state, { updateBlockNumber, updateUSDPrice }], [state, updateBlockNumber, updateUSDPrice])}
>
{children} {children}
</ApplicationContext.Provider> </ApplicationContext.Provider>
) )
...@@ -49,7 +70,24 @@ export default function Provider({ children }) { ...@@ -49,7 +70,24 @@ export default function Provider({ children }) {
export function Updater() { export function Updater() {
const { networkId, library } = useWeb3Context() const { networkId, library } = useWeb3Context()
const [, { updateBlockNumber }] = useApplicationContext() const globalBlockNumber = useBlockNumber()
const [, { updateBlockNumber, updateUSDPrice }] = useApplicationContext()
useEffect(() => {
let stale = false
getUSDPrice(library)
.then(([price]) => {
if (!stale) {
updateUSDPrice(networkId, price)
}
})
.catch(() => {
if (!stale) {
updateUSDPrice(networkId, null)
}
})
}, [globalBlockNumber, library, networkId, updateUSDPrice])
useEffect(() => { useEffect(() => {
if ((networkId || networkId === 0) && library) { if ((networkId || networkId === 0) && library) {
...@@ -88,5 +126,13 @@ export function useBlockNumber() { ...@@ -88,5 +126,13 @@ export function useBlockNumber() {
const [state] = useApplicationContext() const [state] = useApplicationContext()
return safeAccess(state, [BLOCK_NUMBERS, networkId]) return safeAccess(state, [BLOCK_NUMBER, networkId])
}
export function useUSDPrice() {
const { networkId } = useWeb3Context()
const [state] = useApplicationContext()
return safeAccess(state, [USD_PRICE, networkId])
} }
...@@ -79,7 +79,6 @@ export function useAddressBalance(address, tokenAddress) { ...@@ -79,7 +79,6 @@ export function useAddressBalance(address, tokenAddress) {
update(networkId, address, tokenAddress, null, globalBlockNumber) update(networkId, address, tokenAddress, null, globalBlockNumber)
} }
}) })
return () => { return () => {
stale = true stale = true
} }
......
...@@ -7,7 +7,9 @@ const CURRENT_VERSION = 0 ...@@ -7,7 +7,9 @@ const CURRENT_VERSION = 0
const LAST_SAVED = 'LAST_SAVED' const LAST_SAVED = 'LAST_SAVED'
const BETA_MESSAGE_DISMISSED = 'BETA_MESSAGE_DISMISSED' const BETA_MESSAGE_DISMISSED = 'BETA_MESSAGE_DISMISSED'
const UPDATABLE_KEYS = [BETA_MESSAGE_DISMISSED] const DARK_MODE = 'DARK_MODE'
const UPDATABLE_KEYS = [BETA_MESSAGE_DISMISSED, DARK_MODE]
const UPDATE_KEY = 'UPDATE_KEY' const UPDATE_KEY = 'UPDATE_KEY'
...@@ -39,7 +41,8 @@ function reducer(state, { type, payload }) { ...@@ -39,7 +41,8 @@ function reducer(state, { type, payload }) {
function init() { function init() {
const defaultLocalStorage = { const defaultLocalStorage = {
[VERSION]: CURRENT_VERSION, [VERSION]: CURRENT_VERSION,
[BETA_MESSAGE_DISMISSED]: false [BETA_MESSAGE_DISMISSED]: false,
[DARK_MODE]: false
} }
try { try {
...@@ -48,7 +51,7 @@ function init() { ...@@ -48,7 +51,7 @@ function init() {
// this is where we could run migration logic // this is where we could run migration logic
return defaultLocalStorage return defaultLocalStorage
} else { } else {
return parsed return { ...defaultLocalStorage, ...parsed }
} }
} catch { } catch {
return defaultLocalStorage return defaultLocalStorage
...@@ -88,3 +91,15 @@ export function useBetaMessageManager() { ...@@ -88,3 +91,15 @@ export function useBetaMessageManager() {
return [!state[BETA_MESSAGE_DISMISSED], dismissBetaMessage] return [!state[BETA_MESSAGE_DISMISSED], dismissBetaMessage]
} }
export function useDarkModeManager() {
const [state, { updateKey }] = useLocalStorageContext()
const isDarkMode = state[DARK_MODE]
const toggleDarkMode = useCallback(() => {
updateKey(DARK_MODE, !isDarkMode)
}, [updateKey, isDarkMode])
return [state[DARK_MODE], toggleDarkMode]
}
...@@ -89,14 +89,12 @@ const INITIAL_TOKENS_CONTEXT = { ...@@ -89,14 +89,12 @@ const INITIAL_TOKENS_CONTEXT = {
[DECIMALS]: 9, [DECIMALS]: 9,
[EXCHANGE_ADDRESS]: '0xb92dE8B30584392Af27726D5ce04Ef3c4e5c9924' [EXCHANGE_ADDRESS]: '0xb92dE8B30584392Af27726D5ce04Ef3c4e5c9924'
}, },
'0xc719d010B63E5bbF2C0551872CD5316ED26AcD83': { '0xc719d010B63E5bbF2C0551872CD5316ED26AcD83': {
[NAME]: 'Decentralized Insurance Protocol', [NAME]: 'Decentralized Insurance Protocol',
[SYMBOL]: 'DIP', [SYMBOL]: 'DIP',
[DECIMALS]: 18, [DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0x61792F290e5100FBBcBb2309F03A1Bab869fb850' [EXCHANGE_ADDRESS]: '0x61792F290e5100FBBcBb2309F03A1Bab869fb850'
}, },
'0x4946Fcea7C692606e8908002e55A582af44AC121': { '0x4946Fcea7C692606e8908002e55A582af44AC121': {
[NAME]: 'FOAM Token', [NAME]: 'FOAM Token',
[SYMBOL]: 'FOAM', [SYMBOL]: 'FOAM',
...@@ -247,6 +245,12 @@ const INITIAL_TOKENS_CONTEXT = { ...@@ -247,6 +245,12 @@ const INITIAL_TOKENS_CONTEXT = {
[DECIMALS]: 18, [DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0x755899F0540c3548b99E68C59AdB0f15d2695188' [EXCHANGE_ADDRESS]: '0x755899F0540c3548b99E68C59AdB0f15d2695188'
}, },
'0xF970b8E36e23F7fC3FD752EeA86f8Be8D83375A6': {
[NAME]: 'Ripio Credit Network Token',
[SYMBOL]: 'RCN',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0xD91FF16Ef92568fC27F466C3c5613e43313Ab1dc'
},
'0x255Aa6DF07540Cb5d3d297f0D0D4D84cb52bc8e6': { '0x255Aa6DF07540Cb5d3d297f0D0D4D84cb52bc8e6': {
[NAME]: 'Raiden Token', [NAME]: 'Raiden Token',
[SYMBOL]: 'RDN', [SYMBOL]: 'RDN',
...@@ -301,11 +305,11 @@ const INITIAL_TOKENS_CONTEXT = { ...@@ -301,11 +305,11 @@ const INITIAL_TOKENS_CONTEXT = {
[DECIMALS]: 18, [DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0x1aEC8F11A7E78dC22477e91Ed924Fab46e3A88Fd' [EXCHANGE_ADDRESS]: '0x1aEC8F11A7E78dC22477e91Ed924Fab46e3A88Fd'
}, },
'0x2Dea20405c52Fb477ecCa8Fe622661d316Ac5400': { '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F': {
[NAME]: 'Synthetix Network Token', [NAME]: 'Synthetix Network Token',
[SYMBOL]: 'SNX', [SYMBOL]: 'SNX',
[DECIMALS]: 18, [DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0x9fAA0Cb10912DE7Ad1D86705C65de291a9088A61' [EXCHANGE_ADDRESS]: '0x3958B4eC427F8fa24eB60F42821760e88d485f7F'
}, },
'0x42d6622deCe394b54999Fbd73D108123806f6a18': { '0x42d6622deCe394b54999Fbd73D108123806f6a18': {
[NAME]: 'SPANK', [NAME]: 'SPANK',
...@@ -462,7 +466,6 @@ export function useTokenDetails(tokenAddress) { ...@@ -462,7 +466,6 @@ export function useTokenDetails(tokenAddress) {
} }
} }
) )
return () => { return () => {
stale = true stale = true
} }
......
...@@ -58,9 +58,8 @@ export function useENSName(address) { ...@@ -58,9 +58,8 @@ export function useENSName(address) {
useEffect(() => { useEffect(() => {
if (isAddress(address)) { if (isAddress(address)) {
let stale = false let stale = false
library try {
.lookupAddress(address) library.lookupAddress(address).then(name => {
.then(name => {
if (!stale) { if (!stale) {
if (name) { if (name) {
setENSNname(name) setENSNname(name)
...@@ -69,11 +68,9 @@ export function useENSName(address) { ...@@ -69,11 +68,9 @@ export function useENSName(address) {
} }
} }
}) })
.catch(() => { } catch {
if (!stale) {
setENSNname(null) setENSNname(null)
} }
})
return () => { return () => {
stale = true stale = true
......
...@@ -10,6 +10,7 @@ import TransactionContextProvider, { Updater as TransactionContextUpdater } from ...@@ -10,6 +10,7 @@ import TransactionContextProvider, { Updater as TransactionContextUpdater } from
import TokensContextProvider from './contexts/Tokens' import TokensContextProvider from './contexts/Tokens'
import BalancesContextProvider from './contexts/Balances' import BalancesContextProvider from './contexts/Balances'
import AllowancesContextProvider from './contexts/Allowances' import AllowancesContextProvider from './contexts/Allowances'
import AllBalancesContextProvider from './contexts/AllBalances'
import App from './pages/App' import App from './pages/App'
import InjectedConnector from './InjectedConnector' import InjectedConnector from './InjectedConnector'
...@@ -35,7 +36,9 @@ function ContextProviders({ children }) { ...@@ -35,7 +36,9 @@ function ContextProviders({ children }) {
<TransactionContextProvider> <TransactionContextProvider>
<TokensContextProvider> <TokensContextProvider>
<BalancesContextProvider> <BalancesContextProvider>
<AllBalancesContextProvider>
<AllowancesContextProvider>{children}</AllowancesContextProvider> <AllowancesContextProvider>{children}</AllowancesContextProvider>
</AllBalancesContextProvider>
</BalancesContextProvider> </BalancesContextProvider>
</TokensContextProvider> </TokensContextProvider>
</TransactionContextProvider> </TransactionContextProvider>
...@@ -55,16 +58,16 @@ function Updaters() { ...@@ -55,16 +58,16 @@ function Updaters() {
} }
ReactDOM.render( ReactDOM.render(
<ThemeProvider>
<>
<GlobalStyle />
<Web3Provider connectors={connectors} libraryName="ethers.js"> <Web3Provider connectors={connectors} libraryName="ethers.js">
<ContextProviders> <ContextProviders>
<Updaters /> <Updaters />
<ThemeProvider>
<>
<GlobalStyle />
<App /> <App />
</ContextProviders>
</Web3Provider>
</> </>
</ThemeProvider>, </ThemeProvider>
</ContextProviders>
</Web3Provider>,
document.getElementById('root') document.getElementById('root')
) )
...@@ -4,6 +4,8 @@ import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom' ...@@ -4,6 +4,8 @@ import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom'
import Web3ReactManager from '../components/Web3ReactManager' import Web3ReactManager from '../components/Web3ReactManager'
import Header from '../components/Header' import Header from '../components/Header'
import Footer from '../components/Footer'
import NavigationTabs from '../components/NavigationTabs' import NavigationTabs from '../components/NavigationTabs'
import { isAddress } from '../utils' import { isAddress } from '../utils'
...@@ -11,24 +13,38 @@ const Swap = lazy(() => import('./Swap')) ...@@ -11,24 +13,38 @@ const Swap = lazy(() => import('./Swap'))
const Send = lazy(() => import('./Send')) const Send = lazy(() => import('./Send'))
const Pool = lazy(() => import('./Pool')) const Pool = lazy(() => import('./Pool'))
const AppWrapper = styled.div`
display: flex;
flex-flow: column;
align-items: flex-start;
height: 100vh;
`
const HeaderWrapper = styled.div` const HeaderWrapper = styled.div`
${({ theme }) => theme.flexRowNoWrap} ${({ theme }) => theme.flexRowNoWrap}
width: 100%; width: 100%;
justify-content: space-between; justify-content: space-between;
` `
const FooterWrapper = styled.div`
width: 100%;
min-height: 30px;
align-self: flex-end;
`
const BodyWrapper = styled.div` const BodyWrapper = styled.div`
${({ theme }) => theme.flexRowNoWrap} display: flex;
flex-direction: column;
width: 100%; width: 100%;
justify-content: center; justify-content: flex-start;
flex-grow: 1; align-items: center;
flex-basis: 0; flex: 1;
overflow: auto; overflow: auto;
` `
const Body = styled.div` const Body = styled.div`
width: 35rem; max-width: 35rem;
margin: 1.25rem; width: 90%;
/* margin: 0 1.25rem 1.25rem 1.25rem; */
` `
export default function App() { export default function App() {
......
...@@ -9,12 +9,13 @@ import { Button } from '../../theme' ...@@ -9,12 +9,13 @@ import { Button } from '../../theme'
import CurrencyInputPanel from '../../components/CurrencyInputPanel' import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import OversizedPanel from '../../components/OversizedPanel' import OversizedPanel from '../../components/OversizedPanel'
import ContextualInfo from '../../components/ContextualInfo' import ContextualInfo from '../../components/ContextualInfo'
import PlusBlue from '../../assets/images/plus-blue.svg' import { ReactComponent as Plus } from '../../assets/images/plus-blue.svg'
import PlusGrey from '../../assets/images/plus-grey.svg'
import { useExchangeContract } from '../../hooks' import { useExchangeContract } from '../../hooks'
import { amountFormatter, calculateGasMargin } from '../../utils' import { amountFormatter, calculateGasMargin } from '../../utils'
import { useTransactionAdder } from '../../contexts/Transactions' import { useTransactionAdder } from '../../contexts/Transactions'
import { useTokenDetails } from '../../contexts/Tokens' import { useTokenDetails } from '../../contexts/Tokens'
import { useFetchAllBalances } from '../../contexts/AllBalances'
import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances' import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances'
import { useAddressAllowance } from '../../contexts/Allowances' import { useAddressAllowance } from '../../contexts/Allowances'
...@@ -63,14 +64,6 @@ const DownArrowBackground = styled.div` ...@@ -63,14 +64,6 @@ const DownArrowBackground = styled.div`
justify-content: center; justify-content: center;
align-items: center; align-items: center;
` `
const DownArrow = styled.img`
width: 0.625rem;
height: 0.625rem;
position: relative;
padding: 0.875rem;
`
const SummaryPanel = styled.div` const SummaryPanel = styled.div`
${({ theme }) => theme.flexColumnNoWrap} ${({ theme }) => theme.flexColumnNoWrap}
padding: 1rem 0; padding: 1rem 0;
...@@ -87,7 +80,7 @@ const ExchangeRateWrapper = styled.div` ...@@ -87,7 +80,7 @@ const ExchangeRateWrapper = styled.div`
const ExchangeRate = styled.span` const ExchangeRate = styled.span`
flex: 1 1 auto; flex: 1 1 auto;
width: 0; width: 0;
color: ${({ theme }) => theme.chaliceGray}; color: ${({ theme }) => theme.doveGray};
` `
const Flex = styled.div` const Flex = styled.div`
...@@ -100,6 +93,17 @@ const Flex = styled.div` ...@@ -100,6 +93,17 @@ const Flex = styled.div`
} }
` `
const WrappedPlus = ({ isError, highSlippageWarning, ...rest }) => <Plus {...rest} />
const ColoredWrappedPlus = styled(WrappedPlus)`
width: 0.625rem;
height: 0.625rem;
position: relative;
padding: 0.875rem;
path {
stroke: ${({ active, theme }) => (active ? theme.royalBlue : theme.chaliceGray)};
}
`
function calculateSlippageBounds(value) { function calculateSlippageBounds(value) {
if (value) { if (value) {
const offset = value.mul(ALLOWED_SLIPPAGE).div(ethers.utils.bigNumberify(10000)) const offset = value.mul(ALLOWED_SLIPPAGE).div(ethers.utils.bigNumberify(10000))
...@@ -531,6 +535,8 @@ export default function AddLiquidity() { ...@@ -531,6 +535,8 @@ export default function AddLiquidity() {
const isActive = active && account const isActive = active && account
const isValid = (inputError === null || outputError === null) && !showUnlock const isValid = (inputError === null || outputError === null) && !showUnlock
const allBalances = useFetchAllBalances()
return ( return (
<> <>
{isNewExchange ? ( {isNewExchange ? (
...@@ -547,6 +553,7 @@ export default function AddLiquidity() { ...@@ -547,6 +553,7 @@ export default function AddLiquidity() {
<CurrencyInputPanel <CurrencyInputPanel
title={t('deposit')} title={t('deposit')}
allBalances={allBalances}
extraText={inputBalance && formatBalance(amountFormatter(inputBalance, 18, 4))} extraText={inputBalance && formatBalance(amountFormatter(inputBalance, 18, 4))}
onValueChange={inputValue => { onValueChange={inputValue => {
dispatchAddLiquidityState({ type: 'UPDATE_VALUE', payload: { value: inputValue, field: INPUT } }) dispatchAddLiquidityState({ type: 'UPDATE_VALUE', payload: { value: inputValue, field: INPUT } })
...@@ -558,11 +565,12 @@ export default function AddLiquidity() { ...@@ -558,11 +565,12 @@ export default function AddLiquidity() {
/> />
<OversizedPanel> <OversizedPanel>
<DownArrowBackground> <DownArrowBackground>
<DownArrow src={isActive ? PlusBlue : PlusGrey} alt="plus" /> <ColoredWrappedPlus active={isActive} alt="plus" />
</DownArrowBackground> </DownArrowBackground>
</OversizedPanel> </OversizedPanel>
<CurrencyInputPanel <CurrencyInputPanel
title={t('deposit')} title={t('deposit')}
allBalances={allBalances}
description={isNewExchange ? '' : outputValue ? `(${t('estimated')})` : ''} description={isNewExchange ? '' : outputValue ? `(${t('estimated')})` : ''}
extraText={outputBalance && formatBalance(amountFormatter(outputBalance, decimals, Math.min(decimals, 4)))} extraText={outputBalance && formatBalance(amountFormatter(outputBalance, decimals, Math.min(decimals, 4)))}
selectedTokenAddress={outputCurrency} selectedTokenAddress={outputCurrency}
......
...@@ -29,7 +29,7 @@ const ExchangeRateWrapper = styled.div` ...@@ -29,7 +29,7 @@ const ExchangeRateWrapper = styled.div`
const ExchangeRate = styled.span` const ExchangeRate = styled.span`
flex: 1 1 auto; flex: 1 1 auto;
width: 0; width: 0;
color: ${({ theme }) => theme.chaliceGray}; color: ${({ theme }) => theme.doveGray};
` `
const CreateExchangeWrapper = styled.div` const CreateExchangeWrapper = styled.div`
......
...@@ -4,10 +4,13 @@ import { useTranslation } from 'react-i18next' ...@@ -4,10 +4,13 @@ import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import OversizedPanel from '../../components/OversizedPanel' import OversizedPanel from '../../components/OversizedPanel'
import Dropdown from '../../assets/images/dropdown-blue.svg' import { ReactComponent as Dropdown } from '../../assets/images/dropdown-blue.svg'
import Modal from '../../components/Modal' import Modal from '../../components/Modal'
import { useBodyKeyDown } from '../../hooks' import { useBodyKeyDown } from '../../hooks'
import { lighten } from 'polished'
const poolTabOrder = [ const poolTabOrder = [
{ {
path: '/add-liquidity', path: '/add-liquidity',
...@@ -29,13 +32,16 @@ const poolTabOrder = [ ...@@ -29,13 +32,16 @@ const poolTabOrder = [
const LiquidityContainer = styled.div` const LiquidityContainer = styled.div`
${({ theme }) => theme.flexRowNoWrap}; ${({ theme }) => theme.flexRowNoWrap};
align-items: center; align-items: center;
font-size: 0.75rem; padding: 1rem 1rem;
padding: 0.625rem 1rem; font-size: 1rem;
font-size: 0.75rem;
color: ${({ theme }) => theme.royalBlue}; color: ${({ theme }) => theme.royalBlue};
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
:hover {
color: ${({ theme }) => lighten(0.1, theme.royalBlue)};
}
img { img {
height: 0.75rem; height: 0.75rem;
width: 0.75rem; width: 0.75rem;
...@@ -62,21 +68,28 @@ const StyledNavLink = styled(NavLink).attrs({ ...@@ -62,21 +68,28 @@ const StyledNavLink = styled(NavLink).attrs({
font-size: 1rem; font-size: 1rem;
&.${activeClassName} { &.${activeClassName} {
background-color: ${({ theme }) => theme.white}; background-color: ${({ theme }) => theme.inputBackground};
border-radius: 3rem; border-radius: 3rem;
box-shadow: 0 0 1px 1px ${({ theme }) => theme.mercuryGray}; border: 1px solid ${({ theme }) => theme.mercuryGray};
font-weight: 500; font-weight: 500;
color: ${({ theme }) => theme.royalBlue}; color: ${({ theme }) => theme.royalBlue};
} }
` `
const PoolModal = styled.div` const PoolModal = styled.div`
background-color: ${({ theme }) => theme.white}; background-color: ${({ theme }) => theme.inputBackground};
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 2rem 0 2rem 0; padding: 2rem 0 2rem 0;
` `
const WrappedDropdown = ({ isError, highSlippageWarning, ...rest }) => <Dropdown {...rest} />
const ColoredDropdown = styled(WrappedDropdown)`
path {
stroke: ${({ theme }) => theme.royalBlue};
}
`
function ModeSelector({ location: { pathname }, history }) { function ModeSelector({ location: { pathname }, history }) {
const { t } = useTranslation() const { t } = useTranslation()
...@@ -109,7 +122,7 @@ function ModeSelector({ location: { pathname }, history }) { ...@@ -109,7 +122,7 @@ function ModeSelector({ location: { pathname }, history }) {
}} }}
> >
<LiquidityLabel>{t(activeTabKey)}</LiquidityLabel> <LiquidityLabel>{t(activeTabKey)}</LiquidityLabel>
<img src={Dropdown} alt="dropdown" /> <ColoredDropdown alt="arrow down" />
</LiquidityContainer> </LiquidityContainer>
<Modal <Modal
isOpen={modalIsOpen} isOpen={modalIsOpen}
......
...@@ -9,12 +9,13 @@ import { Button } from '../../theme' ...@@ -9,12 +9,13 @@ import { Button } from '../../theme'
import CurrencyInputPanel from '../../components/CurrencyInputPanel' import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import ContextualInfo from '../../components/ContextualInfo' import ContextualInfo from '../../components/ContextualInfo'
import OversizedPanel from '../../components/OversizedPanel' import OversizedPanel from '../../components/OversizedPanel'
import ArrowDownBlue from '../../assets/images/arrow-down-blue.svg' import ArrowDown from '../../assets/svg/SVGArrowDown'
import ArrowDownGrey from '../../assets/images/arrow-down-grey.svg'
import { useExchangeContract } from '../../hooks' import { useExchangeContract } from '../../hooks'
import { useTransactionAdder } from '../../contexts/Transactions' import { useTransactionAdder } from '../../contexts/Transactions'
import { useTokenDetails } from '../../contexts/Tokens' import { useTokenDetails } from '../../contexts/Tokens'
import { useAddressBalance } from '../../contexts/Balances' import { useAddressBalance } from '../../contexts/Balances'
import { useFetchAllBalances } from '../../contexts/AllBalances'
import { calculateGasMargin, amountFormatter } from '../../utils' import { calculateGasMargin, amountFormatter } from '../../utils'
// denominated in bips // denominated in bips
...@@ -36,7 +37,9 @@ const DownArrowBackground = styled.div` ...@@ -36,7 +37,9 @@ const DownArrowBackground = styled.div`
align-items: center; align-items: center;
` `
const DownArrow = styled.img` const DownArrow = styled(ArrowDown)`
${({ theme }) => theme.flexRowNoWrap}
color: ${({ theme, active }) => (active ? theme.royalBlue : theme.doveGray)};
width: 0.625rem; width: 0.625rem;
height: 0.625rem; height: 0.625rem;
position: relative; position: relative;
...@@ -80,7 +83,7 @@ const ExchangeRateWrapper = styled.div` ...@@ -80,7 +83,7 @@ const ExchangeRateWrapper = styled.div`
const ExchangeRate = styled.span` const ExchangeRate = styled.span`
flex: 1 1 auto; flex: 1 1 auto;
width: 0; width: 0;
color: ${({ theme }) => theme.chaliceGray}; color: ${({ theme }) => theme.doveGray};
` `
const Flex = styled.div` const Flex = styled.div`
...@@ -326,10 +329,13 @@ export default function RemoveLiquidity() { ...@@ -326,10 +329,13 @@ export default function RemoveLiquidity() {
const marketRate = getMarketRate(exchangeETHBalance, exchangeTokenBalance, decimals) const marketRate = getMarketRate(exchangeETHBalance, exchangeTokenBalance, decimals)
const allBalances = useFetchAllBalances()
return ( return (
<> <>
<CurrencyInputPanel <CurrencyInputPanel
title={t('poolTokens')} title={t('poolTokens')}
allBalances={allBalances}
extraText={poolTokenBalance && formatBalance(amountFormatter(poolTokenBalance, 18, 4))} extraText={poolTokenBalance && formatBalance(amountFormatter(poolTokenBalance, 18, 4))}
extraTextClickHander={() => { extraTextClickHander={() => {
if (poolTokenBalance) { if (poolTokenBalance) {
...@@ -347,11 +353,12 @@ export default function RemoveLiquidity() { ...@@ -347,11 +353,12 @@ export default function RemoveLiquidity() {
/> />
<OversizedPanel> <OversizedPanel>
<DownArrowBackground> <DownArrowBackground>
<DownArrow src={isActive ? ArrowDownBlue : ArrowDownGrey} alt="arrow" /> <DownArrow active={isActive} alt="arrow" />
</DownArrowBackground> </DownArrowBackground>
</OversizedPanel> </OversizedPanel>
<CurrencyInputPanel <CurrencyInputPanel
title={t('output')} title={t('output')}
allBalances={allBalances}
description={!!(ethWithdrawn && tokenWithdrawn) ? `(${t('estimated')})` : ''} description={!!(ethWithdrawn && tokenWithdrawn) ? `(${t('estimated')})` : ''}
key="remove-liquidity-input" key="remove-liquidity-input"
renderInput={() => renderInput={() =>
......
...@@ -12,8 +12,8 @@ export const Button = styled.button.attrs(({ warning, theme }) => ({ ...@@ -12,8 +12,8 @@ export const Button = styled.button.attrs(({ warning, theme }) => ({
border: none; border: none;
outline: none; outline: none;
background-color: ${({ backgroundColor }) => backgroundColor}; background-color: ${({ backgroundColor }) => backgroundColor};
transition: background-color 150ms ease-out;
color: ${({ theme }) => theme.white}; color: ${({ theme }) => theme.white};
transition: background-color 125ms ease-in-out;
width: 100%; width: 100%;
:hover, :hover,
...@@ -26,7 +26,8 @@ export const Button = styled.button.attrs(({ warning, theme }) => ({ ...@@ -26,7 +26,8 @@ export const Button = styled.button.attrs(({ warning, theme }) => ({
} }
:disabled { :disabled {
background-color: ${({ theme }) => theme.mercuryGray}; background-color: ${({ theme }) => theme.concreteGray};
color: ${({ theme }) => theme.silverGray};
cursor: auto; cursor: auto;
} }
` `
...@@ -50,12 +51,13 @@ export const Link = styled.a.attrs({ ...@@ -50,12 +51,13 @@ export const Link = styled.a.attrs({
` `
export const BorderlessInput = styled.input` export const BorderlessInput = styled.input`
color: ${({ theme }) => theme.mineshaftGray}; color: ${({ theme }) => theme.textColor};
font-size: 1rem; font-size: 1rem;
outline: none; outline: none;
border: none; border: none;
flex: 1 1 auto; flex: 1 1 auto;
width: 0; width: 0;
background-color: ${({ theme }) => theme.inputBackground};
[type='number'] { [type='number'] {
-moz-appearance: textfield; -moz-appearance: textfield;
...@@ -67,7 +69,7 @@ export const BorderlessInput = styled.input` ...@@ -67,7 +69,7 @@ export const BorderlessInput = styled.input`
} }
::placeholder { ::placeholder {
color: ${({ theme }) => theme.mercuryGray}; color: ${({ theme }) => theme.chaliceGray};
} }
` `
......
import React from 'react' import React from 'react'
import { ThemeProvider as StyledComponentsThemeProvider, createGlobalStyle, css } from 'styled-components' import { ThemeProvider as StyledComponentsThemeProvider, createGlobalStyle, css } from 'styled-components'
import { useDarkModeManager } from '../contexts/LocalStorage'
export * from './components' export * from './components'
...@@ -28,23 +29,36 @@ const flexRowNoWrap = css` ...@@ -28,23 +29,36 @@ const flexRowNoWrap = css`
flex-flow: row nowrap; flex-flow: row nowrap;
` `
const theme = { const white = '#FFFFFF'
white: '#FFFFFF', const black = '#000000'
black: '#000000',
const theme = darkMode => ({
white,
black,
textColor: darkMode ? white : '#010101',
// for setting css on <html>
backgroundColor: darkMode ? '#333639' : white,
inputBackground: darkMode ? '#202124' : white,
placeholderGray: darkMode ? '#5F5F5F' : '#E1E1E1',
// grays // grays
concreteGray: '#FAFAFA', concreteGray: darkMode ? '#292C2F' : '#FAFAFA',
mercuryGray: '#E1E1E1', mercuryGray: darkMode ? '#333333' : '#E1E1E1',
silverGray: '#C4C4C4', silverGray: darkMode ? '#737373' : '#C4C4C4',
chaliceGray: '#AEAEAE', chaliceGray: darkMode ? '#7B7B7B' : '#AEAEAE',
doveGray: '#737373', doveGray: darkMode ? '#C4C4C4' : '#737373',
mineshaftGray: '#2B2B2B', mineshaftGray: darkMode ? '#E1E1E1' : '#2B2B2B',
buttonOutlineGrey: '#f2f2f2', buttonOutlineGrey: darkMode ? '#FAFAFA' : '#F2F2F2',
tokenRowHover: darkMode ? '#404040' : '#F2F2F2',
//blacks //blacks
charcoalBlack: '#404040', charcoalBlack: darkMode ? '#F2F2F2' : '#404040',
// blues // blues
zumthorBlue: '#EBF4FF', zumthorBlue: darkMode ? '#212529' : '#EBF4FF',
malibuBlue: '#5CA2FF', malibuBlue: darkMode ? '#E67AEF' : '#5CA2FF',
royalBlue: '#2F80ED', royalBlue: darkMode ? '#DC6BE5' : '#2F80ED',
loadingBlue: darkMode ? '#e4f0ff' : '#e4f0ff',
// purples // purples
wisteriaPurple: '#DC6BE5', wisteriaPurple: '#DC6BE5',
// reds // reds
...@@ -57,15 +71,20 @@ const theme = { ...@@ -57,15 +71,20 @@ const theme = {
uniswapPink: '#DC6BE5', uniswapPink: '#DC6BE5',
connectedGreen: '#27AE60', connectedGreen: '#27AE60',
//specific
textHover: darkMode ? theme.uniswapPink : theme.doveGray,
// media queries // media queries
mediaWidth: mediaWidthTemplates, mediaWidth: mediaWidthTemplates,
// css snippets // css snippets
flexColumnNoWrap, flexColumnNoWrap,
flexRowNoWrap flexRowNoWrap
} })
export default function ThemeProvider({ children }) { export default function ThemeProvider({ children }) {
return <StyledComponentsThemeProvider theme={theme}>{children}</StyledComponentsThemeProvider> const [darkMode] = useDarkModeManager()
return <StyledComponentsThemeProvider theme={theme(darkMode)}>{children}</StyledComponentsThemeProvider>
} }
export const GlobalStyle = createGlobalStyle` export const GlobalStyle = createGlobalStyle`
...@@ -79,20 +98,25 @@ export const GlobalStyle = createGlobalStyle` ...@@ -79,20 +98,25 @@ export const GlobalStyle = createGlobalStyle`
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
body > div {
height: 100%;
overflow: scroll;
-webkit-overflow-scrolling: touch;
}
html {
font-size: 16px; font-size: 16px;
font-variant: none; font-variant: none;
color: ${({ theme }) => theme.textColor};
background-color: ${({ theme }) => theme.backgroundColor};
transition: color 150ms ease-out, background-color 150ms ease-out;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
} }
#root {
${({ theme }) => theme.flexColumnNoWrap}
justify-content: center;
align-items: center;
width: 100vw;
height: 100vh;
overflow-x: hidden;
overflow-y: auto;
}
` `
...@@ -5,6 +5,7 @@ import EXCHANGE_ABI from '../constants/abis/exchange' ...@@ -5,6 +5,7 @@ import EXCHANGE_ABI from '../constants/abis/exchange'
import ERC20_ABI from '../constants/abis/erc20' import ERC20_ABI from '../constants/abis/erc20'
import ERC20_BYTES32_ABI from '../constants/abis/erc20_bytes32' import ERC20_BYTES32_ABI from '../constants/abis/erc20_bytes32'
import { FACTORY_ADDRESSES } from '../constants' import { FACTORY_ADDRESSES } from '../constants'
import { formatFixed } from '@uniswap/sdk'
import UncheckedJsonRpcSigner from './signer' import UncheckedJsonRpcSigner from './signer'
...@@ -178,10 +179,27 @@ export async function getEtherBalance(address, library) { ...@@ -178,10 +179,27 @@ export async function getEtherBalance(address, library) {
if (!isAddress(address)) { if (!isAddress(address)) {
throw Error(`Invalid 'address' parameter '${address}'`) throw Error(`Invalid 'address' parameter '${address}'`)
} }
return library.getBalance(address) return library.getBalance(address)
} }
export function formatEthBalance(balance) {
return amountFormatter(balance, 18, 6)
}
export function formatTokenBalance(balance, decimal) {
return !!(balance && Number.isInteger(decimal)) ? amountFormatter(balance, decimal, Math.min(4, decimal)) : 0
}
export function formatToUsd(price) {
const format = { decimalSeparator: '.', groupSeparator: ',', groupSize: 3 }
const usdPrice = formatFixed(price, {
decimalPlaces: 2,
dropTrailingZeros: false,
format
})
return usdPrice
}
// get the token balance of an address // get the token balance of an address
export async function getTokenBalance(tokenAddress, address, library) { export async function getTokenBalance(tokenAddress, address, library) {
if (!isAddress(tokenAddress) || !isAddress(address)) { if (!isAddress(tokenAddress) || !isAddress(address)) {
......
import { BigNumber } from '@uniswap/sdk'
// returns a deep copied + sorted list of values, as well as a sortmap
export function sortBigNumbers(values) {
const valueMap = values.map((value, i) => ({ value, i }))
valueMap.sort((a, b) => {
if (a.value.isGreaterThan(b.value)) {
return 1
} else if (a.value.isLessThan(b.value)) {
return -1
} else {
return 0
}
})
return [
valueMap.map(element => values[element.i]),
values.map((_, i) => valueMap.findIndex(element => element.i === i))
]
}
export function getMedian(values) {
const [sortedValues, sortMap] = sortBigNumbers(values)
if (values.length % 2 === 0) {
const middle = values.length / 2
const indices = [middle - 1, middle]
return [
sortedValues[middle - 1].plus(sortedValues[middle]).dividedBy(2),
sortMap.map(element => (indices.includes(element) ? new BigNumber(0.5) : new BigNumber(0)))
]
} else {
const middle = Math.floor(values.length / 2)
return [sortedValues[middle], sortMap.map(element => (element === middle ? new BigNumber(1) : new BigNumber(0)))]
}
}
export function getMean(values, _weights) {
const weights = _weights ? _weights : values.map(() => new BigNumber(1))
const weightedValues = values.map((value, i) => value.multipliedBy(weights[i]))
const numerator = weightedValues.reduce(
(accumulator, currentValue) => accumulator.plus(currentValue),
new BigNumber(0)
)
const denominator = weights.reduce((accumulator, currentValue) => accumulator.plus(currentValue), new BigNumber(0))
return [numerator.dividedBy(denominator), weights.map(weight => weight.dividedBy(denominator))]
}
import { getTokenReserves, getMarketDetails } from '@uniswap/sdk'
import { getMedian, getMean } from './math'
const DAI = 'DAI'
const USDC = 'USDC'
const TUSD = 'TUSD'
const USD_STABLECOINS = [DAI, USDC, TUSD]
const USD_STABLECOIN_ADDRESSES = [
'0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359',
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
'0x8dd5fbCe2F6a956C3022bA3663759011Dd51e73E'
]
function forEachStablecoin(runner) {
return USD_STABLECOINS.map((stablecoin, index) => runner(index, stablecoin))
}
export async function getUSDPrice(library) {
return Promise.all(forEachStablecoin(i => getTokenReserves(USD_STABLECOIN_ADDRESSES[i], library))).then(reserves => {
const ethReserves = forEachStablecoin(i => reserves[i].ethReserve.amount)
const marketDetails = forEachStablecoin(i => getMarketDetails(reserves[i], undefined))
const ethPrices = forEachStablecoin(i => marketDetails[i].marketRate.rateInverted)
const [median, medianWeights] = getMedian(ethPrices)
const [mean, meanWeights] = getMean(ethPrices)
const [weightedMean, weightedMeanWeights] = getMean(ethPrices, ethReserves)
const ethPrice = getMean([median, mean, weightedMean])[0]
const _stablecoinWeights = [
getMean([medianWeights[0], meanWeights[0], weightedMeanWeights[0]])[0],
getMean([medianWeights[1], meanWeights[1], weightedMeanWeights[1]])[0],
getMean([medianWeights[2], meanWeights[2], weightedMeanWeights[2]])[0]
]
const stablecoinWeights = forEachStablecoin((i, stablecoin) => ({
[stablecoin]: _stablecoinWeights[i]
})).reduce((accumulator, currentValue) => ({ ...accumulator, ...currentValue }), {})
return [ethPrice, stablecoinWeights]
})
}
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment