feat: new app provider w/ fallback behavior (#7205)

* feat: new app provider w/ fallback behavior

* progress

* update useContractX signer params

* Revert "update useContractX signer params"

This reverts commit 386d1580dff77338810a4b31be38efd039d4b138.

* extend jsonrpcprovider

* add mainnet quicknode example, use old staticJsonRpc extension

* add tests

* unit testing

* fixes to tests/tsc

* Update src/state/routing/gas.ts

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* pr review

* e2e tests should only talk to the chain via connected wallet

* Revert "e2e tests should only talk to the chain via connected wallet"

This reverts commit 0ce76eb7e4132917a52b49531db871a189448e51.

* add charlie's null nit

* fix e2e

* add feature flag

* Update cypress/support/setupTests.ts

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* pr review

* pr feedback

* fix tests

* add generic send test

* fix merge error

* add a failure rate calculation and inline comments on scoring algo w/ an example

* fix sort test

* cleaner provider creation

* simplify sort

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
This commit is contained in:
Jordan Frankfurt 2023-09-22 14:05:27 -05:00 committed by GitHub
parent 2dc5a6efb4
commit 809841df0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 474 additions and 131 deletions

1
.env

@ -5,6 +5,7 @@ REACT_APP_AWS_API_REGION="us-east-2"
REACT_APP_AWS_API_ENDPOINT="https://beta.api.uniswap.org/v1/graphql" REACT_APP_AWS_API_ENDPOINT="https://beta.api.uniswap.org/v1/graphql"
REACT_APP_BNB_RPC_URL="https://rough-sleek-hill.bsc.quiknode.pro/413cc98cbc776cda8fdf1d0f47003583ff73d9bf" REACT_APP_BNB_RPC_URL="https://rough-sleek-hill.bsc.quiknode.pro/413cc98cbc776cda8fdf1d0f47003583ff73d9bf"
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847" REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
REACT_APP_QUICKNODE_MAINNET_RPC_URL="https://magical-alien-tab.quiknode.pro/669e87e569a8277d3fbd9e202f9df93189f19f4c"
REACT_APP_MOONPAY_API="https://api.moonpay.com" REACT_APP_MOONPAY_API="https://api.moonpay.com"
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkV2?platform=web&env=staging" REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkV2?platform=web&env=staging"
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_test_DycfESRid31UaSxhI5yWKe1r5E5kKSz" REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_test_DycfESRid31UaSxhI5yWKe1r5E5kKSz"

@ -12,4 +12,5 @@ REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_live_uQG4BJC4w3cxnqpcSqAfohdBFDTsY6E"
REACT_APP_SENTRY_ENABLED=true REACT_APP_SENTRY_ENABLED=true
REACT_APP_SENTRY_TRACES_SAMPLE_RATE=0.00003 REACT_APP_SENTRY_TRACES_SAMPLE_RATE=0.00003
REACT_APP_STATSIG_PROXY_URL="https://api.uniswap.org/v1/statsig-proxy" REACT_APP_STATSIG_PROXY_URL="https://api.uniswap.org/v1/statsig-proxy"
REACT_APP_QUICKNODE_MAINNET_RPC_URL="https://ultra-blue-flower.quiknode.pro/770b22d5f362c537bc8fe19b034c45b22958f880"
THE_GRAPH_SCHEMA_ENDPOINT="https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3?source=uniswap" THE_GRAPH_SCHEMA_ENDPOINT="https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3?source=uniswap"

@ -1,27 +0,0 @@
import { SwapEventName } from '@uniswap/analytics-events'
import { USDC_MAINNET } from 'constants/tokens'
import { getTestSelector } from '../../utils'
describe('Swap inputs with no wallet connected', () => {
it('can input and load a quote with no wallet connected', () => {
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
cy.get(getTestSelector('web3-status-connected')).click()
// click twice, first time to show confirmation, second to confirm
cy.get(getTestSelector('wallet-disconnect')).click()
cy.get(getTestSelector('wallet-disconnect')).should('contain', 'Disconnect')
cy.get(getTestSelector('wallet-disconnect')).click()
cy.get(getTestSelector('close-account-drawer')).click()
// Enter amount to swap
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
// Verify logging
cy.waitForAmplitudeEvent(SwapEventName.SWAP_QUOTE_RECEIVED).then((event: any) => {
cy.wrap(event.event_properties).should('have.property', 'quote_latency_milliseconds')
cy.wrap(event.event_properties.quote_latency_milliseconds).should('be.a', 'number')
cy.wrap(event.event_properties.quote_latency_milliseconds).should('be.gte', 0)
})
})
})

@ -9,8 +9,9 @@ beforeEach(() => {
req.headers['origin'] = 'https://app.uniswap.org' req.headers['origin'] = 'https://app.uniswap.org'
}) })
// Infura is disabled for cypress tests - calls should be routed through the connected wallet instead. // Network RPCs are disabled for cypress tests - calls should be routed through the connected wallet instead.
cy.intercept(/infura.io/, { statusCode: 404 }) cy.intercept(/infura.io/, { statusCode: 404 })
cy.intercept(/quiknode.pro/, { statusCode: 404 })
// Log requests to hardhat. // Log requests to hardhat.
cy.intercept(/:8545/, logJsonRpc) cy.intercept(/:8545/, logJsonRpc)

