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({
<>
<AccountControl hasENS={!!ENSName} isENS={false}>
<StyledLink hasENS={!!ENSName} isENS={true} href={getEtherscanLink(chainId, ENSName, 'address')}>
View on Etherscan ↗{' '}
View on Etherscan ↗
</StyledLink>
</AccountControl>
</>
......@@ -353,7 +353,7 @@ export default function AccountDetails({
<>
<AccountControl hasENS={!!ENSName} isENS={false}>
<StyledLink hasENS={!!ENSName} isENS={false} href={getEtherscanLink(chainId, account, 'address')}>
View on Etherscan ↗{' '}
View on Etherscan ↗
</StyledLink>
</AccountControl>
</>
......
......@@ -190,15 +190,14 @@ export default function AddressInputPanel({
<TYPE.black color={theme.text2} fontWeight={500} fontSize={14}>
Recipient
</TYPE.black>
{data.name ||
(data.address && (
{(data.name || data.address) && (
<Link
href={getEtherscanLink(chainId, data.name || data.address, 'address')}
style={{ fontSize: '14px' }}
>
(View on Etherscan)
</Link>
))}
)}
</RowBetween>
<Input
type="text"
......
......@@ -55,6 +55,7 @@ import {
TruncatedText,
Wrapper
} from './styleds'
import { useV1TradeLinkIfBetter } from '../../data/V1'
enum SwapType {
EXACT_TOKENS_FOR_TOKENS,
......@@ -184,6 +185,10 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
)
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 userHasSpecifiedInputOutput =
!!tokens[Field.INPUT] &&
......@@ -814,6 +819,7 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
</Text>
</RowBetween>
)}
<RowBetween>
<RowFixed>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
......@@ -1140,6 +1146,7 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
</StyledBalanceMaxMini>
</Text>
</RowBetween>
{trade && (warningHigh || warningMedium) && (
<RowBetween>
<TYPE.main
......@@ -1215,6 +1222,18 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
</Text>
</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>
{tokens[Field.INPUT] && tokens[Field.OUTPUT] && !noRoute && (
<AdvancedDropwdown>
......@@ -1347,7 +1366,7 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
</AutoColumn>
</FixedBottom>
</AdvancedDropwdown>
)}{' '}
)}
</Wrapper>
)
}
......
......@@ -9,7 +9,7 @@ import Web3Status from '../Web3Status'
import { Link } from '../../theme'
import { Text } from 'rebass'
import { WETH } from '@uniswap/sdk'
import { WETH, ChainId } from '@uniswap/sdk'
import { isMobile } from 'react-device-detect'
import { YellowCard } from '../Card'
import { useWeb3React } from '../../hooks'
......@@ -41,8 +41,6 @@ const HeaderFrame = styled.div`
`
const HeaderElement = styled.div`
display: flex;
min-width: 0;
display: flex;
align-items: center;
`
......@@ -65,13 +63,11 @@ const TitleText = styled(Row)`
`
const AccountElement = styled.div<{ active: boolean }>`
display: flex;
display: flex;
flex-direction: row;
align-items: center;
background-color: ${({ theme, active }) => (!active ? theme.bg1 : theme.bg3)};
border-radius: 12px;
padding-left: ${({ active }) => (active ? '8px' : 0)};
white-space: nowrap;
:focus {
......@@ -197,22 +193,19 @@ export default function Header() {
</HeaderElement>
<HeaderElement>
<TestnetWrapper>
{!isMobile && chainId === 4 && <NetworkCard>Rinkeby</NetworkCard>}
{!isMobile && chainId === 3 && <NetworkCard>Ropsten</NetworkCard>}
{!isMobile && chainId === 5 && <NetworkCard>Goerli</NetworkCard>}
{!isMobile && chainId === 42 && <NetworkCard>Kovan</NetworkCard>}
{!isMobile && chainId === ChainId.ROPSTEN && <NetworkCard>Ropsten</NetworkCard>}
{!isMobile && chainId === ChainId.RINKEBY && <NetworkCard>Rinkeby</NetworkCard>}
{!isMobile && chainId === ChainId.GÖRLI && <NetworkCard>rli</NetworkCard>}
{!isMobile && chainId === ChainId.KOVAN && <NetworkCard>Kovan</NetworkCard>}
</TestnetWrapper>
<AccountElement active={!!account}>
{account ? (
<Row style={{ marginRight: '-1.25rem' }}>
<Text fontWeight={500}> {userEthBalance && `${userEthBalance?.toSignificant(4)} ETH`}</Text>
</Row>
) : (
''
)}
{account && userEthBalance ? (
<Text style={{ flexShrink: 0 }} px="0.5rem" fontWeight={500}>
{userEthBalance?.toSignificant(4)} ETH
</Text>
) : null}
<Web3Status />
</AccountElement>
<Menu />
</HeaderElement>
</RowBetween>
......
import React, { useRef, useEffect } from 'react'
import styled from 'styled-components'
import { useDarkModeManager } from '../../contexts/LocalStorage'
import { ReactComponent as MenuIcon } from '../../assets/images/menu.svg'
......@@ -83,8 +82,6 @@ export default function Menu() {
const node = useRef<HTMLDivElement>()
const [open, toggle] = useToggle(false)
const [darkMode, toggleDarkMode] = useDarkModeManager()
useEffect(() => {
const handleClickOutside = e => {
if (node.current?.contains(e.target) ?? false) {
......@@ -123,7 +120,6 @@ export default function Menu() {
<MenuItem id="link" href="https://uniswap.info/">
Analytics
</MenuItem>
<MenuItem onClick={toggleDarkMode}>{darkMode ? 'Light theme' : 'Dark theme'}</MenuItem>
</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 { ChainId, WETH, Token } from '@uniswap/sdk'
export const V1_FACTORY_ADDRESS = '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'
export const ROUTER_ADDRESS = '0xf164fC0Ec4E93095b804a4795bBe1e041497b92a'
// used for display in the default list when adding liquidity
export const COMMON_BASES = {
1: [WETH[ChainId.MAINNET]],
3: [WETH[ChainId.ROPSTEN]],
4: [
[ChainId.MAINNET]: [
WETH[ChainId.MAINNET],
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],
new Token(ChainId.RINKEBY, '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735', 18, 'DAI', 'Dai Stablecoin')
],
5: [WETH[ChainId.GÖRLI]],
42: [WETH[ChainId.KOVAN]]
[ChainId.GÖRLI]: [WETH[ChainId.GÖRLI]],
[ChainId.KOVAN]: [WETH[ChainId.KOVAN]]
}
export const SUPPORTED_THEMES = {
......
......@@ -15,12 +15,7 @@ function getTokenAllowance(
.then((balance: { toString: () => string }) => new TokenAmount(token, balance.toString()))
}
export function useTokenAllowance(
token?: Token,
owner?: string,
spender?: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): TokenAmount {
export function useTokenAllowance(token?: Token, owner?: string, spender?: string): TokenAmount {
const contract = useTokenContract(token?.address, false)
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 {
Allowances,
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