Skip to content

Commit

Permalink
web3 middleware (#6951)
Browse files Browse the repository at this point in the history
* RequestManagerMiddleware

* adding in context

* request manager support

* tests

* lint fix

* lint fix

* sample middleware

* middleware in sample plugin

* middleware test with plugin

* lint fix

* updated Web3Middleware

* extensible middleware

* test update

* lint update

* update

* lint fix
  • Loading branch information
jdevcs authored Apr 18, 2024
1 parent e29deea commit 1ab7a6b
Show file tree
Hide file tree
Showing 8 changed files with 356 additions and 4 deletions.
17 changes: 16 additions & 1 deletion packages/web3-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ 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 { HexString, Transaction } from 'web3-types';
import { HexString, JsonRpcResponse, Transaction, Web3APIMethod, Web3APIRequest, Web3APIReturnType } from 'web3-types';

export type TransactionTypeParser = (
transaction: Transaction,
Expand All @@ -30,3 +30,18 @@ export interface ExtensionObject {
property?: string;
methods: Method[];
}

export interface RequestManagerMiddleware<API> {
processRequest<
AnotherMethod extends Web3APIMethod<API>
>(
request: Web3APIRequest<API, AnotherMethod>,
options?: { [key: string]: unknown }): Promise<Web3APIRequest<API, AnotherMethod>>;

processResponse<
AnotherMethod extends Web3APIMethod<API>,
ResponseType = Web3APIReturnType<API, AnotherMethod>>
(
response: JsonRpcResponse<ResponseType>,
options?: { [key: string]: unknown }): Promise<JsonRpcResponse<ResponseType>>;
}
10 changes: 9 additions & 1 deletion packages/web3-core/src/web3_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -65,6 +65,7 @@ export type Web3ContextInitOptions<
registeredSubscriptions?: RegisteredSubs;
accountProvider?: Web3AccountProvider<Web3BaseWalletAccount>;
wallet?: Web3BaseWallet<Web3BaseWalletAccount>;
requestManagerMiddleware?: RequestManagerMiddleware<API>;
};

// eslint-disable-next-line no-use-before-define
Expand Down Expand Up @@ -129,6 +130,7 @@ export class Web3Context<
registeredSubscriptions,
accountProvider,
wallet,
requestManagerMiddleware
} = providerOrContext as Web3ContextInitOptions<API, RegisteredSubs>;

this.setConfig(config ?? {});
Expand All @@ -138,6 +140,7 @@ export class Web3Context<
new Web3RequestManager<API>(
provider,
config?.enableExperimentalFeatures?.useSubscriptionWhenCheckingBlockTimeout,
requestManagerMiddleware
);

if (subscriptionManager) {
Expand Down Expand Up @@ -352,6 +355,11 @@ export class Web3Context<
this.provider = provider;
return true;
}

public setRequestManagerMiddleware(requestManagerMiddleware: RequestManagerMiddleware<API>){
this.requestManager.setMiddleware(requestManagerMiddleware);
}

/**
* Will return the {@link Web3BatchRequest} constructor.
*/
Expand Down
26 changes: 24 additions & 2 deletions packages/web3-core/src/web3_request_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -73,18 +74,25 @@ export class Web3RequestManager<
}> {
private _provider?: SupportedProviders<API>;
private readonly useRpcCallSpecification?: boolean;
public middleware?: RequestManagerMiddleware<API>;

public constructor(
provider?: SupportedProviders<API> | string,
useRpcCallSpecification?: boolean,
requestManagerMiddleware?: RequestManagerMiddleware<API>
) {
super();

if (!isNullish(provider)) {
this.setProvider(provider);
}
this.useRpcCallSpecification = useRpcCallSpecification;
}

if (!isNullish(requestManagerMiddleware))
this.middleware = requestManagerMiddleware;

}

/**
* Will return all available providers
*/
Expand Down Expand Up @@ -142,6 +150,10 @@ export class Web3RequestManager<
return true;
}

public setMiddleware(requestManagerMiddleware: RequestManagerMiddleware<API>){
this.middleware = requestManagerMiddleware;
}

