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.
......@@ -24,4 +24,6 @@ yarn-debug.log*
yarn-error.log*
notes.txt
.idea/
\ No newline at end of file
.idea/
.vscode/
\ No newline at end of file
......@@ -18,7 +18,8 @@
"unlock": "Unlock",
"pending": "Pending",
"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",
"exchangeRate": "Exchange Rate",
"unknownError": "Oops! An unknown error occurred. Please refresh the page, or visit from another browser or device.",
......@@ -36,6 +37,7 @@
"youWillReceive": "You will receive at least",
"youAreBuying": "You are buying",
"itWillCost": "It will cost at most",
"forAtMost": "for at most",
"insufficientBalance": "Insufficient Balance",
"inputNotValid": "Not a valid input value",
"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'
import styled from 'styled-components'
import { useTranslation } from 'react-i18next'
import { useWeb3Context } from 'web3-react'
import { lighten } from 'polished'
import { transparentize } from 'polished'
import { isAddress } from '../../utils'
import { useDebounce } from '../../hooks'
const InputPanel = styled.div`
${({ 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;
border-radius: 1.25rem;
background-color: ${({ theme }) => theme.white};
background-color: ${({ theme }) => theme.inputBackground};
z-index: 1;
`
......@@ -21,9 +21,10 @@ const ContainerRow = styled.div`
justify-content: center;
align-items: center;
border-radius: 1.25rem;
box-shadow: 0 0 0 1px ${({ error, theme }) => (error ? theme.salmonRed : theme.mercuryGray)};
background-color: ${({ theme }) => theme.white};
transition: box-shadow 200ms ease-in-out;
border: 1px solid ${({ error, theme }) => (error ? theme.salmonRed : theme.mercuryGray)};
background-color: ${({ theme }) => theme.inputBackground};
transition: box-shadow 125ms ease-in-out;
`
const InputContainer = styled.div`
......@@ -59,13 +60,15 @@ const Input = styled.input`
border: none;
flex: 1 1 auto;
width: 0;
background-color: ${({ theme }) => theme.inputBackground};
color: ${({ error, theme }) => (error ? theme.salmonRed : theme.royalBlue)};
transition: color 200ms ease-in-out;
transition: color 125ms ease-in-out;
overflow: hidden;
text-overflow: ellipsis;
::placeholder {
color: ${({ theme }) => theme.chaliceGray};
color: ${({ theme }) => theme.placeholderGray};
}
`
......@@ -93,41 +96,54 @@ export default function AddressInputPanel({ title, initialInput = '', onChange =
let stale = false
if (isAddress(debouncedInput)) {
library
.lookupAddress(debouncedInput)
.then(name => {
if (!stale) {
// if an ENS name exists, set it as the destination
if (name) {
setInput(name)
} else {
setData({ address: debouncedInput, name: '' })
setError(null)
}
}
})
.catch(() => {
setData({ address: debouncedInput, name: '' })
setError(null)
})
} else {
if (debouncedInput !== '') {
try {
library
.resolveName(debouncedInput)
.then(address => {
.lookupAddress(debouncedInput)
.then(name => {
if (!stale) {
// if the debounced input name resolves to an address
if (address) {
setData({ address: address, name: debouncedInput })
setError(null)
// if an ENS name exists, set it as the destination
if (name) {
setInput(name)
} else {
setError(true)
setData({ address: debouncedInput, name: '' })
setError(null)
}
}
})
.catch(() => {
setError(true)
if (!stale) {
setData({ address: debouncedInput, name: '' })
setError(null)
}
})
} catch {
setData({ address: debouncedInput, name: '' })
setError(null)
}
} else {
if (debouncedInput !== '') {
try {
library
.resolveName(debouncedInput)
.then(address => {
if (!stale) {
// if the debounced input name resolves to an address
if (address) {
setData({ address: address, name: debouncedInput })
setError(null)
} else {
setError(true)
}
}
})
.catch(() => {
if (!stale) {
setError(true)
}
})
} catch {
setError(true)
}
}
}
......
......@@ -2,8 +2,8 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import DropdownBlue from '../../assets/images/dropdown-blue.svg'
import DropupBlue from '../../assets/images/dropup-blue.svg'
import { ReactComponent as Dropup } from '../../assets/images/dropup-blue.svg'
import { ReactComponent as Dropdown } from '../../assets/images/dropdown-blue.svg'
const SummaryWrapper = styled.div`
color: ${({ error, theme }) => (error ? theme.salmonRed : theme.doveGray)};
......@@ -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 {
static propTypes = {
openDetailsText: PropTypes.string,
......@@ -89,12 +103,12 @@ class ContextualInfo extends Component {
{!this.state.showDetails ? (
<>
<span>{openDetailsText}</span>
<img src={DropdownBlue} alt="dropdown" />
<ColoredDropup />
</>
) : (
<>
<span>{closeDetailsText}</span>
<img src={DropupBlue} alt="dropup" />
<ColoredDropdown />
</>
)}
</SummaryWrapperContainer>
......
......@@ -32,8 +32,7 @@ const SummaryWrapperContainer = styled.div`
const Details = styled.div`
background-color: ${({ theme }) => theme.concreteGray};
padding: 1.5rem;
padding-bottom: 1rem;
/* padding: 1.25rem 1.25rem 1rem 1.25rem; */
border-radius: 1rem;
font-size: 0.75rem;
margin: 1rem 0.5rem 0 0.5rem;
......@@ -62,7 +61,7 @@ const ErrorSpan = styled.span`
const WrappedDropup = ({ isError, highSlippageWarning, ...rest }) => <Dropup {...rest} />
const ColoredDropup = styled(WrappedDropup)`
path {
stroke: ${({ isError, theme }) => isError && theme.salmonRed};
stroke: ${({ isError, theme }) => (isError ? theme.salmonRed : theme.royalBlue)};
${({ highSlippageWarning, theme }) =>
highSlippageWarning &&
......@@ -75,7 +74,7 @@ const ColoredDropup = styled(WrappedDropup)`
const WrappedDropdown = ({ isError, highSlippageWarning, ...rest }) => <Dropdown {...rest} />
const ColoredDropdown = styled(WrappedDropdown)`
path {
stroke: ${({ isError, theme }) => isError && theme.salmonRed};
stroke: ${({ isError, theme }) => (isError ? theme.salmonRed : theme.royalBlue)};
${({ highSlippageWarning, theme }) =>
highSlippageWarning &&
......
......@@ -2,22 +2,28 @@ import React, { useState, useRef, useMemo } from 'react'
import { Link } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { ethers } from 'ethers'
import { BigNumber } from '@uniswap/sdk'
import styled from 'styled-components'
import escapeStringRegex from 'escape-string-regexp'
import { lighten, darken } from 'polished'
import { darken } from 'polished'
import Tooltip from '@reach/tooltip'
import '@reach/tooltip/styles.css'
import { isMobile } from 'react-device-detect'
import { BorderlessInput } from '../../theme'
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 Modal from '../Modal'
import TokenLogo from '../TokenLogo'
import SearchIcon from '../../assets/images/magnifying-glass.svg'
import { useTransactionAdder, usePendingApproval } from '../../contexts/Transactions'
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)
......@@ -34,29 +40,33 @@ const SubCurrencySelect = styled.button`
outline: none;
cursor: pointer;
user-select: none;
`
const InputRow = styled.div`
${({ theme }) => theme.flexRowNoWrap}
align-items: center;
padding: 0.25rem 0.85rem 0.75rem;
`
const Input = styled(BorderlessInput)`
font-size: 1.5rem;
color: ${({ error, theme }) => error && theme.salmonRed};
background-color: ${({ theme }) => theme.inputBackground};
`
const StyledBorderlessInput = styled(BorderlessInput)`
min-height: 1.75rem;
min-height: 2.5rem;
flex-shrink: 0;
text-align: left;
padding-left: 1.6rem;
background-color: ${({ theme }) => theme.concreteGray};
`
const CurrencySelect = styled.button`
align-items: center;
font-size: 1rem;
color: ${({ selected, theme }) => (selected ? theme.black : theme.royalBlue)};
color: ${({ selected, theme }) => (selected ? theme.textColor : theme.royalBlue)};
height: 2rem;
border: 1px solid ${({ selected, theme }) => (selected ? theme.mercuryGray : theme.royalBlue)};
border-radius: 2.5rem;
......@@ -90,27 +100,28 @@ const StyledDropDown = styled(DropDown)`
height: 35%;
path {
stroke: ${({ selected, theme }) => (selected ? theme.black : theme.royalBlue)};
stroke: ${({ selected, theme }) => (selected ? theme.textColor : theme.royalBlue)};
}
`
const InputPanel = styled.div`
${({ 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;
border-radius: 1.25rem;
background-color: ${({ theme }) => theme.white};
background-color: ${({ theme }) => theme.inputBackground};
z-index: 1;
`
const Container = styled.div`
border-radius: 1.25rem;
box-shadow: 0 0 0 1px ${({ error, theme }) => (error ? theme.salmonRed : theme.mercuryGray)};
background-color: ${({ theme }) => theme.white};
transition: box-shadow 200ms ease-in-out;
border: 1px solid ${({ error, theme }) => (error ? theme.salmonRed : theme.mercuryGray)};
background-color: ${({ theme }) => theme.inputBackground};
transition: box-shadow 150ms ease-out;
: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`
const TokenModal = styled.div`
${({ theme }) => theme.flexColumnNoWrap}
background-color: ${({ theme }) => theme.white};
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`
${({ theme }) => theme.flexRowNoWrap}
padding: 1rem;
border-bottom: 1px solid ${({ theme }) => theme.mercuryGray};
padding: 0.5rem 2rem;
background-color: ${({ theme }) => theme.concreteGray};
`
const TokenModalInfo = styled.div`
......@@ -174,9 +201,8 @@ const TokenList = styled.div`
const TokenModalRow = styled.div`
${({ theme }) => theme.flexRowNoWrap}
align-items: center;
padding: 1rem 1.5rem;
margin: 0.25rem 0.5rem;
justify-content: space-between;
padding: 0.8rem 2rem;
cursor: pointer;
user-select: none;
......@@ -185,16 +211,55 @@ const TokenModalRow = styled.div`
}
: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`
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({
onValueChange = () => {},
allBalances,
renderInput,
onCurrencySelected = () => {},
title,
......@@ -233,7 +298,6 @@ export default function CurrencyInputPanel({
selectedTokenExchangeAddress,
ethers.constants.MaxUint256
)
tokenContract
.approve(selectedTokenExchangeAddress, ethers.constants.MaxUint256, {
gasLimit: calculateGasMargin(estimatedGas, GAS_MARGIN)
......@@ -334,48 +398,106 @@ export default function CurrencyInputPanel({
{!disableTokenSelect && (
<CurrencySelectModal
isOpen={modalIsOpen}
// isOpen={true}
onDismiss={() => {
setModalIsOpen(false)
}}
onTokenSelect={onCurrencySelected}
allBalances={allBalances}
/>
)}
</InputPanel>
)
}
function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect }) {
function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect, allBalances }) {
const { t } = useTranslation()
const [searchQuery, setSearchQuery] = useState('')
const { exchangeAddress } = useTokenDetails(searchQuery)
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(() => {
return Object.keys(allTokens)
.sort((a, b) => {
const aSymbol = allTokens[a].symbol.toLowerCase()
const bSymbol = allTokens[b].symbol.toLowerCase()
if (aSymbol === 'ETH'.toLowerCase() || bSymbol === 'ETH'.toLowerCase()) {
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 => {
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 {
name: allTokens[k].name,
symbol: allTokens[k].symbol,
address: k
address: k,
balance: balance,
usdBalance: usdBalance
}
})
}, [allTokens])
}, [allBalances, allTokens, usdAmounts])
const filteredTokenList = useMemo(() => {
return tokenList.filter(tokenEntry => {
// check the regex for each field
const regexMatches = Object.keys(tokenEntry).map(tokenEntryKey => {
return (
tokenEntry[tokenEntryKey] &&
typeof tokenEntry[tokenEntryKey] === 'string' &&
!!tokenEntry[tokenEntryKey].match(new RegExp(escapeStringRegex(searchQuery), 'i'))
)
})
......@@ -394,7 +516,6 @@ function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect }) {
if (isAddress(searchQuery) && exchangeAddress === undefined) {
return <TokenModalInfo>Searching for Exchange...</TokenModalInfo>
}
if (isAddress(searchQuery) && exchangeAddress === ethers.constants.AddressZero) {
return (
<>
......@@ -405,16 +526,30 @@ function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect }) {
</>
)
}
if (!filteredTokenList.length) {
return <TokenModalInfo>{t('noExchange')}</TokenModalInfo>
}
return filteredTokenList.map(({ address, symbol }) => {
return filteredTokenList.map(({ address, symbol, name, balance, usdBalance }) => {
return (
<TokenModalRow key={address} onClick={() => _onTokenSelect(address)}>
<TokenLogo address={address} />
<span id="symbol">{symbol}</span>
<TokenRowLeft>
<TokenLogo address={address} size={'2rem'} />
<TokenSymbolGroup>
<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>
)
})
......@@ -429,12 +564,33 @@ function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect }) {
setSearchQuery(checksummedInput || input)
}
function clearInputAndDismiss() {
setSearchQuery('')
onDismiss()
}
return (
<Modal isOpen={isOpen} onDismiss={onDismiss} minHeight={50} initialFocusRef={isMobile ? undefined : inputRef}>
<Modal
isOpen={isOpen}
onDismiss={clearInputAndDismiss}
minHeight={60}
initialFocusRef={isMobile ? undefined : inputRef}
>
<TokenModal>
<ModalHeader>
<p>Select Token</p>
<CloseIcon onClick={clearInputAndDismiss}>
<img src={close} alt={'close icon'} />
</CloseIcon>
</ModalHeader>
<SearchContainer>
<StyledBorderlessInput ref={inputRef} type="text" placeholder={t('searchOrPaste')} onChange={onInput} />
<img src={SearchIcon} alt="search" />
<StyledBorderlessInput
ref={inputRef}
type="text"
placeholder={isMobile ? t('searchOrPasteMobile') : t('searchOrPaste')}
onChange={onInput}
/>
</SearchContainer>
<TokenList>{renderTokenList()}</TokenList>
</TokenModal>
......
import React, { useState, useReducer, useEffect } from 'react'
import ReactGA from 'react-ga'
import { useTranslation } from 'react-i18next'
import { useWeb3Context } from 'web3-react'
import { ethers } from 'ethers'
import styled from 'styled-components'
......@@ -10,13 +12,13 @@ import CurrencyInputPanel from '../CurrencyInputPanel'
import AddressInputPanel from '../AddressInputPanel'
import OversizedPanel from '../OversizedPanel'
import TransactionDetails from '../TransactionDetails'
import ArrowDownBlue from '../../assets/images/arrow-down-blue.svg'
import ArrowDownGrey from '../../assets/images/arrow-down-grey.svg'
import ArrowDown from '../../assets/svg/SVGArrowDown'
import { amountFormatter, calculateGasMargin } from '../../utils'
import { useExchangeContract } from '../../hooks'
import { useTokenDetails } from '../../contexts/Tokens'
import { useTransactionAdder } from '../../contexts/Transactions'
import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances'
import { useFetchAllBalances } from '../../contexts/AllBalances'
import { useAddressAllowance } from '../../contexts/Allowances'
const INPUT = 0
......@@ -27,23 +29,24 @@ const TOKEN_TO_ETH = 1
const TOKEN_TO_TOKEN = 2
// denominated in bips
const ALLOWED_SLIPPAGE_DEFAULT = 150
const TOKEN_ALLOWED_SLIPPAGE_DEFAULT = 200
const ALLOWED_SLIPPAGE_DEFAULT = 100
const TOKEN_ALLOWED_SLIPPAGE_DEFAULT = 100
// denominated in seconds
// 15 minutes, denominated in seconds
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 DownArrowBackground = styled.div`
${({ theme }) => theme.flexRowNoWrap}
justify-content: 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;
height: 0.625rem;
position: relative;
......@@ -62,7 +65,7 @@ const ExchangeRateWrapper = styled.div`
const ExchangeRate = styled.span`
flex: 1 1 auto;
width: 0;
color: ${({ theme }) => theme.chaliceGray};
color: ${({ theme }) => theme.doveGray};
`
const Flex = styled.div`
......@@ -241,16 +244,17 @@ export default function ExchangePage({ initialCurrency, sending }) {
const [rawSlippage, setRawSlippage] = useState(ALLOWED_SLIPPAGE_DEFAULT)
const [rawTokenSlippage, setRawTokenSlippage] = useState(TOKEN_ALLOWED_SLIPPAGE_DEFAULT)
let allowedSlippageBig = ethers.utils.bigNumberify(rawSlippage)
let tokenAllowedSlippageBig = ethers.utils.bigNumberify(rawTokenSlippage)
const allowedSlippageBig = ethers.utils.bigNumberify(rawSlippage)
const tokenAllowedSlippageBig = ethers.utils.bigNumberify(rawTokenSlippage)
// analytics
useEffect(() => {
ReactGA.pageview(window.location.pathname + window.location.search)
}, [])
// core swap state-
// core swap state
const [swapState, dispatchSwapState] = useReducer(swapStateReducer, initialCurrency, getInitialSwapState)
const { independentValue, dependentValue, independentField, inputCurrency, outputCurrency } = swapState
const [recipient, setRecipient] = useState({ address: '', name: '' })
......@@ -259,7 +263,7 @@ export default function ExchangePage({ initialCurrency, sending }) {
// get swap type from the currency types
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(
inputCurrency
)
......@@ -583,10 +587,13 @@ export default function ExchangePage({ initialCurrency, sending }) {
const [customSlippageError, setcustomSlippageError] = useState('')
const allBalances = useFetchAllBalances()
return (
<>
<CurrencyInputPanel
title={t('input')}
allBalances={allBalances}
description={inputValueFormatted && independentField === OUTPUT ? estimatedText : ''}
extraText={inputBalanceFormatted && formatBalance(inputBalanceFormatted)}
extraTextClickHander={() => {
......@@ -620,12 +627,13 @@ export default function ExchangePage({ initialCurrency, sending }) {
}}
clickable
alt="swap"
src={isValid ? ArrowDownBlue : ArrowDownGrey}
active={isValid}
/>
</DownArrowBackground>
</OversizedPanel>
<CurrencyInputPanel
title={t('output')}
allBalances={allBalances}
description={outputValueFormatted && independentField === INPUT ? estimatedText : ''}
extraText={outputBalanceFormatted && formatBalance(outputBalanceFormatted)}
onCurrencySelected={outputCurrency => {
......@@ -644,7 +652,7 @@ export default function ExchangePage({ initialCurrency, sending }) {
<>
<OversizedPanel>
<DownArrowBackground>
<DownArrow src={isValid ? ArrowDownBlue : ArrowDownGrey} alt="arrow" />
<DownArrow active={isValid} alt="arrow" />
</DownArrowBackground>
</OversizedPanel>
<AddressInputPanel onChange={setRecipient} onError={setRecipientError} />
......@@ -662,13 +670,13 @@ export default function ExchangePage({ initialCurrency, sending }) {
{inverted ? (
<span>
{exchangeRate
? `1 ${outputSymbol} = ${amountFormatter(exchangeRateInverted, 18, 4, false)} ${inputSymbol}`
? `1 ${inputSymbol} = ${amountFormatter(exchangeRate, 18, 4, false)} ${outputSymbol}`
: ' - '}
</span>
) : (
<span>
{exchangeRate
? `1 ${inputSymbol} = ${amountFormatter(exchangeRate, 18, 4, false)} ${outputSymbol}`
? `1 ${outputSymbol} = ${amountFormatter(exchangeRateInverted, 18, 4, false)} ${inputSymbol}`
: ' - '}
</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'
import Web3Status from '../Web3Status'
import { darken } from 'polished'
const HeaderFrame = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
`
const HeaderElement = styled.div`
margin: 1.25rem;
display: flex;
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`
display: flex;
align-items: center;
#image {
font-size: 1.5rem;
margin-right: 1rem;
:hover {
cursor: pointer;
}
#link {
text-decoration-color: ${({ theme }) => theme.wisteriaPurple};
text-decoration-color: ${({ theme }) => theme.UniswapPink};
}
#title {
......@@ -37,22 +54,24 @@ const Title = styled.div`
export default function Header() {
return (
<>
<HeaderFrame>
<HeaderElement>
<Title>
<span id="image" role="img" aria-label="Unicorn Emoji">
🦄
</span>
<Nod>
<Link id="link" href="https://uniswap.io">
<span role="img" aria-label="unicorn">
🦄{' '}
</span>
</Link>
</Nod>
<Link id="link" href="https://uniswap.io">
<h1 id="title">Uniswap</h1>
</Link>
</Title>
</HeaderElement>
<HeaderElement>
<Web3Status />
</HeaderElement>
</>
</HeaderFrame>
)
}
......@@ -5,8 +5,9 @@ import { DialogOverlay, DialogContent } from '@reach/dialog'
import '@reach/dialog/styles.css'
const AnimatedDialogOverlay = animated(DialogOverlay)
const StyledDialogOverlay = styled(AnimatedDialogOverlay).attrs({
suppressclassnamewarning: 'true'
const WrappedDialogOverlay = ({ suppressClassNameWarning, ...rest }) => <AnimatedDialogOverlay {...rest} />
const StyledDialogOverlay = styled(WrappedDialogOverlay).attrs({
suppressClassNameWarning: true
})`
&[data-reach-dialog-overlay] {
z-index: 2;
......@@ -20,12 +21,14 @@ const FilteredDialogContent = ({ minHeight, ...rest }) => <DialogContent {...res
const StyledDialogContent = styled(FilteredDialogContent)`
&[data-reach-dialog-content] {
margin: 0 0 2rem 0;
${({ theme }) => theme.mediaWidth.upToMedium`margin: 0;`}
padding: 0;
border: 1px solid ${({ theme }) => theme.concreteGray};
background-color: ${({ theme }) => theme.inputBackground};
${({ theme }) => theme.mediaWidth.upToMedium`margin: 0;`};
padding: 0px;
width: 50vw;
max-width: 650px;
${({ theme }) => theme.mediaWidth.upToMedium`width: 65vw;`}
${({ theme }) => theme.mediaWidth.upToSmall`width: 80vw;`}
${({ theme }) => theme.mediaWidth.upToSmall`width: 85vw;`}
max-height: 50vh;
${({ minHeight }) =>
minHeight &&
......@@ -36,7 +39,7 @@ const StyledDialogContent = styled(FilteredDialogContent)`
${({ theme }) => theme.mediaWidth.upToSmall`max-height: 80vh;`}
display: flex;
overflow: hidden;
border-radius: 1.5rem;
border-radius: 10px;
}
`
......@@ -50,7 +53,7 @@ const HiddenCloseButton = styled.button`
export default function Modal({ isOpen, onDismiss, minHeight = false, initialFocusRef, children }) {
const transitions = useTransition(isOpen, null, {
config: { duration: 125 },
config: { duration: 150 },
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 }
......
......@@ -60,7 +60,7 @@ const Tabs = styled.div`
height: 2.5rem;
background-color: ${({ theme }) => theme.concreteGray};
border-radius: 3rem;
box-shadow: 0 0 0 1px ${({ theme }) => darken(0.05, theme.concreteGray)};
/* border: 1px solid ${({ theme }) => theme.mercuryGray}; */
margin-bottom: 1rem;
`
......@@ -73,6 +73,7 @@ const StyledNavLink = styled(NavLink).attrs({
align-items: center;
justify-content: center;
height: 2.5rem;
border: 1px solid ${({ theme }) => transparentize(1, theme.mercuryGray)};
flex: 1 0 auto;
border-radius: 3rem;
outline: none;
......@@ -80,21 +81,24 @@ const StyledNavLink = styled(NavLink).attrs({
text-decoration: none;
color: ${({ theme }) => theme.doveGray};
font-size: 1rem;
box-sizing: border-box;
&.${activeClassName} {
background-color: ${({ theme }) => theme.white};
background-color: ${({ theme }) => theme.inputBackground};
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;
color: ${({ theme }) => theme.royalBlue};
: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,
:focus {
font-weight: 500;
color: ${({ theme }) => darken(0.1, theme.royalBlue)};
}
`
......
......@@ -3,18 +3,20 @@ import styled from 'styled-components'
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 Image = styled.img`
width: ${({ size }) => size};
height: ${({ size }) => size};
background-color: white;
border-radius: 1rem;
`
const Emoji = styled.span`
width: ${({ size }) => size};
font-size: ${({ size }) => size};
height: ${({ size }) => size};
`
const StyledEthereumLogo = styled(EthereumLogo)`
......@@ -29,10 +31,10 @@ export default function TokenLogo({ address, size = '1rem', ...rest }) {
if (address === 'ETH') {
return <StyledEthereumLogo size={size} />
} else if (!error && !BAD_IMAGES[address]) {
path = `${TOKEN_ICON_API}/${address.toLowerCase()}.png`
path = TOKEN_ICON_API(address.toLowerCase())
} else {
return (
<Emoji {...rest}>
<Emoji {...rest} size={size}>
<span role="img" aria-label="Thinking">
🤔
</span>
......
import React, { useState } from 'react'
import React, { useState, useEffect, useRef } from 'react'
import ReactGA from 'react-ga'
import { useTranslation } from 'react-i18next'
import styled, { css, keyframes } from 'styled-components'
import { darken, lighten } from 'polished'
import { amountFormatter } from '../../utils'
import { isAddress, amountFormatter } from '../../utils'
import { useDebounce } from '../../hooks'
import question from '../../assets/images/question.svg'
import NewContextualInfo from '../../components/ContextualInfoNew'
const WARNING_TYPE = Object.freeze({
none: 'none',
emptyInput: 'emptyInput',
invalidEntryBound: 'invalidEntryBound',
riskyEntryHigh: 'riskyEntryHigh',
riskyEntryLow: 'riskyEntryLow'
})
const Flex = styled.div`
display: flex;
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;
width: 100%;
flex-wrap: ${({ wrap }) => wrap && 'wrap'};
flex-direction: row;
justify-content: flex-start;
align-items: center;
font-size: 0.8rem;
width: 100%;
padding: 0;
height: 24px;
margin-bottom: 14px;
padding-top: ${({ wrap }) => wrap && '0.25rem'};
`
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-top: 0.2rem;
padding: 0.2rem;
border: none;
background: none;
outline: none;
cursor: default;
border-radius: 36px;
&:hover {
cursor: pointer;
:hover,
: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)`
width: 228px;
left: -78px;
top: -124px;
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%;
background: #404040;
border-radius: 8px;
color: white;
color: ${({ theme }) => theme.textColor};
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)`
......@@ -97,57 +162,62 @@ const Option = styled(FancyButton)`
}
`}
`
const OptionLarge = styled(Option)`
width: 120px;
`
const Input = styled.input`
width: 123.27px;
background: #ffffff;
height: 2rem;
background: ${({ theme }) => theme.inputBackground};
flex-grow: 1;
font-size: 12px;
outline: none;
margin-left: 20px;
border: 1px solid #f2f2f2;
box-sizing: border-box;
border-radius: 36px;
color: #aeaeae;
&:focus {
}
text-align: left;
padding-left: 0.9rem;
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
}
cursor: inherit;
color: ${({ theme }) => theme.doveGray};
text-align: left;
${({ active }) =>
active &&
`
border: 1px solid #2f80ed;
text-align: right;
padding-right 1.5rem;
padding-left 0rem;
color : inherit;
`}
css`
color: initial;
cursor: initial;
text-align: right;
`}
${({ warning }) =>
warning === 'red' &&
`
color : #FF6871;
border: 1px solid #FF6871;
`}
${({ placeholder }) =>
placeholder !== 'Custom' &&
css`
text-align: right;
color: ${({ theme }) => theme.textColor};
`}
${({ color }) =>
color === 'red' &&
css`
color: ${({ theme }) => theme.salmonRed};
`}
`
const BottomError = styled.div`
margin-top: 1rem;
color: #aeaeae;
${({ show }) =>
show &&
css`
padding-top: 12px;
`}
color: ${({ theme }) => theme.doveGray};
${({ color }) =>
color === 'red' &&
`
color : #FF6871;
`}
css`
color: ${({ theme }) => theme.salmonRed};
`}
`
const OptionCustom = styled(FancyButton)`
......@@ -166,8 +236,18 @@ const OptionCustom = styled(FancyButton)`
}
`}
const OptionLarge = styled(Option)`
width: 120px;
${({ color }) =>
color === 'red' &&
css`
border: 1px solid ${({ theme }) => theme.salmonRed};
`}
input {
width: 100%;
height: 100%;
border: 0px;
border-radius: 2rem;
}
`
const Bold = styled.span`
......@@ -179,39 +259,33 @@ const LastSummaryText = styled.div`
`
const SlippageSelector = styled.div`
margin-top: 1rem;
`
const InputGroup = styled.div`
position: relative;
background-color: ${({ theme }) => darken(0.04, theme.concreteGray)};
padding: 1rem 1.25rem 1rem 1.25rem;
border-radius: 12px;
`
const Percent = styled.div`
right: 14px;
top: 9px;
position: absolute;
color: inherit;
font-size: 0, 8rem;
flex-grow: 0;
${({ color }) =>
${({ color, theme }) =>
(color === 'faded' &&
`
color : #AEAEAE
`) ||
css`
color: ${theme.doveGray};
`) ||
(color === 'red' &&
`
color : #FF6871
`)}
css`
color: ${theme.salmonRed};
`)};
`
const Faded = styled.span`
opacity: 0.7;
`
const ErrorEmoji = styled.span`
left: 30px;
top: 4px;
position: absolute;
const TransactionInfo = styled.div`
padding: 1.25rem 1.25rem 1rem 1.25rem;
`
const ValueWrapper = styled.span`
......@@ -219,12 +293,28 @@ const ValueWrapper = styled.span`
background-color: ${({ theme }) => darken(0.04, theme.concreteGray)};
border-radius: 12px;
font-variant: tabular-nums;
vertical
`
export default function TransactionDetails(props) {
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() {
let contextualInfo = ''
let isError = false
......@@ -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 = () => {
return (
<>
{renderTransactionDetails()}
<Break />
<SlippageSelector>
<SlippageRow>
Limit addtional price slippage
Limit additional price slippage
<QuestionWrapper
onClick={() => {
setPopup(!showPopup)
}}
onMouseEnter={() => {
setPopup(true)
}}
......@@ -298,105 +382,114 @@ export default function TransactionDetails(props) {
setPopup(false)
}}
>
<img src={questionMark} alt="question mark" />
<HelpCircleStyled src={question} alt="popup" />
</QuestionWrapper>
{showPopup ? (
<Popup>
Lowering this limit decreases your risk of frontrunning. This makes it more likely that your transaction
will fail due to normal price movements.
Lowering this limit decreases your risk of frontrunning. However, this makes it more likely that your
transaction will fail due to normal price movements.
</Popup>
) : (
''
)}
</SlippageRow>
<SlippageRow>
<SlippageRow wrap>
<Option
onClick={() => {
updateSlippage(0.1)
setWarningType('none')
setActiveIndex(1)
props.setcustomSlippageError('valid')
setplaceHolder('Custom')
setFromFixed(1, 0.1)
}}
active={activeIndex === 1 ? true : false}
active={activeIndex === 1}
>
0.1%
</Option>
<Option
onClick={() => {
updateSlippage(1)
setWarningType('none')
setActiveIndex(2)
props.setcustomSlippageError('valid')
setplaceHolder('Custom')
setFromFixed(2, 0.5)
}}
active={activeIndex === 2 ? true : false}
active={activeIndex === 2}
>
1%
0.5%
</Option>
<OptionLarge
onClick={() => {
updateSlippage(2)
setWarningType('none')
setActiveIndex(3)
props.setcustomSlippageError('valid')
setplaceHolder('Custom')
setFromFixed(3, 1)
}}
active={activeIndex === 3 ? true : false}
active={activeIndex === 3}
>
2%
<Faded>(suggested)</Faded>
1% <Faded>(suggested)</Faded>
</OptionLarge>
<InputGroup>
{warningType !== 'none' ? <ErrorEmoji>⚠️</ErrorEmoji> : ''}
<Input
placeholder={placeHolder}
value={userInput || ''}
onChange={parseInput}
onClick={e => {
setActiveIndex(4)
setplaceHolder('')
parseInput(e)
}}
active={activeIndex === 4 ? true : false}
warning={
warningType === 'emptyInput'
? ''
: warningType !== 'none' && warningType !== 'riskyEntryLow'
? 'red'
: ''
}
/>
<Percent
color={
warningType === 'emptyInput'
? 'faded'
: warningType !== 'none' && warningType !== 'riskyEntryLow'
? 'red'
: activeIndex !== 4
? 'faded'
: ''
}
>
%
</Percent>
</InputGroup>
<OptionCustom
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
tabIndex={-1}
ref={inputRef}
active={activeIndex === 4}
placeholder={
activeIndex === 4
? !!userInput
? ''
: '0'
: activeIndex !== 4 && userInput !== ''
? userInput
: 'Custom'
}
value={activeIndex === 4 ? userInput : ''}
onChange={parseInput}
color={
warningType === WARNING_TYPE.emptyInput
? ''
: warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
? 'red'
: ''
}
/>
<Percent
color={
activeIndex !== 4
? 'faded'
: warningType === WARNING_TYPE.riskyEntryHigh || warningType === WARNING_TYPE.invalidEntryBound
? 'red'
: ''
}
>
%
</Percent>
</FlexBetween>
</OptionCustom>
</SlippageRow>
<SlippageRow>
<BottomError
show={activeIndex === 4}
color={
warningType === 'emptyInput'
warningType === WARNING_TYPE.emptyInput
? ''
: warningType !== 'none' && warningType !== 'riskyEntryLow'
: warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
? 'red'
: ''
}
>
{warningType === 'emptyInput' ? 'Enter a slippage percentage.' : ''}
{warningType === 'invalidEntry' ? 'Please input a valid percentage.' : ''}
{warningType === 'invalidEntryBound' ? 'Pleae select value less than 50%' : ''}
{warningType === 'riskyEntryHigh' ? 'Your transaction may be frontrun.' : ''}
{warningType === 'riskyEntryLow' ? 'Your transaction may fail.' : ''}
{activeIndex === 4 && warningType.toString() === 'none' && 'Custom slippage value'}
{warningType === WARNING_TYPE.emptyInput && 'Enter a slippage percentage'}
{warningType === WARNING_TYPE.invalidEntryBound && 'Please select a value no greater than 50%'}
{warningType === WARNING_TYPE.riskyEntryHigh && 'Your transaction may be frontrun'}
{warningType === WARNING_TYPE.riskyEntryLow && 'Your transaction may fail'}
</BottomError>
</SlippageRow>
</SlippageSelector>
......@@ -404,52 +497,65 @@ export default function TransactionDetails(props) {
)
}
const [userInput, setUserInput] = useState()
const setFromCustom = () => {
setActiveIndex(4)
inputRef.current.focus()
// if there's a value, evaluate the bounds
checkBounds(debouncedInput)
}
const parseInput = e => {
let input = e.target.value
if (input === '') {
setUserInput(input)
// 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 checkBounds = slippageValue => {
setWarningType(WARNING_TYPE.none)
props.setcustomSlippageError('valid')
if (slippageValue === '' || slippageValue === '.') {
props.setcustomSlippageError('invalid')
return setWarningType('emptyInput')
return setWarningType(WARNING_TYPE.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
// check bounds and set errors
if (Number(slippageValue) < 0 || Number(slippageValue) > 50) {
props.setcustomSlippageError('invalid')
return setWarningType(WARNING_TYPE.invalidEntryBound)
}
if (isValid) {
checkAcceptablePercentValue(input)
} else {
setWarningType('invalidEntry')
if (Number(slippageValue) >= 0 && Number(slippageValue) < 0.1) {
props.setcustomSlippageError('valid')
setWarningType(WARNING_TYPE.riskyEntryLow)
}
if (Number(slippageValue) > 5) {
props.setcustomSlippageError('warning')
setWarningType(WARNING_TYPE.riskyEntryHigh)
}
//update the actual slippage value in parent
updateSlippage(Number(slippageValue))
}
const checkAcceptablePercentValue = input => {
setTimeout(function() {
setWarningType('none')
props.setcustomSlippageError('valid')
if (input < 0 || input > 50) {
props.setcustomSlippageError('invalid')
return setWarningType('invalidEntryBound')
}
if (input >= 0 && input < 0.1) {
props.setcustomSlippageError('valid')
setWarningType('riskyEntryLow')
}
if (input >= 5) {
props.setcustomSlippageError('warning')
setWarningType('riskyEntryHigh')
}
updateSlippage(input)
}, 300)
// 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)
}
}
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.setRawTokenSlippage(numParsed)
}
......@@ -464,34 +570,37 @@ export default function TransactionDetails(props) {
if (props.independentField === props.INPUT) {
return props.sending ? (
<div>
<TransactionInfo>
<div>
{t('youAreSelling')}{' '}
{b(
`${amountFormatter(
props.independentValueParsed,
props.independentDecimals,
Math.min(4, props.independentDecimals)
)} ${props.inputSymbol}`
)}
.
<ValueWrapper>
{b(
`${amountFormatter(
props.independentValueParsed,
props.independentDecimals,
Math.min(4, props.independentDecimals)
)} ${props.inputSymbol}`
)}
</ValueWrapper>
</div>
<LastSummaryText>
{b(props.recipientAddress)} {t('willReceive')}{' '}
{b(
`${amountFormatter(
props.dependentValueMinumum,
props.dependentDecimals,
Math.min(4, props.dependentDecimals)
)} ${props.outputSymbol}`
)}{' '}
<ValueWrapper>
{b(
`${amountFormatter(
props.dependentValueMinumum,
props.dependentDecimals,
Math.min(4, props.dependentDecimals)
)} ${props.outputSymbol}`
)}
</ValueWrapper>{' '}
</LastSummaryText>
<LastSummaryText>
{t('priceChange')} {b(`${props.percentSlippageFormatted}%`)}.
{t('priceChange')} <ValueWrapper>{b(`${props.percentSlippageFormatted}%`)}</ValueWrapper>
</LastSummaryText>
</div>
</TransactionInfo>
) : (
<div>
<TransactionInfo>
<div>
{t('youAreSelling')}{' '}
<ValueWrapper>
......@@ -503,7 +612,7 @@ export default function TransactionDetails(props) {
)} ${props.inputSymbol}`
)}
</ValueWrapper>{' '}
{t('forAtLeast')}{' '}
{t('forAtLeast')}
<ValueWrapper>
{b(
`${amountFormatter(
......@@ -513,45 +622,17 @@ export default function TransactionDetails(props) {
)} ${props.outputSymbol}`
)}
</ValueWrapper>
.
</div>
<LastSummaryText>
{t('priceChange')} <ValueWrapper>{b(`${props.percentSlippageFormatted}%`)}</ValueWrapper>
</LastSummaryText>
</div>
</TransactionInfo>
)
} else {
return props.sending ? (
<div>
<TransactionInfo>
<div>
{t('youAreSending')}{' '}
{b(
`${amountFormatter(
props.independentValueParsed,
props.independentDecimals,
Math.min(4, props.independentDecimals)
)} ${props.outputSymbol}`
)}{' '}
{t('to')} {b(props.recipientAddress)}.
</div>
<LastSummaryText>
{t('itWillCost')}{' '}
{b(
`${amountFormatter(
props.dependentValueMaximum,
props.dependentDecimals,
Math.min(4, props.dependentDecimals)
)} ${props.inputSymbol}`
)}{' '}
</LastSummaryText>
<LastSummaryText>
{t('priceChange')} {b(`${props.percentSlippageFormatted}%`)}.
</LastSummaryText>
</div>
) : (
<div>
<div>
{t('youAreBuying')}{' '}
<ValueWrapper>
{b(
`${amountFormatter(
......@@ -560,23 +641,48 @@ export default function TransactionDetails(props) {
Math.min(4, props.independentDecimals)
)} ${props.outputSymbol}`
)}
</ValueWrapper>
.
</ValueWrapper>{' '}
{t('to')} {b(props.recipientAddress)} {t('forAtMost')}{' '}
<ValueWrapper>
{b(
`${amountFormatter(
props.independentValueParsed,
props.independentDecimals,
Math.min(4, props.independentDecimals)
)} ${props.outputSymbol}`
)}
</ValueWrapper>{' '}
</div>
<LastSummaryText>
{t('itWillCost')}{' '}
{t('priceChange')} <ValueWrapper>{b(`${props.percentSlippageFormatted}%`)}</ValueWrapper>
</LastSummaryText>
</TransactionInfo>
) : (
<TransactionInfo>
{t('youAreBuying')}{' '}
<ValueWrapper>
{b(
`${amountFormatter(
props.independentValueParsed,
props.independentDecimals,
Math.min(4, props.independentDecimals)
)} ${props.outputSymbol}`
)}
</ValueWrapper>{' '}
{t('forAtMost')}{' '}
<ValueWrapper>
{b(
`${amountFormatter(
props.dependentValueMaximum,
props.dependentDecimals,
Math.min(4, props.dependentDecimals)
)} ${props.inputSymbol}`
)}{' '}
</LastSummaryText>
)}
</ValueWrapper>{' '}
<LastSummaryText>
{t('priceChange')} <ValueWrapper>{b(`${props.percentSlippageFormatted}%`)}</ValueWrapper>
</LastSummaryText>
</div>
</TransactionInfo>
)
}
}
......
......@@ -32,8 +32,8 @@ const Web3StatusGeneric = styled.button`
`
const Web3StatusError = styled(Web3StatusGeneric)`
background-color: ${({ theme }) => theme.salmonRed};
color: ${({ theme }) => theme.white};
border: 1px solid ${({ theme }) => theme.salmonRed};
color: ${({ theme }) => theme.white};
font-weight: 500;
:hover,
:focus {
......@@ -43,9 +43,10 @@ const Web3StatusError = styled(Web3StatusGeneric)`
const Web3StatusConnect = styled(Web3StatusGeneric)`
background-color: ${({ theme }) => theme.royalBlue};
color: ${({ theme }) => theme.white};
border: 1px solid ${({ theme }) => theme.royalBlue};
color: ${({ theme }) => theme.white};
font-weight: 500;
:hover,
:focus {
background-color: ${({ theme }) => darken(0.1, theme.royalBlue)};
......@@ -53,14 +54,18 @@ const Web3StatusConnect = styled(Web3StatusGeneric)`
`
const Web3StatusConnected = styled(Web3StatusGeneric)`
background-color: ${({ pending, theme }) => (pending ? theme.zumthorBlue : theme.white)};
color: ${({ pending, theme }) => (pending ? theme.royalBlue : theme.doveGray)};
background-color: ${({ pending, theme }) => (pending ? theme.zumthorBlue : theme.inputBackground)};
border: 1px solid ${({ pending, theme }) => (pending ? theme.royalBlue : theme.mercuryGray)};
color: ${({ pending, theme }) => (pending ? theme.royalBlue : theme.doveGray)};
font-weight: 400;
:hover {
> P {
color: ${({ theme }) => theme.uniswapPink};
}
background-color: ${({ pending, theme }) =>
pending ? transparentize(0.9, theme.royalBlue) : transparentize(0.9, theme.mercuryGray)};
}
:focus {
border: 1px solid
${({ pending, theme }) => (pending ? darken(0.1, theme.royalBlue) : darken(0.1, theme.mercuryGray))};
......@@ -72,7 +77,6 @@ const Text = styled.p`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 0 0.5rem 0 0.25rem;
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 { useWeb3Context } from 'web3-react'
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_USD_PRICE = 'UPDATE_USD_PRICE'
const ApplicationContext = createContext()
......@@ -18,12 +22,22 @@ function reducer(state, { type, payload }) {
const { networkId, blockNumber } = payload
return {
...state,
[BLOCK_NUMBERS]: {
...(safeAccess(state, [BLOCK_NUMBERS]) || {}),
[BLOCK_NUMBER]: {
...(safeAccess(state, [BLOCK_NUMBER]) || {}),
[networkId]: blockNumber
}
}
}
case UPDATE_USD_PRICE: {
const { networkId, USDPrice } = payload
return {
...state,
[USD_PRICE]: {
...(safeAccess(state, [USD_PRICE]) || {}),
[networkId]: USDPrice
}
}
}
default: {
throw Error(`Unexpected action type in ApplicationContext reducer: '${type}'.`)
}
......@@ -32,15 +46,22 @@ function reducer(state, { type, payload }) {
export default function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, {
[BLOCK_NUMBERS]: {}
[BLOCK_NUMBER]: {},
[USD_PRICE]: {}
})
const updateBlockNumber = useCallback((networkId, blockNumber) => {
dispatch({ type: UPDATE_BLOCK_NUMBER, payload: { networkId, blockNumber } })
}, [])
const updateUSDPrice = useCallback((networkId, USDPrice) => {
dispatch({ type: UPDATE_USD_PRICE, payload: { networkId, USDPrice } })
}, [])
return (
<ApplicationContext.Provider value={useMemo(() => [state, { updateBlockNumber }], [state, updateBlockNumber])}>
<ApplicationContext.Provider
value={useMemo(() => [state, { updateBlockNumber, updateUSDPrice }], [state, updateBlockNumber, updateUSDPrice])}
>
{children}
</ApplicationContext.Provider>
)
......@@ -49,7 +70,24 @@ export default function Provider({ children }) {
export function Updater() {
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(() => {
if ((networkId || networkId === 0) && library) {
......@@ -88,5 +126,13 @@ export function useBlockNumber() {
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) {
update(networkId, address, tokenAddress, null, globalBlockNumber)
}
})
return () => {
stale = true
}
......
......@@ -7,7 +7,9 @@ const CURRENT_VERSION = 0
const LAST_SAVED = 'LAST_SAVED'
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'
......@@ -39,7 +41,8 @@ function reducer(state, { type, payload }) {
function init() {
const defaultLocalStorage = {
[VERSION]: CURRENT_VERSION,
[BETA_MESSAGE_DISMISSED]: false
[BETA_MESSAGE_DISMISSED]: false,
[DARK_MODE]: false
}
try {
......@@ -48,7 +51,7 @@ function init() {
// this is where we could run migration logic
return defaultLocalStorage
} else {
return parsed
return { ...defaultLocalStorage, ...parsed }
}
} catch {
return defaultLocalStorage
......@@ -88,3 +91,15 @@ export function useBetaMessageManager() {
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 = {
[DECIMALS]: 9,
[EXCHANGE_ADDRESS]: '0xb92dE8B30584392Af27726D5ce04Ef3c4e5c9924'
},
'0xc719d010B63E5bbF2C0551872CD5316ED26AcD83': {
[NAME]: 'Decentralized Insurance Protocol',
[SYMBOL]: 'DIP',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0x61792F290e5100FBBcBb2309F03A1Bab869fb850'
},
'0x4946Fcea7C692606e8908002e55A582af44AC121': {
[NAME]: 'FOAM Token',
[SYMBOL]: 'FOAM',
......@@ -247,6 +245,12 @@ const INITIAL_TOKENS_CONTEXT = {
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0x755899F0540c3548b99E68C59AdB0f15d2695188'
},
'0xF970b8E36e23F7fC3FD752EeA86f8Be8D83375A6': {
[NAME]: 'Ripio Credit Network Token',
[SYMBOL]: 'RCN',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0xD91FF16Ef92568fC27F466C3c5613e43313Ab1dc'
},
'0x255Aa6DF07540Cb5d3d297f0D0D4D84cb52bc8e6': {
[NAME]: 'Raiden Token',
[SYMBOL]: 'RDN',
......@@ -301,11 +305,11 @@ const INITIAL_TOKENS_CONTEXT = {
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0x1aEC8F11A7E78dC22477e91Ed924Fab46e3A88Fd'
},
'0x2Dea20405c52Fb477ecCa8Fe622661d316Ac5400': {
'0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F': {
[NAME]: 'Synthetix Network Token',
[SYMBOL]: 'SNX',
[DECIMALS]: 18,
[EXCHANGE_ADDRESS]: '0x9fAA0Cb10912DE7Ad1D86705C65de291a9088A61'
[EXCHANGE_ADDRESS]: '0x3958B4eC427F8fa24eB60F42821760e88d485f7F'
},
'0x42d6622deCe394b54999Fbd73D108123806f6a18': {
[NAME]: 'SPANK',
......@@ -462,7 +466,6 @@ export function useTokenDetails(tokenAddress) {
}
}
)
return () => {
stale = true
}
......
......@@ -58,9 +58,8 @@ export function useENSName(address) {
useEffect(() => {
if (isAddress(address)) {
let stale = false
library
.lookupAddress(address)
.then(name => {
try {
library.lookupAddress(address).then(name => {
if (!stale) {
if (name) {
setENSNname(name)
......@@ -69,11 +68,9 @@ export function useENSName(address) {
}
}
})
.catch(() => {
if (!stale) {
setENSNname(null)
}
})
} catch {
setENSNname(null)
}
return () => {
stale = true
......
......@@ -10,6 +10,7 @@ import TransactionContextProvider, { Updater as TransactionContextUpdater } from
import TokensContextProvider from './contexts/Tokens'
import BalancesContextProvider from './contexts/Balances'
import AllowancesContextProvider from './contexts/Allowances'
import AllBalancesContextProvider from './contexts/AllBalances'
import App from './pages/App'
import InjectedConnector from './InjectedConnector'
......@@ -35,7 +36,9 @@ function ContextProviders({ children }) {
<TransactionContextProvider>
<TokensContextProvider>
<BalancesContextProvider>
<AllowancesContextProvider>{children}</AllowancesContextProvider>
<AllBalancesContextProvider>
<AllowancesContextProvider>{children}</AllowancesContextProvider>
</AllBalancesContextProvider>
</BalancesContextProvider>
</TokensContextProvider>
</TransactionContextProvider>
......@@ -55,16 +58,16 @@ function Updaters() {
}
ReactDOM.render(
<ThemeProvider>
<>
<GlobalStyle />
<Web3Provider connectors={connectors} libraryName="ethers.js">
<ContextProviders>
<Updaters />
<Web3Provider connectors={connectors} libraryName="ethers.js">
<ContextProviders>
<Updaters />
<ThemeProvider>
<>
<GlobalStyle />
<App />
</ContextProviders>
</Web3Provider>
</>
</ThemeProvider>,
</>
</ThemeProvider>
</ContextProviders>
</Web3Provider>,
document.getElementById('root')
)
......@@ -4,6 +4,8 @@ import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom'
import Web3ReactManager from '../components/Web3ReactManager'
import Header from '../components/Header'
import Footer from '../components/Footer'
import NavigationTabs from '../components/NavigationTabs'
import { isAddress } from '../utils'
......@@ -11,24 +13,38 @@ const Swap = lazy(() => import('./Swap'))
const Send = lazy(() => import('./Send'))
const Pool = lazy(() => import('./Pool'))
const AppWrapper = styled.div`
display: flex;
flex-flow: column;
align-items: flex-start;
height: 100vh;
`
const HeaderWrapper = styled.div`
${({ theme }) => theme.flexRowNoWrap}
width: 100%;
justify-content: space-between;
`
const FooterWrapper = styled.div`
width: 100%;
min-height: 30px;
align-self: flex-end;
`
const BodyWrapper = styled.div`
${({ theme }) => theme.flexRowNoWrap}
display: flex;
flex-direction: column;
width: 100%;
justify-content: center;
flex-grow: 1;
flex-basis: 0;
justify-content: flex-start;
align-items: center;
flex: 1;
overflow: auto;
`
const Body = styled.div`
width: 35rem;
margin: 1.25rem;
max-width: 35rem;
width: 90%;
/* margin: 0 1.25rem 1.25rem 1.25rem; */
`
export default function App() {
......
......@@ -9,12 +9,13 @@ import { Button } from '../../theme'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import OversizedPanel from '../../components/OversizedPanel'
import ContextualInfo from '../../components/ContextualInfo'
import PlusBlue from '../../assets/images/plus-blue.svg'
import PlusGrey from '../../assets/images/plus-grey.svg'
import { ReactComponent as Plus } from '../../assets/images/plus-blue.svg'
import { useExchangeContract } from '../../hooks'
import { amountFormatter, calculateGasMargin } from '../../utils'
import { useTransactionAdder } from '../../contexts/Transactions'
import { useTokenDetails } from '../../contexts/Tokens'
import { useFetchAllBalances } from '../../contexts/AllBalances'
import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances'
import { useAddressAllowance } from '../../contexts/Allowances'
......@@ -63,14 +64,6 @@ const DownArrowBackground = styled.div`
justify-content: center;
align-items: center;
`
const DownArrow = styled.img`
width: 0.625rem;
height: 0.625rem;
position: relative;
padding: 0.875rem;
`
const SummaryPanel = styled.div`
${({ theme }) => theme.flexColumnNoWrap}
padding: 1rem 0;
......@@ -87,7 +80,7 @@ const ExchangeRateWrapper = styled.div`
const ExchangeRate = styled.span`
flex: 1 1 auto;
width: 0;
color: ${({ theme }) => theme.chaliceGray};
color: ${({ theme }) => theme.doveGray};
`
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) {
if (value) {
const offset = value.mul(ALLOWED_SLIPPAGE).div(ethers.utils.bigNumberify(10000))
......@@ -531,6 +535,8 @@ export default function AddLiquidity() {
const isActive = active && account
const isValid = (inputError === null || outputError === null) && !showUnlock
const allBalances = useFetchAllBalances()
return (
<>
{isNewExchange ? (
......@@ -547,6 +553,7 @@ export default function AddLiquidity() {
<CurrencyInputPanel
title={t('deposit')}
allBalances={allBalances}
extraText={inputBalance && formatBalance(amountFormatter(inputBalance, 18, 4))}
onValueChange={inputValue => {
dispatchAddLiquidityState({ type: 'UPDATE_VALUE', payload: { value: inputValue, field: INPUT } })
......@@ -558,11 +565,12 @@ export default function AddLiquidity() {
/>
<OversizedPanel>
<DownArrowBackground>
<DownArrow src={isActive ? PlusBlue : PlusGrey} alt="plus" />
<ColoredWrappedPlus active={isActive} alt="plus" />
</DownArrowBackground>
</OversizedPanel>
<CurrencyInputPanel
title={t('deposit')}
allBalances={allBalances}
description={isNewExchange ? '' : outputValue ? `(${t('estimated')})` : ''}
extraText={outputBalance && formatBalance(amountFormatter(outputBalance, decimals, Math.min(decimals, 4)))}
selectedTokenAddress={outputCurrency}
......
......@@ -29,7 +29,7 @@ const ExchangeRateWrapper = styled.div`
const ExchangeRate = styled.span`
flex: 1 1 auto;
width: 0;
color: ${({ theme }) => theme.chaliceGray};
color: ${({ theme }) => theme.doveGray};
`
const CreateExchangeWrapper = styled.div`
......
......@@ -4,10 +4,13 @@ import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
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 { useBodyKeyDown } from '../../hooks'
import { lighten } from 'polished'
const poolTabOrder = [
{
path: '/add-liquidity',
......@@ -29,13 +32,16 @@ const poolTabOrder = [
const LiquidityContainer = styled.div`
${({ theme }) => theme.flexRowNoWrap};
align-items: center;
font-size: 0.75rem;
padding: 0.625rem 1rem;
font-size: 0.75rem;
padding: 1rem 1rem;
font-size: 1rem;
color: ${({ theme }) => theme.royalBlue};
font-weight: 500;
cursor: pointer;
:hover {
color: ${({ theme }) => lighten(0.1, theme.royalBlue)};
}
img {
height: 0.75rem;
width: 0.75rem;
......@@ -62,21 +68,28 @@ const StyledNavLink = styled(NavLink).attrs({
font-size: 1rem;
&.${activeClassName} {
background-color: ${({ theme }) => theme.white};
background-color: ${({ theme }) => theme.inputBackground};
border-radius: 3rem;
box-shadow: 0 0 1px 1px ${({ theme }) => theme.mercuryGray};
border: 1px solid ${({ theme }) => theme.mercuryGray};
font-weight: 500;
color: ${({ theme }) => theme.royalBlue};
}
`
const PoolModal = styled.div`
background-color: ${({ theme }) => theme.white};
background-color: ${({ theme }) => theme.inputBackground};
width: 100%;
height: 100%;
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 }) {
const { t } = useTranslation()
......@@ -109,7 +122,7 @@ function ModeSelector({ location: { pathname }, history }) {
}}
>
<LiquidityLabel>{t(activeTabKey)}</LiquidityLabel>
<img src={Dropdown} alt="dropdown" />
<ColoredDropdown alt="arrow down" />
</LiquidityContainer>
<Modal
isOpen={modalIsOpen}
......
......@@ -9,12 +9,13 @@ import { Button } from '../../theme'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import ContextualInfo from '../../components/ContextualInfo'
import OversizedPanel from '../../components/OversizedPanel'
import ArrowDownBlue from '../../assets/images/arrow-down-blue.svg'
import ArrowDownGrey from '../../assets/images/arrow-down-grey.svg'
import ArrowDown from '../../assets/svg/SVGArrowDown'
import { useExchangeContract } from '../../hooks'
import { useTransactionAdder } from '../../contexts/Transactions'
import { useTokenDetails } from '../../contexts/Tokens'
import { useAddressBalance } from '../../contexts/Balances'
import { useFetchAllBalances } from '../../contexts/AllBalances'
import { calculateGasMargin, amountFormatter } from '../../utils'
// denominated in bips
......@@ -36,7 +37,9 @@ const DownArrowBackground = styled.div`
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;
height: 0.625rem;
position: relative;
......@@ -80,7 +83,7 @@ const ExchangeRateWrapper = styled.div`
const ExchangeRate = styled.span`
flex: 1 1 auto;
width: 0;
color: ${({ theme }) => theme.chaliceGray};
color: ${({ theme }) => theme.doveGray};
`
const Flex = styled.div`
......@@ -326,10 +329,13 @@ export default function RemoveLiquidity() {
const marketRate = getMarketRate(exchangeETHBalance, exchangeTokenBalance, decimals)
const allBalances = useFetchAllBalances()
return (
<>
<CurrencyInputPanel
title={t('poolTokens')}
allBalances={allBalances}
extraText={poolTokenBalance && formatBalance(amountFormatter(poolTokenBalance, 18, 4))}
extraTextClickHander={() => {
if (poolTokenBalance) {
......@@ -347,11 +353,12 @@ export default function RemoveLiquidity() {
/>
<OversizedPanel>
<DownArrowBackground>
<DownArrow src={isActive ? ArrowDownBlue : ArrowDownGrey} alt="arrow" />
<DownArrow active={isActive} alt="arrow" />
</DownArrowBackground>
</OversizedPanel>
<CurrencyInputPanel
title={t('output')}
allBalances={allBalances}
description={!!(ethWithdrawn && tokenWithdrawn) ? `(${t('estimated')})` : ''}
key="remove-liquidity-input"
renderInput={() =>
......
......@@ -12,8 +12,8 @@ export const Button = styled.button.attrs(({ warning, theme }) => ({
border: none;
outline: none;
background-color: ${({ backgroundColor }) => backgroundColor};
transition: background-color 150ms ease-out;
color: ${({ theme }) => theme.white};
transition: background-color 125ms ease-in-out;
width: 100%;
:hover,
......@@ -26,7 +26,8 @@ export const Button = styled.button.attrs(({ warning, theme }) => ({
}
:disabled {
background-color: ${({ theme }) => theme.mercuryGray};
background-color: ${({ theme }) => theme.concreteGray};
color: ${({ theme }) => theme.silverGray};
cursor: auto;
}
`
......@@ -50,12 +51,13 @@ export const Link = styled.a.attrs({
`
export const BorderlessInput = styled.input`
color: ${({ theme }) => theme.mineshaftGray};
color: ${({ theme }) => theme.textColor};
font-size: 1rem;
outline: none;
border: none;
flex: 1 1 auto;
width: 0;
background-color: ${({ theme }) => theme.inputBackground};
[type='number'] {
-moz-appearance: textfield;
......@@ -67,7 +69,7 @@ export const BorderlessInput = styled.input`
}
::placeholder {
color: ${({ theme }) => theme.mercuryGray};
color: ${({ theme }) => theme.chaliceGray};
}
`
......
import React from 'react'
import { ThemeProvider as StyledComponentsThemeProvider, createGlobalStyle, css } from 'styled-components'
import { useDarkModeManager } from '../contexts/LocalStorage'
export * from './components'
......@@ -28,23 +29,36 @@ const flexRowNoWrap = css`
flex-flow: row nowrap;
`
const theme = {
white: '#FFFFFF',
black: '#000000',
const white = '#FFFFFF'
const 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
concreteGray: '#FAFAFA',
mercuryGray: '#E1E1E1',
silverGray: '#C4C4C4',
chaliceGray: '#AEAEAE',
doveGray: '#737373',
mineshaftGray: '#2B2B2B',
buttonOutlineGrey: '#f2f2f2',
concreteGray: darkMode ? '#292C2F' : '#FAFAFA',
mercuryGray: darkMode ? '#333333' : '#E1E1E1',
silverGray: darkMode ? '#737373' : '#C4C4C4',
chaliceGray: darkMode ? '#7B7B7B' : '#AEAEAE',
doveGray: darkMode ? '#C4C4C4' : '#737373',
mineshaftGray: darkMode ? '#E1E1E1' : '#2B2B2B',
buttonOutlineGrey: darkMode ? '#FAFAFA' : '#F2F2F2',
tokenRowHover: darkMode ? '#404040' : '#F2F2F2',
//blacks
charcoalBlack: '#404040',
charcoalBlack: darkMode ? '#F2F2F2' : '#404040',
// blues
zumthorBlue: '#EBF4FF',
malibuBlue: '#5CA2FF',
royalBlue: '#2F80ED',
zumthorBlue: darkMode ? '#212529' : '#EBF4FF',
malibuBlue: darkMode ? '#E67AEF' : '#5CA2FF',
royalBlue: darkMode ? '#DC6BE5' : '#2F80ED',
loadingBlue: darkMode ? '#e4f0ff' : '#e4f0ff',
// purples
wisteriaPurple: '#DC6BE5',
// reds
......@@ -57,15 +71,20 @@ const theme = {
uniswapPink: '#DC6BE5',
connectedGreen: '#27AE60',
//specific
textHover: darkMode ? theme.uniswapPink : theme.doveGray,
// media queries
mediaWidth: mediaWidthTemplates,
// css snippets
flexColumnNoWrap,
flexRowNoWrap
}
})
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`
......@@ -79,20 +98,25 @@ export const GlobalStyle = createGlobalStyle`
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
body > div {
height: 100%;
overflow: scroll;
-webkit-overflow-scrolling: touch;
}
html {
font-size: 16px;
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;
-moz-osx-font-smoothing: grayscale;
-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'
import ERC20_ABI from '../constants/abis/erc20'
import ERC20_BYTES32_ABI from '../constants/abis/erc20_bytes32'
import { FACTORY_ADDRESSES } from '../constants'
import { formatFixed } from '@uniswap/sdk'
import UncheckedJsonRpcSigner from './signer'
......@@ -178,10 +179,27 @@ export async function getEtherBalance(address, library) {
if (!isAddress(address)) {
throw Error(`Invalid 'address' parameter '${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
export async function getTokenBalance(tokenAddress, address, library) {
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