Commit b28ad286 authored by Noah Zinsmeister's avatar Noah Zinsmeister Committed by GitHub

add v1 slippage checks (only for mainnet) (#746)

* add v1 slippage checks (only for mainnet)

* Add v1 trade notification UI

* Design tweaks

* add minimum delta

* remove dark theme toggle from menu

* only render view on etherscan if an address exists

* fix weird spacing on logged-in header

* lint error

* remove mainnet
Co-authored-by: default avatarCallil Capuozzo <callil.capuozzo@gmail.com>
parent 0d410893
...@@ -345,7 +345,7 @@ export default function AccountDetails({ ...@@ -345,7 +345,7 @@ export default function AccountDetails({
<> <>
<AccountControl hasENS={!!ENSName} isENS={false}> <AccountControl hasENS={!!ENSName} isENS={false}>
<StyledLink hasENS={!!ENSName} isENS={true} href={getEtherscanLink(chainId, ENSName, 'address')}> <StyledLink hasENS={!!ENSName} isENS={true} href={getEtherscanLink(chainId, ENSName, 'address')}>
View on Etherscan ↗{' '} View on Etherscan ↗
</StyledLink> </StyledLink>
</AccountControl> </AccountControl>
</> </>
...@@ -353,7 +353,7 @@ export default function AccountDetails({ ...@@ -353,7 +353,7 @@ export default function AccountDetails({
<> <>
<AccountControl hasENS={!!ENSName} isENS={false}> <AccountControl hasENS={!!ENSName} isENS={false}>
<StyledLink hasENS={!!ENSName} isENS={false} href={getEtherscanLink(chainId, account, 'address')}> <StyledLink hasENS={!!ENSName} isENS={false} href={getEtherscanLink(chainId, account, 'address')}>
View on Etherscan ↗{' '} View on Etherscan ↗
</StyledLink> </StyledLink>
</AccountControl> </AccountControl>
</> </>
......
...@@ -190,15 +190,14 @@ export default function AddressInputPanel({ ...@@ -190,15 +190,14 @@ export default function AddressInputPanel({
<TYPE.black color={theme.text2} fontWeight={500} fontSize={14}> <TYPE.black color={theme.text2} fontWeight={500} fontSize={14}>
Recipient Recipient
</TYPE.black> </TYPE.black>
{data.name || {(data.name || data.address) && (
(data.address && (
<Link <Link
href={getEtherscanLink(chainId, data.name || data.address, 'address')} href={getEtherscanLink(chainId, data.name || data.address, 'address')}
style={{ fontSize: '14px' }} style={{ fontSize: '14px' }}
> >
(View on Etherscan) (View on Etherscan)
</Link> </Link>
))} )}
</RowBetween> </RowBetween>
<Input <Input
type="text" type="text"
......
...@@ -55,6 +55,7 @@ import { ...@@ -55,6 +55,7 @@ import {
TruncatedText, TruncatedText,
Wrapper Wrapper
} from './styleds' } from './styleds'
import { useV1TradeLinkIfBetter } from '../../data/V1'
enum SwapType { enum SwapType {
EXACT_TOKENS_FOR_TOKENS, EXACT_TOKENS_FOR_TOKENS,
...@@ -184,6 +185,10 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro ...@@ -184,6 +185,10 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
) )
const trade = tradeType === TradeType.EXACT_INPUT ? bestTradeExactIn : bestTradeExactOut const trade = tradeType === TradeType.EXACT_INPUT ? bestTradeExactIn : bestTradeExactOut
// return link to the appropriate v1 pair if the slippage on v1 is lower
const v1TradeLinkIfBetter = useV1TradeLinkIfBetter(trade, new Percent('50', '10000'))
const route = trade?.route const route = trade?.route
const userHasSpecifiedInputOutput = const userHasSpecifiedInputOutput =
!!tokens[Field.INPUT] && !!tokens[Field.INPUT] &&
...@@ -814,6 +819,7 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro ...@@ -814,6 +819,7 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
</Text> </Text>
</RowBetween> </RowBetween>
)} )}
<RowBetween> <RowBetween>
<RowFixed> <RowFixed>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}> <TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
...@@ -1140,6 +1146,7 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro ...@@ -1140,6 +1146,7 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
</StyledBalanceMaxMini> </StyledBalanceMaxMini>
</Text> </Text>
</RowBetween> </RowBetween>
{trade && (warningHigh || warningMedium) && ( {trade && (warningHigh || warningMedium) && (
<RowBetween> <RowBetween>
<TYPE.main <TYPE.main
...@@ -1215,6 +1222,18 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro ...@@ -1215,6 +1222,18 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
</Text> </Text>
</ButtonError> </ButtonError>
)} )}
{v1TradeLinkIfBetter && (
<YellowCard style={{ marginTop: '12px', padding: '8px 4px' }}>
<AutoColumn gap="sm" justify="center" style={{ alignItems: 'center', textAlign: 'center' }}>
<Text lineHeight="145.23%;" fontSize={14} fontWeight={400} color={theme.text1}>
There is a better price for this trade on
<Link href={v1TradeLinkIfBetter}>
<b> Uniswap V1 ↗</b>
</Link>
</Text>
</AutoColumn>
</YellowCard>
)}
</BottomGrouping> </BottomGrouping>
{tokens[Field.INPUT] && tokens[Field.OUTPUT] && !noRoute && ( {tokens[Field.INPUT] && tokens[Field.OUTPUT] && !noRoute && (
<AdvancedDropwdown> <AdvancedDropwdown>
...@@ -1347,7 +1366,7 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro ...@@ -1347,7 +1366,7 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
</AutoColumn> </AutoColumn>
</FixedBottom> </FixedBottom>
</AdvancedDropwdown> </AdvancedDropwdown>
)}{' '} )}
</Wrapper> </Wrapper>
) )
} }
......
...@@ -9,7 +9,7 @@ import Web3Status from '../Web3Status' ...@@ -9,7 +9,7 @@ import Web3Status from '../Web3Status'
import { Link } from '../../theme' import { Link } from '../../theme'
import { Text } from 'rebass' import { Text } from 'rebass'
import { WETH } from '@uniswap/sdk' import { WETH, ChainId } from '@uniswap/sdk'
import { isMobile } from 'react-device-detect' import { isMobile } from 'react-device-detect'
import { YellowCard } from '../Card' import { YellowCard } from '../Card'
import { useWeb3React } from '../../hooks' import { useWeb3React } from '../../hooks'
...@@ -41,8 +41,6 @@ const HeaderFrame = styled.div` ...@@ -41,8 +41,6 @@ const HeaderFrame = styled.div`
` `
const HeaderElement = styled.div` const HeaderElement = styled.div`
display: flex;
min-width: 0;
display: flex; display: flex;
align-items: center; align-items: center;
` `
...@@ -65,13 +63,11 @@ const TitleText = styled(Row)` ...@@ -65,13 +63,11 @@ const TitleText = styled(Row)`
` `
const AccountElement = styled.div<{ active: boolean }>` const AccountElement = styled.div<{ active: boolean }>`
display: flex;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
background-color: ${({ theme, active }) => (!active ? theme.bg1 : theme.bg3)}; background-color: ${({ theme, active }) => (!active ? theme.bg1 : theme.bg3)};
border-radius: 12px; border-radius: 12px;
padding-left: ${({ active }) => (active ? '8px' : 0)};
white-space: nowrap; white-space: nowrap;
:focus { :focus {
...@@ -197,22 +193,19 @@ export default function Header() { ...@@ -197,22 +193,19 @@ export default function Header() {
</HeaderElement> </HeaderElement>
<HeaderElement> <HeaderElement>
<TestnetWrapper> <TestnetWrapper>
{!isMobile && chainId === 4 && <NetworkCard>Rinkeby</NetworkCard>} {!isMobile && chainId === ChainId.ROPSTEN && <NetworkCard>Ropsten</NetworkCard>}
{!isMobile && chainId === 3 && <NetworkCard>Ropsten</NetworkCard>} {!isMobile && chainId === ChainId.RINKEBY && <NetworkCard>Rinkeby</NetworkCard>}
{!isMobile && chainId === 5 && <NetworkCard>Goerli</NetworkCard>} {!isMobile && chainId === ChainId.GÖRLI && <NetworkCard>rli</NetworkCard>}
{!isMobile && chainId === 42 && <NetworkCard>Kovan</NetworkCard>} {!isMobile && chainId === ChainId.KOVAN && <NetworkCard>Kovan</NetworkCard>}
</TestnetWrapper> </TestnetWrapper>
<AccountElement active={!!account}> <AccountElement active={!!account}>
{account ? ( {account && userEthBalance ? (
<Row style={{ marginRight: '-1.25rem' }}> <Text style={{ flexShrink: 0 }} px="0.5rem" fontWeight={500}>
<Text fontWeight={500}> {userEthBalance && `${userEthBalance?.toSignificant(4)} ETH`}</Text> {userEthBalance?.toSignificant(4)} ETH
</Row> </Text>
) : ( ) : null}
''
)}
<Web3Status /> <Web3Status />
</AccountElement> </AccountElement>
<Menu /> <Menu />
</HeaderElement> </HeaderElement>
</RowBetween> </RowBetween>
......
import React, { useRef, useEffect } from 'react' import React, { useRef, useEffect } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { useDarkModeManager } from '../../contexts/LocalStorage'
import { ReactComponent as MenuIcon } from '../../assets/images/menu.svg' import { ReactComponent as MenuIcon } from '../../assets/images/menu.svg'
...@@ -83,8 +82,6 @@ export default function Menu() { ...@@ -83,8 +82,6 @@ export default function Menu() {
const node = useRef<HTMLDivElement>() const node = useRef<HTMLDivElement>()
const [open, toggle] = useToggle(false) const [open, toggle] = useToggle(false)
const [darkMode, toggleDarkMode] = useDarkModeManager()
useEffect(() => { useEffect(() => {
const handleClickOutside = e => { const handleClickOutside = e => {
if (node.current?.contains(e.target) ?? false) { if (node.current?.contains(e.target) ?? false) {
...@@ -123,7 +120,6 @@ export default function Menu() { ...@@ -123,7 +120,6 @@ export default function Menu() {
<MenuItem id="link" href="https://uniswap.info/"> <MenuItem id="link" href="https://uniswap.info/">
Analytics Analytics
</MenuItem> </MenuItem>
<MenuItem onClick={toggleDarkMode}>{darkMode ? 'Light theme' : 'Dark theme'}</MenuItem>
</MenuFlyout> </MenuFlyout>
) : ( ) : (
'' ''
......
[
{
"name": "NewExchange",
"inputs": [
{ "type": "address", "name": "token", "indexed": true },
{ "type": "address", "name": "exchange", "indexed": true }
],
"anonymous": false,
"type": "event"
},
{
"name": "initializeFactory",
"outputs": [],
"inputs": [{ "type": "address", "name": "template" }],
"constant": false,
"payable": false,
"type": "function",
"gas": 35725
},
{
"name": "createExchange",
"outputs": [{ "type": "address", "name": "out" }],
"inputs": [{ "type": "address", "name": "token" }],
"constant": false,
"payable": false,
"type": "function",
"gas": 187911
},
{
"name": "getExchange",
"outputs": [{ "type": "address", "name": "out" }],
"inputs": [{ "type": "address", "name": "token" }],
"constant": true,
"payable": false,
"type": "function",
"gas": 715
},
{
"name": "getToken",
"outputs": [{ "type": "address", "name": "out" }],
"inputs": [{ "type": "address", "name": "exchange" }],
"constant": true,
"payable": false,
"type": "function",
"gas": 745
},
{
"name": "getTokenWithId",
"outputs": [{ "type": "address", "name": "out" }],
"inputs": [{ "type": "uint256", "name": "token_id" }],
"constant": true,
"payable": false,
"type": "function",
"gas": 736
},
{
"name": "exchangeTemplate",
"outputs": [{ "type": "address", "name": "out" }],
"inputs": [],
"constant": true,
"payable": false,
"type": "function",
"gas": 633
},
{
"name": "tokenCount",
"outputs": [{ "type": "uint256", "name": "out" }],
"inputs": [],
"constant": true,
"payable": false,
"type": "function",
"gas": 663
}
]
import { injected, walletconnect, walletlink, fortmatic, portis } from '../connectors' import { injected, walletconnect, walletlink, fortmatic, portis } from '../connectors'
import { ChainId, WETH, Token } from '@uniswap/sdk' import { ChainId, WETH, Token } from '@uniswap/sdk'
export const V1_FACTORY_ADDRESS = '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'
export const ROUTER_ADDRESS = '0xf164fC0Ec4E93095b804a4795bBe1e041497b92a' export const ROUTER_ADDRESS = '0xf164fC0Ec4E93095b804a4795bBe1e041497b92a'
// used for display in the default list when adding liquidity
export const COMMON_BASES = { export const COMMON_BASES = {
1: [WETH[ChainId.MAINNET]], [ChainId.MAINNET]: [
3: [WETH[ChainId.ROPSTEN]], WETH[ChainId.MAINNET],
4: [ new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'),
new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C')
],
[ChainId.ROPSTEN]: [WETH[ChainId.ROPSTEN]],
[ChainId.RINKEBY]: [
WETH[ChainId.RINKEBY], WETH[ChainId.RINKEBY],
new Token(ChainId.RINKEBY, '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735', 18, 'DAI', 'Dai Stablecoin') new Token(ChainId.RINKEBY, '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735', 18, 'DAI', 'Dai Stablecoin')
], ],
5: [WETH[ChainId.GÖRLI]], [ChainId.GÖRLI]: [WETH[ChainId.GÖRLI]],
42: [WETH[ChainId.KOVAN]] [ChainId.KOVAN]: [WETH[ChainId.KOVAN]]
} }
export const SUPPORTED_THEMES = { export const SUPPORTED_THEMES = {
......
...@@ -15,12 +15,7 @@ function getTokenAllowance( ...@@ -15,12 +15,7 @@ function getTokenAllowance(
.then((balance: { toString: () => string }) => new TokenAmount(token, balance.toString())) .then((balance: { toString: () => string }) => new TokenAmount(token, balance.toString()))
} }
export function useTokenAllowance( export function useTokenAllowance(token?: Token, owner?: string, spender?: string): TokenAmount {
token?: Token,
owner?: string,
spender?: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): TokenAmount {
const contract = useTokenContract(token?.address, false) const contract = useTokenContract(token?.address, false)
const shouldFetch = !!contract && typeof owner === 'string' && typeof spender === 'string' const shouldFetch = !!contract && typeof owner === 'string' && typeof spender === 'string'
......
import { Web3Provider } from '@ethersproject/providers'
import { Contract } from '@ethersproject/contracts'
import { Token, TokenAmount } from '@uniswap/sdk'
import useSWR from 'swr'
import { useWeb3React } from '@web3-react/core'
import { BigNumber } from '@ethersproject/bignumber'
import { SWRKeys } from '.'
import { useTokenContract } from '../hooks'
function getTokenBalance(
contract: Contract,
token: Token
): (_: SWRKeys, __: number, ___: string, owner: string) => Promise<TokenAmount> {
return async (_, __, ___, owner: string): Promise<TokenAmount> =>
contract.balanceOf(owner).then((balance: { toString: () => string }) => new TokenAmount(token, balance.toString()))
}
export function useTokenBalance(token?: Token, owner?: string): TokenAmount {
const contract = useTokenContract(token?.address, false)
const shouldFetch = !!contract && typeof owner === 'string'
const { data } = useSWR(
shouldFetch ? [SWRKeys.TokenBalance, token.chainId, token.address, owner] : null,
getTokenBalance(contract, token),
{
dedupingInterval: 10 * 1000,
refreshInterval: 20 * 1000
}
)
return data
}
function getETHBalance(library: Web3Provider): (_: SWRKeys, owner: string) => Promise<BigNumber> {
return async (_, owner: string): Promise<BigNumber> => library.getBalance(owner)
}
export function useETHBalance(owner?: string): BigNumber {
const { library } = useWeb3React()
const shouldFetch = !!library && typeof owner === 'string'
const { data } = useSWR(shouldFetch ? [SWRKeys.ETHBalance, owner] : null, getETHBalance(library), {
dedupingInterval: 10 * 1000,
refreshInterval: 20 * 1000
})
return data
}
import { Contract } from '@ethersproject/contracts'
import { Token, TokenAmount, Pair, Trade, ChainId, WETH, Route, TradeType, Percent } from '@uniswap/sdk'
import useSWR from 'swr'
import IUniswapV1Factory from '../constants/abis/v1_factory.json'
import { V1_FACTORY_ADDRESS } from '../constants'
import { useContract } from '../hooks'
import { SWRKeys } from '.'
import { useTokenBalance, useETHBalance } from './Balances'
import { AddressZero } from '@ethersproject/constants'
function getV1PairAddress(contract: Contract): (_: SWRKeys, tokenAddress: string) => Promise<string> {
return async (_: SWRKeys, tokenAddress: string): Promise<string> => contract.getExchange(tokenAddress)
}
function useV1PairAddress(tokenAddress: string) {
const contract = useContract(V1_FACTORY_ADDRESS, IUniswapV1Factory, false)
const shouldFetch = typeof tokenAddress === 'string' && !!contract
const { data } = useSWR(shouldFetch ? [SWRKeys.V1PairAddress, tokenAddress] : null, getV1PairAddress(contract), {
refreshInterval: 0 // don't need to update these
})
return data
}
const DUMMY_ETH = new Token(ChainId.MAINNET, AddressZero, 18)
function useMockV1Pair(token?: Token) {
const mainnet = token?.chainId === ChainId.MAINNET
const isWETH = token?.equals(WETH[ChainId.MAINNET])
const v1PairAddress = useV1PairAddress(mainnet && !isWETH ? token?.address : undefined)
const tokenBalance = useTokenBalance(token, v1PairAddress)
const ETHBalance = useETHBalance(v1PairAddress)
return tokenBalance && ETHBalance
? new Pair(tokenBalance, new TokenAmount(DUMMY_ETH, ETHBalance.toString()))
: undefined
}
export function useV1TradeLinkIfBetter(trade: Trade, minimumDelta: Percent = new Percent('0')): string {
const inputPair = useMockV1Pair(trade?.route?.input)
const outputPair = useMockV1Pair(trade?.route?.output)
const mainnet = trade?.route?.input?.chainId === ChainId.MAINNET
const inputIsWETH = mainnet && trade?.route?.input?.equals(WETH[ChainId.MAINNET])
const outputIsWETH = mainnet && trade?.route?.output?.equals(WETH[ChainId.MAINNET])
const neitherWETH = mainnet && !!trade && !inputIsWETH && !outputIsWETH
let pairs: Pair[]
if (inputIsWETH && outputPair) {
pairs = [outputPair]
} else if (outputIsWETH && inputPair) {
pairs = [inputPair]
} else if (neitherWETH && inputPair && outputPair) {
pairs = [inputPair, outputPair]
}
const route = pairs && new Route(pairs, inputIsWETH ? DUMMY_ETH : trade.route.input)
const v1Trade =
route &&
new Trade(
route,
trade.tradeType === TradeType.EXACT_INPUT ? trade.inputAmount : trade.outputAmount,
trade.tradeType
)
const v1HasBetterRate = v1Trade?.slippage?.add(minimumDelta)?.lessThan(trade?.slippage)
return v1HasBetterRate
? `https://v1.uniswap.exchange/swap?inputCurrency=${
inputIsWETH ? 'ETH' : trade.route.input.address
}&outputCurrency=${outputIsWETH ? 'ETH' : trade.route.output.address}`
: undefined
}
export enum SWRKeys { export enum SWRKeys {
Allowances, Allowances,
Reserves, Reserves,
TotalSupply TotalSupply,
TokenBalance,
ETHBalance,
V1PairAddress
} }
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