/*
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 {
ContractExecutionError,
InvalidResponseError,
ProviderError,
ResponseError,
rpcErrorsMap,
RpcError,
} from 'web3-errors';
import HttpProvider from 'web3-providers-http';
import WSProvider from 'web3-providers-ws';
import {
EthExecutionAPI,
JsonRpcBatchRequest,
JsonRpcBatchResponse,
JsonRpcPayload,
JsonRpcResponse,
JsonRpcError,
JsonRpcResponseWithResult,
JsonRpcResponseWithError,
SupportedProviders,
Web3APIMethod,
Web3APIPayload,
Web3APIRequest,
Web3APIReturnType,
Web3APISpec,
Web3BaseProvider,
Web3BaseProviderConstructor,
} from 'web3-types';
import { isNullish, isPromise, jsonRpc, isResponseRpcError } from 'web3-utils';
import {
isEIP1193Provider,
isLegacyRequestProvider,
isLegacySendAsyncProvider,
isLegacySendProvider,
isWeb3Provider,
} from './utils.js';
import { Web3EventEmitter } from './web3_event_emitter.js';
import { RequestManagerMiddleware } from './types.js';
export enum Web3RequestManagerEvent {
PROVIDER_CHANGED = 'PROVIDER_CHANGED',
BEFORE_PROVIDER_CHANGE = 'BEFORE_PROVIDER_CHANGE',
}
const availableProviders: {
HttpProvider: Web3BaseProviderConstructor;
WebsocketProvider: Web3BaseProviderConstructor;
} = {
HttpProvider: HttpProvider as Web3BaseProviderConstructor,
WebsocketProvider: WSProvider as Web3BaseProviderConstructor,
};
export class Web3RequestManager<
API extends Web3APISpec = EthExecutionAPI,
> extends Web3EventEmitter<{
[key in Web3RequestManagerEvent]: SupportedProviders | undefined;
}> {
private _provider?: SupportedProviders;
private readonly useRpcCallSpecification?: boolean;
public middleware?: RequestManagerMiddleware;
public constructor(
provider?: SupportedProviders | string,
useRpcCallSpecification?: boolean,
requestManagerMiddleware?: RequestManagerMiddleware,
) {
super();
if (!isNullish(provider)) {
this.setProvider(provider);
}
this.useRpcCallSpecification = useRpcCallSpecification;
if (!isNullish(requestManagerMiddleware)) this.middleware = requestManagerMiddleware;
}
/**
* Will return all available providers
*/
public static get providers() {
return availableProviders;
}
/**
* Will return the current provider.
*
* @returns Returns the current provider
*/
public get provider() {
return this._provider;
}
/**
* Will return all available providers
*/
// eslint-disable-next-line class-methods-use-this
public get providers() {
return availableProviders;
}
/**
* Use to set provider. Provider can be a provider instance or a string.
*
* @param provider - The provider to set
*/
public setProvider(provider?: SupportedProviders | string): boolean {
let newProvider: SupportedProviders | undefined;
// autodetect provider
if (provider && typeof provider === 'string' && this.providers) {
// HTTP
if (/^http(s)?:\/\//i.test(provider)) {
newProvider = new this.providers.HttpProvider(provider);
// WS
} else if (/^ws(s)?:\/\//i.test(provider)) {
newProvider = new this.providers.WebsocketProvider(provider);
} else {
throw new ProviderError(`Can't autodetect provider for "${provider}"`);
}
} else if (isNullish(provider)) {
// In case want to unset the provider
newProvider = undefined;
} else {
newProvider = provider as SupportedProviders;
}
this.emit(Web3RequestManagerEvent.BEFORE_PROVIDER_CHANGE, this._provider);
this._provider = newProvider;
this.emit(Web3RequestManagerEvent.PROVIDER_CHANGED, this._provider);
return true;
}
public setMiddleware(requestManagerMiddleware: RequestManagerMiddleware) {
this.middleware = requestManagerMiddleware;
}
/**
*
* Will execute a request
*
* @param request - {@link Web3APIRequest} The request to send
*
* @returns The response of the request {@link ResponseType}. If there is error
* in the response, will throw an error
*/
public async send<
Method extends Web3APIMethod,
ResponseType = Web3APIReturnType,
>(request: Web3APIRequest): Promise {
const requestObj = { ...request };
let response = await this._sendRequest(requestObj);
if (!isNullish(this.middleware)) response = await this.middleware.processResponse(response);
if (jsonRpc.isResponseWithResult(response)) {
return response.result;
}
throw new ResponseError(response);
}
/**
* Same as send, but, will execute a batch of requests
*
* @param request {@link JsonRpcBatchRequest} The batch request to send
*/
public async sendBatch(request: JsonRpcBatchRequest): Promise> {
const response = await this._sendRequest(request);
return response as JsonRpcBatchResponse;
}
private async _sendRequest<
Method extends Web3APIMethod,
ResponseType = Web3APIReturnType,
>(
request: Web3APIRequest | JsonRpcBatchRequest,
): Promise> {
const { provider } = this;
if (isNullish(provider)) {
throw new ProviderError(
'Provider not available. Use `.setProvider` or `.provider=` to initialize the provider.',
);
}
let payload = (
jsonRpc.isBatchRequest(request)
? jsonRpc.toBatchPayload(request)
: jsonRpc.toPayload(request)
) as JsonRpcPayload;
if (!isNullish(this.middleware)) {
payload = await this.middleware.processRequest(payload);
}
if (isWeb3Provider(provider)) {
let response;
try {
response = await provider.request(
payload as Web3APIPayload,
);
} catch (error) {
// Check if the provider throw an error instead of reject with error
response = error as JsonRpcResponse;
}
return this._processJsonRpcResponse(payload, response, { legacy: false, error: false });
}
if (isEIP1193Provider(provider)) {
return (provider as Web3BaseProvider)
.request(payload as Web3APIPayload)
.then(
res =>
this._processJsonRpcResponse(payload, res, {
legacy: true,
error: false,
}) as JsonRpcResponseWithResult,
)
.catch(error =>
this._processJsonRpcResponse(
payload,
error as JsonRpcResponse,
{ legacy: true, error: true },
),
);
}
// TODO: This could be deprecated and removed.
if (isLegacyRequestProvider(provider)) {
return new Promise>((resolve, reject) => {
const rejectWithError = (err: unknown) => {
reject(
this._processJsonRpcResponse(
payload,
err as JsonRpcResponse,
{
legacy: true,
error: true,
},
),
);
};
const resolveWithResponse = (response: JsonRpcResponse) =>
resolve(
this._processJsonRpcResponse(payload, response, {
legacy: true,
error: false,
}),
);
const result = provider.request(
payload,
// a callback that is expected to be called after getting the response:
(err, response) => {
if (err) {
return rejectWithError(err);
}
return resolveWithResponse(response);
},
);
// Some providers, that follow a previous drafted version of EIP1193, has a `request` function
// that is not defined as `async`, but it returns a promise.
// Such providers would not be picked with if(isEIP1193Provider(provider)) above
// because the `request` function was not defined with `async` and so the function definition is not `AsyncFunction`.
// Like this provider: https://github.dev/NomicFoundation/hardhat/blob/62bea2600785595ba36f2105564076cf5cdf0fd8/packages/hardhat-core/src/internal/core/providers/backwards-compatibility.ts#L19
// So check if the returned result is a Promise, and resolve with it accordingly.
// Note: in this case we expect the callback provided above to never be called.
if (isPromise(result)) {
const responsePromise = result as unknown as Promise<
JsonRpcResponse
>;
responsePromise.then(resolveWithResponse).catch(error => {
try {
// Attempt to process the error response
const processedError = this._processJsonRpcResponse(
payload,
error as JsonRpcResponse,
{ legacy: true, error: true },
);
reject(processedError);
} catch (processingError) {
// Catch any errors that occur during the error processing
reject(processingError);
}
});
}
});
}
// TODO: This could be deprecated and removed.
if (isLegacySendProvider(provider)) {
return new Promise>((resolve, reject): void => {
provider.send(payload, (err, response) => {
if (err) {
return reject(
this._processJsonRpcResponse(
payload,
err as unknown as JsonRpcResponse,
{
legacy: true,
error: true,
},
),
);
}
if (isNullish(response)) {
throw new ResponseError(
{} as never,
'Got a "nullish" response from provider.',
);
}
return resolve(
this._processJsonRpcResponse(payload, response, {
legacy: true,
error: false,
}),
);
});
});
}
// TODO: This could be deprecated and removed.
if (isLegacySendAsyncProvider(provider)) {
return provider
.sendAsync(payload)
.then(response =>
this._processJsonRpcResponse(payload, response, { legacy: true, error: false }),
)
.catch(error =>
this._processJsonRpcResponse(payload, error as JsonRpcResponse, {
legacy: true,
error: true,
}),
);
}
throw new ProviderError('Provider does not have a request or send method to use.');
}
// eslint-disable-next-line class-methods-use-this
private _processJsonRpcResponse(
payload: JsonRpcPayload,
response: JsonRpcResponse,
{ legacy, error }: { legacy: boolean; error: boolean },
): JsonRpcResponse | never {
if (isNullish(response)) {
return this._buildResponse(
payload,
// Some providers uses "null" as valid empty response
// eslint-disable-next-line no-null/no-null
null as unknown as JsonRpcResponse,
error,
);
}
// This is the majority of the cases so check these first
// A valid JSON-RPC response with error object
if (jsonRpc.isResponseWithError(response)) {
// check if its an rpc error
if (
this.useRpcCallSpecification &&
isResponseRpcError(response as JsonRpcResponseWithError)
) {
const rpcErrorResponse = response as JsonRpcResponseWithError;
// check if rpc error flag is on and response error code match an EIP-1474 or a standard rpc error code
if (rpcErrorsMap.get(rpcErrorResponse.error.code)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const Err = rpcErrorsMap.get(rpcErrorResponse.error.code)!.error;
throw new Err(rpcErrorResponse);
} else {
throw new RpcError(rpcErrorResponse);
}
} else if (!Web3RequestManager._isReverted(response)) {
throw new InvalidResponseError(response, payload);
}
}
// This is the majority of the cases so check these first
// A valid JSON-RPC response with result object
if (jsonRpc.isResponseWithResult(response)) {
return response;
}
if ((response as unknown) instanceof Error) {
Web3RequestManager._isReverted(response);
throw response;
}
if (!legacy && jsonRpc.isBatchRequest(payload) && jsonRpc.isBatchResponse(response)) {
return response as JsonRpcBatchResponse;
}
if (legacy && !error && jsonRpc.isBatchRequest(payload)) {
return response as JsonRpcBatchResponse;
}
if (legacy && error && jsonRpc.isBatchRequest(payload)) {
// In case of error batch response we don't want to throw Invalid response
throw response;
}
if (
legacy &&
!jsonRpc.isResponseWithError(response) &&
!jsonRpc.isResponseWithResult(response)
) {
return this._buildResponse(payload, response, error);
}
if (jsonRpc.isBatchRequest(payload) && !Array.isArray(response)) {
throw new ResponseError(response, 'Got normal response for a batch request.');
}
if (!jsonRpc.isBatchRequest(payload) && Array.isArray(response)) {
throw new ResponseError(response, 'Got batch response for a normal request.');
}
throw new ResponseError(response, 'Invalid response');
}
private static _isReverted(
response: JsonRpcResponse,
): boolean {
let error: JsonRpcError | undefined;
if (jsonRpc.isResponseWithError(response)) {
error = (response as JsonRpcResponseWithError).error;
} else if ((response as unknown) instanceof Error) {
error = response as unknown as JsonRpcError;
}
// This message means that there was an error while executing the code of the smart contract
// However, more processing will happen at a higher level to decode the error data,
// according to the Error ABI, if it was available as of EIP-838.
if (error?.message.includes('revert')) throw new ContractExecutionError(error);
return false;
}
// Need to use same types as _processJsonRpcResponse so have to declare as instance method
// eslint-disable-next-line class-methods-use-this
private _buildResponse(
payload: JsonRpcPayload,
response: JsonRpcResponse,
error: boolean,
): JsonRpcResponse {
const res = {
jsonrpc: '2.0',
// eslint-disable-next-line no-nested-ternary
id: jsonRpc.isBatchRequest(payload)
? payload[0].id
: 'id' in payload
? payload.id
: // Have to use the null here explicitly
// eslint-disable-next-line no-null/no-null
null,
};
if (error) {
return {
...res,
error: response as unknown,
} as JsonRpcResponse;
}
return {
...res,
result: response as unknown,
} as JsonRpcResponse;
}
}