diff --git a/src/cow-react/modules/wallet/web3-react/connection/coinbase.tsx b/src/cow-react/modules/wallet/web3-react/connection/coinbase.tsx index 8be5384fbb..40eb4dcece 100644 --- a/src/cow-react/modules/wallet/web3-react/connection/coinbase.tsx +++ b/src/cow-react/modules/wallet/web3-react/connection/coinbase.tsx @@ -9,7 +9,7 @@ import { useIsActiveWallet } from 'hooks/useIsActiveWallet' import { ConnectWalletOption } from '@cow/modules/wallet/api/pure/ConnectWalletOption' import CowImage from 'assets/cow-swap/cow_v2.svg' -import { RPC_URLS } from 'constants/networks' +import { getRpcUrls } from 'constants/networks' import { TryActivation, onError } from '.' import { Web3ReactConnection } from '../types' @@ -38,7 +38,7 @@ const [web3CoinbaseWallet, web3CoinbaseWalletHooks] = initializeConnector( return new m.Ledger({ actions, options: { - rpc: RPC_URLS, + rpc: PROVIDERS_RPC_URLS_FIRST_ONLY, }, kit, }) diff --git a/src/cow-react/modules/wallet/web3-react/connection/network.tsx b/src/cow-react/modules/wallet/web3-react/connection/network.tsx index c24d8d4901..18a2f5d1be 100644 --- a/src/cow-react/modules/wallet/web3-react/connection/network.tsx +++ b/src/cow-react/modules/wallet/web3-react/connection/network.tsx @@ -1,12 +1,12 @@ import { initializeConnector } from '@web3-react/core' import { Network } from '@web3-react/network' -import { RPC_URLS } from 'constants/networks' import { ConnectionType } from '@cow/modules/wallet' import { Web3ReactConnection } from '../types' +import { PROVIDERS } from '@src/custom/constants/networks' const [web3Network, web3NetworkHooks] = initializeConnector( - (actions) => new Network({ actions, urlMap: RPC_URLS, defaultChainId: 1 }) + (actions) => new Network({ actions, urlMap: PROVIDERS }) ) export const networkConnection: Web3ReactConnection = { connector: web3Network, diff --git a/src/cow-react/modules/wallet/web3-react/connection/walletConnect.tsx b/src/cow-react/modules/wallet/web3-react/connection/walletConnect.tsx index 8d7ac5607d..bbf9ea649f 100644 --- a/src/cow-react/modules/wallet/web3-react/connection/walletConnect.tsx +++ b/src/cow-react/modules/wallet/web3-react/connection/walletConnect.tsx @@ -13,7 +13,7 @@ import { useWalletMetaData } from '@cow/modules/wallet' import { initializeConnector } from '@web3-react/core' import { WalletConnect } from '@web3-react/walletconnect' -import { RPC_URLS } from 'constants/networks' +import { PROVIDERS_RPC_URLS } from 'constants/networks' import { Web3ReactConnection } from '../types' import { default as WalletConnectImage } from '@cow/modules/wallet/api/assets/walletConnectIcon.svg' import { WC_DISABLED_TEXT } from '@cow/modules/wallet/constants' @@ -29,7 +29,7 @@ const [web3WalletConnect, web3WalletConnectHooks] = initializeConnector { if (!isChainAllowed(connector, chainId)) { throw new Error(`Chain ${chainId} not supported for connector (${typeof connector})`) diff --git a/src/cow-react/types.ts b/src/cow-react/types.ts index 00f51f885c..b99f9f9daa 100644 --- a/src/cow-react/types.ts +++ b/src/cow-react/types.ts @@ -8,6 +8,16 @@ export type Writeable = { -readonly [P in keyof T]: T[P] } export type Nullish = T | null | undefined +/* + A generic class type. + + You can use this to refer to a class and not an instance of it. + More info at: https://www.typescriptlang.org/docs/handbook/2/generics.html#using-class-types-in-generics +*/ +export type Newable any> = { + new (...args: ConstructorParameters): InstanceType +} + // This is for Pixel tracking injected code declare global { interface Window { diff --git a/src/custom/constants/networks.ts b/src/custom/constants/networks.ts index 8e51582093..0178991bd1 100644 --- a/src/custom/constants/networks.ts +++ b/src/custom/constants/networks.ts @@ -1,30 +1,146 @@ -import { JsonRpcProvider } from '@ethersproject/providers' +import { + Network, + Web3Provider, + JsonRpcProvider, + UrlJsonRpcProvider, + StaticJsonRpcProvider, + InfuraProvider, + CloudflareProvider, + AlchemyProvider, + PocketProvider, + AnkrProvider, +} from '@ethersproject/providers' +import * as Sentry from '@sentry/react' import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { Newable } from '@cow/types' const INFURA_KEY = process.env.REACT_APP_INFURA_KEY if (typeof INFURA_KEY === 'undefined') { throw new Error(`REACT_APP_INFURA_KEY must be a defined environment variable`) } -export const MAINNET_PROVIDER = new JsonRpcProvider(`https://mainnet.infura.io/v3/${INFURA_KEY}`) +interface RpcConfig extends Network { + // You should only need to provide this if you are trying to connect to a custom RPC / network. + readonly rpcUrl?: string +} + +const STATIC_RPC_URLS: Partial> = { + [SupportedChainId.GNOSIS_CHAIN]: 'https://rpc.gnosis.gateway.fm', +} + +export const RPC_CONFIG: Record = { + [SupportedChainId.MAINNET]: { + chainId: SupportedChainId.MAINNET, + name: 'homestead', + }, + [SupportedChainId.GOERLI]: { + chainId: SupportedChainId.GOERLI, + name: 'goerli', + }, + [SupportedChainId.GNOSIS_CHAIN]: { + chainId: SupportedChainId.GNOSIS_CHAIN, + name: 'gnosis', + rpcUrl: STATIC_RPC_URLS[SupportedChainId.GNOSIS_CHAIN], + }, +} /** - * These are the network URLs used by the interface when there is not another available source of chain data + * We use this class for type inference. + * + * UrlJsonRpcProvider is an abstract class, therefore we have trouble inferring the type of its constructor. + * However, as all of the RPC providers we use extend UrlJsonRpcProvider, we can use this class to infer the type of the constructor, by turning it into a concrete one. */ -export const RPC_URLS: { [key in SupportedChainId]: string } = { - [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.CELO]: `https://forno.celo.org`, - // [SupportedChainId.CELO_ALFAJORES]: `https://alfajores-forno.celo-testnet.org`, - [SupportedChainId.GNOSIS_CHAIN]: `https://rpc.gnosis.gateway.fm`, +class BaseUrlJsonRpcProvider extends UrlJsonRpcProvider {} +type ProviderClass = Newable + +// List of public providers that don't require an API key, per chain support. +const PUBLIC_PROVIDER_LIST: Record = { + [SupportedChainId.MAINNET]: [InfuraProvider, CloudflareProvider, AlchemyProvider, PocketProvider, AnkrProvider], + [SupportedChainId.GOERLI]: [InfuraProvider, AlchemyProvider, PocketProvider, AnkrProvider], + [SupportedChainId.GNOSIS_CHAIN]: [], +} + +function getProvider(chainId: SupportedChainId): JsonRpcProvider[] { + const result: JsonRpcProvider[] = [] + + if (window.ethereum) { + result.push(new Web3Provider(window.ethereum, RPC_CONFIG[chainId])) + } + + if (typeof RPC_CONFIG[chainId].rpcUrl === 'string') { + result.push(new StaticJsonRpcProvider(RPC_CONFIG[chainId].rpcUrl)) + } else { + result.push(new InfuraProvider(RPC_CONFIG[chainId], INFURA_KEY)) + } + + if (result.length > 0) { + return result + } + + for (const Provider of PUBLIC_PROVIDER_LIST[chainId]) { + try { + const provider = new Provider(RPC_CONFIG[chainId]) + + result.push(provider) + } catch (error) { + const { sentryError, tags } = constructSentryError(error, { message: "Couldn't create public provider" }) + + Sentry.captureException(sentryError, { tags }) + } + } + + return result +} + +export const PROVIDERS: Record = { + [SupportedChainId.MAINNET]: getProvider(SupportedChainId.MAINNET), + [SupportedChainId.GOERLI]: getProvider(SupportedChainId.GOERLI), + [SupportedChainId.GNOSIS_CHAIN]: getProvider(SupportedChainId.GNOSIS_CHAIN), +} + +export const getRpcUrls = (chainId: SupportedChainId) => PROVIDERS[chainId].map((provider) => provider.connection.url) + +export const PROVIDERS_RPC_URLS: Record = { + [SupportedChainId.MAINNET]: getRpcUrls(SupportedChainId.MAINNET), + [SupportedChainId.GOERLI]: getRpcUrls(SupportedChainId.GOERLI), + [SupportedChainId.GNOSIS_CHAIN]: getRpcUrls(SupportedChainId.GNOSIS_CHAIN), +} + +export const PROVIDERS_RPC_URLS_FIRST_ONLY: Record = { + [SupportedChainId.MAINNET]: PROVIDERS_RPC_URLS[SupportedChainId.MAINNET][0], + [SupportedChainId.GOERLI]: PROVIDERS_RPC_URLS[SupportedChainId.GOERLI][0], + [SupportedChainId.GNOSIS_CHAIN]: PROVIDERS_RPC_URLS[SupportedChainId.GNOSIS_CHAIN][0], +} + +export const providers = { + get mainnet() { + const _providers = getProvider(SupportedChainId.MAINNET) + + if (_providers.length > 0) { + return _providers[0] + } else { + const message = 'No provider found for MAINNET_PROVIDER' + const { sentryError, tags } = constructSentryError(new Error(), { + message, + }) + + Sentry.captureException(sentryError, { tags }) + + throw new Error(message) + } + }, +} + +function constructSentryError(baseError: unknown, { message }: { message: string }) { + const constructedError = Object.assign(new Error(), baseError, { + message, + name: 'ProviderError', + }) + + const tags = { + errorType: 'provider', + } + + return { baseError, sentryError: constructedError, tags } } diff --git a/src/custom/hooks/useFetchListCallback/useFetchListCallbackMod.ts b/src/custom/hooks/useFetchListCallback/useFetchListCallbackMod.ts index 77f566afcb..137429bf18 100644 --- a/src/custom/hooks/useFetchListCallback/useFetchListCallbackMod.ts +++ b/src/custom/hooks/useFetchListCallback/useFetchListCallbackMod.ts @@ -1,7 +1,7 @@ import { useWalletInfo } from '@cow/modules/wallet' import { nanoid } from '@reduxjs/toolkit' import { TokenList } from '@uniswap/token-lists' -import { MAINNET_PROVIDER } from 'constants/networks' +import { providers } from 'constants/networks' import getTokenList from 'lib/hooks/useTokenList/fetchTokenList' import resolveENSContentHash from 'lib/utils/resolveENSContentHash' import { useCallback } from 'react' @@ -21,7 +21,7 @@ export function useFetchListCallback(): (listUrl: string, sendDispatch?: boolean const requestId = nanoid() // Mod: add chainId sendDispatch && dispatch(fetchTokenList.pending({ requestId, url: listUrl, chainId })) - return getTokenList(listUrl, (ensName: string) => resolveENSContentHash(ensName, MAINNET_PROVIDER)) + return getTokenList(listUrl, (ensName: string) => resolveENSContentHash(ensName, providers.mainnet)) .then((tokenList) => { // Mod: add chainId sendDispatch && dispatch(fetchTokenList.fulfilled({ url: listUrl, tokenList, requestId, chainId })) diff --git a/src/hooks/useFetchListCallback.ts b/src/hooks/useFetchListCallback.ts index 397deed095..7934fdfc3d 100644 --- a/src/hooks/useFetchListCallback.ts +++ b/src/hooks/useFetchListCallback.ts @@ -1,6 +1,6 @@ import { nanoid } from '@reduxjs/toolkit' import { TokenList } from '@uniswap/token-lists' -import { MAINNET_PROVIDER } from 'constants/networks' +import { providers } from 'constants/networks' import getTokenList from 'lib/hooks/useTokenList/fetchTokenList' import resolveENSContentHash from 'lib/utils/resolveENSContentHash' import { useCallback } from 'react' @@ -16,7 +16,7 @@ export function useFetchListCallback(): (listUrl: string, sendDispatch?: boolean async (listUrl: string, sendDispatch = true) => { const requestId = nanoid() sendDispatch && dispatch(fetchTokenList.pending({ requestId, url: listUrl })) - return getTokenList(listUrl, (ensName: string) => resolveENSContentHash(ensName, MAINNET_PROVIDER)) + return getTokenList(listUrl, (ensName: string) => resolveENSContentHash(ensName, providers.mainnet)) .then((tokenList) => { sendDispatch && dispatch(fetchTokenList.fulfilled({ url: listUrl, tokenList, requestId })) return tokenList diff --git a/src/state/routing/slice.ts b/src/state/routing/slice.ts index 8f41c0e0aa..1f67993ca0 100644 --- a/src/state/routing/slice.ts +++ b/src/state/routing/slice.ts @@ -1,8 +1,8 @@ -import { BaseProvider, JsonRpcProvider } from '@ethersproject/providers' +import { BaseProvider, Web3Provider } from '@ethersproject/providers' import { createApi, fetchBaseQuery, FetchBaseQueryError } from '@reduxjs/toolkit/query/react' import { Protocol } from '@uniswap/router-sdk' import { ChainId } from '@uniswap/smart-order-router' -import { RPC_URLS } from 'constants/networks' +import { PROVIDERS } from 'constants/networks' import { getClientSideQuote, toSupportedChainId } from 'lib/hooks/routing/clientSideSmartOrderRouter' import ms from 'ms.macro' import qs from 'qs' @@ -16,7 +16,7 @@ function getRouterProvider(chainId: ChainId): BaseProvider { const supportedChainId = toSupportedChainId(chainId) if (supportedChainId) { - const provider = new JsonRpcProvider(RPC_URLS[supportedChainId]) + const provider = PROVIDERS[supportedChainId] routerProviders.set(chainId, provider) return provider }