Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Quicknode provider update #7195

Merged
merged 10 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
});

});
Loading