Compare commits

...

14 Commits

Author SHA1 Message Date
Zach Pomerantz
e480f0ebe5 chore: simplify swap info (#3710)
* fix: prevent unnecessary TokenImg renders

* fix: prevent unnecessary trade renders

* fix: simplify swap info computation
2022-04-11 17:09:11 -07:00
Zach Pomerantz
f6ceecbc5e fix: update hook deps to improve ref equality checks (#3707)
* fix: prevent unnecessary TokenImg renders

* fix: prevent unnecessary trade renders
2022-04-11 16:57:53 -07:00
Zach Pomerantz
a0348b45be chore: bump to v1.0.6 (#3696) 2022-04-08 13:13:11 -07:00
Zach Pomerantz
e4b37cffcc fix: skewed swap info state (#3695)
* fix: skewed swap info state

* fix: typings
2022-04-08 13:11:19 -07:00
Zach Pomerantz
dd69cccf91 fix: always run global updaters (#3694) 2022-04-08 12:38:39 -07:00
Zach Pomerantz
8b228de88f chore: bump to v1.0.5 (#3691) 2022-04-08 10:52:30 -07:00
Zach Pomerantz
f91fc3c6a6 fix: defer layout effects (#3687)
* fix: use effect for color

* chore: clean up token defaults

* fix: condition updaters on active tokens
2022-04-08 10:27:10 -07:00
Zach Pomerantz
e0e2b40f9f chore: bump to v1.0.4 (#3686) 2022-04-07 15:05:35 -07:00
Zach Pomerantz
bc1c61b63a fix: omit document ref (#3685) 2022-04-07 15:05:11 -07:00
Alex Dorsch
446ad3e0d4 fix: missing token balance (#3661)
* increase gas required to read token balance

* set token balance gas requirement to 185_000
2022-04-07 15:00:03 -07:00
Zach Pomerantz
65e58a08cf fix: show i18n keys while messages load (#3683)
* fix: show i18n keys while messages load

* fix: i18n initialization check
2022-04-07 14:55:09 -07:00
Zach Pomerantz
71b20b432c fix: block number stability (#3684)
* fix: block number stability

* fix: chainBlock logic
2022-04-07 14:26:50 -07:00
Zach Pomerantz
ecfa179b3f chore: bump to v1.0.3 2022-04-07 11:24:33 -07:00
Zach Pomerantz
6c94a0f585 fix: swap validator (#3682) 2022-04-07 11:23:21 -07:00
17 changed files with 103 additions and 71 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@uniswap/widgets",
"version": "1.0.2",
"version": "1.0.6",
"description": "Uniswap Interface",
"homepage": ".",
"files": [

View File

@@ -13,6 +13,7 @@ import useUSDCPrice, { useUSDCValue } from './useUSDCPrice'
const V3_SWAP_DEFAULT_SLIPPAGE = new Percent(50, 10_000) // .50%
const ONE_TENTHS_PERCENT = new Percent(10, 10_000) // .10%
export const DEFAULT_AUTO_SLIPPAGE = ONE_TENTHS_PERCENT
/**
* Return a guess of the gas cost used in computing slippage tolerance for a given trade
@@ -44,7 +45,7 @@ export default function useAutoSlippageTolerance(
const nativeCurrencyPrice = useUSDCPrice((trade && nativeCurrency) ?? undefined)
return useMemo(() => {
if (!trade || onL2) return ONE_TENTHS_PERCENT
if (!trade || onL2) return DEFAULT_AUTO_SLIPPAGE
const nativeGasCost =
nativeGasPrice && typeof gasEstimate === 'number'

View File

@@ -3,7 +3,7 @@ import { SupportedChainId } from 'constants/chains'
import uriToHttp from 'lib/utils/uriToHttp'
import Vibrant from 'node-vibrant/lib/bundle.js'
import { shade } from 'polished'
import { useLayoutEffect, useState } from 'react'
import { useEffect, useState } from 'react'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import { hex } from 'wcag-contrast'
@@ -64,7 +64,7 @@ async function getColorFromUriPath(uri: string): Promise<string | null> {
export function useColor(token?: Token) {
const [color, setColor] = useState('#2172E5')
useLayoutEffect(() => {
useEffect(() => {
let stale = false
if (token) {
@@ -87,7 +87,7 @@ export function useColor(token?: Token) {
export function useListColor(listImageUri?: string) {
const [color, setColor] = useState('#2172E5')
useLayoutEffect(() => {
useEffect(() => {
let stale = false
if (listImageUri) {

View File

@@ -12,13 +12,14 @@ function isWindowVisible() {
* Returns whether the window is currently visible to the user.
*/
export default function useIsWindowVisible(): boolean {
const [focused, setFocused] = useState<boolean>(isWindowVisible())
const [focused, setFocused] = useState<boolean>(false)
const listener = useCallback(() => {
setFocused(isWindowVisible())
}, [setFocused])
useEffect(() => {
if (!isVisibilityStateSupported()) return undefined
setFocused((focused) => isWindowVisible())
document.addEventListener('visibilitychange', listener)
return () => {

View File

@@ -1,7 +1,7 @@
import { Currency, CurrencyAmount, Price, Token, TradeType } from '@uniswap/sdk-core'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { useMemo } from 'react'
import { useMemo, useRef } from 'react'
import { SupportedChainId } from '../constants/chains'
import { DAI_OPTIMISM, USDC_ARBITRUM, USDC_MAINNET, USDC_POLYGON } from '../constants/tokens'
@@ -32,8 +32,7 @@ export default function useUSDCPrice(currency?: Currency): Price<Currency, Token
maxHops: 2,
})
const v3USDCTrade = useClientSideV3Trade(TradeType.EXACT_OUTPUT, amountOut, currency)
return useMemo(() => {
const price = useMemo(() => {
if (!currency || !stablecoin) {
return undefined
}
@@ -54,6 +53,12 @@ export default function useUSDCPrice(currency?: Currency): Price<Currency, Token
return undefined
}, [currency, stablecoin, v2USDCTrade, v3USDCTrade.trade])
const lastPrice = useRef(price)
if (!price || !lastPrice.current || !price.equalTo(lastPrice.current)) {
lastPrice.current = price
}
return lastPrice.current
}
export function useUSDCValue(currencyAmount: CurrencyAmount<Currency> | undefined | null) {

View File

@@ -3,7 +3,7 @@ import { SupportedChainId } from 'constants/chains'
import { nativeOnChain } from 'constants/tokens'
import { useUpdateAtom } from 'jotai/utils'
import { useSwapInfo } from 'lib/hooks/swap'
import { SwapInfoUpdater } from 'lib/hooks/swap/useSwapInfo'
import { SwapInfoProvider } from 'lib/hooks/swap/useSwapInfo'
import { Field, swapAtom } from 'lib/state/swap'
import { useEffect } from 'react'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
@@ -54,7 +54,8 @@ function Fixture() {
export default (
<>
<SwapInfoUpdater />
<Fixture />
<SwapInfoProvider>
<Fixture />
</SwapInfoProvider>
</>
)

View File

@@ -1,14 +1,14 @@
import { Trans } from '@lingui/macro'
import { TokenInfo } from '@uniswap/token-lists'
import { useAtom } from 'jotai'
import { SwapInfoUpdater } from 'lib/hooks/swap/useSwapInfo'
import { SwapInfoProvider } from 'lib/hooks/swap/useSwapInfo'
import useSyncConvenienceFee, { FeeOptions } from 'lib/hooks/swap/useSyncConvenienceFee'
import useSyncTokenDefaults, { TokenDefaults } from 'lib/hooks/swap/useSyncTokenDefaults'
import { usePendingTransactions } from 'lib/hooks/transactions'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import useHasFocus from 'lib/hooks/useHasFocus'
import useOnSupportedNetwork from 'lib/hooks/useOnSupportedNetwork'
import { useSyncTokenList } from 'lib/hooks/useTokenList'
import { useIsTokenListLoaded, useSyncTokenList } from 'lib/hooks/useTokenList'
import { displayTxHashAtom } from 'lib/state/swap'
import { SwapTransactionInfo, Transaction, TransactionType, WrapTransactionInfo } from 'lib/state/transactions'
import { useState } from 'react'
@@ -47,12 +47,10 @@ export interface SwapProps extends TokenDefaults, FeeOptions {
onConnectWallet?: () => void
}
function Updaters(props: SwapProps & { disabled: boolean }) {
useSyncTokenList(props.tokenList)
function Updaters(props: SwapProps) {
useSyncTokenDefaults(props)
useSyncConvenienceFee(props)
return props.disabled ? null : <SwapInfoUpdater />
return null
}
export default function Swap(props: SwapProps) {
@@ -68,23 +66,28 @@ export default function Swap(props: SwapProps) {
const onSupportedNetwork = useOnSupportedNetwork()
const isDisabled = !(active && onSupportedNetwork)
useSyncTokenList(props.tokenList)
const isTokenListLoaded = useIsTokenListLoaded()
const focused = useHasFocus(wrapper)
return (
<>
<Updaters {...props} disabled={isDisabled} />
{isTokenListLoaded && <Updaters {...props} />}
<Header title={<Trans>Swap</Trans>}>
{active && <Wallet disabled={!account} onClick={props.onConnectWallet} />}
<Settings disabled={isDisabled} />
</Header>
<div ref={setWrapper}>
<BoundaryProvider value={wrapper}>
<Input disabled={isDisabled} focused={focused} />
<ReverseButton disabled={isDisabled} />
<Output disabled={isDisabled} focused={focused}>
<Toolbar disabled={!active} />
<SwapButton disabled={isDisabled} />
</Output>
<SwapInfoProvider disabled={isDisabled}>
<Input disabled={isDisabled} focused={focused} />
<ReverseButton disabled={isDisabled} />
<Output disabled={isDisabled} focused={focused}>
<Toolbar disabled={!active} />
<SwapButton disabled={isDisabled} />
</Output>
</SwapInfoProvider>
</BoundaryProvider>
</div>
{displayTx && (

View File

@@ -1,4 +1,3 @@
import { BigNumber } from '@ethersproject/bignumber'
import { IntegrationError } from 'lib/errors'
import { FeeOptions } from 'lib/hooks/swap/useSyncConvenienceFee'
import { DefaultAddress, TokenDefaults } from 'lib/hooks/swap/useSyncTokenDefaults'
@@ -48,19 +47,23 @@ export default function useValidate(props: ValidatorProps) {
}
}, [convenienceFee, convenienceFeeRecipient])
const { defaultInputTokenAddress, defaultInputAmount, defaultOutputTokenAddress, defaultOutputAmount } = props
const { defaultInputAmount, defaultOutputAmount } = props
useEffect(() => {
if (defaultOutputAmount && defaultInputAmount) {
throw new IntegrationError('defaultInputAmount and defaultOutputAmount may not both be defined.')
}
if (defaultInputAmount && BigNumber.from(defaultInputAmount).lt(0)) {
if (defaultInputAmount && (isNaN(+defaultInputAmount) || defaultInputAmount < 0)) {
throw new IntegrationError(`defaultInputAmount must be a positive number (you set it to ${defaultInputAmount})`)
}
if (defaultOutputAmount && BigNumber.from(defaultOutputAmount).lt(0)) {
if (defaultOutputAmount && (isNaN(+defaultOutputAmount) || defaultOutputAmount < 0)) {
throw new IntegrationError(
`defaultOutputAmount must be a positive number (you set it to ${defaultOutputAmount}).`
)
}
}, [defaultInputAmount, defaultOutputAmount])
const { defaultInputTokenAddress, defaultOutputTokenAddress } = props
useEffect(() => {
if (
defaultInputTokenAddress &&
!isAddressOrAddressMap(defaultInputTokenAddress) &&
@@ -79,5 +82,5 @@ export default function useValidate(props: ValidatorProps) {
`defaultOutputTokenAddress must be a valid address or "NATIVE" (you set it to ${defaultOutputTokenAddress}).`
)
}
}, [defaultInputTokenAddress, defaultInputAmount, defaultOutputTokenAddress, defaultOutputAmount])
}, [defaultInputTokenAddress, defaultOutputTokenAddress])
}

View File

@@ -26,16 +26,17 @@ function TokenImg({ token, ...rest }: TokenImgProps) {
setAttempt((attempt) => ++attempt)
}, [])
return useMemo(() => {
const src = useMemo(() => {
// Trigger a re-render when an error occurs.
void attempt
const src = srcs.find((src) => !badSrcs.has(src))
if (!src) return <MissingToken color="secondary" {...rest} />
return srcs.find((src) => !badSrcs.has(src))
}, [attempt, srcs])
const alt = tokenInfo.name || tokenInfo.symbol
return <img src={src} alt={alt} key={alt} onError={onError} {...rest} />
}, [attempt, onError, rest, srcs, tokenInfo.name, tokenInfo.symbol])
if (!src) return <MissingToken color="secondary" {...rest} />
const alt = tokenInfo.name || tokenInfo.symbol
return <img src={src} alt={alt} key={alt} onError={onError} {...rest} />
}
export default styled(TokenImg)<{ size?: number }>`

View File

@@ -6,6 +6,8 @@ import { InterfaceTrade, TradeState } from 'state/routing/types'
import useClientSideSmartOrderRouterTrade from '../routing/useClientSideSmartOrderRouterTrade'
export const INVALID_TRADE = { state: TradeState.INVALID, trade: undefined }
/**
* Returns the best v2+v3 trade for a desired swap.
* @param tradeType whether the swap is an exact in/out
@@ -39,6 +41,7 @@ export function useBestTrade(
return useMemo(() => {
const { state, trade } = tradeObject
// If the trade is in a settled state, return it.
if (state === TradeState.INVALID) return INVALID_TRADE
if ((state !== TradeState.LOADING && state !== TradeState.SYNCING) || trade) return tradeObject
const [currencyIn, currencyOut] =

View File

@@ -1,16 +1,15 @@
import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core'
import { atom } from 'jotai'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { useAtomValue } from 'jotai/utils'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import { useCurrencyBalances } from 'lib/hooks/useCurrencyBalance'
import useSlippage, { DEFAULT_SLIPPAGE, Slippage } from 'lib/hooks/useSlippage'
import useUSDCPriceImpact, { PriceImpact } from 'lib/hooks/useUSDCPriceImpact'
import { Field, swapAtom } from 'lib/state/swap'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { useEffect, useMemo } from 'react'
import { createContext, PropsWithChildren, useContext, useMemo } from 'react'
import { InterfaceTrade, TradeState } from 'state/routing/types'
import useActiveWeb3React from '../useActiveWeb3React'
import useSlippage, { Slippage } from '../useSlippage'
import useUSDCPriceImpact, { PriceImpact } from '../useUSDCPriceImpact'
import { useBestTrade } from './useBestTrade'
import { INVALID_TRADE, useBestTrade } from './useBestTrade'
import useWrapCallback, { WrapType } from './useWrapCallback'
interface SwapField {
@@ -33,7 +32,6 @@ interface SwapInfo {
// from the current swap inputs, compute the best trade and return it.
function useComputeSwapInfo(): SwapInfo {
const { account } = useActiveWeb3React()
const { type: wrapType } = useWrapCallback()
const isWrapping = wrapType === WrapType.WRAP || wrapType === WrapType.UNWRAP
const { independentField, amount, [Field.INPUT]: currencyIn, [Field.OUTPUT]: currencyOut } = useAtomValue(swapAtom)
@@ -58,6 +56,8 @@ function useComputeSwapInfo(): SwapInfo {
() => (isWrapping || !isExactIn ? parsedAmount : trade.trade?.outputAmount),
[isExactIn, isWrapping, parsedAmount, trade.trade?.outputAmount]
)
const { account } = useActiveWeb3React()
const [balanceIn, balanceOut] = useCurrencyBalances(
account,
useMemo(() => [currencyIn, currencyOut], [currencyIn, currencyOut])
@@ -102,21 +102,24 @@ function useComputeSwapInfo(): SwapInfo {
)
}
const swapInfoAtom = atom<SwapInfo>({
const DEFAULT_SWAP_INFO: SwapInfo = {
[Field.INPUT]: {},
[Field.OUTPUT]: {},
trade: { state: TradeState.INVALID },
slippage: { auto: true, allowed: new Percent(0) },
})
trade: INVALID_TRADE,
slippage: DEFAULT_SLIPPAGE,
}
export function SwapInfoUpdater() {
const setSwapInfo = useUpdateAtom(swapInfoAtom)
const SwapInfoContext = createContext(DEFAULT_SWAP_INFO)
export function SwapInfoProvider({ children, disabled }: PropsWithChildren<{ disabled?: boolean }>) {
const swapInfo = useComputeSwapInfo()
useEffect(() => setSwapInfo(swapInfo), [setSwapInfo, swapInfo])
return null
if (disabled) {
return <SwapInfoContext.Provider value={DEFAULT_SWAP_INFO}>{children}</SwapInfoContext.Provider>
}
return <SwapInfoContext.Provider value={swapInfo}>{children}</SwapInfoContext.Provider>
}
/** Requires that SwapInfoUpdater be installed in the DOM tree. **/
export default function useSwapInfo(): SwapInfo {
return useAtomValue(swapInfoAtom)
return useContext(SwapInfoContext)
}

View File

@@ -5,7 +5,7 @@ import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import { useToken } from 'lib/hooks/useCurrency'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { Field, Swap, swapAtom } from 'lib/state/swap'
import { useCallback, useLayoutEffect, useState } from 'react'
import { useCallback, useLayoutEffect, useRef } from 'react'
import useOnSupportedNetwork from '../useOnSupportedNetwork'
@@ -71,13 +71,11 @@ export default function useSyncTokenDefaults({
updateSwap((swap) => ({ ...swap, ...defaultSwapState }))
}, [defaultInputAmount, defaultInputToken, defaultOutputAmount, defaultOutputToken, updateSwap])
const [previousChainId, setPreviousChainId] = useState(chainId)
const lastChainId = useRef<number | undefined>(undefined)
useLayoutEffect(() => {
setPreviousChainId(chainId)
}, [chainId])
useLayoutEffect(() => {
if (chainId && chainId !== previousChainId) {
if (chainId && chainId !== lastChainId.current) {
setToDefaults()
}
}, [chainId, previousChainId, setToDefaults])
lastChainId.current = chainId
}, [chainId, setToDefaults])
}

View File

@@ -19,9 +19,9 @@ function useUpdateChainBlock() {
(block: number) => {
setChainBlock((chainBlock) => {
if (chainBlock.chainId === chainId) {
if (chainBlock.block === block) return chainBlock
if (typeof chainBlock.block !== 'number') return { chainId, block }
return { chainId, block: Math.max(block, chainBlock.block) }
if (!chainBlock.block || chainBlock.block < block) {
return { chainId, block }
}
}
return chainBlock
})

View File

@@ -47,7 +47,7 @@ export function useNativeCurrencyBalances(uncheckedAddresses?: (string | undefin
}
const ERC20Interface = new Interface(ERC20ABI) as Erc20Interface
const tokenBalancesGasRequirement = { gasRequired: 125_000 }
const tokenBalancesGasRequirement = { gasRequired: 185_000 }
/**
* Returns a map of token addresses to their eventually consistent token balances for a single account.

View File

@@ -1,7 +1,7 @@
import { Currency } from '@uniswap/sdk-core'
import { useTheme } from 'lib/theme'
import Vibrant from 'node-vibrant/lib/bundle.js'
import { useEffect, useLayoutEffect, useState } from 'react'
import { useEffect, useState } from 'react'
import useCurrencyLogoURIs from './useCurrencyLogoURIs'
@@ -57,7 +57,7 @@ export default function useCurrencyColor(token?: Currency) {
const theme = useTheme()
const logoURIs = useCurrencyLogoURIs(token)
useLayoutEffect(() => {
useEffect(() => {
let stale = false
if (theme.tokenColorExtraction && token) {

View File

@@ -1,5 +1,5 @@
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import useAutoSlippageTolerance from 'hooks/useAutoSlippageTolerance'
import useAutoSlippageTolerance, { DEFAULT_AUTO_SLIPPAGE } from 'hooks/useAutoSlippageTolerance'
import { useAtomValue } from 'jotai/utils'
import { autoSlippageAtom, maxSlippageAtom } from 'lib/state/settings'
import { useMemo } from 'react'
@@ -17,6 +17,8 @@ export interface Slippage {
warning?: 'warning' | 'error'
}
export const DEFAULT_SLIPPAGE = { auto: true, allowed: DEFAULT_AUTO_SLIPPAGE }
/** Returns the allowed slippage, and whether it is auto-slippage. */
export default function useSlippage(trade: InterfaceTrade<Currency, Currency, TradeType> | undefined): Slippage {
const shouldUseAutoSlippage = useAtomValue(autoSlippageAtom)
@@ -27,6 +29,9 @@ export default function useSlippage(trade: InterfaceTrade<Currency, Currency, Tr
const auto = shouldUseAutoSlippage || !maxSlippage
const allowed = shouldUseAutoSlippage ? autoSlippage : maxSlippage ?? autoSlippage
const warning = auto ? undefined : getSlippageWarning(allowed)
if (auto && allowed === DEFAULT_AUTO_SLIPPAGE) {
return DEFAULT_SLIPPAGE
}
return { auto, allowed, warning }
}, [autoSlippage, maxSlippage, shouldUseAutoSlippage])
}

View File

@@ -1,6 +1,6 @@
import { i18n } from '@lingui/core'
import { I18nProvider } from '@lingui/react'
import { SupportedLocale } from 'constants/locales'
import { DEFAULT_LOCALE, SupportedLocale } from 'constants/locales'
import {
af,
ar,
@@ -79,8 +79,6 @@ const plurals: LocalePlural = {
export async function dynamicActivate(locale: SupportedLocale) {
i18n.loadLocaleData(locale, { plurals: () => plurals[locale] })
try {
// There are no default messages in production,
// see https://github.com/lingui/js-lingui/issues/388#issuecomment-497779030
const catalog = await import(`${process.env.REACT_APP_LOCALES}/${locale}.js`)
// Bundlers will either export it as default or as a named export named default.
i18n.load(locale, catalog.messages || catalog.default.messages)
@@ -104,6 +102,16 @@ export function Provider({ locale, forceRenderAfterLocaleChange = true, onActiva
})
}, [locale, onActivate])
// Initialize the locale immediately if it is DEFAULT_LOCALE, so that keys are shown while the translation messages load.
// This renders the translation _keys_, not the translation _messages_, which is only acceptable while loading the DEFAULT_LOCALE,
// as [there are no "default" messages](https://github.com/lingui/js-lingui/issues/388#issuecomment-497779030).
// See https://github.com/lingui/js-lingui/issues/1194#issuecomment-1068488619.
if (i18n.locale === undefined && locale === DEFAULT_LOCALE) {
i18n.loadLocaleData(DEFAULT_LOCALE, { plurals: () => plurals[DEFAULT_LOCALE] })
i18n.load(DEFAULT_LOCALE, {})
i18n.activate(DEFAULT_LOCALE)
}
return (
<I18nProvider forceRenderOnLocaleChange={forceRenderAfterLocaleChange} i18n={i18n}>
{children}