Compare commits

..

13 Commits

Author SHA1 Message Date
Christine Legge
71aebf33db fix: remove unused var (#3736) 2022-04-14 16:26:42 -04:00
Christine Legge
5ff428b04b fix: update widgets README 2022-04-14 16:06:57 -04:00
Mark Carbajal
acb0c2056e chore: Remove Portis (#3693)
* Removed portis

* Removed portis

* Removed portis

* Update src/components/WalletModal/index.tsx

Co-authored-by: Bruno Crosier <bruno.crosier@gmail.com>

* regenerate yarn.lock

* revert translation changes

Co-authored-by: Bruno Crosier <bruno.crosier@gmail.com>
Co-authored-by: Noah Zinsmeister <noahwz@gmail.com>
2022-04-14 15:23:31 -04:00
Christine Legge
0a4bcb62da add 1bp fee tier to polygon (#3724) 2022-04-14 15:11:58 -04:00
Zach Pomerantz
f50bcbdb2d fix: initial transitions (#3719)
* fix: rm action fade

* fix: disallow stale swaps

* fix: fade in buttons

* fix: fade in input text

* fix: standardize border handling

* fix: transition token button width

* fix: cleanup transitions

* fix: use transition for button

* chore: cleanup
2022-04-13 11:45:29 -07:00
Christine Legge
cbe421ee23 fix: Reload the app when there is a javascript error and a new version of the app (#3715)
* reload the app when encountering a javascript error if there is an update

* remove console.logs

* Add more comments
2022-04-13 13:53:54 -04:00
Zach Pomerantz
3439786c38 feat: display connecting state (#3713) 2022-04-13 09:21:28 -07:00
Zach Pomerantz
6294915be6 fix: convert token list to context (#3712)
* fix: convert token list to context

* fix: cosmos
2022-04-12 14:53:50 -07:00
Zach Pomerantz
984c742d0e fix: use context for block number (#3708)
* fix: use context for block number

* fix: check for valid BlockNumberContext
2022-04-12 10:10:57 -07:00
Zach Pomerantz
00b151d7fa fix: activation frames (#3711) 2022-04-12 09:23:19 -07:00
guil-lambert
5967cf5d9d fix: bug where user cannot burn lp position if fetching fee values fails. (#3633)
* fix: can burn position even if fetching fees fails.

* Revert "fix: can burn position even if fetching fees fails."

This reverts commit a96f7178e5.

* recover more gracefully from failed fee fetch

Co-authored-by: Noah Zinsmeister <noahwz@gmail.com>
2022-04-12 11:41:31 -04:00
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
45 changed files with 515 additions and 921 deletions

View File

@@ -1,5 +1,4 @@
REACT_APP_INFURA_KEY="099fc58e0de9451d80b18d7c74caa7c1"
REACT_APP_PORTIS_ID="c0e2bf01-4b08-4fd5-ac7b-8e26b58cd236"
REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF"
REACT_APP_GOOGLE_ANALYTICS_ID="G-KDP9B6W4H8"
REACT_APP_FIREBASE_KEY="AIzaSyBcZWwTcTJHj_R6ipZcrJkXdq05PuX0Rs0"

View File

@@ -4,7 +4,7 @@ The Swap Widget bundles the whole swapping experience into a single React compon
![swap widget screenshot](https://raw.githubusercontent.com/Uniswap/interface/main/src/assets/images/widget-screenshot.png)
You can customize the theme (colors, font, border radius, and more) to match the style of your application. You can also configure your own default token list and optionally set a convenience fee on swaps executed through the widget on your site.
You can customize the theme (colors, fonts, border radius, and more) to match the style of your application. You can also configure your own default token list and optionally set a convenience fee on swaps executed through the widget on your site.
## Installation

View File

@@ -90,8 +90,8 @@
"@uniswap/v2-periphery": "^1.1.0-beta.0",
"@uniswap/v3-core": "1.0.0",
"@uniswap/v3-periphery": "^1.1.1",
"@web3-react/metamask": "^8.0.18-beta.0",
"@web3-react/walletconnect": "^8.0.25-beta.0",
"@web3-react/metamask": "^8.0.19-beta.0",
"@web3-react/walletconnect": "^8.0.26-beta.0",
"array.prototype.flat": "^1.2.4",
"array.prototype.flatmap": "^1.2.4",
"babel-plugin-macros": "^3.1.0",
@@ -150,7 +150,6 @@
"web3-react-abstract-connector": "npm:@web3-react/abstract-connector@^6.0.7",
"web3-react-fortmatic-connector": "npm:@web3-react/fortmatic-connector@^6.0.9",
"web3-react-injected-connector": "npm:@web3-react/injected-connector@^6.0.7",
"web3-react-portis-connector": "npm:@web3-react/portis-connector@^6.0.9",
"web3-react-types": "npm:@web3-react/types@^6.0.7",
"web3-react-walletconnect-connector": "npm:@web3-react/walletconnect-connector@^7.0.2-alpha.0",
"web3-react-walletlink-connector": "npm:@web3-react/walletlink-connector@^6.2.13",
@@ -205,11 +204,11 @@
"@uniswap/token-lists": "^1.0.0-beta.27",
"@uniswap/v2-sdk": "^3.0.1",
"@uniswap/v3-sdk": "^3.8.2",
"@web3-react/core": "^8.0.22-beta.0",
"@web3-react/eip1193": "^8.0.17-beta.0",
"@web3-react/empty": "^8.0.11-beta.0",
"@web3-react/types": "^8.0.11-beta.0",
"@web3-react/url": "^8.0.16-beta.0",
"@web3-react/core": "^8.0.23-beta.0",
"@web3-react/eip1193": "^8.0.18-beta.0",
"@web3-react/empty": "^8.0.12-beta.0",
"@web3-react/types": "^8.0.12-beta.0",
"@web3-react/url": "^8.0.17-beta.0",
"ajv": "^6.12.3",
"cids": "^1.0.0",
"ethers": "^5.1.4",

View File

@@ -8,7 +8,7 @@ import styled, { ThemeContext } from 'styled-components/macro'
import { AbstractConnector } from 'web3-react-abstract-connector'
import { ReactComponent as Close } from '../../assets/images/x.svg'
import { injected, portis, walletlink } from '../../connectors'
import { injected, walletlink } from '../../connectors'
import { SUPPORTED_WALLETS } from '../../constants/wallet'
import { clearAllTransactions } from '../../state/transactions/actions'
import { ExternalLink, LinkStyledButton, ThemedText } from '../../theme'
@@ -181,15 +181,6 @@ function WrappedStatusIcon({ connector }: { connector: AbstractConnector | Conne
return (
<IconWrapper size={16}>
<StatusIcon connector={connector} />
{connector === portis && (
<MainWalletAction
onClick={() => {
portis.portis.showPortis()
}}
>
<Trans>Show Portis</Trans>
</MainWalletAction>
)}
</IconWrapper>
)
}
@@ -210,10 +201,6 @@ const WalletAction = styled(ButtonSecondary)`
}
`
const MainWalletAction = styled(WalletAction)`
color: ${({ theme }) => theme.primary1};
`
function renderTransactions(transactions: string[]) {
return (
<TransactionListWrapper>

View File

@@ -49,6 +49,13 @@ type ErrorBoundaryState = {
const IS_UNISWAP = window.location.hostname === 'app.uniswap.org'
async function updateServiceWorker(): Promise<ServiceWorkerRegistration> {
const ready = await navigator.serviceWorker.ready
// the return type of update is incorrectly typed as Promise<void>. See
// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/update
return ready.update() as unknown as Promise<ServiceWorkerRegistration>
}
export default class ErrorBoundary extends React.Component<unknown, ErrorBoundaryState> {
constructor(props: unknown) {
super(props)
@@ -56,6 +63,24 @@ export default class ErrorBoundary extends React.Component<unknown, ErrorBoundar
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
updateServiceWorker()
.then(async (registration) => {
// We want to refresh only if we detect a new service worker is waiting to be activated.
// See details about it: https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle
if (registration?.waiting) {
await registration.unregister()
// Makes Workbox call skipWaiting(). For more info on skipWaiting see: https://developer.chrome.com/docs/workbox/handling-service-worker-updates/
registration.waiting.postMessage({ type: 'SKIP_WAITING' })
// Once the service worker is unregistered, we can reload the page to let
// the browser download a fresh copy of our app (invalidating the cache)
window.location.reload()
}
})
.catch((error) => {
console.error('Failed to update service worker', error)
})
return { error }
}

View File

@@ -10,7 +10,7 @@ export const FEE_AMOUNT_DETAIL: Record<
[FeeAmount.LOWEST]: {
label: '0.01',
description: <Trans>Best for very stable pairs.</Trans>,
supportedChains: [SupportedChainId.MAINNET],
supportedChains: [SupportedChainId.MAINNET, SupportedChainId.POLYGON, SupportedChainId.POLYGON_MUMBAI],
},
[FeeAmount.LOW]: {
label: '0.05',

View File

@@ -3,9 +3,8 @@ import { AbstractConnector } from 'web3-react-abstract-connector'
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
import PortisIcon from '../../assets/images/portisIcon.png'
import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
import { fortmatic, injected, portis, walletconnect, walletlink } from '../../connectors'
import { fortmatic, injected, walletconnect, walletlink } from '../../connectors'
import Identicon from '../Identicon'
export default function StatusIcon({ connector }: { connector: AbstractConnector | Connector }) {
@@ -18,8 +17,6 @@ export default function StatusIcon({ connector }: { connector: AbstractConnector
return <img src={CoinbaseWalletIcon} alt={'Coinbase Wallet'} />
case fortmatic:
return <img src={FortmaticIcon} alt={'Fortmatic'} />
case portis:
return <img src={PortisIcon} alt={'Portis'} />
default:
return null
}

View File

@@ -13,7 +13,7 @@ import { WalletConnectConnector } from 'web3-react-walletconnect-connector'
import MetamaskIcon from '../../assets/images/metamask.png'
import { ReactComponent as Close } from '../../assets/images/x.svg'
import { fortmatic, injected, portis } from '../../connectors'
import { fortmatic, injected } from '../../connectors'
import { OVERLAY_READY } from '../../connectors/Fortmatic'
import { SUPPORTED_WALLETS } from '../../constants/wallet'
import usePrevious from '../../hooks/usePrevious'
@@ -228,11 +228,6 @@ export default function WalletModal({
const option = SUPPORTED_WALLETS[key]
// check for mobile options
if (isMobile) {
//disable portis on mobile for now
if (option.connector === portis) {
return null
}
if (!window.web3 && !window.ethereum && option.mobile) {
return (
<Option

View File

@@ -3,7 +3,6 @@ import { SafeAppConnector } from '@gnosis.pm/safe-apps-web3-react'
import { ALL_SUPPORTED_CHAIN_IDS, SupportedChainId } from 'constants/chains'
import { INFURA_NETWORK_URLS } from 'constants/infura'
import { InjectedConnector } from 'web3-react-injected-connector'
import { PortisConnector } from 'web3-react-portis-connector'
import { WalletConnectConnector } from 'web3-react-walletconnect-connector'
import { WalletLinkConnector } from 'web3-react-walletlink-connector'
@@ -13,7 +12,6 @@ import { FortmaticConnector } from './Fortmatic'
import { NetworkConnector } from './NetworkConnector'
const FORMATIC_KEY = process.env.REACT_APP_FORTMATIC_KEY
const PORTIS_ID = process.env.REACT_APP_PORTIS_ID
export const network = new NetworkConnector({
urls: INFURA_NETWORK_URLS,
@@ -43,12 +41,6 @@ export const fortmatic = new FortmaticConnector({
chainId: 1,
})
// mainnet only
export const portis = new PortisConnector({
dAppId: PORTIS_ID ?? '',
networks: [1],
})
export const walletlink = new WalletLinkConnector({
url: INFURA_NETWORK_URLS[SupportedChainId.MAINNET],
appName: 'Uniswap',

View File

@@ -4,9 +4,8 @@ import INJECTED_ICON_URL from '../assets/images/arrow-right.svg'
import COINBASE_ICON_URL from '../assets/images/coinbaseWalletIcon.svg'
import FORTMATIC_ICON_URL from '../assets/images/fortmaticIcon.png'
import METAMASK_ICON_URL from '../assets/images/metamask.png'
import PORTIS_ICON_URL from '../assets/images/portisIcon.png'
import WALLETCONNECT_ICON_URL from '../assets/images/walletConnectIcon.svg'
import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors'
import { fortmatic, injected, walletconnect, walletlink } from '../connectors'
interface WalletInfo {
connector?: AbstractConnector
@@ -73,13 +72,4 @@ export const SUPPORTED_WALLETS: { [key: string]: WalletInfo } = {
color: '#6748FF',
mobile: true,
},
Portis: {
connector: portis,
name: 'Portis',
iconURL: PORTIS_ICON_URL,
description: 'Login using Portis hosted wallet',
href: null,
color: '#4A6C9B',
mobile: true,
},
}

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

@@ -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

@@ -23,13 +23,11 @@ export function useV3PositionFees(
const tokenIdHexString = tokenId?.toHexString()
const latestBlockNumber = useBlockNumber()
// TODO find a way to get this into multicall
// we can't use multicall for this because we need to simulate the call from a specific address
// latestBlockNumber is included to ensure data stays up-to-date every block
const [amounts, setAmounts] = useState<[BigNumber, BigNumber]>()
const [amounts, setAmounts] = useState<[BigNumber, BigNumber] | undefined>()
useEffect(() => {
let stale = false
if (positionManager && tokenIdHexString && owner && typeof latestBlockNumber === 'number') {
if (positionManager && tokenIdHexString && owner) {
positionManager.callStatic
.collect(
{
@@ -41,19 +39,15 @@ export function useV3PositionFees(
{ from: owner } // need to simulate the call as the owner
)
.then((results) => {
if (!stale) setAmounts([results.amount0, results.amount1])
setAmounts([results.amount0, results.amount1])
})
}
return () => {
stale = true
}
}, [positionManager, tokenIdHexString, owner, latestBlockNumber])
if (pool && amounts) {
return [
CurrencyAmount.fromRawAmount(!asWETH ? unwrappedToken(pool.token0) : pool.token0, amounts[0].toString()),
CurrencyAmount.fromRawAmount(!asWETH ? unwrappedToken(pool.token1) : pool.token1, amounts[1].toString()),
CurrencyAmount.fromRawAmount(asWETH ? pool.token0 : unwrappedToken(pool.token0), amounts[0].toString()),
CurrencyAmount.fromRawAmount(asWETH ? pool.token1 : unwrappedToken(pool.token1), amounts[1].toString()),
]
} else {
return [undefined, undefined]

View File

@@ -3,7 +3,7 @@ import 'inter-ui'
import 'polyfills'
import 'components/analytics'
import { BlockUpdater } from 'lib/hooks/useBlockNumber'
import { BlockNumberProvider } from 'lib/hooks/useBlockNumber'
import { MulticallUpdater } from 'lib/state/multicall'
import { StrictMode } from 'react'
import ReactDOM from 'react-dom'
@@ -40,7 +40,6 @@ function Updaters() {
<UserUpdater />
<ApplicationUpdater />
<TransactionUpdater />
<BlockUpdater />
<MulticallUpdater />
<LogsUpdater />
</>
@@ -55,11 +54,13 @@ ReactDOM.render(
<Web3ReactProvider getLibrary={getLibrary}>
<Web3ProviderNetwork getLibrary={getLibrary}>
<Blocklist>
<Updaters />
<ThemeProvider>
<ThemedGlobalStyle />
<App />
</ThemeProvider>
<BlockNumberProvider>
<Updaters />
<ThemeProvider>
<ThemedGlobalStyle />
<App />
</ThemeProvider>
</BlockNumberProvider>
</Blocklist>
</Web3ProviderNetwork>
</Web3ReactProvider>

View File

@@ -5,17 +5,7 @@ import { ReactNode, useMemo } from 'react'
import Button from './Button'
import Row from './Row'
const fadeIn = keyframes`
from {
opacity: 0;
}
to {
opacity: 1;
}
`
const StyledButton = styled(Button)`
animation: ${fadeIn} 0.25s ease-in;
border-radius: ${({ theme }) => theme.borderRadius * 0.75}em;
flex-grow: 1;
transition: background-color 0.25s ease-out, border-radius 0.25s ease-out, flex-grow 0.25s ease-out;

View File

@@ -1,5 +1,5 @@
import { Icon } from 'lib/icons'
import styled, { Color } from 'lib/theme'
import styled, { Color, css } from 'lib/theme'
import { ComponentProps, forwardRef } from 'react'
export const BaseButton = styled.button`
@@ -15,17 +15,26 @@ export const BaseButton = styled.button`
margin: 0;
padding: 0;
:enabled {
transition: filter 0.125s linear;
}
:disabled {
cursor: initial;
filter: saturate(0) opacity(0.4);
}
`
const transitionCss = css`
transition: background-color 0.125s linear, border-color 0.125s linear, filter 0.125s linear;
`
export default styled(BaseButton)<{ color?: Color }>`
export default styled(BaseButton)<{ color?: Color; transition?: boolean }>`
border: 1px solid transparent;
color: ${({ color = 'interactive', theme }) => color === 'interactive' && theme.onInteractive};
:enabled {
background-color: ${({ color = 'interactive', theme }) => theme[color]};
${({ transition = true }) => transition && transitionCss};
}
:enabled:hover {
@@ -33,9 +42,8 @@ export default styled(BaseButton)<{ color?: Color }>`
}
:disabled {
border: 1px solid ${({ theme }) => theme.outline};
border-color: ${({ theme }) => theme.outline};
color: ${({ theme }) => theme.secondary};
cursor: initial;
}
`

View File

@@ -1,4 +1,6 @@
import { loadingOpacity } from 'lib/css/loading'
import styled, { css } from 'lib/theme'
import { transparentize } from 'polished'
import { ChangeEvent, forwardRef, HTMLProps, useCallback } from 'react'
const Input = styled.input`
@@ -34,6 +36,16 @@ const Input = styled.input`
::placeholder {
color: ${({ theme }) => theme.secondary};
}
:enabled {
transition: color 0.125s linear;
}
:disabled {
// Overrides WebKit's override of input:disabled color.
-webkit-text-fill-color: ${({ theme }) => transparentize(1 - loadingOpacity, theme.primary)};
color: ${({ theme }) => transparentize(1 - loadingOpacity, theme.primary)};
}
`
export default Input

View File

@@ -80,7 +80,7 @@ export default function Input({ disabled, focused }: InputProps) {
// extract eagerly in case of reversal
usePrefetchCurrencyColor(inputCurrency)
const isRouteLoading = tradeState === TradeState.SYNCING || tradeState === TradeState.LOADING
const isRouteLoading = disabled || tradeState === TradeState.SYNCING || tradeState === TradeState.LOADING
const isDependentField = !useIsSwapFieldIndependent(Field.INPUT)
const isLoading = isRouteLoading && isDependentField

View File

@@ -46,7 +46,7 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
const [swapOutputAmount, updateSwapOutputAmount] = useSwapAmount(Field.OUTPUT)
const [swapOutputCurrency, updateSwapOutputCurrency] = useSwapCurrency(Field.OUTPUT)
const isRouteLoading = tradeState === TradeState.SYNCING || tradeState === TradeState.LOADING
const isRouteLoading = disabled || tradeState === TradeState.SYNCING || tradeState === TradeState.LOADING
const isDependentField = !useIsSwapFieldIndependent(Field.OUTPUT)
const isLoading = isRouteLoading && isDependentField

View File

@@ -33,6 +33,7 @@ const Overlay = styled.div`
const StyledReverseButton = styled(Button)<{ turns: number }>`
border-radius: ${({ theme }) => theme.borderRadius * 0.75}em;
color: ${({ theme }) => theme.primary};
height: 2.5em;
position: relative;
width: 2.5em;

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,4 +1,3 @@
import { tokens } from '@uniswap/default-token-list'
import { DAI, USDC_MAINNET } from 'constants/tokens'
import { useUpdateAtom } from 'jotai/utils'
import { useEffect } from 'react'
@@ -65,7 +64,6 @@ function Fixture() {
defaultInputAmount={defaultInputAmount}
defaultOutputTokenAddress={optionsToAddressMap[defaultOutputToken]}
defaultOutputAmount={defaultOutputAmount}
tokenList={tokens}
onConnectWallet={() => console.log('onConnectWallet')} // this handler is included as a test of functionality, but only logs
/>
)

View File

@@ -14,6 +14,7 @@ import { TransactionType } from 'lib/state/transactions'
import { useTheme } from 'lib/theme'
import { isAnimating } from 'lib/utils/animations'
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { TradeState } from 'state/routing/types'
import invariant from 'tiny-invariant'
import ActionButton, { ActionButtonProps } from '../../ActionButton'
@@ -152,13 +153,17 @@ export default memo(function SwapButton({ disabled }: SwapButtonProps) {
if (disableSwap) {
return { disabled: true }
} else if (wrapType === WrapType.NONE) {
return approvalAction ? { action: approvalAction } : { onClick: () => setOpen(true) }
return approvalAction
? { action: approvalAction }
: trade.state === TradeState.VALID
? { onClick: () => setOpen(true) }
: { disabled: true }
} else {
return isPending
? { action: { message: <Trans>Confirm in your wallet</Trans>, icon: Spinner } }
: { onClick: onWrap }
}
}, [approvalAction, disableSwap, isPending, onWrap, wrapType])
}, [approvalAction, disableSwap, isPending, onWrap, trade.state, wrapType])
const Label = useCallback(() => {
switch (wrapType) {
case WrapType.UNWRAP:

View File

@@ -32,6 +32,19 @@ function Caption({ icon: Icon = AlertTriangle, caption }: CaptionProps) {
)
}
export function Connecting() {
return (
<Caption
icon={InlineSpinner}
caption={
<Loading>
<Trans>Connecting</Trans>
</Loading>
}
/>
)
}
export function ConnectWallet() {
return <Caption caption={<Trans>Connect wallet to swap</Trans>} />
}

View File

@@ -17,8 +17,8 @@ const ToolbarRow = styled(Row)`
${largeIconCss}
`
export default memo(function Toolbar({ disabled }: { disabled?: boolean }) {
const { chainId } = useActiveWeb3React()
export default memo(function Toolbar() {
const { active, activating, chainId } = useActiveWeb3React()
const {
[Field.INPUT]: { currency: inputCurrency, balance: inputBalance, amount: inputAmount },
[Field.OUTPUT]: { currency: outputCurrency, usdc: outputUSDC },
@@ -28,11 +28,12 @@ export default memo(function Toolbar({ disabled }: { disabled?: boolean }) {
const isAmountPopulated = useIsAmountPopulated()
const { type: wrapType } = useWrapCallback()
const caption = useMemo(() => {
if (disabled) {
if (!active || !chainId) {
if (activating) return <Caption.Connecting />
return <Caption.ConnectWallet />
}
if (chainId && !ALL_SUPPORTED_CHAIN_IDS.includes(chainId)) {
if (!ALL_SUPPORTED_CHAIN_IDS.includes(chainId)) {
return <Caption.UnsupportedNetwork />
}
@@ -59,8 +60,9 @@ export default memo(function Toolbar({ disabled }: { disabled?: boolean }) {
return <Caption.Empty />
}, [
activating,
active,
chainId,
disabled,
impact,
inputAmount,
inputBalance,

View File

@@ -1,14 +1,12 @@
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 { 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'
@@ -43,19 +41,13 @@ function getTransactionFromMap(
}
export interface SwapProps extends TokenDefaults, FeeOptions {
tokenList?: string | TokenInfo[]
onConnectWallet?: () => void
}
function Updaters(props: SwapProps) {
useSyncTokenDefaults(props)
useSyncConvenienceFee(props)
return <SwapInfoUpdater />
}
export default function Swap(props: SwapProps) {
useValidate(props)
useSyncConvenienceFee(props)
useSyncTokenDefaults(props)
const { active, account } = useActiveWeb3React()
const [wrapper, setWrapper] = useState<HTMLDivElement | null>(null)
@@ -67,26 +59,24 @@ export default function Swap(props: SwapProps) {
const onSupportedNetwork = useOnSupportedNetwork()
const isDisabled = !(active && onSupportedNetwork)
useSyncTokenList(props.tokenList)
const isUpdateable = useIsTokenListLoaded() && !isDisabled
const focused = useHasFocus(wrapper)
return (
<>
{isUpdateable && <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 />
<SwapButton disabled={isDisabled} />
</Output>
</SwapInfoProvider>
</BoundaryProvider>
</div>
{displayTx && (

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

@@ -1,15 +1,15 @@
import DEFAULT_TOKEN_LIST from '@uniswap/default-token-list'
import { useSyncTokenList } from 'lib/hooks/useTokenList'
import { TokenListProvider } from 'lib/hooks/useTokenList'
import { Modal } from './Dialog'
import { TokenSelectDialog } from './TokenSelect'
export default function Fixture() {
useSyncTokenList(DEFAULT_TOKEN_LIST.tokens)
return (
<Modal color="module">
<TokenSelectDialog onSelect={() => void 0} />
<TokenListProvider list={DEFAULT_TOKEN_LIST.tokens}>
<TokenSelectDialog onSelect={() => void 0} />
</TokenListProvider>
</Modal>
)
}

View File

@@ -1,29 +1,33 @@
import { Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core'
import { ChevronDown } from 'lib/icons'
import styled, { ThemedText } from 'lib/theme'
import { useMemo } from 'react'
import styled, { css, ThemedText } from 'lib/theme'
import { useEffect, useMemo, useState } from 'react'
import Button from '../Button'
import Row from '../Row'
import TokenImg from '../TokenImg'
const StyledTokenButton = styled(Button)<{ empty?: boolean }>`
const transitionCss = css`
transition: background-color 0.125s linear, border-color 0.125s linear, filter 0.125s linear, width 0.125s ease-out;
`
const StyledTokenButton = styled(Button)`
border-radius: ${({ theme }) => theme.borderRadius}em;
padding: 0.25em;
padding-left: ${({ empty }) => (empty ? 0.75 : 0.25)}em;
:disabled {
// prevents border from expanding the button's box size
padding: calc(0.25em - 1px);
padding-left: calc(${({ empty }) => (empty ? 0.75 : 0.25)}em - 1px);
:enabled {
${({ transition }) => transition && transitionCss};
}
`
const TokenButtonRow = styled(Row)<{ collapsed: boolean }>`
const TokenButtonRow = styled(Row)<{ empty: boolean; collapsed: boolean }>`
float: right;
height: 1.2em;
// max-width must have an absolute value in order to transition.
max-width: ${({ collapsed }) => (collapsed ? '1.2em' : '12em')};
padding-left: ${({ empty }) => empty && 0.5}em;
width: fit-content;
overflow: hidden;
transition: max-width 0.25s linear;
@@ -42,10 +46,42 @@ interface TokenButtonProps {
export default function TokenButton({ value, collapsed, disabled, onClick }: TokenButtonProps) {
const buttonBackgroundColor = useMemo(() => (value ? 'interactive' : 'accent'), [value])
const contentColor = useMemo(() => (value || disabled ? 'onInteractive' : 'onAccent'), [value, disabled])
// Transition the button only if transitioning from a disabled state.
// This makes initialization cleaner without adding distracting UX to normal swap flows.
const [shouldTransition, setShouldTransition] = useState(disabled)
useEffect(() => {
if (disabled) {
setShouldTransition(true)
}
}, [disabled])
// width must have an absolute value in order to transition, so it is taken from the row ref.
const [row, setRow] = useState<HTMLDivElement | null>(null)
const style = useMemo(() => {
if (!shouldTransition) return
return { width: row ? row.clientWidth + /* padding= */ 8 + /* border= */ 2 : undefined }
}, [row, shouldTransition])
return (
<StyledTokenButton onClick={onClick} empty={!value} color={buttonBackgroundColor} disabled={disabled}>
<StyledTokenButton
onClick={onClick}
color={buttonBackgroundColor}
disabled={disabled}
style={style}
transition={shouldTransition}
onTransitionEnd={() => setShouldTransition(false)}
>
<ThemedText.ButtonLarge color={contentColor}>
<TokenButtonRow gap={0.4} collapsed={Boolean(value) && collapsed}>
<TokenButtonRow
gap={0.4}
empty={!value}
collapsed={collapsed}
// ref is used to set an absolute width, so it must be reset for each value passed.
// To force this, value?.symbol is passed as a key.
ref={setRow}
key={value?.symbol}
>
{value ? (
<>
<TokenImg token={value} size={1.2} />

View File

@@ -25,9 +25,9 @@ const SearchInput = styled(StringInput)`
function usePrefetchBalances() {
const { account } = useActiveWeb3React()
const tokenList = useTokenList()
const [prefetchedTokenList, setPrefetchedTokenList] = useState(tokenList)
useEffect(() => setPrefetchedTokenList(tokenList), [tokenList])
useCurrencyBalances(account, tokenList !== prefetchedTokenList ? tokenList : undefined)
const prefetchedTokenList = useRef<typeof tokenList>()
useCurrencyBalances(account, tokenList !== prefetchedTokenList.current ? tokenList : undefined)
prefetchedTokenList.current = tokenList
}
function useAreBalancesLoaded(): boolean {

View File

@@ -1,10 +1,12 @@
import { JsonRpcProvider } from '@ethersproject/providers'
import { TokenInfo } from '@uniswap/token-lists'
import { Provider as Eip1193Provider } from '@web3-react/types'
import { DEFAULT_LOCALE, SUPPORTED_LOCALES, SupportedLocale } from 'constants/locales'
import { Provider as AtomProvider } from 'jotai'
import { TransactionsUpdater } from 'lib/hooks/transactions'
import { ActiveWeb3Provider } from 'lib/hooks/useActiveWeb3React'
import { BlockUpdater } from 'lib/hooks/useBlockNumber'
import { BlockNumberProvider } from 'lib/hooks/useBlockNumber'
import { TokenListProvider } from 'lib/hooks/useTokenList'
import { Provider as I18nProvider } from 'lib/i18n'
import { MulticallUpdater, store as multicallStore } from 'lib/state/multicall'
import styled, { keyframes, Theme, ThemeProvider } from 'lib/theme'
@@ -29,7 +31,7 @@ const WidgetWrapper = styled.div<{ width?: number | string }>`
font-size: 16px;
font-smooth: always;
font-variant: none;
height: 356px;
height: 360px;
min-width: 300px;
padding: 0.25em;
position: relative;
@@ -80,21 +82,12 @@ const DialogWrapper = styled.div`
}
`
function Updaters() {
return (
<>
<BlockUpdater />
<MulticallUpdater />
<TransactionsUpdater />
</>
)
}
export type WidgetProps = {
theme?: Theme
locale?: SupportedLocale
provider?: Eip1193Provider | JsonRpcProvider
jsonRpcEndpoint?: string | JsonRpcProvider
tokenList?: string | TokenInfo[]
width?: string | number
dialog?: HTMLElement | null
className?: string
@@ -130,8 +123,11 @@ export default function Widget(props: PropsWithChildren<WidgetProps>) {
<ReduxProvider store={multicallStore}>
<AtomProvider>
<ActiveWeb3Provider provider={provider} jsonRpcEndpoint={jsonRpcEndpoint}>
<Updaters />
{children}
<BlockNumberProvider>
<MulticallUpdater />
<TransactionsUpdater />
<TokenListProvider list={props.tokenList}>{children}</TokenListProvider>
</BlockNumberProvider>
</ActiveWeb3Provider>
</AtomProvider>
</ReduxProvider>

View File

@@ -1,3 +1,4 @@
import { tokens } from '@uniswap/default-token-list'
import { initializeConnector } from '@web3-react/core'
import { MetaMask } from '@web3-react/metamask'
import { Connector } from '@web3-react/types'
@@ -75,6 +76,7 @@ export default function Wrapper({ children }: { children: ReactNode }) {
locale={locale}
jsonRpcEndpoint={jsonRpcEndpoint === NO_JSON_RPC ? undefined : jsonRpcEndpoint}
provider={connector?.provider}
tokenList={tokens}
>
{children}
</Widget>

View File

@@ -9,6 +9,6 @@ export const loadingCss = css`
// need to use isLoading as `loading` is a reserved prop
export const loadingTransitionCss = css<{ isLoading: boolean }>`
${({ isLoading }) => isLoading && loadingCss};
transition: opacity ${({ isLoading }) => (isLoading ? 0 : 0.2)}s ease-in-out;
opacity: ${({ isLoading }) => isLoading && loadingOpacity};
transition: color 0.125s linear, opacity ${({ isLoading }) => (isLoading ? 0 : 0.25)}s ease-in-out;
`

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, { Slippage } from 'lib/hooks/useSlippage'
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 { useBestTrade } from './useBestTrade'
import { INVALID_TRADE, useBestTrade } from './useBestTrade'
import useWrapCallback, { WrapType } from './useWrapCallback'
interface SwapField {
@@ -58,6 +57,12 @@ function useComputeSwapInfo(): SwapInfo {
[isExactIn, isWrapping, parsedAmount, trade.trade?.outputAmount]
)
const { account } = useActiveWeb3React()
const [balanceIn, balanceOut] = useCurrencyBalances(
account,
useMemo(() => [currencyIn, currencyOut], [currencyIn, currencyOut])
)
// Compute slippage and impact off of the trade so that it refreshes with the trade.
// (Using amountIn/amountOut would show (incorrect) intermediate values.)
const slippage = useSlippage(trade.trade)
@@ -66,68 +71,55 @@ function useComputeSwapInfo(): SwapInfo {
return useMemo(
() => ({
[Field.INPUT]: {
currency: currencyIn,
amount: amountIn,
balance: balanceIn,
usdc: inputUSDC,
},
[Field.OUTPUT]: {
currency: currencyOut,
amount: amountOut,
balance: balanceOut,
usdc: outputUSDC,
},
trade,
slippage,
impact,
}),
[amountIn, amountOut, impact, inputUSDC, outputUSDC, slippage, trade]
[
amountIn,
amountOut,
balanceIn,
balanceOut,
currencyIn,
currencyOut,
impact,
inputUSDC,
outputUSDC,
slippage,
trade,
]
)
}
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 {
const swapInfo = useAtomValue(swapInfoAtom)
const { [Field.INPUT]: currencyIn, [Field.OUTPUT]: currencyOut } = useAtomValue(swapAtom)
const tradeState = useMemo(() => {
const { trade } = swapInfo
if (trade.state === TradeState.VALID && trade.trade) {
const isTradeStale =
(currencyIn && !trade.trade.inputAmount.currency.equals(currencyIn)) ||
(currencyOut && !trade.trade.outputAmount.currency.equals(currencyOut))
// swapInfo has not yet caught up to swapAtom.
if (isTradeStale) return TradeState.LOADING
}
return trade.state
}, [currencyIn, currencyOut, swapInfo])
const { account } = useActiveWeb3React()
const [balanceIn, balanceOut] = useCurrencyBalances(
account,
useMemo(() => [currencyIn, currencyOut], [currencyIn, currencyOut])
)
// swapInfo will lag behind swapAtom by a frame, because its update is triggered by swapAtom
// so a swap must be marked as loading, with up-to-date currencies, during that update.
// In other words, swapInfo is derived from swapAtom, so it must be used as the source of truth.
return useMemo(
() => ({
...swapInfo,
trade: { ...swapInfo.trade, state: tradeState },
[Field.INPUT]: { ...swapInfo[Field.INPUT], currency: currencyIn, balance: balanceIn },
[Field.OUTPUT]: { ...swapInfo[Field.OUTPUT], currency: currencyOut, balance: balanceOut },
}),
[balanceIn, balanceOut, currencyIn, currencyOut, swapInfo, tradeState]
)
return useContext(SwapInfoContext)
}

View File

@@ -5,9 +5,10 @@ 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, useRef } from 'react'
import { useCallback, useRef } from 'react'
import useOnSupportedNetwork from '../useOnSupportedNetwork'
import { useIsTokenListLoaded } from '../useTokenList'
export type DefaultAddress = string | { [chainId: number]: string | 'NATIVE' } | 'NATIVE'
@@ -72,10 +73,9 @@ export default function useSyncTokenDefaults({
}, [defaultInputAmount, defaultInputToken, defaultOutputAmount, defaultOutputToken, updateSwap])
const lastChainId = useRef<number | undefined>(undefined)
useLayoutEffect(() => {
if (chainId && chainId !== lastChainId.current) {
setToDefaults()
}
const shouldSync = useIsTokenListLoaded() && chainId && chainId !== lastChainId.current
if (shouldSync) {
setToDefaults()
lastChainId.current = chainId
}, [chainId, setToDefaults])
}
}

View File

@@ -7,7 +7,7 @@ import { Url } from '@web3-react/url'
import { useAtom, WritableAtom } from 'jotai'
import { atom } from 'jotai'
import JsonRpcConnector from 'lib/utils/JsonRpcConnector'
import { createContext, PropsWithChildren, useContext, useEffect, useMemo, useState } from 'react'
import { createContext, PropsWithChildren, useContext, useEffect, useMemo } from 'react'
type Web3ContextType = {
connector: Connector
@@ -16,6 +16,7 @@ type Web3ContextType = {
accounts?: ReturnType<Web3ReactHooks['useAccounts']>
account?: ReturnType<Web3ReactHooks['useAccount']>
active?: ReturnType<Web3ReactHooks['useIsActive']>
activating?: ReturnType<Web3ReactHooks['useIsActivating']>
error?: ReturnType<Web3ReactHooks['useError']>
ensNames?: ReturnType<Web3ReactHooks['useENSNames']>
ensName?: ReturnType<Web3ReactHooks['useENSName']>
@@ -80,32 +81,20 @@ export function ActiveWeb3Provider({
const library = hooks.useProvider()
// TODO(zzmp): walletconnect returns chainId as a number, so web3-react incorrectly parses it as hex.
const [chainId, setChainId] = useState(hooks.useChainId())
useEffect(() => {
let stale = false
library?.getNetwork().then(({ chainId }) => {
if (!stale) {
setChainId(chainId)
}
})
return () => {
stale = true
}
}, [library])
const accounts = hooks.useAccounts()
const account = hooks.useAccount()
const activating = hooks.useIsActivating()
const active = hooks.useIsActive()
const error = hooks.useError()
const chainId = hooks.useChainId()
const ensNames = hooks.useENSNames()
const ensName = hooks.useENSName()
const error = hooks.useError()
const web3 = useMemo(() => {
if (connector === EMPTY || !active) {
if (connector === EMPTY || !(active || activating)) {
return EMPTY_CONTEXT
}
return { connector, library, chainId, accounts, account, active, error, ensNames, ensName }
}, [account, accounts, active, chainId, connector, ensName, ensNames, error, library])
return { connector, library, chainId, accounts, account, active, activating, error, ensNames, ensName }
}, [account, accounts, activating, active, chainId, connector, ensName, ensNames, error, library])
// Log web3 errors to facilitate debugging.
useEffect(() => {

View File

@@ -1,69 +0,0 @@
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import useIsWindowVisible from 'hooks/useIsWindowVisible'
import { atom } from 'jotai'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { useCallback, useEffect } from 'react'
interface ChainBlock {
chainId?: number
block?: number
}
const chainBlockAtom = atom<ChainBlock>({})
function useUpdateChainBlock() {
const { chainId, library } = useActiveWeb3React()
const windowVisible = useIsWindowVisible()
const setChainBlock = useUpdateAtom(chainBlockAtom)
const onBlock = useCallback(
(block: number) => {
setChainBlock((chainBlock) => {
if (chainBlock.chainId === chainId) {
if (!chainBlock.block || chainBlock.block < block) {
return { chainId, block }
}
}
return chainBlock
})
},
[chainId, setChainBlock]
)
useEffect(() => {
if (library && chainId && windowVisible) {
// If chainId hasn't changed, don't clear the block. This prevents re-fetching still valid data.
setChainBlock((chainBlock) => (chainBlock.chainId === chainId ? chainBlock : { chainId }))
library
.getBlockNumber()
.then(onBlock)
.catch((error) => {
console.error(`Failed to get block number for chainId ${chainId}`, error)
})
library.on('block', onBlock)
return () => {
library.removeListener('block', onBlock)
}
}
return undefined
}, [chainId, library, onBlock, setChainBlock, windowVisible])
}
export function BlockUpdater() {
useUpdateChainBlock()
return null
}
/** Requires that BlockUpdater be installed in the DOM tree. */
export default function useBlockNumber(): number | undefined {
const { chainId: activeChainId } = useActiveWeb3React()
const { chainId, block } = useAtomValue(chainBlockAtom)
return activeChainId === chainId ? block : undefined
}
export function useFastForwardBlockNumber(): (block: number) => void {
const { chainId } = useActiveWeb3React()
const setChainBlock = useUpdateAtom(chainBlockAtom)
return useCallback((block: number) => setChainBlock({ chainId, block }), [chainId, setChainBlock])
}

View File

@@ -0,0 +1,78 @@
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import useIsWindowVisible from 'hooks/useIsWindowVisible'
import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react'
const MISSING_PROVIDER = Symbol()
const BlockNumberContext = createContext<
| {
value?: number
fastForward(block: number): void
}
| typeof MISSING_PROVIDER
>(MISSING_PROVIDER)
function useBlockNumberContext() {
const blockNumber = useContext(BlockNumberContext)
if (blockNumber === MISSING_PROVIDER) {
throw new Error('BlockNumber hooks must be wrapped in a <BlockNumberProvider>')
}
return blockNumber
}
/** Requires that BlockUpdater be installed in the DOM tree. */
export default function useBlockNumber(): number | undefined {
return useBlockNumberContext().value
}
export function useFastForwardBlockNumber(): (block: number) => void {
return useBlockNumberContext().fastForward
}
export function BlockNumberProvider({ children }: { children: ReactNode }) {
const { chainId: activeChainId, library } = useActiveWeb3React()
const [{ chainId, block }, setChainBlock] = useState<{ chainId?: number; block?: number }>({ chainId: activeChainId })
const onBlock = useCallback(
(block: number) => {
setChainBlock((chainBlock) => {
if (chainBlock.chainId === activeChainId) {
if (!chainBlock.block || chainBlock.block < block) {
return { chainId: activeChainId, block }
}
}
return chainBlock
})
},
[activeChainId, setChainBlock]
)
const windowVisible = useIsWindowVisible()
useEffect(() => {
if (library && activeChainId && windowVisible) {
// If chainId hasn't changed, don't clear the block. This prevents re-fetching still valid data.
setChainBlock((chainBlock) => (chainBlock.chainId === activeChainId ? chainBlock : { chainId: activeChainId }))
library
.getBlockNumber()
.then(onBlock)
.catch((error) => {
console.error(`Failed to get block number for chainId ${activeChainId}`, error)
})
library.on('block', onBlock)
return () => {
library.removeListener('block', onBlock)
}
}
return undefined
}, [activeChainId, library, onBlock, setChainBlock, windowVisible])
const value = useMemo(
() => ({
value: chainId === activeChainId ? block : undefined,
fastForward: (block: number) => setChainBlock({ chainId: activeChainId, block }),
}),
[activeChainId, block, chainId]
)
return <BlockNumberContext.Provider value={value}>{children}</BlockNumberContext.Provider>
}

View File

@@ -5,7 +5,7 @@ import useActiveWeb3React from './useActiveWeb3React'
function useOnSupportedNetwork() {
const { chainId } = useActiveWeb3React()
return useMemo(() => chainId && ALL_SUPPORTED_CHAIN_IDS.includes(chainId), [chainId])
return useMemo(() => Boolean(chainId && ALL_SUPPORTED_CHAIN_IDS.includes(chainId)), [chainId])
}
export default useOnSupportedNetwork

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,10 +1,8 @@
import { NativeCurrency, Token } from '@uniswap/sdk-core'
import { TokenInfo, TokenList } from '@uniswap/token-lists'
import { atom, useAtom } from 'jotai'
import { useAtomValue } from 'jotai/utils'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import resolveENSContentHash from 'lib/utils/resolveENSContentHash'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { createContext, PropsWithChildren, useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import fetchTokenList from './fetchTokenList'
@@ -14,19 +12,61 @@ import { validateTokens } from './validateTokenList'
export const DEFAULT_TOKEN_LIST = 'https://gateway.ipfs.io/ipns/tokens.uniswap.org'
const chainTokenMapAtom = atom<ChainTokenMap | null>(null)
const MISSING_PROVIDER = Symbol()
const ChainTokenMapContext = createContext<ChainTokenMap | undefined | typeof MISSING_PROVIDER>(MISSING_PROVIDER)
export function useIsTokenListLoaded() {
return Boolean(useAtomValue(chainTokenMapAtom))
function useChainTokenMapContext() {
const chainTokenMap = useContext(ChainTokenMapContext)
if (chainTokenMap === MISSING_PROVIDER) {
throw new Error('TokenList hooks must be wrapped in a <TokenListProvider>')
}
return chainTokenMap
}
export function useSyncTokenList(list: string | TokenInfo[] = DEFAULT_TOKEN_LIST): void {
export function useIsTokenListLoaded() {
return Boolean(useChainTokenMapContext())
}
export default function useTokenList(): WrappedTokenInfo[] {
const { chainId } = useActiveWeb3React()
const chainTokenMap = useChainTokenMapContext()
const tokenMap = chainId && chainTokenMap?.[chainId]
return useMemo(() => {
if (!tokenMap) return []
return Object.values(tokenMap).map(({ token }) => token)
}, [tokenMap])
}
export type TokenMap = { [address: string]: Token }
export function useTokenMap(): TokenMap {
const { chainId } = useActiveWeb3React()
const chainTokenMap = useChainTokenMapContext()
const tokenMap = chainId && chainTokenMap?.[chainId]
return useMemo(() => {
if (!tokenMap) return {}
return Object.entries(tokenMap).reduce((map, [address, { token }]) => {
map[address] = token
return map
}, {} as TokenMap)
}, [tokenMap])
}
export function useQueryCurrencies(query = ''): (WrappedTokenInfo | NativeCurrency)[] {
return useQueryTokens(query, useTokenList())
}
export function TokenListProvider({
list = DEFAULT_TOKEN_LIST,
children,
}: PropsWithChildren<{ list?: string | TokenInfo[] }>) {
// Error boundaries will not catch (non-rendering) async errors, but it should still be shown
const [error, setError] = useState<Error>()
if (error) throw error
const [chainTokenMap, setChainTokenMap] = useAtom(chainTokenMapAtom)
useEffect(() => setChainTokenMap(null), [list, setChainTokenMap])
const [chainTokenMap, setChainTokenMap] = useState<ChainTokenMap>()
useEffect(() => setChainTokenMap(undefined), [list])
const { chainId, library } = useActiveWeb3React()
const resolver = useCallback(
@@ -70,34 +110,7 @@ export function useSyncTokenList(list: string | TokenInfo[] = DEFAULT_TOKEN_LIST
}
}
}
}, [chainTokenMap, list, resolver, setChainTokenMap])
}
}, [chainTokenMap, list, resolver])
export default function useTokenList(): WrappedTokenInfo[] {
const { chainId } = useActiveWeb3React()
const chainTokenMap = useAtomValue(chainTokenMapAtom)
const tokenMap = chainId && chainTokenMap?.[chainId]
return useMemo(() => {
if (!tokenMap) return []
return Object.values(tokenMap).map(({ token }) => token)
}, [tokenMap])
}
export type TokenMap = { [address: string]: Token }
export function useTokenMap(): TokenMap {
const { chainId } = useActiveWeb3React()
const chainTokenMap = useAtomValue(chainTokenMapAtom)
const tokenMap = chainId && chainTokenMap?.[chainId]
return useMemo(() => {
if (!tokenMap) return {}
return Object.entries(tokenMap).reduce((map, [address, { token }]) => {
map[address] = token
return map
}, {} as TokenMap)
}, [tokenMap])
}
export function useQueryCurrencies(query = ''): (WrappedTokenInfo | NativeCurrency)[] {
return useQueryTokens(query, useTokenList())
return <ChainTokenMapContext.Provider value={chainTokenMap}>{children}</ChainTokenMapContext.Provider>
}

View File

@@ -336,11 +336,11 @@ export function PositionPage({
const removed = liquidity?.eq(0)
const metadata = usePositionTokenURI(parsedTokenId)
const token0 = useToken(token0Address)
const token1 = useToken(token1Address)
const metadata = usePositionTokenURI(parsedTokenId)
const currency0 = token0 ? unwrappedToken(token0) : undefined
const currency1 = token1 ? unwrappedToken(token1) : undefined
@@ -389,6 +389,10 @@ export function PositionPage({
// fees
const [feeValue0, feeValue1] = useV3PositionFees(pool ?? undefined, positionDetails?.tokenId, receiveWETH)
// these currencies will match the feeValue{0,1} currencies for the purposes of fee collection
const currency0ForFeeCollectionPurposes = pool ? (receiveWETH ? pool.token0 : unwrappedToken(pool.token0)) : undefined
const currency1ForFeeCollectionPurposes = pool ? (receiveWETH ? pool.token1 : unwrappedToken(pool.token1)) : undefined
const [collecting, setCollecting] = useState<boolean>(false)
const [collectMigrationHash, setCollectMigrationHash] = useState<string | null>(null)
const isCollectPending = useIsTransactionPending(collectMigrationHash ?? undefined)
@@ -422,14 +426,25 @@ export function PositionPage({
const addTransaction = useTransactionAdder()
const positionManager = useV3NFTPositionManagerContract()
const collect = useCallback(() => {
if (!chainId || !feeValue0 || !feeValue1 || !positionManager || !account || !tokenId || !library) return
if (
!currency0ForFeeCollectionPurposes ||
!currency1ForFeeCollectionPurposes ||
!chainId ||
!positionManager ||
!account ||
!tokenId ||
!library
)
return
setCollecting(true)
// we fall back to expecting 0 fees in case the fetch fails, which is safe in the
// vast majority of cases
const { calldata, value } = NonfungiblePositionManager.collectCallParameters({
tokenId: tokenId.toString(),
expectedCurrencyOwed0: feeValue0,
expectedCurrencyOwed1: feeValue1,
expectedCurrencyOwed0: feeValue0 ?? CurrencyAmount.fromRawAmount(currency0ForFeeCollectionPurposes, 0),
expectedCurrencyOwed1: feeValue1 ?? CurrencyAmount.fromRawAmount(currency1ForFeeCollectionPurposes, 0),
recipient: account,
})
@@ -458,13 +473,13 @@ export function PositionPage({
ReactGA.event({
category: 'Liquidity',
action: 'CollectV3',
label: [feeValue0.currency.symbol, feeValue1.currency.symbol].join('/'),
label: [currency0ForFeeCollectionPurposes.symbol, currency1ForFeeCollectionPurposes.symbol].join('/'),
})
addTransaction(response, {
type: TransactionType.COLLECT_FEES,
currencyId0: currencyId(feeValue0.currency),
currencyId1: currencyId(feeValue1.currency),
currencyId0: currencyId(currency0ForFeeCollectionPurposes),
currencyId1: currencyId(currency1ForFeeCollectionPurposes),
})
})
})
@@ -472,7 +487,18 @@ export function PositionPage({
setCollecting(false)
console.error(error)
})
}, [chainId, feeValue0, feeValue1, positionManager, account, tokenId, addTransaction, library])
}, [
chainId,
feeValue0,
feeValue1,
currency0ForFeeCollectionPurposes,
currency1ForFeeCollectionPurposes,
positionManager,
account,
tokenId,
addTransaction,
library,
])
const owner = useSingleCallResult(!!tokenId ? positionManager : null, 'ownerOf', [tokenId]).result?.[0]
const ownsNFT = owner === account || positionDetails?.operator === account

View File

@@ -1,7 +1,7 @@
import { BigNumber } from '@ethersproject/bignumber'
import { TransactionResponse } from '@ethersproject/providers'
import { Trans } from '@lingui/macro'
import { Percent } from '@uniswap/sdk-core'
import { CurrencyAmount, Percent } from '@uniswap/sdk-core'
import { NonfungiblePositionManager } from '@uniswap/v3-sdk'
import RangeBadge from 'components/Badge/RangeBadge'
import { ButtonConfirmed, ButtonPrimary } from 'components/Button'
@@ -109,8 +109,6 @@ function Remove({ tokenId }: { tokenId: BigNumber }) {
!deadline ||
!account ||
!chainId ||
!feeValue0 ||
!feeValue1 ||
!positionSDK ||
!liquidityPercentage ||
!library
@@ -118,14 +116,16 @@ function Remove({ tokenId }: { tokenId: BigNumber }) {
return
}
// we fall back to expecting 0 fees in case the fetch fails, which is safe in the
// vast majority of cases
const { calldata, value } = NonfungiblePositionManager.removeCallParameters(positionSDK, {
tokenId: tokenId.toString(),
liquidityPercentage,
slippageTolerance: allowedSlippage,
deadline: deadline.toString(),
collectOptions: {
expectedCurrencyOwed0: feeValue0,
expectedCurrencyOwed1: feeValue1,
expectedCurrencyOwed0: feeValue0 ?? CurrencyAmount.fromRawAmount(liquidityValue0.currency, 0),
expectedCurrencyOwed1: feeValue1 ?? CurrencyAmount.fromRawAmount(liquidityValue1.currency, 0),
recipient: account,
},
})

635
yarn.lock

File diff suppressed because it is too large Load Diff