From d9d0391003f2e1d874d39dd43427e1b18b104d8b Mon Sep 17 00:00:00 2001 From: Junaid <86780488+jdevcs@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:13:14 +0200 Subject: [PATCH] Quicknode provider update (#7195) * added response status code in response error * statusCode in ResponseError * updated error message and it will throw on status code 429 * updated error test snapshot * updated test * updated test for QN * Quicknode provider error * Web3ExternalProvider update * mock ws * lint fix --- packages/web3-errors/CHANGELOG.md | 6 ++- .../web3-errors/src/errors/response_errors.ts | 5 ++- .../unit/__snapshots__/errors.test.ts.snap | 4 ++ packages/web3-providers-http/CHANGELOG.md | 6 ++- packages/web3-providers-http/src/index.ts | 2 +- packages/web3-rpc-providers/CHANGELOG.md | 10 +++-- packages/web3-rpc-providers/package.json | 1 + packages/web3-rpc-providers/src/errors.ts | 5 +-- packages/web3-rpc-providers/src/index.ts | 1 + .../web3-rpc-providers/src/web3_provider.ts | 25 ++++------- .../src/web3_provider_quicknode.ts | 25 ++++++++++- .../test/unit/constructor.test.ts | 44 +++++++++++++++++-- .../test/unit/request.test.ts | 30 ++++++++++--- 13 files changed, 125 insertions(+), 39 deletions(-) diff --git a/packages/web3-errors/CHANGELOG.md b/packages/web3-errors/CHANGELOG.md index 3ed320c80a1..6dbdd143e42 100644 --- a/packages/web3-errors/CHANGELOG.md +++ b/packages/web3-errors/CHANGELOG.md @@ -178,4 +178,8 @@ Documentation: - Fixed the undefined data in `Eip838ExecutionError` constructor (#6905) -## [Unreleased] \ No newline at end of file +## [Unreleased] + +### Added + +- Added optional `statusCode` property of response in ResponseError. \ No newline at end of file diff --git a/packages/web3-errors/src/errors/response_errors.ts b/packages/web3-errors/src/errors/response_errors.ts index 9fb1f09e172..214bf6652f6 100644 --- a/packages/web3-errors/src/errors/response_errors.ts +++ b/packages/web3-errors/src/errors/response_errors.ts @@ -45,11 +45,13 @@ export class ResponseError extends B public code = ERR_RESPONSE; public data?: ErrorType | ErrorType[]; public request?: JsonRpcPayload; + public statusCode?: number; public constructor( response: JsonRpcResponse, message?: string, request?: JsonRpcPayload, + statusCode?: number ) { super( message ?? @@ -66,6 +68,7 @@ export class ResponseError extends B : response?.error?.data; } + this.statusCode = statusCode; this.request = request; let errorOrErrors: JsonRpcError | JsonRpcError[] | undefined; if (`error` in response) { @@ -82,7 +85,7 @@ export class ResponseError extends B } public toJSON() { - return { ...super.toJSON(), data: this.data, request: this.request }; + return { ...super.toJSON(), data: this.data, request: this.request, statusCode: this.statusCode }; } } diff --git a/packages/web3-errors/test/unit/__snapshots__/errors.test.ts.snap b/packages/web3-errors/test/unit/__snapshots__/errors.test.ts.snap index 22a863812e8..c2f3c9f0131 100644 --- a/packages/web3-errors/test/unit/__snapshots__/errors.test.ts.snap +++ b/packages/web3-errors/test/unit/__snapshots__/errors.test.ts.snap @@ -239,6 +239,7 @@ exports[`errors InvalidResponseError should have valid json structure 1`] = ` "message": "Returned error: error message", "name": "InvalidResponseError", "request": undefined, + "statusCode": undefined, } `; @@ -316,6 +317,7 @@ exports[`errors ResponseError should have valid json structure with data 1`] = ` "message": "Returned error: error message", "name": "ResponseError", "request": undefined, + "statusCode": undefined, } `; @@ -336,6 +338,7 @@ exports[`errors ResponseError should have valid json structure without data 1`] "message": "Returned error: error message", "name": "ResponseError", "request": undefined, + "statusCode": undefined, } `; @@ -357,6 +360,7 @@ exports[`errors ResponseError should include the array of inner errors 1`] = ` "message": "Returned error: error message,error message", "name": "ResponseError", "request": undefined, + "statusCode": undefined, } `; diff --git a/packages/web3-providers-http/CHANGELOG.md b/packages/web3-providers-http/CHANGELOG.md index 6d57af6dcf7..dd20cddf35a 100644 --- a/packages/web3-providers-http/CHANGELOG.md +++ b/packages/web3-providers-http/CHANGELOG.md @@ -129,4 +129,8 @@ Documentation: - Fix issue lquixada/cross-fetch#78, enabling to run web3.js in service worker (#6463) -## [Unreleased] \ No newline at end of file +## [Unreleased] + +### Added + +- Added `statusCode` of response in ResponseError, `statusCode` is optional property in ResponseError. \ No newline at end of file diff --git a/packages/web3-providers-http/src/index.ts b/packages/web3-providers-http/src/index.ts index bd069dbc14c..d596d1251f6 100644 --- a/packages/web3-providers-http/src/index.ts +++ b/packages/web3-providers-http/src/index.ts @@ -80,7 +80,7 @@ export default class HttpProvider< }); if (!response.ok) { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - throw new ResponseError(await response.json()) + throw new ResponseError(await response.json(), undefined, undefined, response.status); }; return (await response.json()) as JsonRpcResponseWithResult; diff --git a/packages/web3-rpc-providers/CHANGELOG.md b/packages/web3-rpc-providers/CHANGELOG.md index d9fe3235240..52a8508d082 100644 --- a/packages/web3-rpc-providers/CHANGELOG.md +++ b/packages/web3-rpc-providers/CHANGELOG.md @@ -43,12 +43,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.0.0.rc.1] - ### Added +### Added - When error is returned with code 429, throw rate limit error (#7102) - ### Changed +### Changed - Change request return type `Promise` to `Promise>` (#7102) -## [Unreleased] \ No newline at end of file +## [Unreleased] + +### Added + +- Updated rate limit error of QuickNode provider for HTTP transport \ No newline at end of file diff --git a/packages/web3-rpc-providers/package.json b/packages/web3-rpc-providers/package.json index 606ced0ed22..3d66382ff51 100644 --- a/packages/web3-rpc-providers/package.json +++ b/packages/web3-rpc-providers/package.json @@ -49,6 +49,7 @@ "eslint-config-base-web3": "0.1.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-import": "^2.26.0", + "isomorphic-ws": "^5.0.0", "jest": "^29.7.0", "jest-extended": "^3.0.1", "prettier": "^2.7.1", diff --git a/packages/web3-rpc-providers/src/errors.ts b/packages/web3-rpc-providers/src/errors.ts index dd602fcb05f..54ad3094596 100644 --- a/packages/web3-rpc-providers/src/errors.ts +++ b/packages/web3-rpc-providers/src/errors.ts @@ -16,13 +16,12 @@ along with web3.js. If not, see . */ import { BaseWeb3Error } from 'web3-errors'; -import { } from 'web3-types'; const ERR_QUICK_NODE_RATE_LIMIT = 1300; export class QuickNodeRateLimitError extends BaseWeb3Error { public code = ERR_QUICK_NODE_RATE_LIMIT; - public constructor() { - super(`Too many requests, Quicknode has reached its rate limit.`); + public constructor(error?: Error) { + super(`You've reach the rate limit of free RPC calls from our Partner Quick Nodes. There are two options you can either create a paid Quick Nodes account and get 20% off for 2 months using WEB3JS referral code, or use Free public RPC endpoint.`, error); } } \ No newline at end of file diff --git a/packages/web3-rpc-providers/src/index.ts b/packages/web3-rpc-providers/src/index.ts index eea79f433cb..637cdae4cc0 100644 --- a/packages/web3-rpc-providers/src/index.ts +++ b/packages/web3-rpc-providers/src/index.ts @@ -20,6 +20,7 @@ import { QuickNodeProvider } from './web3_provider_quicknode.js'; export * from './types.js'; export * from './web3_provider_quicknode.js'; export * from './web3_provider.js'; +export * from './errors.js'; // default providers export const mainnet = new QuickNodeProvider(); \ No newline at end of file diff --git a/packages/web3-rpc-providers/src/web3_provider.ts b/packages/web3-rpc-providers/src/web3_provider.ts index a00034048ef..dbd50cf9e56 100644 --- a/packages/web3-rpc-providers/src/web3_provider.ts +++ b/packages/web3-rpc-providers/src/web3_provider.ts @@ -17,7 +17,6 @@ along with web3.js. If not, see . import HttpProvider from "web3-providers-http"; import WebSocketProvider from "web3-providers-ws"; -import { isNullish } from "web3-validator"; import { EthExecutionAPI, JsonRpcResult, ProviderConnectInfo, ProviderMessage, ProviderRpcError, Web3APIMethod, Web3APIPayload, Web3APIReturnType, Web3APISpec, Web3BaseProvider, @@ -29,7 +28,6 @@ import { } from "web3-types"; import { Eip1193Provider } from "web3-utils"; import { Transport, Network } from "./types.js"; -import { QuickNodeRateLimitError } from './errors.js'; /* This class can be used to create new providers only when there is custom logic required in each Request method like @@ -39,21 +37,21 @@ Another simpler approach can be a function simply returning URL strings instead no additional logic implementation is required in the provider. */ -export abstract class Web3ExternalProvider < -API extends Web3APISpec = EthExecutionAPI, +export abstract class Web3ExternalProvider< + API extends Web3APISpec = EthExecutionAPI, > extends Eip1193Provider { public provider!: Web3BaseProvider; public readonly transport: Transport; - public abstract getRPCURL(network: Network,transport: Transport,token: string, host: string): string; + public abstract getRPCURL(network: Network, transport: Transport, token: string, host: string): string; public constructor( network: Network, transport: Transport, token: string, host: string) { - + super(); this.transport = transport; @@ -74,18 +72,11 @@ API extends Web3APISpec = EthExecutionAPI, ): Promise> { if (this.transport === Transport.HTTPS) { - const res = await ( (this.provider as HttpProvider).request(payload, requestOptions)) as unknown as JsonRpcResponseWithResult; - - if (typeof res === 'object' && !isNullish(res) && 'error' in res && !isNullish(res.error) && 'code' in res.error && (res.error as { code: number }).code === 429){ - // rate limiting error by quicknode; - throw new QuickNodeRateLimitError(); - - } - return res; - } - + return await ((this.provider as HttpProvider).request(payload, requestOptions)) as unknown as JsonRpcResponseWithResult; + } + return (this.provider as WebSocketProvider).request(payload); - + } public getStatus(): Web3ProviderStatus { diff --git a/packages/web3-rpc-providers/src/web3_provider_quicknode.ts b/packages/web3-rpc-providers/src/web3_provider_quicknode.ts index a1b66880110..4444110afdd 100644 --- a/packages/web3-rpc-providers/src/web3_provider_quicknode.ts +++ b/packages/web3-rpc-providers/src/web3_provider_quicknode.ts @@ -15,12 +15,17 @@ You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ +import { EthExecutionAPI, JsonRpcResponseWithResult, Web3APIMethod, Web3APIPayload, Web3APIReturnType, Web3APISpec } from "web3-types"; +import { ResponseError } from "web3-errors"; import { Transport, Network } from "./types.js"; import { Web3ExternalProvider } from "./web3_provider.js"; +import { QuickNodeRateLimitError } from "./errors.js"; const isValid = (str: string) => str !== undefined && str.trim().length > 0; -export class QuickNodeProvider extends Web3ExternalProvider { +export class QuickNodeProvider< +API extends Web3APISpec = EthExecutionAPI, +> extends Web3ExternalProvider { public constructor( network: Network = Network.ETH_MAINNET, @@ -32,6 +37,24 @@ export class QuickNodeProvider extends Web3ExternalProvider { } + public async request< + Method extends Web3APIMethod, + ResultType = Web3APIReturnType, + >( + payload: Web3APIPayload, + requestOptions?: RequestInit, + ): Promise> { + + try { + return await super.request(payload, requestOptions); + } catch (error) { + if (error instanceof ResponseError && error.statusCode === 429){ + throw new QuickNodeRateLimitError(error); + } + throw error; + } + } + // eslint-disable-next-line class-methods-use-this public getRPCURL(network: Network, transport: Transport, diff --git a/packages/web3-rpc-providers/test/unit/constructor.test.ts b/packages/web3-rpc-providers/test/unit/constructor.test.ts index eb64b29163a..0daedb8c77d 100644 --- a/packages/web3-rpc-providers/test/unit/constructor.test.ts +++ b/packages/web3-rpc-providers/test/unit/constructor.test.ts @@ -14,13 +14,51 @@ 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 . */ -/* eslint-disable max-classes-per-file */ + import HttpProvider from 'web3-providers-http'; import WebSocketProvider from 'web3-providers-ws'; +import WebSocket from 'isomorphic-ws'; + import { Web3ExternalProvider } from '../../src/web3_provider'; import { Network, Transport } from '../../src/types'; +// Mock implementation so ws doesnt have openhandle after test exits as it attempts to connects at start +jest.mock('isomorphic-ws', () => { + return { + __esModule: true, + default: jest.fn().mockImplementation(() => { + // eslint-disable-next-line @typescript-eslint/ban-types + const eventListeners: { [key: string]: Function[] } = {}; + + return { + addEventListener: jest.fn((event, handler) => { + if (!eventListeners[event]) { + eventListeners[event] = []; + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + eventListeners[event].push(handler); + }), + removeEventListener: jest.fn((event, handler) => { + if (eventListeners[event]) { + eventListeners[event] = eventListeners[event].filter(h => h !== handler); + } + }), + dispatchEvent: jest.fn((event) => { + const eventType = event.type; + if (eventListeners[eventType]) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + eventListeners[eventType].forEach(handler => handler(event)); + } + }), + close: jest.fn(), + send: jest.fn(), + readyState: WebSocket.OPEN, + }; + }), + }; +}); + class MockWeb3ExternalProviderA extends Web3ExternalProvider { public constructor(network: Network, transport: Transport, token: string){ super(network, transport, token, ""); @@ -33,7 +71,7 @@ class MockWeb3ExternalProviderA extends Web3ExternalProvider { else if (_transport === Transport.WebSocket) transport = "wss://"; - return `${transport}example.com/`; + return `${transport}127.0.0.1/`; } } @@ -54,9 +92,7 @@ describe('Web3ExternalProvider', () => { const token = 'your-token'; const provider = new MockWeb3ExternalProviderA(network, transport, token); - expect(provider.provider).toBeInstanceOf(WebSocketProvider); }); }); -/* eslint-enable max-classes-per-file */ \ No newline at end of file diff --git a/packages/web3-rpc-providers/test/unit/request.test.ts b/packages/web3-rpc-providers/test/unit/request.test.ts index d0d38ac5e14..6107949b0f1 100644 --- a/packages/web3-rpc-providers/test/unit/request.test.ts +++ b/packages/web3-rpc-providers/test/unit/request.test.ts @@ -14,10 +14,12 @@ 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 { Web3APIPayload, EthExecutionAPI, Web3APIMethod } from "web3-types"; +import { Web3APIPayload, EthExecutionAPI, Web3APIMethod, JsonRpcResponse } from "web3-types"; +import { ResponseError } from "web3-errors"; import { Network, Transport } from "../../src/types"; import { Web3ExternalProvider } from "../../src/web3_provider"; import { QuickNodeRateLimitError } from '../../src/errors'; +import { QuickNodeProvider } from '../../src/web3_provider_quicknode'; jest.mock('web3-providers-ws', () => { return { @@ -79,7 +81,8 @@ describe('Web3ExternalProvider', () => { const result = await provider.request(payload); expect(result).toEqual({ result: 'mock-result' }); }); - it('should return a rate limiting error when code is 429', async () => { + + it('should throw a rate limiting error when status code is 429', async () => { const network: Network = Network.ETH_MAINNET; const transport: Transport = Transport.HTTPS; const token = 'your-token'; @@ -88,17 +91,29 @@ describe('Web3ExternalProvider', () => { request: jest.fn(), }; - const mockResponse = { + // Create a mock ResponseError with status code 429 + // Create a mock JsonRpcResponse to pass to ResponseError + const mockJsonRpcResponse: JsonRpcResponse = { jsonrpc: '2.0', id: '458408f4-7e2c-43f1-b61d-1fe09a9ee25a', error: { code: 429, - message: 'the method eth_stuff does not exist/is not available' - } + message: 'Rate limit exceeded', + }, }; - mockHttpProvider.request.mockResolvedValue(mockResponse); - const provider = new MockWeb3ExternalProvider(network, transport, token); + // Create a mock ResponseError with status code 429 + const mockError = new ResponseError( + mockJsonRpcResponse, + undefined, + undefined, // request can be undefined + 429 // statusCode + ); + + // Mock the request method to throw the ResponseError + mockHttpProvider.request.mockRejectedValue(mockError); + + const provider = new QuickNodeProvider(network, transport, token); (provider as any).provider = mockHttpProvider; const payload: Web3APIPayload> = { @@ -107,4 +122,5 @@ describe('Web3ExternalProvider', () => { }; await expect(provider.request(payload)).rejects.toThrow(QuickNodeRateLimitError); }); + }); \ No newline at end of file