From e8d579ce6b82a3ae4184d925d1b9cfd87629717d Mon Sep 17 00:00:00 2001 From: Muhammad Altabba <24407834+Muhammad-Altabba@users.noreply.github.com> Date: Sat, 24 Jun 2023 02:58:01 +0200 Subject: [PATCH] Refactor subscription's logic (#6210) * refactor subscription's logic - Moved subscribing to provider events from Web3Subscription to Web3SubscriptionManager - subscribe and un subscribe called at Web3Subscription now is the same as calling them on Web3SubscriptionManager - Web3Subscription is lined now to Web3SubscriptionManager instead of directly to Web3RequestManager - update test cases * add `SimpleProvider` the base of `EIP1193Provider` * update CHANGELOG.md * enable backward compatibility for subscriptions + mark the obsolete as deprecated * add `removeListener` events to `EIP1193Provider` * Fix some events types at `SocketProvider` * add and fix old test cases for multiple subscriptions --- packages/web3-core/CHANGELOG.md | 11 ++ packages/web3-core/src/utils.ts | 3 +- packages/web3-core/src/web3_context.ts | 10 +- .../src/web3_subscription_manager.ts | 123 ++++++++++--- packages/web3-core/src/web3_subscriptions.ts | 161 +++++++++++------- .../__snapshots__/web3_context.test.ts.snap | 1 + .../test/unit/web3_subscription.test.ts | 92 ++++++++-- .../unit/web3_subscription_manager.test.ts | 73 +++++--- .../web3_subscription_old_providers.test.ts | 54 ++++-- .../web3-eth-accounts/test/config/setup.js | 4 + packages/web3-eth-contract/src/contract.ts | 17 +- .../web3-eth-contract/src/log_subscription.ts | 34 +++- packages/web3-eth/src/web3_eth.ts | 6 +- packages/web3-eth/test/integration/setup.js | 2 +- .../integration/subscription_heads.test.ts | 21 ++- .../subscription_on_2_events.test.ts | 114 +++++++++++++ .../test/unit/web3_eth_subscription.test.ts | 17 +- packages/web3-types/CHANGELOG.md | 9 + packages/web3-types/src/web3_base_provider.ts | 34 +++- packages/web3-utils/src/socket_provider.ts | 30 +++- packages/web3/test/e2e/e2e_utils.ts | 3 +- 21 files changed, 645 insertions(+), 174 deletions(-) create mode 100644 packages/web3-eth/test/integration/subscription_on_2_events.test.ts diff --git a/packages/web3-core/CHANGELOG.md b/packages/web3-core/CHANGELOG.md index 9e89681e04d..edfab2e0d46 100644 --- a/packages/web3-core/CHANGELOG.md +++ b/packages/web3-core/CHANGELOG.md @@ -121,6 +121,17 @@ Documentation: ## [Unreleased] +### Added + +- Web3Subscription constructor accept a Subscription Manager (as an alternative to accepting Request Manager that is now marked marked as deprecated) (#6210) + +### Changed + +- Web3Subscription constructor overloading that accept a Request Manager is marked as deprecated (#6210) + ### Fixed - Fixed Batch requests erroring out on one request (#6164) +- Fixed the issue: Subscribing to multiple blockchain events causes every listener to be fired for every registered event (#6210) +- Fixed the issue: Unsubscribe at a Web3Subscription class will still have the id of the subscription at the Web3SubscriptionManager (#6210) +- Fixed the issue: A call to the provider is made for every subscription object (#6210) diff --git a/packages/web3-core/src/utils.ts b/packages/web3-core/src/utils.ts index 30eafa16fe2..3107da22a12 100644 --- a/packages/web3-core/src/utils.ts +++ b/packages/web3-core/src/utils.ts @@ -54,7 +54,8 @@ export const isLegacySendAsyncProvider = ( export const isSupportedProvider = ( provider: SupportedProviders, ): provider is SupportedProviders => - Web3BaseProvider.isWeb3Provider(provider) || + isWeb3Provider(provider) || + isEIP1193Provider(provider) || isLegacyRequestProvider(provider) || isLegacySendAsyncProvider(provider) || isLegacySendProvider(provider); diff --git a/packages/web3-core/src/web3_context.ts b/packages/web3-core/src/web3_context.ts index 1b654822af4..0038b59fb65 100644 --- a/packages/web3-core/src/web3_context.ts +++ b/packages/web3-core/src/web3_context.ts @@ -96,7 +96,7 @@ export class Web3Context< public static givenProvider?: SupportedProviders; public readonly providers = Web3RequestManager.providers; protected _requestManager: Web3RequestManager; - protected _subscriptionManager?: Web3SubscriptionManager; + protected _subscriptionManager: Web3SubscriptionManager; protected _accountProvider?: Web3AccountProvider; protected _wallet?: Web3BaseWallet; @@ -146,10 +146,10 @@ export class Web3Context< if (subscriptionManager) { this._subscriptionManager = subscriptionManager; - } else if (registeredSubscriptions) { + } else { this._subscriptionManager = new Web3SubscriptionManager( this.requestManager, - registeredSubscriptions, + registeredSubscriptions ?? ({} as RegisteredSubs), ); } @@ -195,8 +195,7 @@ export class Web3Context< provider: this.provider, requestManager: this.requestManager, subscriptionManager: this.subscriptionManager, - registeredSubscriptions: this.subscriptionManager - ?.registeredSubscriptions as RegisteredSubs, + registeredSubscriptions: this.subscriptionManager?.registeredSubscriptions, providers: this.providers, wallet: this.wallet, accountProvider: this.accountProvider, @@ -231,6 +230,7 @@ export class Web3Context< this.setConfig(parentContext.config); this._requestManager = parentContext.requestManager; this.provider = parentContext.provider; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment this._subscriptionManager = parentContext.subscriptionManager; this._wallet = parentContext.wallet; this._accountProvider = parentContext._accountProvider; diff --git a/packages/web3-core/src/web3_subscription_manager.ts b/packages/web3-core/src/web3_subscription_manager.ts index 95012249d7c..8e76498e3c2 100644 --- a/packages/web3-core/src/web3_subscription_manager.ts +++ b/packages/web3-core/src/web3_subscription_manager.ts @@ -15,11 +15,22 @@ You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ -import { DataFormat, DEFAULT_RETURN_FORMAT, Web3APISpec } from 'web3-types'; +import { + DataFormat, + DEFAULT_RETURN_FORMAT, + EIP1193Provider, + JsonRpcNotification, + JsonRpcSubscriptionResult, + JsonRpcSubscriptionResultOld, + Log, + Web3APISpec, + Web3BaseProvider, +} from 'web3-types'; import { ProviderError, SubscriptionError } from 'web3-errors'; import { isNullish } from 'web3-utils'; import { isSupportSubscriptions } from './utils.js'; import { Web3RequestManager, Web3RequestManagerEvent } from './web3_request_manager.js'; +// eslint-disable-next-line import/no-cycle import { Web3SubscriptionConstructor } from './web3_subscriptions.js'; type ShouldUnsubscribeCondition = ({ @@ -31,8 +42,10 @@ type ShouldUnsubscribeCondition = ({ }) => boolean | undefined; export class Web3SubscriptionManager< - API extends Web3APISpec, - RegisteredSubs extends { [key: string]: Web3SubscriptionConstructor }, + API extends Web3APISpec = Web3APISpec, + RegisteredSubs extends { [key: string]: Web3SubscriptionConstructor } = { + [key: string]: Web3SubscriptionConstructor; + }, > { private readonly _subscriptions: Map< string, @@ -41,8 +54,8 @@ export class Web3SubscriptionManager< /** * - * @param requestManager - * @param registeredSubscriptions + * @param - requestManager + * @param - registeredSubscriptions * * @example * ```ts @@ -50,9 +63,22 @@ export class Web3SubscriptionManager< * const subscriptionManager = new Web3SubscriptionManager(requestManager, {}); * ``` */ + public constructor( + requestManager: Web3RequestManager, + registeredSubscriptions: RegisteredSubs, + ); + /** + * @deprecated This constructor overloading should not be used + */ + public constructor( + requestManager: Web3RequestManager, + registeredSubscriptions: RegisteredSubs, + tolerateUnlinkedSubscription: boolean, + ); public constructor( public readonly requestManager: Web3RequestManager, public readonly registeredSubscriptions: RegisteredSubs, + private readonly tolerateUnlinkedSubscription: boolean = false, ) { this.requestManager.on(Web3RequestManagerEvent.BEFORE_PROVIDER_CHANGE, async () => { await this.unsubscribe(); @@ -60,15 +86,65 @@ export class Web3SubscriptionManager< this.requestManager.on(Web3RequestManagerEvent.PROVIDER_CHANGED, () => { this.clear(); + this.listenToProviderEvents(); }); + + this.listenToProviderEvents(); } + private listenToProviderEvents() { + const providerAsWebProvider = this.requestManager.provider as Web3BaseProvider; + if ( + !this.requestManager.provider || + (typeof providerAsWebProvider?.supportsSubscriptions === 'function' && + !providerAsWebProvider?.supportsSubscriptions()) + ) { + return; + } + + if (typeof (this.requestManager.provider as EIP1193Provider).on === 'function') { + if ( + typeof (this.requestManager.provider as EIP1193Provider).request === 'function' + ) { + // Listen to provider messages and data + (this.requestManager.provider as EIP1193Provider).on( + 'message', + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument + (message: any) => this.messageListener(message), + ); + } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument + providerAsWebProvider.on('data', (data: any) => this.messageListener(data)); + } + } + } + + protected messageListener( + data?: + | JsonRpcSubscriptionResult + | JsonRpcSubscriptionResultOld + | JsonRpcNotification, + ) { + if (!data) { + throw new SubscriptionError('Should not call messageListener with no data. Type was'); + } + const subscriptionId = + (data as JsonRpcNotification).params?.subscription || + (data as JsonRpcSubscriptionResultOld).data?.subscription || + (data as JsonRpcSubscriptionResult).id?.toString(16); + + // Process if the received data is related to a subscription + if (subscriptionId) { + const sub = this._subscriptions.get(subscriptionId); + sub?.processSubscriptionData(data); + } + } /** * Will create a new subscription * * @param name - The subscription you want to subscribe to - * @param args (optional) - Optional additional parameters, depending on the subscription type - * @param returnFormat ({@link DataFormat} defaults to {@link DEFAULT_RETURN_FORMAT}) - Specifies how the return data from the call should be formatted. + * @param args - Optional additional parameters, depending on the subscription type + * @param returnFormat- ({@link DataFormat} defaults to {@link DEFAULT_RETURN_FORMAT}) - Specifies how the return data from the call should be formatted. * * Will subscribe to a specific topic (note: name) * @returns The subscription object @@ -78,19 +154,16 @@ export class Web3SubscriptionManager< args?: ConstructorParameters[0], returnFormat: DataFormat = DEFAULT_RETURN_FORMAT, ): Promise> { - if (!this.requestManager.provider) { - throw new ProviderError('Provider not available'); - } - const Klass: RegisteredSubs[T] = this.registeredSubscriptions[name]; if (!Klass) { throw new SubscriptionError('Invalid subscription type'); } + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument const subscription = new Klass(args ?? undefined, { - requestManager: this.requestManager, + subscriptionManager: this as Web3SubscriptionManager, returnFormat, - }) as InstanceType; + } as any) as InstanceType; await this.addSubscription(subscription); @@ -111,6 +184,10 @@ export class Web3SubscriptionManager< * @param sub - A {@link Web3Subscription} object */ public async addSubscription(sub: InstanceType) { + if (!this.requestManager.provider) { + throw new ProviderError('Provider not available'); + } + if (!this.supportsSubscriptions()) { throw new SubscriptionError('The current provider does not support subscriptions'); } @@ -119,34 +196,36 @@ export class Web3SubscriptionManager< throw new SubscriptionError(`Subscription with id "${sub.id}" already exists`); } - await sub.subscribe(); + await sub.sendSubscriptionRequest(); if (isNullish(sub.id)) { throw new SubscriptionError('Subscription is not subscribed yet.'); } this._subscriptions.set(sub.id, sub); + + return sub.id; } + /** * Will clear a subscription * * @param id - The subscription of type {@link Web3Subscription} to remove */ - public async removeSubscription(sub: InstanceType) { - if (isNullish(sub.id)) { + const { id } = sub; + + if (isNullish(id)) { throw new SubscriptionError( 'Subscription is not subscribed yet. Or, had already been unsubscribed but not through the Subscription Manager.', ); } - if (!this._subscriptions.has(sub.id)) { - throw new SubscriptionError( - `Subscription with id "${sub.id.toString()}" does not exists`, - ); + if (!this._subscriptions.has(id) && !this.tolerateUnlinkedSubscription) { + throw new SubscriptionError(`Subscription with id "${id.toString()}" does not exists`); } - const { id } = sub; - await sub.unsubscribe(); + + await sub.sendUnsubscribeRequest(); this._subscriptions.delete(id); return id; } diff --git a/packages/web3-core/src/web3_subscriptions.ts b/packages/web3-core/src/web3_subscriptions.ts index 5a7644a11a4..a657920fa3d 100644 --- a/packages/web3-core/src/web3_subscriptions.ts +++ b/packages/web3-core/src/web3_subscriptions.ts @@ -17,23 +17,24 @@ along with web3.js. If not, see . // eslint-disable-next-line max-classes-per-file import { - HexString, BlockOutput, - Web3BaseProvider, - Web3APISpec, - Web3APIParams, + DEFAULT_RETURN_FORMAT, + DataFormat, EthExecutionAPI, - Log, - JsonRpcNotification, JsonRpcSubscriptionResult, - DataFormat, - DEFAULT_RETURN_FORMAT, JsonRpcSubscriptionResultOld, - EIP1193Provider, + JsonRpcNotification, + Log, + HexString, + Web3APIParams, + Web3APISpec, } from 'web3-types'; import { jsonRpc } from 'web3-utils'; -import { Web3EventEmitter, Web3EventMap } from './web3_event_emitter.js'; +import { SubscriptionError } from 'web3-errors'; +// eslint-disable-next-line import/no-cycle +import { Web3SubscriptionManager } from './web3_subscription_manager.js'; +import { Web3EventEmitter, Web3EventMap } from './web3_event_emitter.js'; import { Web3RequestManager } from './web3_request_manager.js'; export abstract class Web3Subscription< @@ -42,20 +43,52 @@ export abstract class Web3Subscription< ArgsType = any, API extends Web3APISpec = EthExecutionAPI, > extends Web3EventEmitter { - private readonly _requestManager: Web3RequestManager; + private readonly _subscriptionManager: Web3SubscriptionManager; private readonly _lastBlock?: BlockOutput; private readonly _returnFormat: DataFormat; - private _id?: HexString; - private _messageListener?: (data?: JsonRpcNotification) => void; + protected _id?: HexString; public constructor( - public readonly args: ArgsType, - + args: ArgsType, + options: { subscriptionManager: Web3SubscriptionManager; returnFormat?: DataFormat }, + ); + /** + * @deprecated This constructor overloading should not be used + */ + public constructor( + args: ArgsType, options: { requestManager: Web3RequestManager; returnFormat?: DataFormat }, + ); + public constructor( + public readonly args: ArgsType, + options: ( + | { subscriptionManager: Web3SubscriptionManager } + | { requestManager: Web3RequestManager } + ) & { + returnFormat?: DataFormat; + }, ) { super(); - this._requestManager = options.requestManager; - this._returnFormat = options.returnFormat ?? (DEFAULT_RETURN_FORMAT as DataFormat); + const { requestManager } = options as { requestManager: Web3RequestManager }; + const { subscriptionManager } = options as { subscriptionManager: Web3SubscriptionManager }; + if (requestManager && subscriptionManager) { + throw new SubscriptionError( + 'Only requestManager or subscriptionManager should be provided at Subscription constructor', + ); + } + if (!requestManager && !subscriptionManager) { + throw new SubscriptionError( + 'Either requestManager or subscriptionManager should be provided at Subscription constructor', + ); + } + if (requestManager) { + // eslint-disable-next-line deprecation/deprecation + this._subscriptionManager = new Web3SubscriptionManager(requestManager, {}, true); + } else { + this._subscriptionManager = subscriptionManager; + } + + this._returnFormat = options?.returnFormat ?? (DEFAULT_RETURN_FORMAT as DataFormat); } public get id() { @@ -66,42 +99,37 @@ export abstract class Web3Subscription< return this._lastBlock; } - public async subscribe() { - this._id = await this._requestManager.send({ - method: 'eth_subscribe', - params: this._buildSubscriptionParams(), - }); + public async subscribe(): Promise { + return this._subscriptionManager.addSubscription(this); + } - const messageListener = ( - data?: - | JsonRpcSubscriptionResult - | JsonRpcSubscriptionResultOld - | JsonRpcNotification, - ) => { + public processSubscriptionData( + data: + | JsonRpcSubscriptionResult + | JsonRpcSubscriptionResultOld + | JsonRpcNotification, + ) { + if (data?.data) { // for EIP-1193 provider - if (data?.data) { - this._processSubscriptionResult(data?.data?.result ?? data?.data); - return; - } - - if ( - data && - jsonRpc.isResponseWithNotification( - data as unknown as JsonRpcSubscriptionResult | JsonRpcNotification, - ) - ) { - this._processSubscriptionResult(data?.params.result); - } - }; - - if (typeof (this._requestManager.provider as EIP1193Provider).request === 'function') { - (this._requestManager.provider as Web3BaseProvider).on('message', messageListener); - } else { - (this._requestManager.provider as Web3BaseProvider).on('data', messageListener); + this._processSubscriptionResult(data?.data?.result ?? data?.data); + } else if ( + data && + jsonRpc.isResponseWithNotification( + data as unknown as JsonRpcSubscriptionResult | JsonRpcNotification, + ) + ) { + this._processSubscriptionResult(data?.params.result); } + } - this._messageListener = messageListener; + public async sendSubscriptionRequest(): Promise { + this._id = await this._subscriptionManager.requestManager.send({ + method: 'eth_subscribe', + params: this._buildSubscriptionParams(), + }); + return this._id; } + protected get returnFormat() { return this._returnFormat; } @@ -115,16 +143,15 @@ export abstract class Web3Subscription< return; } - await this._requestManager.send({ + await this._subscriptionManager.removeSubscription(this); + } + + public async sendUnsubscribeRequest() { + await this._subscriptionManager.requestManager.send({ method: 'eth_unsubscribe', params: [this.id] as Web3APIParams, }); - this._id = undefined; - (this._requestManager.provider as Web3BaseProvider).removeListener( - 'message', - this._messageListener as never, - ); } // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars @@ -148,9 +175,23 @@ export type Web3SubscriptionConstructor< API extends Web3APISpec, // eslint-disable-next-line @typescript-eslint/no-explicit-any SubscriptionType extends Web3Subscription = Web3Subscription, -> = new ( - // We accept any type of arguments here and don't deal with this type internally - // eslint-disable-next-line @typescript-eslint/no-explicit-any - args: any, - options: { requestManager: Web3RequestManager; returnFormat?: DataFormat }, -) => SubscriptionType; +> = + | (new ( + // We accept any type of arguments here and don't deal with this type internally + // eslint-disable-next-line @typescript-eslint/no-explicit-any + args: any, + options: + | { subscriptionManager: Web3SubscriptionManager; returnFormat?: DataFormat } + | { requestManager: Web3RequestManager; returnFormat?: DataFormat }, + ) => SubscriptionType) + | (new ( + args: any, + options: { + subscriptionManager: Web3SubscriptionManager; + returnFormat?: DataFormat; + }, + ) => SubscriptionType) + | (new ( + args: any, + options: { requestManager: Web3RequestManager; returnFormat?: DataFormat }, + ) => SubscriptionType); diff --git a/packages/web3-core/test/unit/__snapshots__/web3_context.test.ts.snap b/packages/web3-core/test/unit/__snapshots__/web3_context.test.ts.snap index a69eb6a8bee..8053e771302 100644 --- a/packages/web3-core/test/unit/__snapshots__/web3_context.test.ts.snap +++ b/packages/web3-core/test/unit/__snapshots__/web3_context.test.ts.snap @@ -73,6 +73,7 @@ Object { }, "useRpcCallSpecification": undefined, }, + "tolerateUnlinkedSubscription": false, }, "wallet": undefined, } diff --git a/packages/web3-core/test/unit/web3_subscription.test.ts b/packages/web3-core/test/unit/web3_subscription.test.ts index 6b1bb911e34..7a5efedaa04 100644 --- a/packages/web3-core/test/unit/web3_subscription.test.ts +++ b/packages/web3-core/test/unit/web3_subscription.test.ts @@ -15,25 +15,32 @@ You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ +import { Web3SubscriptionManager } from '../../src'; import { ExampleSubscription } from './fixtures/example_subscription'; +const subscriptions = { example: ExampleSubscription as never }; + describe('Web3Subscription', () => { let requestManager: any; + let subscriptionManager: Web3SubscriptionManager; let sub: ExampleSubscription; beforeEach(() => { - // const web3 = new Web3('http://localhost:8545'); requestManager = { - send: jest.fn(), + send: jest.fn().mockImplementation(async () => { + return 'sub-id'; + }), on: jest.fn(), provider: { on: jest.fn(), removeListener: jest.fn(), request: jest.fn() }, }; - sub = new ExampleSubscription({ param1: 'value' }, { requestManager }); + subscriptionManager = new Web3SubscriptionManager(requestManager, subscriptions); }); describe('subscribe', () => { + beforeEach(() => { + sub = new ExampleSubscription({ param1: 'value' }, { subscriptionManager }); + }); it('should invoke request manager for subscription', async () => { - (requestManager.send as jest.Mock).mockResolvedValue('sub-id'); await sub.subscribe(); expect(requestManager.send).toHaveBeenCalledTimes(1); @@ -44,8 +51,6 @@ describe('Web3Subscription', () => { }); it('should set correct subscription id', async () => { - (requestManager.send as jest.Mock).mockResolvedValue('sub-id'); - expect(sub.id).toBeUndefined(); await sub.subscribe(); expect(sub.id).toBe('sub-id'); @@ -64,7 +69,9 @@ describe('Web3Subscription', () => { describe('unsubscribe', () => { beforeEach(() => { + sub = new ExampleSubscription({ param1: 'value' }, { subscriptionManager }); sub['_id'] = 'sub-id'; + subscriptionManager.subscriptions.set('sub-id', sub); }); it('should invoke request manager to unsubscribe', async () => { @@ -82,15 +89,80 @@ describe('Web3Subscription', () => { await sub.unsubscribe(); expect(sub.id).toBeUndefined(); }); + }); +}); + +describe('Web3Subscription without subscription manager - (deprecated)', () => { + let requestManager: any; + let sub: ExampleSubscription; + + beforeEach(() => { + requestManager = { + send: jest.fn().mockImplementation(async () => { + return 'sub-id'; + }), + on: jest.fn(), + provider: { on: jest.fn(), removeListener: jest.fn(), request: jest.fn() }, + }; + }); + describe('subscribe', () => { + beforeEach(() => { + // eslint-disable-next-line deprecation/deprecation + sub = new ExampleSubscription({ param1: 'value' }, { requestManager }); + }); + + it('should invoke request manager for subscription', async () => { + (requestManager.send as jest.Mock).mockResolvedValue('sub-id'); + await sub.subscribe(); + + expect(requestManager.send).toHaveBeenCalledTimes(1); + expect(requestManager.send).toHaveBeenCalledWith({ + method: 'eth_subscribe', + params: ['newHeads'], + }); + }); + + it('should set correct subscription id', async () => { + (requestManager.send as jest.Mock).mockResolvedValue('sub-id'); + + expect(sub.id).toBeUndefined(); + await sub.subscribe(); + expect(sub.id).toBe('sub-id'); + }); - it('should remove listener for "message" event', async () => { + it('should start listening to the "message" event', async () => { + // requestManager.provider.on.mockClear(); + await sub.subscribe(); + + expect(requestManager.provider.on).toHaveBeenCalledTimes(1); + expect(requestManager.provider.on).toHaveBeenCalledWith( + 'message', + expect.any(Function), + ); + }); + }); + + describe('unsubscribe', () => { + beforeEach(() => { + // eslint-disable-next-line deprecation/deprecation + sub = new ExampleSubscription({ param1: 'value' }, { requestManager }); + sub['_id'] = 'sub-id'; + }); + + it('should invoke request manager to unsubscribe', async () => { await sub.unsubscribe(); - expect(requestManager.provider.removeListener).toHaveBeenCalledTimes(1); - expect(requestManager.provider.removeListener).toHaveBeenCalledWith( + expect(requestManager.provider.on).toHaveBeenCalledTimes(1); + expect(requestManager.provider.on).toHaveBeenCalledWith( 'message', - undefined, + expect.any(Function), ); }); + + it('should remove the subscription id', async () => { + expect(sub.id).toBe('sub-id'); + await sub.unsubscribe(); + expect(sub.id).toBeUndefined(); + }); }); }); diff --git a/packages/web3-core/test/unit/web3_subscription_manager.test.ts b/packages/web3-core/test/unit/web3_subscription_manager.test.ts index cf2d120ddee..403719b292a 100644 --- a/packages/web3-core/test/unit/web3_subscription_manager.test.ts +++ b/packages/web3-core/test/unit/web3_subscription_manager.test.ts @@ -26,35 +26,54 @@ const subscriptions = { example: ExampleSubscription as never }; describe('Web3SubscriptionManager', () => { let requestManager: any; + let subManager: Web3SubscriptionManager; beforeEach(() => { - requestManager = { send: jest.fn(), on: jest.fn(), provider: jest.fn() }; + requestManager = { + send: jest.fn().mockImplementation(async () => { + return 'sub-id'; + }), + on: jest.fn(), + provider: { on: jest.fn() }, + }; + subManager = new Web3SubscriptionManager(requestManager, subscriptions); (ExampleSubscription as jest.Mock).mockClear(); }); describe('constructor', () => { it('should create subscription manager object', () => { - const subManager = new Web3SubscriptionManager(requestManager, {}); + subManager = new Web3SubscriptionManager(requestManager, {}); expect(subManager).toBeInstanceOf(Web3SubscriptionManager); }); it('should create register events for request manager', () => { - const subManager = new Web3SubscriptionManager(requestManager, {}); - - expect(subManager).toBeDefined(); - expect(requestManager.on).toHaveBeenCalledTimes(2); - expect(requestManager.on).toHaveBeenCalledWith( + const requestMan: any = { + send: jest.fn(), + on: jest.fn(), + provider: { + on: jest + .fn() + .mockImplementation((_: string, callback: (a: string) => unknown) => + callback('something'), + ), + }, + }; + const subscriptionMan = new Web3SubscriptionManager(requestMan, {}); + + expect(subscriptionMan).toBeDefined(); + expect(requestMan.on).toHaveBeenCalledTimes(2); + expect(requestMan.on).toHaveBeenCalledWith( Web3RequestManagerEvent.BEFORE_PROVIDER_CHANGE, expect.any(Function), ); - expect(requestManager.on).toHaveBeenCalledWith( + expect(requestMan.on).toHaveBeenCalledWith( Web3RequestManagerEvent.PROVIDER_CHANGED, expect.any(Function), ); }); it('should register the subscription types', () => { - const subManager = new Web3SubscriptionManager(requestManager, { + subManager = new Web3SubscriptionManager(requestManager, { example: ExampleSubscription as never, }); @@ -63,8 +82,6 @@ describe('Web3SubscriptionManager', () => { }); describe('subscribe', () => { - let subManager: Web3SubscriptionManager; - beforeEach(() => { subManager = new Web3SubscriptionManager(requestManager, subscriptions); @@ -93,33 +110,35 @@ describe('Web3SubscriptionManager', () => { }); it('should return valid subscription type if subscribed', async () => { - jest.spyOn(subManager, 'addSubscription').mockResolvedValue(); + jest.spyOn(subManager, 'addSubscription').mockResolvedValue('123'); const result = await subManager.subscribe('example'); expect(result).toBeInstanceOf(ExampleSubscription); }); it('should initialize subscription with valid args', async () => { - jest.spyOn(subManager, 'addSubscription').mockResolvedValue(); + jest.spyOn(subManager, 'addSubscription').mockResolvedValue('456'); const result = await subManager.subscribe('example', { test1: 'test1' }); expect(result).toBeInstanceOf(ExampleSubscription); expect(ExampleSubscription).toHaveBeenCalledTimes(1); expect(ExampleSubscription).toHaveBeenCalledWith( { test1: 'test1' }, - { requestManager, returnFormat: DEFAULT_RETURN_FORMAT }, + { subscriptionManager: subManager, returnFormat: DEFAULT_RETURN_FORMAT }, ); }); }); describe('addSubscription', () => { - let subManager: Web3SubscriptionManager; let sub: ExampleSubscription; beforeEach(() => { subManager = new Web3SubscriptionManager(requestManager, subscriptions); jest.spyOn(subManager, 'supportsSubscriptions').mockReturnValue(true); - sub = new ExampleSubscription({ param1: 'param1' }, { requestManager }); + sub = new ExampleSubscription( + { param1: 'param1' }, + { subscriptionManager: subManager }, + ); (sub as any).id = '123'; }); @@ -133,10 +152,18 @@ describe('Web3SubscriptionManager', () => { }); it('should try to subscribe the subscription', async () => { + sub = new ExampleSubscription( + { param1: 'param1' }, + { subscriptionManager: subManager }, + ); + jest.spyOn(sub, 'sendSubscriptionRequest').mockImplementation(async () => { + (sub as any).id = 'value'; + return Promise.resolve(sub.id as string); + }); await subManager.addSubscription(sub); - expect(sub.subscribe).toHaveBeenCalledTimes(1); - expect(sub.subscribe).toHaveBeenCalledWith(); + expect(sub.sendSubscriptionRequest).toHaveBeenCalledTimes(1); + expect(sub.sendSubscriptionRequest).toHaveBeenCalledWith(); }); it('should set the subscription to the map', async () => { @@ -149,13 +176,15 @@ describe('Web3SubscriptionManager', () => { }); describe('removeSubscription', () => { - let subManager: Web3SubscriptionManager; let sub: ExampleSubscription; beforeEach(async () => { subManager = new Web3SubscriptionManager(requestManager, subscriptions); jest.spyOn(subManager, 'supportsSubscriptions').mockReturnValue(true); - sub = new ExampleSubscription({ param1: 'param1' }, { requestManager }); + sub = new ExampleSubscription( + { param1: 'param1' }, + { subscriptionManager: subManager }, + ); (sub as any).id = '123'; await subManager.addSubscription(sub); @@ -180,8 +209,8 @@ describe('Web3SubscriptionManager', () => { it('should try to unsubscribe the subscription', async () => { await subManager.removeSubscription(sub); - expect(sub.unsubscribe).toHaveBeenCalledTimes(1); - expect(sub.unsubscribe).toHaveBeenCalledWith(); + expect(sub.sendUnsubscribeRequest).toHaveBeenCalledTimes(1); + expect(sub.sendUnsubscribeRequest).toHaveBeenCalledWith(); }); it('should remove the subscription to the map', async () => { diff --git a/packages/web3-core/test/unit/web3_subscription_old_providers.test.ts b/packages/web3-core/test/unit/web3_subscription_old_providers.test.ts index 36ff7b4d32c..d82856d020a 100644 --- a/packages/web3-core/test/unit/web3_subscription_old_providers.test.ts +++ b/packages/web3-core/test/unit/web3_subscription_old_providers.test.ts @@ -17,10 +17,13 @@ along with web3.js. If not, see . import { ExampleSubscription } from './fixtures/example_subscription'; import { Web3EventEmitter } from '../../src/web3_event_emitter'; +import { Web3SubscriptionManager } from '../../src'; describe('Web3Subscription', () => { let requestManager: any; + let subscriptionManager: Web3SubscriptionManager; let eipRequestManager: any; + let subscriptionManagerWithEipReqMan: Web3SubscriptionManager; let provider: Web3EventEmitter; let eipProvider: Web3EventEmitter; @@ -29,21 +32,37 @@ describe('Web3Subscription', () => { eipProvider = new Web3EventEmitter(); // @ts-expect-error add to test eip providers eipProvider.request = jest.fn(); - requestManager = { send: jest.fn(), on: jest.fn(), provider }; - eipRequestManager = { send: jest.fn(), on: jest.fn(), provider: eipProvider }; + requestManager = { + send: jest.fn().mockImplementation(async () => { + return 'sub-id'; + }), + on: jest.fn(), + provider, + }; + subscriptionManager = new Web3SubscriptionManager(requestManager, {}); + + eipRequestManager = { + send: jest.fn().mockImplementation(async () => { + return 'sub-id'; + }), + on: jest.fn(), + provider: eipProvider, + }; + subscriptionManagerWithEipReqMan = new Web3SubscriptionManager(eipRequestManager, {}); }); describe('providers response for old provider', () => { it('data with result', async () => { + const sub = new ExampleSubscription({ param1: 'param1' }, { subscriptionManager }); + await sub.subscribe(); const testData = { data: { + subscription: sub.id, result: { some: 1, }, }, }; - const sub = new ExampleSubscription({ param1: 'param1' }, { requestManager }); - await sub.subscribe(); // @ts-expect-error spy on protected method const processResult = jest.spyOn(sub, '_processSubscriptionResult'); provider.emit('data', testData); @@ -51,33 +70,35 @@ describe('Web3Subscription', () => { }); it('data without result for old provider', async () => { + const sub = new ExampleSubscription({ param1: 'param1' }, { subscriptionManager }); + await sub.subscribe(); const testData = { data: { + subscription: sub.id, other: { some: 1, }, }, }; - const sub = new ExampleSubscription({ param1: 'param1' }, { requestManager }); - await sub.subscribe(); // @ts-expect-error spy on protected method const processResult = jest.spyOn(sub, '_processSubscriptionResult'); provider.emit('data', testData); expect(processResult).toHaveBeenCalledWith(testData.data); }); it('data with result for eipProvider', async () => { + const sub = new ExampleSubscription( + { param1: 'param1' }, + { subscriptionManager: subscriptionManagerWithEipReqMan }, + ); + await sub.subscribe(); const testData = { data: { + subscription: sub.id, result: { some: 1, }, }, }; - const sub = new ExampleSubscription( - { param1: 'param1' }, - { requestManager: eipRequestManager }, - ); - await sub.subscribe(); // @ts-expect-error spy on protected method const processResult = jest.spyOn(sub, '_processSubscriptionResult'); eipProvider.emit('message', testData); @@ -85,18 +106,19 @@ describe('Web3Subscription', () => { }); it('data without result for eipProvider', async () => { + const sub = new ExampleSubscription( + { param1: 'param1' }, + { subscriptionManager: subscriptionManagerWithEipReqMan }, + ); + await sub.subscribe(); const testData = { data: { + subscription: sub.id, other: { some: 1, }, }, }; - const sub = new ExampleSubscription( - { param1: 'param1' }, - { requestManager: eipRequestManager }, - ); - await sub.subscribe(); // @ts-expect-error spy on protected method const processResult = jest.spyOn(sub, '_processSubscriptionResult'); eipProvider.emit('message', testData); diff --git a/packages/web3-eth-accounts/test/config/setup.js b/packages/web3-eth-accounts/test/config/setup.js index 0b6b9109ce0..b3c35155474 100644 --- a/packages/web3-eth-accounts/test/config/setup.js +++ b/packages/web3-eth-accounts/test/config/setup.js @@ -22,3 +22,7 @@ require('jest-extended'); // @todo extend jest to have "toHaveBeenCalledOnceWith" matcher. process.env.NODE_ENV = 'test'; + +const jestTimeout = 10000; + +jest.setTimeout(jestTimeout); diff --git a/packages/web3-eth-contract/src/contract.ts b/packages/web3-eth-contract/src/contract.ts index 7abe1d6c163..054a2c0a142 100644 --- a/packages/web3-eth-contract/src/contract.ts +++ b/packages/web3-eth-contract/src/contract.ts @@ -15,7 +15,13 @@ You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ -import { Web3Context, Web3EventEmitter, Web3PromiEvent, Web3ConfigEvent } from 'web3-core'; +import { + Web3Context, + Web3EventEmitter, + Web3PromiEvent, + Web3ConfigEvent, + Web3SubscriptionManager, +} from 'web3-core'; import { ContractExecutionError, ContractTransactionDataAndInputError, @@ -1140,7 +1146,14 @@ export class Contract abi, jsonInterface: this._jsonInterface, }, - { requestManager: this.requestManager, returnFormat }, + { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + subscriptionManager: this.subscriptionManager as Web3SubscriptionManager< + unknown, + any + >, + returnFormat, + }, ); if (!isNullish(fromBlock)) { // emit past events when fromBlock is defined diff --git a/packages/web3-eth-contract/src/log_subscription.ts b/packages/web3-eth-contract/src/log_subscription.ts index 3ae67b92cd2..7522d7e82f2 100644 --- a/packages/web3-eth-contract/src/log_subscription.ts +++ b/packages/web3-eth-contract/src/log_subscription.ts @@ -16,7 +16,7 @@ along with web3.js. If not, see . */ import { AbiEventFragment, LogsInput, HexString, Topic, DataFormat } from 'web3-types'; -import { Web3RequestManager, Web3Subscription } from 'web3-core'; +import { Web3RequestManager, Web3Subscription, Web3SubscriptionManager } from 'web3-core'; // eslint-disable-next-line import/no-cycle import { decodeEventABI } from './encoding.js'; // eslint-disable-next-line import/no-cycle @@ -112,12 +112,38 @@ export class LogsSubscription extends Web3Subscription< abi: AbiEventFragment & { signature: HexString }; jsonInterface: ContractAbiWithSignature; }, - options: { - requestManager: Web3RequestManager; + options: { subscriptionManager: Web3SubscriptionManager; returnFormat?: DataFormat }, + ); + /** + * @deprecated This constructor overloading should not be used + */ + public constructor( + args: { + address?: HexString; + // eslint-disable-next-line @typescript-eslint/ban-types + topics?: (Topic | Topic[] | null)[]; + abi: AbiEventFragment & { signature: HexString }; + jsonInterface: ContractAbiWithSignature; + }, + options: { requestManager: Web3RequestManager; returnFormat?: DataFormat }, + ); + public constructor( + args: { + address?: HexString; + // eslint-disable-next-line @typescript-eslint/ban-types + topics?: (Topic | Topic[] | null)[]; + abi: AbiEventFragment & { signature: HexString }; + jsonInterface: ContractAbiWithSignature; + }, + options: ( + | { subscriptionManager: Web3SubscriptionManager } + | { requestManager: Web3RequestManager } + ) & { returnFormat?: DataFormat; }, ) { - super(args, options); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + super(args, options as any); this.address = args.address; this.topics = args.topics; diff --git a/packages/web3-eth/src/web3_eth.ts b/packages/web3-eth/src/web3_eth.ts index 0fbf506090b..d76b2678ed6 100644 --- a/packages/web3-eth/src/web3_eth.ts +++ b/packages/web3-eth/src/web3_eth.ts @@ -1590,11 +1590,7 @@ export class Web3Eth extends Web3Context[0], returnFormat: ReturnType = DEFAULT_RETURN_FORMAT as ReturnType, ): Promise> { - const subscription = (await this.subscriptionManager?.subscribe( - name, - args, - returnFormat, - )) as InstanceType; + const subscription = await this.subscriptionManager?.subscribe(name, args, returnFormat); if ( subscription instanceof LogsSubscription && name === 'logs' && diff --git a/packages/web3-eth/test/integration/setup.js b/packages/web3-eth/test/integration/setup.js index 3982381b2ad..19e711c511e 100644 --- a/packages/web3-eth/test/integration/setup.js +++ b/packages/web3-eth/test/integration/setup.js @@ -19,6 +19,6 @@ along with web3.js. If not, see . // eslint-disable-next-line @typescript-eslint/no-require-imports require('../config/setup'); -const jestTimeout = process.env.WEB3_SYSTEM_TEST_PROVIDER.includes('ipc') ? 30000 : 20000; +const jestTimeout = process.env.WEB3_SYSTEM_TEST_PROVIDER.includes('ipc') ? 60000 : 50000; jest.setTimeout(jestTimeout); diff --git a/packages/web3-eth/test/integration/subscription_heads.test.ts b/packages/web3-eth/test/integration/subscription_heads.test.ts index b79e943d1dc..44ef600efc1 100644 --- a/packages/web3-eth/test/integration/subscription_heads.test.ts +++ b/packages/web3-eth/test/integration/subscription_heads.test.ts @@ -43,9 +43,9 @@ describeIf(isSocket)('subscription', () => { let times = 0; const pr = new Promise((resolve: Resolve, reject) => { sub.on('data', (data: BlockHeaderOutput) => { - if (data.parentHash) { - times += 1; - } + expect(typeof data.parentHash).toBe('string'); + + times += 1; expect(times).toBeGreaterThanOrEqual(times); if (times >= checkTxCount) { resolve(); @@ -65,12 +65,25 @@ describeIf(isSocket)('subscription', () => { await web3.eth.subscriptionManager?.removeSubscription(sub); await closeOpenConnection(web3.eth); }); - it(`clear`, async () => { + it(`remove at subscriptionManager`, async () => { const web3Eth = new Web3Eth(clientUrl); await waitForOpenConnection(web3Eth); const sub: NewHeadsSubscription = await web3Eth.subscribe('newHeads'); expect(sub.id).toBeDefined(); + const subId = sub.id as string; await web3Eth.subscriptionManager?.removeSubscription(sub); + expect(web3Eth.subscriptionManager.subscriptions.has(subId)).toBe(false); + expect(sub.id).toBeUndefined(); + await closeOpenConnection(web3Eth); + }); + it(`remove at subscribe object`, async () => { + const web3Eth = new Web3Eth(clientUrl); + await waitForOpenConnection(web3Eth); + const sub: NewHeadsSubscription = await web3Eth.subscribe('newHeads'); + expect(sub.id).toBeDefined(); + const subId = sub.id as string; + await sub.unsubscribe(); + expect(web3Eth.subscriptionManager.subscriptions.has(subId)).toBe(false); expect(sub.id).toBeUndefined(); await closeOpenConnection(web3Eth); }); diff --git a/packages/web3-eth/test/integration/subscription_on_2_events.test.ts b/packages/web3-eth/test/integration/subscription_on_2_events.test.ts new file mode 100644 index 00000000000..bfcfb99da92 --- /dev/null +++ b/packages/web3-eth/test/integration/subscription_on_2_events.test.ts @@ -0,0 +1,114 @@ +/* +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 . +*/ +// eslint-disable-next-line import/no-extraneous-dependencies +import { BlockHeaderOutput, Web3 } from 'web3'; +import { + closeOpenConnection, + describeIf, + getSystemTestProvider, + isSocket, + sendFewSampleTxs, + waitForOpenConnection, +} from '../fixtures/system_test_utils'; +import { Resolve } from './helper'; + +const checkTxCount = 2; +describeIf(isSocket)('subscription on multiple events', () => { + test(`catch the data of pendingTransactions and newHeads`, async () => { + const web3 = new Web3(getSystemTestProvider()); + const web3Eth = web3.eth; + await waitForOpenConnection(web3Eth); + const pendingTransactionsSub = await web3Eth.subscribe('pendingTransactions'); + + let pendingTransactionsCount = 0; + const pendingTransactionsData = new Promise((resolve: Resolve, reject) => { + (() => { + pendingTransactionsSub.on('data', (data: string) => { + expect(typeof data).toBe('string'); + + pendingTransactionsCount += 1; + if (pendingTransactionsCount >= checkTxCount) { + resolve(); + } + }); + pendingTransactionsSub.on('error', error => { + reject(error); + }); + })(); + }); + + const newHeadsSub = await web3.eth.subscribe('newHeads'); + let newHeadsCount = 0; + const newHeadsData = new Promise((resolve: Resolve, reject) => { + newHeadsSub.on('data', (data: BlockHeaderOutput) => { + expect(typeof data.parentHash).toBe('string'); + + newHeadsCount += 1; + if (newHeadsCount >= checkTxCount) { + resolve(); + } + }); + newHeadsSub.on('error', error => { + reject(error); + }); + }); + + await sendFewSampleTxs(2); + + await pendingTransactionsData; + await newHeadsData; + + await closeOpenConnection(web3Eth); + }); + + test(`catch the data of an event even after subscribing off another one`, async () => { + const web3 = new Web3(getSystemTestProvider()); + const web3Eth = web3.eth; + await waitForOpenConnection(web3Eth); + const pendingTransactionsSub = await web3Eth.subscribe('pendingTransactions'); + + // eslint-disable-next-line @typescript-eslint/no-empty-function + pendingTransactionsSub.on('data', () => {}); + pendingTransactionsSub.on('error', error => { + throw error; + }); + + const newHeadsSub = await web3.eth.subscribe('newHeads'); + let times = 0; + const newHeadsData = new Promise((resolve: Resolve, reject) => { + newHeadsSub.on('data', (data: BlockHeaderOutput) => { + expect(typeof data.parentHash).toBe('string'); + + times += 1; + if (times >= checkTxCount) { + resolve(); + } + }); + newHeadsSub.on('error', error => { + reject(error); + }); + }); + + await pendingTransactionsSub.unsubscribe(); + + await sendFewSampleTxs(2); + + await newHeadsData; + + await closeOpenConnection(web3Eth); + }); +}); diff --git a/packages/web3-eth/test/unit/web3_eth_subscription.test.ts b/packages/web3-eth/test/unit/web3_eth_subscription.test.ts index 3633fd79fa6..edbf28d215f 100644 --- a/packages/web3-eth/test/unit/web3_eth_subscription.test.ts +++ b/packages/web3-eth/test/unit/web3_eth_subscription.test.ts @@ -14,7 +14,7 @@ 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 { Web3RequestManager, Web3SubscriptionManager } from 'web3-core'; +import { Web3SubscriptionManager } from 'web3-core'; import { Web3BaseProvider } from 'web3-types'; import * as rpcMethodWrappers from '../../src/rpc_method_wrappers'; import { LogsSubscription } from '../../src'; @@ -28,7 +28,7 @@ describe('Web3Eth subscribe and clear subscriptions', () => { let web3Eth: Web3Eth; it('should return the subscription data provided by the Subscription Manager', async () => { - const requestManager = { send: jest.fn(), on: jest.fn(), provider: jest.fn() }; + const requestManager = { send: jest.fn(), on: jest.fn(), provider: { on: jest.fn() } }; const subManager = new Web3SubscriptionManager(requestManager as any, undefined as any); const dummyLogs = { logs: { test1: 'test1' } }; @@ -46,15 +46,10 @@ describe('Web3Eth subscribe and clear subscriptions', () => { }); it('should call `_processSubscriptionResult` when the logs are of type LogsSubscription and the `fromBlock` is provided', async () => { - const requestManager = { send: jest.fn(), on: jest.fn(), provider: jest.fn() }; + const requestManager = { send: jest.fn(), on: jest.fn(), provider: { on: jest.fn() } }; const subManager = new Web3SubscriptionManager(requestManager as any, undefined as any); - const dummyLogs = new LogsSubscription( - {}, - { - requestManager: requestManager as unknown as Web3RequestManager, - }, - ); + const dummyLogs = new LogsSubscription({}, { subscriptionManager: subManager }); jest.spyOn(subManager, 'subscribe').mockResolvedValueOnce(dummyLogs); jest.spyOn(rpcMethodWrappers, 'getLogs').mockResolvedValueOnce(mockGetLogsRpcResponse); @@ -76,7 +71,7 @@ describe('Web3Eth subscribe and clear subscriptions', () => { }); it('should be able to clear subscriptions', async () => { - const requestManager = { send: jest.fn(), on: jest.fn(), provider: jest.fn() }; + const requestManager = { send: jest.fn(), on: jest.fn(), provider: { on: jest.fn() } }; const subManager = new Web3SubscriptionManager(requestManager as any, undefined as any); jest.spyOn(subManager, 'unsubscribe'); @@ -94,7 +89,7 @@ describe('Web3Eth subscribe and clear subscriptions', () => { }); it('should be able to clear subscriptions and pass `shouldClearSubscription` when passing `notClearSyncing`', async () => { - const requestManager = { send: jest.fn(), on: jest.fn(), provider: jest.fn() }; + const requestManager = { send: jest.fn(), on: jest.fn(), provider: { on: jest.fn() } }; const subManager = new Web3SubscriptionManager(requestManager as any, undefined as any); jest.spyOn(subManager, 'unsubscribe'); diff --git a/packages/web3-types/CHANGELOG.md b/packages/web3-types/CHANGELOG.md index eae97aa7dde..533fed62170 100644 --- a/packages/web3-types/CHANGELOG.md +++ b/packages/web3-types/CHANGELOG.md @@ -121,6 +121,15 @@ Documentation: ## [Unreleased] +### Added + +- Added the `SimpleProvider` interface which has only `request(args)` method that is compatible with EIP-1193 (#6210) +- Added the `Eip1193EventName` type that contains the possible events names according to EIP-1193 (#6210) + +### Changed + +- The `EIP1193Provider` class has now all the events (for `on` and `removeListener`) according to EIP-1193 (#6210) + ### Fixed - Fixed bug #6185, now web3.js compiles on typescript v5 (#6195) diff --git a/packages/web3-types/src/web3_base_provider.ts b/packages/web3-types/src/web3_base_provider.ts index 2db1b9bee37..6fa7ad18fa7 100644 --- a/packages/web3-types/src/web3_base_provider.ts +++ b/packages/web3-types/src/web3_base_provider.ts @@ -94,12 +94,41 @@ export interface LegacyRequestProvider { ): void; } -export interface EIP1193Provider { +export interface SimpleProvider { request, ResponseType = Web3APIReturnType>( args: Web3APIPayload, ): Promise | unknown>; } +export interface ProviderInfo { + chainId: string; +} + +export type ProviderChainId = string; + +export type ProviderAccounts = string[]; + +export type Eip1193EventName = + | 'connect' + | 'disconnect' + | 'message' + | 'chainChanged' + | 'accountsChanged'; + +export interface EIP1193Provider extends SimpleProvider { + on(event: 'connect', listener: (info: ProviderInfo) => void): void; + on(event: 'disconnect', listener: (error: ProviderRpcError) => void): void; + on(event: 'message', listener: (message: ProviderMessage) => void): void; + on(event: 'chainChanged', listener: (chainId: ProviderChainId) => void): void; + on(event: 'accountsChanged', listener: (accounts: ProviderAccounts) => void): void; + + removeListener(event: 'connect', listener: (info: ProviderInfo) => void): void; + removeListener(event: 'disconnect', listener: (error: ProviderRpcError) => void): void; + removeListener(event: 'message', listener: (message: ProviderMessage) => void): void; + removeListener(event: 'chainChanged', listener: (chainId: ProviderChainId) => void): void; + removeListener(event: 'accountsChanged', listener: (accounts: ProviderAccounts) => void): void; +} + // Provider interface compatible with EIP-1193 // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1193.md export abstract class Web3BaseProvider @@ -244,7 +273,8 @@ export type SupportedProviders = | Web3BaseProvider | LegacyRequestProvider | LegacySendProvider - | LegacySendAsyncProvider; + | LegacySendAsyncProvider + | SimpleProvider; export type Web3BaseProviderConstructor = new ( url: string, diff --git a/packages/web3-utils/src/socket_provider.ts b/packages/web3-utils/src/socket_provider.ts index 86017cfd5c7..3e0e4bd4bae 100644 --- a/packages/web3-utils/src/socket_provider.ts +++ b/packages/web3-utils/src/socket_provider.ts @@ -16,6 +16,7 @@ along with web3.js. If not, see . */ import { ConnectionEvent, + Eip1193EventName, EthExecutionAPI, JsonRpcBatchRequest, JsonRpcBatchResponse, @@ -223,8 +224,11 @@ export abstract class SocketProvider< listener: Web3Eip1193ProviderEventCallback | Web3ProviderEventCallback, ): void; public on( - type: string | 'disconnect' | 'connect' | 'chainChanged' | 'accountsChanged', - listener: Web3Eip1193ProviderEventCallback

