Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10ef04510a | ||
|
|
e3b3d9e825 | ||
|
|
3050e967f7 | ||
|
|
2150450760 | ||
|
|
1b07e95885 | ||
|
|
9bb50d6a7b |
19
cypress/integration/token-warning.ts
Normal file
19
cypress/integration/token-warning.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
describe('Warning', () => {
|
||||
beforeEach(() => {
|
||||
cy.clearLocalStorage()
|
||||
cy.visit('/swap?outputCurrency=0x0a40f26d74274b7f22b28556a27b35d97ce08e0a')
|
||||
})
|
||||
it('Check that warning is displayed', () => {
|
||||
cy.get('.token-warning-container').should('be.visible')
|
||||
})
|
||||
it('Check that warning hides after button dismissal.', () => {
|
||||
cy.get('.token-dismiss-button').click()
|
||||
cy.get('.token-warning-container').should('not.be.visible')
|
||||
})
|
||||
it('Check supression persists across sessions.', () => {
|
||||
cy.get('.token-warning-container').should('be.visible')
|
||||
cy.get('.token-dismiss-button').click()
|
||||
cy.reload()
|
||||
cy.get('.token-warning-container').should('not.be.visible')
|
||||
})
|
||||
})
|
||||
@@ -34,7 +34,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^2.31.0",
|
||||
"@typescript-eslint/parser": "^2.31.0",
|
||||
"@uniswap/sdk": "3.0.3-beta.1",
|
||||
"@uniswap/token-lists": "^1.0.0-beta.9",
|
||||
"@uniswap/token-lists": "^1.0.0-beta.11",
|
||||
"@uniswap/v2-core": "1.0.0",
|
||||
"@uniswap/v2-periphery": "^1.1.0-beta.0",
|
||||
"@web3-react/core": "^6.0.9",
|
||||
@@ -46,7 +46,7 @@
|
||||
"ajv": "^6.12.3",
|
||||
"copy-to-clipboard": "^3.2.0",
|
||||
"cross-env": "^7.0.2",
|
||||
"cypress": "^4.5.0",
|
||||
"cypress": "^4.11.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-prettier": "^3.1.3",
|
||||
|
||||
@@ -2,66 +2,41 @@ import { Currency, Token } from '@uniswap/sdk'
|
||||
import { transparentize } from 'polished'
|
||||
import React, { useMemo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens } from '../../hooks/Tokens'
|
||||
import { useDefaultTokenList } from '../../state/lists/hooks'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { useTokenWarningDismissal } from '../../state/user/hooks'
|
||||
import { ExternalLink, TYPE } from '../../theme'
|
||||
import { getEtherscanLink, isDefaultToken } from '../../utils'
|
||||
import PropsOfExcluding from '../../utils/props-of-excluding'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import CurrencyLogo from '../CurrencyLogo'
|
||||
import { AutoRow, RowBetween } from '../Row'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { ButtonError } from '../Button'
|
||||
import { useTokenWarningDismissal } from '../../state/user/hooks'
|
||||
|
||||
const Wrapper = styled.div<{ error: boolean }>`
|
||||
background: ${({ theme, error }) => transparentize(0.9, error ? theme.red1 : theme.yellow1)};
|
||||
position: relative;
|
||||
background: ${({ theme }) => transparentize(0.6, theme.white)};
|
||||
padding: 0.75rem;
|
||||
border-radius: 20px;
|
||||
`
|
||||
|
||||
const WarningContainer = styled.div`
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
/* border: 0.5px solid ${({ theme, error }) => transparentize(0.4, error ? theme.red1 : theme.yellow1)}; */
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
display: grid;
|
||||
grid-template-rows: 14px auto auto;
|
||||
grid-row-gap: 14px;
|
||||
background: rgba(242, 150, 2, 0.05);
|
||||
border: 1px solid #f3841e;
|
||||
box-sizing: border-box;
|
||||
border-radius: 20px;
|
||||
margin-bottom: 2rem;
|
||||
`
|
||||
|
||||
const Row = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-items: flex-start;
|
||||
& > * {
|
||||
margin-right: 6px;
|
||||
}
|
||||
const StyledWarningIcon = styled(AlertTriangle)`
|
||||
stroke: ${({ theme }) => theme.red2};
|
||||
`
|
||||
|
||||
const CloseColor = styled(Close)`
|
||||
color: #aeaeae;
|
||||
`
|
||||
|
||||
const CloseIcon = styled.div`
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 12px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
& > * {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
`
|
||||
|
||||
const HELP_TEXT = `
|
||||
The Uniswap V2 smart contracts are designed to support any ERC20 token on Ethereum. Any token can be
|
||||
loaded into the interface by entering its Ethereum address into the search field or passing it as a URL
|
||||
parameter.
|
||||
`
|
||||
|
||||
const DUPLICATE_NAME_HELP_TEXT = `${HELP_TEXT} This token has the same name or symbol as another token in your list.`
|
||||
|
||||
interface TokenWarningCardProps extends PropsOfExcluding<typeof Wrapper, 'error'> {
|
||||
token?: Token
|
||||
}
|
||||
@@ -74,8 +49,6 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro
|
||||
const tokenSymbol = token?.symbol?.toLowerCase() ?? ''
|
||||
const tokenName = token?.name?.toLowerCase() ?? ''
|
||||
|
||||
const [dismissed, dismissTokenWarning] = useTokenWarningDismissal(chainId, token)
|
||||
|
||||
const allTokens = useAllTokens()
|
||||
|
||||
const duplicateNameOrSymbol = useMemo(() => {
|
||||
@@ -90,52 +63,77 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro
|
||||
})
|
||||
}, [isDefault, token, chainId, allTokens, tokenSymbol, tokenName])
|
||||
|
||||
if (isDefault || !token || dismissed) return null
|
||||
if (isDefault || !token) return null
|
||||
|
||||
return (
|
||||
<Wrapper error={duplicateNameOrSymbol} {...rest}>
|
||||
{duplicateNameOrSymbol ? null : (
|
||||
<CloseIcon onClick={dismissTokenWarning}>
|
||||
<CloseColor />
|
||||
</CloseIcon>
|
||||
)}
|
||||
<Row>
|
||||
<TYPE.subHeader>{duplicateNameOrSymbol ? 'Duplicate token name or symbol' : 'Imported token'}</TYPE.subHeader>
|
||||
<QuestionHelper text={duplicateNameOrSymbol ? DUPLICATE_NAME_HELP_TEXT : HELP_TEXT} />
|
||||
</Row>
|
||||
<Row>
|
||||
<CurrencyLogo currency={token} />
|
||||
<div style={{ fontWeight: 500 }}>
|
||||
{token && token.name && token.symbol && token.name !== token.symbol
|
||||
? `${token.name} (${token.symbol})`
|
||||
: token.name || token.symbol}
|
||||
</div>
|
||||
<ExternalLink style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'token')}>
|
||||
(View on Etherscan)
|
||||
</ExternalLink>
|
||||
</Row>
|
||||
<Row>
|
||||
<TYPE.italic>Verify this is the correct token before making any transactions.</TYPE.italic>
|
||||
</Row>
|
||||
<AutoRow gap="6px">
|
||||
<AutoColumn gap="24px">
|
||||
<CurrencyLogo currency={token} size={'16px'} />
|
||||
<div> </div>
|
||||
</AutoColumn>
|
||||
<AutoColumn gap="10px" justify="flex-start">
|
||||
<TYPE.main>
|
||||
{token && token.name && token.symbol && token.name !== token.symbol
|
||||
? `${token.name} (${token.symbol})`
|
||||
: token.name || token.symbol}
|
||||
</TYPE.main>
|
||||
<ExternalLink style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'token')}>
|
||||
<TYPE.blue> (View on Etherscan)</TYPE.blue>
|
||||
</ExternalLink>
|
||||
</AutoColumn>
|
||||
</AutoRow>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const WarningContainer = styled.div`
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
`
|
||||
|
||||
export function TokenWarningCards({ currencies }: { currencies: { [field in Field]?: Currency } }) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const [dismissedToken0, dismissToken0] = useTokenWarningDismissal(chainId, currencies[Field.INPUT])
|
||||
const [dismissedToken1, dismissToken1] = useTokenWarningDismissal(chainId, currencies[Field.OUTPUT])
|
||||
|
||||
return (
|
||||
<WarningContainer>
|
||||
{Object.keys(currencies).map(field =>
|
||||
currencies[field] instanceof Token ? (
|
||||
<TokenWarningCard style={{ marginBottom: 14 }} key={field} token={currencies[field]} />
|
||||
) : null
|
||||
)}
|
||||
<WarningContainer className="token-warning-container">
|
||||
<AutoColumn gap="lg">
|
||||
<AutoRow gap="6px">
|
||||
<StyledWarningIcon />
|
||||
<TYPE.main color={'red2'}>Token imported</TYPE.main>
|
||||
</AutoRow>
|
||||
<TYPE.body color={'red2'}>
|
||||
Anyone can create and name any ERC20 token on Ethereum, including creating fake versions of existing tokens
|
||||
and tokens that claim to represent projects that do not have a token.
|
||||
</TYPE.body>
|
||||
<TYPE.body color={'red2'}>
|
||||
Similar to Etherscan, this site can load arbitrary tokens via token addresses. Please do your own research
|
||||
before interacting with any ERC20 token.
|
||||
</TYPE.body>
|
||||
{Object.keys(currencies).map(field => {
|
||||
const dismissed = field === Field.INPUT ? dismissedToken0 : dismissedToken1
|
||||
return currencies[field] instanceof Token && !dismissed ? (
|
||||
<TokenWarningCard key={field} token={currencies[field]} />
|
||||
) : null
|
||||
})}
|
||||
<RowBetween>
|
||||
<div />
|
||||
<ButtonError
|
||||
error={true}
|
||||
width={'140px'}
|
||||
padding="0.5rem 1rem"
|
||||
style={{
|
||||
borderRadius: '10px'
|
||||
}}
|
||||
onClick={() => {
|
||||
dismissToken0 && dismissToken0()
|
||||
dismissToken1 && dismissToken1()
|
||||
}}
|
||||
>
|
||||
<TYPE.body color="white" className="token-dismiss-button">
|
||||
I understand
|
||||
</TYPE.body>
|
||||
</ButtonError>
|
||||
<div />
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</WarningContainer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useMemo } from 'react'
|
||||
|
||||
import { BASES_TO_CHECK_TRADES_AGAINST } from '../constants'
|
||||
import { PairState, usePairs } from '../data/Reserves'
|
||||
import { maxHopsFor } from '../utils/maxHopsFor'
|
||||
import { wrappedCurrency } from '../utils/wrappedCurrency'
|
||||
|
||||
import { useActiveWeb3React } from './index'
|
||||
@@ -58,8 +59,9 @@ export function useTradeExactIn(currencyAmountIn?: CurrencyAmount, currencyOut?:
|
||||
|
||||
return useMemo(() => {
|
||||
if (currencyAmountIn && currencyOut && allowedPairs.length > 0) {
|
||||
const maxHops = maxHopsFor(currencyAmountIn.currency, currencyOut)
|
||||
return (
|
||||
Trade.bestTradeExactIn(allowedPairs, currencyAmountIn, currencyOut, { maxHops: 3, maxNumResults: 1 })[0] ?? null
|
||||
Trade.bestTradeExactIn(allowedPairs, currencyAmountIn, currencyOut, { maxHops, maxNumResults: 1 })[0] ?? null
|
||||
)
|
||||
}
|
||||
return null
|
||||
@@ -74,9 +76,9 @@ export function useTradeExactOut(currencyIn?: Currency, currencyAmountOut?: Curr
|
||||
|
||||
return useMemo(() => {
|
||||
if (currencyIn && currencyAmountOut && allowedPairs.length > 0) {
|
||||
const maxHops = maxHopsFor(currencyIn, currencyAmountOut.currency)
|
||||
return (
|
||||
Trade.bestTradeExactOut(allowedPairs, currencyIn, currencyAmountOut, { maxHops: 3, maxNumResults: 1 })[0] ??
|
||||
null
|
||||
Trade.bestTradeExactOut(allowedPairs, currencyIn, currencyAmountOut, { maxHops, maxNumResults: 1 })[0] ?? null
|
||||
)
|
||||
}
|
||||
return null
|
||||
|
||||
@@ -311,7 +311,7 @@ export default function AddLiquidity({
|
||||
}}
|
||||
attemptingTxn={attemptingTxn}
|
||||
hash={txHash}
|
||||
topContent={() => modalHeader()}
|
||||
topContent={modalHeader}
|
||||
bottomContent={modalBottom}
|
||||
pendingText={pendingText}
|
||||
title={noLiquidity ? 'You are creating a pool' : 'You will receive'}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const BodyWrapper = styled.div`
|
||||
export const BodyWrapper = styled.div<{ disabled?: boolean }>`
|
||||
position: relative;
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
@@ -10,11 +10,13 @@ export const BodyWrapper = styled.div`
|
||||
0px 24px 32px rgba(0, 0, 0, 0.01);
|
||||
border-radius: 30px;
|
||||
padding: 1rem;
|
||||
opacity: ${({ disabled }) => (disabled ? '0.4' : '1')};
|
||||
pointer-events: ${({ disabled }) => disabled && 'none'};
|
||||
`
|
||||
|
||||
/**
|
||||
* The styled container element that wraps the content of most pages and the tabs.
|
||||
*/
|
||||
export default function AppBody({ children }: { children: React.ReactNode }) {
|
||||
return <BodyWrapper>{children}</BodyWrapper>
|
||||
export default function AppBody({ children, disabled }: { children: React.ReactNode; disabled?: boolean }) {
|
||||
return <BodyWrapper disabled={disabled}>{children}</BodyWrapper>
|
||||
}
|
||||
|
||||
@@ -37,7 +37,12 @@ import {
|
||||
useSwapActionHandlers,
|
||||
useSwapState
|
||||
} from '../../state/swap/hooks'
|
||||
import { useExpertModeManager, useUserDeadline, useUserSlippageTolerance } from '../../state/user/hooks'
|
||||
import {
|
||||
useExpertModeManager,
|
||||
useUserDeadline,
|
||||
useUserSlippageTolerance,
|
||||
useTokenWarningDismissal
|
||||
} from '../../state/user/hooks'
|
||||
import { CursorPointer, LinkStyledButton, TYPE } from '../../theme'
|
||||
import { maxAmountSpend } from '../../utils/maxAmountSpend'
|
||||
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
|
||||
@@ -47,7 +52,7 @@ import { ClickableText } from '../Pool/styleds'
|
||||
export default function Swap() {
|
||||
useDefaultsFromURLSearch()
|
||||
|
||||
const { account } = useActiveWeb3React()
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
// toggle wallet when disconnected
|
||||
@@ -241,10 +246,15 @@ export default function Swap() {
|
||||
currencies[Field.INPUT]?.symbol
|
||||
} for ${parsedAmounts[Field.OUTPUT]?.toSignificant(6)} ${currencies[Field.OUTPUT]?.symbol}`
|
||||
|
||||
const [dismissedToken0] = useTokenWarningDismissal(chainId, currencies[Field.INPUT])
|
||||
const [dismissedToken1] = useTokenWarningDismissal(chainId, currencies[Field.OUTPUT])
|
||||
const showWarning =
|
||||
(!dismissedToken0 && !!currencies[Field.INPUT]) || (!dismissedToken1 && !!currencies[Field.OUTPUT])
|
||||
|
||||
return (
|
||||
<>
|
||||
<TokenWarningCards currencies={currencies} />
|
||||
<AppBody>
|
||||
{showWarning && <TokenWarningCards currencies={currencies} />}
|
||||
<AppBody disabled={!!showWarning}>
|
||||
<SwapPoolTabs active={'swap'} />
|
||||
<Wrapper id="swap-page">
|
||||
<ConfirmationModal
|
||||
@@ -424,7 +434,6 @@ export default function Swap() {
|
||||
</BottomGrouping>
|
||||
</Wrapper>
|
||||
</AppBody>
|
||||
|
||||
<AdvancedSwapDetailsDropdown trade={trade} />
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Currency, CurrencyAmount, JSBI, Pair, Percent, Price, TokenAmount } from '@uniswap/sdk'
|
||||
import { Currency, CurrencyAmount, ETHER, JSBI, Pair, Percent, Price, TokenAmount } from '@uniswap/sdk'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { PairState, usePair } from '../../data/Reserves'
|
||||
@@ -65,17 +65,24 @@ export function useDerivedMintInfo(
|
||||
}
|
||||
|
||||
// amounts
|
||||
const independentAmount = tryParseAmount(typedValue, currencies[independentField])
|
||||
const dependentAmount = useMemo(() => {
|
||||
if (noLiquidity && otherTypedValue && currencies[dependentField]) {
|
||||
return tryParseAmount(otherTypedValue, currencies[dependentField])
|
||||
const independentAmount: CurrencyAmount | undefined = tryParseAmount(typedValue, currencies[independentField])
|
||||
const dependentAmount: CurrencyAmount | undefined = useMemo(() => {
|
||||
if (noLiquidity) {
|
||||
if (otherTypedValue && currencies[dependentField]) {
|
||||
return tryParseAmount(otherTypedValue, currencies[dependentField])
|
||||
}
|
||||
return
|
||||
} else if (independentAmount) {
|
||||
// we wrap the currencies just to get the price in terms of the other token
|
||||
const wrappedIndependentAmount = wrappedCurrencyAmount(independentAmount, chainId)
|
||||
const [tokenA, tokenB] = [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)]
|
||||
if (tokenA && tokenB && wrappedIndependentAmount && pair) {
|
||||
return dependentField === Field.CURRENCY_B
|
||||
? pair.priceOf(tokenA).quote(wrappedIndependentAmount)
|
||||
: pair.priceOf(tokenB).quote(wrappedIndependentAmount)
|
||||
const dependentCurrency = dependentField === Field.CURRENCY_B ? currencyB : currencyA
|
||||
const dependentTokenAmount =
|
||||
dependentField === Field.CURRENCY_B
|
||||
? pair.priceOf(tokenA).quote(wrappedIndependentAmount)
|
||||
: pair.priceOf(tokenB).quote(wrappedIndependentAmount)
|
||||
return dependentCurrency === ETHER ? CurrencyAmount.ether(dependentTokenAmount.raw) : dependentTokenAmount
|
||||
}
|
||||
return
|
||||
} else {
|
||||
@@ -89,12 +96,11 @@ export function useDerivedMintInfo(
|
||||
|
||||
const price = useMemo(() => {
|
||||
const { [Field.CURRENCY_A]: currencyAAmount, [Field.CURRENCY_B]: currencyBAmount } = parsedAmounts
|
||||
if (noLiquidity && currencyAAmount && currencyBAmount) {
|
||||
if (currencyAAmount && currencyBAmount) {
|
||||
return new Price(currencyAAmount.currency, currencyBAmount.currency, currencyAAmount.raw, currencyBAmount.raw)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}, [noLiquidity, parsedAmounts])
|
||||
return
|
||||
}, [parsedAmounts])
|
||||
|
||||
// liquidity minted
|
||||
const totalSupply = useTotalSupply(pair?.liquidityToken)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChainId, Pair, Token } from '@uniswap/sdk'
|
||||
import { ChainId, Pair, Token, Currency } from '@uniswap/sdk'
|
||||
import flatMap from 'lodash.flatmap'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux'
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
updateUserExpertMode,
|
||||
updateUserSlippageTolerance
|
||||
} from './actions'
|
||||
import { useDefaultTokenList } from '../lists/hooks'
|
||||
import { isDefaultToken } from '../../utils'
|
||||
|
||||
function serializeToken(token: Token): SerializedToken {
|
||||
return {
|
||||
@@ -165,22 +167,30 @@ export function usePairAdder(): (pair: Pair) => void {
|
||||
* Returns whether a token warning has been dismissed and a callback to dismiss it,
|
||||
* iff it has not already been dismissed and is a valid token.
|
||||
*/
|
||||
export function useTokenWarningDismissal(chainId?: number, token?: Token): [boolean, null | (() => void)] {
|
||||
export function useTokenWarningDismissal(chainId?: number, token?: Currency): [boolean, null | (() => void)] {
|
||||
const dismissalState = useSelector<AppState, AppState['user']['dismissedTokenWarnings']>(
|
||||
state => state.user.dismissedTokenWarnings
|
||||
)
|
||||
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
|
||||
// get default list, mark as dismissed if on list
|
||||
const defaultList = useDefaultTokenList()
|
||||
const isDefault = isDefaultToken(defaultList, token)
|
||||
|
||||
return useMemo(() => {
|
||||
if (!chainId || !token) return [false, null]
|
||||
|
||||
const dismissed: boolean = dismissalState?.[chainId]?.[token.address] === true
|
||||
const dismissed: boolean =
|
||||
token instanceof Token ? dismissalState?.[chainId]?.[token.address] === true || isDefault : true
|
||||
|
||||
const callback = dismissed ? null : () => dispatch(dismissTokenWarning({ chainId, tokenAddress: token.address }))
|
||||
const callback =
|
||||
dismissed || !(token instanceof Token)
|
||||
? null
|
||||
: () => dispatch(dismissTokenWarning({ chainId, tokenAddress: token.address }))
|
||||
|
||||
return [dismissed, callback]
|
||||
}, [chainId, token, dismissalState, dispatch])
|
||||
}, [chainId, token, dismissalState, isDefault, dispatch])
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -75,6 +75,7 @@ export function colors(darkMode: boolean): Colors {
|
||||
|
||||
// other
|
||||
red1: '#FF6871',
|
||||
red2: '#F82D3A',
|
||||
green1: '#27AE60',
|
||||
yellow1: '#FFE270',
|
||||
yellow2: '#F3841E'
|
||||
@@ -171,11 +172,11 @@ export const FixedGlobalStyle = createGlobalStyle`
|
||||
html, input, textarea, button {
|
||||
font-family: 'Inter', sans-serif;
|
||||
letter-spacing: -0.018em;
|
||||
font-display: fallback;
|
||||
}
|
||||
@supports (font-variation-settings: normal) {
|
||||
html, input, textarea, button {
|
||||
font-family: 'Inter var', sans-serif;
|
||||
font-display: fallback;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
1
src/theme/styled.d.ts
vendored
1
src/theme/styled.d.ts
vendored
@@ -39,6 +39,7 @@ export interface Colors {
|
||||
|
||||
// other
|
||||
red1: Color
|
||||
red2: Color
|
||||
green1: Color
|
||||
yellow1: Color
|
||||
yellow2: Color
|
||||
|
||||
32
src/utils/maxHopsFor.ts
Normal file
32
src/utils/maxHopsFor.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { ChainId, Currency, ETHER, Token, WETH } from '@uniswap/sdk'
|
||||
|
||||
function isEtherish(currency: Currency): boolean {
|
||||
return currency === ETHER || (currency instanceof Token && WETH[currency.chainId].equals(currency))
|
||||
}
|
||||
|
||||
const AMPL_TOKEN_ADDRESS = '0xD46bA6D942050d489DBd938a2C909A5d5039A161'
|
||||
|
||||
/**
|
||||
* Band-aid on maxHops because some tokens seems to always fail with multihop swaps
|
||||
* @param currencyIn currency in
|
||||
* @param currencyOut currency out
|
||||
*/
|
||||
export function maxHopsFor(currencyIn: Currency, currencyOut: Currency): number {
|
||||
if (
|
||||
isEtherish(currencyIn) &&
|
||||
currencyOut instanceof Token &&
|
||||
currencyOut.chainId === ChainId.MAINNET &&
|
||||
currencyOut.address === AMPL_TOKEN_ADDRESS
|
||||
) {
|
||||
return 1
|
||||
} else if (
|
||||
isEtherish(currencyOut) &&
|
||||
currencyIn instanceof Token &&
|
||||
currencyIn.chainId === ChainId.MAINNET &&
|
||||
currencyIn.address === AMPL_TOKEN_ADDRESS
|
||||
) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return 3
|
||||
}
|
||||
28
src/utils/uriToHttp.test.ts
Normal file
28
src/utils/uriToHttp.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import uriToHttp from './uriToHttp'
|
||||
|
||||
describe('uriToHttp', () => {
|
||||
it('returns .eth.link for ens names', () => {
|
||||
expect(uriToHttp('t2crtokens.eth')).toEqual(['https://t2crtokens.eth.link'])
|
||||
})
|
||||
it('returns https first for http', () => {
|
||||
expect(uriToHttp('http://test.com')).toEqual(['https://test.com', 'http://test.com'])
|
||||
})
|
||||
it('returns https for https', () => {
|
||||
expect(uriToHttp('https://test.com')).toEqual(['https://test.com'])
|
||||
})
|
||||
it('returns ipfs gateways for ipfs:// urls', () => {
|
||||
expect(uriToHttp('ipfs://QmV8AfDE8GFSGQvt3vck8EwAzsPuNTmtP8VcQJE3qxRPaZ')).toEqual([
|
||||
'https://cloudflare-ipfs.com/ipfs/QmV8AfDE8GFSGQvt3vck8EwAzsPuNTmtP8VcQJE3qxRPaZ/',
|
||||
'https://ipfs.io/ipfs/QmV8AfDE8GFSGQvt3vck8EwAzsPuNTmtP8VcQJE3qxRPaZ/'
|
||||
])
|
||||
})
|
||||
it('returns ipns gateways for ipns:// urls', () => {
|
||||
expect(uriToHttp('ipns://app.uniswap.org')).toEqual([
|
||||
'https://cloudflare-ipfs.com/ipns/app.uniswap.org/',
|
||||
'https://ipfs.io/ipns/app.uniswap.org/'
|
||||
])
|
||||
})
|
||||
it('returns empty array for invalid scheme', () => {
|
||||
expect(uriToHttp('blah:test')).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -10,20 +10,18 @@ export default function uriToHttp(uri: string): string[] {
|
||||
} else if (parsed.protocol === 'https:') {
|
||||
return [uri]
|
||||
} else if (parsed.protocol === 'ipfs:') {
|
||||
const hash = parsed.pathname.substring(2)
|
||||
return [`https://cloudflare-ipfs.com/ipfs/${hash}/`, `https://ipfs.infura.io/ipfs/${hash}/`]
|
||||
const hash = parsed.href.match(/^ipfs:(\/\/)?(.*)$/)?.[2]
|
||||
return [`https://cloudflare-ipfs.com/ipfs/${hash}/`, `https://ipfs.io/ipfs/${hash}/`]
|
||||
} else if (parsed.protocol === 'ipns:') {
|
||||
const name = parsed.pathname.substring(2)
|
||||
return [`https://cloudflare-ipfs.com/ipns/${name}/`, `https://ipfs.infura.io/ipns/${name}/`]
|
||||
const name = parsed.href.match(/^ipns:(\/\/)?(.*)$/)?.[2]
|
||||
return [`https://cloudflare-ipfs.com/ipns/${name}/`, `https://ipfs.io/ipns/${name}/`]
|
||||
} else {
|
||||
console.error('Unrecognized protocol', parsed)
|
||||
return []
|
||||
}
|
||||
} catch (error) {
|
||||
if (uri.toLowerCase().endsWith('.eth')) {
|
||||
return [`https://${uri.toLowerCase()}.link`]
|
||||
}
|
||||
console.error('Failed to parse URI', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
10
yarn.lock
10
yarn.lock
@@ -2681,10 +2681,10 @@
|
||||
tiny-warning "^1.0.3"
|
||||
toformat "^2.0.0"
|
||||
|
||||
"@uniswap/token-lists@^1.0.0-beta.9":
|
||||
version "1.0.0-beta.10"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/token-lists/-/token-lists-1.0.0-beta.10.tgz#9788fdd65c3720f1f7cc3ef262f507ea3f4ac062"
|
||||
integrity sha512-NCdJC6Zlb62GN9oiYZoI0PMm26G5vu2CnBYAkR4rEPUqqKuxagdqntjOt2XfwklQaeSc9egwXUdbAOTU8wgjyA==
|
||||
"@uniswap/token-lists@^1.0.0-beta.11":
|
||||
version "1.0.0-beta.11"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/token-lists/-/token-lists-1.0.0-beta.11.tgz#365dd55d536c67fa550554c0658391bfbc2930b7"
|
||||
integrity sha512-dGHdb58d+rN7G164ziPP6omb1R0hwBVgs95er83OzXKkVRlLKE/FLSdgpDaTxLj1war+P/hZXw2/ToYcKFsobQ==
|
||||
|
||||
"@uniswap/v2-core@1.0.0":
|
||||
version "1.0.0"
|
||||
@@ -5690,7 +5690,7 @@ cyclist@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
|
||||
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
|
||||
|
||||
cypress@*, cypress@^4.5.0:
|
||||
cypress@*, cypress@^4.11.0:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.11.0.tgz#054b0b85fd3aea793f186249ee1216126d5f0a7e"
|
||||
integrity sha512-6Yd598+KPATM+dU1Ig0g2hbA+R/o1MAKt0xIejw4nZBVLSplCouBzqeKve6XsxGU6n4HMSt/+QYsWfFcoQeSEw==
|
||||
|
||||
Reference in New Issue
Block a user