From ed85ccecadc97cbacd0ee00dfb1a2ee7497347b8 Mon Sep 17 00:00:00 2001 From: Muhammad Altabba <24407834+Muhammad-Altabba@users.noreply.github.com> Date: Mon, 21 Oct 2024 18:13:17 +0200 Subject: [PATCH] fix: ensure contractInstance.subscriptionManager.subscribe is not throwing (#7327) * rename `LogsSubscription` at `web3-eth-contract` to `ContractLogsSubscription` * fix issue that `contractInstance.subscriptionManager.subscribe` was throwing * add a unit test --- .../src/contract-subscription-manager.ts | 72 +++++++++++++++++++ packages/web3-eth-contract/src/contract.ts | 27 +++++-- ...iption.ts => contract_log_subscription.ts} | 10 ++- packages/web3-eth-contract/src/index.ts | 6 +- packages/web3-eth-contract/src/types.ts | 4 +- .../web3-eth-contract/test/fixtures/erc20.ts | 8 +-- .../web3-eth-contract/test/fixtures/erc721.ts | 10 +-- .../test/unit/log_subscription.test.ts | 38 +++++++++- 8 files changed, 153 insertions(+), 22 deletions(-) create mode 100644 packages/web3-eth-contract/src/contract-subscription-manager.ts rename packages/web3-eth-contract/src/{log_subscription.ts => contract_log_subscription.ts} (92%) diff --git a/packages/web3-eth-contract/src/contract-subscription-manager.ts b/packages/web3-eth-contract/src/contract-subscription-manager.ts new file mode 100644 index 00000000000..3065e8aa7f4 --- /dev/null +++ b/packages/web3-eth-contract/src/contract-subscription-manager.ts @@ -0,0 +1,72 @@ +/* +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 { Web3SubscriptionConstructor, Web3SubscriptionManager } from 'web3-core'; +import { EthExecutionAPI, ContractAbi, DataFormat, DEFAULT_RETURN_FORMAT } from 'web3-types'; +// eslint-disable-next-line import/no-cycle +import { Contract } from './contract.js'; + +/** + * Similar to `Web3SubscriptionManager` but has a reference to the Contract that uses + */ +export class ContractSubscriptionManager< + API extends EthExecutionAPI, + RegisteredSubs extends { + [key: string]: Web3SubscriptionConstructor; + } = any, // = ContractSubscriptions +> extends Web3SubscriptionManager { + public readonly parentContract: Contract; + + /** + * + * @param - Web3SubscriptionManager + * @param - parentContract + * + * @example + * ```ts + * const requestManager = new Web3RequestManager("ws://localhost:8545"); + * const contract = new Contract(...) + * const subscriptionManager = new Web3SubscriptionManager(requestManager, {}, contract); + * ``` + */ + public constructor( + self: Web3SubscriptionManager, + parentContract: Contract, + ) { + super(self.requestManager, self.registeredSubscriptions); + + this.parentContract = parentContract; + } + + /** + * Will create a new subscription + * + * @param name - The subscription you want to subscribe to + * @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 + */ + public async subscribe( + name: T, + args?: ConstructorParameters[0], + returnFormat: DataFormat = DEFAULT_RETURN_FORMAT, + ): Promise> { + return super.subscribe(name, args ?? this.parentContract.options, returnFormat); + } +} diff --git a/packages/web3-eth-contract/src/contract.ts b/packages/web3-eth-contract/src/contract.ts index 5566d0840bf..a99dc27628e 100644 --- a/packages/web3-eth-contract/src/contract.ts +++ b/packages/web3-eth-contract/src/contract.ts @@ -105,7 +105,7 @@ import { encodeEventABI, encodeMethodABI, } from './encoding.js'; -import { LogsSubscription } from './log_subscription.js'; +import { ContractLogsSubscription } from './contract_log_subscription.js'; import { ContractEventOptions, NonPayableMethodObject, @@ -123,6 +123,8 @@ import { } from './utils.js'; // eslint-disable-next-line import/no-cycle import { DeployerMethodClass } from './contract-deployer-method-class.js'; +// eslint-disable-next-line import/no-cycle +import { ContractSubscriptionManager } from './contract-subscription-manager.js'; type ContractBoundMethod< Abi extends AbiFunctionFragment, @@ -179,9 +181,9 @@ export type ContractMethodSend = Web3PromiEvent< * ``` * * @param options - The options used to subscribe for the event - * @returns - A Promise resolved with {@link LogsSubscription} object + * @returns - A Promise resolved with {@link ContractLogsSubscription} object */ -export type ContractBoundEvent = (options?: ContractEventOptions) => LogsSubscription; +export type ContractBoundEvent = (options?: ContractEventOptions) => ContractLogsSubscription; // To avoid circular dependency between types and encoding, declared these types here. export type ContractEventsInterface< @@ -204,11 +206,13 @@ export type ContractEventEmitterInterface = { type EventParameters = Parameters[2]; const contractSubscriptions = { - logs: LogsSubscription, + logs: ContractLogsSubscription, newHeads: NewHeadsSubscription, newBlockHeaders: NewHeadsSubscription, }; +type ContractSubscriptions = typeof contractSubscriptions; + /** * The `web3.eth.Contract` makes it easy to interact with smart contracts on the ethereum blockchain. * For using contract package, first install Web3 package using: `npm i web3` or `yarn add web3` based on your package manager, after that contracts features can be used as mentioned in following snippet. @@ -406,9 +410,15 @@ const contractSubscriptions = { * */ export class Contract - extends Web3Context + extends Web3Context implements Web3EventEmitter> { + protected override _subscriptionManager: ContractSubscriptionManager; + + public override get subscriptionManager(): ContractSubscriptionManager { + return this._subscriptionManager; + } + /** * The options `object` for the contract instance. `from`, `gas` and `gasPrice` are used as fallback values when sending transactions. * @@ -564,6 +574,11 @@ export class Contract registeredSubscriptions: contractSubscriptions, }); + this._subscriptionManager = new ContractSubscriptionManager< + EthExecutionAPI, + ContractSubscriptions + >(super.subscriptionManager, this); + // Init protected properties if ((contractContext as Web3Context)?.wallet) { this._wallet = (contractContext as Web3Context).wallet; @@ -1414,7 +1429,7 @@ export class Contract abi, params[0] as EventParameters, ); - const sub = new LogsSubscription( + const sub = new ContractLogsSubscription( { address: this.options.address, topics, diff --git a/packages/web3-eth-contract/src/log_subscription.ts b/packages/web3-eth-contract/src/contract_log_subscription.ts similarity index 92% rename from packages/web3-eth-contract/src/log_subscription.ts rename to packages/web3-eth-contract/src/contract_log_subscription.ts index 3cb1a527941..8010309c7cb 100644 --- a/packages/web3-eth-contract/src/log_subscription.ts +++ b/packages/web3-eth-contract/src/contract_log_subscription.ts @@ -28,9 +28,9 @@ import { Web3RequestManager, Web3Subscription, Web3SubscriptionManager } from 'w import { decodeEventABI } from 'web3-eth'; /** - * LogSubscription to be used to subscribe to events logs. + * ContractLogsSubscription to be used to subscribe to events logs. * - * Following events are supported and can be accessed with either {@link LogsSubscription.once} or ${@link LogsSubscription.on} methods. + * Following events are supported and can be accessed with either {@link ContractLogsSubscription.once} or ${@link ContractLogsSubscription.on} methods. * * - **connected**: Emitted when the subscription is connected. * - **data**: Fires on each incoming event with the event object as argument. @@ -81,7 +81,7 @@ import { decodeEventABI } from 'web3-eth'; * } * ``` */ -export class LogsSubscription extends Web3Subscription< +export class ContractLogsSubscription extends Web3Subscription< { data: EventLog; changed: EventLog & { removed: true }; @@ -162,3 +162,7 @@ export class LogsSubscription extends Web3Subscription< return decodeEventABI(this.abi, data as LogsInput, this.jsonInterface, super.returnFormat); } } +/** + * @deprecated LogsSubscription is renamed to ContractLogsSubscription in this package to not be confused with LogsSubscription at 'web3-eth'. + */ +export const LogsSubscription = ContractLogsSubscription; diff --git a/packages/web3-eth-contract/src/index.ts b/packages/web3-eth-contract/src/index.ts index ad7b5ab7fb7..93e90f03b96 100644 --- a/packages/web3-eth-contract/src/index.ts +++ b/packages/web3-eth-contract/src/index.ts @@ -42,11 +42,15 @@ along with web3.js. If not, see . */ import { Contract } from './contract.js'; +import { ContractLogsSubscription } from './contract_log_subscription.js'; +/** @deprecated Use `ContractLogsSubscription` instead. */ +export type LogsSubscription = ContractLogsSubscription; + export * from './encoding.js'; export * from './contract.js'; export * from './contract-deployer-method-class.js'; -export * from './log_subscription.js'; +export * from './contract_log_subscription.js'; export * from './types.js'; export * from './utils.js'; diff --git a/packages/web3-eth-contract/src/types.ts b/packages/web3-eth-contract/src/types.ts index 8f4bf564ca4..6ecff146844 100644 --- a/packages/web3-eth-contract/src/types.ts +++ b/packages/web3-eth-contract/src/types.ts @@ -32,7 +32,7 @@ import { } from 'web3-types'; import { NewHeadsSubscription, SendTransactionEvents } from 'web3-eth'; import type { ContractOptions } from 'web3-types'; -import { LogsSubscription } from './log_subscription.js'; +import { ContractLogsSubscription } from './contract_log_subscription.js'; export type NonPayableTxOptions = NonPayableCallOptions; export type PayableTxOptions = PayableCallOptions; @@ -462,7 +462,7 @@ export type Web3ContractContext = Partial< Web3ContextInitOptions< EthExecutionAPI, { - logs: typeof LogsSubscription; + logs: typeof ContractLogsSubscription; newHeads: typeof NewHeadsSubscription; newBlockHeaders: typeof NewHeadsSubscription; } diff --git a/packages/web3-eth-contract/test/fixtures/erc20.ts b/packages/web3-eth-contract/test/fixtures/erc20.ts index cf4b595ec8d..18b51748a4e 100644 --- a/packages/web3-eth-contract/test/fixtures/erc20.ts +++ b/packages/web3-eth-contract/test/fixtures/erc20.ts @@ -16,7 +16,7 @@ along with web3.js. If not, see . */ import { Address, Numbers } from 'web3-types'; -import { LogsSubscription } from '../../src/log_subscription'; +import { ContractLogsSubscription } from '../../src/contract_log_subscription'; import { ContractEventOptions, PayableMethodObject, NonPayableMethodObject } from '../../src/types'; export interface Erc20Interface { @@ -53,9 +53,9 @@ export interface Erc20Interface { }; events: { - [key: string]: (options?: ContractEventOptions) => LogsSubscription; - Approval: (options?: ContractEventOptions) => LogsSubscription; - Transfer: (options?: ContractEventOptions) => LogsSubscription; + [key: string]: (options?: ContractEventOptions) => ContractLogsSubscription; + Approval: (options?: ContractEventOptions) => ContractLogsSubscription; + Transfer: (options?: ContractEventOptions) => ContractLogsSubscription; }; } diff --git a/packages/web3-eth-contract/test/fixtures/erc721.ts b/packages/web3-eth-contract/test/fixtures/erc721.ts index 36c87289c24..0dab8017ea4 100644 --- a/packages/web3-eth-contract/test/fixtures/erc721.ts +++ b/packages/web3-eth-contract/test/fixtures/erc721.ts @@ -16,7 +16,7 @@ along with web3.js. If not, see . */ import { Address, Numbers } from 'web3-types'; -import { LogsSubscription } from '../../src/log_subscription'; +import { ContractLogsSubscription } from '../../src/contract_log_subscription'; import { ContractEventOptions, NonPayableMethodObject, PayableMethodObject } from '../../src/types'; export interface Erc721Interface { @@ -55,10 +55,10 @@ export interface Erc721Interface { }; events: { - [key: string]: (options?: ContractEventOptions) => LogsSubscription; - Transfer: (options?: ContractEventOptions) => LogsSubscription; - Approval: (options?: ContractEventOptions) => LogsSubscription; - ApprovalForAll: (options?: ContractEventOptions) => LogsSubscription; + [key: string]: (options?: ContractEventOptions) => ContractLogsSubscription; + Transfer: (options?: ContractEventOptions) => ContractLogsSubscription; + Approval: (options?: ContractEventOptions) => ContractLogsSubscription; + ApprovalForAll: (options?: ContractEventOptions) => ContractLogsSubscription; }; } diff --git a/packages/web3-eth-contract/test/unit/log_subscription.test.ts b/packages/web3-eth-contract/test/unit/log_subscription.test.ts index ccc72acaac6..520157a3cab 100644 --- a/packages/web3-eth-contract/test/unit/log_subscription.test.ts +++ b/packages/web3-eth-contract/test/unit/log_subscription.test.ts @@ -17,7 +17,8 @@ along with web3.js. If not, see . import * as eth from 'web3-eth'; import { WebSocketProvider } from 'web3-providers-ws'; -import { Contract } from '../../src'; +import { Web3SubscriptionManager } from 'web3-core'; +import { Contract, ContractLogsSubscription } from '../../src'; import { GreeterAbi, GreeterBytecode } from '../shared_fixtures/build/Greeter'; jest.mock('web3-eth'); @@ -76,4 +77,39 @@ describe('contract log subscription', () => { ], }); }); + + it('should be able to subscribe to logs with contractInstance.subscriptionManager.subscribe', async () => { + const address = '0x407D73d8a49eeb85D32Cf465507dd71d507100c1'; + const contractInstance = new Contract(GreeterAbi, address); + + jest.spyOn(WebSocketProvider.prototype, 'request').mockImplementation( + async (payload: any) => { + return { + jsonrpc: '2.0', + id: payload.id, + result: {}, + }; + }, + ); + + jest.spyOn(Web3SubscriptionManager.prototype, 'subscribe').mockImplementation( + async (name: string | number | symbol, args?: any) => { + expect(name).toBe('logs'); + expect(args.address).toBe(address); + + return new ContractLogsSubscription(args, { + subscriptionManager: contractInstance.subscriptionManager, + }); + }, + ); + + contract.setProvider(providerString); + + const sub = contractInstance.subscriptionManager.subscribe('logs'); + expect(await sub).toBeInstanceOf(ContractLogsSubscription); + + contractInstance.subscriptionManager.clear(); + + contractInstance.provider?.disconnect(); + }); });