| Web3ProviderEventCallback, + type: string | Eip1193EventName, + listener: + | Web3Eip1193ProviderEventCallback

+ | Web3ProviderMessageEventCallback + | Web3ProviderEventCallback, ): void { this._eventEmitter.on(type, listener); } @@ -249,15 +253,20 @@ export abstract class SocketProvider< ): void; public once( type: 'message', - listener: Web3Eip1193ProviderEventCallback | Web3ProviderEventCallback, + listener: + | Web3Eip1193ProviderEventCallback + | Web3ProviderMessageEventCallback, ): void; public once( type: string, listener: Web3Eip1193ProviderEventCallback | Web3ProviderEventCallback, ): void; public once( - type: string | 'disconnect' | 'connect' | 'chainChanged' | 'accountsChanged', - listener: Web3Eip1193ProviderEventCallback

| Web3ProviderEventCallback, + type: string | Eip1193EventName, + listener: + | Web3Eip1193ProviderEventCallback

+ | Web3ProviderMessageEventCallback + | Web3ProviderEventCallback, ): void { this._eventEmitter.once(type, listener); } @@ -285,15 +294,20 @@ export abstract class SocketProvider< ): void; public removeListener( type: 'message', - listener: Web3Eip1193ProviderEventCallback | Web3ProviderEventCallback, + listener: + | Web3Eip1193ProviderEventCallback + | Web3ProviderMessageEventCallback, ): void; public removeListener( type: string, listener: Web3Eip1193ProviderEventCallback | Web3ProviderEventCallback, ): void; public removeListener( - type: string | 'disconnect' | 'connect' | 'chainChanged' | 'accountsChanged', - listener: Web3Eip1193ProviderEventCallback

| Web3ProviderEventCallback, + type: string | Eip1193EventName, + listener: + | Web3Eip1193ProviderEventCallback

+ | Web3ProviderMessageEventCallback + | Web3ProviderEventCallback, ): void { this._eventEmitter.removeListener(type, listener); } diff --git a/packages/web3/test/e2e/e2e_utils.ts b/packages/web3/test/e2e/e2e_utils.ts index 40fdc325265..8001e72fc55 100644 --- a/packages/web3/test/e2e/e2e_utils.ts +++ b/packages/web3/test/e2e/e2e_utils.ts @@ -51,8 +51,9 @@ export const getE2ETestAccountAddress = (): string => { }; export const getE2ETestContractAddress = () => + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion secrets[getSystemTestBackend().toUpperCase() as 'SEPOLIA' | 'MAINNET'] - .DEPLOYED_TEST_CONTRACT_ADDRESS; + .DEPLOYED_TEST_CONTRACT_ADDRESS as string; export const getAllowedSendTransaction = (): boolean => { if (process.env.ALLOWED_SEND_TRANSACTION !== undefined) {