Compare commits

...

18 Commits

Author SHA1 Message Date
vignesh mohankumar
99a7fb3383 fix: remove refetchOnFocus for routing-api (#4601) 2022-09-09 18:35:12 -04:00
vignesh mohankumar
7f4dbf9346 chore: update smart-order-router (#4600)
* bump SOR

* dedupe
2022-09-09 18:31:41 -04:00
Zach Pomerantz
09b00c9974 fix: use static rpc urls (#4599) 2022-09-09 14:58:13 -07:00
Zach Pomerantz
b74fb8174d feat: optimize AlphaRouter usage (#4596)
* feat: add RouterPreference.PRICE

* docs: add reference to quote params

* fix: cache routers between usages

* fix: tune price inquiries

* fix: note price queries tuning

* fix: clean up params - nix mixed

* fix: defer PRICE tuning
2022-09-09 13:07:53 -07:00
Jordan Frankfurt
a7ec5a64b7 feat(explore): add a simple search debounce (#4595)
add a simple search debounce
2022-09-09 14:09:08 -05:00
Greg Bugyis
c619dcf65d fix: Token chart - inconsistent x-axis time intervals (#4579)
* Use timeScale for x-axis

* Use d3 time intervals for ticks

* Drop slice for now, will handle differently

* scaleTime turned out to be unnecessary

* Use .nice() to help with tick spacing at start/end

Co-authored-by: gbugyis <greg@bugyis.com>
2022-09-09 22:08:57 +03:00
aballerr
1221d88e13 chore: Cypress utility function for selecting feature flags and walletdrop down cypress tests (#4536)
* Adding feature flag utility to cypress and adding wallet cypress tests


Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
2022-09-09 14:54:54 -04:00
Charles Bachmeier
48d2ead71d feat: update universal token search and trending tokens endpoint (#4593)
update universal token search and trending tokens endpoint

Co-authored-by: Charlie <charlie@uniswap.org>
2022-09-09 10:28:54 -07:00
aballerr
ed7099bfd6 chore: Merging details page (#4585)
* Initial merge of details page


Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
2022-09-09 13:23:41 -04:00
vignesh mohankumar
2604cdfdae fix: only initialize using chain query (#4567) 2022-09-08 12:16:29 -04:00
cartcrom
94dc389812 feat: widget speedbumps on swap review (#4544)
* initial commit
* finished feature
* addressed PR comments
2022-09-07 16:12:35 -04:00
Greg Bugyis
4a8c621f46 feat: add maxHeight to CurrencySearchModal (#4557)
* Add maxHeight to CurrencySearchModal (search only)

* Combine min and maxHeight into single modalHeight value

* Use clearer variable name to distinguish window height value

Co-authored-by: gbugyis <greg@bugyis.com>
2022-09-07 23:08:29 +03:00
aballerr
477af8af4e fix: Making sure all icons are 24px (#4580)
Making all icons size 24px on web status

Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
2022-09-07 15:47:24 -04:00
aballerr
a9a7d524aa fix: fixing token colors and token select persistence (#4575)
* fixing token colors and token select persistence

Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
2022-09-07 13:35:00 -04:00
aballerr
1cdaff8ddf fix: fixing match design (#4577)
* fixing select token favorite icon to match design



Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
2022-09-07 12:44:19 -04:00
aballerr
eeea3d2dcc fix: fixed wallet scrolling issue (#4574)
* fixed scrolling issue for wallet


Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
2022-09-07 11:44:33 -04:00
lynn
f46b6a0697 fix: ensure nav bar goes above all other components when scrolling (#4576)
* fix nav bar below other components issue

* respond to comments
2022-09-06 17:18:54 -04:00
lynn
622581ee0a feat: show real values for current network balance (#4565)
* init

* remove card when balance is zero

* remove commented code

* remove commented
2022-09-06 11:50:02 -04:00
53 changed files with 688 additions and 320 deletions

1
.env
View File

@@ -5,3 +5,4 @@ REACT_APP_AWS_API_ACCESS_KEY="AKIAYJJWW6AQ47ODATHN"
REACT_APP_AWS_API_ACCESS_SECRET="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"
REACT_APP_AWS_X_API_KEY="z9dReS5UtHu7iTrUsTuWRozLthi3AxOZlvobrIdr14"
REACT_APP_AWS_API_ENDPOINT="https://beta.api.uniswap.org/v1/graphql"
REACT_APP_TEMP_API_URL="https://temp.api.uniswap.org/v1"

View File

@@ -0,0 +1,50 @@
import { FeatureFlag } from '../../src/featureFlags/flags/featureFlags'
import { getTestSelector } from '../utils'
describe('Wallet Dropdown', () => {
before(() => {
cy.visit('/', { featureFlags: [FeatureFlag.navBar, FeatureFlag.tokenSafety] })
})
it('should change the theme', () => {
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-select-theme')).click()
cy.get(getTestSelector('wallet-select-theme')).contains('Light theme').should('exist')
})
it('should select a language', () => {
cy.get(getTestSelector('wallet-select-language')).click()
cy.get(getTestSelector('wallet-language-item')).contains('Afrikaans').click({ force: true })
cy.get(getTestSelector('wallet-header')).should('contain', 'Taal')
cy.get(getTestSelector('wallet-language-item')).contains('English').click({ force: true })
cy.get(getTestSelector('wallet-header')).should('contain', 'Language')
cy.get(getTestSelector('wallet-back')).click()
})
it('should be able to view transactions', () => {
cy.get(getTestSelector('wallet-transactions')).click()
cy.get(getTestSelector('wallet-empty-transaction-text')).should('exist')
cy.get(getTestSelector('wallet-back')).click()
})
it('should change the theme when not connected', () => {
cy.get(getTestSelector('wallet-disconnect')).click()
cy.get(getTestSelector('wallet-select-theme')).click()
cy.get(getTestSelector('wallet-select-theme')).contains('Dark theme').should('exist')
})
it('should select a language when not connected', () => {
cy.get(getTestSelector('wallet-select-language')).click()
cy.get(getTestSelector('wallet-language-item')).contains('Afrikaans').click({ force: true })
cy.get(getTestSelector('wallet-header')).should('contain', 'Taal')
cy.get(getTestSelector('wallet-language-item')).contains('English').click({ force: true })
cy.get(getTestSelector('wallet-header')).should('contain', 'Language')
cy.get(getTestSelector('wallet-back')).click()
})
it('should open the wallet connect modal from the drop down when not connected', () => {
cy.get(getTestSelector('wallet-connect-wallet')).click()
cy.get(getTestSelector('wallet-modal')).should('exist')
cy.get(getTestSelector('wallet-modal-close')).click()
})
})

View File

@@ -9,6 +9,8 @@
import { injected } from './ethereum'
import assert = require('assert')
import { FeatureFlag } from '../../src/featureFlags/flags/featureFlags'
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
@@ -17,6 +19,7 @@ declare global {
}
interface VisitOptions {
serviceWorker?: true
featureFlags?: Array<FeatureFlag>
}
}
}
@@ -36,6 +39,18 @@ Cypress.Commands.overwrite(
options?.onBeforeLoad?.(win)
win.localStorage.clear()
win.localStorage.setItem('redux_localstorage_simple_user', '{"selectedWallet":"INJECTED"}')
if (options?.featureFlags) {
const featureFlags = options.featureFlags.reduce(
(flags, flag) => ({
...flags,
[flag]: 'enabled',
}),
{}
)
win.localStorage.setItem('featureFlags', JSON.stringify(featureFlags))
}
win.ethereum = injected
},
})

1
cypress/utils/index.ts Normal file
View File

@@ -0,0 +1 @@
export const getTestSelector = (selectorId: string) => `[data-testid=${selectorId}]`

View File

@@ -137,7 +137,7 @@
"@uniswap/redux-multicall": "^1.1.5",
"@uniswap/router-sdk": "^1.3.0",
"@uniswap/sdk-core": "^3.0.1",
"@uniswap/smart-order-router": "^2.9.2",
"@uniswap/smart-order-router": "^2.10.0",
"@uniswap/token-lists": "^1.0.0-beta.30",
"@uniswap/v2-core": "1.0.0",
"@uniswap/v2-periphery": "^1.1.0-beta.0",

View File

@@ -4,9 +4,10 @@ import useInterval from 'lib/hooks/useInterval'
import React, { useCallback, useMemo, useState } from 'react'
import { usePopper } from 'react-popper'
import styled from 'styled-components/macro'
import { Z_INDEX } from 'theme'
const PopoverContainer = styled.div<{ show: boolean }>`
z-index: 9999;
z-index: ${Z_INDEX.absoluteTop};
visibility: ${(props) => (props.show ? 'visible' : 'hidden')};
opacity: ${(props) => (props.show ? 1 : 0)};
transition: visibility 150ms linear, opacity 150ms linear;

View File

@@ -8,6 +8,7 @@ import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import { useUserAddedTokens } from 'state/user/hooks'
import useLast from '../../hooks/useLast'
import { useWindowSize } from '../../hooks/useWindowSize'
import Modal from '../Modal'
import { CurrencySearch } from './CurrencySearch'
import { ImportList } from './ImportList'
@@ -97,11 +98,16 @@ export default memo(function CurrencySearchModal({
[setModalView, prevView]
)
const { height: windowHeight } = useWindowSize()
// change min height if not searching
let minHeight: number | undefined = 80
let modalHeight: number | undefined = 80
let content = null
switch (modalView) {
case CurrencyModalView.search:
if (windowHeight) {
// Converts pixel units to vh for Modal component
modalHeight = Math.min(Math.round((680 / windowHeight) * 100), 80)
}
content = (
<CurrencySearch
isOpen={isOpen}
@@ -119,7 +125,7 @@ export default memo(function CurrencySearchModal({
)
break
case CurrencyModalView.tokenSafety:
minHeight = undefined
modalHeight = undefined
if (tokenSafetyFlag === TokenSafetyVariant.Enabled && warningToken) {
content = (
<TokenSafety
@@ -133,7 +139,7 @@ export default memo(function CurrencySearchModal({
break
case CurrencyModalView.importToken:
if (importToken) {
minHeight = undefined
modalHeight = undefined
if (tokenSafetyFlag === TokenSafetyVariant.Enabled) {
showTokenSafetySpeedbump(importToken)
}
@@ -149,7 +155,7 @@ export default memo(function CurrencySearchModal({
}
break
case CurrencyModalView.importList:
minHeight = 40
modalHeight = 40
if (importList && listURL) {
content = <ImportList list={importList} listURL={listURL} onDismiss={onDismiss} setModalView={setModalView} />
}
@@ -167,7 +173,7 @@ export default memo(function CurrencySearchModal({
break
}
return (
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={80} minHeight={minHeight}>
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={modalHeight} minHeight={modalHeight}>
{content}
</Modal>
)

View File

@@ -1,13 +1,8 @@
import Modal from '../Modal'
import TokenSafety from '.'
import TokenSafety, { TokenSafetyProps } from '.'
interface TokenSafetyModalProps {
interface TokenSafetyModalProps extends TokenSafetyProps {
isOpen: boolean
tokenAddress: string | null
secondTokenAddress?: string
onContinue: () => void
onCancel: () => void
showCancel?: boolean
}
export default function TokenSafetyModal({
@@ -16,6 +11,7 @@ export default function TokenSafetyModal({
secondTokenAddress,
onContinue,
onCancel,
onBlocked,
showCancel,
}: TokenSafetyModalProps) {
return (
@@ -23,8 +19,9 @@ export default function TokenSafetyModal({
<TokenSafety
tokenAddress={tokenAddress}
secondTokenAddress={secondTokenAddress}
onCancel={onCancel}
onContinue={onContinue}
onBlocked={onBlocked}
onCancel={onCancel}
showCancel={showCancel}
/>
</Modal>

View File

@@ -73,11 +73,13 @@ const Buttons = ({
warning,
onContinue,
onCancel,
onBlocked,
showCancel,
}: {
warning: Warning
onContinue: () => void
onCancel: () => void
onBlocked?: () => void
showCancel?: boolean
}) => {
return warning.canProceed ? (
@@ -88,7 +90,7 @@ const Buttons = ({
{showCancel && <StyledCancelButton onClick={onCancel}>Cancel</StyledCancelButton>}
</>
) : (
<StyledCloseButton onClick={onCancel}>
<StyledCloseButton onClick={onBlocked ?? onCancel}>
<Trans>Close</Trans>
</StyledCloseButton>
)
@@ -184,11 +186,12 @@ const StyledExternalLink = styled(ExternalLink)`
font-weight: 600;
`
interface TokenSafetyProps {
export interface TokenSafetyProps {
tokenAddress: string | null
secondTokenAddress?: string
onContinue: () => void
onCancel: () => void
onBlocked?: () => void
showCancel?: boolean
}
@@ -197,6 +200,7 @@ export default function TokenSafety({
secondTokenAddress,
onContinue,
onCancel,
onBlocked,
showCancel,
}: TokenSafetyProps) {
const logos = []

View File

@@ -1,12 +1,12 @@
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import { getChainInfoOrDefault } from 'constants/chainInfo'
import { formatToDecimal } from 'components/AmplitudeAnalytics/utils'
import { useToken } from 'hooks/Tokens'
import { useNetworkTokenBalances } from 'hooks/useNetworkTokenBalances'
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
import { useTokenBalance } from 'lib/hooks/useCurrencyBalance'
import { AlertTriangle } from 'react-feather'
import styled, { useTheme } from 'styled-components/macro'
import NetworkBalance from './NetworkBalance'
import styled from 'styled-components/macro'
const BalancesCard = styled.div`
width: 100%;
@@ -33,14 +33,9 @@ const ErrorText = styled.span`
display: flex;
flex-wrap: wrap;
`
const NetworkBalancesSection = styled.div`
height: fit-content;
`
const TotalBalanceSection = styled.div`
height: fit-content;
border-bottom: 1px solid ${({ theme }) => theme.backgroundOutline};
margin-bottom: 20px;
padding-bottom: 20px;
`
const TotalBalance = styled.div`
display: flex;
@@ -54,58 +49,35 @@ const TotalBalanceItem = styled.div`
display: flex;
`
export default function BalanceSummary({
address,
networkBalances,
totalBalance,
}: {
address: string
networkBalances: (JSX.Element | null)[] | null
totalBalance: number
}) {
const theme = useTheme()
const tokenSymbol = useToken(address)?.symbol
const { loading, error, data } = useNetworkTokenBalances({ address })
export default function BalanceSummary({ address }: { address: string }) {
const token = useToken(address)
const { loading, error } = useNetworkTokenBalances({ address })
const { chainId: connectedChainId } = useWeb3React()
const { account } = useWeb3React()
const balance = useTokenBalance(account, token ?? undefined)
const balanceNumber = balance ? formatToDecimal(balance, Math.min(balance.currency.decimals, 6)) : undefined
const balanceUsd = useStablecoinValue(balance)?.toFixed(2)
const balanceUsdNumber = balanceUsd ? parseFloat(balanceUsd) : undefined
const { label: connectedLabel, logoUrl: connectedLogoUrl } = getChainInfoOrDefault(connectedChainId)
const connectedFiatValue = 1
const multipleBalances = true // for testing purposes
if (loading) return null
if (loading || (!error && !balanceNumber && !balanceUsdNumber)) return null
return (
<BalancesCard>
{error ? (
<ErrorState>
<AlertTriangle size={24} />
<ErrorText>
<Trans>There was an error loading your {tokenSymbol} balance</Trans>
<Trans>There was an error loading your {token?.symbol} balance</Trans>
</ErrorText>
</ErrorState>
) : multipleBalances ? (
<>
<TotalBalanceSection>
Your balance across all networks
<TotalBalance>
<TotalBalanceItem>{`${totalBalance} ${tokenSymbol}`}</TotalBalanceItem>
<TotalBalanceItem>$4,210.12</TotalBalanceItem>
</TotalBalance>
</TotalBalanceSection>
<NetworkBalancesSection>Your balances by network</NetworkBalancesSection>
{data && networkBalances}
</>
) : (
<>
Your balance on {connectedLabel}
<NetworkBalance
logoUrl={connectedLogoUrl}
balance={'1'}
tokenSymbol={tokenSymbol ?? 'XXX'}
fiatValue={connectedFiatValue}
label={connectedLabel}
networkColor={theme.textPrimary}
/>
<TotalBalanceSection>
Your balance
<TotalBalance>
<TotalBalanceItem>{`${balanceNumber} ${token?.symbol}`}</TotalBalanceItem>
<TotalBalanceItem>{`$${balanceUsdNumber}`}</TotalBalanceItem>
</TotalBalance>
</TotalBalanceSection>
</>
)}
</BalancesCard>

View File

@@ -5,7 +5,7 @@ import { EventType } from '@visx/event/lib/types'
import { GlyphCircle } from '@visx/glyph'
import { Line } from '@visx/shape'
import { filterTimeAtom } from 'components/Tokens/state'
import { bisect, curveCardinal, NumberValue, scaleLinear } from 'd3'
import { bisect, curveCardinal, NumberValue, scaleLinear, timeDay, timeHour, timeMinute, timeMonth } from 'd3'
import { useTokenPriceQuery } from 'graphql/data/TokenPriceQuery'
import { TimePeriod } from 'graphql/data/TopTokenQuery'
import { useActiveLocale } from 'hooks/useActiveLocale'
@@ -117,35 +117,6 @@ const TimeButton = styled.button<{ active: boolean }>`
}
`
function getTicks(startTimestamp: number, endTimestamp: number, numTicks = 5) {
return Array.from(
{ length: numTicks },
(v, i) => endTimestamp - ((endTimestamp - startTimestamp) / (numTicks + 1)) * (i + 1)
)
}
function tickFormat(
startTimestamp: number,
endTimestamp: number,
timePeriod: TimePeriod,
locale: string
): [TickFormatter<NumberValue>, (v: number) => string, number[]] {
switch (timePeriod) {
case TimePeriod.HOUR:
return [hourFormatter(locale), dayHourFormatter(locale), getTicks(startTimestamp, endTimestamp)]
case TimePeriod.DAY:
return [hourFormatter(locale), dayHourFormatter(locale), getTicks(startTimestamp, endTimestamp)]
case TimePeriod.WEEK:
return [weekFormatter(locale), dayHourFormatter(locale), getTicks(startTimestamp, endTimestamp, 6)]
case TimePeriod.MONTH:
return [monthDayFormatter(locale), dayHourFormatter(locale), getTicks(startTimestamp, endTimestamp)]
case TimePeriod.YEAR:
return [monthTickFormatter(locale), monthYearDayFormatter(locale), getTicks(startTimestamp, endTimestamp)]
case TimePeriod.ALL:
return [monthYearFormatter(locale), monthYearDayFormatter(locale), getTicks(startTimestamp, endTimestamp)]
}
}
const margin = { top: 100, bottom: 48, crosshair: 72 }
const timeOptionsHeight = 44
const crosshairDateOverhang = 80
@@ -181,10 +152,58 @@ export function PriceChart({ width, height, token }: PriceChartProps) {
// Defining scales
// x scale
const timeScale = scaleLinear().domain([startingPrice.timestamp, endingPrice.timestamp]).range([0, width])
const timeScale = scaleLinear().domain([startingPrice.timestamp, endingPrice.timestamp]).range([0, width]).nice()
// y scale
const rdScale = scaleLinear().domain(getPriceBounds(pricePoints)).range([graphInnerHeight, 0])
function tickFormat(
startTimestamp: number,
endTimestamp: number,
timePeriod: TimePeriod,
locale: string
): [TickFormatter<NumberValue>, (v: number) => string, NumberValue[]] {
const startDate = new Date(startingPrice.timestamp.valueOf() * 1000)
const endDate = new Date(endingPrice.timestamp.valueOf() * 1000)
switch (timePeriod) {
case TimePeriod.HOUR:
return [
hourFormatter(locale),
dayHourFormatter(locale),
timeMinute.range(startDate, endDate, 10).map((x) => x.valueOf() / 1000),
]
case TimePeriod.DAY:
return [
hourFormatter(locale),
dayHourFormatter(locale),
timeHour.range(startDate, endDate, 4).map((x) => x.valueOf() / 1000),
]
case TimePeriod.WEEK:
return [
weekFormatter(locale),
dayHourFormatter(locale),
timeDay.range(startDate, endDate, 1).map((x) => x.valueOf() / 1000),
]
case TimePeriod.MONTH:
return [
monthDayFormatter(locale),
dayHourFormatter(locale),
timeDay.range(startDate, endDate, 7).map((x) => x.valueOf() / 1000),
]
case TimePeriod.YEAR:
return [
monthTickFormatter(locale),
monthYearDayFormatter(locale),
timeMonth.range(startDate, endDate, 2).map((x) => x.valueOf() / 1000),
]
case TimePeriod.ALL:
return [
monthYearFormatter(locale),
monthYearDayFormatter(locale),
timeMonth.range(startDate, endDate, 3).map((x) => x.valueOf() / 1000),
]
}
}
const handleHover = useCallback(
(event: Element | EventType) => {
const { x } = localPoint(event) || { x: 0 }

View File

@@ -4,24 +4,22 @@ import { useWeb3React } from '@web3-react/core'
import CurrencyLogo from 'components/CurrencyLogo'
import PriceChart from 'components/Tokens/TokenDetails/PriceChart'
import { VerifiedIcon } from 'components/TokenSafety/TokenSafetyIcon'
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
import { getChainInfo } from 'constants/chainInfo'
import { nativeOnChain, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import { checkWarning, WARNING_LEVEL } from 'constants/tokenSafety'
import { checkWarning } from 'constants/tokenSafety'
import { chainIdToChainName, useTokenDetailQuery } from 'graphql/data/TokenDetailQuery'
import { useCurrency, useIsUserAddedToken, useToken } from 'hooks/Tokens'
import { useCurrency, useToken } from 'hooks/Tokens'
import { useAtomValue } from 'jotai/utils'
import { darken } from 'polished'
import { Suspense, useCallback } from 'react'
import { Suspense } from 'react'
import { useState } from 'react'
import { ArrowLeft, Heart } from 'react-feather'
import { useNavigate } from 'react-router-dom'
import { ArrowLeft } from 'react-feather'
import styled from 'styled-components/macro'
import { ClickableStyle, CopyContractAddress } from 'theme'
import { CopyContractAddress } from 'theme'
import { formatDollarAmount } from 'utils/formatDollarAmt'
import { favoritesAtom, filterNetworkAtom, useToggleFavorite } from '../state'
import { ClickFavorited } from '../TokenTable/TokenRow'
import { filterNetworkAtom, useIsFavorited, useToggleFavorite } from '../state'
import { ClickFavorited, FavoriteIcon } from '../TokenTable/TokenRow'
import LoadingTokenDetail from './LoadingTokenDetail'
import Resource from './Resource'
import ShareButton from './ShareButton'
@@ -81,13 +79,7 @@ const NetworkBadge = styled.div<{ networkColor?: string; backgroundColor?: strin
color: ${({ theme, networkColor }) => networkColor ?? theme.textPrimary};
background-color: ${({ theme, backgroundColor }) => backgroundColor ?? theme.backgroundSurface};
`
const FavoriteIcon = styled(Heart)<{ isFavorited: boolean }>`
${ClickableStyle}
height: 22px;
width: 24px;
color: ${({ isFavorited, theme }) => (isFavorited ? theme.accentAction : theme.textSecondary)};
fill: ${({ isFavorited, theme }) => (isFavorited ? theme.accentAction : 'transparent')};
`
const NoInfoAvailable = styled.span`
color: ${({ theme }) => theme.textTertiary};
font-weight: 400;
@@ -181,21 +173,9 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
const { chainId: connectedChainId } = useWeb3React()
const token = useToken(address)
let currency = useCurrency(address)
const favoriteTokens = useAtomValue<string[]>(favoritesAtom)
const isFavorited = favoriteTokens.includes(address)
const isFavorited = useIsFavorited(address)
const toggleFavorite = useToggleFavorite(address)
const warning = checkWarning(address)
const navigate = useNavigate()
const isUserAddedToken = useIsUserAddedToken(token)
const [warningModalOpen, setWarningModalOpen] = useState(!!warning && !isUserAddedToken)
const handleDismissWarning = useCallback(() => {
setWarningModalOpen(false)
}, [setWarningModalOpen])
const handleCancel = useCallback(() => {
setWarningModalOpen(false)
warning && warning.level === WARNING_LEVEL.BLOCKED && navigate(-1)
}, [setWarningModalOpen, navigate, warning])
const chainInfo = getChainInfo(token?.chainId)
const networkLabel = chainInfo?.label
const networkBadgebackgroundColor = chainInfo?.backgroundColor
@@ -293,12 +273,6 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
</ContractAddress>
</Contract>
</ContractAddressSection>
<TokenSafetyModal
isOpen={warningModalOpen}
tokenAddress={address}
onCancel={handleCancel}
onContinue={handleDismissWarning}
/>
</TopArea>
</Suspense>
)

View File

@@ -16,7 +16,7 @@ const StyledFavoriteButton = styled.button<{ active: boolean }>`
padding: 0px 16px;
border-radius: 12px;
background-color: ${({ theme, active }) => (active ? theme.accentActiveSoft : theme.backgroundInteractive)};
border: none;
border: ${({ active, theme }) => (active ? `1px solid ${theme.accentActive}` : 'none')};
color: ${({ theme, active }) => (active ? theme.accentActive : theme.textPrimary)};
font-size: 16px;
font-weight: 600;
@@ -24,6 +24,7 @@ const StyledFavoriteButton = styled.button<{ active: boolean }>`
:hover {
background-color: ${({ theme, active }) => !active && theme.backgroundModule};
opacity: ${({ active }) => (active ? '60%' : '100%')};
}
`
const FavoriteText = styled.span`
@@ -38,11 +39,7 @@ export default function FavoriteButton() {
return (
<StyledFavoriteButton onClick={() => setShowFavorites(!showFavorites)} active={showFavorites}>
<FavoriteButtonContent>
<Heart
size={17}
color={showFavorites ? theme.accentActive : theme.textPrimary}
fill={showFavorites ? theme.accentActive : 'transparent'}
/>
<Heart size={17} color={showFavorites ? theme.accentActive : theme.textPrimary} />
<FavoriteText>
<Trans>Favorites</Trans>
</FavoriteText>

View File

@@ -1,7 +1,9 @@
import { Trans } from '@lingui/macro'
import searchIcon from 'assets/svg/search.svg'
import xIcon from 'assets/svg/x.svg'
import { useAtom } from 'jotai'
import useDebounce from 'hooks/useDebounce'
import { useUpdateAtom } from 'jotai/utils'
import { useEffect, useState } from 'react'
import styled from 'styled-components/macro'
import { MEDIUM_MEDIA_BREAKPOINT } from '../constants'
@@ -56,7 +58,14 @@ const SearchInput = styled.input`
`
export default function SearchBar() {
const [filterString, setFilterString] = useAtom(filterStringAtom)
const [localFilterString, setLocalFilterString] = useState('')
const setFilterString = useUpdateAtom(filterStringAtom)
const debouncedLocalFilterString = useDebounce(localFilterString, 300)
useEffect(() => {
setFilterString(debouncedLocalFilterString)
}, [debouncedLocalFilterString, setFilterString])
return (
<SearchBarContainer>
<Trans
@@ -66,8 +75,8 @@ export default function SearchBar() {
placeholder={`${translation}`}
id="searchBar"
autoComplete="off"
value={filterString}
onChange={({ target: { value } }) => setFilterString(value)}
value={localFilterString}
onChange={({ target: { value } }) => setLocalFilterString(value)}
/>
)}
>

View File

@@ -7,12 +7,12 @@ import CurrencyLogo from 'components/CurrencyLogo'
import { getChainInfo } from 'constants/chainInfo'
import { TimePeriod, TokenData } from 'graphql/data/TopTokenQuery'
import { useCurrency } from 'hooks/Tokens'
import { useAtom } from 'jotai'
import { useAtomValue } from 'jotai/utils'
import { ReactNode } from 'react'
import { ArrowDown, ArrowUp, Heart } from 'react-feather'
import { Link } from 'react-router-dom'
import styled, { css, useTheme } from 'styled-components/macro'
import { ClickableStyle } from 'theme'
import { formatDollarAmount } from 'utils/formatDollarAmt'
import {
@@ -23,12 +23,12 @@ import {
} from '../constants'
import { LoadingBubble } from '../loading'
import {
favoritesAtom,
filterNetworkAtom,
filterStringAtom,
filterTimeAtom,
sortCategoryAtom,
sortDirectionAtom,
useIsFavorited,
useSetSortCategory,
useToggleFavorite,
} from '../state'
@@ -109,6 +109,14 @@ export const ClickFavorited = styled.span`
}
`
export const FavoriteIcon = styled(Heart)<{ isFavorited: boolean }>`
${ClickableStyle}
height: 22px;
width: 24px;
color: ${({ isFavorited, theme }) => (isFavorited ? theme.accentAction : theme.textSecondary)};
fill: ${({ isFavorited, theme }) => (isFavorited ? theme.accentAction : 'transparent')};
`
const ClickableContent = styled.div`
display: flex;
text-decoration: none;
@@ -461,9 +469,7 @@ export default function LoadedRow({
const currency = useCurrency(tokenAddress)
const tokenName = tokenData.name
const tokenSymbol = tokenData.symbol
const theme = useTheme()
const [favoriteTokens] = useAtom(favoritesAtom)
const isFavorited = favoriteTokens.includes(tokenAddress)
const isFavorited = useIsFavorited(tokenAddress)
const toggleFavorite = useToggleFavorite(tokenAddress)
const filterString = useAtomValue(filterStringAtom)
const filterNetwork = useAtomValue(filterNetworkAtom)
@@ -482,7 +488,6 @@ export default function LoadedRow({
search_token_address_input: filterString,
}
const heartColor = isFavorited ? theme.accentActive : undefined
// TODO: currency logo sizing mobile (32px) vs. desktop (24px)
return (
<StyledLink
@@ -498,7 +503,7 @@ export default function LoadedRow({
toggleFavorite()
}}
>
<Heart size={18} color={heartColor} fill={heartColor} />
<FavoriteIcon isFavorited={isFavorited} />
</ClickFavorited>
}
listNumber={tokenListIndex + 1}

View File

@@ -1,8 +1,8 @@
import { SupportedChainId } from 'constants/chains'
import { TimePeriod } from 'graphql/data/TopTokenQuery'
import { atom, useAtom } from 'jotai'
import { atomWithReset, atomWithStorage } from 'jotai/utils'
import { useCallback } from 'react'
import { atomWithReset, atomWithStorage, useAtomValue } from 'jotai/utils'
import { useCallback, useMemo } from 'react'
import { Category, SortDirection } from './types'
@@ -20,12 +20,12 @@ export function useToggleFavorite(tokenAddress: string) {
return useCallback(() => {
let updatedFavoriteTokens
if (favoriteTokens.includes(tokenAddress)) {
if (favoriteTokens.includes(tokenAddress.toLocaleLowerCase())) {
updatedFavoriteTokens = favoriteTokens.filter((address: string) => {
return address !== tokenAddress
return address !== tokenAddress.toLocaleLowerCase()
})
} else {
updatedFavoriteTokens = [...favoriteTokens, tokenAddress]
updatedFavoriteTokens = [...favoriteTokens, tokenAddress.toLocaleLowerCase()]
}
updateFavoriteTokens(updatedFavoriteTokens)
}, [favoriteTokens, tokenAddress, updateFavoriteTokens])
@@ -47,3 +47,9 @@ export function useSetSortCategory(category: Category) {
}
}, [category, sortCategory, setSortCategory, sortDirection, setDirectionCategory])
}
export function useIsFavorited(tokenAddress: string) {
const favoritedTokens = useAtomValue<string[]>(favoritesAtom)
return useMemo(() => favoritedTokens.includes(tokenAddress.toLocaleLowerCase()), [favoritedTokens, tokenAddress])
}

View File

@@ -132,7 +132,12 @@ const AuthenticatedHeader = () => {
<IconContainer>
<IconButton onClick={copy} Icon={Copy} text={isCopied ? <Trans>Copied!</Trans> : <Trans>Copy</Trans>} />
<IconButton href={`${explorer}address/${account}`} Icon={ExternalLink} text={<Trans>Explore</Trans>} />
<IconButton onClick={disconnect} Icon={Power} text={<Trans>Disconnect</Trans>} />
<IconButton
dataTestId="wallet-disconnect"
onClick={disconnect}
Icon={Power}
text={<Trans>Disconnect</Trans>}
/>
</IconContainer>
</HeaderWrapper>
<Column>

View File

@@ -114,11 +114,13 @@ const WalletDropdown = ({ setMenu }: { setMenu: (state: MenuState) => void }) =>
{isAuthenticated ? (
<AuthenticatedHeader />
) : (
<ConnectButton onClick={toggleWalletModal}>Connect wallet</ConnectButton>
<ConnectButton data-testid="wallet-connect-wallet" onClick={toggleWalletModal}>
Connect wallet
</ConnectButton>
)}
<Divider />
{isAuthenticated && (
<ToggleMenuItem onClick={() => setMenu(MenuState.TRANSACTIONS)}>
<ToggleMenuItem data-testid="wallet-transactions" onClick={() => setMenu(MenuState.TRANSACTIONS)}>
<DefaultText>
<Trans>Transactions</Trans>{' '}
{pendingTransactions.length > 0 && (
@@ -132,7 +134,7 @@ const WalletDropdown = ({ setMenu }: { setMenu: (state: MenuState) => void }) =>
</IconWrap>
</ToggleMenuItem>
)}
<ToggleMenuItem onClick={() => setMenu(MenuState.LANGUAGE)}>
<ToggleMenuItem data-testid="wallet-select-language" onClick={() => setMenu(MenuState.LANGUAGE)}>
<DefaultText>
<Trans>Language</Trans>
</DefaultText>
@@ -145,7 +147,7 @@ const WalletDropdown = ({ setMenu }: { setMenu: (state: MenuState) => void }) =>
</IconWrap>
</FlexContainer>
</ToggleMenuItem>
<ToggleMenuItem onClick={toggleDarkMode}>
<ToggleMenuItem data-testid="wallet-select-theme" onClick={toggleDarkMode}>
<DefaultText>{darkMode ? <Trans> Light theme</Trans> : <Trans>Dark theme</Trans>}</DefaultText>
<IconWrap>{darkMode ? <Sun size={16} /> : <Moon size={16} />}</IconWrap>
</ToggleMenuItem>

View File

@@ -64,18 +64,19 @@ interface IconButtonProps {
Icon: Icon
onClick?: () => void
href?: string
dataTestId?: string
}
const IconButton = ({ Icon, onClick, text, href }: IconButtonProps) => {
const IconButton = ({ Icon, onClick, text, href, dataTestId }: IconButtonProps) => {
return href ? (
<IconBlockLink href={href} target="_blank">
<IconBlockLink data-testId={dataTestId} href={href} target="_blank">
<IconWrapper>
<Icon strokeWidth={1.5} size={16} />
<IconHoverText>{text}</IconHoverText>
</IconWrapper>
</IconBlockLink>
) : (
<IconBlockButton onClick={onClick}>
<IconBlockButton data-testId={dataTestId} onClick={onClick}>
<IconWrapper>
<Icon strokeWidth={1.5} size={16} />
<IconHoverText>{text}</IconHoverText>

View File

@@ -45,7 +45,7 @@ function LanguageMenuItem({ locale, isActive }: { locale: SupportedLocale; isAct
return (
<InternalLinkMenuItem onClick={onClick} to={to}>
<Text fontSize={16} fontWeight={400} lineHeight="24px">
<Text data-testid="wallet-language-item" fontSize={16} fontWeight={400} lineHeight="24px">
{LOCALE_LABEL[locale]}
</Text>
{isActive && <Check color={theme.accentActive} opacity={1} size={20} />}

View File

@@ -101,8 +101,8 @@ export const SlideOutMenu = ({
<Menu>
<BackSection>
<BackSectionContainer>
<StyledChevron onClick={onClose} size={24} />
<Header>{title}</Header>
<StyledChevron data-testid="wallet-back" onClick={onClose} size={24} />
<Header data-testid="wallet-header">{title}</Header>
{onClear && <ClearAll onClick={onClear}>Clear All</ClearAll>}
</BackSectionContainer>
</BackSection>

View File

@@ -158,7 +158,7 @@ export const TransactionHistoryMenu = ({ onClose }: { onClose: () => void }) =>
))}
</>
) : (
<EmptyTransaction>
<EmptyTransaction data-testid="wallet-empty-transaction-text">
<Trans>Your transactions will appear here</Trans>
</EmptyTransaction>
)}

View File

@@ -37,8 +37,8 @@ export enum MenuState {
}
const WalletDropdownWrapper = styled.div`
position: absolute;
top: 65px;
position: fixed;
top: 72px;
right: 20px;
z-index: ${Z_INDEX.dropdown};

View File

@@ -333,7 +333,7 @@ export default function WalletModal({
return (
<UpperSection>
<CloseIcon onClick={toggleWalletModal}>
<CloseIcon data-testid="wallet-modal-close" onClick={toggleWalletModal}>
<CloseColor />
</CloseIcon>
{headerRow}
@@ -363,7 +363,9 @@ export default function WalletModal({
maxHeight={90}
redesignFlag={redesignFlagEnabled}
>
<Wrapper redesignFlag={redesignFlagEnabled}>{getModalContent()}</Wrapper>
<Wrapper data-testid="wallet-modal" redesignFlag={redesignFlagEnabled}>
{getModalContent()}
</Wrapper>
</Modal>
)
}

View File

@@ -218,7 +218,7 @@ function Web3StatusInner() {
} else if (account) {
return (
<Web3StatusConnected data-testid="web3-status-connected" onClick={toggleWallet} pending={hasPendingTransactions}>
{navbarFlagEnabled && !hasPendingTransactions && <StatusIcon connectionType={connectionType} />}
{navbarFlagEnabled && !hasPendingTransactions && <StatusIcon size={24} connectionType={connectionType} />}
{hasPendingTransactions ? (
<RowBetween>
<Text>
@@ -252,14 +252,22 @@ function Web3StatusInner() {
>
{navbarFlagEnabled ? (
<Web3StatusConnectNavbar faded={!account}>
<StyledConnect onClick={toggleWalletModal}>
<StyledConnect data-testid="navbar-connect-wallet" onClick={toggleWalletModal}>
<Trans>Connect</Trans>
</StyledConnect>
<VerticalDivider />
{walletIsOpen ? (
<StyledChevronUp customColor={theme.accentAction} onClick={toggleWalletDropdown} />
<StyledChevronUp
data-testid="navbar-wallet-dropdown"
customColor={theme.accentAction}
onClick={toggleWalletDropdown}
/>
) : (
<StyledChevronDown customColor={theme.accentAction} onClick={toggleWalletDropdown} />
<StyledChevronDown
data-testid="navbar-wallet-dropdown"
customColor={theme.accentAction}
onClick={toggleWalletDropdown}
/>
)}
</Web3StatusConnectNavbar>
) : (

View File

@@ -1,4 +1,4 @@
import { Currency, SwapWidget } from '@uniswap/widgets'
import { Currency, OnReviewSwapClick, SwapWidget } from '@uniswap/widgets'
import { useWeb3React } from '@web3-react/core'
import { RPC_URLS } from 'constants/networks'
import { useActiveLocale } from 'hooks/useActiveLocale'
@@ -16,9 +16,10 @@ const WIDGET_ROUTER_URL = 'https://api.uniswap.org/v1/'
export interface WidgetProps {
defaultToken?: Currency
onReviewSwapClick?: OnReviewSwapClick
}
export default function Widget({ defaultToken }: WidgetProps) {
export default function Widget({ defaultToken, onReviewSwapClick }: WidgetProps) {
const locale = useActiveLocale()
const darkMode = useIsDarkMode()
const theme = useMemo(() => (darkMode ? DARK_THEME : LIGHT_THEME), [darkMode])
@@ -38,6 +39,7 @@ export default function Widget({ defaultToken }: WidgetProps) {
width={WIDGET_WIDTH}
locale={locale}
theme={theme}
onReviewSwapClick={onReviewSwapClick}
// defaultChainId is excluded - it is always inferred from the passed provider
provider={provider}
{...inputs}

View File

@@ -8,7 +8,7 @@ import { WalletConnect } from '@web3-react/walletconnect'
import { SupportedChainId } from 'constants/chains'
import UNISWAP_LOGO_URL from '../assets/svg/logo.svg'
import { RPC_URLS } from '../constants/networks'
import { RPC_PROVIDERS, RPC_URLS } from '../constants/networks'
export enum ConnectionType {
INJECTED = 'INJECTED',
@@ -29,7 +29,7 @@ function onError(error: Error) {
}
const [web3Network, web3NetworkHooks] = initializeConnector<Network>(
(actions) => new Network({ actions, urlMap: RPC_URLS, defaultChainId: 1 })
(actions) => new Network({ actions, urlMap: RPC_PROVIDERS, defaultChainId: 1 })
)
export const networkConnection: Connection = {
connector: web3Network,

View File

@@ -1,4 +1,4 @@
import { JsonRpcProvider } from '@ethersproject/providers'
import { StaticJsonRpcProvider } from '@ethersproject/providers'
import { SupportedChainId } from './chains'
@@ -7,8 +7,6 @@ if (typeof INFURA_KEY === 'undefined') {
throw new Error(`REACT_APP_INFURA_KEY must be a defined environment variable`)
}
export const MAINNET_PROVIDER = new JsonRpcProvider(`https://mainnet.infura.io/v3/${INFURA_KEY}`)
/**
* These are the network URLs used by the interface when there is not another available source of chain data
*/
@@ -27,3 +25,19 @@ export const RPC_URLS: { [key in SupportedChainId]: string } = {
[SupportedChainId.CELO]: `https://forno.celo.org`,
[SupportedChainId.CELO_ALFAJORES]: `https://alfajores-forno.celo-testnet.org`,
}
export const RPC_PROVIDERS: { [key in SupportedChainId]: StaticJsonRpcProvider } = {
[SupportedChainId.MAINNET]: new StaticJsonRpcProvider(RPC_URLS[SupportedChainId.MAINNET]),
[SupportedChainId.RINKEBY]: new StaticJsonRpcProvider(RPC_URLS[SupportedChainId.RINKEBY]),
[SupportedChainId.ROPSTEN]: new StaticJsonRpcProvider(RPC_URLS[SupportedChainId.ROPSTEN]),
[SupportedChainId.GOERLI]: new StaticJsonRpcProvider(RPC_URLS[SupportedChainId.GOERLI]),
[SupportedChainId.KOVAN]: new StaticJsonRpcProvider(RPC_URLS[SupportedChainId.KOVAN]),
[SupportedChainId.OPTIMISM]: new StaticJsonRpcProvider(RPC_URLS[SupportedChainId.OPTIMISM]),
[SupportedChainId.OPTIMISTIC_KOVAN]: new StaticJsonRpcProvider(RPC_URLS[SupportedChainId.OPTIMISTIC_KOVAN]),
[SupportedChainId.ARBITRUM_ONE]: new StaticJsonRpcProvider(RPC_URLS[SupportedChainId.ARBITRUM_ONE]),
[SupportedChainId.ARBITRUM_RINKEBY]: new StaticJsonRpcProvider(RPC_URLS[SupportedChainId.ARBITRUM_RINKEBY]),
[SupportedChainId.POLYGON]: new StaticJsonRpcProvider(RPC_URLS[SupportedChainId.POLYGON]),
[SupportedChainId.POLYGON_MUMBAI]: new StaticJsonRpcProvider(RPC_URLS[SupportedChainId.POLYGON_MUMBAI]),
[SupportedChainId.CELO]: new StaticJsonRpcProvider(RPC_URLS[SupportedChainId.CELO]),
[SupportedChainId.CELO_ALFAJORES]: new StaticJsonRpcProvider(RPC_URLS[SupportedChainId.CELO_ALFAJORES]),
}

View File

@@ -0,0 +1,8 @@
export enum FeatureFlag {
navBar = 'navBar',
nft = 'nfts',
redesign = 'redesign',
tokens = 'tokens',
tokensNetworkFilter = 'tokensNetworkFilter',
tokenSafety = 'tokenSafety',
}

View File

@@ -1,6 +1,7 @@
import { useAtom } from 'jotai'
import { atomWithStorage, useAtomValue } from 'jotai/utils'
import { createContext, ReactNode, useCallback, useContext } from 'react'
export { FeatureFlag } from './flags/featureFlags'
interface FeatureFlagsContextType {
isLoaded: boolean
@@ -53,16 +54,6 @@ export enum BaseVariant {
Enabled = 'enabled',
}
export enum FeatureFlag {
navBar = 'navBar',
wallet = 'wallet',
nft = 'nfts',
redesign = 'redesign',
tokens = 'tokens',
tokensNetworkFilter = 'tokensNetworkFilter',
tokenSafety = 'tokenSafety',
}
export function useBaseFlag(flag: string): BaseVariant {
switch (useFeatureFlagsContext().flags[flag]) {
case 'enabled':

View File

@@ -1,7 +1,9 @@
import { renderHook } from '@testing-library/react'
import { CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { DAI, USDC_MAINNET } from 'constants/tokens'
import { RouterPreference } from 'state/routing/slice'
import { TradeState } from 'state/routing/types'
import { useClientSideRouter } from 'state/user/hooks'
import { useRoutingAPITrade } from '../state/routing/useRoutingAPITrade'
import useAutoRouterSupported from './useAutoRouterSupported'
@@ -25,6 +27,7 @@ const mockUseAutoRouterSupported = useAutoRouterSupported as jest.MockedFunction
const mockUseIsWindowVisible = useIsWindowVisible as jest.MockedFunction<typeof useIsWindowVisible>
const mockUseRoutingAPITrade = useRoutingAPITrade as jest.MockedFunction<typeof useRoutingAPITrade>
const mockUseClientSideRouter = useClientSideRouter as jest.MockedFunction<typeof useClientSideRouter>
const mockUseClientSideV3Trade = useClientSideV3Trade as jest.MockedFunction<typeof useClientSideV3Trade>
// helpers to set mock expectations
@@ -42,6 +45,7 @@ beforeEach(() => {
mockUseIsWindowVisible.mockReturnValue(true)
mockUseAutoRouterSupported.mockReturnValue(true)
mockUseClientSideRouter.mockReturnValue([true, () => undefined])
})
describe('#useBestV3Trade ExactIn', () => {
@@ -52,7 +56,7 @@ describe('#useBestV3Trade ExactIn', () => {
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI))
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, DAI)
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, DAI, RouterPreference.CLIENT)
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, USDCAmount, DAI)
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
})
@@ -64,7 +68,7 @@ describe('#useBestV3Trade ExactIn', () => {
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI))
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, DAI)
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, DAI, RouterPreference.CLIENT)
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, USDCAmount, DAI)
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
})
@@ -128,7 +132,12 @@ describe('#useBestV3Trade ExactOut', () => {
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, USDC_MAINNET)
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(
TradeType.EXACT_OUTPUT,
undefined,
USDC_MAINNET,
RouterPreference.CLIENT
)
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET)
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
})
@@ -140,7 +149,12 @@ describe('#useBestV3Trade ExactOut', () => {
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, USDC_MAINNET)
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(
TradeType.EXACT_OUTPUT,
undefined,
USDC_MAINNET,
RouterPreference.CLIENT
)
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET)
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
})

View File

@@ -1,7 +1,9 @@
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { useMemo } from 'react'
import { RouterPreference } from 'state/routing/slice'
import { InterfaceTrade, TradeState } from 'state/routing/types'
import { useRoutingAPITrade } from 'state/routing/useRoutingAPITrade'
import { useClientSideRouter } from 'state/user/hooks'
import useAutoRouterSupported from './useAutoRouterSupported'
import { useClientSideV3Trade } from './useClientSideV3Trade'
@@ -30,10 +32,12 @@ export function useBestTrade(
200
)
const [clientSideRouter] = useClientSideRouter()
const routingAPITrade = useRoutingAPITrade(
tradeType,
autoRouterSupported && isWindowVisible ? debouncedAmount : undefined,
debouncedOtherCurrency
debouncedOtherCurrency,
clientSideRouter ? RouterPreference.CLIENT : RouterPreference.API
)
const isLoading = routingAPITrade.state === TradeState.LOADING

View File

@@ -1,6 +1,7 @@
import { nanoid } from '@reduxjs/toolkit'
import { TokenList } from '@uniswap/token-lists'
import { MAINNET_PROVIDER } from 'constants/networks'
import { SupportedChainId } from 'constants/chains'
import { RPC_PROVIDERS } from 'constants/networks'
import getTokenList from 'lib/hooks/useTokenList/fetchTokenList'
import resolveENSContentHash from 'lib/utils/resolveENSContentHash'
import { useCallback } from 'react'
@@ -16,7 +17,9 @@ export function useFetchListCallback(): (listUrl: string, sendDispatch?: boolean
async (listUrl: string, sendDispatch = true) => {
const requestId = nanoid()
sendDispatch && dispatch(fetchTokenList.pending({ requestId, url: listUrl }))
return getTokenList(listUrl, (ensName: string) => resolveENSContentHash(ensName, MAINNET_PROVIDER))
return getTokenList(listUrl, (ensName: string) =>
resolveENSContentHash(ensName, RPC_PROVIDERS[SupportedChainId.MAINNET])
)
.then((tokenList) => {
sendDispatch && dispatch(fetchTokenList.fulfilled({ url: listUrl, tokenList, requestId }))
return tokenList

View File

@@ -2,7 +2,8 @@ import { Currency, CurrencyAmount, Price, Token, TradeType } from '@uniswap/sdk-
import { useWeb3React } from '@web3-react/core'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { useMemo, useRef } from 'react'
import { RouterPreference, useRoutingAPITrade } from 'state/routing/useRoutingAPITrade'
import { RouterPreference } from 'state/routing/slice'
import { useRoutingAPITrade } from 'state/routing/useRoutingAPITrade'
import { SupportedChainId } from '../constants/chains'
import { CUSD_CELO, DAI_OPTIMISM, USDC_ARBITRUM, USDC_MAINNET, USDC_POLYGON } from '../constants/tokens'
@@ -27,7 +28,7 @@ export default function useStablecoinPrice(currency?: Currency): Price<Currency,
const amountOut = chainId ? STABLECOIN_AMOUNT_OUT[chainId] : undefined
const stablecoin = amountOut?.currency
const { trade } = useRoutingAPITrade(TradeType.EXACT_OUTPUT, amountOut, currency, RouterPreference.CLIENT)
const { trade } = useRoutingAPITrade(TradeType.EXACT_OUTPUT, amountOut, currency, RouterPreference.PRICE)
const price = useMemo(() => {
if (!currency || !stablecoin) {
return undefined

View File

@@ -1,9 +1,8 @@
import { useWeb3React } from '@web3-react/core'
import { CHAIN_IDS_TO_NAMES, SupportedChainId } from 'constants/chains'
import { CHAIN_IDS_TO_NAMES } from 'constants/chains'
import { ParsedQs } from 'qs'
import { useCallback, useEffect, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { replaceURLParam } from 'utils/routes'
import { useEffect, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import useParsedQueryString from './useParsedQueryString'
import usePrevious from './usePrevious'
@@ -22,15 +21,8 @@ function getParsedChainId(parsedQs?: ParsedQs) {
return getChainIdFromName(chain)
}
function getChainNameFromId(id: string | number) {
// casting here may not be right but fine to return undefined if it's not a supported chain ID
return CHAIN_IDS_TO_NAMES[id as SupportedChainId] || ''
}
export default function useSyncChainQuery() {
const { chainId, isActive } = useWeb3React()
const navigate = useNavigate()
const { search } = useLocation()
const parsedQs = useParsedQueryString()
const urlChainId = getParsedChainId(parsedQs)
@@ -46,35 +38,16 @@ export default function useSyncChainQuery() {
}
}, [chainId, previousChainId])
const replaceURLChainParam = useCallback(() => {
if (chainId) {
navigate({ search: replaceURLParam(search, 'chain', getChainNameFromId(chainId)) }, { replace: true })
}
}, [chainId, search, navigate])
const [searchParams, setSearchParams] = useSearchParams()
const chainQueryUnpopulated = !urlChainId && chainId
const chainChanged = chainId !== previousChainId
const chainQueryStale = urlChainId !== chainId
const chainQueryManuallyUpdated = urlChainId && urlChainId !== previousUrlChainId && isActive
return useEffect(() => {
if (chainQueryUnpopulated) {
// If there is no chain query param, set it to the current chain
replaceURLChainParam()
} else if (chainChanged && chainQueryStale) {
// If the chain changed but the query param is stale, update to the current chain
replaceURLChainParam()
} else if (chainQueryManuallyUpdated) {
if (chainQueryManuallyUpdated) {
// If the query param changed, and the chain didn't change, then activate the new chain
selectChain(urlChainId)
searchParams.delete('chain')
setSearchParams(searchParams)
}
}, [
chainQueryUnpopulated,
chainChanged,
chainQueryStale,
chainQueryManuallyUpdated,
urlChainId,
selectChain,
replaceURLChainParam,
])
}, [chainQueryManuallyUpdated, urlChainId, selectChain, searchParams, setSearchParams])
}

View File

@@ -1,7 +1,7 @@
import { BigintIsh, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
// This file is lazy-loaded, so the import of smart-order-router is intentional.
// eslint-disable-next-line no-restricted-imports
import { AlphaRouter, AlphaRouterConfig, AlphaRouterParams, ChainId } from '@uniswap/smart-order-router'
import { AlphaRouter, AlphaRouterConfig, ChainId } from '@uniswap/smart-order-router'
import { SupportedChainId } from 'constants/chains'
import JSBI from 'jsbi'
import { GetQuoteResult } from 'state/routing/types'
@@ -29,11 +29,9 @@ async function getQuote(
tokenOut: { address: string; chainId: number; decimals: number; symbol?: string }
amount: BigintIsh
},
routerParams: AlphaRouterParams,
routerConfig: Partial<AlphaRouterConfig>
router: AlphaRouter,
config: Partial<AlphaRouterConfig>
): Promise<{ data: GetQuoteResult; error?: unknown }> {
const router = new AlphaRouter(routerParams)
const currencyIn = new Token(tokenIn.chainId, tokenIn.address, tokenIn.decimals, tokenIn.symbol)
const currencyOut = new Token(tokenOut.chainId, tokenOut.address, tokenOut.decimals, tokenOut.symbol)
@@ -46,7 +44,7 @@ async function getQuote(
quoteCurrency,
type === 'exactIn' ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT,
/*swapConfig=*/ undefined,
routerConfig
config
)
if (!swapRoute) throw new Error('Failed to generate client side quote')
@@ -80,8 +78,8 @@ export async function getClientSideQuote(
amount,
type,
}: QuoteArguments,
routerParams: AlphaRouterParams,
routerConfig: Partial<AlphaRouterConfig>
router: AlphaRouter,
config: Partial<AlphaRouterConfig>
) {
return getQuote(
{
@@ -100,7 +98,7 @@ export async function getClientSideQuote(
},
amount,
},
routerParams,
routerConfig
router,
config
)
}

View File

@@ -1,5 +1,6 @@
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { useMemo } from 'react'
import { RouterPreference } from 'state/routing/slice'
/**
* Returns query arguments for the Routing API query or undefined if the
@@ -11,13 +12,13 @@ export function useRoutingAPIArguments({
tokenOut,
amount,
tradeType,
useClientSideRouter,
routerPreference,
}: {
tokenIn: Currency | undefined
tokenOut: Currency | undefined
amount: CurrencyAmount<Currency> | undefined
tradeType: TradeType
useClientSideRouter: boolean
routerPreference: RouterPreference
}) {
return useMemo(
() =>
@@ -33,9 +34,9 @@ export function useRoutingAPIArguments({
tokenOutChainId: tokenOut.wrapped.chainId,
tokenOutDecimals: tokenOut.wrapped.decimals,
tokenOutSymbol: tokenOut.wrapped.symbol,
useClientSideRouter,
routerPreference,
type: (tradeType === TradeType.EXACT_INPUT ? 'exactIn' : 'exactOut') as 'exactIn' | 'exactOut',
},
[amount, tokenIn, tokenOut, tradeType, useClientSideRouter]
[amount, routerPreference, tokenIn, tokenOut, tradeType]
)
}

View File

@@ -59,7 +59,6 @@ export function BlockNumberProvider({ children }: { children: ReactNode }) {
})
provider.on('block', onBlock)
return () => {
stale = true
provider.removeListener('block', onBlock)
@@ -69,11 +68,7 @@ export function BlockNumberProvider({ children }: { children: ReactNode }) {
return void 0
}, [activeChainId, provider, onBlock, setChainBlock, windowVisible])
const value = useMemo(
() => ({
value: chainId === activeChainId ? block : undefined,
}),
[activeChainId, block, chainId]
)
const blockValue = useMemo(() => (chainId === activeChainId ? block : undefined), [activeChainId, block, chainId])
const value = useMemo(() => ({ value: blockValue }), [blockValue])
return <BlockNumberContext.Provider value={value}>{children}</BlockNumberContext.Provider>
}

View File

@@ -0,0 +1,35 @@
import { badge, subheadSmall } from '../../css/common.css'
import { Box, BoxProps } from '../Box'
import { Row } from '../Flex'
import { VerifiedIcon } from '../icons'
export const CollectionProfile = ({
label,
isVerified,
name,
avatarUrl,
...props
}: {
isVerified?: boolean
label: string
name: string
avatarUrl: string
} & BoxProps) => {
return (
<Row {...props}>
{avatarUrl ? (
<Box as="img" src={avatarUrl} height="36" width="36" marginRight="12" borderRadius="round" />
) : (
<Box role="img" background="fallbackGradient" height="36" width="36" marginRight="12" borderRadius="round" />
)}
<div>
<Box as="span" color="darkGray" style={{ textTransform: 'uppercase' }} className={badge}>
{label}
</Box>
<Row marginTop="4" className={subheadSmall} color="blackBlue">
{name} {isVerified && <VerifiedIcon />}
</Row>
</div>
</Row>
)
}

View File

@@ -0,0 +1,71 @@
import { bodySmall } from '../../css/common.css'
import { shortenAddress } from '../../utils/address'
import { Box, BoxProps } from '../Box'
import { Column, Row } from '../Flex'
const DetailItemLabel = (props: BoxProps) => <Box as="span" fontSize="14" color="darkGray" {...props} />
const DetailItemValue = (props: BoxProps) => <Box as="span" fontSize="14" marginLeft="4" color="blackBlue" {...props} />
const Detail = (props: BoxProps) => (
<Row justifyContent="space-between" width="full" style={{ minWidth: '224px' }} {...props} />
)
export const Details = ({
contractAddress,
tokenId,
metadataUrl,
tokenType,
totalSupply,
blockchain,
}: {
contractAddress: string
tokenId: string
metadataUrl: string
tokenType: string
totalSupply: number
blockchain: string
}) => (
<Row gap={{ md: '32', sm: '16' }} width="full" justifyContent="space-between" alignItems="flex-start" flexWrap="wrap">
<Column width={{ sm: 'full', md: 'auto' }} gap="10">
<Detail>
<DetailItemLabel>Contract Address: </DetailItemLabel>
<a
href={`https://etherscan.io/token/${contractAddress}`}
target="_blank"
rel="noreferrer"
style={{ textDecoration: 'none' }}
>
<DetailItemValue>{shortenAddress(contractAddress)}</DetailItemValue>
</a>
</Detail>
<Detail>
<DetailItemLabel>Token ID:</DetailItemLabel>
<DetailItemValue className={bodySmall}>{tokenId}</DetailItemValue>
</Detail>
{metadataUrl ? (
<Detail>
<DetailItemLabel>Metadata:</DetailItemLabel>
<a href={metadataUrl} target="_blank" rel="noreferrer" style={{ textDecoration: 'none' }}>
<DetailItemValue>{metadataUrl.slice(0, 12)}...</DetailItemValue>
</a>
</Detail>
) : null}
</Column>
<Column width={{ sm: 'full', md: 'auto' }} gap="10">
<Detail>
<DetailItemLabel>Contract type:</DetailItemLabel>
<DetailItemValue>{tokenType}</DetailItemValue>
</Detail>
<Detail>
<DetailItemLabel>Total supply:</DetailItemLabel>
<DetailItemValue>{totalSupply}</DetailItemValue>
</Detail>
<Detail>
<DetailItemLabel>Blockchain:</DetailItemLabel>
<DetailItemValue>{blockchain}</DetailItemValue>
</Detail>
</Column>
</Row>
)

View File

@@ -0,0 +1,18 @@
import { style } from '@vanilla-extract/css'
import { sprinkles } from '../../css/sprinkles.css'
export const grid = style([
sprinkles({ gap: '16', display: 'grid' }),
{
gridTemplateColumns: 'repeat(4, 1fr)',
'@media': {
'(max-width: 1536px)': {
gridTemplateColumns: 'repeat(3, 1fr)',
},
'(max-width: 640px)': {
gridTemplateColumns: 'repeat(2, 1fr)',
},
},
},
])

View File

@@ -0,0 +1,75 @@
import qs from 'query-string'
import { badge } from '../../css/common.css'
import { Box } from '../Box'
import { Column } from '../Flex'
import * as styles from './Traits.css'
interface TraitProps {
label: string
value: string
}
const Trait: React.FC<TraitProps> = ({ label, value }: TraitProps) => (
<Column backgroundColor="lightGray" padding="16" gap="4" borderRadius="12">
<Box
as="span"
className={badge}
color="darkGray"
whiteSpace="nowrap"
overflow="hidden"
textOverflow="ellipsis"
style={{ textTransform: 'uppercase' }}
maxWidth={{ sm: '120', md: '160' }}
>
{label}
</Box>
<Box
as="span"
color="blackBlue"
fontSize="16"
fontWeight="normal"
whiteSpace="nowrap"
overflow="hidden"
textOverflow="ellipsis"
maxWidth={{ sm: '120', md: '160' }}
>
{value}
</Box>
</Column>
)
export const Traits = ({
traits,
collectionAddress,
}: {
traits: {
value: string
trait_type: string
}[]
collectionAddress: string
}) => (
<div className={styles.grid}>
{traits.length === 0
? 'No traits'
: traits.map((item) => {
const params = qs.stringify(
{ traits: [`("${item.trait_type}","${item.value}")`] },
{
arrayFormat: 'comma',
}
)
return (
<a
key={`${item.trait_type}-${item.value}`}
href={`#/nft/collection/${collectionAddress}?${params}`}
style={{ textDecoration: 'none' }}
>
<Trait label={item.trait_type} value={item.value} />
</a>
)
})}
</div>
)

View File

@@ -1,5 +1,38 @@
import { useQuery } from 'react-query'
import { useParams } from 'react-router-dom'
import { Details } from '../../components/details/Details'
import { fetchSingleAsset } from '../../queries'
import { CollectionInfoForAsset, GenieAsset } from '../../types'
const Asset = () => {
return <div>NFT Details Page</div>
const { tokenId = '', contractAddress = '' } = useParams()
const { data } = useQuery(['assetDetail', contractAddress, tokenId], () =>
fetchSingleAsset({ contractAddress, tokenId })
)
let asset = {} as GenieAsset
let collection = {} as CollectionInfoForAsset
if (data) {
asset = data[0] || {}
collection = data[1] || {}
}
return (
<div>
{' '}
<Details
contractAddress={contractAddress}
tokenId={tokenId}
tokenType={asset.tokenType}
blockchain="Ethereum"
metadataUrl={asset.externalLink}
totalSupply={collection.totalSupply}
/>
</div>
)
}
export default Asset

View File

@@ -1,7 +1,7 @@
import { FungibleToken } from '../../types'
export const fetchSearchTokens = async (tokenQuery: string): Promise<FungibleToken[]> => {
const url = `${process.env.REACT_APP_GENIE_V3_API_URL}/searchTokens?tokenQuery=${tokenQuery}`
const url = `${process.env.REACT_APP_TEMP_API_URL}/tokens/search?tokenQuery=${tokenQuery}`
const r = await fetch(url, {
method: 'GET',

View File

@@ -1,7 +1,7 @@
import { FungibleToken } from '../../types'
export const fetchTrendingTokens = async (numTokens?: number): Promise<FungibleToken[]> => {
const url = `${process.env.REACT_APP_GENIE_V3_API_URL}/tokens/trending${numTokens ? `?numTokens=${numTokens}` : ''}`
const url = `${process.env.REACT_APP_TEMP_API_URL}/tokens/trending${numTokens ? `?numTokens=${numTokens}` : ''}`
const r = await fetch(url, {
method: 'GET',

View File

@@ -12,6 +12,7 @@ import { lazy, Suspense, useEffect } from 'react'
import { Navigate, Route, Routes, useLocation } from 'react-router-dom'
import { useIsDarkMode } from 'state/user/hooks'
import styled from 'styled-components/macro'
import { Z_INDEX } from 'theme'
import { SpinnerSVG } from 'theme/components'
import { getBrowser } from 'utils/browser'
import { getCLS, getFCP, getFID, getLCP, Metric } from 'web-vitals'
@@ -73,7 +74,7 @@ const HeaderWrapper = styled.div`
justify-content: space-between;
position: fixed;
top: 0;
z-index: 2;
z-index: ${Z_INDEX.absoluteTop};
`
const Marginer = styled.div`

View File

@@ -11,14 +11,15 @@ import LoadingTokenDetail from 'components/Tokens/TokenDetails/LoadingTokenDetai
import NetworkBalance from 'components/Tokens/TokenDetails/NetworkBalance'
import TokenDetail from 'components/Tokens/TokenDetails/TokenDetail'
import TokenSafetyMessage from 'components/TokenSafety/TokenSafetyMessage'
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
import Widget, { WIDGET_WIDTH } from 'components/Widget'
import { getChainInfo } from 'constants/chainInfo'
import { L1_CHAIN_IDS, L2_CHAIN_IDS, SupportedChainId, TESTNET_CHAIN_IDS } from 'constants/chains'
import { checkWarning } from 'constants/tokenSafety'
import { useToken } from 'hooks/Tokens'
import { useIsUserAddedToken, useToken } from 'hooks/Tokens'
import { useNetworkTokenBalances } from 'hooks/useNetworkTokenBalances'
import { useMemo } from 'react'
import { Navigate, useLocation, useParams } from 'react-router-dom'
import { useCallback, useMemo, useState } from 'react'
import { Navigate, useLocation, useNavigate, useParams } from 'react-router-dom'
import styled from 'styled-components/macro'
const Footer = styled.div`
@@ -79,10 +80,28 @@ export default function TokenDetails() {
const location = useLocation()
const { tokenAddress } = useParams<{ tokenAddress?: string }>()
const token = useToken(tokenAddress)
const tokenWarning = tokenAddress ? checkWarning(tokenAddress) : null
const isBlockedToken = tokenWarning?.canProceed === false
const navigate = useNavigate()
const [continueSwap, setContinueSwap] = useState<{ resolve: (value: boolean | PromiseLike<boolean>) => void }>()
const shouldShowSpeedbump = !useIsUserAddedToken(token) && tokenWarning !== null
// Show token safety modal if Swap-reviewing a warning token, at all times if the current token is blocked
const onReviewSwap = useCallback(() => {
return new Promise<boolean>((resolve) => {
shouldShowSpeedbump ? setContinueSwap({ resolve }) : resolve(true)
})
}, [shouldShowSpeedbump])
const onResolveSwap = useCallback(
(value: boolean) => {
continueSwap?.resolve(value)
setContinueSwap(undefined)
},
[continueSwap, setContinueSwap]
)
const tokenWarning = token ? checkWarning(token.address) : null
/* network balance handling */
const { data: networkData } = NetworkBalances(token?.address)
const { chainId: connectedChainId } = useWeb3React()
const totalBalance = 4.3 // dummy data
@@ -128,9 +147,9 @@ export default function TokenDetails() {
<>
<TokenDetail address={token.address} />
<RightPanel>
<Widget defaultToken={token ?? undefined} />
<Widget defaultToken={token ?? undefined} onReviewSwapClick={onReviewSwap} />
{tokenWarning && <TokenSafetyMessage tokenAddress={token.address} warning={tokenWarning} />}
<BalanceSummary address={token.address} totalBalance={totalBalance} networkBalances={balancesByNetwork} />
<BalanceSummary address={token.address} />
</RightPanel>
<Footer>
<FooterBalanceSummary
@@ -139,6 +158,14 @@ export default function TokenDetails() {
networkBalances={balancesByNetwork}
/>
</Footer>
<TokenSafetyModal
isOpen={isBlockedToken || !!continueSwap}
tokenAddress={token.address}
onContinue={() => onResolveSwap(true)}
onBlocked={() => navigate(-1)}
onCancel={() => onResolveSwap(false)}
showCancel={true}
/>
</>
)}
</TokenDetailsLayout>

View File

@@ -87,8 +87,9 @@ const Tokens = () => {
</TitleContainer>
<FiltersWrapper>
<FiltersContainer>
{tokensNetworkFilterFlag === TokensNetworkFilterVariant.Enabled && <NetworkFilter />}
<FavoriteButton />
{tokensNetworkFilterFlag === TokensNetworkFilterVariant.Enabled && <NetworkFilter />}
<TimeSelector />
</FiltersContainer>
<SearchContainer>
@@ -116,8 +117,8 @@ export const LoadingTokens = () => {
</TitleContainer>
<FiltersWrapper>
<FiltersContainer>
{tokensNetworkFilterFlag === TokensNetworkFilterVariant.Enabled && <NetworkFilter />}
<FavoriteButton />
{tokensNetworkFilterFlag === TokensNetworkFilterVariant.Enabled && <NetworkFilter />}
<TimeSelector />
</FiltersContainer>
<SearchContainer>

View File

@@ -1,36 +1,67 @@
import { BaseProvider, JsonRpcProvider } from '@ethersproject/providers'
import { createApi, fetchBaseQuery, FetchBaseQueryError } from '@reduxjs/toolkit/query/react'
import { Protocol } from '@uniswap/router-sdk'
import { ChainId } from '@uniswap/smart-order-router'
import { RPC_URLS } from 'constants/networks'
import { AlphaRouter, ChainId } from '@uniswap/smart-order-router'
import { RPC_PROVIDERS } from 'constants/networks'
import { getClientSideQuote, toSupportedChainId } from 'lib/hooks/routing/clientSideSmartOrderRouter'
import ms from 'ms.macro'
import qs from 'qs'
import { GetQuoteResult } from './types'
const routerProviders = new Map<ChainId, BaseProvider>()
function getRouterProvider(chainId: ChainId): BaseProvider {
const provider = routerProviders.get(chainId)
if (provider) return provider
export enum RouterPreference {
API = 'api',
CLIENT = 'client',
PRICE = 'price',
}
const routers = new Map<ChainId, AlphaRouter>()
function getRouter(chainId: ChainId): AlphaRouter {
const router = routers.get(chainId)
if (router) return router
const supportedChainId = toSupportedChainId(chainId)
if (supportedChainId) {
const provider = new JsonRpcProvider(RPC_URLS[supportedChainId])
routerProviders.set(chainId, provider)
return provider
const provider = RPC_PROVIDERS[supportedChainId]
const router = new AlphaRouter({ chainId, provider })
routers.set(chainId, router)
return router
}
throw new Error(`Router does not support this chain (chainId: ${chainId}).`)
}
const protocols: Protocol[] = [Protocol.V2, Protocol.V3, Protocol.MIXED]
const DEFAULT_QUERY_PARAMS = {
protocols: protocols.map((p) => p.toLowerCase()).join(','),
// example other params
// forceCrossProtocol: 'true',
// minSplits: '5',
// routing API quote params: https://github.com/Uniswap/routing-api/blob/main/lib/handlers/quote/schema/quote-schema.ts
const API_QUERY_PARAMS = {
protocols: 'v2,v3,mixed',
}
const CLIENT_PARAMS = {
protocols: [Protocol.V2, Protocol.V3, Protocol.MIXED],
}
// Price queries are tuned down to minimize the required RPCs to respond to them.
// TODO(zzmp): This will be used after testing router caching.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const PRICE_PARAMS = {
protocols: [Protocol.V2, Protocol.V3],
v2PoolSelection: {
topN: 2,
topNDirectSwaps: 1,
topNTokenInOut: 2,
topNSecondHop: 1,
topNWithEachBaseToken: 2,
topNWithBaseToken: 2,
},
v3PoolSelection: {
topN: 2,
topNDirectSwaps: 1,
topNTokenInOut: 2,
topNSecondHop: 1,
topNWithEachBaseToken: 2,
topNWithBaseToken: 2,
},
maxSwapsPerPath: 2,
minSplits: 1,
maxSplits: 1,
distributionPercent: 100,
}
export const routingApi = createApi({
@@ -51,24 +82,20 @@ export const routingApi = createApi({
tokenOutDecimals: number
tokenOutSymbol?: string
amount: string
useClientSideRouter: boolean // included in key to invalidate on change
routerPreference: RouterPreference
type: 'exactIn' | 'exactOut'
}
>({
async queryFn(args, _api, _extraOptions, fetch) {
const { tokenInAddress, tokenInChainId, tokenOutAddress, tokenOutChainId, amount, useClientSideRouter, type } =
const { tokenInAddress, tokenInChainId, tokenOutAddress, tokenOutChainId, amount, routerPreference, type } =
args
let result
try {
if (useClientSideRouter) {
const chainId = args.tokenInChainId
const params = { chainId, provider: getRouterProvider(chainId) }
result = await getClientSideQuote(args, params, { protocols })
} else {
if (routerPreference === RouterPreference.API) {
const query = qs.stringify({
...DEFAULT_QUERY_PARAMS,
...API_QUERY_PARAMS,
tokenInAddress,
tokenInChainId,
tokenOutAddress,
@@ -77,6 +104,15 @@ export const routingApi = createApi({
type,
})
result = await fetch(`quote?${query}`)
} else {
const router = getRouter(args.tokenInChainId)
result = await getClientSideQuote(
args,
router,
// TODO(zzmp): Use PRICE_PARAMS for RouterPreference.PRICE.
// This change is intentionally being deferred to first see what effect router caching has.
CLIENT_PARAMS
)
}
return { data: result.data as GetQuoteResult }

View File

@@ -7,17 +7,11 @@ import { useRoutingAPIArguments } from 'lib/hooks/routing/useRoutingAPIArguments
import useIsValidBlock from 'lib/hooks/useIsValidBlock'
import ms from 'ms.macro'
import { useMemo } from 'react'
import { useGetQuoteQuery } from 'state/routing/slice'
import { useClientSideRouter } from 'state/user/hooks'
import { RouterPreference, useGetQuoteQuery } from 'state/routing/slice'
import { GetQuoteResult, InterfaceTrade, TradeState } from './types'
import { computeRoutes, transformRoutesToTrade } from './utils'
export enum RouterPreference {
CLIENT = 'client',
API = 'api',
}
/**
* Returns the best trade by invoking the routing api or the smart order router on the client
* @param tradeType whether the swap is an exact in/out
@@ -26,9 +20,9 @@ export enum RouterPreference {
*/
export function useRoutingAPITrade<TTradeType extends TradeType>(
tradeType: TTradeType,
amountSpecified?: CurrencyAmount<Currency>,
otherCurrency?: Currency,
routerPreference?: RouterPreference
amountSpecified: CurrencyAmount<Currency> | undefined,
otherCurrency: Currency | undefined,
routerPreference: RouterPreference
): {
state: TradeState
trade: InterfaceTrade<Currency, Currency, TTradeType> | undefined
@@ -41,22 +35,16 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
[amountSpecified, otherCurrency, tradeType]
)
const [clientSideRouterStoredPreference] = useClientSideRouter()
const clientSideRouter = routerPreference
? routerPreference === RouterPreference.CLIENT
: clientSideRouterStoredPreference
const queryArgs = useRoutingAPIArguments({
tokenIn: currencyIn,
tokenOut: currencyOut,
amount: amountSpecified,
tradeType,
useClientSideRouter: clientSideRouter,
routerPreference,
})
const { isLoading, isError, data, currentData } = useGetQuoteQuery(queryArgs ?? skipToken, {
pollingInterval: ms`15s`,
refetchOnFocus: true,
})
const quoteResult: GetQuoteResult | undefined = useIsValidBlock(Number(data?.blockNumber) || 0) ? data : undefined

View File

@@ -212,6 +212,8 @@ export function ExternalLinkIcon({
)
}
export const MAX_Z_INDEX = 9999
const ToolTipWrapper = styled.div<{ isCopyContractTooltip?: boolean }>`
display: flex;
flex-direction: column;
@@ -219,7 +221,7 @@ const ToolTipWrapper = styled.div<{ isCopyContractTooltip?: boolean }>`
position: ${({ isCopyContractTooltip }) => (isCopyContractTooltip ? 'relative' : 'absolute')};
right: ${({ isCopyContractTooltip }) => isCopyContractTooltip && '50%'};
transform: translate(5px, 32px);
z-index: 9999;
z-index: ${MAX_Z_INDEX};
`
const StyledTooltipTriangle = styled(TooltipTriangle)`

View File

@@ -13,6 +13,7 @@ import { darkTheme } from '../nft/themes/darkTheme'
import { lightTheme } from '../nft/themes/lightTheme'
import { useIsDarkMode } from '../state/user/hooks'
import { colors as ColorsPalette, colorsDark, colorsLight } from './colors'
import { MAX_Z_INDEX } from './components'
import { AllColors, Colors, ThemeColors } from './styled'
import { opacify } from './utils'
@@ -63,6 +64,7 @@ export enum Z_INDEX {
modal = 1060,
popover = 1070,
tooltip = 1080,
absoluteTop = MAX_Z_INDEX,
}
const deprecated_mediaWidthTemplates: { [width in keyof typeof MEDIA_WIDTHS]: typeof css } = Object.keys(

View File

@@ -4079,10 +4079,10 @@
tiny-invariant "^1.1.0"
toformat "^2.0.0"
"@uniswap/smart-order-router@^2.5.26", "@uniswap/smart-order-router@^2.9.2":
version "2.9.2"
resolved "https://registry.yarnpkg.com/@uniswap/smart-order-router/-/smart-order-router-2.9.2.tgz#3c9296b5b3821e191b6759a870330e4b10a9e9df"
integrity sha512-t+ruGvZTOvOJcVjxNPSU4o3GuPU/RYHr8KSKZlAHkZfusjbWrOLrO/aHzy/ncoRMNQz1UMBWQ2n3LDzqBxbTkA==
"@uniswap/smart-order-router@^2.10.0", "@uniswap/smart-order-router@^2.5.26":
version "2.10.0"
resolved "https://registry.yarnpkg.com/@uniswap/smart-order-router/-/smart-order-router-2.10.0.tgz#f9f13bd9a940fc2ee123a6dbe6c64a6fab19a365"
integrity sha512-7dfFlPbg36goZOWlRowTDDrRc1vWwKLhAuhftf6sN+ECJ4CeqRgDDZgxw/ZJhfDSl1RC6IYN71CfEcmhnbRDlw==
dependencies:
"@uniswap/default-token-list" "^2.0.0"
"@uniswap/router-sdk" "^1.3.0"
@@ -5103,9 +5103,9 @@ ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5:
uri-js "^4.2.2"
ajv@^8.0.1:
version "8.6.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.2.tgz#2fb45e0e5fcbc0813326c1c3da535d1881bb0571"
integrity sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==
version "8.11.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f"
integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==
dependencies:
fast-deep-equal "^3.1.1"
json-schema-traverse "^1.0.0"