From 4ca66af181a46f134df514d3bd7b67c4c440f159 Mon Sep 17 00:00:00 2001 From: Oleksii Kosynskyi Date: Wed, 23 Oct 2024 11:47:15 -0400 Subject: [PATCH] Add rpc provider public node (#7322) * init * finished * add unit test * add changelog * Update CHANGELOG.md * Update web3_provider_publicnode.ts * fix * Update web3_provider_publicnode.ts --- CHANGELOG.md | 6 + packages/web3-rpc-providers/CHANGELOG.md | 4 + packages/web3-rpc-providers/src/index.ts | 1 + packages/web3-rpc-providers/src/types.ts | 51 +++++++- .../src/web3_provider_publicnode.ts | 112 ++++++++++++++++++ .../src/web3_provider_quicknode.ts | 2 +- .../test/unit/request.test.ts | 24 ++++ .../test/integration/web3RPCProviders.test.ts | 93 ++++++++++++++- 8 files changed, 287 insertions(+), 6 deletions(-) create mode 100644 packages/web3-rpc-providers/src/web3_provider_publicnode.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a7bdee5a44..d9abf6019de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2765,3 +2765,9 @@ If there are any bugs, improvements, optimizations or any new feature proposal f - fix `padRight` validation failure on large `uint` (#7265) ## [Unreleased] + +### Added + +#### web3-rpc-providers + +- PublicNodeProvider was added (#7322) diff --git a/packages/web3-rpc-providers/CHANGELOG.md b/packages/web3-rpc-providers/CHANGELOG.md index a9e2c2a8b79..110b620707d 100644 --- a/packages/web3-rpc-providers/CHANGELOG.md +++ b/packages/web3-rpc-providers/CHANGELOG.md @@ -59,3 +59,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added optional `HttpProviderOptions | SocketOptions` in `Web3ExternalProvider` and `QuickNodeProvider` for provider configs ## [Unreleased] + +### Added + +- PublicNodeProvider was added (#7322) diff --git a/packages/web3-rpc-providers/src/index.ts b/packages/web3-rpc-providers/src/index.ts index 577d8e6b960..f7c0e6c2613 100644 --- a/packages/web3-rpc-providers/src/index.ts +++ b/packages/web3-rpc-providers/src/index.ts @@ -19,6 +19,7 @@ import { QuickNodeProvider } from './web3_provider_quicknode.js'; export * from './types.js'; export * from './web3_provider_quicknode.js'; +export * from './web3_provider_publicnode.js'; export * from './web3_provider.js'; export * from './errors.js'; diff --git a/packages/web3-rpc-providers/src/types.ts b/packages/web3-rpc-providers/src/types.ts index 9fda306ae0d..258d6411c57 100644 --- a/packages/web3-rpc-providers/src/types.ts +++ b/packages/web3-rpc-providers/src/types.ts @@ -25,13 +25,15 @@ export enum Transport { export enum Network { ETH_MAINNET = 'eth_mainnet', - ETH_GOERLI = 'eth_goerli', ETH_SEPOLIA = 'eth_sepolia', ETH_HOLESKY = 'eth_holesky', POLYGON_MAINNET = 'polygon_mainnet', - POLYGON_MUMBAI = 'polygon_mumbai', - POLYGON_AMONY = 'polygon_amony', + + POLYGON_AMOY = 'polygon_amoy', + AVALANCHE_C_MAINNET = 'avalanche_c_mainnet', + AVALANCHE_P_MAINNET = 'avalanche_p_mainnet', + AVALANCHE_X_MAINNET = 'avalanche_x_mainnet', ARBITRUM_MAINNET = 'arbitrum_mainnet', ARBITRUM_SEPOLIA = 'arbitrum_sepolia', @@ -42,8 +44,51 @@ export enum Network { OPTIMISM_MAINNET = 'optimism_mainnet', OPTIMISM_SEPOLIA = 'optimism_sepolia', + FANTOM_MAINNET = 'fantom_mainnet', + FANTOM_TESTNET = 'fantom_testnet', + + DYMENSION_MAINNET = 'dymension_mainnet', + DYMENSION_TESTNET = 'dymension_testnet', + BNB_MAINNET = 'bnb_mainnet', BNB_TESTNET = 'bnb_testnet', + + BSC_MAINNET = 'bsc_mainnet', + BSC_TESTNET = 'bsc_testnet', + + ARBITRUM_ONE = 'arbitrum_one', + ARBITRUM_NOVA = 'arbitrum_nova', + AVALANCHE_FUJI_C = 'avalanche_fuji_c', + AVALANCHE_FUJI_P = 'avalanche_fuji_p', + AVALANCHE_FUJI_X = 'avalanche_fuji_x', + BLAST_MAINNET = 'blast_mainnet', + OPBNB_MAINNET = 'opbnb_mainnet', + OPBNB_TESTNET = 'opbnb_testnet', + GNOSIS_MAINNET = 'gnosis_mainnet', + GNOSIS_CHIADO = 'gnosis_chiado', + PULSECHAIN_MAINNET = 'pulsechain_mainnet', + PULSECHAIN_TESTNET = 'pulsechain_testnet', + KAVA_MAINNET = 'kava_mainnet', + CRONOS_MAINNET = 'cronos_mainnet', + MANTLE_MAINNET = 'mantle_mainnet', + CHILIZ_MAINNET = 'chiliz_mainnet', + CHILIZ_SPICY = 'chiliz_spicy', + MOONBEAM_MAINNET = 'moonbeam_mainnet', + TAIKO_MAINNET = 'taiko_mainnet', + TAIKO_HEKLA = 'taiko_hekla', + LINEA_MAINNET = 'linea_mainnet', + LINEA_SEPOLIA = 'linea_sepolia', + BAHAMUT_MAINNET = 'bahamut_mainnet', + SCROLL_MAINNET = 'scroll_mainnet', + SCROLL_SEPOLIA = 'scroll_sepolia', + TRON_MAINNET = 'tron_mainnet', + SYSCOIN_MAINNET = 'syscoin_mainnet', + SYSCOIN_TANENBAUM = 'syscoin_tanenbaum', + MOONRIVER_MAINNET = 'moonriver_mainnet', + HAQQ_MAINNET = 'haqq_mainnet', + EVMOS_MAINNET = 'evmos_mainnet', + EVMOS_TESTNET = 'evmos_testnet', + BERACHAIN_TESTNET = 'berachain_testnet', } // Combining the ws types diff --git a/packages/web3-rpc-providers/src/web3_provider_publicnode.ts b/packages/web3-rpc-providers/src/web3_provider_publicnode.ts new file mode 100644 index 00000000000..027d7a45921 --- /dev/null +++ b/packages/web3-rpc-providers/src/web3_provider_publicnode.ts @@ -0,0 +1,112 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ + +import { EthExecutionAPI, Web3APISpec } from 'web3-types'; +import { HttpProviderOptions } from 'web3-providers-http'; +import { Network, SocketOptions, Transport } from './types.js'; +import { Web3ExternalProvider } from './web3_provider.js'; + +const isValid = (str: string) => str !== undefined && str.trim().length > 0; + +const websocketExclusions = [ + Network.DYMENSION_MAINNET, + Network.DYMENSION_TESTNET, + Network.KAVA_MAINNET, + Network.CRONOS_MAINNET, + // deprecated + Network.POLYGON_MAINNET, +]; + +export class PublicNodeProvider< + API extends Web3APISpec = EthExecutionAPI, +> extends Web3ExternalProvider { + // eslint-disable-next-line default-param-last + public constructor( + network: Network = Network.ETH_MAINNET, + transport: Transport = Transport.HTTPS, + host = '', + providerConfigOptions?: HttpProviderOptions | SocketOptions, + ) { + super(network, transport, '', host, providerConfigOptions); + } + public static readonly networkHostMap: { [key: string]: string } = { + [Network.POLYGON_AMOY]: 'polygon-amoy-bor-rpc', + [Network.DYMENSION_MAINNET]: 'dymension-evm-rpc', + [Network.DYMENSION_TESTNET]: 'dymension-testnet-evm-rpc', + [Network.BLAST_MAINNET]: 'blast-rpc', + [Network.GNOSIS_MAINNET]: 'gnosis-rpc', + [Network.PULSECHAIN_MAINNET]: 'pulsechain-rpc', + [Network.PULSECHAIN_TESTNET]: 'pulsechain-testnet-rpc', + [Network.KAVA_MAINNET]: 'kava-evm-rpc', + [Network.CRONOS_MAINNET]: 'cronos-evm-rpc', + [Network.MANTLE_MAINNET]: 'mantle-rpc', + [Network.TAIKO_MAINNET]: 'taiko-rpc', + [Network.TAIKO_HEKLA]: 'taiko-hekla-rpc', + [Network.LINEA_MAINNET]: 'linea-rpc', + [Network.LINEA_SEPOLIA]: 'linea-sepolia-rpc', + [Network.SCROLL_MAINNET]: 'scroll-rpc', + [Network.SCROLL_SEPOLIA]: 'scroll-sepolia-rpc', + [Network.SYSCOIN_MAINNET]: 'syscoin-evm-rpc', + [Network.SYSCOIN_TANENBAUM]: 'syscoin-tanenbaum-evm-rpc', + [Network.HAQQ_MAINNET]: 'haqq-evm-rpc', + [Network.EVMOS_MAINNET]: 'evmos-evm-rpc', + [Network.EVMOS_TESTNET]: 'evmos-testnet-evm-rpc', + [Network.BERACHAIN_TESTNET]: 'berachain-testnet-evm-rpc', + [Network.ETH_MAINNET]: 'ethereum-rpc', + [Network.ETH_SEPOLIA]: 'ethereum-sepolia-rpc', + [Network.ETH_HOLESKY]: 'ethereum-holesky-rpc', + [Network.BSC_MAINNET]: 'bsc-rpc', + [Network.BSC_TESTNET]: 'bsc-testnet-rpc', + [Network.POLYGON_MAINNET]: 'polygon-bor-rpc', + [Network.BASE_MAINNET]: 'base-rpc', + [Network.BASE_SEPOLIA]: 'base-sepolia-rpc', + [Network.ARBITRUM_ONE]: 'arbitrum-one-rpc', + [Network.ARBITRUM_NOVA]: 'arbitrum-nova-rpc', + [Network.ARBITRUM_SEPOLIA]: 'arbitrum-sepolia-rpc', + [Network.AVALANCHE_C_MAINNET]: 'avalanche-c-chain-rpc', + [Network.AVALANCHE_P_MAINNET]: 'avalanche-p-chain-rpc', + [Network.AVALANCHE_X_MAINNET]: 'avalanche-x-chain-rpc', + [Network.AVALANCHE_FUJI_C]: 'avalanche-fuji-c-chain-rpc', + [Network.AVALANCHE_FUJI_P]: 'avalanche-fuji-p-chain-rpc', + [Network.AVALANCHE_FUJI_X]: 'avalanche-fuji-x-chain-rpc', + [Network.OPTIMISM_MAINNET]: 'optimism-rpc', + [Network.OPTIMISM_SEPOLIA]: 'optimism-sepolia-rpc', + [Network.FANTOM_MAINNET]: 'fantom-rpc', + [Network.FANTOM_TESTNET]: 'fantom-testnet-rpc', + [Network.OPBNB_MAINNET]: 'opbnb-rpc', + [Network.OPBNB_TESTNET]: 'opbnb-testnet-rpc', + [Network.GNOSIS_CHIADO]: 'gnosis-chiado-rpc', + [Network.CHILIZ_MAINNET]: 'chiliz-rpc', + [Network.CHILIZ_SPICY]: 'chiliz-spicy-rpc', + [Network.MOONBEAM_MAINNET]: 'moonbeam-rpc', + [Network.BAHAMUT_MAINNET]: 'bahamut-rpc', + [Network.TRON_MAINNET]: 'tron-evm-rpc', + [Network.MOONRIVER_MAINNET]: 'moonriver-rpc', + }; + // eslint-disable-next-line class-methods-use-this + public getRPCURL(network: Network, transport: Transport, _: string, _host: string) { + if (!PublicNodeProvider.networkHostMap[network]) { + throw new Error('Network info not avalible.'); + } + const defaultHost = `${PublicNodeProvider.networkHostMap[network]}.publicnode.com`; + const host = isValid(_host) ? _host : defaultHost; + if (websocketExclusions.includes(network) && transport === Transport.WebSocket) { + return `${transport}://${host}/websocket`; + } + return `${transport}://${host}`; + } +} diff --git a/packages/web3-rpc-providers/src/web3_provider_quicknode.ts b/packages/web3-rpc-providers/src/web3_provider_quicknode.ts index ae74fa49860..4b6e5ae73e6 100644 --- a/packages/web3-rpc-providers/src/web3_provider_quicknode.ts +++ b/packages/web3-rpc-providers/src/web3_provider_quicknode.ts @@ -107,7 +107,7 @@ export class QuickNodeProvider< host = isValid(_host) ? _host : 'small-chaotic-moon.matic.quiknode.pro'; token = isValid(_token) ? _token : '847569f8a017e84d985e10d0f44365d965a951f1'; break; - case Network.POLYGON_AMONY: + case Network.POLYGON_AMOY: host = isValid(_host) ? _host : 'prettiest-side-shape.matic-amoy.quiknode.pro'; token = isValid(_token) ? _token : '79a9476eea661d4f82de614db1d8a895b14b881c'; break; diff --git a/packages/web3-rpc-providers/test/unit/request.test.ts b/packages/web3-rpc-providers/test/unit/request.test.ts index 3de14fc0513..da07c2a578c 100644 --- a/packages/web3-rpc-providers/test/unit/request.test.ts +++ b/packages/web3-rpc-providers/test/unit/request.test.ts @@ -20,6 +20,7 @@ import { Network, Transport } from '../../src/types'; import { Web3ExternalProvider } from '../../src/web3_provider'; import { QuickNodeRateLimitError } from '../../src/errors'; import { QuickNodeProvider } from '../../src/web3_provider_quicknode'; +import { PublicNodeProvider } from '../../src/web3_provider_publicnode'; jest.mock('web3-providers-ws', () => { return { @@ -122,4 +123,27 @@ describe('Web3ExternalProvider', () => { }; await expect(provider.request(payload)).rejects.toThrow(QuickNodeRateLimitError); }); + + it('should make a request using the PublicNodeProvider provider', async () => { + const network: Network = Network.ETH_MAINNET; + const transport: Transport = Transport.HTTPS; + + const mockHttpProvider = { + request: jest.fn(), + }; + + const mockResponse = { result: 'mock-result' }; + mockHttpProvider.request.mockResolvedValue(mockResponse); + + const provider = new PublicNodeProvider(network, transport); + (provider as any).provider = mockHttpProvider; + + const payload: Web3APIPayload> = { + method: 'eth_getBalance', + params: ['0x0123456789012345678901234567890123456789', 'latest'], + }; + + const result = await provider.request(payload); + expect(result).toEqual(mockResponse); + }); }); diff --git a/packages/web3/test/integration/web3RPCProviders.test.ts b/packages/web3/test/integration/web3RPCProviders.test.ts index 1faeffad452..6e6d2994f9c 100644 --- a/packages/web3/test/integration/web3RPCProviders.test.ts +++ b/packages/web3/test/integration/web3RPCProviders.test.ts @@ -15,7 +15,13 @@ You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ -import { mainnet, Network, QuickNodeProvider, Transport } from 'web3-rpc-providers'; +import { + mainnet, + Network, + QuickNodeProvider, + Transport, + PublicNodeProvider, +} from 'web3-rpc-providers'; import { Web3 } from '../../src/index'; describe('Web3 RPC Provider Integration tests', () => { @@ -29,7 +35,7 @@ describe('Web3 RPC Provider Integration tests', () => { Network.BNB_MAINNET, Network.BNB_TESTNET, Network.POLYGON_MAINNET, - Network.POLYGON_AMONY, + Network.POLYGON_AMOY, ]; transports.forEach(transport => { @@ -48,6 +54,89 @@ describe('Web3 RPC Provider Integration tests', () => { }); }); }); + + const publicNodeNetworks = [ + Network.POLYGON_AMOY, + Network.POLYGON_MAINNET, + Network.DYMENSION_MAINNET, + Network.DYMENSION_TESTNET, + Network.BLAST_MAINNET, + Network.GNOSIS_MAINNET, + Network.PULSECHAIN_MAINNET, + Network.PULSECHAIN_TESTNET, + Network.KAVA_MAINNET, + Network.CRONOS_MAINNET, + Network.MANTLE_MAINNET, + Network.HAQQ_MAINNET, + Network.TAIKO_MAINNET, + Network.TAIKO_HEKLA, + Network.EVMOS_MAINNET, + Network.EVMOS_TESTNET, + Network.BERACHAIN_TESTNET, + Network.LINEA_MAINNET, + Network.LINEA_SEPOLIA, + Network.SCROLL_MAINNET, + Network.SCROLL_SEPOLIA, + Network.SYSCOIN_MAINNET, + Network.SYSCOIN_TANENBAUM, + Network.ETH_MAINNET, + Network.ETH_SEPOLIA, + Network.ETH_HOLESKY, + Network.BSC_MAINNET, + Network.BSC_TESTNET, + Network.BASE_MAINNET, + Network.BASE_SEPOLIA, + Network.ARBITRUM_ONE, + Network.ARBITRUM_NOVA, + Network.ARBITRUM_SEPOLIA, + Network.AVALANCHE_C_MAINNET, + Network.AVALANCHE_P_MAINNET, + Network.AVALANCHE_X_MAINNET, + Network.AVALANCHE_FUJI_C, + Network.AVALANCHE_FUJI_P, + Network.AVALANCHE_FUJI_X, + Network.OPTIMISM_MAINNET, + Network.OPTIMISM_SEPOLIA, + Network.FANTOM_MAINNET, + Network.FANTOM_TESTNET, + Network.OPBNB_MAINNET, + Network.OPBNB_TESTNET, + Network.GNOSIS_CHIADO, + Network.CHILIZ_MAINNET, + Network.CHILIZ_SPICY, + Network.MOONBEAM_MAINNET, + Network.BAHAMUT_MAINNET, + Network.TRON_MAINNET, + Network.MOONRIVER_MAINNET, + ]; + transports.forEach(transport => { + publicNodeNetworks.forEach(network => { + if ( + !( + [ + Network.AVALANCHE_P_MAINNET, + Network.AVALANCHE_X_MAINNET, + Network.AVALANCHE_FUJI_X, + Network.AVALANCHE_FUJI_P, + ].includes(network) && transport === Transport.WebSocket + ) + ) { + it(`PublicNodeProvider should work with ${transport} transport and ${network} network`, async () => { + const provider = new PublicNodeProvider(network, transport); + const web3 = new Web3(provider); + const result = await web3.eth.getBlockNumber(); + + expect(typeof result).toBe('bigint'); + expect(result > 0).toBe(true); + + if (transport === Transport.WebSocket) { + web3.provider?.disconnect(); + } + }); + } + }); + }); + it(`should work with mainnet provider`, async () => { const web3 = new Web3(mainnet); const result = await web3.eth.getBlockNumber();