@ -9,8 +9,9 @@ import MulticallJSON from '@uniswap/v3-periphery/artifacts/contracts/lens/Uniswa
import NFTPositionManagerJSON from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json' import NFTPositionManagerJSON from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { isSupportedChain } from 'constants/chains' import { isSupportedChain } from 'constants/chains'
import { RPC_PROVIDERS } from 'constants/providers' import { DEPRECATED_RPC_PROVIDERS, RPC_PROVIDERS } from 'constants/providers'
import { BaseContract } from 'ethers/lib/ethers' import { BaseContract } from 'ethers/lib/ethers'
import { useFallbackProviderEnabled } from 'featureFlags/flags/fallbackProvider'
import { ContractInput, useUniswapPricesQuery } from 'graphql/data/__generated__/types-and-hooks' import { ContractInput, useUniswapPricesQuery } from 'graphql/data/__generated__/types-and-hooks'
import { toContractInput } from 'graphql/data/util' import { toContractInput } from 'graphql/data/util'
import useStablecoinPrice from 'hooks/useStablecoinPrice' import useStablecoinPrice from 'hooks/useStablecoinPrice'
@ -31,6 +32,8 @@ function useContractMultichain<T extends BaseContract>(
): ContractMap<T> { ): ContractMap<T> {
const { chainId: walletChainId, provider: walletProvider } = useWeb3React() const { chainId: walletChainId, provider: walletProvider } = useWeb3React()
const networkProviders = useFallbackProviderEnabled() ? RPC_PROVIDERS : DEPRECATED_RPC_PROVIDERS
return useMemo(() => { return useMemo(() => {
const relevantChains = const relevantChains =
chainIds ?? chainIds ??
@ -43,14 +46,14 @@ function useContractMultichain<T extends BaseContract>(
walletProvider && walletChainId === chainId walletProvider && walletChainId === chainId
? walletProvider ? walletProvider
: isSupportedChain(chainId) : isSupportedChain(chainId)
? RPC_PROVIDERS[chainId] ? networkProviders[chainId]
: undefined : undefined
if (provider) { if (provider) {
acc[chainId] = getContract(addressMap[chainId] ?? '', ABI, provider) as T acc[chainId] = getContract(addressMap[chainId] ?? '', ABI, provider) as T
} }
return acc return acc
}, {}) }, {})
}, [ABI, addressMap, chainIds, walletChainId, walletProvider]) }, [ABI, addressMap, chainIds, networkProviders, walletChainId, walletProvider])
} }
export function useV3ManagerContracts(chainIds: ChainId[]): ContractMap<NonfungiblePositionManager> { export function useV3ManagerContracts(chainIds: ChainId[]): ContractMap<NonfungiblePositionManager> {

@ -1,6 +1,7 @@
import Column from 'components/Column' import Column from 'components/Column'
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags' import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
import { useCurrencyConversionFlag } from 'featureFlags/flags/currencyConversion' import { useCurrencyConversionFlag } from 'featureFlags/flags/currencyConversion'
import { useFallbackProviderEnabledFlag } from 'featureFlags/flags/fallbackProvider'
import { useFotAdjustmentsFlag } from 'featureFlags/flags/fotAdjustments' import { useFotAdjustmentsFlag } from 'featureFlags/flags/fotAdjustments'
import { useInfoExploreFlag } from 'featureFlags/flags/infoExplore' import { useInfoExploreFlag } from 'featureFlags/flags/infoExplore'
import { useInfoLiveViewsFlag } from 'featureFlags/flags/infoLiveViews' import { useInfoLiveViewsFlag } from 'featureFlags/flags/infoLiveViews'
@ -229,6 +230,12 @@ export default function FeatureFlagModal() {
<X size={24} /> <X size={24} />
</CloseButton> </CloseButton>
</Header> </Header>
<FeatureFlagOption
variant={BaseVariant}
value={useFallbackProviderEnabledFlag()}
featureFlag={FeatureFlag.fallbackProvider}
label="Enable fallback provider"
/>
<FeatureFlagOption <FeatureFlagOption
variant={BaseVariant} variant={BaseVariant}
value={useCurrencyConversionFlag()} value={useCurrencyConversionFlag()}

@ -3,9 +3,10 @@ import IconButton from 'components/AccountDrawer/IconButton'
import { AutoColumn } from 'components/Column' import { AutoColumn } from 'components/Column'
import { Settings } from 'components/Icons/Settings' import { Settings } from 'components/Icons/Settings'
import { AutoRow } from 'components/Row' import { AutoRow } from 'components/Row'
import { connections, networkConnection } from 'connection' import { connections, deprecatedNetworkConnection, networkConnection } from 'connection'
import { ActivationStatus, useActivationState } from 'connection/activate' import { ActivationStatus, useActivationState } from 'connection/activate'
import { isSupportedChain } from 'constants/chains' import { isSupportedChain } from 'constants/chains'
import { useFallbackProviderEnabled } from 'featureFlags/flags/fallbackProvider'
import { useEffect } from 'react' import { useEffect } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { ThemedText } from 'theme/components' import { ThemedText } from 'theme/components'
@ -41,13 +42,17 @@ export default function WalletModal({ openSettings }: { openSettings: () => void
const { connector, chainId } = useWeb3React() const { connector, chainId } = useWeb3React()
const { activationState } = useActivationState() const { activationState } = useActivationState()
const fallbackProviderEnabled = useFallbackProviderEnabled()
// Keep the network connector in sync with any active user connector to prevent chain-switching on wallet disconnection. // Keep the network connector in sync with any active user connector to prevent chain-switching on wallet disconnection.
useEffect(() => { useEffect(() => {
if (chainId && isSupportedChain(chainId) && connector !== networkConnection.connector) { if (chainId && isSupportedChain(chainId) && connector !== networkConnection.connector) {
if (fallbackProviderEnabled) {
networkConnection.connector.activate(chainId) networkConnection.connector.activate(chainId)
} else {
deprecatedNetworkConnection.connector.activate(chainId)
} }
}, [chainId, connector]) }
}, [chainId, connector, fallbackProviderEnabled])
return ( return (
<Wrapper data-testid="wallet-modal"> <Wrapper data-testid="wallet-modal">

@ -4,7 +4,8 @@ import { Connector } from '@web3-react/types'
import { sendAnalyticsEvent, user, useTrace } from 'analytics' import { sendAnalyticsEvent, user, useTrace } from 'analytics'
import { connections, getConnection } from 'connection' import { connections, getConnection } from 'connection'
import { isSupportedChain } from 'constants/chains' import { isSupportedChain } from 'constants/chains'
import { RPC_PROVIDERS } from 'constants/providers' import { DEPRECATED_RPC_PROVIDERS, RPC_PROVIDERS } from 'constants/providers'
import { useFallbackProviderEnabled } from 'featureFlags/flags/fallbackProvider'
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc' import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
import usePrevious from 'hooks/usePrevious' import usePrevious from 'hooks/usePrevious'
import { ReactNode, useEffect } from 'react' import { ReactNode, useEffect } from 'react'
@ -31,8 +32,10 @@ function Updater() {
const currentPage = getCurrentPageFromLocation(pathname) const currentPage = getCurrentPageFromLocation(pathname)
const analyticsContext = useTrace() const analyticsContext = useTrace()
const providers = useFallbackProviderEnabled() ? RPC_PROVIDERS : DEPRECATED_RPC_PROVIDERS
// Trace RPC calls (for debugging). // Trace RPC calls (for debugging).
const networkProvider = isSupportedChain(chainId) ? RPC_PROVIDERS[chainId] : undefined const networkProvider = isSupportedChain(chainId) ? providers[chainId] : undefined
const shouldTrace = useTraceJsonRpcFlag() === TraceJsonRpcVariant.Enabled const shouldTrace = useTraceJsonRpcFlag() === TraceJsonRpcVariant.Enabled
useEffect(() => { useEffect(() => {
if (shouldTrace) { if (shouldTrace) {

@ -1,7 +1,7 @@
import { Connector } from '@web3-react/types' import { Connector } from '@web3-react/types'
import { useSyncExternalStore } from 'react' import { useSyncExternalStore } from 'react'
import { getConnection, gnosisSafeConnection, networkConnection } from './index' import { deprecatedNetworkConnection, getConnection, gnosisSafeConnection } from './index'
import { deletePersistedConnectionMeta, getPersistedConnectionMeta } from './meta' import { deletePersistedConnectionMeta, getPersistedConnectionMeta } from './meta'
import { ConnectionType } from './types' import { ConnectionType } from './types'
@ -43,8 +43,7 @@ async function connect(connector: Connector, type: ConnectionType) {
if (window !== window.parent) { if (window !== window.parent) {
connect(gnosisSafeConnection.connector, ConnectionType.GNOSIS_SAFE) connect(gnosisSafeConnection.connector, ConnectionType.GNOSIS_SAFE)
} }
connect(deprecatedNetworkConnection.connector, ConnectionType.DEPRECATED_NETWORK)
connect(networkConnection.connector, ConnectionType.NETWORK)
// Get the persisted wallet type from the last session. // Get the persisted wallet type from the last session.
const meta = getPersistedConnectionMeta() const meta = getPersistedConnectionMeta()

@ -14,7 +14,7 @@ import { useSyncExternalStore } from 'react'
import { isMobile, isNonIOSPhone } from 'utils/userAgent' import { isMobile, isNonIOSPhone } from 'utils/userAgent'
import { RPC_URLS } from '../constants/networks' import { RPC_URLS } from '../constants/networks'
import { RPC_PROVIDERS } from '../constants/providers' import { DEPRECATED_RPC_PROVIDERS, RPC_PROVIDERS } from '../constants/providers'
import { Connection, ConnectionType } from './types' import { Connection, ConnectionType } from './types'
import { getInjection, getIsCoinbaseWallet, getIsInjected, getIsMetaMaskWallet } from './utils' import { getInjection, getIsCoinbaseWallet, getIsInjected, getIsMetaMaskWallet } from './utils'
import { UniwalletConnect as UniwalletWCV2Connect, WalletConnectV2 } from './WalletConnectV2' import { UniwalletConnect as UniwalletWCV2Connect, WalletConnectV2 } from './WalletConnectV2'
@ -34,6 +34,17 @@ export const networkConnection: Connection = {
shouldDisplay: () => false, shouldDisplay: () => false,
} }
const [deprecatedWeb3Network, deprecatedWeb3NetworkHooks] = initializeConnector<Network>(
(actions) => new Network({ actions, urlMap: DEPRECATED_RPC_PROVIDERS, defaultChainId: 1 })
)
export const deprecatedNetworkConnection: Connection = {
getName: () => 'Network',
connector: deprecatedWeb3Network,
hooks: deprecatedWeb3NetworkHooks,
type: ConnectionType.NETWORK,
shouldDisplay: () => false,
}
const getIsCoinbaseWalletBrowser = () => isMobile && getIsCoinbaseWallet() const getIsCoinbaseWalletBrowser = () => isMobile && getIsCoinbaseWallet()
const getIsMetaMaskBrowser = () => isMobile && getIsMetaMaskWallet() const getIsMetaMaskBrowser = () => isMobile && getIsMetaMaskWallet()
const getIsInjectedMobileBrowser = () => getIsCoinbaseWalletBrowser() || getIsMetaMaskBrowser() const getIsInjectedMobileBrowser = () => getIsCoinbaseWalletBrowser() || getIsMetaMaskBrowser()
@ -179,6 +190,7 @@ export const connections = [
walletConnectV2Connection, walletConnectV2Connection,
coinbaseWalletConnection, coinbaseWalletConnection,
networkConnection, networkConnection,
deprecatedNetworkConnection,
] ]
export function getConnection(c: Connector | ConnectionType) { export function getConnection(c: Connector | ConnectionType) {
@ -200,6 +212,8 @@ export function getConnection(c: Connector | ConnectionType) {
return uniwalletWCV2ConnectConnection return uniwalletWCV2ConnectConnection
case ConnectionType.NETWORK: case ConnectionType.NETWORK:
return networkConnection return networkConnection
case ConnectionType.DEPRECATED_NETWORK:
return deprecatedNetworkConnection
case ConnectionType.GNOSIS_SAFE: case ConnectionType.GNOSIS_SAFE:
return gnosisSafeConnection return gnosisSafeConnection
} }

@ -9,6 +9,7 @@ export enum ConnectionType {
WALLET_CONNECT_V2 = 'WALLET_CONNECT_V2', WALLET_CONNECT_V2 = 'WALLET_CONNECT_V2',
NETWORK = 'NETWORK', NETWORK = 'NETWORK',
GNOSIS_SAFE = 'GNOSIS_SAFE', GNOSIS_SAFE = 'GNOSIS_SAFE',
DEPRECATED_NETWORK = 'DEPRECATED_NETWORK',
} }
export function toConnectionType(value = ''): ConnectionType | undefined { export function toConnectionType(value = ''): ConnectionType | undefined {

@ -4,6 +4,10 @@ const INFURA_KEY = process.env.REACT_APP_INFURA_KEY
if (typeof INFURA_KEY === 'undefined') { if (typeof INFURA_KEY === 'undefined') {
throw new Error(`REACT_APP_INFURA_KEY must be a defined environment variable`) throw new Error(`REACT_APP_INFURA_KEY must be a defined environment variable`)
} }
const QUICKNODE_MAINNET_RPC_URL = process.env.REACT_APP_QUICKNODE_MAINNET_RPC_URL
if (typeof QUICKNODE_MAINNET_RPC_URL === 'undefined') {
throw new Error(`REACT_APP_QUICKNODE_MAINNET_RPC_URL must be a defined environment variable`)
}
const QUICKNODE_BNB_RPC_URL = process.env.REACT_APP_BNB_RPC_URL const QUICKNODE_BNB_RPC_URL = process.env.REACT_APP_BNB_RPC_URL
if (typeof QUICKNODE_BNB_RPC_URL === 'undefined') { if (typeof QUICKNODE_BNB_RPC_URL === 'undefined') {
throw new Error(`REACT_APP_BNB_RPC_URL must be a defined environment variable`) throw new Error(`REACT_APP_BNB_RPC_URL must be a defined environment variable`)
@ -122,7 +126,11 @@ export const FALLBACK_URLS = {
* These are the URLs used by the interface when there is not another available source of chain data. * These are the URLs used by the interface when there is not another available source of chain data.
*/ */
export const RPC_URLS = { export const RPC_URLS = {
[ChainId.MAINNET]: [`https://mainnet.infura.io/v3/${INFURA_KEY}`, ...FALLBACK_URLS[ChainId.MAINNET]], [ChainId.MAINNET]: [
`https://mainnet.infura.io/v3/${INFURA_KEY}`,
QUICKNODE_MAINNET_RPC_URL,
...FALLBACK_URLS[ChainId.MAINNET],
],
[ChainId.GOERLI]: [`https://goerli.infura.io/v3/${INFURA_KEY}`, ...FALLBACK_URLS[ChainId.GOERLI]], [ChainId.GOERLI]: [`https://goerli.infura.io/v3/${INFURA_KEY}`, ...FALLBACK_URLS[ChainId.GOERLI]],
[ChainId.SEPOLIA]: [`https://sepolia.infura.io/v3/${INFURA_KEY}`, ...FALLBACK_URLS[ChainId.SEPOLIA]], [ChainId.SEPOLIA]: [`https://sepolia.infura.io/v3/${INFURA_KEY}`, ...FALLBACK_URLS[ChainId.SEPOLIA]],
[ChainId.OPTIMISM]: [`https://optimism-mainnet.infura.io/v3/${INFURA_KEY}`, ...FALLBACK_URLS[ChainId.OPTIMISM]], [ChainId.OPTIMISM]: [`https://optimism-mainnet.infura.io/v3/${INFURA_KEY}`, ...FALLBACK_URLS[ChainId.OPTIMISM]],

@ -1,75 +1,50 @@
import { deepCopy } from '@ethersproject/properties'
// This is the only file which should instantiate new Providers.
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { StaticJsonRpcProvider } from '@ethersproject/providers'
import { isPlain } from '@reduxjs/toolkit'
import { ChainId } from '@uniswap/sdk-core' import { ChainId } from '@uniswap/sdk-core'
import AppRpcProvider from 'rpc/AppRpcProvider'
import AppStaticJsonRpcProvider from 'rpc/StaticJsonRpcProvider'
import StaticJsonRpcProvider from 'rpc/StaticJsonRpcProvider'
import { AVERAGE_L1_BLOCK_TIME } from './chainInfo' import { SupportedInterfaceChain } from './chains'
import { CHAIN_IDS_TO_NAMES, SupportedInterfaceChain } from './chains'
import { RPC_URLS } from './networks' import { RPC_URLS } from './networks'
class AppJsonRpcProvider extends StaticJsonRpcProvider { const providerFactory = (chainId: SupportedInterfaceChain, i = 0) =>
private _blockCache = new Map<string, Promise<any>>() new AppStaticJsonRpcProvider(chainId, RPC_URLS[chainId][i])
get blockCache() {
// If the blockCache has not yet been initialized this block, do so by
// setting a listener to clear it on the next block.
if (!this._blockCache.size) {
this.once('block', () => this._blockCache.clear())
}
return this._blockCache
}
constructor(chainId: SupportedInterfaceChain) {
// Including networkish allows ethers to skip the initial detectNetwork call.
super(RPC_URLS[chainId][0], /* networkish= */ { chainId, name: CHAIN_IDS_TO_NAMES[chainId] })
// NB: Third-party providers (eg MetaMask) will have their own polling intervals,
// which should be left as-is to allow operations (eg transaction confirmation) to resolve faster.
// Network providers (eg AppJsonRpcProvider) need to update less frequently to be considered responsive.
this.pollingInterval = AVERAGE_L1_BLOCK_TIME
}
send(method: string, params: Array<any>): Promise<any> {
// Only cache eth_call's.
if (method !== 'eth_call') return super.send(method, params)
// Only cache if params are serializable.
if (!isPlain(params)) return super.send(method, params)
const key = `call:${JSON.stringify(params)}`
const cached = this.blockCache.get(key)
if (cached) {
this.emit('debug', {
action: 'request',
request: deepCopy({ method, params, id: 'cache' }),
provider: this,
})
return cached
}
const result = super.send(method, params)
this.blockCache.set(key, result)
return result
}
}
/** /**
* These are the only JsonRpcProviders used directly by the interface. * These are the only JsonRpcProviders used directly by the interface.
*/ */
export const RPC_PROVIDERS: { [key in SupportedInterfaceChain]: StaticJsonRpcProvider } = { export const RPC_PROVIDERS: { [key in SupportedInterfaceChain]: StaticJsonRpcProvider } = {
[ChainId.MAINNET]: new AppJsonRpcProvider(ChainId.MAINNET), [ChainId.MAINNET]: new AppRpcProvider(ChainId.MAINNET, [
[ChainId.GOERLI]: new AppJsonRpcProvider(ChainId.GOERLI), providerFactory(ChainId.MAINNET),
[ChainId.SEPOLIA]: new AppJsonRpcProvider(ChainId.SEPOLIA), providerFactory(ChainId.MAINNET, 1),
[ChainId.OPTIMISM]: new AppJsonRpcProvider(ChainId.OPTIMISM), ]),
[ChainId.OPTIMISM_GOERLI]: new AppJsonRpcProvider(ChainId.OPTIMISM_GOERLI), [ChainId.GOERLI]: providerFactory(ChainId.GOERLI),
[ChainId.ARBITRUM_ONE]: new AppJsonRpcProvider(ChainId.ARBITRUM_ONE), [ChainId.SEPOLIA]: providerFactory(ChainId.SEPOLIA),
[ChainId.ARBITRUM_GOERLI]: new AppJsonRpcProvider(ChainId.ARBITRUM_GOERLI), [ChainId.OPTIMISM]: providerFactory(ChainId.OPTIMISM),
[ChainId.POLYGON]: new AppJsonRpcProvider(ChainId.POLYGON), [ChainId.OPTIMISM_GOERLI]: providerFactory(ChainId.OPTIMISM_GOERLI),
[ChainId.POLYGON_MUMBAI]: new AppJsonRpcProvider(ChainId.POLYGON_MUMBAI), [ChainId.ARBITRUM_ONE]: providerFactory(ChainId.ARBITRUM_ONE),
[ChainId.CELO]: new AppJsonRpcProvider(ChainId.CELO), [ChainId.ARBITRUM_GOERLI]: providerFactory(ChainId.ARBITRUM_GOERLI),
[ChainId.CELO_ALFAJORES]: new AppJsonRpcProvider(ChainId.CELO_ALFAJORES), [ChainId.POLYGON]: providerFactory(ChainId.POLYGON),
[ChainId.BNB]: new AppJsonRpcProvider(ChainId.BNB), [ChainId.POLYGON_MUMBAI]: providerFactory(ChainId.POLYGON_MUMBAI),
[ChainId.AVALANCHE]: new AppJsonRpcProvider(ChainId.AVALANCHE), [ChainId.CELO]: providerFactory(ChainId.CELO),
[ChainId.BASE]: new AppJsonRpcProvider(ChainId.BASE), [ChainId.CELO_ALFAJORES]: providerFactory(ChainId.CELO_ALFAJORES),
[ChainId.BNB]: providerFactory(ChainId.BNB),
[ChainId.AVALANCHE]: providerFactory(ChainId.AVALANCHE),
[ChainId.BASE]: providerFactory(ChainId.BASE),
}
export const DEPRECATED_RPC_PROVIDERS: { [key in SupportedInterfaceChain]: AppStaticJsonRpcProvider } = {
[ChainId.MAINNET]: providerFactory(ChainId.MAINNET),
[ChainId.GOERLI]: providerFactory(ChainId.GOERLI),
[ChainId.SEPOLIA]: providerFactory(ChainId.SEPOLIA),
[ChainId.OPTIMISM]: providerFactory(ChainId.OPTIMISM),
[ChainId.OPTIMISM_GOERLI]: providerFactory(ChainId.OPTIMISM_GOERLI),
[ChainId.ARBITRUM_ONE]: providerFactory(ChainId.ARBITRUM_ONE),
[ChainId.ARBITRUM_GOERLI]: providerFactory(ChainId.ARBITRUM_GOERLI),
[ChainId.POLYGON]: providerFactory(ChainId.POLYGON),
[ChainId.POLYGON_MUMBAI]: providerFactory(ChainId.POLYGON_MUMBAI),
[ChainId.CELO]: providerFactory(ChainId.CELO),
[ChainId.CELO_ALFAJORES]: providerFactory(ChainId.CELO_ALFAJORES),
[ChainId.BNB]: providerFactory(ChainId.BNB),
[ChainId.AVALANCHE]: providerFactory(ChainId.AVALANCHE),
[ChainId.BASE]: providerFactory(ChainId.BASE),
} }

@ -0,0 +1,9 @@
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useFallbackProviderEnabledFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.fallbackProvider)
}
export function useFallbackProviderEnabled(): boolean {
return useFallbackProviderEnabledFlag() === BaseVariant.Enabled
}

@ -8,6 +8,7 @@ import { useGate } from 'statsig-react'
export enum FeatureFlag { export enum FeatureFlag {
traceJsonRpc = 'traceJsonRpc', traceJsonRpc = 'traceJsonRpc',
debounceSwapQuote = 'debounce_swap_quote', debounceSwapQuote = 'debounce_swap_quote',
fallbackProvider = 'fallback_provider',
uniswapXSyntheticQuote = 'uniswapx_synthetic_quote', uniswapXSyntheticQuote = 'uniswapx_synthetic_quote',
uniswapXEthOutputEnabled = 'uniswapx_eth_output_enabled', uniswapXEthOutputEnabled = 'uniswapx_eth_output_enabled',
uniswapXExactOutputEnabled = 'uniswapx_exact_output_enabled', uniswapXExactOutputEnabled = 'uniswapx_exact_output_enabled',

@ -28,8 +28,9 @@ import ERC1155_ABI from 'abis/erc1155.json'
import { ArgentWalletDetector, EnsPublicResolver, EnsRegistrar, Erc20, Erc721, Erc1155, Weth } from 'abis/types' import { ArgentWalletDetector, EnsPublicResolver, EnsRegistrar, Erc20, Erc721, Erc1155, Weth } from 'abis/types'
import WETH_ABI from 'abis/weth.json' import WETH_ABI from 'abis/weth.json'
import { sendAnalyticsEvent } from 'analytics' import { sendAnalyticsEvent } from 'analytics'
import { RPC_PROVIDERS } from 'constants/providers' import { DEPRECATED_RPC_PROVIDERS, RPC_PROVIDERS } from 'constants/providers'
import { WRAPPED_NATIVE_CURRENCY } from 'constants/tokens' import { WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import { useFallbackProviderEnabled } from 'featureFlags/flags/fallbackProvider'
import { useEffect, useMemo } from 'react' import { useEffect, useMemo } from 'react'
import { NonfungiblePositionManager, TickLens, UniswapInterfaceMulticall } from 'types/v3' import { NonfungiblePositionManager, TickLens, UniswapInterfaceMulticall } from 'types/v3'
import { V3Migrator } from 'types/v3/V3Migrator' import { V3Migrator } from 'types/v3/V3Migrator'
@ -69,17 +70,19 @@ function useMainnetContract<T extends Contract = Contract>(address: string | und
const { chainId } = useWeb3React() const { chainId } = useWeb3React()
const isMainnet = chainId === ChainId.MAINNET const isMainnet = chainId === ChainId.MAINNET
const contract = useContract(isMainnet ? address : undefined, ABI, false) const contract = useContract(isMainnet ? address : undefined, ABI, false)
const providers = useFallbackProviderEnabled() ? RPC_PROVIDERS : DEPRECATED_RPC_PROVIDERS
return useMemo(() => { return useMemo(() => {
if (isMainnet) return contract if (isMainnet) return contract
if (!address) return null if (!address) return null
const provider = RPC_PROVIDERS[ChainId.MAINNET] const provider = providers[ChainId.MAINNET]
try { try {
return getContract(address, ABI, provider) return getContract(address, ABI, provider)
} catch (error) { } catch (error) {
console.error('Failed to get mainnet contract', error) console.error('Failed to get mainnet contract', error)
return null return null
} }
}, [address, ABI, contract, isMainnet]) as T }, [isMainnet, contract, address, providers, ABI]) as T
} }
export function useV2MigratorContract() { export function useV2MigratorContract() {

@ -1,7 +1,8 @@
import { nanoid } from '@reduxjs/toolkit' import { nanoid } from '@reduxjs/toolkit'
import { ChainId } from '@uniswap/sdk-core' import { ChainId } from '@uniswap/sdk-core'
import { TokenList } from '@uniswap/token-lists' import { TokenList } from '@uniswap/token-lists'
import { RPC_PROVIDERS } from 'constants/providers' import { DEPRECATED_RPC_PROVIDERS, RPC_PROVIDERS } from 'constants/providers'
import { useFallbackProviderEnabled } from 'featureFlags/flags/fallbackProvider'
import getTokenList from 'lib/hooks/useTokenList/fetchTokenList' import getTokenList from 'lib/hooks/useTokenList/fetchTokenList'
import resolveENSContentHash from 'lib/utils/resolveENSContentHash' import resolveENSContentHash from 'lib/utils/resolveENSContentHash'
import { useCallback } from 'react' import { useCallback } from 'react'
@ -11,6 +12,7 @@ import { fetchTokenList } from '../state/lists/actions'
export function useFetchListCallback(): (listUrl: string, skipValidation?: boolean) => Promise<TokenList> { export function useFetchListCallback(): (listUrl: string, skipValidation?: boolean) => Promise<TokenList> {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const providers = useFallbackProviderEnabled() ? RPC_PROVIDERS : DEPRECATED_RPC_PROVIDERS
return useCallback( return useCallback(
async (listUrl: string, skipValidation?: boolean) => { async (listUrl: string, skipValidation?: boolean) => {
@ -18,7 +20,7 @@ export function useFetchListCallback(): (listUrl: string, skipValidation?: boole
dispatch(fetchTokenList.pending({ requestId, url: listUrl })) dispatch(fetchTokenList.pending({ requestId, url: listUrl }))
return getTokenList( return getTokenList(
listUrl, listUrl,
(ensName: string) => resolveENSContentHash(ensName, RPC_PROVIDERS[ChainId.MAINNET]), (ensName: string) => resolveENSContentHash(ensName, providers[ChainId.MAINNET]),
skipValidation skipValidation
) )
.then((tokenList) => { .then((tokenList) => {
@ -31,6 +33,6 @@ export function useFetchListCallback(): (listUrl: string, skipValidation?: boole
throw error throw error
}) })
}, },
[dispatch] [dispatch, providers]
) )
} }

@ -1,6 +1,11 @@
import { ChainId } from '@uniswap/sdk-core' import { ChainId } from '@uniswap/sdk-core'
import { Connector } from '@web3-react/types' import { Connector } from '@web3-react/types'
import { networkConnection, uniwalletWCV2ConnectConnection, walletConnectV2Connection } from 'connection' import {
deprecatedNetworkConnection,
networkConnection,
uniwalletWCV2ConnectConnection,
walletConnectV2Connection,
} from 'connection'
import { getChainInfo } from 'constants/chainInfo' import { getChainInfo } from 'constants/chainInfo'
import { isSupportedChain, SupportedInterfaceChain } from 'constants/chains' import { isSupportedChain, SupportedInterfaceChain } from 'constants/chains'
import { FALLBACK_URLS, RPC_URLS } from 'constants/networks' import { FALLBACK_URLS, RPC_URLS } from 'constants/networks'
@ -37,6 +42,7 @@ export function useSwitchChain() {
walletConnectV2Connection.connector, walletConnectV2Connection.connector,
uniwalletWCV2ConnectConnection.connector, uniwalletWCV2ConnectConnection.connector,
networkConnection.connector, networkConnection.connector,
deprecatedNetworkConnection.connector,
].includes(connector) ].includes(connector)
) { ) {
await connector.activate(chainId) await connector.activate(chainId)

@ -3,7 +3,7 @@ import { BigintIsh, ChainId, CurrencyAmount, Token, TradeType } from '@uniswap/s
// eslint-disable-next-line @typescript-eslint/no-restricted-imports // eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { AlphaRouter, AlphaRouterConfig } from '@uniswap/smart-order-router' import { AlphaRouter, AlphaRouterConfig } from '@uniswap/smart-order-router'
import { asSupportedChain } from 'constants/chains' import { asSupportedChain } from 'constants/chains'
import { RPC_PROVIDERS } from 'constants/providers' import { DEPRECATED_RPC_PROVIDERS } from 'constants/providers'
import { nativeOnChain } from 'constants/tokens' import { nativeOnChain } from 'constants/tokens'
import JSBI from 'jsbi' import JSBI from 'jsbi'
import { GetQuoteArgs, QuoteResult, QuoteState, SwapRouterNativeAssets } from 'state/routing/types' import { GetQuoteArgs, QuoteResult, QuoteState, SwapRouterNativeAssets } from 'state/routing/types'
@ -16,7 +16,7 @@ export function getRouter(chainId: ChainId): AlphaRouter {
const supportedChainId = asSupportedChain(chainId) const supportedChainId = asSupportedChain(chainId)
if (supportedChainId) { if (supportedChainId) {
const provider = RPC_PROVIDERS[supportedChainId] const provider = DEPRECATED_RPC_PROVIDERS[supportedChainId]
const router = new AlphaRouter({ chainId, provider }) const router = new AlphaRouter({ chainId, provider })
routers.set(chainId, router) routers.set(chainId, router)
return router return router

@ -1,6 +1,7 @@
import { ChainId } from '@uniswap/sdk-core' import { ChainId } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { RPC_PROVIDERS } from 'constants/providers' import { DEPRECATED_RPC_PROVIDERS, RPC_PROVIDERS } from 'constants/providers'
import { useFallbackProviderEnabled } from 'featureFlags/flags/fallbackProvider'
import useIsWindowVisible from 'hooks/useIsWindowVisible' import useIsWindowVisible from 'hooks/useIsWindowVisible'
import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react' import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react'
@ -92,9 +93,11 @@ export function BlockNumberProvider({ children }: { children: ReactNode }) {
return void 0 return void 0
}, [activeChainId, provider, windowVisible, onChainBlock]) }, [activeChainId, provider, windowVisible, onChainBlock])
const networkProviders = useFallbackProviderEnabled() ? RPC_PROVIDERS : DEPRECATED_RPC_PROVIDERS
useEffect(() => { useEffect(() => {
if (mainnetBlock === undefined) { if (mainnetBlock === undefined) {
RPC_PROVIDERS[ChainId.MAINNET] networkProviders[ChainId.MAINNET]
.getBlockNumber() .getBlockNumber()
.then((block) => { .then((block) => {
onChainBlock(ChainId.MAINNET, block) onChainBlock(ChainId.MAINNET, block)
@ -102,7 +105,7 @@ export function BlockNumberProvider({ children }: { children: ReactNode }) {
// swallow errors - it's ok if this fails, as we'll try again if we activate mainnet // swallow errors - it's ok if this fails, as we'll try again if we activate mainnet
.catch(() => undefined) .catch(() => undefined)
} }
}, [mainnetBlock, onChainBlock]) }, [mainnetBlock, networkProviders, onChainBlock])
const value = useMemo( const value = useMemo(
() => ({ () => ({

@ -0,0 +1,121 @@
import { TransactionResponse } from '@ethersproject/abstract-provider'
import { JsonRpcProvider, Network, Provider } from '@ethersproject/providers'
import { ChainId } from '@uniswap/sdk-core'
import AppRpcProvider from './AppRpcProvider'
jest.mock('@ethersproject/providers')
describe('AppRpcProvider', () => {
let mockProviders: JsonRpcProvider[]
let mockProvider1: jest.Mocked<JsonRpcProvider>
let mockProvider2: jest.Mocked<JsonRpcProvider>
let mockIsProvider: jest.SpyInstance
beforeEach(() => {
mockProvider1 = new JsonRpcProvider() as jest.Mocked<JsonRpcProvider>
mockProvider2 = new JsonRpcProvider() as jest.Mocked<JsonRpcProvider>
mockIsProvider = jest.spyOn(Provider, 'isProvider').mockReturnValue(true)
mockProviders = [mockProvider1, mockProvider2]
mockProviders.forEach((provider) => {
// override readonly property
// @ts-expect-error
provider.network = {
name: 'homestead',
chainId: 1,
} as Network
provider.getNetwork = jest.fn().mockReturnValue({
name: 'homestead',
chainId: 1,
} as Network)
// override readonly property
// @ts-expect-error
provider.connection = { url: '' }
})
})
test('constructor initializes with valid providers', () => {
expect(() => new AppRpcProvider(ChainId.MAINNET, mockProviders)).not.toThrow()
})
test('constructor throws with empty providers array', () => {
expect(() => new AppRpcProvider(ChainId.MAINNET, [])).toThrow('providers array empty')
})
test('constructor throws with network mismatch', () => {
mockProviders[0].network.chainId = 2
expect(() => new AppRpcProvider(ChainId.MAINNET, mockProviders)).toThrow('networks mismatch')
})
test('constructor throws with invalid providers', () => {
mockIsProvider.mockReturnValueOnce(false)
expect(() => new AppRpcProvider(ChainId.MAINNET, [{} as JsonRpcProvider])).toThrow('invalid provider')
})
test('handles sendTransaction', async () => {
const hash = '0x123'
mockProvider1.sendTransaction.mockResolvedValue({ hash } as TransactionResponse)
const provider = new AppRpcProvider(ChainId.MAINNET, [mockProvider1])
const result = await provider.perform('sendTransaction', { signedTransaction: '0xabc' })
expect(result).toBe(hash)
})
test('handles call', async () => {
const hash = '0x123'
mockProvider1.perform.mockResolvedValue({ hash } as TransactionResponse)
const provider = new AppRpcProvider(ChainId.MAINNET, [mockProvider1])
const { hash: result } = await provider.perform('call', [{ hash }])
expect(result).toBe(hash)
})
test('should sort faster providers before slower providers', async () => {
const SLOW = 500
mockProvider1.getBlockNumber = jest.fn(() => new Promise((resolve) => setTimeout(() => resolve(1), SLOW)))
const FAST = 1
mockProvider2.getBlockNumber = jest.fn(() => new Promise((resolve) => setTimeout(() => resolve(1), FAST)))
const appRpcProvider = new AppRpcProvider(ChainId.MAINNET, mockProviders)
// Evaluate all providers
const evaluationPromises = appRpcProvider.providerEvaluations.map(appRpcProvider.evaluateProvider)
await Promise.all(evaluationPromises)
// Validate that the providers are sorted correctly by latency
const [fastProvider, slowProvider] = AppRpcProvider.sortProviders(appRpcProvider.providerEvaluations.slice())
expect(fastProvider.performance.latency).toBeLessThan(slowProvider.performance.latency)
})
test('should sort failing providers after successful providers', async () => {
mockProvider1.getBlockNumber = jest.fn(
() => new Promise((_resolve, reject) => setTimeout(() => reject('fail'), 50))
)
mockProvider2.getBlockNumber = jest.fn(() => new Promise((resolve) => setTimeout(() => resolve(1), 50)))
const appRpcProvider = new AppRpcProvider(ChainId.MAINNET, mockProviders)
// Evaluate all providers
const evaluationPromises = appRpcProvider.providerEvaluations.map(appRpcProvider.evaluateProvider)
await Promise.all(evaluationPromises)
// Validate that the providers are sorted correctly by latency
const [provider, failingProvider] = AppRpcProvider.sortProviders(appRpcProvider.providerEvaluations.slice())
expect(provider.performance.failureCount).toBeLessThan(failingProvider.performance.failureCount)
})
test('should increment failureCount on provider failure', async () => {
mockProvider1.getBlockNumber.mockRejectedValue(new Error('Failed'))
const appRpcProvider = new AppRpcProvider(ChainId.MAINNET, mockProviders)
// Evaluate the failing provider
await appRpcProvider.evaluateProvider(appRpcProvider.providerEvaluations[0])
// Validate that the failureCount was incremented
expect(appRpcProvider.providerEvaluations[0].performance.failureCount).toBe(1)
})
})

158
src/rpc/AppRpcProvider.ts Normal file

@ -0,0 +1,158 @@
import { Network } from '@ethersproject/networks'
import { JsonRpcProvider, Provider } from '@ethersproject/providers'
import { SupportedInterfaceChain } from 'constants/chains'
import AppStaticJsonRpcProvider from './StaticJsonRpcProvider'
function checkNetworks(networks: Array<Network>): Network | null {
let result: Network | null = null
for (let i = 0; i < networks.length; i++) {
const network = networks[i]
// Null! We do not know our network; bail.
if (network == null) {
throw new Error('unknown network')
}
if (result) {
// Make sure the network matches the previous networks
if (
!(
result.name === network.name &&
result.chainId === network.chainId &&
(result.ensAddress === network.ensAddress || (result.ensAddress == null && network.ensAddress == null))
)
) {
throw new Error('networks mismatch')
}
} else {
result = network
}
}
return result
}
interface ProviderPerformance {
callCount: number
latency: number
failureCount: number
lastEvaluated: number
}
interface FallbackProviderEvaluation {
provider: JsonRpcProvider
performance: ProviderPerformance
}
/**
* This provider balances requests among multiple JSON-RPC endpoints.
*/
export default class AppRpcProvider extends AppStaticJsonRpcProvider {
providerEvaluations: ReadonlyArray<FallbackProviderEvaluation>
readonly evaluationIntervalMs: number
constructor(chainId: SupportedInterfaceChain, providers: JsonRpcProvider[], evaluationIntervalMs = 30000) {
if (providers.length === 0) throw new Error('providers array empty')
providers.forEach((provider, i) => {
if (!Provider.isProvider(provider)) throw new Error(`invalid provider ${i}`)
})
checkNetworks(providers.map((p) => p.network))
super(chainId, providers[0].connection.url)
this.providerEvaluations = providers.map((provider) => ({
provider,
performance: {
callCount: 0,
latency: 1,
failureCount: 0,
lastEvaluated: 0,
},
}))
this.evaluationIntervalMs = evaluationIntervalMs
}
/**
* Perform a JSON-RPC request.
* Throws an error if all providers fail to perform the operation.
*/
async perform(method: string, params: { [name: string]: any }): Promise<any> {
// Periodically evaluate all providers
const currentTime = Date.now()
// Note that this async action will not affect the current perform call
this.providerEvaluations.forEach((providerEval) => {
if (currentTime - providerEval.performance.lastEvaluated >= this.evaluationIntervalMs) {
this.evaluateProvider(providerEval)
}
})
this.providerEvaluations = AppRpcProvider.sortProviders(this.providerEvaluations.slice())
// Always broadcast "sendTransaction" to all backends
if (method === 'sendTransaction') {
const results: Array<string | Error> = await Promise.all(
this.providerEvaluations.map(({ performance, provider }) => {
performance.callCount++
return provider.sendTransaction(params.signedTransaction).then(
(result) => result.hash,
(error) => error
)
})
)
// Any success is good enough
for (let i = 0; i < results.length; i++) {
if (typeof results[i] === 'string') return results[i]
}
// They were all an error; pick the first error
throw results[0]
} else {
for (const { provider, performance } of this.providerEvaluations) {
performance.callCount++
try {
return await provider.perform(method, params)
} catch (error) {
performance.failureCount++
console.warn('rpc action failed', error)
}
}
throw new Error('All providers failed to perform the operation.')
}
}
/**
* Evaluates the performance of a provider. Updates latency and failure count metrics.
*/
async evaluateProvider(config: FallbackProviderEvaluation): Promise<void> {
const startTime = Date.now()
config.performance.callCount++
try {
await config.provider.getBlockNumber()
} catch (error) {
config.performance.failureCount++
}
const latency = Date.now() - startTime
config.performance.latency += latency
config.performance.lastEvaluated = Date.now()
}
static sortProviders(providerEvaluations: FallbackProviderEvaluation[]) {
return providerEvaluations.sort((a, b) => {
// Provider a calculations
const aAverageLatency = a.performance.latency / (a.performance.callCount || 1)
const aFailRate = (a.performance.failureCount || 0.01) / (a.performance.callCount || 1)
// Provider b calculations
const bAverageLatency = b.performance.latency / (b.performance.callCount || 1)
const bFailRate = (b.performance.failureCount || 0.01) / (b.performance.callCount || 1)
if (aFailRate < bFailRate) return -1
if (aAverageLatency < bAverageLatency) return -1
return 1
})
}
}

@ -0,0 +1,53 @@
import { deepCopy } from '@ethersproject/properties'
// This is the only file which should instantiate new Providers.
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { StaticJsonRpcProvider } from '@ethersproject/providers'
import { isPlain } from '@reduxjs/toolkit'
import { AVERAGE_L1_BLOCK_TIME } from '../constants/chainInfo'
import { CHAIN_IDS_TO_NAMES, SupportedInterfaceChain } from '../constants/chains'
export default class AppStaticJsonRpcProvider extends StaticJsonRpcProvider {
private _blockCache = new Map<string, Promise<any>>()
get blockCache() {
// If the blockCache has not yet been initialized this block, do so by
// setting a listener to clear it on the next block.
if (!this._blockCache.size) {
this.once('block', () => this._blockCache.clear())
}
return this._blockCache
}
constructor(chainId: SupportedInterfaceChain, url: string) {
// Including networkish allows ethers to skip the initial detectNetwork call.
super(url, /* networkish= */ { chainId, name: CHAIN_IDS_TO_NAMES[chainId] })
// NB: Third-party providers (eg MetaMask) will have their own polling intervals,
// which should be left as-is to allow operations (eg transaction confirmation) to resolve faster.
// Network providers (eg AppStaticJsonRpcProvider) need to update less frequently to be considered responsive.
this.pollingInterval = AVERAGE_L1_BLOCK_TIME
}
send(method: string, params: Array<any>): Promise<any> {
// Only cache eth_call's.
if (method !== 'eth_call') return super.send(method, params)
// Only cache if params are serializable.
if (!isPlain(params)) return super.send(method, params)
const key = `call:${JSON.stringify(params)}`
const cached = this.blockCache.get(key)
if (cached) {
this.emit('debug', {
action: 'request',
request: deepCopy({ method, params, id: 'cache' }),
provider: this,
})
return cached
}
const result = super.send(method, params)
this.blockCache.set(key, result)
return result
}
}

@ -4,7 +4,7 @@ import ERC20_ABI from 'abis/erc20.json'
import { Erc20, Weth } from 'abis/types' import { Erc20, Weth } from 'abis/types'
import WETH_ABI from 'abis/weth.json' import WETH_ABI from 'abis/weth.json'
import { SupportedInterfaceChain } from 'constants/chains' import { SupportedInterfaceChain } from 'constants/chains'
import { RPC_PROVIDERS } from 'constants/providers' import { DEPRECATED_RPC_PROVIDERS } from 'constants/providers'
import { WRAPPED_NATIVE_CURRENCY } from 'constants/tokens' import { WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import { getContract } from 'utils' import { getContract } from 'utils'
@ -26,7 +26,7 @@ export async function getApproveInfo(
// If any of these arguments aren't provided, then we cannot generate approval cost info // If any of these arguments aren't provided, then we cannot generate approval cost info
if (!account || !usdCostPerGas) return { needsApprove: false } if (!account || !usdCostPerGas) return { needsApprove: false }
const provider = RPC_PROVIDERS[currency.chainId as SupportedInterfaceChain] const provider = DEPRECATED_RPC_PROVIDERS[currency.chainId as SupportedInterfaceChain]
const tokenContract = getContract(currency.address, ERC20_ABI, provider) as Erc20 const tokenContract = getContract(currency.address, ERC20_ABI, provider) as Erc20
let approveGasUseEstimate let approveGasUseEstimate
@ -58,7 +58,7 @@ export async function getWrapInfo(
): Promise<WrapInfo> { ): Promise<WrapInfo> {
if (!needsWrap) return { needsWrap: false } if (!needsWrap) return { needsWrap: false }
const provider = RPC_PROVIDERS[chainId] const provider = DEPRECATED_RPC_PROVIDERS[chainId]
const wethAddress = WRAPPED_NATIVE_CURRENCY[chainId]?.address const wethAddress = WRAPPED_NATIVE_CURRENCY[chainId]?.address
// If any of these arguments aren't provided, then we cannot generate wrap cost info // If any of these arguments aren't provided, then we cannot generate wrap cost info

@ -1,11 +1,10 @@
import { Signer } from '@ethersproject/abstract-signer'
import { AddressZero } from '@ethersproject/constants' import { AddressZero } from '@ethersproject/constants'
import { Contract } from '@ethersproject/contracts' import { Contract } from '@ethersproject/contracts'
import { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers' import { JsonRpcProvider, Provider } from '@ethersproject/providers'
import { isAddress } from './addresses' import { isAddress } from './addresses'
// account is optional
export function getContract(address: string, ABI: any, provider: JsonRpcProvider, account?: string): Contract { export function getContract(address: string, ABI: any, provider: JsonRpcProvider, account?: string): Contract {
if (!isAddress(address) || address === AddressZero) { if (!isAddress(address) || address === AddressZero) {
throw Error(`Invalid 'address' parameter '${address}'.`) throw Error(`Invalid 'address' parameter '${address}'.`)
@ -13,11 +12,8 @@ export function getContract(address: string, ABI: any, provider: JsonRpcProvider
return new Contract(address, ABI, getProviderOrSigner(provider, account) as any) return new Contract(address, ABI, getProviderOrSigner(provider, account) as any)
} }
// account is not optional
function getSigner(provider: JsonRpcProvider, account: string): JsonRpcSigner {
return provider.getSigner(account).connectUnchecked()
}
// account is optional // account is optional
function getProviderOrSigner(provider: JsonRpcProvider, account?: string): JsonRpcProvider | JsonRpcSigner { function getProviderOrSigner(provider: JsonRpcProvider, account?: string): Provider | Signer {
return account ? getSigner(provider, account) : provider return account ? provider.getSigner(account).connectUnchecked() : provider
} }