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:
parent
2dc5a6efb4
commit
809841df0a
1
.env
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_BNB_RPC_URL="https://rough-sleek-hill.bsc.quiknode.pro/413cc98cbc776cda8fdf1d0f47003583ff73d9bf"
|
||||
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_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkV2?platform=web&env=staging"
|
||||
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_TRACES_SAMPLE_RATE=0.00003
|
||||
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"
|
||||
|
@ -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'
|
||||
})
|
||||
|
||||
// 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(/quiknode.pro/, { statusCode: 404 })
|
||||
|
||||
// Log requests to hardhat.
|
||||
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 { useWeb3React } from '@web3-react/core'
|
||||
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 { useFallbackProviderEnabled } from 'featureFlags/flags/fallbackProvider'
|
||||
import { ContractInput, useUniswapPricesQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { toContractInput } from 'graphql/data/util'
|
||||
import useStablecoinPrice from 'hooks/useStablecoinPrice'
|
||||
@ -31,6 +32,8 @@ function useContractMultichain<T extends BaseContract>(
|
||||
): ContractMap<T> {
|
||||
const { chainId: walletChainId, provider: walletProvider } = useWeb3React()
|
||||
|
||||
const networkProviders = useFallbackProviderEnabled() ? RPC_PROVIDERS : DEPRECATED_RPC_PROVIDERS
|
||||
|
||||
return useMemo(() => {
|
||||
const relevantChains =
|
||||
chainIds ??
|
||||
@ -43,14 +46,14 @@ function useContractMultichain<T extends BaseContract>(
|
||||
walletProvider && walletChainId === chainId
|
||||
? walletProvider
|
||||
: isSupportedChain(chainId)
|
||||
? RPC_PROVIDERS[chainId]
|
||||
? networkProviders[chainId]
|
||||
: undefined
|
||||
if (provider) {
|
||||
acc[chainId] = getContract(addressMap[chainId] ?? '', ABI, provider) as T
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
}, [ABI, addressMap, chainIds, walletChainId, walletProvider])
|
||||
}, [ABI, addressMap, chainIds, networkProviders, walletChainId, walletProvider])
|
||||
}
|
||||
|
||||
export function useV3ManagerContracts(chainIds: ChainId[]): ContractMap<NonfungiblePositionManager> {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import Column from 'components/Column'
|
||||
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
|
||||
import { useCurrencyConversionFlag } from 'featureFlags/flags/currencyConversion'
|
||||
import { useFallbackProviderEnabledFlag } from 'featureFlags/flags/fallbackProvider'
|
||||
import { useFotAdjustmentsFlag } from 'featureFlags/flags/fotAdjustments'
|
||||
import { useInfoExploreFlag } from 'featureFlags/flags/infoExplore'
|
||||
import { useInfoLiveViewsFlag } from 'featureFlags/flags/infoLiveViews'
|
||||
@ -229,6 +230,12 @@ export default function FeatureFlagModal() {
|
||||
<X size={24} />
|
||||
</CloseButton>
|
||||
</Header>
|
||||
<FeatureFlagOption
|
||||
variant={BaseVariant}
|
||||
value={useFallbackProviderEnabledFlag()}
|
||||
featureFlag={FeatureFlag.fallbackProvider}
|
||||
label="Enable fallback provider"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={BaseVariant}
|
||||
value={useCurrencyConversionFlag()}
|
||||
|
@ -3,9 +3,10 @@ import IconButton from 'components/AccountDrawer/IconButton'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { Settings } from 'components/Icons/Settings'
|
||||
import { AutoRow } from 'components/Row'
|
||||
import { connections, networkConnection } from 'connection'
|
||||
import { connections, deprecatedNetworkConnection, networkConnection } from 'connection'
|
||||
import { ActivationStatus, useActivationState } from 'connection/activate'
|
||||
import { isSupportedChain } from 'constants/chains'
|
||||
import { useFallbackProviderEnabled } from 'featureFlags/flags/fallbackProvider'
|
||||
import { useEffect } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { ThemedText } from 'theme/components'
|
||||
@ -41,13 +42,17 @@ export default function WalletModal({ openSettings }: { openSettings: () => void
|
||||
const { connector, chainId } = useWeb3React()
|
||||
|
||||
const { activationState } = useActivationState()
|
||||
|
||||
const fallbackProviderEnabled = useFallbackProviderEnabled()
|
||||
// Keep the network connector in sync with any active user connector to prevent chain-switching on wallet disconnection.
|
||||
useEffect(() => {
|
||||
if (chainId && isSupportedChain(chainId) && connector !== networkConnection.connector) {
|
||||
networkConnection.connector.activate(chainId)
|
||||
if (fallbackProviderEnabled) {
|
||||
networkConnection.connector.activate(chainId)
|
||||
} else {
|
||||
deprecatedNetworkConnection.connector.activate(chainId)
|
||||
}
|
||||
}
|
||||
}, [chainId, connector])
|
||||
}, [chainId, connector, fallbackProviderEnabled])
|
||||
|
||||
return (
|
||||
<Wrapper data-testid="wallet-modal">
|
||||
|
@ -4,7 +4,8 @@ import { Connector } from '@web3-react/types'
|
||||
import { sendAnalyticsEvent, user, useTrace } from 'analytics'
|
||||
import { connections, getConnection } from 'connection'
|
||||
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 usePrevious from 'hooks/usePrevious'
|
||||
import { ReactNode, useEffect } from 'react'
|
||||
@ -31,8 +32,10 @@ function Updater() {
|
||||
const currentPage = getCurrentPageFromLocation(pathname)
|
||||
const analyticsContext = useTrace()
|
||||
|
||||
const providers = useFallbackProviderEnabled() ? RPC_PROVIDERS : DEPRECATED_RPC_PROVIDERS
|
||||
|
||||
// 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
|
||||
useEffect(() => {
|
||||
if (shouldTrace) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Connector } from '@web3-react/types'
|
||||
import { useSyncExternalStore } from 'react'
|
||||
|
||||
import { getConnection, gnosisSafeConnection, networkConnection } from './index'
|
||||
import { deprecatedNetworkConnection, getConnection, gnosisSafeConnection } from './index'
|
||||
import { deletePersistedConnectionMeta, getPersistedConnectionMeta } from './meta'
|
||||
import { ConnectionType } from './types'
|
||||
|
||||
@ -43,8 +43,7 @@ async function connect(connector: Connector, type: ConnectionType) {
|
||||
if (window !== window.parent) {
|
||||
connect(gnosisSafeConnection.connector, ConnectionType.GNOSIS_SAFE)
|
||||
}
|
||||
|
||||
connect(networkConnection.connector, ConnectionType.NETWORK)
|
||||
connect(deprecatedNetworkConnection.connector, ConnectionType.DEPRECATED_NETWORK)
|
||||
|
||||
// Get the persisted wallet type from the last session.
|
||||
const meta = getPersistedConnectionMeta()
|
||||
|
@ -14,7 +14,7 @@ import { useSyncExternalStore } from 'react'
|
||||
import { isMobile, isNonIOSPhone } from 'utils/userAgent'
|
||||
|
||||
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 { getInjection, getIsCoinbaseWallet, getIsInjected, getIsMetaMaskWallet } from './utils'
|
||||
import { UniwalletConnect as UniwalletWCV2Connect, WalletConnectV2 } from './WalletConnectV2'
|
||||
@ -34,6 +34,17 @@ export const networkConnection: Connection = {
|
||||
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 getIsMetaMaskBrowser = () => isMobile && getIsMetaMaskWallet()
|
||||
const getIsInjectedMobileBrowser = () => getIsCoinbaseWalletBrowser() || getIsMetaMaskBrowser()
|
||||
@ -179,6 +190,7 @@ export const connections = [
|
||||
walletConnectV2Connection,
|
||||
coinbaseWalletConnection,
|
||||
networkConnection,
|
||||
deprecatedNetworkConnection,
|
||||
]
|
||||
|
||||
export function getConnection(c: Connector | ConnectionType) {
|
||||
@ -200,6 +212,8 @@ export function getConnection(c: Connector | ConnectionType) {
|
||||
return uniwalletWCV2ConnectConnection
|
||||
case ConnectionType.NETWORK:
|
||||
return networkConnection
|
||||
case ConnectionType.DEPRECATED_NETWORK:
|
||||
return deprecatedNetworkConnection
|
||||
case ConnectionType.GNOSIS_SAFE:
|
||||
return gnosisSafeConnection
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ export enum ConnectionType {
|
||||
WALLET_CONNECT_V2 = 'WALLET_CONNECT_V2',
|
||||
NETWORK = 'NETWORK',
|
||||
GNOSIS_SAFE = 'GNOSIS_SAFE',
|
||||
DEPRECATED_NETWORK = 'DEPRECATED_NETWORK',
|
||||
}
|
||||
|
||||
export function toConnectionType(value = ''): ConnectionType | undefined {
|
||||
|
@ -4,6 +4,10 @@ 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`)
|
||||
}
|
||||
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
|
||||
if (typeof QUICKNODE_BNB_RPC_URL === 'undefined') {
|
||||
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.
|
||||
*/
|
||||
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.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]],
|
||||
|
@ -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 AppRpcProvider from 'rpc/AppRpcProvider'
|
||||
import AppStaticJsonRpcProvider from 'rpc/StaticJsonRpcProvider'
|
||||
import StaticJsonRpcProvider from 'rpc/StaticJsonRpcProvider'
|
||||
|
||||
import { AVERAGE_L1_BLOCK_TIME } from './chainInfo'
|
||||
import { CHAIN_IDS_TO_NAMES, SupportedInterfaceChain } from './chains'
|
||||
import { SupportedInterfaceChain } from './chains'
|
||||
import { RPC_URLS } from './networks'
|
||||
|
||||
class AppJsonRpcProvider 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) {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
const providerFactory = (chainId: SupportedInterfaceChain, i = 0) =>
|
||||
new AppStaticJsonRpcProvider(chainId, RPC_URLS[chainId][i])
|
||||
|
||||
/**
|
||||
* These are the only JsonRpcProviders used directly by the interface.
|
||||
*/
|
||||
export const RPC_PROVIDERS: { [key in SupportedInterfaceChain]: StaticJsonRpcProvider } = {
|
||||
[ChainId.MAINNET]: new AppJsonRpcProvider(ChainId.MAINNET),
|
||||
[ChainId.GOERLI]: new AppJsonRpcProvider(ChainId.GOERLI),
|
||||
[ChainId.SEPOLIA]: new AppJsonRpcProvider(ChainId.SEPOLIA),
|
||||
[ChainId.OPTIMISM]: new AppJsonRpcProvider(ChainId.OPTIMISM),
|
||||
[ChainId.OPTIMISM_GOERLI]: new AppJsonRpcProvider(ChainId.OPTIMISM_GOERLI),
|
||||
[ChainId.ARBITRUM_ONE]: new AppJsonRpcProvider(ChainId.ARBITRUM_ONE),
|
||||
[ChainId.ARBITRUM_GOERLI]: new AppJsonRpcProvider(ChainId.ARBITRUM_GOERLI),
|
||||
[ChainId.POLYGON]: new AppJsonRpcProvider(ChainId.POLYGON),
|
||||
[ChainId.POLYGON_MUMBAI]: new AppJsonRpcProvider(ChainId.POLYGON_MUMBAI),
|
||||
[ChainId.CELO]: new AppJsonRpcProvider(ChainId.CELO),
|
||||
[ChainId.CELO_ALFAJORES]: new AppJsonRpcProvider(ChainId.CELO_ALFAJORES),
|
||||
[ChainId.BNB]: new AppJsonRpcProvider(ChainId.BNB),
|
||||
[ChainId.AVALANCHE]: new AppJsonRpcProvider(ChainId.AVALANCHE),
|
||||
[ChainId.BASE]: new AppJsonRpcProvider(ChainId.BASE),
|
||||
[ChainId.MAINNET]: new AppRpcProvider(ChainId.MAINNET, [
|
||||
providerFactory(ChainId.MAINNET),
|
||||
providerFactory(ChainId.MAINNET, 1),
|
||||
]),
|
||||
[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),
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
|
9
src/featureFlags/flags/fallbackProvider.ts
Normal file
9
src/featureFlags/flags/fallbackProvider.ts
Normal file
@ -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 {
|
||||
traceJsonRpc = 'traceJsonRpc',
|
||||
debounceSwapQuote = 'debounce_swap_quote',
|
||||
fallbackProvider = 'fallback_provider',
|
||||
uniswapXSyntheticQuote = 'uniswapx_synthetic_quote',
|
||||
uniswapXEthOutputEnabled = 'uniswapx_eth_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 WETH_ABI from 'abis/weth.json'
|
||||
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 { useFallbackProviderEnabled } from 'featureFlags/flags/fallbackProvider'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { NonfungiblePositionManager, TickLens, UniswapInterfaceMulticall } from 'types/v3'
|
||||
import { V3Migrator } from 'types/v3/V3Migrator'
|
||||
@ -69,17 +70,19 @@ function useMainnetContract<T extends Contract = Contract>(address: string | und
|
||||
const { chainId } = useWeb3React()
|
||||
const isMainnet = chainId === ChainId.MAINNET
|
||||
const contract = useContract(isMainnet ? address : undefined, ABI, false)
|
||||
const providers = useFallbackProviderEnabled() ? RPC_PROVIDERS : DEPRECATED_RPC_PROVIDERS
|
||||
|
||||
return useMemo(() => {
|
||||
if (isMainnet) return contract
|
||||
if (!address) return null
|
||||
const provider = RPC_PROVIDERS[ChainId.MAINNET]
|
||||
const provider = providers[ChainId.MAINNET]
|
||||
try {
|
||||
return getContract(address, ABI, provider)
|
||||
} catch (error) {
|
||||
console.error('Failed to get mainnet contract', error)
|
||||
return null
|
||||
}
|
||||
}, [address, ABI, contract, isMainnet]) as T
|
||||
}, [isMainnet, contract, address, providers, ABI]) as T
|
||||
}
|
||||
|
||||
export function useV2MigratorContract() {
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { ChainId } from '@uniswap/sdk-core'
|
||||
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 resolveENSContentHash from 'lib/utils/resolveENSContentHash'
|
||||
import { useCallback } from 'react'
|
||||
@ -11,6 +12,7 @@ import { fetchTokenList } from '../state/lists/actions'
|
||||
|
||||
export function useFetchListCallback(): (listUrl: string, skipValidation?: boolean) => Promise<TokenList> {
|
||||
const dispatch = useAppDispatch()
|
||||
const providers = useFallbackProviderEnabled() ? RPC_PROVIDERS : DEPRECATED_RPC_PROVIDERS
|
||||
|
||||
return useCallback(
|
||||
async (listUrl: string, skipValidation?: boolean) => {
|
||||
@ -18,7 +20,7 @@ export function useFetchListCallback(): (listUrl: string, skipValidation?: boole
|
||||
dispatch(fetchTokenList.pending({ requestId, url: listUrl }))
|
||||
return getTokenList(
|
||||
listUrl,
|
||||
(ensName: string) => resolveENSContentHash(ensName, RPC_PROVIDERS[ChainId.MAINNET]),
|
||||
(ensName: string) => resolveENSContentHash(ensName, providers[ChainId.MAINNET]),
|
||||
skipValidation
|
||||
)
|
||||
.then((tokenList) => {
|
||||
@ -31,6 +33,6 @@ export function useFetchListCallback(): (listUrl: string, skipValidation?: boole
|
||||
throw error
|
||||
})
|
||||
},
|
||||
[dispatch]
|
||||
[dispatch, providers]
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { ChainId } from '@uniswap/sdk-core'
|
||||
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 { isSupportedChain, SupportedInterfaceChain } from 'constants/chains'
|
||||
import { FALLBACK_URLS, RPC_URLS } from 'constants/networks'
|
||||
@ -37,6 +42,7 @@ export function useSwitchChain() {
|
||||
walletConnectV2Connection.connector,
|
||||
uniwalletWCV2ConnectConnection.connector,
|
||||
networkConnection.connector,
|
||||
deprecatedNetworkConnection.connector,
|
||||
].includes(connector)
|
||||
) {
|
||||
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
|
||||
import { AlphaRouter, AlphaRouterConfig } from '@uniswap/smart-order-router'
|
||||
import { asSupportedChain } from 'constants/chains'
|
||||
import { RPC_PROVIDERS } from 'constants/providers'
|
||||
import { DEPRECATED_RPC_PROVIDERS } from 'constants/providers'
|
||||
import { nativeOnChain } from 'constants/tokens'
|
||||
import JSBI from 'jsbi'
|
||||
import { GetQuoteArgs, QuoteResult, QuoteState, SwapRouterNativeAssets } from 'state/routing/types'
|
||||
@ -16,7 +16,7 @@ export function getRouter(chainId: ChainId): AlphaRouter {
|
||||
|
||||
const supportedChainId = asSupportedChain(chainId)
|
||||
if (supportedChainId) {
|
||||
const provider = RPC_PROVIDERS[supportedChainId]
|
||||
const provider = DEPRECATED_RPC_PROVIDERS[supportedChainId]
|
||||
const router = new AlphaRouter({ chainId, provider })
|
||||
routers.set(chainId, router)
|
||||
return router
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { ChainId } from '@uniswap/sdk-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 { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
@ -92,9 +93,11 @@ export function BlockNumberProvider({ children }: { children: ReactNode }) {
|
||||
return void 0
|
||||
}, [activeChainId, provider, windowVisible, onChainBlock])
|
||||
|
||||
const networkProviders = useFallbackProviderEnabled() ? RPC_PROVIDERS : DEPRECATED_RPC_PROVIDERS
|
||||
|
||||
useEffect(() => {
|
||||
if (mainnetBlock === undefined) {
|
||||
RPC_PROVIDERS[ChainId.MAINNET]
|
||||
networkProviders[ChainId.MAINNET]
|
||||
.getBlockNumber()
|
||||
.then((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
|
||||
.catch(() => undefined)
|
||||
}
|
||||
}, [mainnetBlock, onChainBlock])
|
||||
}, [mainnetBlock, networkProviders, onChainBlock])
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
|
121
src/rpc/AppRpcProvider.test.ts
Normal file
121
src/rpc/AppRpcProvider.test.ts
Normal file
@ -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
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
|
||||
})
|
||||
}
|
||||
}
|
53
src/rpc/StaticJsonRpcProvider.ts
Normal file
53
src/rpc/StaticJsonRpcProvider.ts
Normal file
@ -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 WETH_ABI from 'abis/weth.json'
|
||||
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 { 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 (!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
|
||||
|
||||
let approveGasUseEstimate
|
||||
@ -58,7 +58,7 @@ export async function getWrapInfo(
|
||||
): Promise<WrapInfo> {
|
||||
if (!needsWrap) return { needsWrap: false }
|
||||
|
||||
const provider = RPC_PROVIDERS[chainId]
|
||||
const provider = DEPRECATED_RPC_PROVIDERS[chainId]
|
||||
const wethAddress = WRAPPED_NATIVE_CURRENCY[chainId]?.address
|
||||
|
||||
// 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 { Contract } from '@ethersproject/contracts'
|
||||
import { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers'
|
||||
import { JsonRpcProvider, Provider } from '@ethersproject/providers'
|
||||
|
||||
import { isAddress } from './addresses'
|
||||
|
||||
// account is optional
|
||||
|
||||
export function getContract(address: string, ABI: any, provider: JsonRpcProvider, account?: string): Contract {
|
||||
if (!isAddress(address) || address === AddressZero) {
|
||||
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)
|
||||
}
|
||||
// account is not optional
|
||||
function getSigner(provider: JsonRpcProvider, account: string): JsonRpcSigner {
|
||||
return provider.getSigner(account).connectUnchecked()
|
||||
}
|
||||
|
||||
// account is optional
|
||||
function getProviderOrSigner(provider: JsonRpcProvider, account?: string): JsonRpcProvider | JsonRpcSigner {
|
||||
return account ? getSigner(provider, account) : provider
|
||||
function getProviderOrSigner(provider: JsonRpcProvider, account?: string): Provider | Signer {
|
||||
return account ? provider.getSigner(account).connectUnchecked() : provider
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user