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

web3 middleware #6951

Merged
merged 16 commits into from
Apr 18, 2024
13 changes: 12 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,14 @@ export interface ExtensionObject {
property?: string;
methods: Method[];
}

export interface RequestManagerMiddleware<API> {
processRequest<
Method extends Web3APIMethod<API>
>(request: Web3APIRequest<API, Method>): Promise<Web3APIRequest<API, Method>>;

processResponse<
Method extends Web3APIMethod<API>,
ResponseType = Web3APIReturnType<API, Method>>
(response: JsonRpcResponse<ResponseType>): 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))
jdevcs marked this conversation as resolved.
Show resolved Hide resolved
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
141 changes: 141 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,141 @@
/*
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> {
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);
}

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'],
});

});
});
Loading