diff --git a/packages/web3-core/src/types.ts b/packages/web3-core/src/types.ts index 3f4e66d1f52..890a5e20c5a 100644 --- a/packages/web3-core/src/types.ts +++ b/packages/web3-core/src/types.ts @@ -15,7 +15,7 @@ You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ -import { HexString, Transaction } from 'web3-types'; +import { HexString, JsonRpcResponse, Transaction, Web3APIMethod, Web3APIRequest, Web3APIReturnType } from 'web3-types'; export type TransactionTypeParser = ( transaction: Transaction, @@ -30,3 +30,18 @@ export interface ExtensionObject { property?: string; methods: Method[]; } + +export interface RequestManagerMiddleware { + processRequest< + AnotherMethod extends Web3APIMethod + >( + request: Web3APIRequest, + options?: { [key: string]: unknown }): Promise>; + + processResponse< + AnotherMethod extends Web3APIMethod, + ResponseType = Web3APIReturnType> + ( + response: JsonRpcResponse, + options?: { [key: string]: unknown }): Promise>; + } \ No newline at end of file diff --git a/packages/web3-core/src/web3_context.ts b/packages/web3-core/src/web3_context.ts index 76afcaf8875..b50f8ba30f7 100644 --- a/packages/web3-core/src/web3_context.ts +++ b/packages/web3-core/src/web3_context.ts @@ -25,7 +25,7 @@ import { isNullish } from 'web3-utils'; import { BaseTransaction, TransactionFactory } from 'web3-eth-accounts'; import { isSupportedProvider } from './utils.js'; // eslint-disable-next-line import/no-cycle -import { ExtensionObject } from './types.js'; +import { ExtensionObject, RequestManagerMiddleware } from './types.js'; import { Web3BatchRequest } from './web3_batch_request.js'; // eslint-disable-next-line import/no-cycle import { Web3Config, Web3ConfigEvent, Web3ConfigOptions } from './web3_config.js'; @@ -65,6 +65,7 @@ export type Web3ContextInitOptions< registeredSubscriptions?: RegisteredSubs; accountProvider?: Web3AccountProvider; wallet?: Web3BaseWallet; + requestManagerMiddleware?: RequestManagerMiddleware; }; // eslint-disable-next-line no-use-before-define @@ -129,6 +130,7 @@ export class Web3Context< registeredSubscriptions, accountProvider, wallet, + requestManagerMiddleware } = providerOrContext as Web3ContextInitOptions; this.setConfig(config ?? {}); @@ -138,6 +140,7 @@ export class Web3Context< new Web3RequestManager( provider, config?.enableExperimentalFeatures?.useSubscriptionWhenCheckingBlockTimeout, + requestManagerMiddleware ); if (subscriptionManager) { @@ -352,6 +355,11 @@ export class Web3Context< this.provider = provider; return true; } + + public setRequestManagerMiddleware(requestManagerMiddleware: RequestManagerMiddleware){ + this.requestManager.setMiddleware(requestManagerMiddleware); + } + /** * Will return the {@link Web3BatchRequest} constructor. */ diff --git a/packages/web3-core/src/web3_request_manager.ts b/packages/web3-core/src/web3_request_manager.ts index 3c6cabd0182..dc12d919fd0 100644 --- a/packages/web3-core/src/web3_request_manager.ts +++ b/packages/web3-core/src/web3_request_manager.ts @@ -52,6 +52,7 @@ import { isWeb3Provider, } from './utils.js'; import { Web3EventEmitter } from './web3_event_emitter.js'; +import { RequestManagerMiddleware } from './types.js'; export enum Web3RequestManagerEvent { PROVIDER_CHANGED = 'PROVIDER_CHANGED', @@ -73,9 +74,12 @@ export class Web3RequestManager< }> { private _provider?: SupportedProviders; private readonly useRpcCallSpecification?: boolean; + public middleware?: RequestManagerMiddleware; + public constructor( provider?: SupportedProviders | string, useRpcCallSpecification?: boolean, + requestManagerMiddleware?: RequestManagerMiddleware ) { super(); @@ -83,8 +87,12 @@ export class Web3RequestManager< this.setProvider(provider); } this.useRpcCallSpecification = useRpcCallSpecification; - } + if (!isNullish(requestManagerMiddleware)) + this.middleware = requestManagerMiddleware; + + } + /** * Will return all available providers */ @@ -142,6 +150,10 @@ export class Web3RequestManager< return true; } + public setMiddleware(requestManagerMiddleware: RequestManagerMiddleware){ + this.middleware = requestManagerMiddleware; + } + /** * * Will execute a request @@ -155,7 +167,17 @@ export class Web3RequestManager< Method extends Web3APIMethod, ResponseType = Web3APIReturnType, >(request: Web3APIRequest): Promise { - const response = await this._sendRequest(request); + + let requestObj = {...request}; + + if (!isNullish(this.middleware)) + requestObj = await this.middleware.processRequest(requestObj); + + let response = await this._sendRequest(requestObj); + + if (!isNullish(this.middleware)) + response = await this.middleware.processResponse(response); + if (jsonRpc.isResponseWithResult(response)) { return response.result; } diff --git a/packages/web3-core/test/unit/web3_context.test.ts b/packages/web3-core/test/unit/web3_context.test.ts index 3c433f7259a..186122df54e 100644 --- a/packages/web3-core/test/unit/web3_context.test.ts +++ b/packages/web3-core/test/unit/web3_context.test.ts @@ -18,8 +18,10 @@ along with web3.js. If not, see . // eslint-disable-next-line max-classes-per-file import { ExistingPluginNamespaceError } from 'web3-errors'; import HttpProvider from 'web3-providers-http'; +import { EthExecutionAPI, JsonRpcResponse, Web3APIMethod, Web3APIRequest, Web3APIReturnType } from 'web3-types'; import { Web3Context, Web3PluginBase } from '../../src/web3_context'; import { Web3RequestManager } from '../../src/web3_request_manager'; +import { RequestManagerMiddleware } from '../../src/types'; // eslint-disable-next-line @typescript-eslint/ban-types class Context1 extends Web3Context<{}> {} @@ -63,6 +65,19 @@ describe('Web3Context', () => { expect(context.currentProvider).toBeInstanceOf(HttpProvider); }); + + it('should set middleware for the request manager', () => { + const context = new Web3Context('http://test.com'); + + const middleware: RequestManagerMiddleware + = { + processRequest: jest.fn(async >(request: Web3APIRequest) => request), + processResponse: jest.fn(async , ResponseType = Web3APIReturnType>(response: JsonRpcResponse) => response), + }; + + context.setRequestManagerMiddleware(middleware); + expect(context.requestManager.middleware).toEqual(middleware); + }); }); describe('getContextObject', () => { diff --git a/packages/web3-core/test/unit/web3_middleware_request_manager.test.ts b/packages/web3-core/test/unit/web3_middleware_request_manager.test.ts new file mode 100644 index 00000000000..d240831d91b --- /dev/null +++ b/packages/web3-core/test/unit/web3_middleware_request_manager.test.ts @@ -0,0 +1,144 @@ +/* +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, JsonRpcResponse, Web3APIMethod, Web3APIRequest, Web3APIReturnType } from 'web3-types'; +import { jsonRpc } from 'web3-utils'; +import { RequestManagerMiddleware } from '../../src/types'; +import { Web3RequestManager } from '../../src/web3_request_manager'; + +class Web3Middleware implements RequestManagerMiddleware { + + // eslint-disable-next-line class-methods-use-this + public async processRequest>( + request: Web3APIRequest + ): Promise> { + // Implement the processRequest logic here + + let requestObj = {...request}; + if (request.method === 'eth_call' && Array.isArray(request.params)) { + requestObj = { + ...requestObj, + params: [...request.params, '0x0', '0x1'], + }; + } + + return Promise.resolve(requestObj); + } + + // eslint-disable-next-line class-methods-use-this + public async processResponse< + Method extends Web3APIMethod, + ResponseType = Web3APIReturnType + >( + response: JsonRpcResponse + ): Promise> { + + let responseObj = {...response}; + if (!jsonRpc.isBatchResponse(responseObj) && responseObj.id === 1) { + responseObj = { + ...responseObj, + result: '0x6a756e616964' as any, + }; + } + + return Promise.resolve(responseObj); + } +} + +describe('Request Manager Middleware', () => { + let requestManagerMiddleware: RequestManagerMiddleware; + + beforeAll(() => { + requestManagerMiddleware = { + processRequest: jest.fn(async >(request: Web3APIRequest) => request), + processResponse: jest.fn(async , ResponseType = Web3APIReturnType>(response: JsonRpcResponse) => response), + }; + + }); + + it('should set requestManagerMiddleware via constructor', () => { + const web3RequestManager1: Web3RequestManager = new Web3RequestManager(undefined, true, requestManagerMiddleware); + + expect(web3RequestManager1.middleware).toBeDefined(); + expect(web3RequestManager1.middleware).toEqual(requestManagerMiddleware); + }); + + it('should set requestManagerMiddleware via set method', () => { + + const middleware2: RequestManagerMiddleware = new Web3Middleware(); + const web3RequestManager2: Web3RequestManager = new Web3RequestManager('http://localhost:8181'); + web3RequestManager2.setMiddleware(middleware2); + + expect(web3RequestManager2.middleware).toBeDefined(); + expect(web3RequestManager2.middleware).toEqual(middleware2); + }); + + it('should call processRequest and processResponse functions of requestManagerMiddleware', async () => { + + const web3RequestManager3 = new Web3RequestManager('http://localhost:8080', true, requestManagerMiddleware ); + + const expectedResponse: JsonRpcResponse = { + jsonrpc: '2.0', + id: 1, + result: '0x0', + }; + + jest.spyOn(web3RequestManager3 as any, '_sendRequest').mockResolvedValue(expectedResponse); + + const request = { + id: 1, + method: 'eth_call', + params: [], + }; + + await web3RequestManager3.send(request); + + expect(requestManagerMiddleware.processRequest).toHaveBeenCalledWith(request); + expect(requestManagerMiddleware.processResponse).toHaveBeenCalled(); + }); + + it('should allow modification of request and response', async () => { + + const middleware3: RequestManagerMiddleware = new Web3Middleware(); + + const web3RequestManager3 = new Web3RequestManager('http://localhost:8080', true, middleware3); + + const expectedResponse: JsonRpcResponse = { + jsonrpc: '2.0', + id: 1, + result: '0x0', + }; + + const mockSendRequest = jest.spyOn(web3RequestManager3 as any, '_sendRequest'); + mockSendRequest.mockResolvedValue(expectedResponse); + + const request = { + id: 1, + method: 'eth_call', + params: ['0x3'], + }; + + const response = await web3RequestManager3.send(request); + expect(response).toBe('0x6a756e616964'); + + expect(mockSendRequest).toHaveBeenCalledWith({ + ...request, + params: [...request.params, '0x0', '0x1'], + }); + + }); +}); diff --git a/tools/web3-plugin-example/src/custom_rpc_methods.ts b/tools/web3-plugin-example/src/custom_rpc_methods.ts index 101816a9c27..ed1c2fbee23 100644 --- a/tools/web3-plugin-example/src/custom_rpc_methods.ts +++ b/tools/web3-plugin-example/src/custom_rpc_methods.ts @@ -17,6 +17,8 @@ along with web3.js. If not, see . import { Web3PluginBase } from 'web3-core'; // eslint-disable-next-line require-extensions/require-extensions import { Web3Context } from './reexported_web3_context'; +// eslint-disable-next-line require-extensions/require-extensions +import { Web3Middleware } from './middleware'; type CustomRpcApi = { custom_rpc_method: () => string; @@ -25,6 +27,24 @@ type CustomRpcApi = { export class CustomRpcMethodsPlugin extends Web3PluginBase { public pluginNamespace = 'customRpcMethods'; + public web3Middleware: Web3Middleware | undefined; + + public constructor(testMiddleware = false) { + super(); + + if (testMiddleware) { + this.web3Middleware = new Web3Middleware(); + } + } + + public link(parentContext: Web3Context): void { + + if (this.web3Middleware) + parentContext.requestManager.setMiddleware(this.web3Middleware); + + super.link(parentContext); + } + public async customRpcMethod() { return this.requestManager.send({ @@ -39,6 +59,7 @@ export class CustomRpcMethodsPlugin extends Web3PluginBase { params: [parameter1, parameter2], }); } + } // Module Augmentation diff --git a/tools/web3-plugin-example/src/middleware.ts b/tools/web3-plugin-example/src/middleware.ts new file mode 100644 index 00000000000..3af435fd025 --- /dev/null +++ b/tools/web3-plugin-example/src/middleware.ts @@ -0,0 +1,61 @@ +/* +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 { RequestManagerMiddleware } from "web3-core"; +import { Web3APIMethod, Web3APIRequest, Web3APIReturnType, JsonRpcResponse } from "web3-types"; +import { jsonRpc } from "web3-utils"; + +export class Web3Middleware implements RequestManagerMiddleware { + + // eslint-disable-next-line class-methods-use-this + public async processRequest>( + request: Web3APIRequest + ): Promise> { + + // add your custom logic here for processing requests + let reqObj = {...request}; + if (reqObj.method === 'eth_call' && Array.isArray(reqObj.params)) { + reqObj = { + ...reqObj, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + params: [...reqObj.params, '0x0', '0x1'], + }; + } + + return Promise.resolve(reqObj); + } + + // eslint-disable-next-line class-methods-use-this + public async processResponse< + Method extends Web3APIMethod, + ResponseType = Web3APIReturnType + >( + response: JsonRpcResponse + ): Promise> { + + // add your custom logic here for processing responses, following is just a demo + let resObj = {...response}; + if (!jsonRpc.isBatchResponse(resObj) && resObj.id === 1) { + resObj = { + ...resObj, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + result: '0x6a756e616964' as any, + }; + } + + return Promise.resolve(resObj); + } +} \ No newline at end of file diff --git a/tools/web3-plugin-example/test/unit/middleware.test.ts b/tools/web3-plugin-example/test/unit/middleware.test.ts new file mode 100644 index 00000000000..717ddc8856d --- /dev/null +++ b/tools/web3-plugin-example/test/unit/middleware.test.ts @@ -0,0 +1,66 @@ +/* +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 Web3, { JsonRpcResponse, TransactionCall } from 'web3'; +import { CustomRpcMethodsPlugin } from '../../src/custom_rpc_methods'; + + +describe('CustomRpcMethodsPlugin Middleware', () => { + it('should modify request and response using middleware plugin', async () => { + + const web3 = new Web3('http://127.0.0.1:8545'); + const plugin = new CustomRpcMethodsPlugin(true); + + // Test mocks and spy - code block start + const expectedResponse: JsonRpcResponse = { + jsonrpc: '2.0', + id: 1, + result: '0x0', + }; + + const mockSendRequest = jest.spyOn(web3.requestManager as any, '_sendRequest'); + mockSendRequest.mockResolvedValue(expectedResponse); + // Test mocks and spy - code block end + + web3.registerPlugin(plugin); + + const transaction: TransactionCall = { + from: '0xee815C0a7cD0Ab35273Bc5943a3c6839a680Eaf0', + to: '0xe3342ae375e9B02F7D5513a1BB2276438D193e15', + type: '0x0', + data: '0x', + nonce: '0x4', + chain: 'mainnet', + hardfork: 'berlin', + chainId: '0x1', + }; + const result = await web3.eth.call(transaction); + expect(result).toBe('0x6a756e616964'); // result modified by response processor , so its 0x6a756e616964 instead of 0x0 + + const expectedCall = { + method: "eth_call", + params: [ + {...transaction}, + "latest", + "0x0", // added by middleware by request processor + "0x1", // added by middleware by request processor + ], + }; + expect(mockSendRequest).toHaveBeenCalledWith(expectedCall); + + }); +});