Skip to content

Commit

Permalink
Quicknode provider update (#7195)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jdevcs authored Aug 20, 2024
1 parent 0db2b18 commit d9d0391
Show file tree
Hide file tree
Showing 13 changed files with 125 additions and 39 deletions.
6 changes: 5 additions & 1 deletion packages/web3-errors/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,8 @@ Documentation:

- Fixed the undefined data in `Eip838ExecutionError` constructor (#6905)

## [Unreleased]
## [Unreleased]

### Added

- Added optional `statusCode` property of response in ResponseError.
5 changes: 4 additions & 1 deletion packages/web3-errors/src/errors/response_errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,13 @@ export class ResponseError<ErrorType = unknown, RequestType = unknown> extends B
public code = ERR_RESPONSE;
public data?: ErrorType | ErrorType[];
public request?: JsonRpcPayload<RequestType>;
public statusCode?: number;

public constructor(
response: JsonRpcResponse<unknown, ErrorType>,
message?: string,
request?: JsonRpcPayload<RequestType>,
statusCode?: number
) {
super(
message ??
Expand All @@ -66,6 +68,7 @@ export class ResponseError<ErrorType = unknown, RequestType = unknown> extends B
: response?.error?.data;
}

this.statusCode = statusCode;
this.request = request;
let errorOrErrors: JsonRpcError | JsonRpcError[] | undefined;
if (`error` in response) {
Expand All @@ -82,7 +85,7 @@ export class ResponseError<ErrorType = unknown, RequestType = unknown> 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 };
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ exports[`errors InvalidResponseError should have valid json structure 1`] = `
"message": "Returned error: error message",
"name": "InvalidResponseError",
"request": undefined,
"statusCode": undefined,
}
`;

Expand Down Expand Up @@ -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,
}
`;

Expand All @@ -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,
}
`;

Expand All @@ -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,
}
`;

Expand Down
6 changes: 5 additions & 1 deletion packages/web3-providers-http/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,8 @@ Documentation:

- Fix issue lquixada/cross-fetch#78, enabling to run web3.js in service worker (#6463)

## [Unreleased]
## [Unreleased]

### Added

- Added `statusCode` of response in ResponseError, `statusCode` is optional property in ResponseError.
2 changes: 1 addition & 1 deletion packages/web3-providers-http/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ResultType>;
Expand Down
10 changes: 7 additions & 3 deletions packages/web3-rpc-providers/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<ResultType>` to `Promise<JsonRpcResponseWithResult<ResultType>>` (#7102)

## [Unreleased]
## [Unreleased]

### Added

- Updated rate limit error of QuickNode provider for HTTP transport
1 change: 1 addition & 0 deletions packages/web3-rpc-providers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 2 additions & 3 deletions packages/web3-rpc-providers/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,12 @@ along with web3.js. If not, see <http://www.gnu.org/licenses/>.
*/

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);
}
}
1 change: 1 addition & 0 deletions packages/web3-rpc-providers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
25 changes: 8 additions & 17 deletions packages/web3-rpc-providers/src/web3_provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ along with web3.js. If not, see <http://www.gnu.org/licenses/>.

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,
Expand All @@ -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
Expand All @@ -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;
Expand All @@ -74,18 +72,11 @@ API extends Web3APISpec = EthExecutionAPI,
): Promise<JsonRpcResponseWithResult<ResultType>> {

if (this.transport === Transport.HTTPS) {
const res = await ( (this.provider as HttpProvider).request(payload, requestOptions)) as unknown as JsonRpcResponseWithResult<ResultType>;

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<ResultType>;
}

return (this.provider as WebSocketProvider).request(payload);

}

public getStatus(): Web3ProviderStatus {
Expand Down
25 changes: 24 additions & 1 deletion packages/web3-rpc-providers/src/web3_provider_quicknode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@ You should have received a copy of the GNU Lesser General Public License
along with web3.js. If not, see <http://www.gnu.org/licenses/>.
*/

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,
Expand All @@ -32,6 +37,24 @@ export class QuickNodeProvider extends Web3ExternalProvider {

}

public async request<
Method extends Web3APIMethod<API>,
ResultType = Web3APIReturnType<API, Method>,
>(
payload: Web3APIPayload<EthExecutionAPI, Method>,
requestOptions?: RequestInit,
): Promise<JsonRpcResponseWithResult<ResultType>> {

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,
Expand Down
44 changes: 40 additions & 4 deletions packages/web3-rpc-providers/test/unit/constructor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <http://www.gnu.org/licenses/>.
*/
/* 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, "");
Expand All @@ -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/`;
}
}

Expand All @@ -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 */
30 changes: 23 additions & 7 deletions packages/web3-rpc-providers/test/unit/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <http://www.gnu.org/licenses/>.
*/
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 {
Expand Down Expand Up @@ -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';
Expand All @@ -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<EthExecutionAPI, Web3APIMethod<EthExecutionAPI>> = {
Expand All @@ -107,4 +122,5 @@ describe('Web3ExternalProvider', () => {
};
await expect(provider.request(payload)).rejects.toThrow(QuickNodeRateLimitError);
});

});

1 comment on commit d9d0391

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Benchmark suite Current: d9d0391 Previous: 0db2b18 Ratio
processingTx 21240 ops/sec (±7.90%) 21418 ops/sec (±7.87%) 1.01
processingContractDeploy 38066 ops/sec (±7.88%) 40091 ops/sec (±5.61%) 1.05
processingContractMethodSend 15268 ops/sec (±7.65%) 15185 ops/sec (±10.09%) 0.99
processingContractMethodCall 27168 ops/sec (±7.01%) 27867 ops/sec (±6.56%) 1.03
abiEncode 41584 ops/sec (±6.92%) 41653 ops/sec (±7.57%) 1.00
abiDecode 29253 ops/sec (±7.27%) 28976 ops/sec (±7.90%) 0.99
sign 1493 ops/sec (±1.31%) 1503 ops/sec (±0.71%) 1.01
verify 360 ops/sec (±0.75%) 351 ops/sec (±2.70%) 0.97

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.