Compare commits

...

35 Commits

Author SHA1 Message Date
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
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
Zach Pomerantz
600aeaaff1 fix: polling memory leak (#3676)
* chore: clarify stale callback

* fix: polling memory leak
2022-04-06 17:21:44 -07:00
Zach Pomerantz
3bfbc74e47 chore: bump to v1.0.2 (#3675) 2022-04-06 13:05:06 -07:00
Zach Pomerantz
84f76e34b2 fix: do not fetch wrap price (#3673)
* fix: do not fetch wrap price

* fix: abort trade computation for wraps
2022-04-06 13:04:16 -07:00
Zach Pomerantz
b965bed865 fix: token input height (#3672) 2022-04-06 12:14:15 -07:00
Zach Pomerantz
a9039e8d0b chore: bump to v1.0.1 (#3670) 2022-04-06 10:02:37 -07:00
Zach Pomerantz
60d35b46f3 fix: simplify validation (#3665)
* fix: simplify widget validation

* test: update cosmos to trigger edge cases

* fix: simplify swap validation
2022-04-06 09:21:50 -07:00
Ian Lapham
3d422cf707 update address list (#3669) 2022-04-06 11:48:47 -04:00
Zach Pomerantz
84c70ac84d chore: bump to v1.0.0 (#3663) 2022-04-05 10:45:36 -07:00
Zach Pomerantz
de3a33dfcb fix: stale data edge cases (#3657)
* fix: stale chain block

* chore: simplify atom usage

* fix: support single-token chain

* fix: avoid extra rpcs

* chore: rename isDisabled

* fix: simplify useUSDCPrice

* fix: simplify useComputeSwapInfo

* chore: include type

* fix: guard hasAmounts
2022-04-05 10:45:21 -07:00
Zach Pomerantz
99a084f230 fix: JsonRpc url wrapper (#3662)
* fix: JsonRpc url wrapper

* chore: finish renaming
2022-04-05 10:00:17 -07:00
Jordan Frankfurt
e880955743 chore(widgets): bump version (#3645) 2022-04-01 18:06:12 -04:00
Zach Pomerantz
bbf43fcd27 fix: walletconnect numeric chain id (#3643) 2022-04-01 10:57:11 -07:00
Zach Pomerantz
a00ac56389 chore: bump to v0.0.30-beta (#3637) 2022-03-31 14:49:44 -07:00
Noah Zinsmeister
7201944bc2 Revert "fix(error handling): try reloading the app when encountering a javascript error (#3435)"
This reverts commit 5cf9e84db5.
2022-03-31 17:24:47 -04:00
49 changed files with 552 additions and 434 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@uniswap/widgets",
"version": "0.0.26-beta",
"version": "1.0.6",
"description": "Uniswap Interface",
"homepage": ".",
"files": [
@@ -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",
@@ -205,11 +205,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

@@ -33,6 +33,7 @@ const BLOCKED_ADDRESSES: string[] = [
'0x5512d943ed1f7c8a43f3435c85f7ab68b30121b0',
'0xc455f7fd3e0e12afd51fba5c106909934d8a0e4a',
'0x629e7Da20197a5429d30da36E77d06CdF796b71A',
'0x7FF9cFad3877F21d41Da833E2F775dB0569eE3D9',
]
export default function Blocklist({ children }: { children: ReactNode }) {

View File

@@ -49,9 +49,11 @@ type ErrorBoundaryState = {
const IS_UNISWAP = window.location.hostname === 'app.uniswap.org'
async function updateServiceWorker(): Promise<void> {
async function updateServiceWorker(): Promise<ServiceWorkerRegistration> {
const ready = await navigator.serviceWorker.ready
await ready.update()
// 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> {
@@ -62,8 +64,19 @@ export default class ErrorBoundary extends React.Component<unknown, ErrorBoundar
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
updateServiceWorker()
.then(() => {
window.location.reload()
.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)

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
@@ -41,10 +42,10 @@ export default function useAutoSlippageTolerance(
const gasEstimate = guesstimateGas(trade)
const nativeCurrency = useNativeCurrency()
const nativeCurrencyPrice = useUSDCPrice(nativeCurrency ?? undefined)
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

@@ -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,30 +0,0 @@
import { SUPPORTED_LOCALES } from 'constants/locales'
import { WidgetProps } from 'lib/components/Widget'
import { IntegrationError } from 'lib/errors'
import { PropsWithChildren, useEffect } from 'react'
export default function WidgetsPropsValidator(props: PropsWithChildren<WidgetProps>) {
const { jsonRpcEndpoint, provider } = props
useEffect(() => {
if (!provider && !jsonRpcEndpoint) {
throw new IntegrationError('This widget requires a provider or jsonRpcEndpoint.')
}
}, [provider, jsonRpcEndpoint])
const { width } = props
useEffect(() => {
if (width && width < 300) {
throw new IntegrationError(`Set widget width to at least 300px. (You set it to ${width}.)`)
}
}, [width])
const { locale } = props
useEffect(() => {
if (locale && locale !== 'pseudo' && !SUPPORTED_LOCALES.includes(locale)) {
console.warn('Unsupported locale: ', locale)
}
}, [locale])
return null
}

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

@@ -18,7 +18,8 @@ const TokenInputRow = styled(Row)`
const ValueInput = styled(DecimalInput)`
color: ${({ theme }) => theme.primary};
height: 1em;
height: 1.5em;
margin: -0.25em 0;
:hover:not(:focus-within) {
color: ${({ theme }) => theme.onHover(theme.primary)};

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,17 +1,15 @@
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 useTokenList, { useSyncTokenList } from 'lib/hooks/useTokenList'
import { displayTxHashAtom } from 'lib/state/swap'
import { SwapTransactionInfo, Transaction, TransactionType, WrapTransactionInfo } from 'lib/state/transactions'
import { useMemo, useState } from 'react'
import { useState } from 'react'
import Dialog from '../Dialog'
import Header from '../Header'
@@ -23,8 +21,8 @@ import ReverseButton from './ReverseButton'
import Settings from './Settings'
import { StatusDialog } from './Status'
import SwapButton from './SwapButton'
import SwapPropValidator from './SwapPropValidator'
import Toolbar from './Toolbar'
import useValidate from './useValidate'
function getTransactionFromMap(
txs: { [hash: string]: Transaction },
@@ -43,19 +41,14 @@ function getTransactionFromMap(
}
export interface SwapProps extends TokenDefaults, FeeOptions {
tokenList?: string | TokenInfo[]
onConnectWallet?: () => void
}
function Updaters(props: SwapProps & { disabled: boolean }) {
useSyncTokenList(props.tokenList)
useSyncTokenDefaults(props)
useSyncConvenienceFee(props)
return props.disabled ? null : <SwapInfoUpdater />
}
export default function Swap(props: SwapProps) {
useValidate(props)
useSyncConvenienceFee(props)
useSyncTokenDefaults(props)
const { active, account } = useActiveWeb3React()
const [wrapper, setWrapper] = useState<HTMLDivElement | null>(null)
@@ -63,33 +56,27 @@ export default function Swap(props: SwapProps) {
const pendingTxs = usePendingTransactions()
const displayTx = getTransactionFromMap(pendingTxs, displayTxHash)
const tokenList = useTokenList()
const onSupportedNetwork = useOnSupportedNetwork()
const isSwapSupported = useMemo(
() => Boolean(active && onSupportedNetwork && tokenList?.length),
[active, onSupportedNetwork, tokenList?.length]
)
const isDisabled = !(active && onSupportedNetwork)
const focused = useHasFocus(wrapper)
const isInteractive = Boolean(active && onSupportedNetwork)
return (
<>
<SwapPropValidator {...props} />
<Updaters {...props} disabled={!isSwapSupported} />
<Header title={<Trans>Swap</Trans>}>
{active && <Wallet disabled={!account} onClick={props.onConnectWallet} />}
<Settings disabled={!isInteractive} />
<Settings disabled={isDisabled} />
</Header>
<div ref={setWrapper}>
<BoundaryProvider value={wrapper}>
<Input disabled={!isInteractive} focused={focused} />
<ReverseButton disabled={!isInteractive} />
<Output disabled={!isInteractive} focused={focused}>
<Toolbar disabled={!active} />
<SwapButton disabled={!isSwapSupported} />
</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

@@ -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'
@@ -18,12 +17,12 @@ function isAddressOrAddressMap(addressOrMap: DefaultAddress): boolean {
type ValidatorProps = PropsWithChildren<TokenDefaults & FeeOptions>
export default function SwapPropValidator(props: ValidatorProps) {
export default function useValidate(props: ValidatorProps) {
const { convenienceFee, convenienceFeeRecipient } = props
useEffect(() => {
if (convenienceFee) {
if (convenienceFee > 100 || convenienceFee < 0) {
throw new IntegrationError(`convenienceFee must be between 0 and 100. (You set it to ${convenienceFee})`)
throw new IntegrationError(`convenienceFee must be between 0 and 100 (you set it to ${convenienceFee}).`)
}
if (!convenienceFeeRecipient) {
throw new IntegrationError('convenienceFeeRecipient is required when convenienceFee is set.')
@@ -32,7 +31,7 @@ export default function SwapPropValidator(props: ValidatorProps) {
if (typeof convenienceFeeRecipient === 'string') {
if (!isAddress(convenienceFeeRecipient)) {
throw new IntegrationError(
`convenienceFeeRecipient must be a valid address. (You set it to ${convenienceFeeRecipient}.)`
`convenienceFeeRecipient must be a valid address (you set it to ${convenienceFeeRecipient}).`
)
}
} else if (typeof convenienceFeeRecipient === 'object') {
@@ -40,7 +39,7 @@ export default function SwapPropValidator(props: ValidatorProps) {
if (!isAddress(recipient)) {
const values = Object.values(convenienceFeeRecipient).join(', ')
throw new IntegrationError(
`All values in convenienceFeeRecipient object must be valid addresses. (You used ${values}.)`
`All values in convenienceFeeRecipient object must be valid addresses (you used ${values}).`
)
}
})
@@ -48,26 +47,30 @@ export default function SwapPropValidator(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)) {
throw new IntegrationError(`defaultInputAmount must be a positive number. (You set it to ${defaultInputAmount})`)
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})`
`defaultOutputAmount must be a positive number (you set it to ${defaultOutputAmount}).`
)
}
}, [defaultInputAmount, defaultOutputAmount])
const { defaultInputTokenAddress, defaultOutputTokenAddress } = props
useEffect(() => {
if (
defaultInputTokenAddress &&
!isAddressOrAddressMap(defaultInputTokenAddress) &&
defaultInputTokenAddress !== 'NATIVE'
) {
throw new IntegrationError(
`defaultInputTokenAddress(es) must be a valid address or "NATIVE". (You set it to ${defaultInputTokenAddress}`
`defaultInputTokenAddress must be a valid address or "NATIVE" (you set it to ${defaultInputTokenAddress}).`
)
}
if (
@@ -76,10 +79,8 @@ export default function SwapPropValidator(props: ValidatorProps) {
defaultOutputTokenAddress !== 'NATIVE'
) {
throw new IntegrationError(
`defaultOutputTokenAddress(es) must be a valid address or "NATIVE". (You set it to ${defaultOutputTokenAddress}`
`defaultOutputTokenAddress must be a valid address or "NATIVE" (you set it to ${defaultOutputTokenAddress}).`
)
}
}, [defaultInputTokenAddress, defaultInputAmount, defaultOutputTokenAddress, defaultOutputAmount])
return null
}, [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

@@ -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,20 +1,21 @@
import { JsonRpcProvider } from '@ethersproject/providers'
import { TokenInfo } from '@uniswap/token-lists'
import { Provider as Eip1193Provider } from '@web3-react/types'
import { DEFAULT_LOCALE, SupportedLocale } from 'constants/locales'
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'
import { UNMOUNTING } from 'lib/utils/animations'
import { PropsWithChildren, StrictMode, useState } from 'react'
import { PropsWithChildren, StrictMode, useMemo, useState } from 'react'
import { Provider as ReduxProvider } from 'react-redux'
import { Modal, Provider as DialogProvider } from './Dialog'
import ErrorBoundary, { ErrorHandler } from './Error/ErrorBoundary'
import WidgetPropValidator from './Error/WidgetsPropsValidator'
const WidgetWrapper = styled.div<{ width?: number | string }>`
-moz-osx-font-smoothing: grayscale;
@@ -30,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;
@@ -81,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
@@ -103,17 +95,22 @@ export type WidgetProps = {
}
export default function Widget(props: PropsWithChildren<WidgetProps>) {
const {
children,
theme,
locale = DEFAULT_LOCALE,
provider,
jsonRpcEndpoint,
width = 360,
dialog: userDialog,
className,
onError,
} = props
const { children, theme, provider, jsonRpcEndpoint, dialog: userDialog, className, onError } = props
const width = useMemo(() => {
if (props.width && props.width < 300) {
console.warn(`Widget width must be at least 300px (you set it to ${props.width}). Falling back to 300px.`)
return 300
}
return props.width ?? 360
}, [props.width])
const locale = useMemo(() => {
if (props.locale && ![...SUPPORTED_LOCALES, 'pseudo'].includes(props.locale)) {
console.warn(`Unsupported locale: ${props.locale}. Falling back to ${DEFAULT_LOCALE}.`)
return DEFAULT_LOCALE
}
return props.locale ?? DEFAULT_LOCALE
}, [props.locale])
const [dialog, setDialog] = useState<HTMLDivElement | null>(null)
return (
<StrictMode>
@@ -123,12 +120,14 @@ export default function Widget(props: PropsWithChildren<WidgetProps>) {
<DialogWrapper ref={setDialog} />
<DialogProvider value={userDialog || dialog}>
<ErrorBoundary onError={onError}>
<WidgetPropValidator {...props} />
<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'
@@ -17,14 +18,15 @@ const [walletConnect] = initializeConnector<WalletConnect>(
export default function Wrapper({ children }: { children: ReactNode }) {
const [width] = useValue('width', { defaultValue: 360 })
const [locale] = useSelect('locale', { defaultValue: DEFAULT_LOCALE, options: ['pseudo', ...SUPPORTED_LOCALES] })
const [locale] = useSelect('locale', {
defaultValue: DEFAULT_LOCALE,
options: ['fa-KE (unsupported)', 'pseudo', ...SUPPORTED_LOCALES],
})
const [darkMode] = useValue('dark mode', { defaultValue: false })
const [theme, setTheme] = useValue('theme', { defaultValue: { ...defaultTheme, ...lightTheme } })
useEffect(() => {
setTheme({ ...defaultTheme, ...(darkMode ? darkTheme : lightTheme) })
// cosmos does not maintain referential equality for setters
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [darkMode])
}, [darkMode, setTheme])
const NO_JSON_RPC = 'None'
const [jsonRpcEndpoint] = useSelect('JSON-RPC', {
@@ -74,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

@@ -92,7 +92,7 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
const getIsValidBlock = useGetIsValidBlock()
const { data: quoteResult, error } = usePoll(getQuoteResult, JSON.stringify(queryArgs), {
debounce: isDebouncing,
staleCallback: useCallback(({ data }) => !getIsValidBlock(Number(data?.blockNumber) || 0), [getIsValidBlock]),
isStale: useCallback(({ data }) => !getIsValidBlock(Number(data?.blockNumber) || 0), [getIsValidBlock]),
}) ?? {
error: undefined,
}

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)
@@ -43,10 +41,11 @@ function useComputeSwapInfo(): SwapInfo {
() => tryParseCurrencyAmount(amount, (isExactIn ? currencyIn : currencyOut) ?? undefined),
[amount, isExactIn, currencyIn, currencyOut]
)
const hasAmounts = currencyIn && currencyOut && parsedAmount && !isWrapping
const trade = useBestTrade(
isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT,
parsedAmount,
(isExactIn ? currencyOut : currencyIn) ?? undefined
hasAmounts ? parsedAmount : undefined,
hasAmounts ? (isExactIn ? currencyOut : currencyIn) : undefined
)
const amountIn = useMemo(
@@ -57,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])
@@ -101,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), [swapInfo, setSwapInfo])
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,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, useState } from 'react'
import { useCallback, useRef } from 'react'
import useOnSupportedNetwork from '../useOnSupportedNetwork'
import { useIsTokenListLoaded } from '../useTokenList'
export type DefaultAddress = string | { [chainId: number]: string | 'NATIVE' } | 'NATIVE'
@@ -71,13 +72,10 @@ export default function useSyncTokenDefaults({
updateSwap((swap) => ({ ...swap, ...defaultSwapState }))
}, [defaultInputAmount, defaultInputToken, defaultOutputAmount, defaultOutputToken, updateSwap])
const [previousChainId, setPreviousChainId] = useState(chainId)
useLayoutEffect(() => {
setPreviousChainId(chainId)
}, [chainId])
useLayoutEffect(() => {
if (chainId && chainId !== previousChainId) {
setToDefaults()
}
}, [chainId, previousChainId, setToDefaults])
const lastChainId = useRef<number | undefined>(undefined)
const shouldSync = useIsTokenListLoaded() && chainId && chainId !== lastChainId.current
if (shouldSync) {
setToDefaults()
lastChainId.current = chainId
}
}

View File

@@ -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']>
@@ -23,7 +24,7 @@ type Web3ContextType = {
const EMPTY_CONNECTOR = initializeConnector(() => EMPTY)
const EMPTY_CONTEXT: Web3ContextType = { connector: EMPTY }
const urlConnectorAtom = atom<[Connector, Web3ReactHooks, Web3ReactStore]>(EMPTY_CONNECTOR)
const jsonRpcConnectorAtom = atom<[Connector, Web3ReactHooks, Web3ReactStore]>(EMPTY_CONNECTOR)
const injectedConnectorAtom = atom<[Connector, Web3ReactHooks, Web3ReactStore]>(EMPTY_CONNECTOR)
const Web3Context = createContext(EMPTY_CONTEXT)
@@ -69,23 +70,31 @@ export function ActiveWeb3Provider({
return EIP1193
}, [provider]) as { new (actions: Actions, initializer: typeof provider): Connector }
const injectedConnector = useConnector(injectedConnectorAtom, Injected, provider)
const urlConnector = useConnector(urlConnectorAtom, Url, jsonRpcEndpoint)
const [connector, hooks] = injectedConnector[1].useIsActive() ? injectedConnector : urlConnector ?? EMPTY_CONNECTOR
const JsonRpc = useMemo(() => {
if (JsonRpcProvider.isProvider(jsonRpcEndpoint)) return JsonRpcConnector
return Url
}, [jsonRpcEndpoint]) as { new (actions: Actions, initializer: typeof jsonRpcEndpoint): Connector }
const jsonRpcConnector = useConnector(jsonRpcConnectorAtom, JsonRpc, jsonRpcEndpoint)
const [connector, hooks] = injectedConnector[1].useIsActive()
? injectedConnector
: jsonRpcConnector ?? EMPTY_CONNECTOR
const library = hooks.useProvider()
const chainId = hooks.useChainId()
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,70 +0,0 @@
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import useDebounce from 'hooks/useDebounce'
import useIsWindowVisible from 'hooks/useIsWindowVisible'
import { atom } from 'jotai'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { useCallback, useEffect, useState } from 'react'
function useBlock() {
const { chainId, library } = useActiveWeb3React()
const windowVisible = useIsWindowVisible()
const [state, setState] = useState<{ chainId?: number; block?: number }>({ chainId })
const onBlock = useCallback(
(block: number) => {
setState((state) => {
if (state.chainId === chainId) {
if (typeof state.block !== 'number') return { chainId, block }
return { chainId, block: Math.max(block, state.block) }
}
return state
})
},
[chainId]
)
useEffect(() => {
if (library && chainId && windowVisible) {
// If chainId hasn't changed, don't clear the block. This prevents re-fetching still valid data.
setState((state) => (state.chainId === chainId ? state : { 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, windowVisible])
const debouncedBlock = useDebounce(state.block, 100)
return state.block ? debouncedBlock : undefined
}
const blockAtom = atom<number | undefined>(undefined)
export function BlockUpdater() {
const setBlock = useUpdateAtom(blockAtom)
const block = useBlock()
useEffect(() => {
setBlock(block)
}, [block, setBlock])
return null
}
/** Requires that BlockUpdater be installed in the DOM tree. */
export default function useBlockNumber(): number | undefined {
const { chainId } = useActiveWeb3React()
const block = useAtomValue(blockAtom)
return chainId ? block : undefined
}
export function useFastForwardBlockNumber(): (block: number) => void {
return useUpdateAtom(blockAtom)
}

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

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

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

@@ -9,7 +9,7 @@ interface PollingOptions<T> {
debounce?: boolean
// If stale, any cached result will be returned, and a new fetch will be initiated.
staleCallback?: (value: T) => boolean
isStale?: (value: T) => boolean
pollingInterval?: number
keepUnusedDataFor?: number
@@ -25,7 +25,7 @@ export default function usePoll<T>(
key = '',
{
debounce = false,
staleCallback,
isStale,
pollingInterval = DEFAULT_POLLING_INTERVAL,
keepUnusedDataFor = DEFAULT_KEEP_UNUSED_DATA_FOR,
}: PollingOptions<T>
@@ -39,11 +39,10 @@ export default function usePoll<T>(
let timeout: number
const entry = cache.get(key)
const isStale = staleCallback && entry?.result !== undefined ? staleCallback(entry.result) : false
if (entry) {
// If there is not a pending fetch (and there should be), queue one.
if (entry.ttl) {
if (isStale) {
if (isStale && entry?.result !== undefined ? isStale(entry.result) : false) {
poll() // stale results should be refetched immediately
} else if (entry.ttl && entry.ttl + keepUnusedDataFor > Date.now()) {
timeout = setTimeout(poll, Math.max(0, entry.ttl - Date.now()))
@@ -57,6 +56,7 @@ export default function usePoll<T>(
return () => {
clearTimeout(timeout)
timeout = 0
}
async function poll(ttl = Date.now() + pollingInterval) {
@@ -66,9 +66,9 @@ export default function usePoll<T>(
// Always set the result in the cache, but only set it as data if the key is still being queried.
const result = await fetch()
cache.set(key, { ttl, result })
setData((data) => (data.key === key ? { key, result } : data))
if (timeout) setData((data) => (data.key === key ? { key, result } : data))
}
}, [cache, debounce, fetch, keepUnusedDataFor, key, pollingInterval, staleCallback])
}, [cache, debounce, fetch, isStale, keepUnusedDataFor, key, pollingInterval])
useEffect(() => {
// Cleanup stale entries when a new key is used.

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

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

View File

@@ -11,8 +11,8 @@ export const store = createStore(reducer)
export default multicall
export function MulticallUpdater() {
const latestBlockNumber = useBlockNumber()
const { chainId } = useActiveWeb3React()
const latestBlockNumber = useBlockNumber()
const contract = useInterfaceMulticall()
return <multicall.Updater chainId={chainId} latestBlockNumber={latestBlockNumber} contract={contract} />
}

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,
},
})

110
yarn.lock
View File

@@ -5235,74 +5235,74 @@
dependencies:
"@web3-react/types" "^6.0.7"
"@web3-react/core@^8.0.22-beta.0":
version "8.0.22-beta.0"
resolved "https://registry.yarnpkg.com/@web3-react/core/-/core-8.0.22-beta.0.tgz#4a198a1ad9abe76af69d853b07a80dd2f9463b93"
integrity sha512-WfMV6VOK+cJ6RoOMtXpcb8PCLRQAUWwZPADzY8n1tYL+5LaH/dkFoeGyAanrIYiNA/EfbvE/T8fP0QD6ooMx7g==
"@web3-react/core@^8.0.23-beta.0":
version "8.0.23-beta.0"
resolved "https://registry.yarnpkg.com/@web3-react/core/-/core-8.0.23-beta.0.tgz#3a33234fbe25459ae2ce82b8bada5077386961f1"
integrity sha512-NmVWUKSzYr+Yk0nbZdOLMtidtgXs6qOCR1DcxLF4Aa0zJ/8hRrX23siFfrVNTSpck4fgyURv8QVzE5+VHQx2HA==
dependencies:
"@web3-react/store" "^8.0.16-beta.0"
"@web3-react/types" "^8.0.11-beta.0"
zustand "^4.0.0-beta.2"
"@web3-react/store" "^8.0.17-beta.0"
"@web3-react/types" "^8.0.12-beta.0"
zustand "^4.0.0-beta.3"
optionalDependencies:
"@ethersproject/providers" "^5"
"@web3-react/eip1193@^8.0.17-beta.0":
version "8.0.17-beta.0"
resolved "https://registry.yarnpkg.com/@web3-react/eip1193/-/eip1193-8.0.17-beta.0.tgz#84d1eb6bfbc04d4c5be19130daf9be1b3aeb962d"
integrity sha512-PMy4UXpe/4QM2arKykyYY6iLgO6sQEmiHXuEXUxeU6r6V/G9Eq5bv6Yfo8owd9QJEMlYWgR4v8rpx8VCMqOdKw==
dependencies:
"@web3-react/types" "^8.0.11-beta.0"
"@web3-react/empty@^8.0.11-beta.0":
version "8.0.11-beta.0"
resolved "https://registry.yarnpkg.com/@web3-react/empty/-/empty-8.0.11-beta.0.tgz#2905654cd4e608fa3a7ccfed4e971e446c6768e8"
integrity sha512-/zMps6bgG+17rEQS0b4HCBHqo6xuEtiJnoO9z0uPKFXrS6dJ5X85chyq6MTmZ+rieeG8O/NILn6/eEJPuCAkcg==
dependencies:
"@web3-react/types" "^8.0.11-beta.0"
"@web3-react/metamask@^8.0.18-beta.0":
"@web3-react/eip1193@^8.0.18-beta.0":
version "8.0.18-beta.0"
resolved "https://registry.yarnpkg.com/@web3-react/metamask/-/metamask-8.0.18-beta.0.tgz#7ce17c1f86af7ba042e56f0ecc93f0f688c9a646"
integrity sha512-40sWOJnYIO5u007GMqjiXvgdRAi02WCnlRcMTU2UhJpAtqALNKG14Gyd01vPMAyVYlVmsoGHr5mkFE/i75BpAA==
resolved "https://registry.yarnpkg.com/@web3-react/eip1193/-/eip1193-8.0.18-beta.0.tgz#4c1af5c50f19d65bb221bf5d0512bcab9bd16a6f"
integrity sha512-GsPLRP6VUw+uBhesYOrentD51gdj3yDK6oBsUKZLDwgcVaIWgmGjf0J7PTEWyOhs484ezewSaSmoitmWG19rZg==
dependencies:
"@web3-react/types" "^8.0.12-beta.0"
"@web3-react/empty@^8.0.12-beta.0":
version "8.0.12-beta.0"
resolved "https://registry.yarnpkg.com/@web3-react/empty/-/empty-8.0.12-beta.0.tgz#e71cb6a6085876177a8aa7bac224de2eee4c3d91"
integrity sha512-gUOuaeOaf5brx3Qi38vPShajOsnBPXeZBDbMNBEIaWmXf5RYYcwLnjdmauLTfRcvja+8FszPuMCs2GLNQOdEag==
dependencies:
"@web3-react/types" "^8.0.12-beta.0"
"@web3-react/metamask@^8.0.19-beta.0":
version "8.0.19-beta.0"
resolved "https://registry.yarnpkg.com/@web3-react/metamask/-/metamask-8.0.19-beta.0.tgz#7d7c306c1245ada7dff2e1c4bdae871abb126956"
integrity sha512-KRW+uvOnEadQEWAhX//o/OsCHuQCGuHmRUwcH8o5Q1jnjXtelTA9HmjWt8tyLQchPM7PHgrxfY1OH/Li05XTEA==
dependencies:
"@metamask/detect-provider" "^1.2.0"
"@web3-react/types" "^8.0.11-beta.0"
"@web3-react/types" "^8.0.12-beta.0"
"@web3-react/store@^8.0.16-beta.0":
version "8.0.16-beta.0"
resolved "https://registry.yarnpkg.com/@web3-react/store/-/store-8.0.16-beta.0.tgz#2e76571f6d6443173a89097825f4abe7331eaa6a"
integrity sha512-W79ZDgGZ/pyA1CaxbsfWZ56Ud142eHJw4oP8TSwH9lmRqfPXvFeKUCACy9YpKBjmTMDRR3bQ67kEMYQjjGkY6g==
"@web3-react/store@^8.0.17-beta.0":
version "8.0.17-beta.0"
resolved "https://registry.yarnpkg.com/@web3-react/store/-/store-8.0.17-beta.0.tgz#a059b6e761598e108c25dfccabb227682b32899d"
integrity sha512-1JGkYs8HCd7ixLB5Yb7TZzz98QmCTvg0vhR+7s2m29wbp9ADsJN/EI9Avr1Kxi0UbRXTrXnEfgvl68HHmAC0GQ==
dependencies:
"@ethersproject/address" "^5"
"@web3-react/types" "^8.0.11-beta.0"
zustand "^4.0.0-beta.2"
"@web3-react/types" "^8.0.12-beta.0"
zustand "^4.0.0-beta.3"
"@web3-react/types@^6.0.7", "web3-react-types@npm:@web3-react/types@^6.0.7":
version "6.0.7"
resolved "https://registry.yarnpkg.com/@web3-react/types/-/types-6.0.7.tgz#34a6204224467eedc6123abaf55fbb6baeb2809f"
integrity sha512-ofGmfDhxmNT1/P/MgVa8IKSkCStFiyvXe+U5tyZurKdrtTDFU+wJ/LxClPDtFerWpczNFPUSrKcuhfPX1sI6+A==
"@web3-react/types@^8.0.11-beta.0":
version "8.0.11-beta.0"
resolved "https://registry.yarnpkg.com/@web3-react/types/-/types-8.0.11-beta.0.tgz#9b34ff16dfc8f37f2dbdbc69b89ba7fc66429827"
integrity sha512-KFaQn/5+1uA5+pPCGytqRK2zgdkgKJIL/KnzljkAoR3JuBkgqqAkbFlkJyiyzETAJPjwutKnl2st+ufZVPecTw==
"@web3-react/types@^8.0.12-beta.0":
version "8.0.12-beta.0"
resolved "https://registry.yarnpkg.com/@web3-react/types/-/types-8.0.12-beta.0.tgz#787c7ca8b4dfd66bed0e4801fef7039ba42db59e"
integrity sha512-BWz8RnpO0gdySjL62iURcn41ZDGxq4kokV2qbgqgASHwst5/uZ7RI2MJHTzn8vH0AUrCsg/y6E8oeTNsCAQvfg==
dependencies:
zustand "^4.0.0-beta.2"
zustand "^4.0.0-beta.3"
"@web3-react/url@^8.0.16-beta.0":
version "8.0.16-beta.0"
resolved "https://registry.yarnpkg.com/@web3-react/url/-/url-8.0.16-beta.0.tgz#28b51448072c9cac6b84ea5e5a8f11560a10a829"
integrity sha512-9Lz2/yNottir9ymkH3QmNWr/I7yRS46LHj10TGh6n3ZReEa1116kL/pTw2U7OaP285ieKq93Hev+OaP7upxQOg==
"@web3-react/url@^8.0.17-beta.0":
version "8.0.17-beta.0"
resolved "https://registry.yarnpkg.com/@web3-react/url/-/url-8.0.17-beta.0.tgz#c972c0e5445902d05a5562bef3cdd3e8f0a32ea5"
integrity sha512-03fGMa22wdabeCNrD2sIr/IePn+VoL0Bbpyz0jJrh9dUfrDEgsLrtcdmktLhfM4LCtGAye1W1FZpi/65mczSXA==
dependencies:
"@ethersproject/providers" "^5"
"@web3-react/types" "^8.0.11-beta.0"
"@web3-react/types" "^8.0.12-beta.0"
"@web3-react/walletconnect@^8.0.25-beta.0":
version "8.0.25-beta.0"
resolved "https://registry.yarnpkg.com/@web3-react/walletconnect/-/walletconnect-8.0.25-beta.0.tgz#52634ea0cd160b7a28baa3164cd0e54b9dd62dd4"
integrity sha512-8On75QWiykvMuMuc6j+Qzb1o4CtJTJRGDNd1hFTYbj99SJJGUFgVJsvhJuHutm2Iv6NuC0wmeggb8S0iqYrahQ==
"@web3-react/walletconnect@^8.0.26-beta.0":
version "8.0.26-beta.0"
resolved "https://registry.yarnpkg.com/@web3-react/walletconnect/-/walletconnect-8.0.26-beta.0.tgz#0dd903bcb90c986876e142a948c8c92d51e65072"
integrity sha512-v7cdAmVjIxqMhH8xTuOFJOKphb089SOYHlk61GhOZCEyLcfbbc+rFuGkKiY0lFtD1/ET3cHJjRok2buXOJRn8g==
dependencies:
"@web3-react/types" "^8.0.11-beta.0"
"@web3-react/types" "^8.0.12-beta.0"
eventemitter3 "^4.0.7"
"@webassemblyjs/ast@1.9.0":
@@ -19617,10 +19617,10 @@ use-sidecar@^1.0.1:
detect-node-es "^1.1.0"
tslib "^1.9.3"
use-sync-external-store@1.0.0-rc.1-next-629036a9c-20220224:
version "1.0.0-rc.1-next-629036a9c-20220224"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.0.0-rc.1-next-629036a9c-20220224.tgz#40cf472454789403c2de6c8471d177459d184dc1"
integrity sha512-IhuMl0apVVYsT3XPfV+0nuwf0T6+3d4YxQXV4tDRsGpSQcYVG4zoWwfX4zdtouUfuelYg4t2SEmFifIMrxPfIw==
use-sync-external-store@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.0.0.tgz#d98f4a9c2e73d0f958e7e2d2c2bfb5f618cbd8fd"
integrity sha512-AFVsxg5GkFg8GDcxnl+Z0lMAz9rE8DGJCc28qnBuQF7lac57B5smLcT37aXpXIIPz75rW4g3eXHPjhHwdGskOw==
use@^3.1.0:
version "3.1.1"
@@ -20707,9 +20707,9 @@ yocto-queue@^1.0.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251"
integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
zustand@^4.0.0-beta.2:
version "4.0.0-beta.2"
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.0.0-beta.2.tgz#27fdc32b62225cc18976c0cf8866ecee9a9f4a98"
integrity sha512-aJ5ypnOwPIa/uSjdZv/oHChTWPplpFOG/hvWwzkR5ahFiPI5R6ifyObf8Fz1Vi6Obz2wY1N32fT2pNrpT2hzPw==
zustand@^4.0.0-beta.3:
version "4.0.0-beta.3"
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.0.0-beta.3.tgz#16dc82b48b65ed61fe2bae5dea4501f49bd450c7"
integrity sha512-cVDcspaK0CXgVmGcXB/oenhT7EFaKqD46pTmg30ciMsOoQN0ZuxEuHzpNIy9ejah0gzBL8aqHN89IMT2uFNOaA==
dependencies:
use-sync-external-store "1.0.0-rc.1-next-629036a9c-20220224"
use-sync-external-store "1.0.0"