diff --git a/src/utils/__tests__/available-rpc.test.ts b/src/utils/__tests__/available-rpc.test.ts index dd4e3ee..da89383 100644 --- a/src/utils/__tests__/available-rpc.test.ts +++ b/src/utils/__tests__/available-rpc.test.ts @@ -2,8 +2,9 @@ import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; import { FallbackProviderJsonConfig, - createFallbackProviderFromJsonConfig, -} from '../fallback-provider'; + ProviderJson, + createProviderFromJsonConfig, +} from '../provider'; chai.use(chaiAsPromised); const { expect } = chai; @@ -24,12 +25,12 @@ describe('available-rpc', () => { ], }; - const fallbackProvider = createFallbackProviderFromJsonConfig(config); + const fallbackProvider = createProviderFromJsonConfig(config); await fallbackProvider.getBlockNumber(); }).timeout(5000); - it('Should check fallback provider cascade for FallbackProvider of WSS RPC', async () => { + it('Should fail to allow WSS RPC in a FallbackProvider', async () => { const config: FallbackProviderJsonConfig = { chainId: 1, providers: [ @@ -42,11 +43,61 @@ describe('available-rpc', () => { ], }; - const fallbackProvider = createFallbackProviderFromJsonConfig(config); - - await fallbackProvider.getBlockNumber(); + try { + // This should throw an error because WSS is not supported in FallbackProvider + createProviderFromJsonConfig(config); + throw new Error("Test should have thrown an error but did not"); + } catch (error) { + if (error instanceof Error) { + expect(error.message).to.equal( + "WebSocketProvider not supported in FallbackProvider as it will use polling instead of eth_subscribe" + ); + } else { + throw new Error("Caught an unexpected error type"); + } + } }).timeout(5000); + it('Should fail to create single provider with missing chainId', async () => { + const config = { + provider: 'https://eth-mainnet.publicnode.com', + priority: 1, + weight: 2, + stallTimeout: 2500, + } as ProviderJson; + + try { + createProviderFromJsonConfig(config); + expect.fail("Test should have thrown an error but did not"); + } catch (error) { + if (error instanceof Error) { + expect(error.message).to.equal('chainId is required for single provider configuration'); + } else { + expect.fail("Caught an unexpected error type"); + } + } + }); + + it('Should fail to create single provider with missing provider URL', async () => { + const config = { + chainid: 1, + priority: 1, + weight: 2, + stallTimeout: 2500, + }; + + try { + createProviderFromJsonConfig(config as unknown as ProviderJson); + expect.fail("Test should have thrown an error but did not"); + } catch (error) { + if (error instanceof Error) { + expect(error.message).to.equal('provider is required for single provider configuration'); + } else { + expect.fail("Caught an unexpected error type"); + } + } + }); + it('Should sort ascending and descending', () => { const allConfigs = [ { diff --git a/src/utils/available-rpc.ts b/src/utils/available-rpc.ts index ef2554d..39483f8 100644 --- a/src/utils/available-rpc.ts +++ b/src/utils/available-rpc.ts @@ -1,6 +1,6 @@ /// import { JsonRpcProvider, Network, type Provider, WebSocketProvider } from 'ethers'; -import { ProviderJson } from './fallback-provider'; +import { ProviderJson } from './provider'; import { getUpperBoundMedian } from './median'; import { promiseTimeout } from './promises'; @@ -68,13 +68,13 @@ const getBlockNumber = async ( // Conditionally handle what type of provider is being passed let rpcProvider: Provider; - - // If URL starts with wss, use WebSocketProvider - if(provider.startsWith('wss')) { - rpcProvider = new WebSocketProvider(provider, network); + if (provider.startsWith('wss')) { + rpcProvider = new WebSocketProvider(provider, network, { + staticNetwork: network, // Network is dictated in the RPC URL, will not change + }); } else { rpcProvider = new JsonRpcProvider(provider, network, { - staticNetwork: network, + staticNetwork: network, // Network is dictated in the RPC URL, will not change }); } diff --git a/src/utils/fallback-provider.ts b/src/utils/fallback-provider.ts deleted file mode 100644 index f13351a..0000000 --- a/src/utils/fallback-provider.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { FallbackProvider, Network, WebSocketProvider } from 'ethers'; -import { ConfiguredJsonRpcProvider } from './configured-json-rpc-provider'; -import { FallbackProviderConfig } from 'ethers/lib.commonjs/providers/provider-fallback'; - -export type FallbackProviderJsonConfig = { - chainId: number; - providers: ProviderJson[]; -}; - -export type ProviderJson = { - priority: number; - weight: number; - provider: string; - stallTimeout?: number; - maxLogsPerBatch?: number; -}; - -export const createFallbackProviderFromJsonConfig = ( - config: FallbackProviderJsonConfig, -): FallbackProvider => { - try { - const totalWeight = config.providers.reduce( - (acc, { weight }) => acc + weight, - 0, - ); - if (totalWeight < 2) { - throw new Error( - 'Total weight across providers must be >= 2 for fallback quorum.', - ); - } - - const network = Network.from(Number(config.chainId)); - - const providers: FallbackProviderConfig[] = config.providers.map( - ({ - provider: providerURL, - priority, - weight, - stallTimeout, - maxLogsPerBatch, - }) => { - const isWebsocket = providerURL.startsWith('wss'); - const provider = isWebsocket - ? new WebSocketProvider(providerURL, network) - : new ConfiguredJsonRpcProvider( - providerURL, - network, - maxLogsPerBatch, - ); - - const fallbackProviderConfig: FallbackProviderConfig = { - provider, - priority, - weight, - stallTimeout, - }; - return fallbackProviderConfig; - }, - ); - - return new FallbackProvider(providers, network); - } catch (cause) { - if (!(cause instanceof Error)) { - throw new Error( - 'Non-error thrown from createFallbackProviderFromJsonConfig', - { cause }, - ); - } - throw new Error( - `Invalid fallback provider config for chain ${config.chainId}`, - { cause }, - ); - } -}; diff --git a/src/utils/index.ts b/src/utils/index.ts index 8a9c0c5..8480616 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,7 +1,7 @@ export * from './artifact-v2'; export * from './available-rpc'; export * from './compare'; -export * from './fallback-provider'; +export * from './provider'; export * from './error'; export * from './format'; export * from './gas'; diff --git a/src/utils/provider.ts b/src/utils/provider.ts new file mode 100644 index 0000000..c6a95d7 --- /dev/null +++ b/src/utils/provider.ts @@ -0,0 +1,117 @@ +import { FallbackProvider, JsonRpcProvider, WebSocketProvider } from 'ethers'; +import { FallbackProviderConfig } from 'ethers/lib.commonjs/providers/provider-fallback'; +import { isDefined } from './util'; + +export type FallbackProviderJsonConfig = { + chainId: number; + providers: ProviderJson[]; +}; + +export type ProviderJson = { + priority: number; + weight: number; + provider: string; + chainId?: number; + stallTimeout?: number; + maxLogsPerBatch?: number; +}; + +export const createProviderFromJsonConfig = ( + config: FallbackProviderJsonConfig | ProviderJson, + pollingInterval?: number, +): FallbackProvider | WebSocketProvider | JsonRpcProvider => { + try { + // Handle single provider case + if (!('providers' in config)) { + // Get RPC URL + const providerURL = config.provider; + // Ensure providerURL exists and is a string + if (!isDefined(providerURL)) { + throw new Error('provider is required for single provider configuration'); + } else if (typeof providerURL !== 'string') { + throw new Error('provider must be a string'); + } + + // Ensure chainId is present + if (!isDefined(config.chainId)) { + throw new Error('chainId is required for single provider configuration'); + } + + // Create singular provider depending on the URL + let provider: JsonRpcProvider | WebSocketProvider; + if (providerURL.startsWith('wss')) { + provider = new WebSocketProvider(providerURL, config.chainId, { + staticNetwork: true, + }); + } else { + provider = new JsonRpcProvider(providerURL, config.chainId, { + staticNetwork: true, + pollingInterval, + }); + } + + return provider; + } + + const totalWeight = config.providers.reduce( + (acc, { weight }) => acc + weight, + 0, + ); + if (totalWeight < 2) { + throw new Error( + 'Total weight across providers must be >= 2 for fallback quorum.', + ); + } + + const providers: FallbackProviderConfig[] = config.providers.map( + ({ + provider: providerURL, + priority, + weight, + stallTimeout, + maxLogsPerBatch, + }) => { + const isWebsocket = providerURL.startsWith('wss'); + if (isWebsocket) { + throw new Error( + 'WebSocketProvider not supported in FallbackProvider as it will use polling instead of eth_subscribe', + ); + } + + // Create JsonRpcProvider + const provider = new JsonRpcProvider( + providerURL, + config.chainId, + { + staticNetwork: true, + pollingInterval, + batchMaxCount: maxLogsPerBatch, + }, + ); + + // Add JsonRpcProvider to FallbackProviderConfig + const fallbackProviderConfig: FallbackProviderConfig = { + provider, + priority, + weight, + stallTimeout, + }; + return fallbackProviderConfig; + }, + ); + + // Return FallbackProvider containing array of JsonRpcProviders + return new FallbackProvider(providers, config.chainId, { + pollingInterval + }); + } catch (cause) { + if (!(cause instanceof Error)) { + throw new Error( + 'Non-error thrown from createFallbackProviderFromJsonConfig', + { cause }, + ); + } + // Preserve the original error message + throw cause; + } +};