/**
*
* Will execute a request
Expand All @@ -155,7 +167,17 @@ export class Web3RequestManager<
Method extends Web3APIMethod<API>,
ResponseType = Web3APIReturnType<API, Method>,
>(request: Web3APIRequest<API, Method>): Promise<ResponseType> {
const response = await this._sendRequest<Method, ResponseType>(request);

let requestObj = {...request};

if (!isNullish(this.middleware))
requestObj = await this.middleware.processRequest(requestObj);

let response = await this._sendRequest<Method, ResponseType>(requestObj);

if (!isNullish(this.middleware))
response = await this.middleware.processResponse(response);

if (jsonRpc.isResponseWithResult(response)) {
return response.result;
}
Expand Down
15 changes: 15 additions & 0 deletions packages/web3-core/test/unit/web3_context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ along with web3.js. If not, see <http://www.gnu.org/licenses/>.
// 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<{}> {}
Expand Down Expand Up @@ -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<EthExecutionAPI>
= {
processRequest: jest.fn(async <Method extends Web3APIMethod<EthExecutionAPI>>(request: Web3APIRequest<EthExecutionAPI, Method>) => request),
processResponse: jest.fn(async <Method extends Web3APIMethod<EthExecutionAPI>, ResponseType = Web3APIReturnType<EthExecutionAPI, Method>>(response: JsonRpcResponse<ResponseType>) => response),
};

context.setRequestManagerMiddleware(middleware);
expect(context.requestManager.middleware).toEqual(middleware);
});
});

describe('getContextObject', () => {
Expand Down
144 changes: 144 additions & 0 deletions packages/web3-core/test/unit/web3_middleware_request_manager.test.ts
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

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<API> implements RequestManagerMiddleware<API> {

// eslint-disable-next-line class-methods-use-this
public async processRequest<Method extends Web3APIMethod<API>>(
request: Web3APIRequest<API, Method>
): Promise<Web3APIRequest<API, Method>> {
// 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<API>,
ResponseType = Web3APIReturnType<API, Method>
>(
response: JsonRpcResponse<ResponseType>
): Promise<JsonRpcResponse<ResponseType>> {

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<EthExecutionAPI>;

beforeAll(() => {
requestManagerMiddleware = {
processRequest: jest.fn(async <Method extends Web3APIMethod<EthExecutionAPI>>(request: Web3APIRequest<EthExecutionAPI, Method>) => request),
processResponse: jest.fn(async <Method extends Web3APIMethod<EthExecutionAPI>, ResponseType = Web3APIReturnType<EthExecutionAPI, Method>>(response: JsonRpcResponse<ResponseType>) => response),
};

});

it('should set requestManagerMiddleware via constructor', () => {
const web3RequestManager1: Web3RequestManager = new Web3RequestManager<EthExecutionAPI>(undefined, true, requestManagerMiddleware);

expect(web3RequestManager1.middleware).toBeDefined();
expect(web3RequestManager1.middleware).toEqual(requestManagerMiddleware);
});

it('should set requestManagerMiddleware via set method', () => {

const middleware2: RequestManagerMiddleware<EthExecutionAPI> = new Web3Middleware<EthExecutionAPI>();
const web3RequestManager2: Web3RequestManager = new Web3RequestManager<EthExecutionAPI>('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<EthExecutionAPI>('http://localhost:8080', true, requestManagerMiddleware );

const expectedResponse: JsonRpcResponse<string> = {
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<EthExecutionAPI> = new Web3Middleware<EthExecutionAPI>();

const web3RequestManager3 = new Web3RequestManager<EthExecutionAPI>('http://localhost:8080', true, middleware3);

const expectedResponse: JsonRpcResponse<string> = {
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'],
});

});
});
21 changes: 21 additions & 0 deletions tools/web3-plugin-example/src/custom_rpc_methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ along with web3.js. If not, see <http://www.gnu.org/licenses/>.
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;
Expand All @@ -25,6 +27,24 @@ type CustomRpcApi = {

export class CustomRpcMethodsPlugin extends Web3PluginBase<CustomRpcApi> {
public pluginNamespace = 'customRpcMethods';
public web3Middleware: Web3Middleware<CustomRpcApi> | undefined;

public constructor(testMiddleware = false) {
super();

if (testMiddleware) {
this.web3Middleware = new Web3Middleware<CustomRpcApi>();
}
}

public link(parentContext: Web3Context): void {

if (this.web3Middleware)
parentContext.requestManager.setMiddleware(this.web3Middleware);

super.link(parentContext);
}


public async customRpcMethod() {
return this.requestManager.send({
Expand All @@ -39,6 +59,7 @@ export class CustomRpcMethodsPlugin extends Web3PluginBase<CustomRpcApi> {
params: [parameter1, parameter2],
});
}

}

// Module Augmentation
Expand Down
Loading

1 comment on commit 1ab7a6b

@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: 1ab7a6b Previous: 6c075db Ratio
processingTx 9407 ops/sec (±3.37%) 9301 ops/sec (±4.81%) 0.99
processingContractDeploy 39019 ops/sec (±6.25%) 39129 ops/sec (±7.62%) 1.00
processingContractMethodSend 20315 ops/sec (±6.71%) 19443 ops/sec (±5.19%) 0.96
processingContractMethodCall 40764 ops/sec (±6.10%) 38971 ops/sec (±6.34%) 0.96
abiEncode 45656 ops/sec (±6.92%) 44252 ops/sec (±6.92%) 0.97
abiDecode 30969 ops/sec (±7.99%) 30419 ops/sec (±8.89%) 0.98
sign 1605 ops/sec (±3.74%) 1656 ops/sec (±4.08%) 1.03
verify 378 ops/sec (±0.47%) 373 ops/sec (±0.78%) 0.99

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

Please sign in to comment.