Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71aebf33db | ||
|
|
5ff428b04b | ||
|
|
acb0c2056e | ||
|
|
0a4bcb62da | ||
|
|
f50bcbdb2d | ||
|
|
cbe421ee23 | ||
|
|
3439786c38 | ||
|
|
6294915be6 | ||
|
|
984c742d0e | ||
|
|
00b151d7fa | ||
|
|
5967cf5d9d | ||
|
|
e480f0ebe5 | ||
|
|
f6ceecbc5e |
@@ -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"
|
||||
|
||||
@@ -4,7 +4,7 @@ The Swap Widget bundles the whole swapping experience into a single React compon
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||
|
||||
15
package.json
15
package.json
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>} />
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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 }>`
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
`
|
||||
|
||||
@@ -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] =
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
78
src/lib/hooks/useBlockNumber.tsx
Normal file
78
src/lib/hooks/useBlockNumber.tsx
Normal 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>
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user