From 12e0b71f4082ebe0106c32721c4c11a2c1465ca1 Mon Sep 17 00:00:00 2001 From: Kristie Huang Date: Fri, 17 Jun 2022 14:27:37 -0400 Subject: [PATCH 01/44] feat: set up basic wallet connection functionality (#32) * feat: set up ui elements for wallet connect * set up account avatar function * structure connect wallet modal * more ui updates for wc modal * wip provider utils * mm connection wip * linter * semi-working MM connection flow * Implement Metamask connection * minor alignment css * Add WIP WC functionality * Refactor useprovider fxn * Add link to help center for nowallet * Clean up useProvider * Fix WalletConnect; need window.Buffer polyfill * Fix WC connection * Add mainnet RPC URL fallback constant * Leave TODOs for readability * Leave TODOs for readability * Another todo * Update yarn.lock * Update .nvmrc for new @node/types * Better modal layout UI * Only use wallet if wallet is active * Update yarn.lock * Should only use our own flow iff provider & callbacks not given * fix: refactor wallet connection components (#33) * Refactor connect wallet into a separate component, and rename connect wallet callbacks * Rename callbacks * Fixes from PRs * Remove fallback JSON RPC URL logic; move to separate PR * Move web3 connection-related hooks into new folder * Rename and move connect web3-related hooks * fix: move updating Web3Context into hook (#34) * wip * Move context s.t. context is not exposed * Clean commit * Connect eagerly on mount * Minor css * Move components to Wallet * Rename dialog buttons to wallet name * Put network conditional back (fallback not implemented yet) * Add back disabled prop * Use e.preventDefault to determine opening our connection flow * Clean up unused code * nit * Clean comments * Don't deactivate other wallets before activating this one * Update yarn.lock --- .nvmrc | 2 +- package.json | 2 +- src/assets/images/metamaskIcon.png | Bin 0 -> 114217 bytes src/assets/images/walletConnectIcon.svg | 9 ++ .../ConnectWallet/ConnectWallet.tsx | 56 +++++++++ .../ConnectWallet/ConnectWalletDialog.tsx | 111 +++++++++++++++++ .../ConnectWallet/ConnectedWalletChip.tsx | 37 ++++++ src/components/ConnectWallet/index.tsx | 25 ++++ src/components/EtherscanLink.tsx | 2 +- src/components/Swap/SwapButton/index.tsx | 2 +- src/components/Swap/Toolbar/index.tsx | 2 +- src/components/Swap/index.tsx | 15 ++- src/components/TokenSelect/TokenOptions.tsx | 2 +- src/components/TokenSelect/index.tsx | 2 +- src/components/Wallet.tsx | 30 ----- src/components/Widget.tsx | 10 +- src/cosmos/Swap.fixture.tsx | 5 +- src/hooks/connectWeb3/useActiveWeb3React.tsx | 116 ++++++++++++++++++ src/hooks/connectWeb3/useConnect.ts | 87 +++++++++++++ src/hooks/multicall.ts | 2 +- .../useClientSideSmartOrderRouterTrade.ts | 2 +- src/hooks/swap/useSwapApproval.ts | 2 +- src/hooks/swap/useSwapCallback.tsx | 2 +- src/hooks/swap/useSwapInfo.tsx | 2 +- src/hooks/swap/useSyncConvenienceFee.ts | 2 +- src/hooks/swap/useSyncTokenDefaults.ts | 2 +- src/hooks/swap/useWrapCallback.tsx | 2 +- src/hooks/transactions/index.tsx | 2 +- src/hooks/transactions/updater.tsx | 2 +- src/hooks/useActiveWeb3React.tsx | 93 -------------- src/hooks/useAllV3Routes.ts | 2 +- src/hooks/useApproval.ts | 2 +- src/hooks/useArgentWalletContract.ts | 2 +- src/hooks/useAutoSlippageTolerance.ts | 2 +- src/hooks/useBlockNumber.tsx | 2 +- src/hooks/useClientSideV3Trade.ts | 2 +- src/hooks/useContract.ts | 2 +- src/hooks/useCurrency.ts | 2 +- src/hooks/useCurrencyBalance.ts | 2 +- src/hooks/useERC20Permit.ts | 2 +- src/hooks/useIsArgentWallet.ts | 2 +- src/hooks/useIsValidBlock.ts | 2 +- src/hooks/useNativeCurrency.ts | 2 +- src/hooks/useOnSupportedNetwork.ts | 2 +- src/hooks/usePools.ts | 2 +- src/hooks/useSwapCallArguments.tsx | 2 +- src/hooks/useTokenList/index.tsx | 2 +- src/hooks/useTokenList/useQueryTokens.ts | 2 +- src/hooks/useTransactionDeadline.ts | 2 +- src/hooks/useUSDCPrice.ts | 2 +- src/hooks/useV3SwapPools.ts | 2 +- src/state/multicall.tsx | 2 +- yarn.lock | 8 +- 53 files changed, 507 insertions(+), 173 deletions(-) create mode 100644 src/assets/images/metamaskIcon.png create mode 100644 src/assets/images/walletConnectIcon.svg create mode 100644 src/components/ConnectWallet/ConnectWallet.tsx create mode 100644 src/components/ConnectWallet/ConnectWalletDialog.tsx create mode 100644 src/components/ConnectWallet/ConnectedWalletChip.tsx create mode 100644 src/components/ConnectWallet/index.tsx delete mode 100644 src/components/Wallet.tsx create mode 100644 src/hooks/connectWeb3/useActiveWeb3React.tsx create mode 100644 src/hooks/connectWeb3/useConnect.ts delete mode 100644 src/hooks/useActiveWeb3React.tsx diff --git a/.nvmrc b/.nvmrc index b009dfb9d..b6a7d89c6 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -lts/* +16 diff --git a/package.json b/package.json index d55891d41..bcc586883 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ "@types/lingui__react": "^2.8.3", "@types/ms.macro": "^2.0.0", "@types/multicodec": "^1.0.0", - "@types/node": "^13.13.5", + "@types/node": "^16.7.13", "@types/qs": "^6.9.2", "@types/react": "^17.0.2", "@types/react-dom": "^17.0.1", diff --git a/src/assets/images/metamaskIcon.png b/src/assets/images/metamaskIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..85714ea2908834e9bec0b6948ae6d29fa04daaf9 GIT binary patch literal 114217 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

b^7#A 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 + + + + + + + + diff --git a/src/components/ConnectWallet/ConnectWallet.tsx b/src/components/ConnectWallet/ConnectWallet.tsx new file mode 100644 index 000000000..9fc35af72 --- /dev/null +++ b/src/components/ConnectWallet/ConnectWallet.tsx @@ -0,0 +1,56 @@ +import { Trans } from '@lingui/macro' +import { Wallet as WalletIcon } from 'icons' +import { useCallback, useState } from 'react' +import styled from 'styled-components/macro' +import { ThemedText } from 'theme' + +import { TextButton } from '../Button' +import Dialog from '../Dialog' +import Row from '../Row' +import { ConnectWalletDialog } from './ConnectWalletDialog' + +interface ConnectWalletProps { + disabled?: boolean + onIntegratorConnectWalletCallback?: (e?: React.MouseEvent) => void +} + +const WalletButton = styled(TextButton)<{ hidden?: boolean }>` + filter: none; + visibility: ${({ hidden }) => hidden && 'hidden'}; +` + +export default function ConnectWallet({ disabled, onIntegratorConnectWalletCallback }: ConnectWalletProps) { + // Opens a dialog that initiates own wallet connection flow + const [open, setOpen] = useState(false) + + const onClose = useCallback(() => setOpen(false), []) + + const onClick = useCallback( + (e?: React.MouseEvent) => { + if (onIntegratorConnectWalletCallback) { + onIntegratorConnectWalletCallback(e) + if (e && e.defaultPrevented) return + } + setOpen(true) // Initiate our own wallet connection flow + }, + [onIntegratorConnectWalletCallback] + ) + + return ( + <> + + {open && ( +

+ + + )} + + ) +} diff --git a/src/components/ConnectWallet/ConnectWalletDialog.tsx b/src/components/ConnectWallet/ConnectWalletDialog.tsx new file mode 100644 index 000000000..8004a6b6c --- /dev/null +++ b/src/components/ConnectWallet/ConnectWalletDialog.tsx @@ -0,0 +1,111 @@ +import { Trans } from '@lingui/macro' +import METAMASK_ICON_URL from 'assets/images/metamaskIcon.png' +import WALLETCONNECT_ICON_URL from 'assets/images/walletConnectIcon.svg' +import Button from 'components/Button' +import Column from 'components/Column' +import { Header } from 'components/Dialog' +import Row from 'components/Row' +import useConnect, { connections } from 'hooks/connectWeb3/useConnect' +import styled from 'styled-components/macro' +import { ThemedText } from 'theme' + +const Body = styled(Column)` + height: calc(100% - 2.5em); +` + +const SecondaryOptionsRow = styled(Row)` + align-self: end; + grid-template-columns: repeat(2, calc(50% - 0.75em / 2)); + height: fit-content; +` + +const ButtonContents = styled(Column)` + gap: 0.75em; + justify-items: center; +` + +const StyledMainButton = styled(Button)` + background-color: ${({ theme }) => theme.container}; + border-radius: ${({ theme }) => theme.borderRadius * 0.75}em; + height: 183px; +` + +const StyledSmallButton = styled(Button)` + background-color: ${({ theme }) => theme.container}; + border-radius: ${({ theme }) => theme.borderRadius * 0.75}em; + height: 90px; + padding: 20px; +` + +const StyledNoWalletText = styled(ThemedText.Subhead1)` + line-height: 20px; + white-space: pre-wrap; +` + +interface ButtonProps { + walletName?: string + logoSrc?: string + caption?: string + onClick: () => void +} + +function WalletConnectButton({ walletName, logoSrc, caption, onClick }: ButtonProps) { + return ( + + + {walletName} + + {walletName} + + + + ) +} + +function MetaMaskButton({ walletName, logoSrc, onClick }: ButtonProps) { + return ( + + + {walletName} + + {walletName} + + + + ) +} + +function NoWalletButton() { + const helpCenterUrl = 'https://help.uniswap.org/en/articles/5391585-how-to-get-a-wallet' + return ( + window.open(helpCenterUrl)}> + + I don't have a wallet + + + ) +} + +export function ConnectWalletDialog() { + const [mmConnection, wcConnection] = connections + // TODO(kristiehuang): what happens when I try to connect one wallet without disconnecting the other? + + return ( + <> +
Connect wallet} /> + + + + + + + + + + + ) +} diff --git a/src/components/ConnectWallet/ConnectedWalletChip.tsx b/src/components/ConnectWallet/ConnectedWalletChip.tsx new file mode 100644 index 000000000..0dde17ed2 --- /dev/null +++ b/src/components/ConnectWallet/ConnectedWalletChip.tsx @@ -0,0 +1,37 @@ +import { TextButton } from 'components/Button' +import Row from 'components/Row' +import styled from 'styled-components/macro' +import { ThemedText } from 'theme' + +const AccountButton = styled(TextButton)<{ hidden?: boolean }>` + filter: none; + visibility: ${({ hidden }) => hidden && 'hidden'}; +` + +export default function ConnectedWalletChip({ disabled, account }: { disabled?: boolean; account?: string }) { + // TODO(kristiehuang): AccountDialog & AccountAvatar UI does not yet exist + // const [open, setOpen] = useState(false) + + return ( + <> + + {/* {open && ( + setOpen(false)}> + + + )} */} + + ) +} diff --git a/src/components/ConnectWallet/index.tsx b/src/components/ConnectWallet/index.tsx new file mode 100644 index 000000000..86e54b233 --- /dev/null +++ b/src/components/ConnectWallet/index.tsx @@ -0,0 +1,25 @@ +import { connections } from 'hooks/connectWeb3/useConnect' +import { useEffect } from 'react' + +import ConnectedWalletChip from './ConnectedWalletChip' +import ConnectWallet from './ConnectWallet' + +interface WalletProps { + disabled?: boolean + account?: string + onConnectWallet?: (e?: React.MouseEvent) => void +} + +export default function Wallet({ disabled, account, onConnectWallet }: WalletProps) { + // Attempt to connect eagerly on mount + useEffect(() => { + connections.forEach(([wallet, _]) => wallet.connectEagerly()) + }, []) + + const isConnected = Boolean(account) + return isConnected ? ( + + ) : ( + + ) +} diff --git a/src/components/EtherscanLink.tsx b/src/components/EtherscanLink.tsx index 94f67f124..c27f0bd14 100644 --- a/src/components/EtherscanLink.tsx +++ b/src/components/EtherscanLink.tsx @@ -1,5 +1,5 @@ import { SupportedChainId } from 'constants/chains' -import useActiveWeb3React from 'hooks/useActiveWeb3React' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { Link } from 'icons' import { ReactNode, useMemo } from 'react' import styled from 'styled-components/macro' diff --git a/src/components/Swap/SwapButton/index.tsx b/src/components/Swap/SwapButton/index.tsx index 67be2ff0a..47ebcba31 100644 --- a/src/components/Swap/SwapButton/index.tsx +++ b/src/components/Swap/SwapButton/index.tsx @@ -1,10 +1,10 @@ import { Trans } from '@lingui/macro' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useSwapInfo } from 'hooks/swap' import { useSwapApprovalOptimizedTrade } from 'hooks/swap/useSwapApproval' import { useSwapCallback } from 'hooks/swap/useSwapCallback' import useWrapCallback, { WrapType } from 'hooks/swap/useWrapCallback' import { useAddTransaction } from 'hooks/transactions' -import useActiveWeb3React from 'hooks/useActiveWeb3React' import { useSetOldestValidBlock } from 'hooks/useIsValidBlock' import useTransactionDeadline from 'hooks/useTransactionDeadline' import { Spinner } from 'icons' diff --git a/src/components/Swap/Toolbar/index.tsx b/src/components/Swap/Toolbar/index.tsx index 58065648a..a74a8a535 100644 --- a/src/components/Swap/Toolbar/index.tsx +++ b/src/components/Swap/Toolbar/index.tsx @@ -1,7 +1,7 @@ import { ALL_SUPPORTED_CHAIN_IDS } from 'constants/chains' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useIsAmountPopulated, useSwapInfo } from 'hooks/swap' import useWrapCallback, { WrapType } from 'hooks/swap/useWrapCallback' -import useActiveWeb3React from 'hooks/useActiveWeb3React' import { largeIconCss } from 'icons' import { memo, useMemo } from 'react' import { TradeState } from 'state/routing/types' diff --git a/src/components/Swap/index.tsx b/src/components/Swap/index.tsx index 689a65536..ec650107e 100644 --- a/src/components/Swap/index.tsx +++ b/src/components/Swap/index.tsx @@ -1,9 +1,12 @@ +import { JsonRpcProvider } from '@ethersproject/providers' import { Trans } from '@lingui/macro' +import { Provider as Eip1193Provider } from '@web3-react/types' +import Wallet from 'components/ConnectWallet' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { SwapInfoProvider } from 'hooks/swap/useSwapInfo' import useSyncConvenienceFee, { FeeOptions } from 'hooks/swap/useSyncConvenienceFee' import useSyncTokenDefaults, { TokenDefaults } from 'hooks/swap/useSyncTokenDefaults' import { usePendingTransactions } from 'hooks/transactions' -import useActiveWeb3React from 'hooks/useActiveWeb3React' import useHasFocus from 'hooks/useHasFocus' import useOnSupportedNetwork from 'hooks/useOnSupportedNetwork' import { useAtom } from 'jotai' @@ -14,7 +17,6 @@ import { SwapTransactionInfo, Transaction, TransactionType, WrapTransactionInfo import Dialog from '../Dialog' import Header from '../Header' import { BoundaryProvider } from '../Popover' -import Wallet from '../Wallet' import Input from './Input' import Output from './Output' import ReverseButton from './ReverseButton' @@ -40,8 +42,11 @@ function getTransactionFromMap( return } +// SwapProps also currently includes props needed for wallet connection, since the wallet connection component exists within the Swap component +// TODO(kristiehuang): refactor WalletConnection outside of Swap component export interface SwapProps extends TokenDefaults, FeeOptions { - onConnectWallet?: () => void + provider?: Eip1193Provider | JsonRpcProvider + onClickConnectWallet?: (e?: React.MouseEvent) => void } export default function Swap(props: SwapProps) { @@ -61,10 +66,12 @@ export default function Swap(props: SwapProps) { const focused = useHasFocus(wrapper) + const hideConnectionUI = false // TODO(kristiehuang): add new prop to allow integrator to hide entire connection UI + return ( <>
Swap}> - +
diff --git a/src/components/TokenSelect/TokenOptions.tsx b/src/components/TokenSelect/TokenOptions.tsx index aad5da561..1dae6577a 100644 --- a/src/components/TokenSelect/TokenOptions.tsx +++ b/src/components/TokenSelect/TokenOptions.tsx @@ -1,6 +1,6 @@ import { useLingui } from '@lingui/react' import { Currency } from '@uniswap/sdk-core' -import useActiveWeb3React from 'hooks/useActiveWeb3React' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import useCurrencyBalance from 'hooks/useCurrencyBalance' import useNativeEvent from 'hooks/useNativeEvent' import useScrollbar from 'hooks/useScrollbar' diff --git a/src/components/TokenSelect/index.tsx b/src/components/TokenSelect/index.tsx index 62ca97df9..1ad390504 100644 --- a/src/components/TokenSelect/index.tsx +++ b/src/components/TokenSelect/index.tsx @@ -1,6 +1,6 @@ import { t, Trans } from '@lingui/macro' import { Currency } from '@uniswap/sdk-core' -import useActiveWeb3React from 'hooks/useActiveWeb3React' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useCurrencyBalances } from 'hooks/useCurrencyBalance' import useNativeCurrency from 'hooks/useNativeCurrency' import useTokenList, { useIsTokenListLoaded, useQueryTokens } from 'hooks/useTokenList' diff --git a/src/components/Wallet.tsx b/src/components/Wallet.tsx deleted file mode 100644 index 17ddfd96c..000000000 --- a/src/components/Wallet.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Trans } from '@lingui/macro' -import { Wallet as WalletIcon } from 'icons' -import styled from 'styled-components/macro' -import { ThemedText } from 'theme' - -import { TextButton } from './Button' -import Row from './Row' - -interface WalletProps { - disabled?: boolean - onClick?: () => void -} - -const WalletButton = styled(TextButton)<{ hidden?: boolean }>` - filter: none; - visibility: ${({ hidden }) => hidden && 'hidden'}; -` - -export default function Wallet({ disabled, onClick }: WalletProps) { - return ( - - ) -} diff --git a/src/components/Widget.tsx b/src/components/Widget.tsx index 2d10f459f..ff38aad25 100644 --- a/src/components/Widget.tsx +++ b/src/components/Widget.tsx @@ -2,8 +2,9 @@ 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 { ActiveWeb3Provider } from 'hooks/connectWeb3/useActiveWeb3React' +import { useActiveProvider } from 'hooks/connectWeb3/useConnect' import { TransactionsUpdater } from 'hooks/transactions' -import { ActiveWeb3Provider } from 'hooks/useActiveWeb3React' import { BlockNumberProvider } from 'hooks/useBlockNumber' import { TokenListProvider } from 'hooks/useTokenList' import { Provider as I18nProvider } from 'i18n' @@ -96,7 +97,7 @@ export type WidgetProps = { } export default function Widget(props: PropsWithChildren) { - const { children, theme, provider, jsonRpcEndpoint, dialog: userDialog, className, onError } = props + const { children, theme, 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.`) @@ -112,6 +113,11 @@ export default function Widget(props: PropsWithChildren) { return props.locale ?? DEFAULT_LOCALE }, [props.locale]) + const activeProvider = useActiveProvider() + const provider = useMemo(() => { + return props.provider ?? activeProvider + }, [props.provider, activeProvider]) + const [dialog, setDialog] = useState(null) return ( diff --git a/src/cosmos/Swap.fixture.tsx b/src/cosmos/Swap.fixture.tsx index 2b87474c3..d041a8411 100644 --- a/src/cosmos/Swap.fixture.tsx +++ b/src/cosmos/Swap.fixture.tsx @@ -71,7 +71,10 @@ function Fixture() { theme={theme} tokenList={tokenList} width={width} - onConnectWallet={() => console.log('onConnectWallet')} // this handler is included as a test of functionality, but only logs + onClickConnectWallet={() => { + // e?.preventDefault() + console.log('integrator provided a onConnectWallet') + }} /> ) } diff --git a/src/hooks/connectWeb3/useActiveWeb3React.tsx b/src/hooks/connectWeb3/useActiveWeb3React.tsx new file mode 100644 index 000000000..0d6acb5fb --- /dev/null +++ b/src/hooks/connectWeb3/useActiveWeb3React.tsx @@ -0,0 +1,116 @@ +import { ExternalProvider, JsonRpcProvider, Web3Provider } from '@ethersproject/providers' +import { initializeConnector, Web3ReactHooks } from '@web3-react/core' +import { EIP1193 } from '@web3-react/eip1193' +import { EMPTY } from '@web3-react/empty' +import { Connector, Provider as Eip1193Provider } from '@web3-react/types' +import { Url } from '@web3-react/url' +import { createContext, PropsWithChildren, useContext, useEffect, useMemo } from 'react' +import JsonRpcConnector from 'utils/JsonRpcConnector' + +export type Web3ContextType = { + connector: Connector + library?: (JsonRpcProvider & { provider?: ExternalProvider }) | Web3Provider + chainId?: ReturnType + accounts?: ReturnType + account?: ReturnType + active?: ReturnType + activating?: ReturnType + error?: ReturnType +} + +const [EMPTY_CONNECTOR, EMPTY_HOOKS] = initializeConnector(() => EMPTY) +const EMPTY_STATE = { connector: EMPTY_CONNECTOR, hooks: EMPTY_HOOKS } +const EMPTY_WEB3: Web3ContextType = { connector: EMPTY } +const EMPTY_CONTEXT = { web3: EMPTY_WEB3, updateWeb3: (updateContext: Web3ContextType) => console.log(updateContext) } +export const Web3Context = createContext(EMPTY_CONTEXT) + +export default function useActiveWeb3React() { + const { web3 } = useContext(Web3Context) + return web3 +} + +export function useUpdateActiveWeb3ReactCallback() { + const { updateWeb3 } = useContext(Web3Context) + return updateWeb3 +} + +export function getNetwork(jsonRpcEndpoint?: string | JsonRpcProvider) { + if (jsonRpcEndpoint) { + let connector, hooks + if (JsonRpcProvider.isProvider(jsonRpcEndpoint)) { + ;[connector, hooks] = initializeConnector((actions) => new JsonRpcConnector(actions, jsonRpcEndpoint)) + } else { + ;[connector, hooks] = initializeConnector((actions) => new Url(actions, jsonRpcEndpoint)) + } + connector.activate() + return { connector, hooks } + } + return EMPTY_STATE +} + +function getWallet(provider?: JsonRpcProvider | Eip1193Provider) { + if (provider) { + let connector, hooks + if (JsonRpcProvider.isProvider(provider)) { + ;[connector, hooks] = initializeConnector((actions) => new JsonRpcConnector(actions, provider)) + } else if (JsonRpcProvider.isProvider((provider as any).provider)) { + throw new Error('Eip1193Bridge is experimental: pass your ethers Provider directly') + } else { + ;[connector, hooks] = initializeConnector((actions) => new EIP1193(actions, provider)) + } + connector.activate() + return { connector, hooks } + } + return EMPTY_STATE +} + +interface ActiveWeb3ProviderProps { + jsonRpcEndpoint?: string | JsonRpcProvider + provider?: Eip1193Provider | JsonRpcProvider +} + +export function ActiveWeb3Provider({ + jsonRpcEndpoint, + provider, + children, +}: PropsWithChildren) { + const network = useMemo(() => getNetwork(jsonRpcEndpoint), [jsonRpcEndpoint]) + const wallet = useMemo(() => getWallet(provider), [provider]) + + // eslint-disable-next-line prefer-const + let { connector, hooks } = wallet.hooks.useIsActive() || network === EMPTY_STATE ? wallet : network + let accounts = hooks.useAccounts() + let account = hooks.useAccount() + let activating = hooks.useIsActivating() + let active = hooks.useIsActive() + let chainId = hooks.useChainId() + let error = hooks.useError() + let library = hooks.useProvider() + + const web3 = useMemo(() => { + if (connector === EMPTY || !(active || activating)) { + return EMPTY_WEB3 + } + return { connector, library, chainId, accounts, account, active, activating, error } + }, [account, accounts, activating, active, chainId, connector, error, library]) + + const updateWeb3 = (updateContext: Web3ContextType) => { + connector = updateContext.connector + accounts = updateContext.accounts + account = updateContext.account + activating = updateContext.activating ?? false + active = updateContext.active ?? false + chainId = updateContext.chainId + error = updateContext.error + library = updateContext.library as Web3Provider + } + + // Log web3 errors to facilitate debugging. + useEffect(() => { + if (error) { + console.error('web3 error:', error) + } + }, [error]) + + return {children} +} diff --git a/src/hooks/connectWeb3/useConnect.ts b/src/hooks/connectWeb3/useConnect.ts new file mode 100644 index 000000000..39917fd1b --- /dev/null +++ b/src/hooks/connectWeb3/useConnect.ts @@ -0,0 +1,87 @@ +import { JsonRpcProvider, Web3Provider } from '@ethersproject/providers' +import { getPriorityConnector, initializeConnector, Web3ReactHooks } from '@web3-react/core' +import { MetaMask } from '@web3-react/metamask' +import { Connector, Web3ReactStore } from '@web3-react/types' +import { WalletConnect } from '@web3-react/walletconnect' +import { Buffer } from 'buffer' +import { getNetwork, useUpdateActiveWeb3ReactCallback, Web3ContextType } from 'hooks/connectWeb3/useActiveWeb3React' +import { useCallback } from 'react' + +export type Web3Connection = [Connector, Web3ReactHooks] + +function toWeb3Connection([connector, hooks]: [T, Web3ReactHooks, Web3ReactStore]): [ + T, + Web3ReactHooks +] { + return [connector, hooks] +} + +const metaMaskConnection = toWeb3Connection(initializeConnector((actions) => new MetaMask(actions))) + +function getWalletConnectConnection(jsonRpcEndpoint?: string | JsonRpcProvider) { + // WalletConnect relies on Buffer, so it must be polyfilled. + if (!('Buffer' in window)) { + window.Buffer = Buffer + } + + let rpcUrl: string + if (JsonRpcProvider.isProvider(jsonRpcEndpoint)) { + rpcUrl = jsonRpcEndpoint.connection.url + } else { + rpcUrl = jsonRpcEndpoint ?? '' // FIXME: use fallback RPC URL + } + + return toWeb3Connection( + initializeConnector( + (actions) => + new WalletConnect( + actions, + { + rpc: { 1: rpcUrl }, // TODO(kristiehuang): WC only works on network chainid 1? + }, + false + ) + ) + ) +} + +export const connections = [metaMaskConnection, getWalletConnectConnection()] + +export function useActiveProvider(): Web3Provider | undefined { + const activeWalletProvider = getPriorityConnector(...connections).usePriorityProvider() as Web3Provider + const { connector: network } = getNetwork() // Return network-only provider if no wallet is connected + return activeWalletProvider ?? network.provider +} + +export default function useConnect(connection: Web3Connection) { + const [wallet, hooks] = connection + const isActive = hooks.useIsActive() + const accounts = hooks.useAccounts() + const account = hooks.useAccount() + const activating = hooks.useIsActivating() + const chainId = hooks.useChainId() + const error = hooks.useError() + const library = hooks.useProvider() + const updateActiveWeb3ReactCallback = useUpdateActiveWeb3ReactCallback() + + const useWallet = useCallback(() => { + if (!isActive) { + wallet.activate() + } else { + // wallet should be already be active + const updateContext: Web3ContextType = { + connector: wallet, + library, + accounts, + account, + activating, + active: isActive, + chainId, + error, + } + updateActiveWeb3ReactCallback(updateContext) + } + }, [account, accounts, activating, chainId, error, isActive, library, updateActiveWeb3ReactCallback, wallet]) + + return useWallet +} diff --git a/src/hooks/multicall.ts b/src/hooks/multicall.ts index b2c648986..c1ee2aa9d 100644 --- a/src/hooks/multicall.ts +++ b/src/hooks/multicall.ts @@ -1,4 +1,4 @@ -import useActiveWeb3React from 'hooks/useActiveWeb3React' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import useBlockNumber from 'hooks/useBlockNumber' import multicall from 'state/multicall' diff --git a/src/hooks/routing/useClientSideSmartOrderRouterTrade.ts b/src/hooks/routing/useClientSideSmartOrderRouterTrade.ts index 9c6686075..1479907cf 100644 --- a/src/hooks/routing/useClientSideSmartOrderRouterTrade.ts +++ b/src/hooks/routing/useClientSideSmartOrderRouterTrade.ts @@ -9,8 +9,8 @@ import { useCallback, useMemo } from 'react' import { GetQuoteResult, InterfaceTrade, TradeState } from 'state/routing/types' import { computeRoutes, transformRoutesToTrade } from 'state/routing/utils' +import useActiveWeb3React from '../connectWeb3/useActiveWeb3React' import useWrapCallback, { WrapType } from '../swap/useWrapCallback' -import useActiveWeb3React from '../useActiveWeb3React' import { useGetIsValidBlock } from '../useIsValidBlock' import usePoll from '../usePoll' import { useRoutingAPIArguments } from './useRoutingAPIArguments' diff --git a/src/hooks/swap/useSwapApproval.ts b/src/hooks/swap/useSwapApproval.ts index db80f382c..9a2cbd094 100644 --- a/src/hooks/swap/useSwapApproval.ts +++ b/src/hooks/swap/useSwapApproval.ts @@ -3,7 +3,7 @@ import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sd import { Pair, Route as V2Route, Trade as V2Trade } from '@uniswap/v2-sdk' import { Pool, Route as V3Route, Trade as V3Trade } from '@uniswap/v3-sdk' import { SWAP_ROUTER_ADDRESSES, V2_ROUTER_ADDRESS, V3_ROUTER_ADDRESS } from 'constants/addresses' -import useActiveWeb3React from 'hooks/useActiveWeb3React' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useERC20PermitFromTrade, UseERC20PermitState } from 'hooks/useERC20Permit' import useTransactionDeadline from 'hooks/useTransactionDeadline' import { useCallback, useMemo } from 'react' diff --git a/src/hooks/swap/useSwapCallback.tsx b/src/hooks/swap/useSwapCallback.tsx index 3de227da0..ebaa215a4 100644 --- a/src/hooks/swap/useSwapCallback.tsx +++ b/src/hooks/swap/useSwapCallback.tsx @@ -4,7 +4,7 @@ import { TransactionResponse } from '@ethersproject/providers' import { Trans } from '@lingui/macro' import { Percent } from '@uniswap/sdk-core' import { FeeOptions } from '@uniswap/v3-sdk' -import useActiveWeb3React from 'hooks/useActiveWeb3React' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import useENS from 'hooks/useENS' import { SignatureData } from 'hooks/useERC20Permit' import { AnyTrade, useSwapCallArguments } from 'hooks/useSwapCallArguments' diff --git a/src/hooks/swap/useSwapInfo.tsx b/src/hooks/swap/useSwapInfo.tsx index f009a13a0..f69c6b265 100644 --- a/src/hooks/swap/useSwapInfo.tsx +++ b/src/hooks/swap/useSwapInfo.tsx @@ -1,5 +1,5 @@ import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' -import useActiveWeb3React from 'hooks/useActiveWeb3React' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useCurrencyBalances } from 'hooks/useCurrencyBalance' import useSlippage, { DEFAULT_SLIPPAGE, Slippage } from 'hooks/useSlippage' import useUSDCPriceImpact, { PriceImpact } from 'hooks/useUSDCPriceImpact' diff --git a/src/hooks/swap/useSyncConvenienceFee.ts b/src/hooks/swap/useSyncConvenienceFee.ts index 684796f99..aa169ad53 100644 --- a/src/hooks/swap/useSyncConvenienceFee.ts +++ b/src/hooks/swap/useSyncConvenienceFee.ts @@ -1,5 +1,5 @@ import { Percent } from '@uniswap/sdk-core' -import useActiveWeb3React from 'hooks/useActiveWeb3React' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useUpdateAtom } from 'jotai/utils' import { useEffect } from 'react' import { feeOptionsAtom } from 'state/swap' diff --git a/src/hooks/swap/useSyncTokenDefaults.ts b/src/hooks/swap/useSyncTokenDefaults.ts index 2191ed749..92bc50d09 100644 --- a/src/hooks/swap/useSyncTokenDefaults.ts +++ b/src/hooks/swap/useSyncTokenDefaults.ts @@ -1,6 +1,6 @@ import { Currency } from '@uniswap/sdk-core' import { nativeOnChain } from 'constants/tokens' -import useActiveWeb3React from 'hooks/useActiveWeb3React' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useToken } from 'hooks/useCurrency' import useNativeCurrency from 'hooks/useNativeCurrency' import { useUpdateAtom } from 'jotai/utils' diff --git a/src/hooks/swap/useWrapCallback.tsx b/src/hooks/swap/useWrapCallback.tsx index d778ae746..5d4f6f9f8 100644 --- a/src/hooks/swap/useWrapCallback.tsx +++ b/src/hooks/swap/useWrapCallback.tsx @@ -6,7 +6,7 @@ import { useMemo } from 'react' import { Field, swapAtom } from 'state/swap' import tryParseCurrencyAmount from 'utils/tryParseCurrencyAmount' -import useActiveWeb3React from '../useActiveWeb3React' +import useActiveWeb3React from '../connectWeb3/useActiveWeb3React' import useCurrencyBalance from '../useCurrencyBalance' export enum WrapType { diff --git a/src/hooks/transactions/index.tsx b/src/hooks/transactions/index.tsx index 1585debf6..776f19a80 100644 --- a/src/hooks/transactions/index.tsx +++ b/src/hooks/transactions/index.tsx @@ -1,5 +1,5 @@ import { Token } from '@uniswap/sdk-core' -import useActiveWeb3React from 'hooks/useActiveWeb3React' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useAtomValue, useUpdateAtom } from 'jotai/utils' import ms from 'ms.macro' import { useCallback } from 'react' diff --git a/src/hooks/transactions/updater.tsx b/src/hooks/transactions/updater.tsx index e1f1d0840..ccf4b95e5 100644 --- a/src/hooks/transactions/updater.tsx +++ b/src/hooks/transactions/updater.tsx @@ -1,6 +1,6 @@ import { TransactionReceipt } from '@ethersproject/abstract-provider' import { SupportedChainId } from 'constants/chains' -import useActiveWeb3React from 'hooks/useActiveWeb3React' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import useBlockNumber, { useFastForwardBlockNumber } from 'hooks/useBlockNumber' import ms from 'ms.macro' import { useCallback, useEffect } from 'react' diff --git a/src/hooks/useActiveWeb3React.tsx b/src/hooks/useActiveWeb3React.tsx deleted file mode 100644 index e5d6d6d19..000000000 --- a/src/hooks/useActiveWeb3React.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { ExternalProvider, JsonRpcProvider, Web3Provider } from '@ethersproject/providers' -import { initializeConnector, Web3ReactHooks } from '@web3-react/core' -import { EIP1193 } from '@web3-react/eip1193' -import { EMPTY } from '@web3-react/empty' -import { Connector, Provider as Eip1193Provider } from '@web3-react/types' -import { Url } from '@web3-react/url' -import { createContext, PropsWithChildren, useContext, useEffect, useMemo } from 'react' -import JsonRpcConnector from 'utils/JsonRpcConnector' - -type Web3ContextType = { - connector: Connector - library?: (JsonRpcProvider & { provider?: ExternalProvider }) | Web3Provider - chainId?: ReturnType - accounts?: ReturnType - account?: ReturnType - active?: ReturnType - activating?: ReturnType - error?: ReturnType -} - -const [EMPTY_CONNECTOR, EMPTY_HOOKS] = initializeConnector(() => EMPTY) -const EMPTY_STATE = { connector: EMPTY_CONNECTOR, hooks: EMPTY_HOOKS } -const EMPTY_CONTEXT: Web3ContextType = { connector: EMPTY } -const Web3Context = createContext(EMPTY_CONTEXT) - -export default function useActiveWeb3React() { - return useContext(Web3Context) -} - -interface ActiveWeb3ProviderProps { - jsonRpcEndpoint?: string | JsonRpcProvider - provider?: Eip1193Provider | JsonRpcProvider -} - -export function ActiveWeb3Provider({ - jsonRpcEndpoint, - provider, - children, -}: PropsWithChildren) { - const network = useMemo(() => { - if (jsonRpcEndpoint) { - let connector, hooks - if (JsonRpcProvider.isProvider(jsonRpcEndpoint)) { - ;[connector, hooks] = initializeConnector((actions) => new JsonRpcConnector(actions, jsonRpcEndpoint)) - } else { - ;[connector, hooks] = initializeConnector((actions) => new Url(actions, jsonRpcEndpoint)) - } - connector.activate() - return { connector, hooks } - } - return EMPTY_STATE - }, [jsonRpcEndpoint]) - - const wallet = useMemo(() => { - if (provider) { - let connector, hooks - if (JsonRpcProvider.isProvider(provider)) { - ;[connector, hooks] = initializeConnector((actions) => new JsonRpcConnector(actions, provider)) - } else if (JsonRpcProvider.isProvider((provider as any).provider)) { - throw new Error('Eip1193Bridge is experimental: pass your ethers Provider directly') - } else { - ;[connector, hooks] = initializeConnector((actions) => new EIP1193(actions, provider)) - } - connector.activate() - return { connector, hooks } - } - return EMPTY_STATE - }, [provider]) - - const { connector, hooks } = wallet.hooks.useIsActive() || network === EMPTY_STATE ? wallet : network - const accounts = hooks.useAccounts() - const account = hooks.useAccount() - const activating = hooks.useIsActivating() - const active = hooks.useIsActive() - const chainId = hooks.useChainId() - const error = hooks.useError() - const library = hooks.useProvider() - const web3 = useMemo(() => { - if (connector === EMPTY || !(active || activating)) { - return EMPTY_CONTEXT - } - return { connector, library, chainId, accounts, account, active, activating, error } - }, [account, accounts, activating, active, chainId, connector, error, library]) - - // Log web3 errors to facilitate debugging. - useEffect(() => { - if (error) { - console.error('web3 error:', error) - } - }, [error]) - - return {children} -} diff --git a/src/hooks/useAllV3Routes.ts b/src/hooks/useAllV3Routes.ts index 01f109307..28f3ec735 100644 --- a/src/hooks/useAllV3Routes.ts +++ b/src/hooks/useAllV3Routes.ts @@ -1,6 +1,6 @@ import { Currency } from '@uniswap/sdk-core' import { Pool, Route } from '@uniswap/v3-sdk' -import useActiveWeb3React from 'hooks/useActiveWeb3React' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useMemo } from 'react' import { useV3SwapPools } from './useV3SwapPools' diff --git a/src/hooks/useApproval.ts b/src/hooks/useApproval.ts index 30e3394a6..f6804b971 100644 --- a/src/hooks/useApproval.ts +++ b/src/hooks/useApproval.ts @@ -1,7 +1,7 @@ import { MaxUint256 } from '@ethersproject/constants' import { TransactionResponse } from '@ethersproject/providers' import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core' -import useActiveWeb3React from 'hooks/useActiveWeb3React' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useTokenContract } from 'hooks/useContract' import { useTokenAllowance } from 'hooks/useTokenAllowance' import { useCallback, useMemo } from 'react' diff --git a/src/hooks/useArgentWalletContract.ts b/src/hooks/useArgentWalletContract.ts index 8cd178106..c9e70b0f0 100644 --- a/src/hooks/useArgentWalletContract.ts +++ b/src/hooks/useArgentWalletContract.ts @@ -1,6 +1,6 @@ import ArgentWalletContractABI from 'abis/argent-wallet-contract.json' import { ArgentWalletContract } from 'abis/types' -import useActiveWeb3React from 'hooks/useActiveWeb3React' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useContract } from './useContract' import useIsArgentWallet from './useIsArgentWallet' diff --git a/src/hooks/useAutoSlippageTolerance.ts b/src/hooks/useAutoSlippageTolerance.ts index 06e5e78ef..1e7547574 100644 --- a/src/hooks/useAutoSlippageTolerance.ts +++ b/src/hooks/useAutoSlippageTolerance.ts @@ -2,7 +2,7 @@ import { Trade } from '@uniswap/router-sdk' import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core' import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains' import { L2_CHAIN_IDS } from 'constants/chains' -import useActiveWeb3React from 'hooks/useActiveWeb3React' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import useNativeCurrency from 'hooks/useNativeCurrency' import JSBI from 'jsbi' import { useMemo } from 'react' diff --git a/src/hooks/useBlockNumber.tsx b/src/hooks/useBlockNumber.tsx index 434d7e248..6f52a7ecf 100644 --- a/src/hooks/useBlockNumber.tsx +++ b/src/hooks/useBlockNumber.tsx @@ -1,4 +1,4 @@ -import useActiveWeb3React from 'hooks/useActiveWeb3React' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import useIsWindowVisible from 'hooks/useIsWindowVisible' import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react' diff --git a/src/hooks/useClientSideV3Trade.ts b/src/hooks/useClientSideV3Trade.ts index c960f82b8..0d8cc96cb 100644 --- a/src/hooks/useClientSideV3Trade.ts +++ b/src/hooks/useClientSideV3Trade.ts @@ -1,8 +1,8 @@ import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' import { Route, SwapQuoter } from '@uniswap/v3-sdk' import { SupportedChainId } from 'constants/chains' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useSingleContractWithCallData } from 'hooks/multicall' -import useActiveWeb3React from 'hooks/useActiveWeb3React' import JSBI from 'jsbi' import { useMemo } from 'react' import { InterfaceTrade, TradeState } from 'state/routing/types' diff --git a/src/hooks/useContract.ts b/src/hooks/useContract.ts index 7d776c597..2f4c6db34 100644 --- a/src/hooks/useContract.ts +++ b/src/hooks/useContract.ts @@ -27,7 +27,7 @@ import { V3_MIGRATOR_ADDRESSES, } from 'constants/addresses' import { WRAPPED_NATIVE_CURRENCY } from 'constants/tokens' -import useActiveWeb3React from 'hooks/useActiveWeb3React' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useMemo } from 'react' import { NonfungiblePositionManager, Quoter, TickLens, UniswapInterfaceMulticall } from 'types/v3' import { V3Migrator } from 'types/v3/V3Migrator' diff --git a/src/hooks/useCurrency.ts b/src/hooks/useCurrency.ts index 2012a1dac..d7ee5eecf 100644 --- a/src/hooks/useCurrency.ts +++ b/src/hooks/useCurrency.ts @@ -2,8 +2,8 @@ import { arrayify } from '@ethersproject/bytes' import { parseBytes32String } from '@ethersproject/strings' import { Currency, Token } from '@uniswap/sdk-core' import { TOKEN_SHORTHANDS } from 'constants/tokens' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { NEVER_RELOAD, useSingleCallResult } from 'hooks/multicall' -import useActiveWeb3React from 'hooks/useActiveWeb3React' import { useBytes32TokenContract, useTokenContract } from 'hooks/useContract' import useNativeCurrency from 'hooks/useNativeCurrency' import { useMemo } from 'react' diff --git a/src/hooks/useCurrencyBalance.ts b/src/hooks/useCurrencyBalance.ts index 401f3553c..a0aa32a28 100644 --- a/src/hooks/useCurrencyBalance.ts +++ b/src/hooks/useCurrencyBalance.ts @@ -3,8 +3,8 @@ import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core' import ERC20ABI from 'abis/erc20.json' import { Erc20Interface } from 'abis/types/Erc20' import { nativeOnChain } from 'constants/tokens' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useMultipleContractSingleData, useSingleContractMultipleData } from 'hooks/multicall' -import useActiveWeb3React from 'hooks/useActiveWeb3React' import { useInterfaceMulticall } from 'hooks/useContract' import JSBI from 'jsbi' import { useMemo } from 'react' diff --git a/src/hooks/useERC20Permit.ts b/src/hooks/useERC20Permit.ts index 2a7608bcc..c25ac1d9e 100644 --- a/src/hooks/useERC20Permit.ts +++ b/src/hooks/useERC20Permit.ts @@ -6,8 +6,8 @@ import { Trade as V2Trade } from '@uniswap/v2-sdk' import { Trade as V3Trade } from '@uniswap/v3-sdk' import { SWAP_ROUTER_ADDRESSES, V3_ROUTER_ADDRESS } from 'constants/addresses' import { DAI, UNI, USDC_MAINNET } from 'constants/tokens' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useSingleCallResult } from 'hooks/multicall' -import useActiveWeb3React from 'hooks/useActiveWeb3React' import JSBI from 'jsbi' import { useMemo, useState } from 'react' diff --git a/src/hooks/useIsArgentWallet.ts b/src/hooks/useIsArgentWallet.ts index 3c48f1e85..86bcbf735 100644 --- a/src/hooks/useIsArgentWallet.ts +++ b/src/hooks/useIsArgentWallet.ts @@ -1,5 +1,5 @@ +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { NEVER_RELOAD, useSingleCallResult } from 'hooks/multicall' -import useActiveWeb3React from 'hooks/useActiveWeb3React' import { useMemo } from 'react' import { useArgentWalletDetectorContract } from './useContract' diff --git a/src/hooks/useIsValidBlock.ts b/src/hooks/useIsValidBlock.ts index 1ad259d41..51ed02962 100644 --- a/src/hooks/useIsValidBlock.ts +++ b/src/hooks/useIsValidBlock.ts @@ -2,7 +2,7 @@ import { atomWithImmer } from 'jotai/immer' import { useAtomValue, useUpdateAtom } from 'jotai/utils' import { useCallback } from 'react' -import useActiveWeb3React from './useActiveWeb3React' +import useActiveWeb3React from './connectWeb3/useActiveWeb3React' import useBlockNumber from './useBlockNumber' // The oldest block (per chain) to be considered valid. diff --git a/src/hooks/useNativeCurrency.ts b/src/hooks/useNativeCurrency.ts index 6aee623b7..c649e8781 100644 --- a/src/hooks/useNativeCurrency.ts +++ b/src/hooks/useNativeCurrency.ts @@ -1,7 +1,7 @@ import { NativeCurrency } from '@uniswap/sdk-core' import { SupportedChainId } from 'constants/chains' import { nativeOnChain } from 'constants/tokens' -import useActiveWeb3React from 'hooks/useActiveWeb3React' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useMemo } from 'react' export default function useNativeCurrency(): NativeCurrency { diff --git a/src/hooks/useOnSupportedNetwork.ts b/src/hooks/useOnSupportedNetwork.ts index 6a5039f7d..b3addb2d0 100644 --- a/src/hooks/useOnSupportedNetwork.ts +++ b/src/hooks/useOnSupportedNetwork.ts @@ -1,7 +1,7 @@ import { ALL_SUPPORTED_CHAIN_IDS } from 'constants/chains' import { useMemo } from 'react' -import useActiveWeb3React from './useActiveWeb3React' +import useActiveWeb3React from './connectWeb3/useActiveWeb3React' function useOnSupportedNetwork() { const { chainId } = useActiveWeb3React() diff --git a/src/hooks/usePools.ts b/src/hooks/usePools.ts index 5cfba4992..0ee604739 100644 --- a/src/hooks/usePools.ts +++ b/src/hooks/usePools.ts @@ -4,8 +4,8 @@ import { abi as IUniswapV3PoolStateABI } from '@uniswap/v3-core/artifacts/contra import { computePoolAddress } from '@uniswap/v3-sdk' import { FeeAmount, Pool } from '@uniswap/v3-sdk' import { V3_CORE_FACTORY_ADDRESSES } from 'constants/addresses' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useMultipleContractSingleData } from 'hooks/multicall' -import useActiveWeb3React from 'hooks/useActiveWeb3React' import JSBI from 'jsbi' import { useMemo } from 'react' import { IUniswapV3PoolStateInterface } from 'types/v3/IUniswapV3PoolState' diff --git a/src/hooks/useSwapCallArguments.tsx b/src/hooks/useSwapCallArguments.tsx index efe045889..7f8710790 100644 --- a/src/hooks/useSwapCallArguments.tsx +++ b/src/hooks/useSwapCallArguments.tsx @@ -4,7 +4,7 @@ import { Currency, Percent, TradeType } from '@uniswap/sdk-core' import { Router as V2SwapRouter, Trade as V2Trade } from '@uniswap/v2-sdk' import { FeeOptions, SwapRouter as V3SwapRouter, Trade as V3Trade } from '@uniswap/v3-sdk' import { SWAP_ROUTER_ADDRESSES, V3_ROUTER_ADDRESS } from 'constants/addresses' -import useActiveWeb3React from 'hooks/useActiveWeb3React' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useMemo } from 'react' import approveAmountCalldata from 'utils/approveAmountCalldata' diff --git a/src/hooks/useTokenList/index.tsx b/src/hooks/useTokenList/index.tsx index 70a412b8d..468bd4dd8 100644 --- a/src/hooks/useTokenList/index.tsx +++ b/src/hooks/useTokenList/index.tsx @@ -1,6 +1,6 @@ import { Token } from '@uniswap/sdk-core' import { TokenInfo, TokenList } from '@uniswap/token-lists' -import useActiveWeb3React from 'hooks/useActiveWeb3React' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { createContext, PropsWithChildren, useCallback, useContext, useEffect, useMemo, useState } from 'react' import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo' import resolveENSContentHash from 'utils/resolveENSContentHash' diff --git a/src/hooks/useTokenList/useQueryTokens.ts b/src/hooks/useTokenList/useQueryTokens.ts index 08375d20c..509fda8f5 100644 --- a/src/hooks/useTokenList/useQueryTokens.ts +++ b/src/hooks/useTokenList/useQueryTokens.ts @@ -1,5 +1,5 @@ import { nativeOnChain } from 'constants/tokens' -import useActiveWeb3React from 'hooks/useActiveWeb3React' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useTokenBalances } from 'hooks/useCurrencyBalance' import useDebounce from 'hooks/useDebounce' import { useMemo } from 'react' diff --git a/src/hooks/useTransactionDeadline.ts b/src/hooks/useTransactionDeadline.ts index 78589672b..b58ce5d68 100644 --- a/src/hooks/useTransactionDeadline.ts +++ b/src/hooks/useTransactionDeadline.ts @@ -6,7 +6,7 @@ import { useAtom } from 'jotai' import { useMemo } from 'react' import { transactionTtlAtom } from 'state/settings' -import useActiveWeb3React from './useActiveWeb3React' +import useActiveWeb3React from './connectWeb3/useActiveWeb3React' /** Returns the default transaction TTL for the chain, in minutes. */ export function useDefaultTransactionTtl(): number { diff --git a/src/hooks/useUSDCPrice.ts b/src/hooks/useUSDCPrice.ts index cda4b2765..a3f122d30 100644 --- a/src/hooks/useUSDCPrice.ts +++ b/src/hooks/useUSDCPrice.ts @@ -1,7 +1,7 @@ import { Currency, CurrencyAmount, Price, Token, TradeType } from '@uniswap/sdk-core' import { SupportedChainId } from 'constants/chains' import { DAI_OPTIMISM, USDC_ARBITRUM, USDC_MAINNET, USDC_POLYGON } from 'constants/tokens' -import useActiveWeb3React from 'hooks/useActiveWeb3React' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useMemo, useRef } from 'react' import tryParseCurrencyAmount from 'utils/tryParseCurrencyAmount' diff --git a/src/hooks/useV3SwapPools.ts b/src/hooks/useV3SwapPools.ts index 290e968c7..c0d5eccde 100644 --- a/src/hooks/useV3SwapPools.ts +++ b/src/hooks/useV3SwapPools.ts @@ -1,7 +1,7 @@ import { Currency, Token } from '@uniswap/sdk-core' import { FeeAmount, Pool } from '@uniswap/v3-sdk' import { SupportedChainId } from 'constants/chains' -import useActiveWeb3React from 'hooks/useActiveWeb3React' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useMemo } from 'react' import { useAllCurrencyCombinations } from './useAllCurrencyCombinations' diff --git a/src/state/multicall.tsx b/src/state/multicall.tsx index d26a7f807..bc9e9e91b 100644 --- a/src/state/multicall.tsx +++ b/src/state/multicall.tsx @@ -1,5 +1,5 @@ import { createMulticall } from '@uniswap/redux-multicall' -import useActiveWeb3React from 'hooks/useActiveWeb3React' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import useBlockNumber from 'hooks/useBlockNumber' import { useInterfaceMulticall } from 'hooks/useContract' import { combineReducers, createStore } from 'redux' diff --git a/yarn.lock b/yarn.lock index 0ae408a07..c4ec2ca0f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3309,10 +3309,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b" integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== -"@types/node@^13.13.5": - version "13.13.52" - resolved "https://registry.npmjs.org/@types/node/-/node-13.13.52.tgz" - integrity sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ== +"@types/node@^16.7.13": + version "16.11.41" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.41.tgz#88eb485b1bfdb4c224d878b7832239536aa2f813" + integrity sha512-mqoYK2TnVjdkGk8qXAVGc/x9nSaTpSrFaGFm43BUH3IdoBV0nta6hYaGmdOvIMlbHJbUEVen3gvwpwovAZKNdQ== "@types/normalize-package-data@^2.4.0": version "2.4.1" From 15e021817d587e58f8e95382a2c8fda22ed294a8 Mon Sep 17 00:00:00 2001 From: Kristie Huang Date: Wed, 22 Jun 2022 11:59:13 -0400 Subject: [PATCH 02/44] feat: add gradient identicons (#39) * Add useENSAvatar from interface, WIP identicon * Only use gradient icon * Remove broken useENSAvatar hook for now * nit: update comments --- .../images/identicons/IdenticonGradient-0.png | Bin 0 -> 7099 bytes .../images/identicons/IdenticonGradient-1.png | Bin 0 -> 7639 bytes .../images/identicons/IdenticonGradient-2.png | Bin 0 -> 8034 bytes .../images/identicons/IdenticonGradient-3.png | Bin 0 -> 7709 bytes .../images/identicons/IdenticonGradient-4.png | Bin 0 -> 8440 bytes .../images/identicons/IdenticonGradient-5.png | Bin 0 -> 8391 bytes .../images/identicons/IdenticonGradient-6.png | Bin 0 -> 7684 bytes .../images/identicons/IdenticonGradient-7.png | Bin 0 -> 7641 bytes .../images/identicons/IdenticonGradient-8.png | Bin 0 -> 7534 bytes .../images/identicons/IdenticonGradient-9.png | Bin 0 -> 7099 bytes .../ConnectWallet/ConnectedWalletChip.tsx | 7 ++-- src/icons/identicon.tsx | 38 ++++++++++++++++++ src/icons/index.tsx | 4 ++ 13 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 src/assets/images/identicons/IdenticonGradient-0.png create mode 100644 src/assets/images/identicons/IdenticonGradient-1.png create mode 100644 src/assets/images/identicons/IdenticonGradient-2.png create mode 100644 src/assets/images/identicons/IdenticonGradient-3.png create mode 100644 src/assets/images/identicons/IdenticonGradient-4.png create mode 100644 src/assets/images/identicons/IdenticonGradient-5.png create mode 100644 src/assets/images/identicons/IdenticonGradient-6.png create mode 100644 src/assets/images/identicons/IdenticonGradient-7.png create mode 100644 src/assets/images/identicons/IdenticonGradient-8.png create mode 100644 src/assets/images/identicons/IdenticonGradient-9.png create mode 100644 src/icons/identicon.tsx diff --git a/src/assets/images/identicons/IdenticonGradient-0.png b/src/assets/images/identicons/IdenticonGradient-0.png new file mode 100644 index 0000000000000000000000000000000000000000..bcb318660a0389008a5cc29e3d3deecf243b33c8 GIT binary patch literal 7099 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 literal 0 HcmV?d00001 diff --git a/src/assets/images/identicons/IdenticonGradient-1.png b/src/assets/images/identicons/IdenticonGradient-1.png new file mode 100644 index 0000000000000000000000000000000000000000..f83406e77e32d968799aca460b3f79ac567fea2b GIT binary patch literal 7639 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 FV1g*Nl!^cV literal 0 HcmV?d00001 diff --git a/src/assets/images/identicons/IdenticonGradient-2.png b/src/assets/images/identicons/IdenticonGradient-2.png new file mode 100644 index 0000000000000000000000000000000000000000..2b82db5966fca0412a4536901a13b51a42698be1 GIT binary patch literal 8034 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 aw*Lph->N`rNO02t0000?ujgAMmGtJ`TeoiA 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 literal 0 HcmV?d00001 diff --git a/src/assets/images/identicons/IdenticonGradient-6.png b/src/assets/images/identicons/IdenticonGradient-6.png new file mode 100644 index 0000000000000000000000000000000000000000..321131777087f085c127354074b9f9fd4685d792 GIT binary patch literal 7684 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 Hu0mjfF{rve literal 0 HcmV?d00001 diff --git a/src/assets/images/identicons/IdenticonGradient-8.png b/src/assets/images/identicons/IdenticonGradient-8.png new file mode 100644 index 0000000000000000000000000000000000000000..0a2a4165f201bc69b780f4e0424407b74616c7c0 GIT binary patch literal 7534 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 Ef<5|~1^@s6 literal 0 HcmV?d00001 diff --git a/src/assets/images/identicons/IdenticonGradient-9.png b/src/assets/images/identicons/IdenticonGradient-9.png new file mode 100644 index 0000000000000000000000000000000000000000..37546a725fadc8c03247edf70b93b7f107c44df3 GIT binary patch literal 7099 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 literal 0 HcmV?d00001 diff --git a/src/components/ConnectWallet/ConnectedWalletChip.tsx b/src/components/ConnectWallet/ConnectedWalletChip.tsx index 0dde17ed2..22a81ea27 100644 --- a/src/components/ConnectWallet/ConnectedWalletChip.tsx +++ b/src/components/ConnectWallet/ConnectedWalletChip.tsx @@ -1,5 +1,6 @@ import { TextButton } from 'components/Button' import Row from 'components/Row' +import Identicon from 'icons/identicon' import styled from 'styled-components/macro' import { ThemedText } from 'theme' @@ -9,20 +10,20 @@ const AccountButton = styled(TextButton)<{ hidden?: boolean }>` ` export default function ConnectedWalletChip({ disabled, account }: { disabled?: boolean; account?: string }) { - // TODO(kristiehuang): AccountDialog & AccountAvatar UI does not yet exist + // TODO(kristiehuang): AccountDialog UI does not yet exist // const [open, setOpen] = useState(false) return ( <>

+ ) } @@ -66,7 +149,7 @@ function MetaMaskButton({ walletName, logoSrc, onClick }: ButtonProps) { return ( - {walletName} + {walletName} {walletName} @@ -87,21 +170,25 @@ function NoWalletButton() { } export function ConnectWalletDialog() { - const [mmConnection, wcConnection] = connections - // TODO(kristiehuang): what happens when I try to connect one wallet without disconnecting the other? + const [mmConnection, wcTileConnection, wcPopupConnection] = connections return ( <>
Connect wallet} /> - + - + diff --git a/src/components/ConnectWallet/ConnectedWalletChip.tsx b/src/components/ConnectWallet/ConnectedWalletChip.tsx index 22a81ea27..aa287aab3 100644 --- a/src/components/ConnectWallet/ConnectedWalletChip.tsx +++ b/src/components/ConnectWallet/ConnectedWalletChip.tsx @@ -1,5 +1,6 @@ import { TextButton } from 'components/Button' import Row from 'components/Row' +import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import Identicon from 'icons/identicon' import styled from 'styled-components/macro' import { ThemedText } from 'theme' @@ -9,10 +10,12 @@ const AccountButton = styled(TextButton)<{ hidden?: boolean }>` visibility: ${({ hidden }) => hidden && 'hidden'}; ` -export default function ConnectedWalletChip({ disabled, account }: { disabled?: boolean; account?: string }) { +export default function ConnectedWalletChip({ disabled }: { disabled?: boolean }) { // TODO(kristiehuang): AccountDialog UI does not yet exist // const [open, setOpen] = useState(false) + const { account } = useActiveWeb3React() + return ( <> ) => void + onClickConnectWallet?: (e?: React.MouseEvent) => void } -export default function Wallet({ disabled, account, onConnectWallet }: WalletProps) { +export default function Wallet({ disabled, onClickConnectWallet }: WalletProps) { // Attempt to connect eagerly on mount useEffect(() => { - connections.forEach(([wallet, _]) => wallet.connectEagerly()) + connections.forEach(([wallet, _]) => wallet?.connectEagerly?.()) }, []) - const isConnected = Boolean(account) + const { account, active } = useActiveWeb3React() + + const isConnected = active && Boolean(account) return isConnected ? ( - + ) : ( - + ) } diff --git a/src/components/Swap/index.tsx b/src/components/Swap/index.tsx index ec650107e..f21d6db3a 100644 --- a/src/components/Swap/index.tsx +++ b/src/components/Swap/index.tsx @@ -54,7 +54,7 @@ export default function Swap(props: SwapProps) { useSyncConvenienceFee(props) useSyncTokenDefaults(props) - const { active, account } = useActiveWeb3React() + const { active } = useActiveWeb3React() const [wrapper, setWrapper] = useState(null) const [displayTxHash, setDisplayTxHash] = useAtom(displayTxHashAtom) @@ -71,7 +71,7 @@ export default function Swap(props: SwapProps) { return ( <>
Swap}> - +
diff --git a/src/components/Widget.tsx b/src/components/Widget.tsx index ff38aad25..9501e8da3 100644 --- a/src/components/Widget.tsx +++ b/src/components/Widget.tsx @@ -3,7 +3,6 @@ import { TokenInfo } from '@uniswap/token-lists' import { Provider as Eip1193Provider } from '@web3-react/types' import { DEFAULT_LOCALE, SUPPORTED_LOCALES, SupportedLocale } from 'constants/locales' import { ActiveWeb3Provider } from 'hooks/connectWeb3/useActiveWeb3React' -import { useActiveProvider } from 'hooks/connectWeb3/useConnect' import { TransactionsUpdater } from 'hooks/transactions' import { BlockNumberProvider } from 'hooks/useBlockNumber' import { TokenListProvider } from 'hooks/useTokenList' @@ -97,7 +96,7 @@ export type WidgetProps = { } export default function Widget(props: PropsWithChildren) { - const { children, theme, jsonRpcEndpoint, dialog: userDialog, className, onError } = props + const { children, theme, jsonRpcEndpoint, provider, 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.`) @@ -113,11 +112,6 @@ export default function Widget(props: PropsWithChildren) { return props.locale ?? DEFAULT_LOCALE }, [props.locale]) - const activeProvider = useActiveProvider() - const provider = useMemo(() => { - return props.provider ?? activeProvider - }, [props.provider, activeProvider]) - const [dialog, setDialog] = useState(null) return ( diff --git a/src/cosmos/useJsonRpcEndpoint.ts b/src/cosmos/useJsonRpcEndpoint.ts index 331a1dd42..998625ecd 100644 --- a/src/cosmos/useJsonRpcEndpoint.ts +++ b/src/cosmos/useJsonRpcEndpoint.ts @@ -7,24 +7,24 @@ if (INFURA_KEY === undefined) { console.warn(`INFURA_KEY must be a defined environment variable to use JsonRpcEndpoints in the cosmos viewer`) } -export const INFURA_NETWORK_URLS: { [key in SupportedChainId]?: string } = INFURA_KEY +export const INFURA_NETWORK_URLS: { [key in SupportedChainId]?: string[] } = INFURA_KEY ? { - [SupportedChainId.MAINNET]: `https://mainnet.infura.io/v3/${INFURA_KEY}`, - [SupportedChainId.RINKEBY]: `https://rinkeby.infura.io/v3/${INFURA_KEY}`, - [SupportedChainId.ROPSTEN]: `https://ropsten.infura.io/v3/${INFURA_KEY}`, - [SupportedChainId.GOERLI]: `https://goerli.infura.io/v3/${INFURA_KEY}`, - [SupportedChainId.KOVAN]: `https://kovan.infura.io/v3/${INFURA_KEY}`, - [SupportedChainId.OPTIMISM]: `https://optimism-mainnet.infura.io/v3/${INFURA_KEY}`, - [SupportedChainId.OPTIMISTIC_KOVAN]: `https://optimism-kovan.infura.io/v3/${INFURA_KEY}`, - [SupportedChainId.ARBITRUM_ONE]: `https://arbitrum-mainnet.infura.io/v3/${INFURA_KEY}`, - [SupportedChainId.ARBITRUM_RINKEBY]: `https://arbitrum-rinkeby.infura.io/v3/${INFURA_KEY}`, - [SupportedChainId.POLYGON]: `https://polygon-mainnet.infura.io/v3/${INFURA_KEY}`, - [SupportedChainId.POLYGON_MUMBAI]: `https://polygon-mumbai.infura.io/v3/${INFURA_KEY}`, + [SupportedChainId.MAINNET]: [`https://mainnet.infura.io/v3/${INFURA_KEY}`], + [SupportedChainId.RINKEBY]: [`https://rinkeby.infura.io/v3/${INFURA_KEY}`], + [SupportedChainId.ROPSTEN]: [`https://ropsten.infura.io/v3/${INFURA_KEY}`], + [SupportedChainId.GOERLI]: [`https://goerli.infura.io/v3/${INFURA_KEY}`], + [SupportedChainId.KOVAN]: [`https://kovan.infura.io/v3/${INFURA_KEY}`], + [SupportedChainId.OPTIMISM]: [`https://optimism-mainnet.infura.io/v3/${INFURA_KEY}`], + [SupportedChainId.OPTIMISTIC_KOVAN]: [`https://optimism-kovan.infura.io/v3/${INFURA_KEY}`], + [SupportedChainId.ARBITRUM_ONE]: [`https://arbitrum-mainnet.infura.io/v3/${INFURA_KEY}`], + [SupportedChainId.ARBITRUM_RINKEBY]: [`https://arbitrum-rinkeby.infura.io/v3/${INFURA_KEY}`], + [SupportedChainId.POLYGON]: [`https://polygon-mainnet.infura.io/v3/${INFURA_KEY}`], + [SupportedChainId.POLYGON_MUMBAI]: [`https://polygon-mumbai.infura.io/v3/${INFURA_KEY}`], } : {} export default function useJsonRpcEndpoint() { - const endpoints = Object.entries(INFURA_NETWORK_URLS).reduce<{ [chainId: string]: string }>( + const endpoints = Object.entries(INFURA_NETWORK_URLS).reduce<{ [chainId: string]: string[] }>( (acc, [chainId, url]) => ({ ...acc, [SupportedChainId[Number(chainId)]]: url, @@ -35,5 +35,5 @@ export default function useJsonRpcEndpoint() { return useOption('jsonRpcEndpoint', { options: endpoints, defaultValue: INFURA_NETWORK_URLS[SupportedChainId.MAINNET] ? SupportedChainId[SupportedChainId.MAINNET] : NONE, - }) + })?.[0] } diff --git a/src/cosmos/useProvider.ts b/src/cosmos/useProvider.ts index 0e3ce5807..bfe750862 100644 --- a/src/cosmos/useProvider.ts +++ b/src/cosmos/useProvider.ts @@ -11,9 +11,15 @@ enum Wallet { MetaMask = 'MetaMask', WalletConnect = 'WalletConenct', } -const [metaMask] = initializeConnector((actions) => new MetaMask(actions)) +const [metaMask] = initializeConnector((actions) => new MetaMask({ actions })) const [walletConnect] = initializeConnector( - (actions) => new WalletConnect(actions, { rpc: INFURA_NETWORK_URLS as { [chainId: number]: string } }) + (actions) => + new WalletConnect({ + actions, + options: { + rpc: INFURA_NETWORK_URLS as { [chainId: number]: string[] }, + }, + }) ) export default function useProvider() { diff --git a/src/hooks/connectWeb3/useActiveWeb3React.tsx b/src/hooks/connectWeb3/useActiveWeb3React.tsx index 0d6acb5fb..eeecee826 100644 --- a/src/hooks/connectWeb3/useActiveWeb3React.tsx +++ b/src/hooks/connectWeb3/useActiveWeb3React.tsx @@ -1,10 +1,14 @@ import { ExternalProvider, JsonRpcProvider, Web3Provider } from '@ethersproject/providers' -import { initializeConnector, Web3ReactHooks } from '@web3-react/core' +import { getPriorityConnector, initializeConnector, Web3ReactHooks } from '@web3-react/core' import { EIP1193 } from '@web3-react/eip1193' import { EMPTY } from '@web3-react/empty' -import { Connector, Provider as Eip1193Provider } from '@web3-react/types' +import { MetaMask } from '@web3-react/metamask' +import { Connector, Provider as Eip1193Provider, Web3ReactStore } from '@web3-react/types' import { Url } from '@web3-react/url' -import { createContext, PropsWithChildren, useContext, useEffect, useMemo } from 'react' +import { WalletConnect } from '@web3-react/walletconnect' +import { Buffer } from 'buffer' +import { createContext, PropsWithChildren, useContext, useMemo } from 'react' +import { useCallback } from 'react' import JsonRpcConnector from 'utils/JsonRpcConnector' export type Web3ContextType = { @@ -15,32 +19,31 @@ export type Web3ContextType = { account?: ReturnType active?: ReturnType activating?: ReturnType - error?: ReturnType } const [EMPTY_CONNECTOR, EMPTY_HOOKS] = initializeConnector(() => EMPTY) const EMPTY_STATE = { connector: EMPTY_CONNECTOR, hooks: EMPTY_HOOKS } const EMPTY_WEB3: Web3ContextType = { connector: EMPTY } const EMPTY_CONTEXT = { web3: EMPTY_WEB3, updateWeb3: (updateContext: Web3ContextType) => console.log(updateContext) } -export const Web3Context = createContext(EMPTY_CONTEXT) +const Web3Context = createContext(EMPTY_CONTEXT) export default function useActiveWeb3React() { const { web3 } = useContext(Web3Context) return web3 } -export function useUpdateActiveWeb3ReactCallback() { +function useUpdateActiveWeb3ReactCallback() { const { updateWeb3 } = useContext(Web3Context) return updateWeb3 } -export function getNetwork(jsonRpcEndpoint?: string | JsonRpcProvider) { +function getNetwork(jsonRpcEndpoint?: string | JsonRpcProvider) { if (jsonRpcEndpoint) { let connector, hooks if (JsonRpcProvider.isProvider(jsonRpcEndpoint)) { ;[connector, hooks] = initializeConnector((actions) => new JsonRpcConnector(actions, jsonRpcEndpoint)) } else { - ;[connector, hooks] = initializeConnector((actions) => new Url(actions, jsonRpcEndpoint)) + ;[connector, hooks] = initializeConnector((actions) => new Url({ actions, url: jsonRpcEndpoint })) } connector.activate() return { connector, hooks } @@ -56,7 +59,7 @@ function getWallet(provider?: JsonRpcProvider | Eip1193Provider) { } else if (JsonRpcProvider.isProvider((provider as any).provider)) { throw new Error('Eip1193Bridge is experimental: pass your ethers Provider directly') } else { - ;[connector, hooks] = initializeConnector((actions) => new EIP1193(actions, provider)) + ;[connector, hooks] = initializeConnector((actions) => new EIP1193({ actions, provider })) } connector.activate() return { connector, hooks } @@ -64,6 +67,8 @@ function getWallet(provider?: JsonRpcProvider | Eip1193Provider) { return EMPTY_STATE } +export let connections: [Connector, Web3ReactHooks][] = [] + interface ActiveWeb3ProviderProps { jsonRpcEndpoint?: string | JsonRpcProvider provider?: Eip1193Provider | JsonRpcProvider @@ -71,28 +76,39 @@ interface ActiveWeb3ProviderProps { export function ActiveWeb3Provider({ jsonRpcEndpoint, - provider, + provider: propsProvider, children, }: PropsWithChildren) { + const metaMaskConnection = useMemo( + () => toWeb3Connection(initializeConnector((actions) => new MetaMask({ actions }))), + [] + ) + const walletConnectConnectionQR = useMemo(() => getWalletConnectConnection(false, jsonRpcEndpoint), [jsonRpcEndpoint]) // WC via tile QR code scan + const walletConnectConnectionPopup = useMemo( + () => getWalletConnectConnection(true, jsonRpcEndpoint), + [jsonRpcEndpoint] + ) // WC via built-in popup + connections = [metaMaskConnection, walletConnectConnectionQR, walletConnectConnectionPopup] + const network = useMemo(() => getNetwork(jsonRpcEndpoint), [jsonRpcEndpoint]) - const wallet = useMemo(() => getWallet(provider), [provider]) + const activeProvider = useActiveWalletProvider() + const wallet = useMemo(() => getWallet(propsProvider ?? activeProvider), [propsProvider, activeProvider]) // eslint-disable-next-line prefer-const - let { connector, hooks } = wallet.hooks.useIsActive() || network === EMPTY_STATE ? wallet : network + let { connector, hooks } = wallet !== EMPTY_STATE ? wallet : network let accounts = hooks.useAccounts() let account = hooks.useAccount() let activating = hooks.useIsActivating() let active = hooks.useIsActive() let chainId = hooks.useChainId() - let error = hooks.useError() let library = hooks.useProvider() const web3 = useMemo(() => { if (connector === EMPTY || !(active || activating)) { return EMPTY_WEB3 } - return { connector, library, chainId, accounts, account, active, activating, error } - }, [account, accounts, activating, active, chainId, connector, error, library]) + return { connector, library, chainId, accounts, account, active, activating } + }, [account, accounts, activating, active, chainId, connector, library]) const updateWeb3 = (updateContext: Web3ContextType) => { connector = updateContext.connector @@ -101,16 +117,84 @@ export function ActiveWeb3Provider({ activating = updateContext.activating ?? false active = updateContext.active ?? false chainId = updateContext.chainId - error = updateContext.error library = updateContext.library as Web3Provider } - // Log web3 errors to facilitate debugging. - useEffect(() => { - if (error) { - console.error('web3 error:', error) + return {children} +} + +export type Web3Connection = [Connector, Web3ReactHooks] + +function toWeb3Connection([connector, hooks]: [T, Web3ReactHooks, Web3ReactStore]): [ + T, + Web3ReactHooks +] { + return [connector, hooks] +} + +function getWalletConnectConnection(useDefault: boolean, jsonRpcEndpoint?: string | JsonRpcProvider) { + // TODO(kristiehuang): implement RPC URL fallback, then jsonRpcEndpoint can be optional + + // WalletConnect relies on Buffer, so it must be polyfilled. + if (!('Buffer' in window)) { + window.Buffer = Buffer + } + + let rpcUrl: string + if (JsonRpcProvider.isProvider(jsonRpcEndpoint)) { + rpcUrl = jsonRpcEndpoint.connection.url + } else { + rpcUrl = jsonRpcEndpoint ?? '' // TODO(kristiehuang): use fallback RPC URL + } + + return toWeb3Connection( + initializeConnector( + (actions) => + new WalletConnect({ + actions, + options: { + rpc: { + 1: [rpcUrl].filter((url) => url !== undefined && url !== ''), + }, + qrcode: useDefault, + }, // TODO(kristiehuang): WC only works on network chainid 1? + }) + ) + ) +} + +function useActiveWalletProvider(): Web3Provider | undefined { + return getPriorityConnector(...connections).usePriorityProvider() as Web3Provider +} + +export function useConnectCallback(connection: Web3Connection) { + const [wallet, hooks] = connection + const isActive = hooks.useIsActive() + const accounts = hooks.useAccounts() + const account = hooks.useAccount() + const activating = hooks.useIsActivating() + const chainId = hooks.useChainId() + const library = hooks.useProvider() + + const updateActiveWeb3ReactCallback = useUpdateActiveWeb3ReactCallback() + + const activateWallet = useCallback(() => { + if (!isActive) { + wallet.activate() + } else { + // wallet should be already be active + const updateContext: Web3ContextType = { + connector: wallet, + library, + accounts, + account, + activating, + active: isActive, + chainId, + } + updateActiveWeb3ReactCallback(updateContext) } - }, [error]) + }, [account, accounts, activating, chainId, isActive, library, updateActiveWeb3ReactCallback, wallet]) - return {children} + return activateWallet } diff --git a/src/hooks/connectWeb3/useConnect.ts b/src/hooks/connectWeb3/useConnect.ts deleted file mode 100644 index 39917fd1b..000000000 --- a/src/hooks/connectWeb3/useConnect.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { JsonRpcProvider, Web3Provider } from '@ethersproject/providers' -import { getPriorityConnector, initializeConnector, Web3ReactHooks } from '@web3-react/core' -import { MetaMask } from '@web3-react/metamask' -import { Connector, Web3ReactStore } from '@web3-react/types' -import { WalletConnect } from '@web3-react/walletconnect' -import { Buffer } from 'buffer' -import { getNetwork, useUpdateActiveWeb3ReactCallback, Web3ContextType } from 'hooks/connectWeb3/useActiveWeb3React' -import { useCallback } from 'react' - -export type Web3Connection = [Connector, Web3ReactHooks] - -function toWeb3Connection([connector, hooks]: [T, Web3ReactHooks, Web3ReactStore]): [ - T, - Web3ReactHooks -] { - return [connector, hooks] -} - -const metaMaskConnection = toWeb3Connection(initializeConnector((actions) => new MetaMask(actions))) - -function getWalletConnectConnection(jsonRpcEndpoint?: string | JsonRpcProvider) { - // WalletConnect relies on Buffer, so it must be polyfilled. - if (!('Buffer' in window)) { - window.Buffer = Buffer - } - - let rpcUrl: string - if (JsonRpcProvider.isProvider(jsonRpcEndpoint)) { - rpcUrl = jsonRpcEndpoint.connection.url - } else { - rpcUrl = jsonRpcEndpoint ?? '' // FIXME: use fallback RPC URL - } - - return toWeb3Connection( - initializeConnector( - (actions) => - new WalletConnect( - actions, - { - rpc: { 1: rpcUrl }, // TODO(kristiehuang): WC only works on network chainid 1? - }, - false - ) - ) - ) -} - -export const connections = [metaMaskConnection, getWalletConnectConnection()] - -export function useActiveProvider(): Web3Provider | undefined { - const activeWalletProvider = getPriorityConnector(...connections).usePriorityProvider() as Web3Provider - const { connector: network } = getNetwork() // Return network-only provider if no wallet is connected - return activeWalletProvider ?? network.provider -} - -export default function useConnect(connection: Web3Connection) { - const [wallet, hooks] = connection - const isActive = hooks.useIsActive() - const accounts = hooks.useAccounts() - const account = hooks.useAccount() - const activating = hooks.useIsActivating() - const chainId = hooks.useChainId() - const error = hooks.useError() - const library = hooks.useProvider() - const updateActiveWeb3ReactCallback = useUpdateActiveWeb3ReactCallback() - - const useWallet = useCallback(() => { - if (!isActive) { - wallet.activate() - } else { - // wallet should be already be active - const updateContext: Web3ContextType = { - connector: wallet, - library, - accounts, - account, - activating, - active: isActive, - chainId, - error, - } - updateActiveWeb3ReactCallback(updateContext) - } - }, [account, accounts, activating, chainId, error, isActive, library, updateActiveWeb3ReactCallback, wallet]) - - return useWallet -} diff --git a/src/utils/JsonRpcConnector.ts b/src/utils/JsonRpcConnector.ts index 2826d8734..7cb2161c5 100644 --- a/src/utils/JsonRpcConnector.ts +++ b/src/utils/JsonRpcConnector.ts @@ -13,7 +13,8 @@ export default class JsonRpcConnector extends Connector { this.actions.update({ chainId: parseChainId(chainId) }) }) .on('disconnect', (error: ProviderRpcError): void => { - this.actions.reportError(error) + this.onError?.(error) + this.actions.resetState() }) .on('chainChanged', (chainId: string): void => { this.actions.update({ chainId: parseChainId(chainId) }) @@ -33,7 +34,8 @@ export default class JsonRpcConnector extends Connector { ]) this.actions.update({ chainId, accounts }) } catch (e) { - this.actions.reportError(e) + this.actions.resetState() + throw e } } } diff --git a/yarn.lock b/yarn.lock index c4ec2ca0f..65b8f9585 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3341,6 +3341,13 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== +"@types/qrcode@^1.4.2": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.4.2.tgz#7d7142d6fa9921f195db342ed08b539181546c74" + integrity sha512-7uNT9L4WQTNJejHTSTdaJhfBSCN73xtXaHFyBJ8TSwiLhe4PRuTue7Iph0s2nG9R/ifUaSnGhLUOZavlBEqDWQ== + dependencies: + "@types/node" "*" + "@types/qs@^6.9.2": version "6.9.7" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" @@ -4044,69 +4051,69 @@ dependencies: "@walletconnect/window-getters" "^1.0.0" -"@web3-react/core@8.0.30-beta.0": - version "8.0.30-beta.0" - resolved "https://registry.yarnpkg.com/@web3-react/core/-/core-8.0.30-beta.0.tgz#744c8a47a59eb75ff8f3e407c2190a90762a9905" - integrity sha512-Z6PTxs1xi7kO8fh/pjz29aY1rGyM6Bvoy909sSdVl2BRBMvo+XsezF6JEsAi/XbMrmtOEqzmaTSuokU0KusePw== +"@web3-react/core@8.0.33-beta.0": + version "8.0.33-beta.0" + resolved "https://registry.yarnpkg.com/@web3-react/core/-/core-8.0.33-beta.0.tgz#018686511e9dcff1001965048d5e16c9ae60268a" + integrity sha512-qif/t5zycmlercIGPGNLpxM0+qiRZpbBIVFHgTGSS67bssgZoeBAXwafesbD1KRjiubATMgYBsKy16KskjxwTA== dependencies: - "@web3-react/store" "^8.0.22-beta.0" - "@web3-react/types" "^8.0.17-beta.0" + "@web3-react/store" "^8.0.24-beta.0" + "@web3-react/types" "^8.0.19-beta.0" zustand "^4.0.0-rc.0" optionalDependencies: "@ethersproject/providers" "^5" -"@web3-react/eip1193@8.0.23-beta.0": - version "8.0.23-beta.0" - resolved "https://registry.yarnpkg.com/@web3-react/eip1193/-/eip1193-8.0.23-beta.0.tgz#5a38665f4a81b972b2f2ca2f70de99067551dbe8" - integrity sha512-q8jGkMo3HKcuptYw+IdEDECbSUnt4ueuk1lP4nRi4dA0vKUaeX5vjTstjs6uGD97JC1CN3jN7qtEHB+jXfGyvg== +"@web3-react/eip1193@8.0.25-beta.0": + version "8.0.25-beta.0" + resolved "https://registry.yarnpkg.com/@web3-react/eip1193/-/eip1193-8.0.25-beta.0.tgz#1df94b7c5ecef1ea97181015f0717a26b0a08bc7" + integrity sha512-L96B+xWjklR23bYyxYCvDWWcBsoThgY076oKmjJDel+3BnuEK7IH92aWcB1wUo3oLKzETdOm7eWi0ZyjBoM3Ag== dependencies: - "@web3-react/types" "^8.0.17-beta.0" + "@web3-react/types" "^8.0.19-beta.0" -"@web3-react/empty@8.0.17-beta.0": - version "8.0.17-beta.0" - resolved "https://registry.yarnpkg.com/@web3-react/empty/-/empty-8.0.17-beta.0.tgz#992a7cf4baa58bd9094536dffcd62ac14f57b305" - integrity sha512-1aKYWE7E5EUVol7M1G5kEiiVXG0e+9g4ehFk8O/3zKsjEWRBIP6xwT12Q/9H6AwrDLc2hNJmKe0BW7F8mDd6KQ== +"@web3-react/empty@8.0.19-beta.0": + version "8.0.19-beta.0" + resolved "https://registry.yarnpkg.com/@web3-react/empty/-/empty-8.0.19-beta.0.tgz#884114cbdc5a82a2cd2e8e661162d2e69284a2ba" + integrity sha512-AAAeaS3Hn6PHdoP86vgx4O0ZjWQ9RFYAID+rJy84DDjLUodl9XLLYidGZmotEdv7kpKw5B5OtxnTjiMrSw64ag== dependencies: - "@web3-react/types" "^8.0.17-beta.0" + "@web3-react/types" "^8.0.19-beta.0" -"@web3-react/metamask@8.0.24-beta.0": - version "8.0.24-beta.0" - resolved "https://registry.yarnpkg.com/@web3-react/metamask/-/metamask-8.0.24-beta.0.tgz#047bfe321f4064bd3dd0443c06d2497373e6d511" - integrity sha512-kCzWkGiha/7dn73DKB6LkSAsp+j6SdVnBg7Dp9EyInes2npwcIVl7PABQMop0g+7Drv4bpiJBwMIetcVHkr9sQ== +"@web3-react/metamask@8.0.26-beta.0": + version "8.0.26-beta.0" + resolved "https://registry.yarnpkg.com/@web3-react/metamask/-/metamask-8.0.26-beta.0.tgz#c22dfec8d09be95493668fbb140e5d3a6ff45a8e" + integrity sha512-srYzA4nvMBWlH7kJLCunYueyucbTLmGqDUOK3QquVmQ+avvwOlYVzyvKPQrHf4M2+6f6GpzWYuw6AjNknvfaIA== dependencies: "@metamask/detect-provider" "^1.2.0" - "@web3-react/types" "^8.0.17-beta.0" + "@web3-react/types" "^8.0.19-beta.0" -"@web3-react/store@^8.0.22-beta.0": - version "8.0.22-beta.0" - resolved "https://registry.yarnpkg.com/@web3-react/store/-/store-8.0.22-beta.0.tgz#c4a56edb5d89f14bd6b1d53910a793c2bf854fb9" - integrity sha512-8POKnRntMA+8aUCGwMcPV32PM9A+CGEu1Eha7PKU5uaMrNMwf6eIufpm0pa9FxDZtk5n8lFCfGed8DApwiUvjw== +"@web3-react/store@^8.0.24-beta.0": + version "8.0.24-beta.0" + resolved "https://registry.yarnpkg.com/@web3-react/store/-/store-8.0.24-beta.0.tgz#fb7e25d8478fce8ae95e6f50d7310f97876dc591" + integrity sha512-R2MA1lByr9LwQKhso3PYq31GRYEPbM1HKC4uWGGAY42JBXG+Fnhf/0fUIDcdJzdT6tonGBKRgqXKXQ+8HEjgzA== dependencies: "@ethersproject/address" "^5" - "@web3-react/types" "^8.0.17-beta.0" + "@web3-react/types" "^8.0.19-beta.0" zustand "^4.0.0-rc.0" -"@web3-react/types@8.0.17-beta.0", "@web3-react/types@^8.0.17-beta.0": - version "8.0.17-beta.0" - resolved "https://registry.yarnpkg.com/@web3-react/types/-/types-8.0.17-beta.0.tgz#a7a2898065a67a0e574572f49a73d39879e75676" - integrity sha512-5zh+BrykylyN29P18/rNPFVVOI4Go/jYLwl5a/55eb5X1wgCfPU62ZBiVeKIOWBCwJ558uie31p909xJjeE2ig== +"@web3-react/types@8.0.19-beta.0", "@web3-react/types@^8.0.19-beta.0": + version "8.0.19-beta.0" + resolved "https://registry.yarnpkg.com/@web3-react/types/-/types-8.0.19-beta.0.tgz#07daeef090cb252302f3bdf138d133df99826fb1" + integrity sha512-tM63N3IiL5E61HBvx62kDmQlQ/zCfnlYAkJCQnLWFpzQ2Kafcfpi0qAynvFye8DqSgy4E3d6kQah/NIs6BL+qw== dependencies: zustand "^4.0.0-rc.0" -"@web3-react/url@8.0.22-beta.0": - version "8.0.22-beta.0" - resolved "https://registry.yarnpkg.com/@web3-react/url/-/url-8.0.22-beta.0.tgz#721425fe9271fa5bcafe697233f060eb47bb82bb" - integrity sha512-ru8KybaxmqGQWacbsU1K2v3hgW0CWXBbn0hSkOCMMtl16h7hdVQ3lFuuzBXjErWH5bZj3/AuzAOb468y4/9yLw== +"@web3-react/url@8.0.24-beta.0": + version "8.0.24-beta.0" + resolved "https://registry.yarnpkg.com/@web3-react/url/-/url-8.0.24-beta.0.tgz#7417cdb37bace531aa0fe76ccc4ccef8ed55c0e9" + integrity sha512-+pAMWVKNqg2h0iCRQ6b8zETdmSk903Vg3jinjMwwWSdxLAArCPlUoMgCtnxIFonQzwphynCEIo9w55uQBrOglA== dependencies: "@ethersproject/providers" "^5" - "@web3-react/types" "^8.0.17-beta.0" + "@web3-react/types" "^8.0.19-beta.0" -"@web3-react/walletconnect@8.0.31-beta.0": - version "8.0.31-beta.0" - resolved "https://registry.yarnpkg.com/@web3-react/walletconnect/-/walletconnect-8.0.31-beta.0.tgz#d2fa2b998364a9390ddeba954f200b130d63e84b" - integrity sha512-j3Mdf4t3xSjs1JreLH9GQInS2zvLj5zMn9bwrq1xvgdx0ETAyh00gNTB+WoYKtjWBHmoXxTQ7YXwKfB6C/5Dsw== +"@web3-react/walletconnect@8.0.34-beta.0": + version "8.0.34-beta.0" + resolved "https://registry.yarnpkg.com/@web3-react/walletconnect/-/walletconnect-8.0.34-beta.0.tgz#b0dee4d8e37dbb8783b46f6e181cbb7c317084d8" + integrity sha512-CG6dc6Lq0QVNvTp5lCFycIaHoPPV+X/6ay3swd5t2LD9R9Ao8sSzF0zU9Fi1iq41UDCue6WO4u2Lp7uHNAIcxw== dependencies: - "@web3-react/types" "^8.0.17-beta.0" + "@web3-react/types" "^8.0.19-beta.0" eventemitter3 "^4.0.7" "@webassemblyjs/ast@1.11.1": @@ -5505,6 +5512,15 @@ cliui@^5.0.0: strip-ansi "^5.2.0" wrap-ansi "^5.1.0" +cliui@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" + integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^6.2.0" + cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" @@ -6383,6 +6399,11 @@ emojis-list@^3.0.0: resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== +encode-utf8@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/encode-utf8/-/encode-utf8-1.0.3.tgz#f30fdd31da07fb596f281beb2f6b027851994cda" + integrity sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw== + encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -11103,6 +11124,11 @@ pngjs@^3.0.0, pngjs@^3.3.0, pngjs@^3.3.3: resolved "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz" integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w== +pngjs@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" + integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== + pofile@^1.1.0: version "1.1.1" resolved "https://registry.npmjs.org/pofile/-/pofile-1.1.1.tgz" @@ -11328,6 +11354,16 @@ qrcode@1.4.4: pngjs "^3.3.0" yargs "^13.2.4" +qrcode@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.0.tgz#95abb8a91fdafd86f8190f2836abbfc500c72d1b" + integrity sha512-9MgRpgVc+/+47dFvQeD6U2s0Z92EsKzcHogtum4QB+UNd025WOJSHvn/hjk9xmzj7Stj95CyUAs31mrjxliEsQ== + dependencies: + dijkstrajs "^1.0.1" + encode-utf8 "^1.0.3" + pngjs "^5.0.0" + yargs "^15.3.1" + qs@6.7.0: version "6.7.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" @@ -13674,6 +13710,15 @@ wrap-ansi@^5.1.0: string-width "^3.0.0" strip-ansi "^5.0.0" +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -13814,6 +13859,14 @@ yargs-parser@^13.1.2: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^18.1.2: + version "18.1.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + yargs-parser@^20.2.2, yargs-parser@^20.2.3: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" @@ -13858,6 +13911,23 @@ yargs@^13.2.4: y18n "^4.0.0" yargs-parser "^13.1.2" +yargs@^15.3.1: + version "15.4.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" + integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== + dependencies: + cliui "^6.0.0" + decamelize "^1.2.0" + find-up "^4.1.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^4.2.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^18.1.2" + yeast@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" From b62cd44aa088772bb149ef842553ccfa37638f1d Mon Sep 17 00:00:00 2001 From: Kristie Huang Date: Tue, 5 Jul 2022 15:46:25 -0400 Subject: [PATCH 05/44] feat: switch to using w3r's Web3ReactProvider (#50) * Use w3r's provider * Use provider instead of library * reformat * Finalize changes * Update useWeb3React imports * Rename file * PR review fixes * Remove useConnectCallback fxn * Revert w3r/network version --- package.json | 1 + .../ConnectWallet/ConnectWalletDialog.tsx | 6 +- .../ConnectWallet/ConnectedWalletChip.tsx | 4 +- src/components/ConnectWallet/index.tsx | 9 +- src/components/EtherscanLink.tsx | 4 +- src/components/Swap/SwapButton/index.tsx | 4 +- src/components/Swap/Toolbar/index.tsx | 24 ++--- src/components/Swap/index.tsx | 6 +- src/components/TokenSelect/TokenOptions.tsx | 4 +- src/components/TokenSelect/index.tsx | 8 +- src/components/Widget.tsx | 2 +- src/cosmos/useProvider.ts | 2 +- src/hooks/connectWeb3/useWeb3React.tsx | 93 +++++++++++++++++++ src/hooks/multicall.ts | 4 +- .../useClientSideSmartOrderRouterTrade.ts | 6 +- src/hooks/swap/useSendSwapTransaction.tsx | 12 +-- src/hooks/swap/useSwapApproval.ts | 6 +- src/hooks/swap/useSwapCallback.tsx | 10 +- src/hooks/swap/useSwapInfo.tsx | 4 +- src/hooks/swap/useSyncConvenienceFee.ts | 4 +- src/hooks/swap/useSyncTokenDefaults.ts | 4 +- src/hooks/swap/useWrapCallback.tsx | 4 +- src/hooks/transactions/index.tsx | 8 +- src/hooks/transactions/updater.tsx | 14 +-- src/hooks/useAllV3Routes.ts | 4 +- src/hooks/useApproval.ts | 6 +- src/hooks/useArgentWalletContract.ts | 4 +- src/hooks/useAutoSlippageTolerance.ts | 4 +- src/hooks/useBlockNumber.tsx | 16 ++-- src/hooks/useClientSideV3Trade.ts | 4 +- src/hooks/useContract.ts | 14 +-- src/hooks/useCurrency.ts | 6 +- src/hooks/useCurrencyBalance.ts | 4 +- src/hooks/useERC20Permit.ts | 12 +-- src/hooks/useIsArgentWallet.ts | 4 +- src/hooks/useIsValidBlock.ts | 6 +- src/hooks/useNativeCurrency.ts | 4 +- src/hooks/useOnSupportedNetwork.ts | 5 +- src/hooks/usePools.ts | 4 +- src/hooks/useSwapCallArguments.tsx | 8 +- src/hooks/useTokenList/index.tsx | 14 +-- src/hooks/useTokenList/useQueryTokens.ts | 4 +- src/hooks/useTransactionDeadline.ts | 5 +- src/hooks/useUSDCPrice.ts | 4 +- src/hooks/useV3SwapPools.ts | 4 +- src/icons/identicon.tsx | 4 +- src/index.tsx | 2 + src/polyfills.ts | 6 ++ src/state/multicall.tsx | 4 +- src/utils/index.ts | 12 +-- yarn.lock | 8 ++ 51 files changed, 262 insertions(+), 153 deletions(-) create mode 100644 src/hooks/connectWeb3/useWeb3React.tsx create mode 100644 src/polyfills.ts diff --git a/package.json b/package.json index 257f09bef..1b32132e2 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "@web3-react/eip1193": "8.0.25-beta.0", "@web3-react/empty": "8.0.19-beta.0", "@web3-react/metamask": "8.0.26-beta.0", + "@web3-react/network": "8.0.26-beta.0", "@web3-react/types": "8.0.19-beta.0", "@web3-react/url": "8.0.24-beta.0", "@web3-react/walletconnect": "8.0.34-beta.0", diff --git a/src/components/ConnectWallet/ConnectWalletDialog.tsx b/src/components/ConnectWallet/ConnectWalletDialog.tsx index e6993d4d6..a4b85fec7 100644 --- a/src/components/ConnectWallet/ConnectWalletDialog.tsx +++ b/src/components/ConnectWallet/ConnectWalletDialog.tsx @@ -8,7 +8,7 @@ import Column from 'components/Column' import { Header } from 'components/Dialog' import Row from 'components/Row' import EventEmitter from 'events' -import { connections, useConnectCallback, Web3Connection } from 'hooks/connectWeb3/useActiveWeb3React' +import { connections, Web3Connection } from 'hooks/connectWeb3/useWeb3React' import { atom, useAtom } from 'jotai' import QRCode from 'qrcode' import { useEffect, useState } from 'react' @@ -181,13 +181,13 @@ export function ConnectWalletDialog() { walletName="WalletConnect" logoSrc={WALLETCONNECT_ICON_URL} connection={wcTileConnection} - onClick={useConnectCallback(wcPopupConnection)} + onClick={() => wcPopupConnection[0].activate()} /> mmConnection[0].activate()} /> diff --git a/src/components/ConnectWallet/ConnectedWalletChip.tsx b/src/components/ConnectWallet/ConnectedWalletChip.tsx index aa287aab3..0ffc781e0 100644 --- a/src/components/ConnectWallet/ConnectedWalletChip.tsx +++ b/src/components/ConnectWallet/ConnectedWalletChip.tsx @@ -1,6 +1,6 @@ +import { useWeb3React } from '@web3-react/core' import { TextButton } from 'components/Button' import Row from 'components/Row' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import Identicon from 'icons/identicon' import styled from 'styled-components/macro' import { ThemedText } from 'theme' @@ -14,7 +14,7 @@ export default function ConnectedWalletChip({ disabled }: { disabled?: boolean } // TODO(kristiehuang): AccountDialog UI does not yet exist // const [open, setOpen] = useState(false) - const { account } = useActiveWeb3React() + const { account } = useWeb3React() return ( <> diff --git a/src/components/ConnectWallet/index.tsx b/src/components/ConnectWallet/index.tsx index 57c459c70..d0a2cfcc2 100644 --- a/src/components/ConnectWallet/index.tsx +++ b/src/components/ConnectWallet/index.tsx @@ -1,4 +1,5 @@ -import useActiveWeb3React, { connections } from 'hooks/connectWeb3/useActiveWeb3React' +import { useWeb3React } from '@web3-react/core' +import { connections } from 'hooks/connectWeb3/useWeb3React' import { useEffect } from 'react' import ConnectedWalletChip from './ConnectedWalletChip' @@ -12,12 +13,12 @@ interface WalletProps { export default function Wallet({ disabled, onClickConnectWallet }: WalletProps) { // Attempt to connect eagerly on mount useEffect(() => { - connections.forEach(([wallet, _]) => wallet?.connectEagerly?.()) + connections.forEach(([wallet, _]) => (wallet.connectEagerly ? wallet.connectEagerly() : wallet.activate())) }, []) - const { account, active } = useActiveWeb3React() + const { account, isActive } = useWeb3React() - const isConnected = active && Boolean(account) + const isConnected = isActive && Boolean(account) return isConnected ? ( ) : ( diff --git a/src/components/EtherscanLink.tsx b/src/components/EtherscanLink.tsx index c27f0bd14..188794c40 100644 --- a/src/components/EtherscanLink.tsx +++ b/src/components/EtherscanLink.tsx @@ -1,5 +1,5 @@ +import { useWeb3React } from '@web3-react/core' import { SupportedChainId } from 'constants/chains' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { Link } from 'icons' import { ReactNode, useMemo } from 'react' import styled from 'styled-components/macro' @@ -22,7 +22,7 @@ interface EtherscanLinkProps { } export default function EtherscanLink({ data, type, color = 'currentColor', children }: EtherscanLinkProps) { - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() const url = useMemo( () => data && getExplorerLink(chainId || SupportedChainId.MAINNET, data, type), [chainId, data, type] diff --git a/src/components/Swap/SwapButton/index.tsx b/src/components/Swap/SwapButton/index.tsx index 47ebcba31..5d2039c1f 100644 --- a/src/components/Swap/SwapButton/index.tsx +++ b/src/components/Swap/SwapButton/index.tsx @@ -1,5 +1,5 @@ import { Trans } from '@lingui/macro' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' +import { useWeb3React } from '@web3-react/core' import { useSwapInfo } from 'hooks/swap' import { useSwapApprovalOptimizedTrade } from 'hooks/swap/useSwapApproval' import { useSwapCallback } from 'hooks/swap/useSwapCallback' @@ -27,7 +27,7 @@ interface SwapButtonProps { } export default memo(function SwapButton({ disabled }: SwapButtonProps) { - const { account, chainId } = useActiveWeb3React() + const { account, chainId } = useWeb3React() const { [Field.INPUT]: { currency: inputCurrency, diff --git a/src/components/Swap/Toolbar/index.tsx b/src/components/Swap/Toolbar/index.tsx index a74a8a535..f9d58e796 100644 --- a/src/components/Swap/Toolbar/index.tsx +++ b/src/components/Swap/Toolbar/index.tsx @@ -1,5 +1,5 @@ +import { useWeb3React } from '@web3-react/core' import { ALL_SUPPORTED_CHAIN_IDS } from 'constants/chains' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useIsAmountPopulated, useSwapInfo } from 'hooks/swap' import useWrapCallback, { WrapType } from 'hooks/swap/useWrapCallback' import { largeIconCss } from 'icons' @@ -18,7 +18,7 @@ const ToolbarRow = styled(Row)` ` export default memo(function Toolbar() { - const { active, activating, chainId } = useActiveWeb3React() + const { account, isActivating, chainId } = useWeb3React() const { [Field.INPUT]: { currency: inputCurrency, balance: inputBalance, amount: inputAmount }, [Field.OUTPUT]: { currency: outputCurrency, usdc: outputUSDC }, @@ -28,8 +28,8 @@ export default memo(function Toolbar() { const isAmountPopulated = useIsAmountPopulated() const { type: wrapType } = useWrapCallback() const caption = useMemo(() => { - if (!active || !chainId) { - if (activating) return + if (!account || !chainId) { + if (isActivating) return return } @@ -60,19 +60,19 @@ export default memo(function Toolbar() { return }, [ - activating, - active, + account, chainId, - impact, - inputAmount, - inputBalance, inputCurrency, - isAmountPopulated, outputCurrency, - outputUSDC, + isAmountPopulated, + isActivating, state, - trade, + inputBalance, + inputAmount, wrapType, + trade, + outputUSDC, + impact, ]) return ( diff --git a/src/components/Swap/index.tsx b/src/components/Swap/index.tsx index f21d6db3a..0bfe7e1b2 100644 --- a/src/components/Swap/index.tsx +++ b/src/components/Swap/index.tsx @@ -1,8 +1,8 @@ import { JsonRpcProvider } from '@ethersproject/providers' import { Trans } from '@lingui/macro' +import { useWeb3React } from '@web3-react/core' import { Provider as Eip1193Provider } from '@web3-react/types' import Wallet from 'components/ConnectWallet' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { SwapInfoProvider } from 'hooks/swap/useSwapInfo' import useSyncConvenienceFee, { FeeOptions } from 'hooks/swap/useSyncConvenienceFee' import useSyncTokenDefaults, { TokenDefaults } from 'hooks/swap/useSyncTokenDefaults' @@ -54,7 +54,7 @@ export default function Swap(props: SwapProps) { useSyncConvenienceFee(props) useSyncTokenDefaults(props) - const { active } = useActiveWeb3React() + const { isActive } = useWeb3React() const [wrapper, setWrapper] = useState(null) const [displayTxHash, setDisplayTxHash] = useAtom(displayTxHashAtom) @@ -62,7 +62,7 @@ export default function Swap(props: SwapProps) { const displayTx = getTransactionFromMap(pendingTxs, displayTxHash) const onSupportedNetwork = useOnSupportedNetwork() - const isDisabled = !(active && onSupportedNetwork) + const isDisabled = !(isActive && onSupportedNetwork) const focused = useHasFocus(wrapper) diff --git a/src/components/TokenSelect/TokenOptions.tsx b/src/components/TokenSelect/TokenOptions.tsx index 1dae6577a..3674fec2a 100644 --- a/src/components/TokenSelect/TokenOptions.tsx +++ b/src/components/TokenSelect/TokenOptions.tsx @@ -1,6 +1,6 @@ import { useLingui } from '@lingui/react' import { Currency } from '@uniswap/sdk-core' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' +import { useWeb3React } from '@web3-react/core' import useCurrencyBalance from 'hooks/useCurrencyBalance' import useNativeEvent from 'hooks/useNativeEvent' import useScrollbar from 'hooks/useScrollbar' @@ -87,7 +87,7 @@ function TokenOption({ index, value, style }: TokenOptionProps) { e.ref = ref.current ?? undefined } - const { account } = useActiveWeb3React() + const { account } = useWeb3React() const balance = useCurrencyBalance(account, value) return ( diff --git a/src/components/TokenSelect/index.tsx b/src/components/TokenSelect/index.tsx index 1ad390504..6160c07c5 100644 --- a/src/components/TokenSelect/index.tsx +++ b/src/components/TokenSelect/index.tsx @@ -1,6 +1,6 @@ import { t, Trans } from '@lingui/macro' import { Currency } from '@uniswap/sdk-core' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' +import { useWeb3React } from '@web3-react/core' import { useCurrencyBalances } from 'hooks/useCurrencyBalance' import useNativeCurrency from 'hooks/useNativeCurrency' import useTokenList, { useIsTokenListLoaded, useQueryTokens } from 'hooks/useTokenList' @@ -23,7 +23,7 @@ const SearchInput = styled(StringInput)` ` function usePrefetchBalances() { - const { account } = useActiveWeb3React() + const { account } = useWeb3React() const tokenList = useTokenList() const prefetchedTokenList = useRef() useCurrencyBalances(account, tokenList !== prefetchedTokenList.current ? tokenList : undefined) @@ -31,7 +31,7 @@ function usePrefetchBalances() { } function useAreBalancesLoaded(): boolean { - const { account } = useActiveWeb3React() + const { account } = useWeb3React() const tokens = useTokenList() const native = useNativeCurrency() const currencies = useMemo(() => [native, ...tokens], [native, tokens]) @@ -70,7 +70,7 @@ export function TokenSelectDialog({ value, onSelect, onClose }: TokenSelectDialo useEffect(() => input.current?.focus({ preventScroll: true }), [input]) const [options, setOptions] = useState | null>(null) - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() const listHasTokens = useMemo(() => list.some((token) => token.chainId === chainId), [chainId, list]) if (!listHasTokens && isLoaded) { diff --git a/src/components/Widget.tsx b/src/components/Widget.tsx index 9501e8da3..3b98f945d 100644 --- a/src/components/Widget.tsx +++ b/src/components/Widget.tsx @@ -2,7 +2,7 @@ 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 { ActiveWeb3Provider } from 'hooks/connectWeb3/useActiveWeb3React' +import { ActiveWeb3Provider } from 'hooks/connectWeb3/useWeb3React' import { TransactionsUpdater } from 'hooks/transactions' import { BlockNumberProvider } from 'hooks/useBlockNumber' import { TokenListProvider } from 'hooks/useTokenList' diff --git a/src/cosmos/useProvider.ts b/src/cosmos/useProvider.ts index bfe750862..92fe03cbd 100644 --- a/src/cosmos/useProvider.ts +++ b/src/cosmos/useProvider.ts @@ -33,7 +33,7 @@ export default function useProvider() { } async function activateConnector(connectorType: Wallet | undefined) { - let connector: Connector + let connector: Connector | undefined switch (connectorType) { case Wallet.MetaMask: await metaMask.activate() diff --git a/src/hooks/connectWeb3/useWeb3React.tsx b/src/hooks/connectWeb3/useWeb3React.tsx new file mode 100644 index 000000000..05f74fa7f --- /dev/null +++ b/src/hooks/connectWeb3/useWeb3React.tsx @@ -0,0 +1,93 @@ +import { JsonRpcProvider } from '@ethersproject/providers' +import { initializeConnector, Web3ReactHooks, Web3ReactProvider } from '@web3-react/core' +import { EIP1193 } from '@web3-react/eip1193' +import { MetaMask } from '@web3-react/metamask' +import { Network } from '@web3-react/network' +import { Connector, Provider as Eip1193Provider, Web3ReactStore } from '@web3-react/types' +import { Url } from '@web3-react/url' +import { WalletConnect } from '@web3-react/walletconnect' +import { SupportedChainId } from 'constants/chains' +import { PropsWithChildren, useMemo } from 'react' + +export let connections: [Connector, Web3ReactHooks][] = [] +export type Web3Connection = [Connector, Web3ReactHooks] + +function toWeb3Connection([connector, hooks]: [T, Web3ReactHooks, Web3ReactStore]): [ + T, + Web3ReactHooks +] { + return [connector, hooks] +} + +function getWallet(provider?: JsonRpcProvider | Eip1193Provider) { + if (!provider) return + if (JsonRpcProvider.isProvider(provider)) { + return toWeb3Connection(initializeConnector((actions) => new Url({ actions, url: provider }))) + } else if (JsonRpcProvider.isProvider((provider as any).provider)) { + throw new Error('Eip1193Bridge is experimental: pass your ethers Provider directly') + } else { + return toWeb3Connection(initializeConnector((actions) => new EIP1193({ actions, provider }))) + } +} + +function getWalletConnectConnection(useDefault: boolean, jsonRpcEndpoint?: string | JsonRpcProvider) { + // TODO(kristiehuang): implement RPC URL fallback, then jsonRpcEndpoint can be optional + let rpcUrl: string + if (JsonRpcProvider.isProvider(jsonRpcEndpoint)) { + rpcUrl = jsonRpcEndpoint.connection.url + } else { + // TODO(kristiehuang): temporarily needed to instantiate WC if integrator doesn't provide RPC + // replace logic when we add in fallback JSON RPC URL + rpcUrl = jsonRpcEndpoint ?? 'https://cloudflare-eth.com' + } + + return toWeb3Connection( + initializeConnector( + (actions) => + new WalletConnect({ + actions, + options: { + rpc: { + 1: [rpcUrl].filter((url) => url !== undefined && url !== ''), + }, + qrcode: useDefault, + }, // TODO(kristiehuang): WC only works on network chainid 1? + }) + ) + ) +} + +interface ActiveWeb3ProviderProps { + provider?: Eip1193Provider | JsonRpcProvider + jsonRpcEndpoint?: string | JsonRpcProvider +} + +export function ActiveWeb3Provider({ + provider: propsProvider, + jsonRpcEndpoint, + children, +}: PropsWithChildren) { + const integratorConnection = useMemo(() => getWallet(propsProvider), [propsProvider]) + const metaMaskConnection = useMemo( + () => toWeb3Connection(initializeConnector((actions) => new MetaMask({ actions }))), + [] + ) + const walletConnectConnectionQR = useMemo(() => getWalletConnectConnection(false, jsonRpcEndpoint), [jsonRpcEndpoint]) // WC via tile QR code scan + const walletConnectConnectionPopup = useMemo( + () => getWalletConnectConnection(true, jsonRpcEndpoint), + [jsonRpcEndpoint] + ) // WC via built-in popup + + const networkConnection = useMemo(() => { + if (!jsonRpcEndpoint) return + const networkRpc = JsonRpcProvider.isProvider(jsonRpcEndpoint) ? [jsonRpcEndpoint] : [jsonRpcEndpoint] + const urlMap = { [SupportedChainId.MAINNET]: networkRpc } + return toWeb3Connection(initializeConnector((actions) => new Network({ actions, urlMap }))) + }, [jsonRpcEndpoint]) + + connections = [metaMaskConnection, walletConnectConnectionQR, walletConnectConnectionPopup] + if (integratorConnection) connections = [integratorConnection, ...connections] + if (networkConnection) connections.push(networkConnection) + + return {children} +} diff --git a/src/hooks/multicall.ts b/src/hooks/multicall.ts index c1ee2aa9d..f2ce869e3 100644 --- a/src/hooks/multicall.ts +++ b/src/hooks/multicall.ts @@ -1,4 +1,4 @@ -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' +import { useWeb3React } from '@web3-react/core' import useBlockNumber from 'hooks/useBlockNumber' import multicall from 'state/multicall' @@ -37,7 +37,7 @@ export function useSingleContractWithCallData( } function useCallContext() { - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() const latestBlock = useBlockNumber() return { chainId, latestBlock } } diff --git a/src/hooks/routing/useClientSideSmartOrderRouterTrade.ts b/src/hooks/routing/useClientSideSmartOrderRouterTrade.ts index 1479907cf..f0d4e2616 100644 --- a/src/hooks/routing/useClientSideSmartOrderRouterTrade.ts +++ b/src/hooks/routing/useClientSideSmartOrderRouterTrade.ts @@ -2,6 +2,7 @@ import 'setimmediate' import { Protocol } from '@uniswap/router-sdk' import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' import { SupportedChainId } from 'constants/chains' import useDebounce from 'hooks/useDebounce' import { useStablecoinAmountFromFiatValue } from 'hooks/useUSDCPrice' @@ -9,7 +10,6 @@ import { useCallback, useMemo } from 'react' import { GetQuoteResult, InterfaceTrade, TradeState } from 'state/routing/types' import { computeRoutes, transformRoutesToTrade } from 'state/routing/utils' -import useActiveWeb3React from '../connectWeb3/useActiveWeb3React' import useWrapCallback, { WrapType } from '../swap/useWrapCallback' import { useGetIsValidBlock } from '../useIsValidBlock' import usePoll from '../usePoll' @@ -66,8 +66,8 @@ export default function useClientSideSmartOrderRouterTrade chainId && library && { chainId, provider: library }, [chainId, library]) + const { provider } = useWeb3React() + const params = useMemo(() => chainId && provider && { chainId, provider }, [chainId, provider]) const config = useMemo(() => getConfig(chainId), [chainId]) const { type: wrapType } = useWrapCallback() diff --git a/src/hooks/swap/useSendSwapTransaction.tsx b/src/hooks/swap/useSendSwapTransaction.tsx index dd7eaabcf..33a347199 100644 --- a/src/hooks/swap/useSendSwapTransaction.tsx +++ b/src/hooks/swap/useSendSwapTransaction.tsx @@ -40,12 +40,12 @@ interface FailedCall extends SwapCallEstimate { export default function useSendSwapTransaction( account: string | null | undefined, chainId: number | undefined, - library: JsonRpcProvider | undefined, + provider: JsonRpcProvider | undefined, trade: AnyTrade | undefined, // trade to execute, required swapCalls: SwapCall[] ): { callback: null | (() => Promise) } { return useMemo(() => { - if (!trade || !library || !account || !chainId) { + if (!trade || !provider || !account || !chainId) { return { callback: null } } return { @@ -64,7 +64,7 @@ export default function useSendSwapTransaction( value, } - return library + return provider .estimateGas(tx) .then((gasEstimate) => { return { @@ -75,7 +75,7 @@ export default function useSendSwapTransaction( .catch((gasError) => { console.debug('Gas estimate failed, trying eth_call to extract error', call) - return library + return provider .call(tx) .then((result) => { console.debug('Unexpected successful call after failed estimate gas', call, gasError, result) @@ -110,7 +110,7 @@ export default function useSendSwapTransaction( call: { address, calldata, value }, } = bestCallOption - return library + return provider .getSigner() .sendTransaction({ from: account, @@ -136,5 +136,5 @@ export default function useSendSwapTransaction( }) }, } - }, [account, chainId, library, swapCalls, trade]) + }, [account, chainId, provider, swapCalls, trade]) } diff --git a/src/hooks/swap/useSwapApproval.ts b/src/hooks/swap/useSwapApproval.ts index 9a2cbd094..11e4fe4ed 100644 --- a/src/hooks/swap/useSwapApproval.ts +++ b/src/hooks/swap/useSwapApproval.ts @@ -2,8 +2,8 @@ import { Protocol, Trade } from '@uniswap/router-sdk' import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core' import { Pair, Route as V2Route, Trade as V2Trade } from '@uniswap/v2-sdk' import { Pool, Route as V3Route, Trade as V3Trade } from '@uniswap/v3-sdk' +import { useWeb3React } from '@web3-react/core' import { SWAP_ROUTER_ADDRESSES, V2_ROUTER_ADDRESS, V3_ROUTER_ADDRESS } from 'constants/addresses' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useERC20PermitFromTrade, UseERC20PermitState } from 'hooks/useERC20Permit' import useTransactionDeadline from 'hooks/useTransactionDeadline' import { useCallback, useMemo } from 'react' @@ -18,7 +18,7 @@ function useSwapApprovalStates( allowedSlippage: Percent, useIsPendingApproval: (token?: Token, spender?: string) => boolean ): { v2: ApprovalState; v3: ApprovalState; v2V3: ApprovalState } { - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() const amountToApprove = useMemo( () => (trade && trade.inputAmount.currency.isToken ? trade.maximumAmountIn(allowedSlippage) : undefined), @@ -42,7 +42,7 @@ export function useSwapRouterAddress( | Trade | undefined ) { - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() return useMemo( () => chainId diff --git a/src/hooks/swap/useSwapCallback.tsx b/src/hooks/swap/useSwapCallback.tsx index ebaa215a4..38d88b833 100644 --- a/src/hooks/swap/useSwapCallback.tsx +++ b/src/hooks/swap/useSwapCallback.tsx @@ -4,7 +4,7 @@ import { TransactionResponse } from '@ethersproject/providers' import { Trans } from '@lingui/macro' import { Percent } from '@uniswap/sdk-core' import { FeeOptions } from '@uniswap/v3-sdk' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' +import { useWeb3React } from '@web3-react/core' import useENS from 'hooks/useENS' import { SignatureData } from 'hooks/useERC20Permit' import { AnyTrade, useSwapCallArguments } from 'hooks/useSwapCallArguments' @@ -42,7 +42,7 @@ export function useSwapCallback({ deadline, feeOptions, }: UseSwapCallbackArgs): UseSwapCallbackReturns { - const { account, chainId, library } = useActiveWeb3React() + const { account, chainId, provider } = useWeb3React() const swapCalls = useSwapCallArguments( trade, @@ -52,13 +52,13 @@ export function useSwapCallback({ deadline, feeOptions ) - const { callback } = useSendSwapTransaction(account, chainId, library, trade, swapCalls) + const { callback } = useSendSwapTransaction(account, chainId, provider, trade, swapCalls) const { address: recipientAddress } = useENS(recipientAddressOrName) const recipient = recipientAddressOrName === null ? account : recipientAddress return useMemo(() => { - if (!trade || !library || !account || !chainId || !callback) { + if (!trade || !provider || !account || !chainId || !callback) { return { state: SwapCallbackState.INVALID, error: Missing dependencies } } if (!recipient) { @@ -73,5 +73,5 @@ export function useSwapCallback({ state: SwapCallbackState.VALID, callback: async () => callback(), } - }, [trade, library, account, chainId, callback, recipient, recipientAddressOrName]) + }, [trade, provider, account, chainId, callback, recipient, recipientAddressOrName]) } diff --git a/src/hooks/swap/useSwapInfo.tsx b/src/hooks/swap/useSwapInfo.tsx index f69c6b265..92f66b574 100644 --- a/src/hooks/swap/useSwapInfo.tsx +++ b/src/hooks/swap/useSwapInfo.tsx @@ -1,5 +1,5 @@ import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' +import { useWeb3React } from '@web3-react/core' import { useCurrencyBalances } from 'hooks/useCurrencyBalance' import useSlippage, { DEFAULT_SLIPPAGE, Slippage } from 'hooks/useSlippage' import useUSDCPriceImpact, { PriceImpact } from 'hooks/useUSDCPriceImpact' @@ -57,7 +57,7 @@ function useComputeSwapInfo(): SwapInfo { [isExactIn, isWrapping, parsedAmount, trade.trade?.outputAmount] ) - const { account } = useActiveWeb3React() + const { account } = useWeb3React() const [balanceIn, balanceOut] = useCurrencyBalances( account, useMemo(() => [currencyIn, currencyOut], [currencyIn, currencyOut]) diff --git a/src/hooks/swap/useSyncConvenienceFee.ts b/src/hooks/swap/useSyncConvenienceFee.ts index aa169ad53..3d5238a5e 100644 --- a/src/hooks/swap/useSyncConvenienceFee.ts +++ b/src/hooks/swap/useSyncConvenienceFee.ts @@ -1,5 +1,5 @@ import { Percent } from '@uniswap/sdk-core' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' +import { useWeb3React } from '@web3-react/core' import { useUpdateAtom } from 'jotai/utils' import { useEffect } from 'react' import { feeOptionsAtom } from 'state/swap' @@ -10,7 +10,7 @@ export interface FeeOptions { } export default function useSyncConvenienceFee({ convenienceFee, convenienceFeeRecipient }: FeeOptions) { - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() const updateFeeOptions = useUpdateAtom(feeOptionsAtom) useEffect(() => { diff --git a/src/hooks/swap/useSyncTokenDefaults.ts b/src/hooks/swap/useSyncTokenDefaults.ts index 92bc50d09..f0450405f 100644 --- a/src/hooks/swap/useSyncTokenDefaults.ts +++ b/src/hooks/swap/useSyncTokenDefaults.ts @@ -1,6 +1,6 @@ import { Currency } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' import { nativeOnChain } from 'constants/tokens' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useToken } from 'hooks/useCurrency' import useNativeCurrency from 'hooks/useNativeCurrency' import { useUpdateAtom } from 'jotai/utils' @@ -47,7 +47,7 @@ export default function useSyncTokenDefaults({ defaultOutputAmount, }: TokenDefaults) { const updateSwap = useUpdateAtom(swapAtom) - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() const onSupportedNetwork = useOnSupportedNetwork() const nativeCurrency = useNativeCurrency() const defaultOutputToken = useDefaultToken(defaultOutputTokenAddress, chainId) diff --git a/src/hooks/swap/useWrapCallback.tsx b/src/hooks/swap/useWrapCallback.tsx index 5d4f6f9f8..16e1b3ca5 100644 --- a/src/hooks/swap/useWrapCallback.tsx +++ b/src/hooks/swap/useWrapCallback.tsx @@ -1,4 +1,5 @@ import { ContractTransaction } from '@ethersproject/contracts' +import { useWeb3React } from '@web3-react/core' import { WRAPPED_NATIVE_CURRENCY } from 'constants/tokens' import { useWETHContract } from 'hooks/useContract' import { useAtomValue } from 'jotai/utils' @@ -6,7 +7,6 @@ import { useMemo } from 'react' import { Field, swapAtom } from 'state/swap' import tryParseCurrencyAmount from 'utils/tryParseCurrencyAmount' -import useActiveWeb3React from '../connectWeb3/useActiveWeb3React' import useCurrencyBalance from '../useCurrencyBalance' export enum WrapType { @@ -20,7 +20,7 @@ interface UseWrapCallbackReturns { } export default function useWrapCallback(): UseWrapCallbackReturns { - const { account, chainId } = useActiveWeb3React() + const { account, chainId } = useWeb3React() const wrappedNativeCurrencyContract = useWETHContract() const { amount, [Field.INPUT]: inputCurrency, [Field.OUTPUT]: outputCurrency } = useAtomValue(swapAtom) diff --git a/src/hooks/transactions/index.tsx b/src/hooks/transactions/index.tsx index 776f19a80..140ccecc0 100644 --- a/src/hooks/transactions/index.tsx +++ b/src/hooks/transactions/index.tsx @@ -1,5 +1,5 @@ import { Token } from '@uniswap/sdk-core' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' +import { useWeb3React } from '@web3-react/core' import { useAtomValue, useUpdateAtom } from 'jotai/utils' import ms from 'ms.macro' import { useCallback } from 'react' @@ -14,13 +14,13 @@ function isTransactionRecent(transaction: Transaction) { } export function usePendingTransactions() { - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() const txs = useAtomValue(transactionsAtom) return (chainId ? txs[chainId] : null) ?? {} } export function useAddTransaction() { - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() const blockNumber = useBlockNumber() const updateTxs = useUpdateAtom(transactionsAtom) @@ -42,7 +42,7 @@ export function useAddTransaction() { /** Returns the hash of a pending approval transaction, if it exists. */ export function usePendingApproval(token?: Token, spender?: string): string | undefined { - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() const txs = useAtomValue(transactionsAtom) if (!chainId || !token || !spender) return undefined diff --git a/src/hooks/transactions/updater.tsx b/src/hooks/transactions/updater.tsx index ccf4b95e5..b6865649e 100644 --- a/src/hooks/transactions/updater.tsx +++ b/src/hooks/transactions/updater.tsx @@ -1,6 +1,6 @@ import { TransactionReceipt } from '@ethersproject/abstract-provider' +import { useWeb3React } from '@web3-react/core' import { SupportedChainId } from 'constants/chains' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import useBlockNumber, { useFastForwardBlockNumber } from 'hooks/useBlockNumber' import ms from 'ms.macro' import { useCallback, useEffect } from 'react' @@ -45,18 +45,18 @@ interface UpdaterProps { } export default function Updater({ pendingTransactions, onCheck, onReceipt }: UpdaterProps): null { - const { chainId, library } = useActiveWeb3React() + const { chainId, provider } = useWeb3React() const lastBlockNumber = useBlockNumber() const fastForwardBlockNumber = useFastForwardBlockNumber() const getReceipt = useCallback( (hash: string) => { - if (!library || !chainId) throw new Error('No library or chainId') + if (!provider || !chainId) throw new Error('No library or chainId') const retryOptions = RETRY_OPTIONS_BY_CHAIN_ID[chainId] ?? DEFAULT_RETRY_OPTIONS return retry( () => - library.getTransactionReceipt(hash).then((receipt) => { + provider.getTransactionReceipt(hash).then((receipt) => { if (receipt === null) { console.debug(`Retrying tranasaction receipt for ${hash}`) throw new RetryableError() @@ -66,11 +66,11 @@ export default function Updater({ pendingTransactions, onCheck, onReceipt }: Upd retryOptions ) }, - [chainId, library] + [chainId, provider] ) useEffect(() => { - if (!chainId || !library || !lastBlockNumber) return + if (!chainId || !provider || !lastBlockNumber) return const cancels = Object.keys(pendingTransactions) .filter((hash) => shouldCheck(lastBlockNumber, pendingTransactions[hash])) @@ -95,7 +95,7 @@ export default function Updater({ pendingTransactions, onCheck, onReceipt }: Upd return () => { cancels.forEach((cancel) => cancel()) } - }, [chainId, library, lastBlockNumber, getReceipt, fastForwardBlockNumber, onReceipt, onCheck, pendingTransactions]) + }, [chainId, provider, lastBlockNumber, getReceipt, fastForwardBlockNumber, onReceipt, onCheck, pendingTransactions]) return null } diff --git a/src/hooks/useAllV3Routes.ts b/src/hooks/useAllV3Routes.ts index 28f3ec735..641b3b5d1 100644 --- a/src/hooks/useAllV3Routes.ts +++ b/src/hooks/useAllV3Routes.ts @@ -1,6 +1,6 @@ import { Currency } from '@uniswap/sdk-core' import { Pool, Route } from '@uniswap/v3-sdk' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' +import { useWeb3React } from '@web3-react/core' import { useMemo } from 'react' import { useV3SwapPools } from './useV3SwapPools' @@ -63,7 +63,7 @@ export function useAllV3Routes( currencyIn?: Currency, currencyOut?: Currency ): { loading: boolean; routes: Route[] } { - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() const { pools, loading: poolsLoading } = useV3SwapPools(currencyIn, currencyOut) return useMemo(() => { diff --git a/src/hooks/useApproval.ts b/src/hooks/useApproval.ts index f6804b971..96eebdb20 100644 --- a/src/hooks/useApproval.ts +++ b/src/hooks/useApproval.ts @@ -1,7 +1,7 @@ import { MaxUint256 } from '@ethersproject/constants' import { TransactionResponse } from '@ethersproject/providers' import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' +import { useWeb3React } from '@web3-react/core' import { useTokenContract } from 'hooks/useContract' import { useTokenAllowance } from 'hooks/useTokenAllowance' import { useCallback, useMemo } from 'react' @@ -19,7 +19,7 @@ export function useApprovalStateForSpender( spender: string | undefined, useIsPendingApproval: (token?: Token, spender?: string) => boolean ): ApprovalState { - const { account } = useActiveWeb3React() + const { account } = useWeb3React() const token = amountToApprove?.currency?.isToken ? amountToApprove.currency : undefined const currentAllowance = useTokenAllowance(token, account ?? undefined, spender) @@ -48,7 +48,7 @@ export function useApproval( ApprovalState, () => Promise<{ response: TransactionResponse; tokenAddress: string; spenderAddress: string } | undefined> ] { - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() const token = amountToApprove?.currency?.isToken ? amountToApprove.currency : undefined // check the current approval status diff --git a/src/hooks/useArgentWalletContract.ts b/src/hooks/useArgentWalletContract.ts index c9e70b0f0..c407b120f 100644 --- a/src/hooks/useArgentWalletContract.ts +++ b/src/hooks/useArgentWalletContract.ts @@ -1,12 +1,12 @@ +import { useWeb3React } from '@web3-react/core' import ArgentWalletContractABI from 'abis/argent-wallet-contract.json' import { ArgentWalletContract } from 'abis/types' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useContract } from './useContract' import useIsArgentWallet from './useIsArgentWallet' export function useArgentWalletContract(): ArgentWalletContract | null { - const { account } = useActiveWeb3React() + const { account } = useWeb3React() const isArgentWallet = useIsArgentWallet() return useContract( isArgentWallet ? account ?? undefined : undefined, diff --git a/src/hooks/useAutoSlippageTolerance.ts b/src/hooks/useAutoSlippageTolerance.ts index 1e7547574..3baf3cf1d 100644 --- a/src/hooks/useAutoSlippageTolerance.ts +++ b/src/hooks/useAutoSlippageTolerance.ts @@ -1,8 +1,8 @@ import { Trade } from '@uniswap/router-sdk' import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains' import { L2_CHAIN_IDS } from 'constants/chains' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import useNativeCurrency from 'hooks/useNativeCurrency' import JSBI from 'jsbi' import { useMemo } from 'react' @@ -35,7 +35,7 @@ const MAX_AUTO_SLIPPAGE_TOLERANCE = new Percent(25, 100) // 25% export default function useAutoSlippageTolerance( trade: InterfaceTrade | undefined ): Percent { - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() const onL2 = chainId && L2_CHAIN_IDS.includes(chainId) const outputDollarValue = useUSDCValue(trade?.outputAmount) const nativeGasPrice = useGasPrice() diff --git a/src/hooks/useBlockNumber.tsx b/src/hooks/useBlockNumber.tsx index 6f52a7ecf..1e0c533a4 100644 --- a/src/hooks/useBlockNumber.tsx +++ b/src/hooks/useBlockNumber.tsx @@ -1,4 +1,4 @@ -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' +import { useWeb3React } from '@web3-react/core' import useIsWindowVisible from 'hooks/useIsWindowVisible' import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react' @@ -29,7 +29,7 @@ export function useFastForwardBlockNumber(): (block: number) => void { } export function BlockNumberProvider({ children }: { children: ReactNode }) { - const { chainId: activeChainId, library } = useActiveWeb3React() + const { chainId: activeChainId, provider } = useWeb3React() const [{ chainId, block }, setChainBlock] = useState<{ chainId?: number; block?: number }>({ chainId: activeChainId }) const onBlock = useCallback( @@ -48,29 +48,29 @@ export function BlockNumberProvider({ children }: { children: ReactNode }) { const windowVisible = useIsWindowVisible() useEffect(() => { - if (library && activeChainId && windowVisible) { + if (provider && 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 })) let stale = false - library + provider .getBlockNumber() .then((block) => { if (stale) return onBlock(block) }) - .catch((error) => { + .catch((error: Error) => { console.error(`Failed to get block number for chainId ${activeChainId}`, error) }) - library.on('block', onBlock) + provider.on('block', onBlock) return () => { stale = true - library.off('block', onBlock) + provider.off('block', onBlock) } } return undefined - }, [activeChainId, library, onBlock, setChainBlock, windowVisible]) + }, [activeChainId, provider, onBlock, setChainBlock, windowVisible]) const value = useMemo( () => ({ diff --git a/src/hooks/useClientSideV3Trade.ts b/src/hooks/useClientSideV3Trade.ts index 0d8cc96cb..eaaaab958 100644 --- a/src/hooks/useClientSideV3Trade.ts +++ b/src/hooks/useClientSideV3Trade.ts @@ -1,7 +1,7 @@ import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' import { Route, SwapQuoter } from '@uniswap/v3-sdk' +import { useWeb3React } from '@web3-react/core' import { SupportedChainId } from 'constants/chains' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useSingleContractWithCallData } from 'hooks/multicall' import JSBI from 'jsbi' import { useMemo } from 'react' @@ -35,7 +35,7 @@ export function useClientSideV3Trade( const { routes, loading: routesLoading } = useAllV3Routes(currencyIn, currencyOut) const quoter = useV3Quoter() - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() const callData = useMemo( () => amountSpecified diff --git a/src/hooks/useContract.ts b/src/hooks/useContract.ts index 2f4c6db34..586d454e8 100644 --- a/src/hooks/useContract.ts +++ b/src/hooks/useContract.ts @@ -6,6 +6,7 @@ import TickLensJson from '@uniswap/v3-periphery/artifacts/contracts/lens/TickLen import UniswapInterfaceMulticallJson from '@uniswap/v3-periphery/artifacts/contracts/lens/UniswapInterfaceMulticall.sol/UniswapInterfaceMulticall.json' import NonfungiblePositionManagerJson from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json' import V3MigratorJson from '@uniswap/v3-periphery/artifacts/contracts/V3Migrator.sol/V3Migrator.json' +import { useWeb3React } from '@web3-react/core' import ARGENT_WALLET_DETECTOR_ABI from 'abis/argent-wallet-detector.json' import EIP_2612 from 'abis/eip_2612.json' import ENS_PUBLIC_RESOLVER_ABI from 'abis/ens-public-resolver.json' @@ -27,7 +28,6 @@ import { V3_MIGRATOR_ADDRESSES, } from 'constants/addresses' import { WRAPPED_NATIVE_CURRENCY } from 'constants/tokens' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useMemo } from 'react' import { NonfungiblePositionManager, Quoter, TickLens, UniswapInterfaceMulticall } from 'types/v3' import { V3Migrator } from 'types/v3/V3Migrator' @@ -47,21 +47,21 @@ export function useContract( ABI: any, withSignerIfPossible = true ): T | null { - const { library, account, chainId } = useActiveWeb3React() + const { provider, account, chainId } = useWeb3React() return useMemo(() => { - if (!addressOrAddressMap || !ABI || !library || !chainId) return null + if (!addressOrAddressMap || !ABI || !provider || !chainId) return null let address: string | undefined if (typeof addressOrAddressMap === 'string') address = addressOrAddressMap else address = addressOrAddressMap[chainId] if (!address) return null try { - return getContract(address, ABI, library, withSignerIfPossible && account ? account : undefined) + return getContract(address, ABI, provider, withSignerIfPossible && account ? account : undefined) } catch (error) { console.error('Failed to get contract', error) return null } - }, [addressOrAddressMap, ABI, library, chainId, withSignerIfPossible, account]) as T + }, [addressOrAddressMap, ABI, provider, chainId, withSignerIfPossible, account]) as T } export function useV2MigratorContract() { @@ -73,7 +73,7 @@ export function useTokenContract(tokenAddress?: string, withSignerIfPossible?: b } export function useWETHContract(withSignerIfPossible?: boolean) { - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() return useContract( chainId ? WRAPPED_NATIVE_CURRENCY[chainId]?.address : undefined, WETH_ABI, @@ -134,7 +134,7 @@ export function useV3Quoter() { } export function useTickLens(): TickLens | null { - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() const address = chainId ? TICK_LENS_ADDRESSES[chainId] : undefined return useContract(address, TickLensABI) as TickLens | null } diff --git a/src/hooks/useCurrency.ts b/src/hooks/useCurrency.ts index d7ee5eecf..7b307eccf 100644 --- a/src/hooks/useCurrency.ts +++ b/src/hooks/useCurrency.ts @@ -1,8 +1,8 @@ import { arrayify } from '@ethersproject/bytes' import { parseBytes32String } from '@ethersproject/strings' import { Currency, Token } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' import { TOKEN_SHORTHANDS } from 'constants/tokens' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { NEVER_RELOAD, useSingleCallResult } from 'hooks/multicall' import { useBytes32TokenContract, useTokenContract } from 'hooks/useContract' import useNativeCurrency from 'hooks/useNativeCurrency' @@ -30,7 +30,7 @@ function parseStringOrBytes32(str: string | undefined, bytes32: string | undefin * Returns undefined if tokenAddress is invalid or token does not exist. */ export function useTokenFromNetwork(tokenAddress: string | null | undefined): Token | null | undefined { - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() const formattedAddress = isAddress(tokenAddress) @@ -102,7 +102,7 @@ export function useToken(tokenAddress?: string | null): Token | null | undefined */ export function useCurrencyFromMap(tokens: TokenMap, currencyId?: string | null): Currency | null | undefined { const nativeCurrency = useNativeCurrency() - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() const isNative = Boolean(nativeCurrency && currencyId?.toUpperCase() === 'ETH') const shorthandMatchAddress = useMemo(() => { const chain = supportedChainId(chainId) diff --git a/src/hooks/useCurrencyBalance.ts b/src/hooks/useCurrencyBalance.ts index a0aa32a28..c06b8a2ce 100644 --- a/src/hooks/useCurrencyBalance.ts +++ b/src/hooks/useCurrencyBalance.ts @@ -1,9 +1,9 @@ import { Interface } from '@ethersproject/abi' import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' import ERC20ABI from 'abis/erc20.json' import { Erc20Interface } from 'abis/types/Erc20' import { nativeOnChain } from 'constants/tokens' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useMultipleContractSingleData, useSingleContractMultipleData } from 'hooks/multicall' import { useInterfaceMulticall } from 'hooks/useContract' import JSBI from 'jsbi' @@ -16,7 +16,7 @@ import { isAddress } from 'utils' export function useNativeCurrencyBalances(uncheckedAddresses?: (string | undefined)[]): { [address: string]: CurrencyAmount | undefined } { - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() const multicallContract = useInterfaceMulticall() const validAddressInputs: [string][] = useMemo( diff --git a/src/hooks/useERC20Permit.ts b/src/hooks/useERC20Permit.ts index c25ac1d9e..459c127f8 100644 --- a/src/hooks/useERC20Permit.ts +++ b/src/hooks/useERC20Permit.ts @@ -4,9 +4,9 @@ import { Trade } from '@uniswap/router-sdk' import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core' import { Trade as V2Trade } from '@uniswap/v2-sdk' import { Trade as V3Trade } from '@uniswap/v3-sdk' +import { useWeb3React } from '@web3-react/core' import { SWAP_ROUTER_ADDRESSES, V3_ROUTER_ADDRESS } from 'constants/addresses' import { DAI, UNI, USDC_MAINNET } from 'constants/tokens' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useSingleCallResult } from 'hooks/multicall' import JSBI from 'jsbi' import { useMemo, useState } from 'react' @@ -126,7 +126,7 @@ export function useERC20Permit( state: UseERC20PermitState gatherPermitSignature: null | (() => Promise) } { - const { account, chainId, library } = useActiveWeb3React() + const { account, chainId, provider } = useWeb3React() const tokenAddress = currencyAmount?.currency?.isToken ? currencyAmount.currency.address : undefined const eip2612Contract = useEIP2612Contract(tokenAddress) const isArgentWallet = useIsArgentWallet() @@ -145,7 +145,7 @@ export function useERC20Permit( !account || !chainId || !transactionDeadline || - !library || + !provider || !tokenNonceState.valid || !tokenAddress || !spender || @@ -221,7 +221,7 @@ export function useERC20Permit( message, }) - return library + return provider .send('eth_signTypedData_v4', [account, data]) .then(splitSignature) .then((signature) => { @@ -248,7 +248,7 @@ export function useERC20Permit( chainId, isArgentWallet, transactionDeadline, - library, + provider, tokenNonceState.loading, tokenNonceState.valid, tokenNonceState.result, @@ -268,7 +268,7 @@ export function useERC20PermitFromTrade( allowedSlippage: Percent, transactionDeadline: BigNumber | undefined ) { - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() const swapRouterAddress = chainId ? // v2 router does not support trade instanceof V2Trade diff --git a/src/hooks/useIsArgentWallet.ts b/src/hooks/useIsArgentWallet.ts index 86bcbf735..6a12a7ead 100644 --- a/src/hooks/useIsArgentWallet.ts +++ b/src/hooks/useIsArgentWallet.ts @@ -1,11 +1,11 @@ -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' +import { useWeb3React } from '@web3-react/core' import { NEVER_RELOAD, useSingleCallResult } from 'hooks/multicall' import { useMemo } from 'react' import { useArgentWalletDetectorContract } from './useContract' export default function useIsArgentWallet(): boolean { - const { account } = useActiveWeb3React() + const { account } = useWeb3React() const argentWalletDetector = useArgentWalletDetectorContract() const inputs = useMemo(() => [account ?? undefined], [account]) const call = useSingleCallResult(argentWalletDetector, 'isArgentWallet', inputs, NEVER_RELOAD) diff --git a/src/hooks/useIsValidBlock.ts b/src/hooks/useIsValidBlock.ts index 51ed02962..589b2cd70 100644 --- a/src/hooks/useIsValidBlock.ts +++ b/src/hooks/useIsValidBlock.ts @@ -1,8 +1,8 @@ +import { useWeb3React } from '@web3-react/core' import { atomWithImmer } from 'jotai/immer' import { useAtomValue, useUpdateAtom } from 'jotai/utils' import { useCallback } from 'react' -import useActiveWeb3React from './connectWeb3/useActiveWeb3React' import useBlockNumber from './useBlockNumber' // The oldest block (per chain) to be considered valid. @@ -11,7 +11,7 @@ const oldestBlockMapAtom = atomWithImmer<{ [chainId: number]: number }>({}) const DEFAULT_MAX_BLOCK_AGE = 10 export function useSetOldestValidBlock(): (block: number) => void { - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() const updateValidBlock = useUpdateAtom(oldestBlockMapAtom) return useCallback( (block: number) => { @@ -25,7 +25,7 @@ export function useSetOldestValidBlock(): (block: number) => void { } export function useGetIsValidBlock(maxBlockAge = DEFAULT_MAX_BLOCK_AGE): (block: number) => boolean { - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() const currentBlock = useBlockNumber() const oldestBlockMap = useAtomValue(oldestBlockMapAtom) const oldestBlock = chainId ? oldestBlockMap[chainId] : 0 diff --git a/src/hooks/useNativeCurrency.ts b/src/hooks/useNativeCurrency.ts index c649e8781..29c8eb09a 100644 --- a/src/hooks/useNativeCurrency.ts +++ b/src/hooks/useNativeCurrency.ts @@ -1,11 +1,11 @@ import { NativeCurrency } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' import { SupportedChainId } from 'constants/chains' import { nativeOnChain } from 'constants/tokens' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useMemo } from 'react' export default function useNativeCurrency(): NativeCurrency { - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() return useMemo( () => chainId diff --git a/src/hooks/useOnSupportedNetwork.ts b/src/hooks/useOnSupportedNetwork.ts index b3addb2d0..aeec47377 100644 --- a/src/hooks/useOnSupportedNetwork.ts +++ b/src/hooks/useOnSupportedNetwork.ts @@ -1,10 +1,9 @@ +import { useWeb3React } from '@web3-react/core' import { ALL_SUPPORTED_CHAIN_IDS } from 'constants/chains' import { useMemo } from 'react' -import useActiveWeb3React from './connectWeb3/useActiveWeb3React' - function useOnSupportedNetwork() { - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() return useMemo(() => Boolean(chainId && ALL_SUPPORTED_CHAIN_IDS.includes(chainId)), [chainId]) } diff --git a/src/hooks/usePools.ts b/src/hooks/usePools.ts index 0ee604739..c3e4f78f3 100644 --- a/src/hooks/usePools.ts +++ b/src/hooks/usePools.ts @@ -3,8 +3,8 @@ import { BigintIsh, Currency, Token } from '@uniswap/sdk-core' import { abi as IUniswapV3PoolStateABI } from '@uniswap/v3-core/artifacts/contracts/interfaces/pool/IUniswapV3PoolState.sol/IUniswapV3PoolState.json' import { computePoolAddress } from '@uniswap/v3-sdk' import { FeeAmount, Pool } from '@uniswap/v3-sdk' +import { useWeb3React } from '@web3-react/core' import { V3_CORE_FACTORY_ADDRESSES } from 'constants/addresses' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useMultipleContractSingleData } from 'hooks/multicall' import JSBI from 'jsbi' import { useMemo } from 'react' @@ -85,7 +85,7 @@ export enum PoolState { export function usePools( poolKeys: [Currency | undefined, Currency | undefined, FeeAmount | undefined][] ): [PoolState, Pool | null][] { - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() const poolTokens: ([Token, Token, FeeAmount] | undefined)[] = useMemo(() => { if (!chainId) return new Array(poolKeys.length) diff --git a/src/hooks/useSwapCallArguments.tsx b/src/hooks/useSwapCallArguments.tsx index 7f8710790..ce51bb05e 100644 --- a/src/hooks/useSwapCallArguments.tsx +++ b/src/hooks/useSwapCallArguments.tsx @@ -3,8 +3,8 @@ import { SwapRouter, Trade } from '@uniswap/router-sdk' import { Currency, Percent, TradeType } from '@uniswap/sdk-core' import { Router as V2SwapRouter, Trade as V2Trade } from '@uniswap/v2-sdk' import { FeeOptions, SwapRouter as V3SwapRouter, Trade as V3Trade } from '@uniswap/v3-sdk' +import { useWeb3React } from '@web3-react/core' import { SWAP_ROUTER_ADDRESSES, V3_ROUTER_ADDRESS } from 'constants/addresses' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useMemo } from 'react' import approveAmountCalldata from 'utils/approveAmountCalldata' @@ -39,7 +39,7 @@ export function useSwapCallArguments( deadline: BigNumber | undefined, feeOptions: FeeOptions | undefined ): SwapCall[] { - const { account, chainId, library } = useActiveWeb3React() + const { account, chainId, provider } = useWeb3React() const { address: recipientAddress } = useENS(recipientAddressOrName) const recipient = recipientAddressOrName === null ? account : recipientAddress @@ -47,7 +47,7 @@ export function useSwapCallArguments( const argentWalletContract = useArgentWalletContract() return useMemo(() => { - if (!trade || !recipient || !library || !account || !chainId || !deadline) return [] + if (!trade || !recipient || !provider || !account || !chainId || !deadline) return [] if (trade instanceof V2Trade) { if (!routerContract) return [] @@ -175,7 +175,7 @@ export function useSwapCallArguments( chainId, deadline, feeOptions, - library, + provider, recipient, routerContract, signatureData, diff --git a/src/hooks/useTokenList/index.tsx b/src/hooks/useTokenList/index.tsx index 468bd4dd8..deb654885 100644 --- a/src/hooks/useTokenList/index.tsx +++ b/src/hooks/useTokenList/index.tsx @@ -1,6 +1,6 @@ import { Token } from '@uniswap/sdk-core' import { TokenInfo, TokenList } from '@uniswap/token-lists' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' +import { useWeb3React } from '@web3-react/core' import { createContext, PropsWithChildren, useCallback, useContext, useEffect, useMemo, useState } from 'react' import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo' import resolveENSContentHash from 'utils/resolveENSContentHash' @@ -29,7 +29,7 @@ export function useIsTokenListLoaded() { } export default function useTokenList(): WrappedTokenInfo[] { - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() const chainTokenMap = useChainTokenMapContext() const tokenMap = chainId && chainTokenMap?.[chainId] return useMemo(() => { @@ -41,7 +41,7 @@ export default function useTokenList(): WrappedTokenInfo[] { export type TokenMap = { [address: string]: Token } export function useTokenMap(): TokenMap { - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() const chainTokenMap = useChainTokenMapContext() const tokenMap = chainId && chainTokenMap?.[chainId] return useMemo(() => { @@ -65,15 +65,15 @@ export function TokenListProvider({ useEffect(() => setChainTokenMap(undefined), [list]) - const { chainId, library } = useActiveWeb3React() + const { chainId, provider } = useWeb3React() const resolver = useCallback( (ensName: string) => { - if (library && chainId === 1) { - return resolveENSContentHash(ensName, library) + if (provider && chainId === 1) { + return resolveENSContentHash(ensName, provider) } throw new Error('Could not construct mainnet ENS resolver') }, - [chainId, library] + [chainId, provider] ) useEffect(() => { diff --git a/src/hooks/useTokenList/useQueryTokens.ts b/src/hooks/useTokenList/useQueryTokens.ts index 509fda8f5..277261f11 100644 --- a/src/hooks/useTokenList/useQueryTokens.ts +++ b/src/hooks/useTokenList/useQueryTokens.ts @@ -1,5 +1,5 @@ +import { useWeb3React } from '@web3-react/core' import { nativeOnChain } from 'constants/tokens' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useTokenBalances } from 'hooks/useCurrencyBalance' import useDebounce from 'hooks/useDebounce' import { useMemo } from 'react' @@ -9,7 +9,7 @@ import { getTokenFilter } from './filtering' import { tokenComparator, useSortTokensByQuery } from './sorting' export function useQueryTokens(query: string, tokens: WrappedTokenInfo[]) { - const { chainId, account } = useActiveWeb3React() + const { chainId, account } = useWeb3React() const balances = useTokenBalances(account, tokens) const sortedTokens = useMemo( // Create a new array because sort is in-place and returns a referentially equivalent array. diff --git a/src/hooks/useTransactionDeadline.ts b/src/hooks/useTransactionDeadline.ts index b58ce5d68..ee05d4376 100644 --- a/src/hooks/useTransactionDeadline.ts +++ b/src/hooks/useTransactionDeadline.ts @@ -1,4 +1,5 @@ import { BigNumber } from '@ethersproject/bignumber' +import { useWeb3React } from '@web3-react/core' import { L2_CHAIN_IDS } from 'constants/chains' import { DEFAULT_DEADLINE_FROM_NOW, L2_DEADLINE_FROM_NOW } from 'constants/misc' import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp' @@ -6,11 +7,9 @@ import { useAtom } from 'jotai' import { useMemo } from 'react' import { transactionTtlAtom } from 'state/settings' -import useActiveWeb3React from './connectWeb3/useActiveWeb3React' - /** Returns the default transaction TTL for the chain, in minutes. */ export function useDefaultTransactionTtl(): number { - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() if (chainId && L2_CHAIN_IDS.includes(chainId)) return L2_DEADLINE_FROM_NOW / 60 return DEFAULT_DEADLINE_FROM_NOW / 60 } diff --git a/src/hooks/useUSDCPrice.ts b/src/hooks/useUSDCPrice.ts index a3f122d30..61d98d94a 100644 --- a/src/hooks/useUSDCPrice.ts +++ b/src/hooks/useUSDCPrice.ts @@ -1,7 +1,7 @@ import { Currency, CurrencyAmount, Price, Token, TradeType } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' import { SupportedChainId } from 'constants/chains' import { DAI_OPTIMISM, USDC_ARBITRUM, USDC_MAINNET, USDC_POLYGON } from 'constants/tokens' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useMemo, useRef } from 'react' import tryParseCurrencyAmount from 'utils/tryParseCurrencyAmount' @@ -80,7 +80,7 @@ export function useUSDCValue(currencyAmount: CurrencyAmount | undefine * @returns CurrencyAmount where currency is stablecoin on active chain */ export function useStablecoinAmountFromFiatValue(fiatValue: string | null | undefined) { - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() const stablecoin = chainId ? STABLECOIN_AMOUNT_OUT[chainId]?.currency : undefined return useMemo(() => { diff --git a/src/hooks/useV3SwapPools.ts b/src/hooks/useV3SwapPools.ts index c0d5eccde..c7c13ff14 100644 --- a/src/hooks/useV3SwapPools.ts +++ b/src/hooks/useV3SwapPools.ts @@ -1,7 +1,7 @@ import { Currency, Token } from '@uniswap/sdk-core' import { FeeAmount, Pool } from '@uniswap/v3-sdk' +import { useWeb3React } from '@web3-react/core' import { SupportedChainId } from 'constants/chains' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useMemo } from 'react' import { useAllCurrencyCombinations } from './useAllCurrencyCombinations' @@ -19,7 +19,7 @@ export function useV3SwapPools( pools: Pool[] loading: boolean } { - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() const allCurrencyCombinations = useAllCurrencyCombinations(currencyIn, currencyOut) diff --git a/src/icons/identicon.tsx b/src/icons/identicon.tsx index 4ae982924..1975022a4 100644 --- a/src/icons/identicon.tsx +++ b/src/icons/identicon.tsx @@ -1,3 +1,4 @@ +import { useWeb3React } from '@web3-react/core' import IdenticonGradient0 from 'assets/images/identicons/IdenticonGradient-0.png' import IdenticonGradient1 from 'assets/images/identicons/IdenticonGradient-1.png' import IdenticonGradient2 from 'assets/images/identicons/IdenticonGradient-2.png' @@ -8,7 +9,6 @@ import IdenticonGradient6 from 'assets/images/identicons/IdenticonGradient-6.png import IdenticonGradient7 from 'assets/images/identicons/IdenticonGradient-7.png' import IdenticonGradient8 from 'assets/images/identicons/IdenticonGradient-8.png' import IdenticonGradient9 from 'assets/images/identicons/IdenticonGradient-9.png' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' import { useMemo } from 'react' const gradients = [ @@ -31,7 +31,7 @@ function getGradientIconSrc(account: string) { } export default function IdenticonIcon() { - const { account } = useActiveWeb3React() + const { account } = useWeb3React() const iconSrc = useMemo(() => account && getGradientIconSrc(account), [account]) return account icon diff --git a/src/index.tsx b/src/index.tsx index b7585d1e3..6d7acc495 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,3 +1,5 @@ +import 'polyfills' + import Swap, { SwapProps } from 'components/Swap' import Widget, { WidgetProps } from 'components/Widget' export type { Provider as EthersProvider } from '@ethersproject/abstract-provider' diff --git a/src/polyfills.ts b/src/polyfills.ts new file mode 100644 index 000000000..25d9a8d80 --- /dev/null +++ b/src/polyfills.ts @@ -0,0 +1,6 @@ +import { Buffer } from 'buffer' + +// WalletConnect relies on Buffer, so it must be polyfilled. +if (!('Buffer' in window)) { + window.Buffer = Buffer +} diff --git a/src/state/multicall.tsx b/src/state/multicall.tsx index bc9e9e91b..dd5cec94c 100644 --- a/src/state/multicall.tsx +++ b/src/state/multicall.tsx @@ -1,5 +1,5 @@ import { createMulticall } from '@uniswap/redux-multicall' -import useActiveWeb3React from 'hooks/connectWeb3/useActiveWeb3React' +import { useWeb3React } from '@web3-react/core' import useBlockNumber from 'hooks/useBlockNumber' import { useInterfaceMulticall } from 'hooks/useContract' import { combineReducers, createStore } from 'redux' @@ -11,7 +11,7 @@ export const store = createStore(reducer) export default multicall export function MulticallUpdater() { - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() const latestBlockNumber = useBlockNumber() const contract = useInterfaceMulticall() return diff --git a/src/utils/index.ts b/src/utils/index.ts index 7d3e12025..04376858f 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -25,22 +25,22 @@ export function shortenAddress(address: string, chars = 4): string { } // account is not optional -function getSigner(library: JsonRpcProvider, account: string): JsonRpcSigner { - return library.getSigner(account).connectUnchecked() +function getSigner(provider: JsonRpcProvider, account: string): JsonRpcSigner { + return provider.getSigner(account).connectUnchecked() } // account is optional -function getProviderOrSigner(library: JsonRpcProvider, account?: string): JsonRpcProvider | JsonRpcSigner { - return account ? getSigner(library, account) : library +function getProviderOrSigner(provider: JsonRpcProvider, account?: string): JsonRpcProvider | JsonRpcSigner { + return account ? getSigner(provider, account) : provider } // account is optional -export function getContract(address: string, ABI: any, library: JsonRpcProvider, account?: string): Contract { +export function getContract(address: string, ABI: any, provider: JsonRpcProvider, account?: string): Contract { if (!isAddress(address) || address === AddressZero) { throw Error(`Invalid 'address' parameter '${address}'.`) } - return new Contract(address, ABI, getProviderOrSigner(library, account) as any) + return new Contract(address, ABI, getProviderOrSigner(provider, account) as any) } export function escapeRegExp(string: string): string { diff --git a/yarn.lock b/yarn.lock index 65b8f9585..99fa689b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4084,6 +4084,14 @@ "@metamask/detect-provider" "^1.2.0" "@web3-react/types" "^8.0.19-beta.0" +"@web3-react/network@8.0.26-beta.0": + version "8.0.26-beta.0" + resolved "https://registry.yarnpkg.com/@web3-react/network/-/network-8.0.26-beta.0.tgz#43582d7652ae3a3785df0d3c3e582b03e6672e72" + integrity sha512-8VSYbWYHpGvlJOSXgUduSp+UDUaUU1tx+/9ivMb96TOgeyUqmkbERJzzPiaE3d3Ehe+7y/uvu0+v20tQqlx8Uw== + dependencies: + "@ethersproject/providers" "^5" + "@web3-react/types" "^8.0.19-beta.0" + "@web3-react/store@^8.0.24-beta.0": version "8.0.24-beta.0" resolved "https://registry.yarnpkg.com/@web3-react/store/-/store-8.0.24-beta.0.tgz#fb7e25d8478fce8ae95e6f50d7310f97876dc591" From 72c5106280a991e68602df7d75b385117f0381b9 Mon Sep 17 00:00:00 2001 From: Kristie Huang Date: Tue, 5 Jul 2022 16:01:02 -0400 Subject: [PATCH 06/44] feat: add fallback mainnet JSON RPC endpoint (#43) * feat: add fallback mainnet JSON RPC endpoint * Add better chainId support * Clean up code * nit: remove console .log * PR review updates * Remove console logs --- package.json | 8 +- src/components/Widget.tsx | 9 +- src/constants/jsonRpcEndpoints.ts | 9 + src/hooks/connectWeb3/useActiveWeb3React.tsx | 200 ------------------- src/hooks/connectWeb3/useWeb3React.tsx | 25 ++- 5 files changed, 37 insertions(+), 214 deletions(-) create mode 100644 src/constants/jsonRpcEndpoints.ts delete mode 100644 src/hooks/connectWeb3/useActiveWeb3React.tsx diff --git a/package.json b/package.json index 1b32132e2..701ce6fda 100644 --- a/package.json +++ b/package.json @@ -67,15 +67,11 @@ "@uniswap/token-lists": "^1.0.0-beta.27", "@uniswap/v2-sdk": "^3.0.1", "@uniswap/v3-sdk": "^3.8.2", - "@walletconnect/ethereum-provider": "^1.7.1", "@web3-react/core": "8.0.33-beta.0", "@web3-react/eip1193": "8.0.25-beta.0", "@web3-react/empty": "8.0.19-beta.0", - "@web3-react/metamask": "8.0.26-beta.0", - "@web3-react/network": "8.0.26-beta.0", "@web3-react/types": "8.0.19-beta.0", "@web3-react/url": "8.0.24-beta.0", - "@web3-react/walletconnect": "8.0.34-beta.0", "ajv": "^6.12.3", "cids": "^1.0.0", "ethers": "^5.1.4", @@ -159,6 +155,10 @@ "@uniswap/v2-periphery": "^1.1.0-beta.0", "@uniswap/v3-core": "1.0.0", "@uniswap/v3-periphery": "^1.1.1", + "@walletconnect/ethereum-provider": "^1.7.1", + "@web3-react/metamask": "8.0.26-beta.0", + "@web3-react/network": "8.0.26-beta.0", + "@web3-react/walletconnect": "8.0.34-beta.0", "babel-jest": "^27.5.1", "babel-loader": "^8.2.5", "babel-plugin-macros": "^3.1.0", diff --git a/src/components/Widget.tsx b/src/components/Widget.tsx index 3b98f945d..bc5cef74f 100644 --- a/src/components/Widget.tsx +++ b/src/components/Widget.tsx @@ -1,6 +1,8 @@ import { JsonRpcProvider } from '@ethersproject/providers' import { TokenInfo } from '@uniswap/token-lists' import { Provider as Eip1193Provider } from '@web3-react/types' +import { SupportedChainId } from 'constants/chains' +import { JSON_RPC_FALLBACK_ENDPOINTS } from 'constants/jsonRpcEndpoints' import { DEFAULT_LOCALE, SUPPORTED_LOCALES, SupportedLocale } from 'constants/locales' import { ActiveWeb3Provider } from 'hooks/connectWeb3/useWeb3React' import { TransactionsUpdater } from 'hooks/transactions' @@ -87,6 +89,7 @@ export type WidgetProps = { theme?: Theme locale?: SupportedLocale provider?: Eip1193Provider | JsonRpcProvider + // TODO(kristiehuang): allow integrator to pass in {chainId: [rpcUrls]} for multichain jsonRpcEndpoints jsonRpcEndpoint?: string | JsonRpcProvider tokenList?: string | TokenInfo[] width?: string | number @@ -96,7 +99,7 @@ export type WidgetProps = { } export default function Widget(props: PropsWithChildren) { - const { children, theme, jsonRpcEndpoint, provider, dialog: userDialog, className, onError } = props + const { children, theme, provider, 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.`) @@ -111,6 +114,10 @@ export default function Widget(props: PropsWithChildren) { } return props.locale ?? DEFAULT_LOCALE }, [props.locale]) + const jsonRpcEndpoint = useMemo( + () => props.jsonRpcEndpoint ?? JSON_RPC_FALLBACK_ENDPOINTS[SupportedChainId.MAINNET]?.[0], + [props.jsonRpcEndpoint] + ) const [dialog, setDialog] = useState(null) return ( diff --git a/src/constants/jsonRpcEndpoints.ts b/src/constants/jsonRpcEndpoints.ts new file mode 100644 index 000000000..06b31888e --- /dev/null +++ b/src/constants/jsonRpcEndpoints.ts @@ -0,0 +1,9 @@ +import { SupportedChainId } from './chains' + +/** + * Fallback JSON RPC endpoints if integrator does not provide one + */ +export const JSON_RPC_FALLBACK_ENDPOINTS: { [key in SupportedChainId]?: string[] } = { + [SupportedChainId.MAINNET]: ['https://cloudflare-eth.com/v1/mainnet'], + [SupportedChainId.RINKEBY]: ['https://cloudflare-eth.com/v1/rinkeby'], +} diff --git a/src/hooks/connectWeb3/useActiveWeb3React.tsx b/src/hooks/connectWeb3/useActiveWeb3React.tsx deleted file mode 100644 index eeecee826..000000000 --- a/src/hooks/connectWeb3/useActiveWeb3React.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import { ExternalProvider, JsonRpcProvider, Web3Provider } from '@ethersproject/providers' -import { getPriorityConnector, initializeConnector, Web3ReactHooks } from '@web3-react/core' -import { EIP1193 } from '@web3-react/eip1193' -import { EMPTY } from '@web3-react/empty' -import { MetaMask } from '@web3-react/metamask' -import { Connector, Provider as Eip1193Provider, Web3ReactStore } from '@web3-react/types' -import { Url } from '@web3-react/url' -import { WalletConnect } from '@web3-react/walletconnect' -import { Buffer } from 'buffer' -import { createContext, PropsWithChildren, useContext, useMemo } from 'react' -import { useCallback } from 'react' -import JsonRpcConnector from 'utils/JsonRpcConnector' - -export type Web3ContextType = { - connector: Connector - library?: (JsonRpcProvider & { provider?: ExternalProvider }) | Web3Provider - chainId?: ReturnType - accounts?: ReturnType - account?: ReturnType - active?: ReturnType - activating?: ReturnType -} - -const [EMPTY_CONNECTOR, EMPTY_HOOKS] = initializeConnector(() => EMPTY) -const EMPTY_STATE = { connector: EMPTY_CONNECTOR, hooks: EMPTY_HOOKS } -const EMPTY_WEB3: Web3ContextType = { connector: EMPTY } -const EMPTY_CONTEXT = { web3: EMPTY_WEB3, updateWeb3: (updateContext: Web3ContextType) => console.log(updateContext) } -const Web3Context = createContext(EMPTY_CONTEXT) - -export default function useActiveWeb3React() { - const { web3 } = useContext(Web3Context) - return web3 -} - -function useUpdateActiveWeb3ReactCallback() { - const { updateWeb3 } = useContext(Web3Context) - return updateWeb3 -} - -function getNetwork(jsonRpcEndpoint?: string | JsonRpcProvider) { - if (jsonRpcEndpoint) { - let connector, hooks - if (JsonRpcProvider.isProvider(jsonRpcEndpoint)) { - ;[connector, hooks] = initializeConnector((actions) => new JsonRpcConnector(actions, jsonRpcEndpoint)) - } else { - ;[connector, hooks] = initializeConnector((actions) => new Url({ actions, url: jsonRpcEndpoint })) - } - connector.activate() - return { connector, hooks } - } - return EMPTY_STATE -} - -function getWallet(provider?: JsonRpcProvider | Eip1193Provider) { - if (provider) { - let connector, hooks - if (JsonRpcProvider.isProvider(provider)) { - ;[connector, hooks] = initializeConnector((actions) => new JsonRpcConnector(actions, provider)) - } else if (JsonRpcProvider.isProvider((provider as any).provider)) { - throw new Error('Eip1193Bridge is experimental: pass your ethers Provider directly') - } else { - ;[connector, hooks] = initializeConnector((actions) => new EIP1193({ actions, provider })) - } - connector.activate() - return { connector, hooks } - } - return EMPTY_STATE -} - -export let connections: [Connector, Web3ReactHooks][] = [] - -interface ActiveWeb3ProviderProps { - jsonRpcEndpoint?: string | JsonRpcProvider - provider?: Eip1193Provider | JsonRpcProvider -} - -export function ActiveWeb3Provider({ - jsonRpcEndpoint, - provider: propsProvider, - children, -}: PropsWithChildren) { - const metaMaskConnection = useMemo( - () => toWeb3Connection(initializeConnector((actions) => new MetaMask({ actions }))), - [] - ) - const walletConnectConnectionQR = useMemo(() => getWalletConnectConnection(false, jsonRpcEndpoint), [jsonRpcEndpoint]) // WC via tile QR code scan - const walletConnectConnectionPopup = useMemo( - () => getWalletConnectConnection(true, jsonRpcEndpoint), - [jsonRpcEndpoint] - ) // WC via built-in popup - connections = [metaMaskConnection, walletConnectConnectionQR, walletConnectConnectionPopup] - - const network = useMemo(() => getNetwork(jsonRpcEndpoint), [jsonRpcEndpoint]) - const activeProvider = useActiveWalletProvider() - const wallet = useMemo(() => getWallet(propsProvider ?? activeProvider), [propsProvider, activeProvider]) - - // eslint-disable-next-line prefer-const - let { connector, hooks } = wallet !== EMPTY_STATE ? wallet : network - let accounts = hooks.useAccounts() - let account = hooks.useAccount() - let activating = hooks.useIsActivating() - let active = hooks.useIsActive() - let chainId = hooks.useChainId() - let library = hooks.useProvider() - - const web3 = useMemo(() => { - if (connector === EMPTY || !(active || activating)) { - return EMPTY_WEB3 - } - return { connector, library, chainId, accounts, account, active, activating } - }, [account, accounts, activating, active, chainId, connector, library]) - - const updateWeb3 = (updateContext: Web3ContextType) => { - connector = updateContext.connector - accounts = updateContext.accounts - account = updateContext.account - activating = updateContext.activating ?? false - active = updateContext.active ?? false - chainId = updateContext.chainId - library = updateContext.library as Web3Provider - } - - return {children} -} - -export type Web3Connection = [Connector, Web3ReactHooks] - -function toWeb3Connection([connector, hooks]: [T, Web3ReactHooks, Web3ReactStore]): [ - T, - Web3ReactHooks -] { - return [connector, hooks] -} - -function getWalletConnectConnection(useDefault: boolean, jsonRpcEndpoint?: string | JsonRpcProvider) { - // TODO(kristiehuang): implement RPC URL fallback, then jsonRpcEndpoint can be optional - - // WalletConnect relies on Buffer, so it must be polyfilled. - if (!('Buffer' in window)) { - window.Buffer = Buffer - } - - let rpcUrl: string - if (JsonRpcProvider.isProvider(jsonRpcEndpoint)) { - rpcUrl = jsonRpcEndpoint.connection.url - } else { - rpcUrl = jsonRpcEndpoint ?? '' // TODO(kristiehuang): use fallback RPC URL - } - - return toWeb3Connection( - initializeConnector( - (actions) => - new WalletConnect({ - actions, - options: { - rpc: { - 1: [rpcUrl].filter((url) => url !== undefined && url !== ''), - }, - qrcode: useDefault, - }, // TODO(kristiehuang): WC only works on network chainid 1? - }) - ) - ) -} - -function useActiveWalletProvider(): Web3Provider | undefined { - return getPriorityConnector(...connections).usePriorityProvider() as Web3Provider -} - -export function useConnectCallback(connection: Web3Connection) { - const [wallet, hooks] = connection - const isActive = hooks.useIsActive() - const accounts = hooks.useAccounts() - const account = hooks.useAccount() - const activating = hooks.useIsActivating() - const chainId = hooks.useChainId() - const library = hooks.useProvider() - - const updateActiveWeb3ReactCallback = useUpdateActiveWeb3ReactCallback() - - const activateWallet = useCallback(() => { - if (!isActive) { - wallet.activate() - } else { - // wallet should be already be active - const updateContext: Web3ContextType = { - connector: wallet, - library, - accounts, - account, - activating, - active: isActive, - chainId, - } - updateActiveWeb3ReactCallback(updateContext) - } - }, [account, accounts, activating, chainId, isActive, library, updateActiveWeb3ReactCallback, wallet]) - - return activateWallet -} diff --git a/src/hooks/connectWeb3/useWeb3React.tsx b/src/hooks/connectWeb3/useWeb3React.tsx index 05f74fa7f..f62bde927 100644 --- a/src/hooks/connectWeb3/useWeb3React.tsx +++ b/src/hooks/connectWeb3/useWeb3React.tsx @@ -7,6 +7,7 @@ import { Connector, Provider as Eip1193Provider, Web3ReactStore } from '@web3-re import { Url } from '@web3-react/url' import { WalletConnect } from '@web3-react/walletconnect' import { SupportedChainId } from 'constants/chains' +import { JSON_RPC_FALLBACK_ENDPOINTS } from 'constants/jsonRpcEndpoints' import { PropsWithChildren, useMemo } from 'react' export let connections: [Connector, Web3ReactHooks][] = [] @@ -31,16 +32,18 @@ function getWallet(provider?: JsonRpcProvider | Eip1193Provider) { } function getWalletConnectConnection(useDefault: boolean, jsonRpcEndpoint?: string | JsonRpcProvider) { - // TODO(kristiehuang): implement RPC URL fallback, then jsonRpcEndpoint can be optional - let rpcUrl: string + // WalletConnect relies on Buffer, so it must be polyfilled. + if (!('Buffer' in window)) { + window.Buffer = Buffer + } + + // FIXME(kristiehuang): we don't know what the props.jsonRpcEndpoint chain is; assume mainnet for WC instantiation + let mainnetRpcUrl: string | undefined if (JsonRpcProvider.isProvider(jsonRpcEndpoint)) { - rpcUrl = jsonRpcEndpoint.connection.url + mainnetRpcUrl = jsonRpcEndpoint.connection.url } else { - // TODO(kristiehuang): temporarily needed to instantiate WC if integrator doesn't provide RPC - // replace logic when we add in fallback JSON RPC URL - rpcUrl = jsonRpcEndpoint ?? 'https://cloudflare-eth.com' + mainnetRpcUrl = jsonRpcEndpoint } - return toWeb3Connection( initializeConnector( (actions) => @@ -48,10 +51,14 @@ function getWalletConnectConnection(useDefault: boolean, jsonRpcEndpoint?: strin actions, options: { rpc: { - 1: [rpcUrl].filter((url) => url !== undefined && url !== ''), + [SupportedChainId.MAINNET]: [ + mainnetRpcUrl ?? '', + ...(JSON_RPC_FALLBACK_ENDPOINTS[SupportedChainId.MAINNET] ?? []), + ].filter((url) => url !== undefined && url !== ''), + [SupportedChainId.RINKEBY]: JSON_RPC_FALLBACK_ENDPOINTS[SupportedChainId.RINKEBY] ?? [], }, qrcode: useDefault, - }, // TODO(kristiehuang): WC only works on network chainid 1? + }, }) ) ) From e95f07bb98bbad38c928466b5693faf7b6a260c6 Mon Sep 17 00:00:00 2001 From: Kristie Huang Date: Fri, 8 Jul 2022 18:26:27 -0400 Subject: [PATCH 07/44] fix: fix integrator provider accounts.every error --- .../ConnectWallet/ConnectWalletDialog.tsx | 13 ++++++++++++- src/hooks/connectWeb3/useWeb3React.tsx | 6 +++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/components/ConnectWallet/ConnectWalletDialog.tsx b/src/components/ConnectWallet/ConnectWalletDialog.tsx index a4b85fec7..92d33084d 100644 --- a/src/components/ConnectWallet/ConnectWalletDialog.tsx +++ b/src/components/ConnectWallet/ConnectWalletDialog.tsx @@ -1,5 +1,8 @@ import { Trans } from '@lingui/macro' import { Web3ReactHooks } from '@web3-react/core' +import { EIP1193 } from '@web3-react/eip1193' +import { Connector } from '@web3-react/types' +import { Url } from '@web3-react/url' import { URI_AVAILABLE, WalletConnect } from '@web3-react/walletconnect' import METAMASK_ICON_URL from 'assets/images/metamaskIcon.png' import WALLETCONNECT_ICON_URL from 'assets/images/walletConnectIcon.svg' @@ -170,7 +173,15 @@ function NoWalletButton() { } export function ConnectWalletDialog() { - const [mmConnection, wcTileConnection, wcPopupConnection] = connections + let defaultConnections: [Connector, Web3ReactHooks][] + const firstConnector: Connector = connections[0][0] + if (firstConnector instanceof EIP1193 || firstConnector instanceof Url) { + // If first connector is the integrator-provided connector + defaultConnections = connections.slice(1) + } else { + defaultConnections = connections + } + const [mmConnection, wcTileConnection, wcPopupConnection] = defaultConnections return ( <> diff --git a/src/hooks/connectWeb3/useWeb3React.tsx b/src/hooks/connectWeb3/useWeb3React.tsx index f62bde927..d66b4b7c0 100644 --- a/src/hooks/connectWeb3/useWeb3React.tsx +++ b/src/hooks/connectWeb3/useWeb3React.tsx @@ -96,5 +96,9 @@ export function ActiveWeb3Provider({ if (integratorConnection) connections = [integratorConnection, ...connections] if (networkConnection) connections.push(networkConnection) - return {children} + return ( + + {children} + + ) } From 7de77fffabcad59209350addf8921ce4e7c26f2a Mon Sep 17 00:00:00 2001 From: Kristie Huang Date: Fri, 8 Jul 2022 18:27:14 -0400 Subject: [PATCH 08/44] add flashbots RPC endpoint to fallbacks --- src/constants/jsonRpcEndpoints.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants/jsonRpcEndpoints.ts b/src/constants/jsonRpcEndpoints.ts index 06b31888e..b767f6f9f 100644 --- a/src/constants/jsonRpcEndpoints.ts +++ b/src/constants/jsonRpcEndpoints.ts @@ -4,6 +4,6 @@ import { SupportedChainId } from './chains' * Fallback JSON RPC endpoints if integrator does not provide one */ export const JSON_RPC_FALLBACK_ENDPOINTS: { [key in SupportedChainId]?: string[] } = { - [SupportedChainId.MAINNET]: ['https://cloudflare-eth.com/v1/mainnet'], + [SupportedChainId.MAINNET]: ['https://rpc.flashbots.net', 'https://cloudflare-eth.com'], [SupportedChainId.RINKEBY]: ['https://cloudflare-eth.com/v1/rinkeby'], } From 243f85b8f3d0720f014dc05a4d5538a776cf010c Mon Sep 17 00:00:00 2001 From: Kristie Huang Date: Fri, 8 Jul 2022 18:40:41 -0400 Subject: [PATCH 09/44] lint --- e2e/connect.test.tsx | 10 +++++----- src/components/ConnectWallet/ConnectWalletDialog.tsx | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/e2e/connect.test.tsx b/e2e/connect.test.tsx index d6c922e5a..2b8c4d385 100644 --- a/e2e/connect.test.tsx +++ b/e2e/connect.test.tsx @@ -5,7 +5,7 @@ import '@ethersproject/providers' import 'jest-environment-hardhat' -import { render, RenderResult, waitFor } from '@testing-library/react' +import { act, render, RenderResult, waitFor } from '@testing-library/react' import { tokens } from '@uniswap/default-token-list' import { SwapWidget } from '../src' @@ -28,11 +28,11 @@ describe('connect', () => { describe('with jsonRpcEndpoint', () => { it('prompts for wallet connection in the Wallet', async () => { - component.rerender() + act(() => component.rerender()) expect(wallet.hidden).toBeTruthy() expect(toolbar.textContent).toBe('Connecting…') - await waitFor(() => expect(toolbar.textContent).not.toBe('Connecting…')) + act(async () => await waitFor(() => expect(toolbar.textContent).not.toBe('Connecting…'))) expect(wallet.hidden).toBeFalsy() expect(wallet.textContent).toBe('Connect wallet to swap') expect(toolbar.textContent).toBe('Enter an amount') @@ -41,11 +41,11 @@ describe('connect', () => { describe('with provider', () => { it('does not prompt for wallet connection', async () => { - component.rerender() + act(() => component.rerender()) expect(wallet.hidden).toBeTruthy() expect(toolbar.textContent).toBe('Connecting…') - await waitFor(() => expect(toolbar.textContent).not.toBe('Connecting…')) + act(async () => await waitFor(() => expect(toolbar.textContent).not.toBe('Connecting…'))) expect(wallet.hidden).toBeTruthy() expect(toolbar.textContent).toBe('Enter an amount') }) diff --git a/src/components/ConnectWallet/ConnectWalletDialog.tsx b/src/components/ConnectWallet/ConnectWalletDialog.tsx index 92d33084d..d085642ba 100644 --- a/src/components/ConnectWallet/ConnectWalletDialog.tsx +++ b/src/components/ConnectWallet/ConnectWalletDialog.tsx @@ -166,7 +166,7 @@ function NoWalletButton() { return ( window.open(helpCenterUrl)}> - I don't have a wallet + I don't have a wallet ) From 12c874945a9d74d1d7cab792442ee74346ddfab3 Mon Sep 17 00:00:00 2001 From: Kristie Huang Date: Mon, 11 Jul 2022 11:58:45 -0400 Subject: [PATCH 10/44] fix: use undistorted QR code (#53) * fix: use safer QR image * Use svg, change width * nit * fix: check staleness before setting state * fix: color match qr code * nit Co-authored-by: Zach Pomerantz --- .../ConnectWallet/ConnectWalletDialog.tsx | 60 ++++++++++++------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/src/components/ConnectWallet/ConnectWalletDialog.tsx b/src/components/ConnectWallet/ConnectWalletDialog.tsx index d085642ba..2c115bf9d 100644 --- a/src/components/ConnectWallet/ConnectWalletDialog.tsx +++ b/src/components/ConnectWallet/ConnectWalletDialog.tsx @@ -16,7 +16,7 @@ import { atom, useAtom } from 'jotai' import QRCode from 'qrcode' import { useEffect, useState } from 'react' import styled from 'styled-components/macro' -import { ThemedText } from 'theme' +import { lightTheme, ThemedText } from 'theme' const Body = styled(Column)` height: calc(100% - 2.5em); @@ -57,6 +57,15 @@ const StyledNoWalletText = styled(ThemedText.Subhead1)` white-space: pre-wrap; ` +const QRCodeWrapper = styled.div` + height: 110px; + width: 110px; + path { + /* Maximize contrast: transparent in light theme, otherwise hard-coded to light theme. */ + fill: ${({ theme }) => (theme.container === lightTheme.container ? '#00000000' : lightTheme.container)}; + } +` + interface ButtonProps { walletName?: string logoSrc?: string @@ -66,20 +75,40 @@ interface ButtonProps { const wcQRUriAtom = atom(undefined) +function toQrCodeSvg(qrUri: string): Promise { + return QRCode.toString(qrUri, { + // Leave a margin to increase contrast in dark mode. + margin: 1, + // Use 55*2=110 for the width to prevent distortion. The generated viewbox is "0 0 55 55". + width: 110, + type: 'svg', + }) +} + function WalletConnectButton({ walletName, logoSrc, connection: wcTileConnection, onClick }: ButtonProps) { const [walletConnect] = wcTileConnection as [WalletConnect, Web3ReactHooks] const [error, setError] = useState(undefined) - const [QRUri, setQRUri] = useAtom(wcQRUriAtom) + const [qrUri, setQrUri] = useAtom(wcQRUriAtom) const [qrCodeSvg, setQrCodeSvg] = useState('') useEffect(() => { - if (QRUri) { - formatQrCodeImage(QRUri) + let stale = false + if (qrUri) { + toQrCodeSvg(qrUri).then((qrCodeSvg) => { + if (stale) return + setQrCodeSvg(qrCodeSvg) + }) } else { - walletConnect.activate().catch(setError) + walletConnect.activate().catch((e) => { + if (stale) return + setError(e) + }) } - }, [QRUri, walletConnect]) + return () => { + stale = true + } + }, [qrUri, walletConnect]) useEffect(() => { // Log web3 errors @@ -92,7 +121,7 @@ function WalletConnectButton({ walletName, logoSrc, connection: wcTileConnection const disconnectListener = async (err: Error | null, _: any) => { if (err) console.warn(err) // Clear saved QR URI after disconnection - setQRUri(undefined) + setQrUri(undefined) walletConnect.deactivate() } walletConnect.provider?.connector.on('disconnect', disconnectListener) @@ -100,8 +129,7 @@ function WalletConnectButton({ walletName, logoSrc, connection: wcTileConnection // Need both URI event listeners walletConnect.events.on(URI_AVAILABLE, async (uri: string) => { if (uri) { - setQRUri(uri) - await formatQrCodeImage(uri) + setQrUri(uri) } }) @@ -109,8 +137,7 @@ function WalletConnectButton({ walletName, logoSrc, connection: wcTileConnection if (err) console.warn(err) const uri: string = payload.params[0] if (uri) { - setQRUri(uri) - await formatQrCodeImage(uri) + setQrUri(uri) } } walletConnect.provider?.connector.on('display_uri', uriListener) @@ -121,15 +148,6 @@ function WalletConnectButton({ walletName, logoSrc, connection: wcTileConnection } }) - async function formatQrCodeImage(uri: string) { - let result = '' - const dataString = await QRCode.toString(uri, { margin: 0, type: 'svg' }) - if (typeof dataString === 'string') { - result = dataString.replace(' @@ -142,7 +160,7 @@ function WalletConnectButton({ walletName, logoSrc, connection: wcTileConnection Scan to connect your wallet. Works with most wallets. -
+
) From 77fcc2f681732160482498ce1f59671a0dc10d99 Mon Sep 17 00:00:00 2001 From: Kristie Huang Date: Tue, 12 Jul 2022 19:28:03 -0400 Subject: [PATCH 11/44] nit: show loading state if trade is loading --- src/components/Swap/Toolbar/index.tsx | 7 ++++--- src/constants/jsonRpcEndpoints.ts | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/Swap/Toolbar/index.tsx b/src/components/Swap/Toolbar/index.tsx index f9d58e796..136ad746a 100644 --- a/src/components/Swap/Toolbar/index.tsx +++ b/src/components/Swap/Toolbar/index.tsx @@ -28,6 +28,10 @@ export default memo(function Toolbar() { const isAmountPopulated = useIsAmountPopulated() const { type: wrapType } = useWrapCallback() const caption = useMemo(() => { + if (state === TradeState.SYNCING || state === TradeState.LOADING) { + return + } + if (!account || !chainId) { if (isActivating) return return @@ -38,9 +42,6 @@ export default memo(function Toolbar() { } if (inputCurrency && outputCurrency && isAmountPopulated) { - if (state === TradeState.SYNCING || state === TradeState.LOADING) { - return - } if (inputBalance && inputAmount?.greaterThan(inputBalance)) { return } diff --git a/src/constants/jsonRpcEndpoints.ts b/src/constants/jsonRpcEndpoints.ts index b767f6f9f..ef25a1215 100644 --- a/src/constants/jsonRpcEndpoints.ts +++ b/src/constants/jsonRpcEndpoints.ts @@ -4,6 +4,6 @@ import { SupportedChainId } from './chains' * Fallback JSON RPC endpoints if integrator does not provide one */ export const JSON_RPC_FALLBACK_ENDPOINTS: { [key in SupportedChainId]?: string[] } = { - [SupportedChainId.MAINNET]: ['https://rpc.flashbots.net', 'https://cloudflare-eth.com'], + [SupportedChainId.MAINNET]: ['https://cloudflare-eth.com'], [SupportedChainId.RINKEBY]: ['https://cloudflare-eth.com/v1/rinkeby'], } From ba31255ddec7b9bb9e5c60f931728ac49873e128 Mon Sep 17 00:00:00 2001 From: Kristie Huang Date: Wed, 13 Jul 2022 13:56:24 -0400 Subject: [PATCH 12/44] feat: give integrators option to pass in 'false' to disable our built-in flow --- .../ConnectWallet/ConnectWallet.tsx | 4 +++- src/components/ConnectWallet/index.tsx | 2 +- src/components/Swap/index.tsx | 2 +- src/cosmos/Swap.fixture.tsx | 1 + src/hooks/connectWeb3/useWeb3React.tsx | 19 ++++++++++++++++--- 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/components/ConnectWallet/ConnectWallet.tsx b/src/components/ConnectWallet/ConnectWallet.tsx index 9fc35af72..f6869ad1b 100644 --- a/src/components/ConnectWallet/ConnectWallet.tsx +++ b/src/components/ConnectWallet/ConnectWallet.tsx @@ -11,7 +11,7 @@ import { ConnectWalletDialog } from './ConnectWalletDialog' interface ConnectWalletProps { disabled?: boolean - onIntegratorConnectWalletCallback?: (e?: React.MouseEvent) => void + onIntegratorConnectWalletCallback?: false | ((e?: React.MouseEvent) => void) } const WalletButton = styled(TextButton)<{ hidden?: boolean }>` @@ -26,7 +26,9 @@ export default function ConnectWallet({ disabled, onIntegratorConnectWalletCallb const onClose = useCallback(() => setOpen(false), []) const onClick = useCallback( + // Block our own flow iff e.defaultPrevented or onIntegratorConnectWalletCallback === false (e?: React.MouseEvent) => { + if (onIntegratorConnectWalletCallback === false) return if (onIntegratorConnectWalletCallback) { onIntegratorConnectWalletCallback(e) if (e && e.defaultPrevented) return diff --git a/src/components/ConnectWallet/index.tsx b/src/components/ConnectWallet/index.tsx index d0a2cfcc2..78702a0ff 100644 --- a/src/components/ConnectWallet/index.tsx +++ b/src/components/ConnectWallet/index.tsx @@ -7,7 +7,7 @@ import ConnectWallet from './ConnectWallet' interface WalletProps { disabled?: boolean - onClickConnectWallet?: (e?: React.MouseEvent) => void + onClickConnectWallet?: false | ((e?: React.MouseEvent) => void) } export default function Wallet({ disabled, onClickConnectWallet }: WalletProps) { diff --git a/src/components/Swap/index.tsx b/src/components/Swap/index.tsx index 0bfe7e1b2..313eb3e0e 100644 --- a/src/components/Swap/index.tsx +++ b/src/components/Swap/index.tsx @@ -46,7 +46,7 @@ function getTransactionFromMap( // TODO(kristiehuang): refactor WalletConnection outside of Swap component export interface SwapProps extends TokenDefaults, FeeOptions { provider?: Eip1193Provider | JsonRpcProvider - onClickConnectWallet?: (e?: React.MouseEvent) => void + onClickConnectWallet?: false | ((e?: React.MouseEvent) => void) } export default function Swap(props: SwapProps) { diff --git a/src/cosmos/Swap.fixture.tsx b/src/cosmos/Swap.fixture.tsx index d041a8411..41071a6e4 100644 --- a/src/cosmos/Swap.fixture.tsx +++ b/src/cosmos/Swap.fixture.tsx @@ -71,6 +71,7 @@ function Fixture() { theme={theme} tokenList={tokenList} width={width} + // onClickConnectWallet={false} onClickConnectWallet={() => { // e?.preventDefault() console.log('integrator provided a onConnectWallet') diff --git a/src/hooks/connectWeb3/useWeb3React.tsx b/src/hooks/connectWeb3/useWeb3React.tsx index d66b4b7c0..b86723b19 100644 --- a/src/hooks/connectWeb3/useWeb3React.tsx +++ b/src/hooks/connectWeb3/useWeb3React.tsx @@ -9,6 +9,7 @@ import { WalletConnect } from '@web3-react/walletconnect' import { SupportedChainId } from 'constants/chains' import { JSON_RPC_FALLBACK_ENDPOINTS } from 'constants/jsonRpcEndpoints' import { PropsWithChildren, useMemo } from 'react' +import { json } from 'stream/consumers' export let connections: [Connector, Web3ReactHooks][] = [] export type Web3Connection = [Connector, Web3ReactHooks] @@ -88,16 +89,28 @@ export function ActiveWeb3Provider({ const networkConnection = useMemo(() => { if (!jsonRpcEndpoint) return const networkRpc = JsonRpcProvider.isProvider(jsonRpcEndpoint) ? [jsonRpcEndpoint] : [jsonRpcEndpoint] - const urlMap = { [SupportedChainId.MAINNET]: networkRpc } + console.log(networkRpc) + const urlMap = { [SupportedChainId.MAINNET]: networkRpc, [SupportedChainId.RINKEBY]: networkRpc } return toWeb3Connection(initializeConnector((actions) => new Network({ actions, urlMap }))) }, [jsonRpcEndpoint]) connections = [metaMaskConnection, walletConnectConnectionQR, walletConnectConnectionPopup] if (integratorConnection) connections = [integratorConnection, ...connections] - if (networkConnection) connections.push(networkConnection) + if (networkConnection) { + networkConnection[0].customProvider?.on('network', (newNetwork, oldNetwork) => { + // When a Provider makes its initial connection, it emits a "network" + // event with a null oldNetwork along with the newNetwork. So, if the + // oldNetwork exists, it represents a changing network + if (oldNetwork) { + window.location.reload(); + } + }) + connections.push(networkConnection) + } + return ( - + {children} ) From fd40f63c96d41785d88011fbe452735e46e3ec32 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Wed, 13 Jul 2022 17:57:57 +0000 Subject: [PATCH 13/44] style(lint): lint action with ESLint --- src/hooks/connectWeb3/useWeb3React.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/hooks/connectWeb3/useWeb3React.tsx b/src/hooks/connectWeb3/useWeb3React.tsx index b86723b19..ee929fd01 100644 --- a/src/hooks/connectWeb3/useWeb3React.tsx +++ b/src/hooks/connectWeb3/useWeb3React.tsx @@ -9,7 +9,6 @@ import { WalletConnect } from '@web3-react/walletconnect' import { SupportedChainId } from 'constants/chains' import { JSON_RPC_FALLBACK_ENDPOINTS } from 'constants/jsonRpcEndpoints' import { PropsWithChildren, useMemo } from 'react' -import { json } from 'stream/consumers' export let connections: [Connector, Web3ReactHooks][] = [] export type Web3Connection = [Connector, Web3ReactHooks] @@ -99,16 +98,15 @@ export function ActiveWeb3Provider({ if (networkConnection) { networkConnection[0].customProvider?.on('network', (newNetwork, oldNetwork) => { // When a Provider makes its initial connection, it emits a "network" - // event with a null oldNetwork along with the newNetwork. So, if the - // oldNetwork exists, it represents a changing network - if (oldNetwork) { - window.location.reload(); + // event with a null oldNetwork along with the newNetwork. So, if the + // oldNetwork exists, it represents a changing network + if (oldNetwork) { + window.location.reload() } }) connections.push(networkConnection) } - return ( {children} From 9179e6dbe9c223dfc3f931e8d6d8e88036055594 Mon Sep 17 00:00:00 2001 From: Kristie Huang Date: Wed, 13 Jul 2022 14:18:22 -0400 Subject: [PATCH 14/44] Revert "feat: give integrators option to pass in 'false' to disable our built-in flow" This reverts commit ba31255ddec7b9bb9e5c60f931728ac49873e128. --- .../ConnectWallet/ConnectWallet.tsx | 4 +--- src/components/ConnectWallet/index.tsx | 2 +- src/components/Swap/index.tsx | 2 +- src/cosmos/Swap.fixture.tsx | 1 - src/hooks/connectWeb3/useWeb3React.tsx | 19 +++---------------- 5 files changed, 6 insertions(+), 22 deletions(-) diff --git a/src/components/ConnectWallet/ConnectWallet.tsx b/src/components/ConnectWallet/ConnectWallet.tsx index f6869ad1b..9fc35af72 100644 --- a/src/components/ConnectWallet/ConnectWallet.tsx +++ b/src/components/ConnectWallet/ConnectWallet.tsx @@ -11,7 +11,7 @@ import { ConnectWalletDialog } from './ConnectWalletDialog' interface ConnectWalletProps { disabled?: boolean - onIntegratorConnectWalletCallback?: false | ((e?: React.MouseEvent) => void) + onIntegratorConnectWalletCallback?: (e?: React.MouseEvent) => void } const WalletButton = styled(TextButton)<{ hidden?: boolean }>` @@ -26,9 +26,7 @@ export default function ConnectWallet({ disabled, onIntegratorConnectWalletCallb const onClose = useCallback(() => setOpen(false), []) const onClick = useCallback( - // Block our own flow iff e.defaultPrevented or onIntegratorConnectWalletCallback === false (e?: React.MouseEvent) => { - if (onIntegratorConnectWalletCallback === false) return if (onIntegratorConnectWalletCallback) { onIntegratorConnectWalletCallback(e) if (e && e.defaultPrevented) return diff --git a/src/components/ConnectWallet/index.tsx b/src/components/ConnectWallet/index.tsx index 78702a0ff..d0a2cfcc2 100644 --- a/src/components/ConnectWallet/index.tsx +++ b/src/components/ConnectWallet/index.tsx @@ -7,7 +7,7 @@ import ConnectWallet from './ConnectWallet' interface WalletProps { disabled?: boolean - onClickConnectWallet?: false | ((e?: React.MouseEvent) => void) + onClickConnectWallet?: (e?: React.MouseEvent) => void } export default function Wallet({ disabled, onClickConnectWallet }: WalletProps) { diff --git a/src/components/Swap/index.tsx b/src/components/Swap/index.tsx index 313eb3e0e..0bfe7e1b2 100644 --- a/src/components/Swap/index.tsx +++ b/src/components/Swap/index.tsx @@ -46,7 +46,7 @@ function getTransactionFromMap( // TODO(kristiehuang): refactor WalletConnection outside of Swap component export interface SwapProps extends TokenDefaults, FeeOptions { provider?: Eip1193Provider | JsonRpcProvider - onClickConnectWallet?: false | ((e?: React.MouseEvent) => void) + onClickConnectWallet?: (e?: React.MouseEvent) => void } export default function Swap(props: SwapProps) { diff --git a/src/cosmos/Swap.fixture.tsx b/src/cosmos/Swap.fixture.tsx index 41071a6e4..d041a8411 100644 --- a/src/cosmos/Swap.fixture.tsx +++ b/src/cosmos/Swap.fixture.tsx @@ -71,7 +71,6 @@ function Fixture() { theme={theme} tokenList={tokenList} width={width} - // onClickConnectWallet={false} onClickConnectWallet={() => { // e?.preventDefault() console.log('integrator provided a onConnectWallet') diff --git a/src/hooks/connectWeb3/useWeb3React.tsx b/src/hooks/connectWeb3/useWeb3React.tsx index b86723b19..d66b4b7c0 100644 --- a/src/hooks/connectWeb3/useWeb3React.tsx +++ b/src/hooks/connectWeb3/useWeb3React.tsx @@ -9,7 +9,6 @@ import { WalletConnect } from '@web3-react/walletconnect' import { SupportedChainId } from 'constants/chains' import { JSON_RPC_FALLBACK_ENDPOINTS } from 'constants/jsonRpcEndpoints' import { PropsWithChildren, useMemo } from 'react' -import { json } from 'stream/consumers' export let connections: [Connector, Web3ReactHooks][] = [] export type Web3Connection = [Connector, Web3ReactHooks] @@ -89,28 +88,16 @@ export function ActiveWeb3Provider({ const networkConnection = useMemo(() => { if (!jsonRpcEndpoint) return const networkRpc = JsonRpcProvider.isProvider(jsonRpcEndpoint) ? [jsonRpcEndpoint] : [jsonRpcEndpoint] - console.log(networkRpc) - const urlMap = { [SupportedChainId.MAINNET]: networkRpc, [SupportedChainId.RINKEBY]: networkRpc } + const urlMap = { [SupportedChainId.MAINNET]: networkRpc } return toWeb3Connection(initializeConnector((actions) => new Network({ actions, urlMap }))) }, [jsonRpcEndpoint]) connections = [metaMaskConnection, walletConnectConnectionQR, walletConnectConnectionPopup] if (integratorConnection) connections = [integratorConnection, ...connections] - if (networkConnection) { - networkConnection[0].customProvider?.on('network', (newNetwork, oldNetwork) => { - // When a Provider makes its initial connection, it emits a "network" - // event with a null oldNetwork along with the newNetwork. So, if the - // oldNetwork exists, it represents a changing network - if (oldNetwork) { - window.location.reload(); - } - }) - connections.push(networkConnection) - } - + if (networkConnection) connections.push(networkConnection) return ( - + {children} ) From 495ac687b09b5b282d7594b4feb06946b6143214 Mon Sep 17 00:00:00 2001 From: Kristie Huang Date: Wed, 13 Jul 2022 15:43:28 -0400 Subject: [PATCH 15/44] fix: bump w3r versions to fix bug --- package.json | 16 +++++----- yarn.lock | 90 ++++++++++++++++++++++++++-------------------------- 2 files changed, 53 insertions(+), 53 deletions(-) diff --git a/package.json b/package.json index 701ce6fda..a8cf8b3c4 100644 --- a/package.json +++ b/package.json @@ -67,11 +67,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.33-beta.0", - "@web3-react/eip1193": "8.0.25-beta.0", - "@web3-react/empty": "8.0.19-beta.0", - "@web3-react/types": "8.0.19-beta.0", - "@web3-react/url": "8.0.24-beta.0", + "@web3-react/core" : "8.0.35-beta.0", + "@web3-react/eip1193": "8.0.26-beta.0", + "@web3-react/empty": "8.0.20-beta.0", + "@web3-react/types": "8.0.20-beta.0", + "@web3-react/url": "8.0.25-beta.0", "ajv": "^6.12.3", "cids": "^1.0.0", "ethers": "^5.1.4", @@ -156,9 +156,9 @@ "@uniswap/v3-core": "1.0.0", "@uniswap/v3-periphery": "^1.1.1", "@walletconnect/ethereum-provider": "^1.7.1", - "@web3-react/metamask": "8.0.26-beta.0", - "@web3-react/network": "8.0.26-beta.0", - "@web3-react/walletconnect": "8.0.34-beta.0", + "@web3-react/metamask": "8.0.27-beta.0", + "@web3-react/network": "8.0.27-beta.0", + "@web3-react/walletconnect": "8.0.35-beta.0", "babel-jest": "^27.5.1", "babel-loader": "^8.2.5", "babel-plugin-macros": "^3.1.0", diff --git a/yarn.lock b/yarn.lock index 99fa689b6..6941cdb97 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4051,77 +4051,77 @@ dependencies: "@walletconnect/window-getters" "^1.0.0" -"@web3-react/core@8.0.33-beta.0": - version "8.0.33-beta.0" - resolved "https://registry.yarnpkg.com/@web3-react/core/-/core-8.0.33-beta.0.tgz#018686511e9dcff1001965048d5e16c9ae60268a" - integrity sha512-qif/t5zycmlercIGPGNLpxM0+qiRZpbBIVFHgTGSS67bssgZoeBAXwafesbD1KRjiubATMgYBsKy16KskjxwTA== +"@web3-react/core@8.0.35-beta.0": + version "8.0.35-beta.0" + resolved "https://registry.yarnpkg.com/@web3-react/core/-/core-8.0.35-beta.0.tgz#8667483bdbc283fc8377d7f56faba1ec2fcdb095" + integrity sha512-vkEL2Vafu57lTA9T/cd3DNkZoDZ3G/JDUgxgjHqKLQVF4bPucrkeErqIHutAJ4suIi4bLOD0dFPMpFs+Bq7RgA== dependencies: - "@web3-react/store" "^8.0.24-beta.0" - "@web3-react/types" "^8.0.19-beta.0" + "@web3-react/store" "^8.0.25-beta.0" + "@web3-react/types" "^8.0.20-beta.0" zustand "^4.0.0-rc.0" optionalDependencies: "@ethersproject/providers" "^5" -"@web3-react/eip1193@8.0.25-beta.0": - version "8.0.25-beta.0" - resolved "https://registry.yarnpkg.com/@web3-react/eip1193/-/eip1193-8.0.25-beta.0.tgz#1df94b7c5ecef1ea97181015f0717a26b0a08bc7" - integrity sha512-L96B+xWjklR23bYyxYCvDWWcBsoThgY076oKmjJDel+3BnuEK7IH92aWcB1wUo3oLKzETdOm7eWi0ZyjBoM3Ag== +"@web3-react/eip1193@8.0.26-beta.0": + version "8.0.26-beta.0" + resolved "https://registry.yarnpkg.com/@web3-react/eip1193/-/eip1193-8.0.26-beta.0.tgz#2e36423bab637a1fbc57b36d4f0f38ac531fd541" + integrity sha512-n/2ajjABcP8DktZfXxSHrxtPIxeSRJ9tsgfmd9XPEIW9FO7xLzLrF+nloTSghtZ8H+ZKKcswlKjYq4rbX/oqHg== dependencies: - "@web3-react/types" "^8.0.19-beta.0" + "@web3-react/types" "^8.0.20-beta.0" -"@web3-react/empty@8.0.19-beta.0": - version "8.0.19-beta.0" - resolved "https://registry.yarnpkg.com/@web3-react/empty/-/empty-8.0.19-beta.0.tgz#884114cbdc5a82a2cd2e8e661162d2e69284a2ba" - integrity sha512-AAAeaS3Hn6PHdoP86vgx4O0ZjWQ9RFYAID+rJy84DDjLUodl9XLLYidGZmotEdv7kpKw5B5OtxnTjiMrSw64ag== +"@web3-react/empty@8.0.20-beta.0": + version "8.0.20-beta.0" + resolved "https://registry.yarnpkg.com/@web3-react/empty/-/empty-8.0.20-beta.0.tgz#f8e2a6414ba49c7da3937776c213eb4c8ff6e2c7" + integrity sha512-hde1Wq7w03cal6hD0E+seVg6ZFMWKcaSZ0S5UwJi5CDxhF79oL8QuzpvTBslohRkWqzqlvl52wCSVaY+l2+F8Q== dependencies: - "@web3-react/types" "^8.0.19-beta.0" + "@web3-react/types" "^8.0.20-beta.0" -"@web3-react/metamask@8.0.26-beta.0": - version "8.0.26-beta.0" - resolved "https://registry.yarnpkg.com/@web3-react/metamask/-/metamask-8.0.26-beta.0.tgz#c22dfec8d09be95493668fbb140e5d3a6ff45a8e" - integrity sha512-srYzA4nvMBWlH7kJLCunYueyucbTLmGqDUOK3QquVmQ+avvwOlYVzyvKPQrHf4M2+6f6GpzWYuw6AjNknvfaIA== +"@web3-react/metamask@8.0.27-beta.0": + version "8.0.27-beta.0" + resolved "https://registry.yarnpkg.com/@web3-react/metamask/-/metamask-8.0.27-beta.0.tgz#a2871a776365c8aac4798cc28d53a0e6173f6688" + integrity sha512-x97x3sy/kKoqoGRZ8+Dld1XEEQ9iv6/bY2gyjadJL99RXoEcvAPbZqCWSJGSySyzTFkq7M7rounkuGmaCMC8lg== dependencies: "@metamask/detect-provider" "^1.2.0" - "@web3-react/types" "^8.0.19-beta.0" + "@web3-react/types" "^8.0.20-beta.0" -"@web3-react/network@8.0.26-beta.0": - version "8.0.26-beta.0" - resolved "https://registry.yarnpkg.com/@web3-react/network/-/network-8.0.26-beta.0.tgz#43582d7652ae3a3785df0d3c3e582b03e6672e72" - integrity sha512-8VSYbWYHpGvlJOSXgUduSp+UDUaUU1tx+/9ivMb96TOgeyUqmkbERJzzPiaE3d3Ehe+7y/uvu0+v20tQqlx8Uw== +"@web3-react/network@8.0.27-beta.0": + version "8.0.27-beta.0" + resolved "https://registry.yarnpkg.com/@web3-react/network/-/network-8.0.27-beta.0.tgz#7cb522b02efc9d0f877ac285f350810fbf322292" + integrity sha512-kLHilUpLkDejx0C5Rr57puQSEVA+BQmT58xN6D/elphcZpVHAIkbh/MCYm0XrnLmqq0uOjw+jDhEYBBn80ncHQ== dependencies: "@ethersproject/providers" "^5" - "@web3-react/types" "^8.0.19-beta.0" + "@web3-react/types" "^8.0.20-beta.0" -"@web3-react/store@^8.0.24-beta.0": - version "8.0.24-beta.0" - resolved "https://registry.yarnpkg.com/@web3-react/store/-/store-8.0.24-beta.0.tgz#fb7e25d8478fce8ae95e6f50d7310f97876dc591" - integrity sha512-R2MA1lByr9LwQKhso3PYq31GRYEPbM1HKC4uWGGAY42JBXG+Fnhf/0fUIDcdJzdT6tonGBKRgqXKXQ+8HEjgzA== +"@web3-react/store@^8.0.25-beta.0": + version "8.0.25-beta.0" + resolved "https://registry.yarnpkg.com/@web3-react/store/-/store-8.0.25-beta.0.tgz#853a029a9f82d8950e306adb455ba308f908c8f4" + integrity sha512-YHrZ42EHiQ9UAJq8Y/pHsyXu6GhzbCSHaQJ9U0wCDhbPoQ9QjOj0Pwcaa/VLXljuynK/7ERP942nI1IzArt9Sg== dependencies: "@ethersproject/address" "^5" - "@web3-react/types" "^8.0.19-beta.0" + "@web3-react/types" "^8.0.20-beta.0" zustand "^4.0.0-rc.0" -"@web3-react/types@8.0.19-beta.0", "@web3-react/types@^8.0.19-beta.0": - version "8.0.19-beta.0" - resolved "https://registry.yarnpkg.com/@web3-react/types/-/types-8.0.19-beta.0.tgz#07daeef090cb252302f3bdf138d133df99826fb1" - integrity sha512-tM63N3IiL5E61HBvx62kDmQlQ/zCfnlYAkJCQnLWFpzQ2Kafcfpi0qAynvFye8DqSgy4E3d6kQah/NIs6BL+qw== +"@web3-react/types@8.0.20-beta.0", "@web3-react/types@^8.0.20-beta.0": + version "8.0.20-beta.0" + resolved "https://registry.yarnpkg.com/@web3-react/types/-/types-8.0.20-beta.0.tgz#6b4509bef8c5c7eb866e49295880c865c20fb565" + integrity sha512-qOZYMyUmsm3Um6t6Pg3OgnE86ufhWZpB5/VxsooB8cdpXc/C/f8KMyYSeM63GoKSMScOKwfqV6yODFL7g/Qc8g== dependencies: zustand "^4.0.0-rc.0" -"@web3-react/url@8.0.24-beta.0": - version "8.0.24-beta.0" - resolved "https://registry.yarnpkg.com/@web3-react/url/-/url-8.0.24-beta.0.tgz#7417cdb37bace531aa0fe76ccc4ccef8ed55c0e9" - integrity sha512-+pAMWVKNqg2h0iCRQ6b8zETdmSk903Vg3jinjMwwWSdxLAArCPlUoMgCtnxIFonQzwphynCEIo9w55uQBrOglA== +"@web3-react/url@8.0.25-beta.0": + version "8.0.25-beta.0" + resolved "https://registry.yarnpkg.com/@web3-react/url/-/url-8.0.25-beta.0.tgz#68e464d2e78b89496e50e9e4a28e91281c4d53d6" + integrity sha512-cjFfAFjsWF5vqJ7TG79HT72jNNWlcS9bqbNK2jvu25zej62zMpPvy4iyYiV7zy2SLbAQTdsgvIMYAdxRbnzlWg== dependencies: "@ethersproject/providers" "^5" - "@web3-react/types" "^8.0.19-beta.0" + "@web3-react/types" "^8.0.20-beta.0" -"@web3-react/walletconnect@8.0.34-beta.0": - version "8.0.34-beta.0" - resolved "https://registry.yarnpkg.com/@web3-react/walletconnect/-/walletconnect-8.0.34-beta.0.tgz#b0dee4d8e37dbb8783b46f6e181cbb7c317084d8" - integrity sha512-CG6dc6Lq0QVNvTp5lCFycIaHoPPV+X/6ay3swd5t2LD9R9Ao8sSzF0zU9Fi1iq41UDCue6WO4u2Lp7uHNAIcxw== +"@web3-react/walletconnect@8.0.35-beta.0": + version "8.0.35-beta.0" + resolved "https://registry.yarnpkg.com/@web3-react/walletconnect/-/walletconnect-8.0.35-beta.0.tgz#49c6c77447d58bfb295f28fa87c8fbfeec95cff5" + integrity sha512-fUrqcnwAr5oecZ6VUE/7+RSVURrohbAgWMLKYxd8Zo47AtTPzgJ1t5Lydh/EX4xJPLhfK1LqX5YgMwiys3DvhQ== dependencies: - "@web3-react/types" "^8.0.19-beta.0" + "@web3-react/types" "^8.0.20-beta.0" eventemitter3 "^4.0.7" "@webassemblyjs/ast@1.11.1": From 291c409bc7ec8dce1fea7b3092ed9505341643c8 Mon Sep 17 00:00:00 2001 From: Kristie Huang Date: Thu, 21 Jul 2022 13:26:11 -0400 Subject: [PATCH 16/44] feat: add widget disconnection via hover (#61) * feat: add disconnect hover * feat: add deactivate fxn * add comments * nit: use ternary * lint * Use wallet disconnection icon * nit: fix disconnection icon stroke/fill * nit * nit- comments * nit --- src/assets/svg/wallet_disconnect.svg | 5 +++ .../ConnectWallet/ConnectedWalletChip.tsx | 42 +++++++++++++++---- src/icons/index.tsx | 6 +++ 3 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 src/assets/svg/wallet_disconnect.svg diff --git a/src/assets/svg/wallet_disconnect.svg b/src/assets/svg/wallet_disconnect.svg new file mode 100644 index 000000000..b1edce83f --- /dev/null +++ b/src/assets/svg/wallet_disconnect.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/ConnectWallet/ConnectedWalletChip.tsx b/src/components/ConnectWallet/ConnectedWalletChip.tsx index 0ffc781e0..aad23a9f7 100644 --- a/src/components/ConnectWallet/ConnectedWalletChip.tsx +++ b/src/components/ConnectWallet/ConnectedWalletChip.tsx @@ -1,7 +1,13 @@ +import { Trans } from '@lingui/macro' import { useWeb3React } from '@web3-react/core' +import { Network } from '@web3-react/network' +import { Url } from '@web3-react/url' import { TextButton } from 'components/Button' import Row from 'components/Row' +import { connections } from 'hooks/connectWeb3/useWeb3React' +import { WalletDisconnect as WalletDisconnectIcon } from 'icons' import Identicon from 'icons/identicon' +import { useState } from 'react' import styled from 'styled-components/macro' import { ThemedText } from 'theme' @@ -14,22 +20,44 @@ export default function ConnectedWalletChip({ disabled }: { disabled?: boolean } // TODO(kristiehuang): AccountDialog UI does not yet exist // const [open, setOpen] = useState(false) + // TODO: hover to see disconnect button is temporary; disconnection should live inside AccountDialog + const [hover, setHover] = useState(false) + const { account } = useWeb3React() + function disconnectWallet() { + connections.forEach(([wallet, _]) => { + if (!(wallet instanceof Network || wallet instanceof Url)) { + // only deactivate non-network wallet connectors + wallet.deactivate ? wallet.deactivate() : wallet.resetState() + } + }) + } return ( <> {/* {open && ( setOpen(false)}> diff --git a/src/icons/index.tsx b/src/icons/index.tsx index 1c7b1dbe2..70f44ba59 100644 --- a/src/icons/index.tsx +++ b/src/icons/index.tsx @@ -5,6 +5,7 @@ import { ReactComponent as InlineSpinnerIcon } from 'assets/svg/inline_spinner.s import { ReactComponent as LogoIcon } from 'assets/svg/logo.svg' import { ReactComponent as SpinnerIcon } from 'assets/svg/spinner.svg' import { ReactComponent as WalletIcon } from 'assets/svg/wallet.svg' +import { ReactComponent as WalletDisconnectIcon } from 'assets/svg/wallet_disconnect.svg' import { loadingCss } from 'css/loading' import { FunctionComponent, SVGProps } from 'react' /* eslint-disable no-restricted-imports */ @@ -126,6 +127,11 @@ export const Logo = styled(icon(LogoIcon))` stroke: none; ` +export const WalletDisconnect = styled(icon(WalletDisconnectIcon))<{ color?: Color }>` + fill: currentColor; + stroke: none; +` + const rotate = keyframes` from { transform: rotate(0deg); From 04631865bd200916cd5405fdc23b95b335a0a3dd Mon Sep 17 00:00:00 2001 From: Kristie Huang Date: Wed, 27 Jul 2022 15:32:03 -0400 Subject: [PATCH 17/44] fix: multichain widget (#59) * activate network on json change * wip: use more fallback endpoints & use Url instead of Network * style(lint): lint action with ESLint * wip: use mapping * feat: add defaultChainId prop & clean up comments * nit comments * isProvider then/catch * Add defaultChainId when activating wallets * Support optimistic_kovan, squash error bug * add kovanfallback * Add validation for defaultChainId * Prop validation + DRY * Remove string/JsonRpcProvider types * remove useJsonRpcEndpoint in cosmos * Clean up getting IDs from names + dependency arrays * Fallback to free endpoints if not provided * Tidy up array indexing + prop validation * nit * Rename prop to jsonRpcUrlMap * descriptive console.warn * Fix provider key * Add chainName enum * Replace defaultChainId with atom * nit Co-authored-by: Lint Action --- e2e/connect.test.tsx | 4 +- .../ConnectWallet/ConnectWalletDialog.tsx | 33 ++++---- src/components/ConnectWallet/index.tsx | 14 +++- src/components/Widget.tsx | 39 ++++++++-- src/constants/chains.ts | 38 ++++++--- src/constants/jsonRpcEndpoints.ts | 14 +++- src/cosmos/Swap.fixture.tsx | 17 ++-- src/cosmos/useJsonRpcEndpoint.ts | 39 ---------- src/cosmos/useProvider.ts | 31 ++++++-- src/hooks/connectWeb3/useWeb3React.tsx | 78 +++++++++---------- 10 files changed, 177 insertions(+), 130 deletions(-) delete mode 100644 src/cosmos/useJsonRpcEndpoint.ts diff --git a/e2e/connect.test.tsx b/e2e/connect.test.tsx index 2b8c4d385..23c444b0b 100644 --- a/e2e/connect.test.tsx +++ b/e2e/connect.test.tsx @@ -26,9 +26,9 @@ describe('connect', () => { expect(toolbar.textContent).toBe('Connect wallet to swap') }) - describe('with jsonRpcEndpoint', () => { + describe('with jsonRpcUrlMap', () => { it('prompts for wallet connection in the Wallet', async () => { - act(() => component.rerender()) + act(() => component.rerender()) expect(wallet.hidden).toBeTruthy() expect(toolbar.textContent).toBe('Connecting…') diff --git a/src/components/ConnectWallet/ConnectWalletDialog.tsx b/src/components/ConnectWallet/ConnectWalletDialog.tsx index 2c115bf9d..5635a7a48 100644 --- a/src/components/ConnectWallet/ConnectWalletDialog.tsx +++ b/src/components/ConnectWallet/ConnectWalletDialog.tsx @@ -1,7 +1,6 @@ import { Trans } from '@lingui/macro' import { Web3ReactHooks } from '@web3-react/core' import { EIP1193 } from '@web3-react/eip1193' -import { Connector } from '@web3-react/types' import { Url } from '@web3-react/url' import { URI_AVAILABLE, WalletConnect } from '@web3-react/walletconnect' import METAMASK_ICON_URL from 'assets/images/metamaskIcon.png' @@ -11,10 +10,11 @@ import Column from 'components/Column' import { Header } from 'components/Dialog' import Row from 'components/Row' import EventEmitter from 'events' -import { connections, Web3Connection } from 'hooks/connectWeb3/useWeb3React' +import { connections, defaultChainIdAtom, Web3Connection } from 'hooks/connectWeb3/useWeb3React' import { atom, useAtom } from 'jotai' +import { useAtomValue } from 'jotai/utils' import QRCode from 'qrcode' -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import styled from 'styled-components/macro' import { lightTheme, ThemedText } from 'theme' @@ -88,6 +88,7 @@ function toQrCodeSvg(qrUri: string): Promise { function WalletConnectButton({ walletName, logoSrc, connection: wcTileConnection, onClick }: ButtonProps) { const [walletConnect] = wcTileConnection as [WalletConnect, Web3ReactHooks] const [error, setError] = useState(undefined) + const defaultChainId = useAtomValue(defaultChainIdAtom) const [qrUri, setQrUri] = useAtom(wcQRUriAtom) const [qrCodeSvg, setQrCodeSvg] = useState('') @@ -100,7 +101,7 @@ function WalletConnectButton({ walletName, logoSrc, connection: wcTileConnection setQrCodeSvg(qrCodeSvg) }) } else { - walletConnect.activate().catch((e) => { + walletConnect.activate(defaultChainId).catch((e) => { if (stale) return setError(e) }) @@ -108,7 +109,7 @@ function WalletConnectButton({ walletName, logoSrc, connection: wcTileConnection return () => { stale = true } - }, [qrUri, walletConnect]) + }, [qrUri, walletConnect, defaultChainId]) useEffect(() => { // Log web3 errors @@ -191,8 +192,8 @@ function NoWalletButton() { } export function ConnectWalletDialog() { - let defaultConnections: [Connector, Web3ReactHooks][] - const firstConnector: Connector = connections[0][0] + let defaultConnections: Web3Connection[] + const [firstConnector] = connections[0] if (firstConnector instanceof EIP1193 || firstConnector instanceof Url) { // If first connector is the integrator-provided connector defaultConnections = connections.slice(1) @@ -200,6 +201,16 @@ export function ConnectWalletDialog() { defaultConnections = connections } const [mmConnection, wcTileConnection, wcPopupConnection] = defaultConnections + const defaultChainId = useAtomValue(defaultChainIdAtom) + + const activateWalletConnectPopup = useCallback(() => { + const [walletConnectPopup] = wcPopupConnection + walletConnectPopup.activate(defaultChainId) + }, [wcPopupConnection, defaultChainId]) + const activateMetaMask = useCallback(() => { + const [metamask] = mmConnection + metamask.activate(defaultChainId) + }, [mmConnection, defaultChainId]) return ( <> @@ -210,14 +221,10 @@ export function ConnectWalletDialog() { walletName="WalletConnect" logoSrc={WALLETCONNECT_ICON_URL} connection={wcTileConnection} - onClick={() => wcPopupConnection[0].activate()} + onClick={activateWalletConnectPopup} /> - mmConnection[0].activate()} - /> + diff --git a/src/components/ConnectWallet/index.tsx b/src/components/ConnectWallet/index.tsx index d0a2cfcc2..dc6ea8334 100644 --- a/src/components/ConnectWallet/index.tsx +++ b/src/components/ConnectWallet/index.tsx @@ -1,5 +1,6 @@ import { useWeb3React } from '@web3-react/core' -import { connections } from 'hooks/connectWeb3/useWeb3React' +import { connections, defaultChainIdAtom } from 'hooks/connectWeb3/useWeb3React' +import { useAtomValue } from 'jotai/utils' import { useEffect } from 'react' import ConnectedWalletChip from './ConnectedWalletChip' @@ -11,10 +12,15 @@ interface WalletProps { } export default function Wallet({ disabled, onClickConnectWallet }: WalletProps) { - // Attempt to connect eagerly on mount + // Attempt to connect eagerly on mount, and prompt switch networks when integrator's defaultChainId changes + const defaultChainId = useAtomValue(defaultChainIdAtom) useEffect(() => { - connections.forEach(([wallet, _]) => (wallet.connectEagerly ? wallet.connectEagerly() : wallet.activate())) - }, []) + connections.forEach(([wallet, _]) => + wallet.connectEagerly + ? wallet.connectEagerly(defaultChainId)?.catch((e) => console.log(e)) + : wallet.activate(defaultChainId) + ) + }, [defaultChainId]) const { account, isActive } = useWeb3React() diff --git a/src/components/Widget.tsx b/src/components/Widget.tsx index bc5cef74f..705e549ab 100644 --- a/src/components/Widget.tsx +++ b/src/components/Widget.tsx @@ -1,7 +1,7 @@ import { JsonRpcProvider } from '@ethersproject/providers' import { TokenInfo } from '@uniswap/token-lists' import { Provider as Eip1193Provider } from '@web3-react/types' -import { SupportedChainId } from 'constants/chains' +import { ALL_SUPPORTED_CHAIN_IDS, SupportedChainId } from 'constants/chains' import { JSON_RPC_FALLBACK_ENDPOINTS } from 'constants/jsonRpcEndpoints' import { DEFAULT_LOCALE, SUPPORTED_LOCALES, SupportedLocale } from 'constants/locales' import { ActiveWeb3Provider } from 'hooks/connectWeb3/useWeb3React' @@ -20,6 +20,8 @@ import { UNMOUNTING } from 'utils/animations' import { Modal, Provider as DialogProvider } from './Dialog' import ErrorBoundary, { ErrorHandler } from './Error/ErrorBoundary' +const DEFAULT_CHAIN_ID = SupportedChainId.MAINNET + const WidgetWrapper = styled.div<{ width?: number | string }>` -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; @@ -89,8 +91,8 @@ export type WidgetProps = { theme?: Theme locale?: SupportedLocale provider?: Eip1193Provider | JsonRpcProvider - // TODO(kristiehuang): allow integrator to pass in {chainId: [rpcUrls]} for multichain jsonRpcEndpoints - jsonRpcEndpoint?: string | JsonRpcProvider + jsonRpcUrlMap?: { [chainId: number]: string[] } + defaultChainId?: number tokenList?: string | TokenInfo[] width?: string | number dialog?: HTMLElement | null @@ -114,10 +116,27 @@ export default function Widget(props: PropsWithChildren) { } return props.locale ?? DEFAULT_LOCALE }, [props.locale]) - const jsonRpcEndpoint = useMemo( - () => props.jsonRpcEndpoint ?? JSON_RPC_FALLBACK_ENDPOINTS[SupportedChainId.MAINNET]?.[0], - [props.jsonRpcEndpoint] - ) + const defaultChainId = useMemo(() => { + if (!props.defaultChainId) return DEFAULT_CHAIN_ID + if (!ALL_SUPPORTED_CHAIN_IDS.includes(props.defaultChainId)) { + console.warn(`Unsupported chainId: ${props.defaultChainId}. Falling back to 1 (Ethereum Mainnet).`) + return DEFAULT_CHAIN_ID + } + return props.defaultChainId + }, [props.defaultChainId]) + const jsonRpcUrlMap: string | JsonRpcProvider | { [chainId: number]: string[] } = useMemo(() => { + if (!props.jsonRpcUrlMap) return JSON_RPC_FALLBACK_ENDPOINTS + for (const supportedChainId of ALL_SUPPORTED_CHAIN_IDS) { + if (!Object.keys(props.jsonRpcUrlMap).includes(`${supportedChainId}`)) { + const fallbackRpc = JSON_RPC_FALLBACK_ENDPOINTS[supportedChainId as number] + console.warn( + `Did not provide a jsonRpcUrlMap for chainId: ${supportedChainId}. Falling back to free public RPC endpoint ${fallbackRpc}.` + ) + props.jsonRpcUrlMap[supportedChainId as number] = fallbackRpc + } + } + return props.jsonRpcUrlMap + }, [props.jsonRpcUrlMap]) const [dialog, setDialog] = useState(null) return ( @@ -130,7 +149,11 @@ export default function Widget(props: PropsWithChildren) { - + diff --git a/src/constants/chains.ts b/src/constants/chains.ts index 11cfe4e96..d398e7435 100644 --- a/src/constants/chains.ts +++ b/src/constants/chains.ts @@ -18,18 +18,32 @@ export enum SupportedChainId { POLYGON_MUMBAI = 80001, } -export const CHAIN_IDS_TO_NAMES = { - [SupportedChainId.MAINNET]: 'mainnet', - [SupportedChainId.ROPSTEN]: 'ropsten', - [SupportedChainId.RINKEBY]: 'rinkeby', - [SupportedChainId.GOERLI]: 'goerli', - [SupportedChainId.KOVAN]: 'kovan', - [SupportedChainId.POLYGON]: 'polygon', - [SupportedChainId.POLYGON_MUMBAI]: 'polygon_mumbai', - [SupportedChainId.ARBITRUM_ONE]: 'arbitrum', - [SupportedChainId.ARBITRUM_RINKEBY]: 'arbitrum_rinkeby', - [SupportedChainId.OPTIMISM]: 'optimism', - [SupportedChainId.OPTIMISTIC_KOVAN]: 'optimistic_kovan', +export enum ChainName { + MAINNET = 'mainnet', + ROPSTEN = 'ropsten', + RINKEBY = 'rinkeby', + GOERLI = 'goerli', + KOVAN = 'kovan', + OPTIMISM = 'optimism-mainnet', + OPTIMISTIC_KOVAN = 'optimism-kovan', + ARBITRUM_ONE = 'arbitrum-mainnet', + ARBITRUM_RINKEBY = 'arbitrum-rinkeby', + POLYGON = 'polygon-mainnet', + POLYGON_MUMBAI = 'polygon-mumbai', +} + +export const CHAIN_NAMES_TO_IDS: { [ChainName: string]: SupportedChainId } = { + [ChainName.MAINNET]: SupportedChainId.MAINNET, + [ChainName.ROPSTEN]: SupportedChainId.ROPSTEN, + [ChainName.RINKEBY]: SupportedChainId.RINKEBY, + [ChainName.GOERLI]: SupportedChainId.GOERLI, + [ChainName.KOVAN]: SupportedChainId.KOVAN, + [ChainName.POLYGON]: SupportedChainId.POLYGON, + [ChainName.POLYGON_MUMBAI]: SupportedChainId.POLYGON_MUMBAI, + [ChainName.ARBITRUM_ONE]: SupportedChainId.ARBITRUM_ONE, + [ChainName.ARBITRUM_RINKEBY]: SupportedChainId.ARBITRUM_RINKEBY, + [ChainName.OPTIMISM]: SupportedChainId.OPTIMISM, + [ChainName.OPTIMISTIC_KOVAN]: SupportedChainId.OPTIMISTIC_KOVAN, } /** diff --git a/src/constants/jsonRpcEndpoints.ts b/src/constants/jsonRpcEndpoints.ts index ef25a1215..cc6813641 100644 --- a/src/constants/jsonRpcEndpoints.ts +++ b/src/constants/jsonRpcEndpoints.ts @@ -3,7 +3,17 @@ import { SupportedChainId } from './chains' /** * Fallback JSON RPC endpoints if integrator does not provide one */ -export const JSON_RPC_FALLBACK_ENDPOINTS: { [key in SupportedChainId]?: string[] } = { +export const JSON_RPC_FALLBACK_ENDPOINTS: { [chainId: number]: string[] } = { [SupportedChainId.MAINNET]: ['https://cloudflare-eth.com'], - [SupportedChainId.RINKEBY]: ['https://cloudflare-eth.com/v1/rinkeby'], + [SupportedChainId.ROPSTEN]: ['https://rpc.ankr.com/eth_ropsten'], + [SupportedChainId.RINKEBY]: ['https://rpc.ankr.com/eth_rinkeby'], + [SupportedChainId.GOERLI]: ['https://rpc.ankr.com/eth_goerli'], + [SupportedChainId.KOVAN]: ['https://kovan.poa.network'], // as of 7/21/22, this kovan endpoint is down :( + [SupportedChainId.POLYGON]: ['https://polygon-rpc.com', 'https://rpc-mainnet.matic.quiknode.pro'], + [SupportedChainId.POLYGON_MUMBAI]: ['https://rpc-mumbai.maticvigil.com'], + [SupportedChainId.ARBITRUM_ONE]: ['https://arbitrum.public-rpc.com'], + [SupportedChainId.ARBITRUM_RINKEBY]: ['https://rinkeby.arbitrum.io/rpc'], + [SupportedChainId.OPTIMISM]: ['https://rpc.ankr.com/optimism'], + [SupportedChainId.OPTIMISTIC_KOVAN]: ['https://kovan.optimism.io'], + // [SupportedChainId.CELO]: ['https://rpc.ankr.com/celo'], // TODO: need to add support for Celo } diff --git a/src/cosmos/Swap.fixture.tsx b/src/cosmos/Swap.fixture.tsx index d041a8411..e5c1b0e4c 100644 --- a/src/cosmos/Swap.fixture.tsx +++ b/src/cosmos/Swap.fixture.tsx @@ -9,13 +9,13 @@ import { SupportedChainId, SwapWidget, } from '@uniswap/widgets' +import { CHAIN_NAMES_TO_IDS } from 'constants/chains' import { useEffect } from 'react' import { useValue } from 'react-cosmos/fixture' import { DAI, USDC_MAINNET } from '../constants/tokens' -import useJsonRpcEndpoint from './useJsonRpcEndpoint' import useOption from './useOption' -import useProvider from './useProvider' +import useProvider, { INFURA_NETWORK_URLS } from './useProvider' function Fixture() { const [convenienceFee] = useValue('convenienceFee', { defaultValue: 0 }) @@ -47,8 +47,14 @@ function Fixture() { const [darkMode] = useValue('darkMode', { defaultValue: false }) useEffect(() => setTheme((theme) => ({ ...theme, ...(darkMode ? darkTheme : lightTheme) })), [darkMode, setTheme]) - const jsonRpcEndpoint = useJsonRpcEndpoint() - const connector = useProvider() + const jsonRpcUrlMap = INFURA_NETWORK_URLS + + const defaultNetwork = useOption('defaultChainId', { + options: Object.keys(CHAIN_NAMES_TO_IDS), + defaultValue: 'mainnet', + }) + const defaultChainId = defaultNetwork ? CHAIN_NAMES_TO_IDS[defaultNetwork] : undefined + const connector = useProvider(defaultChainId) const tokenLists: Record = { Default: tokens, @@ -66,7 +72,8 @@ function Fixture() { defaultOutputTokenAddress={defaultOutputToken} defaultOutputAmount={defaultOutputAmount} locale={locale} - jsonRpcEndpoint={jsonRpcEndpoint} + jsonRpcUrlMap={jsonRpcUrlMap} + defaultChainId={defaultChainId} provider={connector} theme={theme} tokenList={tokenList} diff --git a/src/cosmos/useJsonRpcEndpoint.ts b/src/cosmos/useJsonRpcEndpoint.ts deleted file mode 100644 index 998625ecd..000000000 --- a/src/cosmos/useJsonRpcEndpoint.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { SupportedChainId } from '@uniswap/widgets' - -import useOption, { NONE } from './useOption' - -const INFURA_KEY = process.env.INFURA_KEY -if (INFURA_KEY === undefined) { - console.warn(`INFURA_KEY must be a defined environment variable to use JsonRpcEndpoints in the cosmos viewer`) -} - -export const INFURA_NETWORK_URLS: { [key in SupportedChainId]?: string[] } = INFURA_KEY - ? { - [SupportedChainId.MAINNET]: [`https://mainnet.infura.io/v3/${INFURA_KEY}`], - [SupportedChainId.RINKEBY]: [`https://rinkeby.infura.io/v3/${INFURA_KEY}`], - [SupportedChainId.ROPSTEN]: [`https://ropsten.infura.io/v3/${INFURA_KEY}`], - [SupportedChainId.GOERLI]: [`https://goerli.infura.io/v3/${INFURA_KEY}`], - [SupportedChainId.KOVAN]: [`https://kovan.infura.io/v3/${INFURA_KEY}`], - [SupportedChainId.OPTIMISM]: [`https://optimism-mainnet.infura.io/v3/${INFURA_KEY}`], - [SupportedChainId.OPTIMISTIC_KOVAN]: [`https://optimism-kovan.infura.io/v3/${INFURA_KEY}`], - [SupportedChainId.ARBITRUM_ONE]: [`https://arbitrum-mainnet.infura.io/v3/${INFURA_KEY}`], - [SupportedChainId.ARBITRUM_RINKEBY]: [`https://arbitrum-rinkeby.infura.io/v3/${INFURA_KEY}`], - [SupportedChainId.POLYGON]: [`https://polygon-mainnet.infura.io/v3/${INFURA_KEY}`], - [SupportedChainId.POLYGON_MUMBAI]: [`https://polygon-mumbai.infura.io/v3/${INFURA_KEY}`], - } - : {} - -export default function useJsonRpcEndpoint() { - const endpoints = Object.entries(INFURA_NETWORK_URLS).reduce<{ [chainId: string]: string[] }>( - (acc, [chainId, url]) => ({ - ...acc, - [SupportedChainId[Number(chainId)]]: url, - }), - {} - ) - - return useOption('jsonRpcEndpoint', { - options: endpoints, - defaultValue: INFURA_NETWORK_URLS[SupportedChainId.MAINNET] ? SupportedChainId[SupportedChainId.MAINNET] : NONE, - })?.[0] -} diff --git a/src/cosmos/useProvider.ts b/src/cosmos/useProvider.ts index 92fe03cbd..f6efb1e28 100644 --- a/src/cosmos/useProvider.ts +++ b/src/cosmos/useProvider.ts @@ -2,11 +2,32 @@ import { initializeConnector } from '@web3-react/core' import { MetaMask } from '@web3-react/metamask' import { Connector } from '@web3-react/types' import { WalletConnect } from '@web3-react/walletconnect' +import { SupportedChainId } from 'constants/chains' import { useEffect, useState } from 'react' -import { INFURA_NETWORK_URLS } from './useJsonRpcEndpoint' import useOption from './useOption' +const INFURA_KEY = process.env.INFURA_KEY +if (INFURA_KEY === undefined) { + console.warn(`INFURA_KEY must be a defined environment variable to use jsonRpcUrlMap in the cosmos viewer`) +} + +export const INFURA_NETWORK_URLS: { [chainId: number]: string[] } = INFURA_KEY + ? { + [SupportedChainId.MAINNET]: [`https://mainnet.infura.io/v3/${INFURA_KEY}`], + [SupportedChainId.RINKEBY]: [`https://rinkeby.infura.io/v3/${INFURA_KEY}`], + [SupportedChainId.ROPSTEN]: [`https://ropsten.infura.io/v3/${INFURA_KEY}`], + [SupportedChainId.GOERLI]: [`https://goerli.infura.io/v3/${INFURA_KEY}`], + [SupportedChainId.KOVAN]: [`https://kovan.infura.io/v3/${INFURA_KEY}`], + [SupportedChainId.OPTIMISM]: [`https://optimism-mainnet.infura.io/v3/${INFURA_KEY}`], + [SupportedChainId.OPTIMISTIC_KOVAN]: [`https://optimism-kovan.infura.io/v3/${INFURA_KEY}`], + [SupportedChainId.ARBITRUM_ONE]: [`https://arbitrum-mainnet.infura.io/v3/${INFURA_KEY}`], + [SupportedChainId.ARBITRUM_RINKEBY]: [`https://arbitrum-rinkeby.infura.io/v3/${INFURA_KEY}`], + [SupportedChainId.POLYGON]: [`https://polygon-mainnet.infura.io/v3/${INFURA_KEY}`], + [SupportedChainId.POLYGON_MUMBAI]: [`https://polygon-mumbai.infura.io/v3/${INFURA_KEY}`], + } + : {} + enum Wallet { MetaMask = 'MetaMask', WalletConnect = 'WalletConenct', @@ -22,7 +43,7 @@ const [walletConnect] = initializeConnector( }) ) -export default function useProvider() { +export default function useProvider(defaultChainId?: number) { const connectorType = useOption('provider', { options: [Wallet.MetaMask, Wallet.WalletConnect] }) const [connector, setConnector] = useState() useEffect(() => { @@ -36,11 +57,11 @@ export default function useProvider() { let connector: Connector | undefined switch (connectorType) { case Wallet.MetaMask: - await metaMask.activate() + await metaMask.activate(defaultChainId) connector = metaMask break case Wallet.WalletConnect: - await walletConnect.activate() + await walletConnect.activate(defaultChainId) connector = walletConnect } if (!stale) { @@ -50,7 +71,7 @@ export default function useProvider() { }) } } - }, [connectorType]) + }, [connectorType, defaultChainId]) return connector?.provider } diff --git a/src/hooks/connectWeb3/useWeb3React.tsx b/src/hooks/connectWeb3/useWeb3React.tsx index d66b4b7c0..89bd295db 100644 --- a/src/hooks/connectWeb3/useWeb3React.tsx +++ b/src/hooks/connectWeb3/useWeb3React.tsx @@ -6,12 +6,12 @@ import { Network } from '@web3-react/network' import { Connector, Provider as Eip1193Provider, Web3ReactStore } from '@web3-react/types' import { Url } from '@web3-react/url' import { WalletConnect } from '@web3-react/walletconnect' -import { SupportedChainId } from 'constants/chains' -import { JSON_RPC_FALLBACK_ENDPOINTS } from 'constants/jsonRpcEndpoints' -import { PropsWithChildren, useMemo } from 'react' +import { atom, useAtom } from 'jotai' +import { PropsWithChildren, useEffect, useMemo } from 'react' -export let connections: [Connector, Web3ReactHooks][] = [] export type Web3Connection = [Connector, Web3ReactHooks] +export let connections: Web3Connection[] = [] +export const defaultChainIdAtom = atom(1) function toWeb3Connection([connector, hooks]: [T, Web3ReactHooks, Web3ReactStore]): [ T, @@ -31,34 +31,21 @@ function getWallet(provider?: JsonRpcProvider | Eip1193Provider) { } } -function getWalletConnectConnection(useDefault: boolean, jsonRpcEndpoint?: string | JsonRpcProvider) { - // WalletConnect relies on Buffer, so it must be polyfilled. - if (!('Buffer' in window)) { - window.Buffer = Buffer - } - - // FIXME(kristiehuang): we don't know what the props.jsonRpcEndpoint chain is; assume mainnet for WC instantiation - let mainnetRpcUrl: string | undefined - if (JsonRpcProvider.isProvider(jsonRpcEndpoint)) { - mainnetRpcUrl = jsonRpcEndpoint.connection.url - } else { - mainnetRpcUrl = jsonRpcEndpoint - } +function getWalletConnectConnection( + useDefault: boolean, + jsonRpcUrlMap: { [chainId: number]: string[] }, + defaultChainId: number +) { return toWeb3Connection( initializeConnector( (actions) => new WalletConnect({ actions, options: { - rpc: { - [SupportedChainId.MAINNET]: [ - mainnetRpcUrl ?? '', - ...(JSON_RPC_FALLBACK_ENDPOINTS[SupportedChainId.MAINNET] ?? []), - ].filter((url) => url !== undefined && url !== ''), - [SupportedChainId.RINKEBY]: JSON_RPC_FALLBACK_ENDPOINTS[SupportedChainId.RINKEBY] ?? [], - }, + rpc: jsonRpcUrlMap, qrcode: useDefault, }, + defaultChainId, }) ) ) @@ -66,38 +53,49 @@ function getWalletConnectConnection(useDefault: boolean, jsonRpcEndpoint?: strin interface ActiveWeb3ProviderProps { provider?: Eip1193Provider | JsonRpcProvider - jsonRpcEndpoint?: string | JsonRpcProvider + jsonRpcUrlMap: { [chainId: number]: string[] } + defaultChainId: number } export function ActiveWeb3Provider({ - provider: propsProvider, - jsonRpcEndpoint, + provider, + jsonRpcUrlMap, + defaultChainId: propsDefaultChainId, children, }: PropsWithChildren) { - const integratorConnection = useMemo(() => getWallet(propsProvider), [propsProvider]) + const [defaultChainId, setDefaultChainId] = useAtom(defaultChainIdAtom) + useEffect(() => { + if (propsDefaultChainId !== defaultChainId) setDefaultChainId(propsDefaultChainId) + }, [propsDefaultChainId, defaultChainId, setDefaultChainId]) + + const integratorConnection = useMemo(() => getWallet(provider), [provider]) const metaMaskConnection = useMemo( () => toWeb3Connection(initializeConnector((actions) => new MetaMask({ actions }))), [] ) - const walletConnectConnectionQR = useMemo(() => getWalletConnectConnection(false, jsonRpcEndpoint), [jsonRpcEndpoint]) // WC via tile QR code scan + const walletConnectConnectionQR = useMemo( + () => getWalletConnectConnection(false, jsonRpcUrlMap, propsDefaultChainId), + [jsonRpcUrlMap, propsDefaultChainId] + ) // WC via tile QR code scan const walletConnectConnectionPopup = useMemo( - () => getWalletConnectConnection(true, jsonRpcEndpoint), - [jsonRpcEndpoint] + () => getWalletConnectConnection(true, jsonRpcUrlMap, propsDefaultChainId), + [jsonRpcUrlMap, propsDefaultChainId] ) // WC via built-in popup - const networkConnection = useMemo(() => { - if (!jsonRpcEndpoint) return - const networkRpc = JsonRpcProvider.isProvider(jsonRpcEndpoint) ? [jsonRpcEndpoint] : [jsonRpcEndpoint] - const urlMap = { [SupportedChainId.MAINNET]: networkRpc } - return toWeb3Connection(initializeConnector((actions) => new Network({ actions, urlMap }))) - }, [jsonRpcEndpoint]) + const networkConnection = useMemo( + () => + toWeb3Connection( + initializeConnector((actions) => new Network({ actions, urlMap: jsonRpcUrlMap, propsDefaultChainId })) + ), + [jsonRpcUrlMap, propsDefaultChainId] + ) - connections = [metaMaskConnection, walletConnectConnectionQR, walletConnectConnectionPopup] + connections = [metaMaskConnection, walletConnectConnectionQR, walletConnectConnectionPopup, networkConnection] if (integratorConnection) connections = [integratorConnection, ...connections] - if (networkConnection) connections.push(networkConnection) + const key = `${connections.length}+${Object.keys(jsonRpcUrlMap)}+${propsDefaultChainId}` return ( - + {children} ) From f5f9fe43f45855dde62033d227e2498b5803c27f Mon Sep 17 00:00:00 2001 From: Kristie Huang Date: Thu, 28 Jul 2022 12:06:19 -0400 Subject: [PATCH 18/44] fix: use web3react instead of activeweb3react --- src/hooks/routing/useRouterTrade.ts | 6 +++--- src/hooks/useStablecoinAmountFromFiatValue.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/hooks/routing/useRouterTrade.ts b/src/hooks/routing/useRouterTrade.ts index af0e66849..f15f03d8b 100644 --- a/src/hooks/routing/useRouterTrade.ts +++ b/src/hooks/routing/useRouterTrade.ts @@ -1,11 +1,11 @@ import { JsonRpcProvider } from '@ethersproject/providers' import { skipToken } from '@reduxjs/toolkit/query/react' import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' // Importing just the type, so smart-order-router is lazy-loaded // eslint-disable-next-line no-restricted-imports import type { ChainId } from '@uniswap/smart-order-router' import { useRouterArguments } from 'hooks/routing/useRouterArguments' -import useActiveWeb3React from 'hooks/useActiveWeb3React' import useDebounce from 'hooks/useDebounce' import useIsValidBlock from 'hooks/useIsValidBlock' import useIsWindowVisible from 'hooks/useIsWindowVisible' @@ -36,7 +36,7 @@ export function useRouterTrade( state: TradeState trade: InterfaceTrade | undefined } { - const { chainId, library } = useActiveWeb3React() + const { chainId, provider } = useWeb3React() const autoRouterSupported = isAutoRouterSupportedChain(chainId) const isWindowVisible = useIsWindowVisible() // Debounce is used to prevent excessive requests to SOR, as it is data intensive. @@ -67,7 +67,7 @@ export function useRouterTrade( amount: debouncedAmountSpecified, tradeType, routerUrl, - provider: library as JsonRpcProvider, + provider: provider as JsonRpcProvider, }) const { isError, data, currentData } = useGetQuoteQuery(queryArgs ?? skipToken, { diff --git a/src/hooks/useStablecoinAmountFromFiatValue.ts b/src/hooks/useStablecoinAmountFromFiatValue.ts index efe0cb3f9..182d91b2f 100644 --- a/src/hooks/useStablecoinAmountFromFiatValue.ts +++ b/src/hooks/useStablecoinAmountFromFiatValue.ts @@ -1,7 +1,7 @@ import { CurrencyAmount, Token } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' import { SupportedChainId } from 'constants/chains' import { DAI_OPTIMISM, USDC_ARBITRUM, USDC_MAINNET, USDC_POLYGON } from 'constants/tokens' -import useActiveWeb3React from 'hooks/useActiveWeb3React' import { useMemo } from 'react' import tryParseCurrencyAmount from 'utils/tryParseCurrencyAmount' @@ -20,7 +20,7 @@ export const STABLECOIN_AMOUNT_OUT: { [chainId: number]: CurrencyAmount } * @returns CurrencyAmount where currency is stablecoin on active chain */ export function useStablecoinAmountFromFiatValue(fiatValue: string | null | undefined) { - const { chainId } = useActiveWeb3React() + const { chainId } = useWeb3React() const stablecoin = chainId ? STABLECOIN_AMOUNT_OUT[chainId]?.currency : undefined return useMemo(() => { From c9c8264cdd5f8ce7bed2cd64d2cd9f6cea062a46 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Thu, 28 Jul 2022 16:07:54 +0000 Subject: [PATCH 19/44] style(lint): lint action with ESLint --- src/hooks/routing/useRouterTrade.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/routing/useRouterTrade.ts b/src/hooks/routing/useRouterTrade.ts index f15f03d8b..a463c2ad5 100644 --- a/src/hooks/routing/useRouterTrade.ts +++ b/src/hooks/routing/useRouterTrade.ts @@ -1,10 +1,10 @@ import { JsonRpcProvider } from '@ethersproject/providers' import { skipToken } from '@reduxjs/toolkit/query/react' import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' -import { useWeb3React } from '@web3-react/core' // Importing just the type, so smart-order-router is lazy-loaded // eslint-disable-next-line no-restricted-imports import type { ChainId } from '@uniswap/smart-order-router' +import { useWeb3React } from '@web3-react/core' import { useRouterArguments } from 'hooks/routing/useRouterArguments' import useDebounce from 'hooks/useDebounce' import useIsValidBlock from 'hooks/useIsValidBlock' From e7ef6cbcd734f79649cced81b12cba50ab4e9666 Mon Sep 17 00:00:00 2001 From: Kristie Huang Date: Thu, 28 Jul 2022 13:01:45 -0400 Subject: [PATCH 20/44] Add public rpcs --- src/constants/jsonRpcEndpoints.ts | 4 ++-- src/cosmos/Swap.fixture.tsx | 32 +++++++++++++++---------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/constants/jsonRpcEndpoints.ts b/src/constants/jsonRpcEndpoints.ts index cc6813641..4eea2696e 100644 --- a/src/constants/jsonRpcEndpoints.ts +++ b/src/constants/jsonRpcEndpoints.ts @@ -4,11 +4,11 @@ import { SupportedChainId } from './chains' * Fallback JSON RPC endpoints if integrator does not provide one */ export const JSON_RPC_FALLBACK_ENDPOINTS: { [chainId: number]: string[] } = { - [SupportedChainId.MAINNET]: ['https://cloudflare-eth.com'], + [SupportedChainId.MAINNET]: ['https://eth-mainnet.public.blastapi.io', 'https://cloudflare-eth.com'], [SupportedChainId.ROPSTEN]: ['https://rpc.ankr.com/eth_ropsten'], [SupportedChainId.RINKEBY]: ['https://rpc.ankr.com/eth_rinkeby'], [SupportedChainId.GOERLI]: ['https://rpc.ankr.com/eth_goerli'], - [SupportedChainId.KOVAN]: ['https://kovan.poa.network'], // as of 7/21/22, this kovan endpoint is down :( + [SupportedChainId.KOVAN]: ['https://eth-kovan.public.blastapi.io', 'https://kovan.poa.network'], [SupportedChainId.POLYGON]: ['https://polygon-rpc.com', 'https://rpc-mainnet.matic.quiknode.pro'], [SupportedChainId.POLYGON_MUMBAI]: ['https://rpc-mumbai.maticvigil.com'], [SupportedChainId.ARBITRUM_ONE]: ['https://arbitrum.public-rpc.com'], diff --git a/src/cosmos/Swap.fixture.tsx b/src/cosmos/Swap.fixture.tsx index 3bbc5cab9..da514f6ab 100644 --- a/src/cosmos/Swap.fixture.tsx +++ b/src/cosmos/Swap.fixture.tsx @@ -67,24 +67,24 @@ function Fixture() { return ( { - // e?.preventDefault() - console.log('integrator provided a onConnectWallet') - }} + // onClickConnectWallet={() => { + // // e?.preventDefault() + // console.log('integrator provided a onConnectWallet') + // }} /> ) } From d67063c54ff1d90f613ff9e0f402e1fab9e5287f Mon Sep 17 00:00:00 2001 From: Kristie Huang Date: Thu, 28 Jul 2022 13:02:35 -0400 Subject: [PATCH 21/44] nit --- src/cosmos/Swap.fixture.tsx | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/cosmos/Swap.fixture.tsx b/src/cosmos/Swap.fixture.tsx index da514f6ab..3bbc5cab9 100644 --- a/src/cosmos/Swap.fixture.tsx +++ b/src/cosmos/Swap.fixture.tsx @@ -67,24 +67,24 @@ function Fixture() { return ( { - // // e?.preventDefault() - // console.log('integrator provided a onConnectWallet') - // }} + onClickConnectWallet={() => { + // e?.preventDefault() + console.log('integrator provided a onConnectWallet') + }} /> ) } From f8f7e0efe281516a5cf492acd6b994a43d6cb0d4 Mon Sep 17 00:00:00 2001 From: Kristie Huang Date: Thu, 28 Jul 2022 18:47:04 -0400 Subject: [PATCH 22/44] Update jsonRpcEndpoints --- src/constants/jsonRpcEndpoints.ts | 4 ++-- src/hooks/connectWeb3/useWeb3React.tsx | 16 +++++++--------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/constants/jsonRpcEndpoints.ts b/src/constants/jsonRpcEndpoints.ts index 4eea2696e..1e0bcfe79 100644 --- a/src/constants/jsonRpcEndpoints.ts +++ b/src/constants/jsonRpcEndpoints.ts @@ -4,9 +4,9 @@ import { SupportedChainId } from './chains' * Fallback JSON RPC endpoints if integrator does not provide one */ export const JSON_RPC_FALLBACK_ENDPOINTS: { [chainId: number]: string[] } = { - [SupportedChainId.MAINNET]: ['https://eth-mainnet.public.blastapi.io', 'https://cloudflare-eth.com'], + [SupportedChainId.MAINNET]: ['https://cloudflare-eth.com'], [SupportedChainId.ROPSTEN]: ['https://rpc.ankr.com/eth_ropsten'], - [SupportedChainId.RINKEBY]: ['https://rpc.ankr.com/eth_rinkeby'], + [SupportedChainId.RINKEBY]: ['https://rinkeby-light.eth.linkpool.io/', 'https://rpc.ankr.com/eth_rinkeby'], [SupportedChainId.GOERLI]: ['https://rpc.ankr.com/eth_goerli'], [SupportedChainId.KOVAN]: ['https://eth-kovan.public.blastapi.io', 'https://kovan.poa.network'], [SupportedChainId.POLYGON]: ['https://polygon-rpc.com', 'https://rpc-mainnet.matic.quiknode.pro'], diff --git a/src/hooks/connectWeb3/useWeb3React.tsx b/src/hooks/connectWeb3/useWeb3React.tsx index d698340ff..3181bc4a8 100644 --- a/src/hooks/connectWeb3/useWeb3React.tsx +++ b/src/hooks/connectWeb3/useWeb3React.tsx @@ -74,28 +74,26 @@ export function ActiveWeb3Provider({ [] ) const walletConnectConnectionQR = useMemo( - () => getWalletConnectConnection(false, jsonRpcUrlMap, propsDefaultChainId), - [jsonRpcUrlMap, propsDefaultChainId] + () => getWalletConnectConnection(false, jsonRpcUrlMap, defaultChainId), + [jsonRpcUrlMap, defaultChainId] ) // WC via tile QR code scan const walletConnectConnectionPopup = useMemo( - () => getWalletConnectConnection(true, jsonRpcUrlMap, propsDefaultChainId), - [jsonRpcUrlMap, propsDefaultChainId] + () => getWalletConnectConnection(true, jsonRpcUrlMap, defaultChainId), + [jsonRpcUrlMap, defaultChainId] ) // WC via built-in popup const networkConnection = useMemo( () => toWeb3Connection( - initializeConnector( - (actions) => new Network({ actions, urlMap: jsonRpcUrlMap, defaultChainId: propsDefaultChainId }) - ) + initializeConnector((actions) => new Network({ actions, urlMap: jsonRpcUrlMap, defaultChainId })) ), - [jsonRpcUrlMap, propsDefaultChainId] + [jsonRpcUrlMap, defaultChainId] ) connections = [metaMaskConnection, walletConnectConnectionQR, walletConnectConnectionPopup, networkConnection] if (integratorConnection) connections = [integratorConnection, ...connections] - const key = `${connections.length}+${Object.keys(jsonRpcUrlMap)}+${propsDefaultChainId}` + const key = `${connections.length}+${Object.keys(jsonRpcUrlMap)}+${propsDefaultChainId}+${defaultChainId}` return ( {children} From 2875281b01e646ffe93fcf72983f6a21eaa612c9 Mon Sep 17 00:00:00 2001 From: Kristie Huang Date: Thu, 28 Jul 2022 19:06:48 -0400 Subject: [PATCH 23/44] Update jsonRpcEndpoints to support routing --- src/constants/jsonRpcEndpoints.ts | 8 ++++---- src/cosmos/Swap.fixture.tsx | 34 +++++++++++++++---------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/constants/jsonRpcEndpoints.ts b/src/constants/jsonRpcEndpoints.ts index 1e0bcfe79..c393ac6ca 100644 --- a/src/constants/jsonRpcEndpoints.ts +++ b/src/constants/jsonRpcEndpoints.ts @@ -4,13 +4,13 @@ import { SupportedChainId } from './chains' * Fallback JSON RPC endpoints if integrator does not provide one */ export const JSON_RPC_FALLBACK_ENDPOINTS: { [chainId: number]: string[] } = { - [SupportedChainId.MAINNET]: ['https://cloudflare-eth.com'], + [SupportedChainId.MAINNET]: ['https://rpc.ankr.com/eth', 'https://eth-mainnet.public.blastapi.io'], [SupportedChainId.ROPSTEN]: ['https://rpc.ankr.com/eth_ropsten'], - [SupportedChainId.RINKEBY]: ['https://rinkeby-light.eth.linkpool.io/', 'https://rpc.ankr.com/eth_rinkeby'], + [SupportedChainId.RINKEBY]: ['https://rinkeby-light.eth.linkpool.io/'], [SupportedChainId.GOERLI]: ['https://rpc.ankr.com/eth_goerli'], [SupportedChainId.KOVAN]: ['https://eth-kovan.public.blastapi.io', 'https://kovan.poa.network'], - [SupportedChainId.POLYGON]: ['https://polygon-rpc.com', 'https://rpc-mainnet.matic.quiknode.pro'], - [SupportedChainId.POLYGON_MUMBAI]: ['https://rpc-mumbai.maticvigil.com'], + [SupportedChainId.POLYGON]: ['https://rpc-mainnet.matic.quiknode.pro', 'https://polygon-rpc.com'], + [SupportedChainId.POLYGON_MUMBAI]: ['https://matic-mumbai.chainstacklabs.com'], [SupportedChainId.ARBITRUM_ONE]: ['https://arbitrum.public-rpc.com'], [SupportedChainId.ARBITRUM_RINKEBY]: ['https://rinkeby.arbitrum.io/rpc'], [SupportedChainId.OPTIMISM]: ['https://rpc.ankr.com/optimism'], diff --git a/src/cosmos/Swap.fixture.tsx b/src/cosmos/Swap.fixture.tsx index 3bbc5cab9..97c3442f2 100644 --- a/src/cosmos/Swap.fixture.tsx +++ b/src/cosmos/Swap.fixture.tsx @@ -67,24 +67,24 @@ function Fixture() { return ( { - // e?.preventDefault() - console.log('integrator provided a onConnectWallet') - }} + // provider={connector} + // theme={theme} + // tokenList={tokenList} + // width={width} + // routerUrl={routerUrl} + // onClickConnectWallet={() => { + // // e?.preventDefault() + // console.log('integrator provided a onConnectWallet') + // }} /> ) } From 401566d9abac8ea32772ad10affe12ef023f91aa Mon Sep 17 00:00:00 2001 From: Kristie Huang Date: Mon, 1 Aug 2022 16:50:43 -0400 Subject: [PATCH 24/44] fix: catch errors (#55) * fix: catch errors * fix: move onError to Dialog component * test: update tests for new wcf [wip] * activate network on json change * wip: use more fallback endpoints & use Url instead of Network * style(lint): lint action with ESLint * wip: use mapping * feat: add defaultChainId prop & clean up comments * nit comments * isProvider then/catch * Add defaultChainId when activating wallets * Support optimistic_kovan, squash error bug * add kovanfallback * Add validation for defaultChainId * Prop validation + DRY * Remove string/JsonRpcProvider types * remove useJsonRpcEndpoint in cosmos * Clean up getting IDs from names + dependency arrays * Fallback to free endpoints if not provided * Tidy up array indexing + prop validation * nit * Rename prop to jsonRpcUrlMap * descriptive console.warn * Fix provider key * Revert test changes * PR review * add onError * fix Co-authored-by: Lint Action --- .../ConnectWallet/ConnectWalletDialog.tsx | 25 +++++------ src/components/ConnectWallet/index.tsx | 10 +++-- src/cosmos/Swap.fixture.tsx | 1 + src/hooks/connectWeb3/useWeb3React.tsx | 45 ++++++++++++------- 4 files changed, 48 insertions(+), 33 deletions(-) diff --git a/src/components/ConnectWallet/ConnectWalletDialog.tsx b/src/components/ConnectWallet/ConnectWalletDialog.tsx index 5635a7a48..97eb2efe1 100644 --- a/src/components/ConnectWallet/ConnectWalletDialog.tsx +++ b/src/components/ConnectWallet/ConnectWalletDialog.tsx @@ -71,6 +71,7 @@ interface ButtonProps { logoSrc?: string connection?: Web3Connection onClick: () => void + onError?: (e: Error | undefined) => void } const wcQRUriAtom = atom(undefined) @@ -85,9 +86,8 @@ function toQrCodeSvg(qrUri: string): Promise { }) } -function WalletConnectButton({ walletName, logoSrc, connection: wcTileConnection, onClick }: ButtonProps) { +function WalletConnectButton({ walletName, logoSrc, connection: wcTileConnection, onClick, onError }: ButtonProps) { const [walletConnect] = wcTileConnection as [WalletConnect, Web3ReactHooks] - const [error, setError] = useState(undefined) const defaultChainId = useAtomValue(defaultChainIdAtom) const [qrUri, setQrUri] = useAtom(wcQRUriAtom) @@ -103,24 +103,17 @@ function WalletConnectButton({ walletName, logoSrc, connection: wcTileConnection } else { walletConnect.activate(defaultChainId).catch((e) => { if (stale) return - setError(e) + onError?.(e) }) } return () => { stale = true } - }, [qrUri, walletConnect, defaultChainId]) - - useEffect(() => { - // Log web3 errors - if (error) { - console.error('web3 error:', error) - } - }, [error]) + }, [qrUri, walletConnect, defaultChainId, onError]) useEffect(() => { const disconnectListener = async (err: Error | null, _: any) => { - if (err) console.warn(err) + if (err) onError?.(err) // Clear saved QR URI after disconnection setQrUri(undefined) walletConnect.deactivate() @@ -135,7 +128,7 @@ function WalletConnectButton({ walletName, logoSrc, connection: wcTileConnection }) const uriListener = async (err: Error | null, payload: any) => { - if (err) console.warn(err) + if (err) onError?.(err) const uri: string = payload.params[0] if (uri) { setQrUri(uri) @@ -203,13 +196,14 @@ export function ConnectWalletDialog() { const [mmConnection, wcTileConnection, wcPopupConnection] = defaultConnections const defaultChainId = useAtomValue(defaultChainIdAtom) + const onError = (error: any) => error && console.error('web3 error:', error) const activateWalletConnectPopup = useCallback(() => { const [walletConnectPopup] = wcPopupConnection - walletConnectPopup.activate(defaultChainId) + walletConnectPopup.activate(defaultChainId)?.catch(onError) }, [wcPopupConnection, defaultChainId]) const activateMetaMask = useCallback(() => { const [metamask] = mmConnection - metamask.activate(defaultChainId) + metamask.activate(defaultChainId)?.catch(onError) }, [mmConnection, defaultChainId]) return ( @@ -222,6 +216,7 @@ export function ConnectWalletDialog() { logoSrc={WALLETCONNECT_ICON_URL} connection={wcTileConnection} onClick={activateWalletConnectPopup} + onError={onError} /> diff --git a/src/components/ConnectWallet/index.tsx b/src/components/ConnectWallet/index.tsx index dc6ea8334..ef637538b 100644 --- a/src/components/ConnectWallet/index.tsx +++ b/src/components/ConnectWallet/index.tsx @@ -1,5 +1,5 @@ import { useWeb3React } from '@web3-react/core' -import { connections, defaultChainIdAtom } from 'hooks/connectWeb3/useWeb3React' +import { connections, defaultChainIdAtom, getConnectorName } from 'hooks/connectWeb3/useWeb3React' import { useAtomValue } from 'jotai/utils' import { useEffect } from 'react' @@ -17,8 +17,12 @@ export default function Wallet({ disabled, onClickConnectWallet }: WalletProps) useEffect(() => { connections.forEach(([wallet, _]) => wallet.connectEagerly - ? wallet.connectEagerly(defaultChainId)?.catch((e) => console.log(e)) - : wallet.activate(defaultChainId) + ? wallet.connectEagerly(defaultChainId)?.catch(() => { + console.log('Could not connect eagerly to', getConnectorName(wallet)) + }) + : wallet.activate(defaultChainId)?.catch(() => { + console.log('Could not activate', getConnectorName(wallet)) + }) ) }, [defaultChainId]) diff --git a/src/cosmos/Swap.fixture.tsx b/src/cosmos/Swap.fixture.tsx index 97c3442f2..4bc8bb454 100644 --- a/src/cosmos/Swap.fixture.tsx +++ b/src/cosmos/Swap.fixture.tsx @@ -54,6 +54,7 @@ function Fixture() { defaultValue: 'mainnet', }) const defaultChainId = defaultNetwork ? CHAIN_NAMES_TO_IDS[defaultNetwork] : undefined + const connector = useProvider(defaultChainId) const tokenLists: Record = { diff --git a/src/hooks/connectWeb3/useWeb3React.tsx b/src/hooks/connectWeb3/useWeb3React.tsx index 3181bc4a8..08c7b08d9 100644 --- a/src/hooks/connectWeb3/useWeb3React.tsx +++ b/src/hooks/connectWeb3/useWeb3React.tsx @@ -13,28 +13,39 @@ export type Web3Connection = [Connector, Web3ReactHooks] export let connections: Web3Connection[] = [] export const defaultChainIdAtom = atom(1) -function toWeb3Connection([connector, hooks]: [T, Web3ReactHooks, Web3ReactStore]): [ +function toWeb3Connection([connector, hooks]: [ T, - Web3ReactHooks -] { + Web3ReactHooks, + Web3ReactStore +]): Web3Connection { return [connector, hooks] } -function getWallet(provider?: JsonRpcProvider | Eip1193Provider) { +export function getConnectorName(connector: Connector) { + if (connector instanceof MetaMask) return 'MetaMask' + if (connector instanceof WalletConnect) return 'WalletConnect' + if (connector instanceof Network) return 'Network' + if (connector instanceof Url) return 'Url' + if (connector instanceof EIP1193) return 'EIP1193' + return 'Unknown' +} + +function getWalletFromProvider(onError: (error: Error) => void, provider?: JsonRpcProvider | Eip1193Provider) { if (!provider) return if (JsonRpcProvider.isProvider(provider)) { return toWeb3Connection(initializeConnector((actions) => new Url({ actions, url: provider }))) } else if (JsonRpcProvider.isProvider((provider as any).provider)) { throw new Error('Eip1193Bridge is experimental: pass your ethers Provider directly') } else { - return toWeb3Connection(initializeConnector((actions) => new EIP1193({ actions, provider }))) + return toWeb3Connection(initializeConnector((actions) => new EIP1193({ actions, provider, onError }))) } } -function getWalletConnectConnection( +function getWalletFromWalletConnect( useDefault: boolean, jsonRpcUrlMap: { [chainId: number]: string[] }, - defaultChainId: number + defaultChainId: number, + onError: (error: Error) => void ) { return toWeb3Connection( initializeConnector( @@ -45,6 +56,7 @@ function getWalletConnectConnection( rpc: jsonRpcUrlMap, qrcode: useDefault, }, + onError, defaultChainId, }) ) @@ -63,29 +75,32 @@ export function ActiveWeb3Provider({ defaultChainId: propsDefaultChainId, children, }: PropsWithChildren) { + const onError = console.error const [defaultChainId, setDefaultChainId] = useAtom(defaultChainIdAtom) useEffect(() => { if (propsDefaultChainId !== defaultChainId) setDefaultChainId(propsDefaultChainId) }, [propsDefaultChainId, defaultChainId, setDefaultChainId]) - const integratorConnection = useMemo(() => getWallet(provider), [provider]) + const integratorConnection = useMemo(() => getWalletFromProvider(onError, provider), [onError, provider]) const metaMaskConnection = useMemo( - () => toWeb3Connection(initializeConnector((actions) => new MetaMask({ actions }))), - [] + () => toWeb3Connection(initializeConnector((actions) => new MetaMask({ actions, onError }))), + [onError] ) const walletConnectConnectionQR = useMemo( - () => getWalletConnectConnection(false, jsonRpcUrlMap, defaultChainId), - [jsonRpcUrlMap, defaultChainId] + () => getWalletFromWalletConnect(false, jsonRpcUrlMap, propsDefaultChainId, onError), + [jsonRpcUrlMap, propsDefaultChainId, onError] ) // WC via tile QR code scan const walletConnectConnectionPopup = useMemo( - () => getWalletConnectConnection(true, jsonRpcUrlMap, defaultChainId), - [jsonRpcUrlMap, defaultChainId] + () => getWalletFromWalletConnect(true, jsonRpcUrlMap, propsDefaultChainId, onError), + [jsonRpcUrlMap, propsDefaultChainId, onError] ) // WC via built-in popup const networkConnection = useMemo( () => toWeb3Connection( - initializeConnector((actions) => new Network({ actions, urlMap: jsonRpcUrlMap, defaultChainId })) + initializeConnector( + (actions) => new Network({ actions, urlMap: jsonRpcUrlMap, defaultChainId: propsDefaultChainId }) + ) ), [jsonRpcUrlMap, defaultChainId] ) From 2184784a7d844ea4aadae3cd6071491a737d8c37 Mon Sep 17 00:00:00 2001 From: Kristie Huang Date: Mon, 1 Aug 2022 16:52:25 -0400 Subject: [PATCH 25/44] Use JsonRpcConnector built-in for custom wallet provider --- src/hooks/connectWeb3/useWeb3React.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/hooks/connectWeb3/useWeb3React.tsx b/src/hooks/connectWeb3/useWeb3React.tsx index 08c7b08d9..6835d8dda 100644 --- a/src/hooks/connectWeb3/useWeb3React.tsx +++ b/src/hooks/connectWeb3/useWeb3React.tsx @@ -4,10 +4,10 @@ import { EIP1193 } from '@web3-react/eip1193' import { MetaMask } from '@web3-react/metamask' import { Network } from '@web3-react/network' import { Connector, Provider as Eip1193Provider, Web3ReactStore } from '@web3-react/types' -import { Url } from '@web3-react/url' import { WalletConnect } from '@web3-react/walletconnect' import { atom, useAtom } from 'jotai' import { PropsWithChildren, useEffect, useMemo } from 'react' +import JsonRpcConnector from 'utils/JsonRpcConnector' export type Web3Connection = [Connector, Web3ReactHooks] export let connections: Web3Connection[] = [] @@ -25,7 +25,7 @@ export function getConnectorName(connector: Connector) { if (connector instanceof MetaMask) return 'MetaMask' if (connector instanceof WalletConnect) return 'WalletConnect' if (connector instanceof Network) return 'Network' - if (connector instanceof Url) return 'Url' + if (connector instanceof JsonRpcConnector) return 'JsonRpcConnector' if (connector instanceof EIP1193) return 'EIP1193' return 'Unknown' } @@ -33,7 +33,7 @@ export function getConnectorName(connector: Connector) { function getWalletFromProvider(onError: (error: Error) => void, provider?: JsonRpcProvider | Eip1193Provider) { if (!provider) return if (JsonRpcProvider.isProvider(provider)) { - return toWeb3Connection(initializeConnector((actions) => new Url({ actions, url: provider }))) + return toWeb3Connection(initializeConnector((actions) => new JsonRpcConnector(actions, provider))) } else if (JsonRpcProvider.isProvider((provider as any).provider)) { throw new Error('Eip1193Bridge is experimental: pass your ethers Provider directly') } else { @@ -102,7 +102,7 @@ export function ActiveWeb3Provider({ (actions) => new Network({ actions, urlMap: jsonRpcUrlMap, defaultChainId: propsDefaultChainId }) ) ), - [jsonRpcUrlMap, defaultChainId] + [jsonRpcUrlMap, propsDefaultChainId] ) connections = [metaMaskConnection, walletConnectConnectionQR, walletConnectConnectionPopup, networkConnection] From 3dbfcc9df36f4ff70a8b8a9ff0c2dd99a913db86 Mon Sep 17 00:00:00 2001 From: Kristie Huang Date: Mon, 1 Aug 2022 16:57:28 -0400 Subject: [PATCH 26/44] revert swap.fixture --- src/cosmos/Swap.fixture.tsx | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/cosmos/Swap.fixture.tsx b/src/cosmos/Swap.fixture.tsx index 4bc8bb454..3f798f117 100644 --- a/src/cosmos/Swap.fixture.tsx +++ b/src/cosmos/Swap.fixture.tsx @@ -68,24 +68,24 @@ function Fixture() { return ( { - // // e?.preventDefault() - // console.log('integrator provided a onConnectWallet') - // }} + provider={connector} + theme={theme} + tokenList={tokenList} + width={width} + routerUrl={routerUrl} + onClickConnectWallet={() => { + // e?.preventDefault() + console.log('integrator provided a onConnectWallet') + }} /> ) } From 24ce4f9bc161c54dfd086dbbb23b22f55191ba42 Mon Sep 17 00:00:00 2001 From: Kristie Huang Date: Mon, 1 Aug 2022 16:58:28 -0400 Subject: [PATCH 27/44] Rename onClickConnectWallet to onConnectWalletClick --- src/components/ConnectWallet/index.tsx | 6 +++--- src/components/Swap/index.tsx | 4 ++-- src/cosmos/Swap.fixture.tsx | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/ConnectWallet/index.tsx b/src/components/ConnectWallet/index.tsx index ef637538b..06e5cf639 100644 --- a/src/components/ConnectWallet/index.tsx +++ b/src/components/ConnectWallet/index.tsx @@ -8,10 +8,10 @@ import ConnectWallet from './ConnectWallet' interface WalletProps { disabled?: boolean - onClickConnectWallet?: (e?: React.MouseEvent) => void + onConnectWalletClick?: (e?: React.MouseEvent) => void } -export default function Wallet({ disabled, onClickConnectWallet }: WalletProps) { +export default function Wallet({ disabled, onConnectWalletClick }: WalletProps) { // Attempt to connect eagerly on mount, and prompt switch networks when integrator's defaultChainId changes const defaultChainId = useAtomValue(defaultChainIdAtom) useEffect(() => { @@ -32,6 +32,6 @@ export default function Wallet({ disabled, onClickConnectWallet }: WalletProps) return isConnected ? ( ) : ( - + ) } diff --git a/src/components/Swap/index.tsx b/src/components/Swap/index.tsx index 511ede9a7..52ffe984f 100644 --- a/src/components/Swap/index.tsx +++ b/src/components/Swap/index.tsx @@ -47,7 +47,7 @@ function getTransactionFromMap( export interface SwapProps extends TokenDefaults, FeeOptions { provider?: Eip1193Provider | JsonRpcProvider routerUrl?: string - onClickConnectWallet?: (e?: React.MouseEvent) => void + onConnectWalletClick?: (e?: React.MouseEvent) => void } export default function Swap(props: SwapProps) { @@ -72,7 +72,7 @@ export default function Swap(props: SwapProps) { return ( <>
Swap}> - +
diff --git a/src/cosmos/Swap.fixture.tsx b/src/cosmos/Swap.fixture.tsx index 3f798f117..fc74e5042 100644 --- a/src/cosmos/Swap.fixture.tsx +++ b/src/cosmos/Swap.fixture.tsx @@ -82,9 +82,9 @@ function Fixture() { tokenList={tokenList} width={width} routerUrl={routerUrl} - onClickConnectWallet={() => { + onConnectWalletClick={() => { // e?.preventDefault() - console.log('integrator provided a onConnectWallet') + console.log('integrator provided a onConnectWalletClick') }} /> ) From 92b4894eb8fc27ac43d722c9bf1cc126c9787980 Mon Sep 17 00:00:00 2001 From: Kristie Huang Date: Tue, 2 Aug 2022 12:18:32 -0400 Subject: [PATCH 28/44] Address PR reviews --- package.json | 8 ++--- .../ConnectWallet/ConnectWalletDialog.tsx | 35 +++++++------------ .../ConnectWallet/ConnectedWalletChip.tsx | 12 ++----- src/components/ConnectWallet/index.tsx | 4 +-- src/cosmos/Swap.fixture.tsx | 2 +- src/cosmos/useProvider.ts | 2 +- src/hooks/connectWeb3/useWeb3React.tsx | 10 +++--- 7 files changed, 28 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index e966ede34..2657de307 100644 --- a/package.json +++ b/package.json @@ -67,11 +67,14 @@ "@uniswap/token-lists": "^1.0.0-beta.30", "@uniswap/v2-sdk": "^3.0.1", "@uniswap/v3-sdk": "^3.8.2", - "@web3-react/core" : "8.0.35-beta.0", + "@web3-react/core": "8.0.35-beta.0", "@web3-react/eip1193": "8.0.26-beta.0", "@web3-react/empty": "8.0.20-beta.0", + "@web3-react/metamask": "8.0.27-beta.0", + "@web3-react/network": "8.0.27-beta.0", "@web3-react/types": "8.0.20-beta.0", "@web3-react/url": "8.0.25-beta.0", + "@web3-react/walletconnect": "8.0.35-beta.0", "ajv": "^6.12.3", "cids": "^1.0.0", "ethers": "^5.1.4", @@ -156,9 +159,6 @@ "@uniswap/v3-core": "1.0.0", "@uniswap/v3-periphery": "^1.1.1", "@walletconnect/ethereum-provider": "^1.7.1", - "@web3-react/metamask": "8.0.27-beta.0", - "@web3-react/network": "8.0.27-beta.0", - "@web3-react/walletconnect": "8.0.35-beta.0", "babel-jest": "^27.5.1", "babel-loader": "^8.2.5", "babel-plugin-macros": "^3.1.0", diff --git a/src/components/ConnectWallet/ConnectWalletDialog.tsx b/src/components/ConnectWallet/ConnectWalletDialog.tsx index 97eb2efe1..027b3233a 100644 --- a/src/components/ConnectWallet/ConnectWalletDialog.tsx +++ b/src/components/ConnectWallet/ConnectWalletDialog.tsx @@ -18,6 +18,9 @@ import { useCallback, useEffect, useState } from 'react' import styled from 'styled-components/macro' import { lightTheme, ThemedText } from 'theme' +const NO_WALLET_HELP_CENTER_URL = 'https://help.uniswap.org/en/articles/5391585-how-to-get-a-wallet' +const onError = (error: Error) => console.error('web3 error:', error) + const Body = styled(Column)` height: calc(100% - 2.5em); ` @@ -71,7 +74,6 @@ interface ButtonProps { logoSrc?: string connection?: Web3Connection onClick: () => void - onError?: (e: Error | undefined) => void } const wcQRUriAtom = atom(undefined) @@ -86,7 +88,7 @@ function toQrCodeSvg(qrUri: string): Promise { }) } -function WalletConnectButton({ walletName, logoSrc, connection: wcTileConnection, onClick, onError }: ButtonProps) { +function WalletConnectButton({ walletName, logoSrc, connection: wcTileConnection, onClick }: ButtonProps) { const [walletConnect] = wcTileConnection as [WalletConnect, Web3ReactHooks] const defaultChainId = useAtomValue(defaultChainIdAtom) @@ -109,7 +111,7 @@ function WalletConnectButton({ walletName, logoSrc, connection: wcTileConnection return () => { stale = true } - }, [qrUri, walletConnect, defaultChainId, onError]) + }, [qrUri, walletConnect, defaultChainId]) useEffect(() => { const disconnectListener = async (err: Error | null, _: any) => { @@ -147,9 +149,7 @@ function WalletConnectButton({ walletName, logoSrc, connection: wcTileConnection {walletName} - - {walletName} - + {walletName} Scan to connect your wallet. Works with most wallets. @@ -165,38 +165,30 @@ function MetaMaskButton({ walletName, logoSrc, onClick }: ButtonProps) { {walletName} - - {walletName} - + {walletName} ) } function NoWalletButton() { - const helpCenterUrl = 'https://help.uniswap.org/en/articles/5391585-how-to-get-a-wallet' return ( - window.open(helpCenterUrl)}> + window.open(NO_WALLET_HELP_CENTER_URL)}> - I don't have a wallet + {`I don't have a wallet`} ) } export function ConnectWalletDialog() { - let defaultConnections: Web3Connection[] const [firstConnector] = connections[0] - if (firstConnector instanceof EIP1193 || firstConnector instanceof Url) { - // If first connector is the integrator-provided connector - defaultConnections = connections.slice(1) - } else { - defaultConnections = connections - } - const [mmConnection, wcTileConnection, wcPopupConnection] = defaultConnections + const integratorProvidedConnector = firstConnector instanceof EIP1193 || firstConnector instanceof Url + const [mmConnection, wcTileConnection, wcPopupConnection]: Web3Connection[] = integratorProvidedConnector + ? connections.slice(1) + : connections const defaultChainId = useAtomValue(defaultChainIdAtom) - const onError = (error: any) => error && console.error('web3 error:', error) const activateWalletConnectPopup = useCallback(() => { const [walletConnectPopup] = wcPopupConnection walletConnectPopup.activate(defaultChainId)?.catch(onError) @@ -216,7 +208,6 @@ export function ConnectWalletDialog() { logoSrc={WALLETCONNECT_ICON_URL} connection={wcTileConnection} onClick={activateWalletConnectPopup} - onError={onError} /> diff --git a/src/components/ConnectWallet/ConnectedWalletChip.tsx b/src/components/ConnectWallet/ConnectedWalletChip.tsx index aad23a9f7..d49e20c41 100644 --- a/src/components/ConnectWallet/ConnectedWalletChip.tsx +++ b/src/components/ConnectWallet/ConnectedWalletChip.tsx @@ -23,21 +23,13 @@ export default function ConnectedWalletChip({ disabled }: { disabled?: boolean } // TODO: hover to see disconnect button is temporary; disconnection should live inside AccountDialog const [hover, setHover] = useState(false) - const { account } = useWeb3React() - function disconnectWallet() { - connections.forEach(([wallet, _]) => { - if (!(wallet instanceof Network || wallet instanceof Url)) { - // only deactivate non-network wallet connectors - wallet.deactivate ? wallet.deactivate() : wallet.resetState() - } - }) - } + const { account, connector } = useWeb3React() return ( <>