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_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) {
|
||||||
networkConnection.connector.activate(chainId)
|
if (fallbackProviderEnabled) {
|
||||||
|
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),
|
||||||
}
|
}
|
||||||
|
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 {
|
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(
|
||||||
() => ({
|
() => ({
|
||||||
|
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 { 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
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user